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 ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"strings" |  | ||||||
| 	"testing" | 	"testing" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/juanfont/headscale" | 	"github.com/juanfont/headscale" | ||||||
| 	"github.com/juanfont/headscale/integration/hsic" | 	"github.com/juanfont/headscale/integration/hsic" | ||||||
| 	"github.com/juanfont/headscale/integration/tsic" | 	"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 | 	var err error | ||||||
|  | 
 | ||||||
| 	for attempts := 0; attempts < times; attempts++ { | 	for attempts := 0; attempts < times; attempts++ { | ||||||
| 		var result string | 		tempResult, tempStderr, err := doWork() | ||||||
| 		result, err = doWork() | 
 | ||||||
|  | 		result += tempResult | ||||||
|  | 		stderr += tempStderr | ||||||
|  | 
 | ||||||
| 		if err == nil { | 		if err == nil { | ||||||
| 			return result, nil | 			return result, stderr, nil | ||||||
| 		} | 		} | ||||||
| 		time.Sleep(sleepInterval) | 		time.Sleep(sleepInterval) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return "", err | 	return result, stderr, err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestSSHOneNamespaceAllToAll(t *testing.T) { | func TestSSHOneNamespaceAllToAll(t *testing.T) { | ||||||
| @ -81,26 +89,16 @@ func TestSSHOneNamespaceAllToAll(t *testing.T) { | |||||||
| 		t.Errorf("failed to get FQDNs: %s", err) | 		t.Errorf("failed to get FQDNs: %s", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	success := 0 |  | ||||||
| 
 |  | ||||||
| 	for _, client := range allClients { | 	for _, client := range allClients { | ||||||
| 		for _, peer := range allClients { | 		for _, peer := range allClients { | ||||||
| 			if client.Hostname() == peer.Hostname() { | 			if client.Hostname() == peer.Hostname() { | ||||||
| 				continue | 				continue | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			if doSSH(t, client, peer) { | 			assertSSHHostname(t, client, peer) | ||||||
| 				success++ |  | ||||||
| 			} |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	t.Logf( |  | ||||||
| 		"%d successful pings out of %d", |  | ||||||
| 		success, |  | ||||||
| 		(len(allClients)*len(allClients))-len(allClients), |  | ||||||
| 	) |  | ||||||
| 
 |  | ||||||
| 	err = scenario.Shutdown() | 	err = scenario.Shutdown() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Errorf("failed to tear down scenario: %s", err) | 		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) | 		t.Errorf("failed to get FQDNs: %s", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	success := 0 |  | ||||||
| 
 |  | ||||||
| 	testInterNamespaceSSH := func(sourceClients []TailscaleClient, targetClients []TailscaleClient) { | 	testInterNamespaceSSH := func(sourceClients []TailscaleClient, targetClients []TailscaleClient) { | ||||||
| 		for _, client := range sourceClients { | 		for _, client := range sourceClients { | ||||||
| 			for _, peer := range targetClients { | 			for _, peer := range targetClients { | ||||||
| 				if doSSH(t, client, peer) { | 				assertSSHHostname(t, client, peer) | ||||||
| 					success++ |  | ||||||
| 				} |  | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| @ -184,11 +178,71 @@ func TestSSHMultipleNamespacesAllToAll(t *testing.T) { | |||||||
| 	testInterNamespaceSSH(nsOneClients, nsTwoClients) | 	testInterNamespaceSSH(nsOneClients, nsTwoClients) | ||||||
| 	testInterNamespaceSSH(nsTwoClients, nsOneClients) | 	testInterNamespaceSSH(nsTwoClients, nsOneClients) | ||||||
| 
 | 
 | ||||||
| 	t.Logf( | 	err = scenario.Shutdown() | ||||||
| 		"%d successful pings out of %d", | 	if err != nil { | ||||||
| 		success, | 		t.Errorf("failed to tear down scenario: %s", err) | ||||||
| 		((len(nsOneClients)*len(nsOneClients))-len(nsOneClients))*2, | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 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() | 	err = scenario.Shutdown() | ||||||
| 	if err != nil { | 	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() | 	t.Helper() | ||||||
| 
 | 
 | ||||||
| 	clientFQDN, _ := client.FQDN() |  | ||||||
| 	peerFQDN, _ := peer.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( | 	return retry(10, 1*time.Second, func() (string, string, error) { | ||||||
| 		fmt.Sprintf("%s-%s", clientFQDN, peerFQDN), | 		return client.Execute(command) | ||||||
| 		func(t *testing.T) { | 	}) | ||||||
| 			command := []string{ | } | ||||||
| 				"ssh", "-o StrictHostKeyChecking=no", "-o ConnectTimeout=1", | 
 | ||||||
| 				fmt.Sprintf("%s@%s", "ssh-it-user", peerFQDN), | func assertSSHHostname(t *testing.T, client TailscaleClient, peer TailscaleClient) { | ||||||
| 				"'hostname'", | 	t.Helper() | ||||||
| 			} | 
 | ||||||
| 
 | 	result, _, err := doSSH(t, client, peer) | ||||||
| 			result, err := retry(10, 1*time.Second, func() (string, error) { | 	assert.NoError(t, err) | ||||||
| 				result, _, err := client.Execute(command) | 
 | ||||||
| 
 | 	assert.Contains(t, peer.ID(), result) | ||||||
| 				return result, err | } | ||||||
| 			}) | 
 | ||||||
| 			if err != nil { | func assertSSHPermissionDenied(t *testing.T, client TailscaleClient, peer TailscaleClient) { | ||||||
| 				t.Errorf("failed to execute command over SSH: %s", err) | 	t.Helper() | ||||||
| 			} | 
 | ||||||
| 
 | 	result, stderr, err := doSSH(t, client, peer) | ||||||
| 			if strings.Contains(peer.ID(), result) { | 	assert.Error(t, err) | ||||||
| 				t.Logf( | 
 | ||||||
| 					"failed to get correct container ID from %s, expected: %s, got: %s", | 	assert.Empty(t, result) | ||||||
| 					peer.Hostname(), | 
 | ||||||
| 					peer.ID(), | 	assert.Contains(t, stderr, "Permission denied (tailscale)") | ||||||
| 					result, | } | ||||||
| 				) | 
 | ||||||
| 				t.Fail() | func assertSSHTimeout(t *testing.T, client TailscaleClient, peer TailscaleClient) { | ||||||
| 			} else { | 	t.Helper() | ||||||
| 				success = true | 
 | ||||||
| 			} | 	result, stderr, err := doSSH(t, client, peer) | ||||||
| 		}, | 	assert.NoError(t, err) | ||||||
| 	) | 
 | ||||||
| 
 | 	assert.Empty(t, result) | ||||||
| 	return success | 
 | ||||||
|  | 	assert.Contains(t, stderr, "Connection timed out") | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user