mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-28 10:51:44 +01:00 
			
		
		
		
	Add negative tests
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
		
							parent
							
								
									f610be632e
								
							
						
					
					
						commit
						e28d308796
					
				| @ -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") | ||||
| } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user