mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-11-04 10:01:05 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			442 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			442 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package integration
 | 
						|
 | 
						|
import (
 | 
						|
	"fmt"
 | 
						|
	"strings"
 | 
						|
	"testing"
 | 
						|
 | 
						|
	"github.com/juanfont/headscale"
 | 
						|
	"github.com/juanfont/headscale/integration/hsic"
 | 
						|
	"github.com/juanfont/headscale/integration/tsic"
 | 
						|
	"github.com/stretchr/testify/assert"
 | 
						|
)
 | 
						|
 | 
						|
const numberOfTestClients = 2
 | 
						|
 | 
						|
func aclScenario(t *testing.T, policy headscale.ACLPolicy) *Scenario {
 | 
						|
	t.Helper()
 | 
						|
	scenario, err := NewScenario()
 | 
						|
	assert.NoError(t, err)
 | 
						|
 | 
						|
	spec := map[string]int{
 | 
						|
		"user1": numberOfTestClients,
 | 
						|
		"user2": numberOfTestClients,
 | 
						|
	}
 | 
						|
 | 
						|
	err = scenario.CreateHeadscaleEnv(spec,
 | 
						|
		[]tsic.Option{
 | 
						|
			tsic.WithDockerEntrypoint([]string{
 | 
						|
				"/bin/bash",
 | 
						|
				"-c",
 | 
						|
				"/bin/sleep 3 ; update-ca-certificates ; python3 -m http.server 80 & tailscaled --tun=tsdev",
 | 
						|
			}),
 | 
						|
			tsic.WithDockerWorkdir("/"),
 | 
						|
		},
 | 
						|
		hsic.WithACLPolicy(&policy),
 | 
						|
		hsic.WithTestName("acl"),
 | 
						|
	)
 | 
						|
	assert.NoError(t, err)
 | 
						|
 | 
						|
	// allClients, err := scenario.ListTailscaleClients()
 | 
						|
	// assert.NoError(t, err)
 | 
						|
 | 
						|
	err = scenario.WaitForTailscaleSync()
 | 
						|
	assert.NoError(t, err)
 | 
						|
 | 
						|
	_, err = scenario.ListTailscaleClientsFQDNs()
 | 
						|
	assert.NoError(t, err)
 | 
						|
 | 
						|
	return scenario
 | 
						|
}
 | 
						|
 | 
						|
// This tests a different ACL mechanism, if a host _cannot_ connect
 | 
						|
// to another node at all based on ACL, it should just not be part
 | 
						|
// of the NetMap sent to the host. This is slightly different than
 | 
						|
// the other tests as we can just check if the hosts are present
 | 
						|
// or not.
 | 
						|
