|
|
|
|
@@ -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
|
|
|
|
|
@@ -634,7 +645,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 {
|
|
|
|
|
@@ -677,13 +695,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)
|
|
|
|
|
@@ -723,6 +745,119 @@ 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`},
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("outgoing-tcp-ipv4", func(t *testing.T) {
|
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
|
s := &http.Server{
|
|
|
|
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
cancel()
|
|
|
|
|
fmt.Fprintln(w, "connection established")
|
|
|
|
|
}),
|
|
|
|
|
}
|
|
|
|
|
ln, err := net.Listen("tcp", net.JoinHostPort("::", "0"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("can't make HTTP server: %v", err)
|
|
|
|
|
}
|
|
|
|
|
_, port, _ := net.SplitHostPort(ln.Addr().String())
|
|
|
|
|
go s.Serve(ln)
|
|
|
|
|
|
|
|
|
|
runTestCommands(t, timeout, cli, []expect.Batcher{
|
|
|
|
|
&expect.BSnd{S: fmt.Sprintf("curl http://%s:%s\n", "100.64.0.1", port)},
|
|
|
|
|
&expect.BExp{R: `connection established`},
|
|
|
|
|
})
|
|
|
|
|
<-ctx.Done()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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) {
|
|
|
|
|
e, _, err := expect.SpawnSSH(cli, timeout,
|
|
|
|
|
expect.Verbose(true),
|
|
|
|
|
expect.VerboseWriter(logger.FuncWriter(t.Logf)),
|
|
|
|
|
@@ -732,42 +867,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()
|
|
|
|
|
@@ -896,19 +999,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)
|
|
|
|
|
}
|
|
|
|
|
@@ -944,6 +1065,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 {
|
|
|
|
|
|