diff --git a/Dockerfile.tailscale b/Dockerfile.tailscale index 145ab6f7..69f6f656 100644 --- a/Dockerfile.tailscale +++ b/Dockerfile.tailscale @@ -4,13 +4,17 @@ ARG TAILSCALE_VERSION=* ARG TAILSCALE_CHANNEL=stable RUN apt-get update \ - && apt-get install -y gnupg curl \ + && apt-get install -y gnupg curl ssh \ && curl -fsSL https://pkgs.tailscale.com/${TAILSCALE_CHANNEL}/ubuntu/focal.gpg | apt-key add - \ && curl -fsSL https://pkgs.tailscale.com/${TAILSCALE_CHANNEL}/ubuntu/focal.list | tee /etc/apt/sources.list.d/tailscale.list \ && apt-get update \ && apt-get install -y ca-certificates tailscale=${TAILSCALE_VERSION} dnsutils \ && rm -rf /var/lib/apt/lists/* +RUN adduser --shell=/bin/bash ssh-it-user + +RUN service ssh start + ADD integration_test/etc_embedded_derp/tls/server.crt /usr/local/share/ca-certificates/ RUN chmod 644 /usr/local/share/ca-certificates/server.crt diff --git a/Dockerfile.tailscale-HEAD b/Dockerfile.tailscale-HEAD index c6e894da..4b0eead9 100644 --- a/Dockerfile.tailscale-HEAD +++ b/Dockerfile.tailscale-HEAD @@ -1,9 +1,12 @@ FROM golang:latest RUN apt-get update \ - && apt-get install -y ca-certificates dnsutils git iptables \ + && apt-get install -y ca-certificates dnsutils git iptables ssh \ && rm -rf /var/lib/apt/lists/* +RUN useradd --shell=/bin/bash --create-home ssh-it-user + +RUN service ssh start RUN git clone https://github.com/tailscale/tailscale.git diff --git a/integration/ssh_test.go b/integration/ssh_test.go new file mode 100644 index 00000000..22ad228f --- /dev/null +++ b/integration/ssh_test.go @@ -0,0 +1,115 @@ +package integration + +import ( + "fmt" + "testing" + + "github.com/juanfont/headscale" +) + +func TestSSHIntoAll(t *testing.T) { + IntegrationSkip(t) + + scenario, err := NewScenario() + if err != nil { + t.Errorf("failed to create scenario: %s", err) + } + + spec := &HeadscaleSpec{ + namespaces: map[string]int{ + // Omit versions before 1.24 because they don't support SSH + "namespace1": len(TailscaleVersions) - 4, + "namespace2": len(TailscaleVersions) - 4, + }, + enableSSH: true, + acl: &headscale.ACLPolicy{ + Groups: map[string][]string{ + "group:integration-test": {"namespace1", "namespace2"}, + }, + ACLs: []headscale.ACL{ + { + Action: "accept", + Sources: []string{"*"}, + Destinations: []string{"*:*"}, + }, + }, + SSHs: []headscale.SSH{ + { + Action: "accept", + Sources: []string{"group:integration-test"}, + Destinations: []string{"group:integration-test"}, + Users: []string{"ssh-it-user"}, + }, + }, + }, + } + + err = scenario.CreateHeadscaleEnv(spec) + if err != nil { + t.Errorf("failed to create headscale environment: %s", err) + } + err = scenario.WaitForTailscaleSync() + if err != nil { + t.Errorf("failed wait for tailscale clients to be in sync: %s", err) + } + + for namespace := range spec.namespaces { + // This will essentially fetch and cache all the FQDNs for the given namespace + nsFQDNs, err := scenario.ListTailscaleClientsFQDNs(namespace) + if err != nil { + t.Errorf("failed to get FQDNs: %s", err) + } + + nsClients, err := scenario.ListTailscaleClients(namespace) + if err != nil { + t.Errorf("failed to get clients: %s", err) + } + + for _, client := range nsClients { + currentClientFqdn, _ := client.FQDN() + sshTargets := removeFromSlice(nsFQDNs, currentClientFqdn) + + for _, target := range sshTargets { + t.Run( + fmt.Sprintf("%s-%s", currentClientFqdn, target), + func(t *testing.T) { + command := []string{ + "ssh", "-o StrictHostKeyChecking=no", + fmt.Sprintf("%s@%s", "ssh-it-user", target), + "'hostname'", + } + + result, err := client.Execute(command) + if err != nil { + t.Errorf("failed to execute command over SSH: %s", err) + } + + if result != target { + t.Logf("result=%s, target=%s", result, target) + t.Fail() + } + + t.Logf("Result for %s: %s\n", target, result) + }, + ) + } + + // t.Logf("%s wants to SSH into %+v", currentClientFqdn, sshTargets) + } + } + + err = scenario.Shutdown() + if err != nil { + t.Errorf("failed to tear down scenario: %s", err) + } +} + +func removeFromSlice(haystack []string, needle string) []string { + for i, value := range haystack { + if needle == value { + return append(haystack[:i], haystack[i+1:]...) + } + } + + return haystack +}