func TestACLHostsInNetMapTable(t *testing.T) {
 | 
						|
	IntegrationSkip(t)
 | 
						|
 | 
						|
	// NOTE: All want cases currently checks the
 | 
						|
	// total count of expected peers, this would
 | 
						|
	// typically be the client count of the users
 | 
						|
	// they can access minus one (them self).
 | 
						|
	tests := map[string]struct {
 | 
						|
		users  map[string]int
 | 
						|
		policy headscale.ACLPolicy
 | 
						|
		want   map[string]int
 | 
						|
	}{
 | 
						|
		// Test that when we have no ACL, each client netmap has
 | 
						|
		// the amount of peers of the total amount of clients
 | 
						|
		"base-acls": {
 | 
						|
			users: map[string]int{
 | 
						|
				"user1": 2,
 | 
						|
				"user2": 2,
 | 
						|
			},
 | 
						|
			policy: headscale.ACLPolicy{
 | 
						|
				ACLs: []headscale.ACL{
 | 
						|
					{
 | 
						|
						Action:       "accept",
 | 
						|
						Sources:      []string{"*"},
 | 
						|
						Destinations: []string{"*:*"},
 | 
						|
					},
 | 
						|
				},
 | 
						|
			}, want: map[string]int{
 | 
						|
				"user1": 3, // ns1 + ns2
 | 
						|
				"user2": 3, // ns2 + ns1
 | 
						|
			},
 | 
						|
		},
 | 
						|
		// Test that when we have two users, which cannot see
 | 
						|
		// eachother, each node has only the number of pairs from
 | 
						|
		// their own user.
 | 
						|
		"two-isolated-users": {
 | 
						|
			users: map[string]int{
 | 
						|
				"user1": 2,
 | 
						|
				"user2": 2,
 | 
						|
			},
 | 
						|
			policy: headscale.ACLPolicy{
 | 
						|
				ACLs: []headscale.ACL{
 | 
						|
					{
 | 
						|
						Action:       "accept",
 | 
						|
						Sources:      []string{"user1"},
 | 
						|
						Destinations: []string{"user1:*"},
 | 
						|
					},
 | 
						|
					{
 | 
						|
						Action:       "accept",
 | 
						|
						Sources:      []string{"user2"},
 | 
						|
						Destinations: []string{"user2:*"},
 | 
						|
					},
 | 
						|
				},
 | 
						|
			}, want: map[string]int{
 | 
						|
				"user1": 1,
 | 
						|
				"user2": 1,
 | 
						|
			},
 | 
						|
		},
 | 
						|
		// Test that when we have two users, with ACLs and they
 | 
						|
		// are restricted to a single port, nodes are still present
 | 
						|
		// in the netmap.
 | 
						|
		"two-restricted-present-in-netmap": {
 | 
						|
			users: map[string]int{
 | 
						|
				"user1": 2,
 | 
						|
				"user2": 2,
 | 
						|
			},
 | 
						|
			policy: headscale.ACLPolicy{
 | 
						|
				ACLs: []headscale.ACL{
 | 
						|
					{
 | 
						|
						Action:       "accept",
 | 
						|
						Sources:      []string{"user1"},
 | 
						|
						Destinations: []string{"user1:22"},
 | 
						|
					},
 | 
						|
					{
 | 
						|
						Action:       "accept",
 | 
						|
						Sources:      []string{"user2"},
 | 
						|
						Destinations: []string{"user2:22"},
 | 
						|
					},
 | 
						|
					{
 | 
						|
						Action:       "accept",
 | 
						|
						Sources:      []string{"user1"},
 | 
						|
						Destinations: []string{"user2:22"},
 | 
						|
					},
 | 
						|
					{
 | 
						|
						Action:       "accept",
 | 
						|
						Sources:      []string{"user2"},
 | 
						|
						Destinations: []string{"user1:22"},
 | 
						|
					},
 | 
						|
				},
 | 
						|
			}, want: map[string]int{
 | 
						|
				"user1": 3,
 | 
						|
				"user2": 3,
 | 
						|
			},
 | 
						|
		},
 | 
						|
		// Test that when we have two users, that are isolated,
 | 
						|
		// but one can see the others, we have the appropriate number
 | 
						|
		// of peers. This will still result in all the peers as we
 | 
						|
		// need them present on the other side for the "return path".
 | 
						|
		"two-ns-one-isolated": {
 | 
						|
			users: map[string]int{
 | 
						|
				"user1": 2,
 | 
						|
				"user2": 2,
 | 
						|
			},
 | 
						|
			policy: headscale.ACLPolicy{
 | 
						|
				ACLs: []headscale.ACL{
 | 
						|
					{
 | 
						|
						Action:       "accept",
 | 
						|
						Sources:      []string{"user1"},
 | 
						|
						Destinations: []string{"user1:*"},
 | 
						|
					},
 | 
						|
					{
 | 
						|
						Action:       "accept",
 | 
						|
						Sources:      []string{"user2"},
 | 
						|
						Destinations: []string{"user2:*"},
 | 
						|
					},
 | 
						|
					{
 | 
						|
						Action:       "accept",
 | 
						|
						Sources:      []string{"user1"},
 | 
						|
						Destinations: []string{"user2:*"},
 | 
						|
					},
 | 
						|
				},
 | 
						|
			}, want: map[string]int{
 | 
						|
				"user1": 3, // ns1 + ns2
 | 
						|
				"user2": 3, // ns1 + ns2 (return path)
 | 
						|
			},
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for name, testCase := range tests {
 | 
						|
		t.Run(name, func(t *testing.T) {
 | 
						|
			scenario, err := NewScenario()
 | 
						|
			assert.NoError(t, err)
 | 
						|
 | 
						|
			spec := testCase.users
 | 
						|
 | 
						|
			err = scenario.CreateHeadscaleEnv(spec,
 | 
						|
				[]tsic.Option{},
 | 
						|
				hsic.WithACLPolicy(&testCase.policy),
 | 
						|
				// hsic.WithTestName(fmt.Sprintf("aclinnetmap%s", name)),
 | 
						|
			)
 | 
						|
			assert.NoError(t, err)
 | 
						|
 | 
						|
			allClients, err := scenario.ListTailscaleClients()
 | 
						|
			assert.NoError(t, err)
 | 
						|
 | 
						|
			err = scenario.WaitForTailscaleSync()
 | 
						|
			assert.NoError(t, err)
 | 
						|
 | 
						|
			// allHostnames, err := scenario.ListTailscaleClientsFQDNs()
 | 
						|
			// assert.NoError(t, err)
 | 
						|
 | 
						|
			for _, client := range allClients {
 | 
						|
				status, err := client.Status()
 | 
						|
				assert.NoError(t, err)
 | 
						|
 | 
						|
				user := status.User[status.Self.UserID].LoginName
 | 
						|
 | 
						|
				assert.Equal(t, (testCase.want[user]), len(status.Peer))
 | 
						|
			}
 | 
						|
 | 
						|
			err = scenario.Shutdown()
 | 
						|
			assert.NoError(t, err)
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// Test to confirm that we can use user:80 from one user
 | 
						|
// This should make the node appear in the peer list, but
 | 
						|
// disallow ping.
 | 
						|
// This ACL will not allow user1 access its own machines.
 | 
						|
// Reported: https://github.com/juanfont/headscale/issues/699
 | 
						|
func TestACLAllowUser80Dst(t *testing.T) {
 | 
						|
	IntegrationSkip(t)
 | 
						|
 | 
						|
	scenario := aclScenario(t,
 | 
						|
		headscale.ACLPolicy{
 | 
						|
			ACLs: []headscale.ACL{
 | 
						|
				{
 | 
						|
					Action:       "accept",
 | 
						|
					Sources:      []string{"user1"},
 | 
						|
					Destinations: []string{"user2:80"},
 | 
						|
				},
 | 
						|
			},
 | 
						|
		},
 | 
						|
	)
 | 
						|
 | 
						|
	user1Clients, err := scenario.ListTailscaleClients("user1")
 | 
						|
	assert.NoError(t, err)
 | 
						|
 | 
						|
	user2Clients, err := scenario.ListTailscaleClients("user2")
 | 
						|
	assert.NoError(t, err)
 | 
						|
 | 
						|
	// Test that user1 can visit all user2
 | 
						|
	for _, client := range user1Clients {
 | 
						|
		for _, peer := range user2Clients {
 | 
						|
			fqdn, err := peer.FQDN()
 | 
						|
			assert.NoError(t, err)
 | 
						|
 | 
						|
			url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
 | 
						|
			t.Logf("url from %s to %s", client.Hostname(), url)
 | 
						|
 | 
						|
			result, err := client.Curl(url)
 | 
						|
			assert.Len(t, result, 13)
 | 
						|
			assert.NoError(t, err)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Test that user2 _cannot_ visit user1
 | 
						|
	for _, client := range user2Clients {
 | 
						|
		for _, peer := range user1Clients {
 | 
						|
			fqdn, err := peer.FQDN()
 | 
						|
			assert.NoError(t, err)
 | 
						|
 | 
						|
			url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
 | 
						|
			t.Logf("url from %s to %s", client.Hostname(), url)
 | 
						|
 | 
						|
			result, err := client.Curl(url)
 | 
						|
			assert.Empty(t, result)
 | 
						|
			assert.Error(t, err)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	err = scenario.Shutdown()
 | 
						|
	assert.NoError(t, err)
 | 
						|
}
 | 
						|
 | 
						|
func TestACLDenyAllPort80(t *testing.T) {
 | 
						|
	IntegrationSkip(t)
 | 
						|
 | 
						|
	scenario := aclScenario(t,
 | 
						|
		headscale.ACLPolicy{
 | 
						|
			Groups: map[string][]string{
 | 
						|
				"group:integration-acl-test": {"user1", "user2"},
 | 
						|
			},
 | 
						|
			ACLs: []headscale.ACL{
 | 
						|
				{
 | 
						|
					Action:       "accept",
 | 
						|
					Sources:      []string{"group:integration-acl-test"},
 | 
						|
					Destinations: []string{"*:22"},
 | 
						|
				},
 | 
						|
			},
 | 
						|
		},
 | 
						|
	)
 | 
						|
 | 
						|
	allClients, err := scenario.ListTailscaleClients()
 | 
						|
	assert.NoError(t, err)
 | 
						|
 | 
						|
	allHostnames, err := scenario.ListTailscaleClientsFQDNs()
 | 
						|
	assert.NoError(t, err)
 | 
						|
 | 
						|
	for _, client := range allClients {
 | 
						|
		for _, hostname := range allHostnames {
 | 
						|
			// We will always be allowed to check _self_ so shortcircuit
 | 
						|
			// the test here.
 | 
						|
			if strings.Contains(hostname, client.Hostname()) {
 | 
						|
				continue
 | 
						|
			}
 | 
						|
 | 
						|
			url := fmt.Sprintf("http://%s/etc/hostname", hostname)
 | 
						|
			t.Logf("url from %s to %s", client.Hostname(), url)
 | 
						|
 | 
						|
			result, err := client.Curl(url)
 | 
						|
			assert.Empty(t, result)
 | 
						|
			assert.Error(t, err)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	err = scenario.Shutdown()
 | 
						|
	assert.NoError(t, err)
 | 
						|
}
 | 
						|
 | 
						|
// Test to confirm that we can use user:* from one user.
 | 
						|
// This ACL will not allow user1 access its own machines.
 | 
						|
// Reported: https://github.com/juanfont/headscale/issues/699
 | 
						|
func TestACLAllowUserDst(t *testing.T) {
 | 
						|
	IntegrationSkip(t)
 | 
						|
 | 
						|
	scenario := aclScenario(t,
 | 
						|
		headscale.ACLPolicy{
 | 
						|
			ACLs: []headscale.ACL{
 | 
						|
				{
 | 
						|
					Action:       "accept",
 | 
						|
					Sources:      []string{"user1"},
 | 
						|
					Destinations: []string{"user2:*"},
 | 
						|
				},
 | 
						|
			},
 | 
						|
		},
 | 
						|
	)
 | 
						|
 | 
						|
	user1Clients, err := scenario.ListTailscaleClients("user1")
 | 
						|
	assert.NoError(t, err)
 | 
						|
 | 
						|
	user2Clients, err := scenario.ListTailscaleClients("user2")
 | 
						|
	assert.NoError(t, err)
 | 
						|
 | 
						|
	// Test that user1 can visit all user2
 | 
						|
	for _, client := range user1Clients {
 | 
						|
		for _, peer := range user2Clients {
 | 
						|
			fqdn, err := peer.FQDN()
 | 
						|
			assert.NoError(t, err)
 | 
						|
 | 
						|
			url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
 | 
						|
			t.Logf("url from %s to %s", client.Hostname(), url)
 | 
						|
 | 
						|
			result, err := client.Curl(url)
 | 
						|
			assert.Len(t, result, 13)
 | 
						|
			assert.NoError(t, err)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Test that user2 _cannot_ visit user1
 | 
						|
	for _, client := range user2Clients {
 | 
						|
		for _, peer := range user1Clients {
 | 
						|
			fqdn, err := peer.FQDN()
 | 
						|
			assert.NoError(t, err)
 | 
						|
 | 
						|
			url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
 | 
						|
			t.Logf("url from %s to %s", client.Hostname(), url)
 | 
						|
 | 
						|
			result, err := client.Curl(url)
 | 
						|
			assert.Empty(t, result)
 | 
						|
			assert.Error(t, err)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	err = scenario.Shutdown()
 | 
						|
	assert.NoError(t, err)
 | 
						|
}
 | 
						|
 | 
						|
// Test to confirm that we can use *:* from one user
 | 
						|
// Reported: https://github.com/juanfont/headscale/issues/699
 | 
						|
func TestACLAllowStarDst(t *testing.T) {
 | 
						|
	IntegrationSkip(t)
 | 
						|
 | 
						|
	scenario := aclScenario(t,
 | 
						|
		headscale.ACLPolicy{
 | 
						|
			ACLs: []headscale.ACL{
 | 
						|
				{
 | 
						|
					Action:       "accept",
 | 
						|
					Sources:      []string{"user1"},
 | 
						|
					Destinations: []string{"*:*"},
 | 
						|
				},
 | 
						|
			},
 | 
						|
		},
 | 
						|
	)
 | 
						|
 | 
						|
	user1Clients, err := scenario.ListTailscaleClients("user1")
 | 
						|
	assert.NoError(t, err)
 | 
						|
 | 
						|
	user2Clients, err := scenario.ListTailscaleClients("user2")
 | 
						|
	assert.NoError(t, err)
 | 
						|
 | 
						|
	// Test that user1 can visit all user2
 | 
						|
	for _, client := range user1Clients {
 | 
						|
		for _, peer := range user2Clients {
 | 
						|
			fqdn, err := peer.FQDN()
 | 
						|
			assert.NoError(t, err)
 | 
						|
 | 
						|
			url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
 | 
						|
			t.Logf("url from %s to %s", client.Hostname(), url)
 | 
						|
 | 
						|
			result, err := client.Curl(url)
 | 
						|
			assert.Len(t, result, 13)
 | 
						|
			assert.NoError(t, err)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Test that user2 _cannot_ visit user1
 | 
						|
	for _, client := range user2Clients {
 | 
						|
		for _, peer := range user1Clients {
 | 
						|
			fqdn, err := peer.FQDN()
 | 
						|
			assert.NoError(t, err)
 | 
						|
 | 
						|
			url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
 | 
						|
			t.Logf("url from %s to %s", client.Hostname(), url)
 | 
						|
 | 
						|
			result, err := client.Curl(url)
 | 
						|
			assert.Empty(t, result)
 | 
						|
			assert.Error(t, err)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	err = scenario.Shutdown()
 | 
						|
	assert.NoError(t, err)
 | 
						|
}
 |