From e28d308796ec67f27c28549acbad56ce75516e10 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sat, 19 Nov 2022 12:41:12 +0100 Subject: [PATCH] Add negative tests Signed-off-by: Kristoffer Dalby --- integration/ssh_test.go | 375 +++++++++++++++++++++++++++++++++------- 1 file changed, 313 insertions(+), 62 deletions(-) diff --git a/integration/ssh_test.go b/integration/ssh_test.go index 0fbebdca..6520e994 100644 --- a/integration/ssh_test.go +++ b/integration/ssh_test.go @@ -2,27 +2,35 @@ package integration import ( "fmt" - "strings" "testing" "time" "github.com/juanfont/headscale" "github.com/juanfont/headscale/integration/hsic" "github.com/juanfont/headscale/integration/tsic" + "github.com/stretchr/testify/assert" ) -var retry = func(times int, sleepInterval time.Duration, doWork func() (string, error)) (string, error) { +var retry = func(times int, sleepInterval time.Duration, + doWork func() (string, string, error), +) (string, string, error) { + var result string + var stderr string var err error + for attempts := 0; attempts < times; attempts++ { - var result string - result, err = doWork() + tempResult, tempStderr, err := doWork() + + result += tempResult + stderr += tempStderr + if err == nil { - return result, nil + return result, stderr, nil } time.Sleep(sleepInterval) } - return "", err + return result, stderr, err } func TestSSHOneNamespaceAllToAll(t *testing.T) { @@ -81,26 +89,16 @@ func TestSSHOneNamespaceAllToAll(t *testing.T) { t.Errorf("failed to get FQDNs: %s", err) } - success := 0 - for _, client := range allClients { for _, peer := range allClients { if client.Hostname() == peer.Hostname() { continue } - if doSSH(t, client, peer) { - success++ - } + assertSSHHostname(t, client, peer) } } - t.Logf( - "%d successful pings out of %d", - success, - (len(allClients)*len(allClients))-len(allClients), - ) - err = scenario.Shutdown() if err != nil { t.Errorf("failed to tear down scenario: %s", err) @@ -169,14 +167,10 @@ func TestSSHMultipleNamespacesAllToAll(t *testing.T) { t.Errorf("failed to get FQDNs: %s", err) } - success := 0 - testInterNamespaceSSH := func(sourceClients []TailscaleClient, targetClients []TailscaleClient) { for _, client := range sourceClients { for _, peer := range targetClients { - if doSSH(t, client, peer) { - success++ - } + assertSSHHostname(t, client, peer) } } } @@ -184,11 +178,71 @@ func TestSSHMultipleNamespacesAllToAll(t *testing.T) { testInterNamespaceSSH(nsOneClients, nsTwoClients) testInterNamespaceSSH(nsTwoClients, nsOneClients) - t.Logf( - "%d successful pings out of %d", - success, - ((len(nsOneClients)*len(nsOneClients))-len(nsOneClients))*2, + err = scenario.Shutdown() + if err != nil { + t.Errorf("failed to tear down scenario: %s", err) + } +} + +func TestSSHNoSSHConfigured(t *testing.T) { + IntegrationSkip(t) + + scenario, err := NewScenario() + if err != nil { + t.Errorf("failed to create scenario: %s", err) + } + + spec := map[string]int{ + "namespace1": len(TailscaleVersions) - 5, + } + + err = scenario.CreateHeadscaleEnv(spec, + []tsic.Option{tsic.WithSSH()}, + hsic.WithACLPolicy( + &headscale.ACLPolicy{ + Groups: map[string][]string{ + "group:integration-test": {"namespace1"}, + }, + ACLs: []headscale.ACL{ + { + Action: "accept", + Sources: []string{"*"}, + Destinations: []string{"*:*"}, + }, + }, + SSHs: []headscale.SSH{}, + }, + ), + hsic.WithTestName("sshnoneconfigured"), ) + if err != nil { + t.Errorf("failed to create headscale environment: %s", err) + } + + allClients, err := scenario.ListTailscaleClients() + if err != nil { + t.Errorf("failed to get clients: %s", err) + } + + err = scenario.WaitForTailscaleSync() + if err != nil { + t.Errorf("failed wait for tailscale clients to be in sync: %s", err) + } + + _, err = scenario.ListTailscaleClientsFQDNs() + if err != nil { + t.Errorf("failed to get FQDNs: %s", err) + } + + for _, client := range allClients { + for _, peer := range allClients { + if client.Hostname() == peer.Hostname() { + continue + } + + assertSSHPermissionDenied(t, client, peer) + } + } err = scenario.Shutdown() if err != nil { @@ -196,45 +250,242 @@ func TestSSHMultipleNamespacesAllToAll(t *testing.T) { } } -func doSSH(t *testing.T, client TailscaleClient, peer TailscaleClient) bool { +func TestSSHIsBlockedInACL(t *testing.T) { + IntegrationSkip(t) + + scenario, err := NewScenario() + if err != nil { + t.Errorf("failed to create scenario: %s", err) + } + + spec := map[string]int{ + "namespace1": len(TailscaleVersions) - 5, + } + + err = scenario.CreateHeadscaleEnv(spec, + []tsic.Option{tsic.WithSSH()}, + hsic.WithACLPolicy( + &headscale.ACLPolicy{ + Groups: map[string][]string{ + "group:integration-test": {"namespace1"}, + }, + ACLs: []headscale.ACL{ + { + Action: "accept", + Sources: []string{"*"}, + Destinations: []string{"*:80"}, + }, + }, + SSHs: []headscale.SSH{ + { + Action: "accept", + Sources: []string{"group:integration-test"}, + Destinations: []string{"group:integration-test"}, + Users: []string{"ssh-it-user"}, + }, + }, + }, + ), + hsic.WithTestName("sshisblockedinacl"), + ) + if err != nil { + t.Errorf("failed to create headscale environment: %s", err) + } + + allClients, err := scenario.ListTailscaleClients() + if err != nil { + t.Errorf("failed to get clients: %s", err) + } + + err = scenario.WaitForTailscaleSync() + if err != nil { + t.Errorf("failed wait for tailscale clients to be in sync: %s", err) + } + + _, err = scenario.ListTailscaleClientsFQDNs() + if err != nil { + t.Errorf("failed to get FQDNs: %s", err) + } + + for _, client := range allClients { + for _, peer := range allClients { + if client.Hostname() == peer.Hostname() { + continue + } + + assertSSHTimeout(t, client, peer) + } + } + + err = scenario.Shutdown() + if err != nil { + t.Errorf("failed to tear down scenario: %s", err) + } +} + +func TestSSNamespaceOnlyIsolation(t *testing.T) { + IntegrationSkip(t) + + scenario, err := NewScenario() + if err != nil { + t.Errorf("failed to create scenario: %s", err) + } + + spec := map[string]int{ + "namespaceacl1": len(TailscaleVersions) - 5, + "namespaceacl2": len(TailscaleVersions) - 5, + } + + err = scenario.CreateHeadscaleEnv(spec, + []tsic.Option{tsic.WithSSH()}, + hsic.WithACLPolicy( + &headscale.ACLPolicy{ + Groups: map[string][]string{ + "group:ssh1": {"namespaceacl1"}, + "group:ssh2": {"namespaceacl2"}, + }, + ACLs: []headscale.ACL{ + { + Action: "accept", + Sources: []string{"*"}, + Destinations: []string{"*:*"}, + }, + }, + SSHs: []headscale.SSH{ + { + Action: "accept", + Sources: []string{"group:ssh1"}, + Destinations: []string{"group:ssh1"}, + Users: []string{"ssh-it-user"}, + }, + { + Action: "accept", + Sources: []string{"group:ssh2"}, + Destinations: []string{"group:ssh2"}, + Users: []string{"ssh-it-user"}, + }, + }, + }, + ), + hsic.WithTestName("sshtwonamespaceaclblock"), + ) + if err != nil { + t.Errorf("failed to create headscale environment: %s", err) + } + + ssh1Clients, err := scenario.ListTailscaleClients("namespaceacl1") + if err != nil { + t.Errorf("failed to get clients: %s", err) + } + + ssh2Clients, err := scenario.ListTailscaleClients("namespaceacl2") + if err != nil { + t.Errorf("failed to get clients: %s", err) + } + + err = scenario.WaitForTailscaleSync() + if err != nil { + t.Errorf("failed wait for tailscale clients to be in sync: %s", err) + } + + _, err = scenario.ListTailscaleClientsFQDNs() + if err != nil { + t.Errorf("failed to get FQDNs: %s", err) + } + + // TODO(kradalby,evenh): ACLs do currently not cover reject + // cases properly, and currently will accept all incomming connections + // as long as a rule is present. + // + // for _, client := range ssh1Clients { + // for _, peer := range ssh2Clients { + // if client.Hostname() == peer.Hostname() { + // continue + // } + // + // assertSSHPermissionDenied(t, client, peer) + // } + // } + // + // for _, client := range ssh2Clients { + // for _, peer := range ssh1Clients { + // if client.Hostname() == peer.Hostname() { + // continue + // } + // + // assertSSHPermissionDenied(t, client, peer) + // } + // } + + for _, client := range ssh1Clients { + for _, peer := range ssh1Clients { + if client.Hostname() == peer.Hostname() { + continue + } + + assertSSHHostname(t, client, peer) + } + } + + for _, client := range ssh2Clients { + for _, peer := range ssh2Clients { + if client.Hostname() == peer.Hostname() { + continue + } + + assertSSHHostname(t, client, peer) + } + } + + err = scenario.Shutdown() + if err != nil { + t.Errorf("failed to tear down scenario: %s", err) + } +} + +func doSSH(t *testing.T, client TailscaleClient, peer TailscaleClient) (string, string, error) { t.Helper() - clientFQDN, _ := client.FQDN() peerFQDN, _ := peer.FQDN() - success := false + command := []string{ + "ssh", "-o StrictHostKeyChecking=no", "-o ConnectTimeout=1", + fmt.Sprintf("%s@%s", "ssh-it-user", peerFQDN), + "'hostname'", + } - t.Run( - fmt.Sprintf("%s-%s", clientFQDN, peerFQDN), - func(t *testing.T) { - command := []string{ - "ssh", "-o StrictHostKeyChecking=no", "-o ConnectTimeout=1", - fmt.Sprintf("%s@%s", "ssh-it-user", peerFQDN), - "'hostname'", - } - - result, err := retry(10, 1*time.Second, func() (string, error) { - result, _, err := client.Execute(command) - - return result, err - }) - if err != nil { - t.Errorf("failed to execute command over SSH: %s", err) - } - - if strings.Contains(peer.ID(), result) { - t.Logf( - "failed to get correct container ID from %s, expected: %s, got: %s", - peer.Hostname(), - peer.ID(), - result, - ) - t.Fail() - } else { - success = true - } - }, - ) - - return success + return retry(10, 1*time.Second, func() (string, string, error) { + return client.Execute(command) + }) +} + +func assertSSHHostname(t *testing.T, client TailscaleClient, peer TailscaleClient) { + t.Helper() + + result, _, err := doSSH(t, client, peer) + assert.NoError(t, err) + + assert.Contains(t, peer.ID(), result) +} + +func assertSSHPermissionDenied(t *testing.T, client TailscaleClient, peer TailscaleClient) { + t.Helper() + + result, stderr, err := doSSH(t, client, peer) + assert.Error(t, err) + + assert.Empty(t, result) + + assert.Contains(t, stderr, "Permission denied (tailscale)") +} + +func assertSSHTimeout(t *testing.T, client TailscaleClient, peer TailscaleClient) { + t.Helper() + + result, stderr, err := doSSH(t, client, peer) + assert.NoError(t, err) + + assert.Empty(t, result) + + assert.Contains(t, stderr, "Connection timed out") }