mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-28 10:51:44 +01:00 
			
		
		
		
	Corrected existing integration tests. New integration tests added
This commit is contained in:
		
							parent
							
								
									d671462811
								
							
						
					
					
						commit
						651212c201
					
				
							
								
								
									
										281
									
								
								integration/auth_approval_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										281
									
								
								integration/auth_approval_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,281 @@ | ||||
| package integration | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	v1 "github.com/juanfont/headscale/gen/go/headscale/v1" | ||||
| 	"github.com/juanfont/headscale/integration/hsic" | ||||
| 	"github.com/samber/lo" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"io" | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| 	"net/netip" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| type AuthApprovalScenario struct { | ||||
| 	*Scenario | ||||
| } | ||||
| 
 | ||||
| func TestAuthNodeApproval(t *testing.T) { | ||||
| 	IntegrationSkip(t) | ||||
| 	t.Parallel() | ||||
| 
 | ||||
| 	baseScenario, err := NewScenario(dockertestMaxWait()) | ||||
| 	assertNoErr(t, err) | ||||
| 
 | ||||
| 	scenario := AuthApprovalScenario{ | ||||
| 		Scenario: baseScenario, | ||||
| 	} | ||||
| 	defer scenario.ShutdownAssertNoPanics(t) | ||||
| 
 | ||||
| 	spec := map[string]int{ | ||||
| 		"user1": len(MustTestVersions), | ||||
| 	} | ||||
| 
 | ||||
| 	err = scenario.CreateHeadscaleEnv( | ||||
| 		spec, | ||||
| 		hsic.WithTestName("approval"), | ||||
| 		hsic.WithManualApproveNewNode(), | ||||
| 	) | ||||
| 	assertNoErrHeadscaleEnv(t, err) | ||||
| 
 | ||||
| 	allClients, err := scenario.ListTailscaleClients() | ||||
| 	assertNoErrListClients(t, err) | ||||
| 
 | ||||
| 	err = scenario.WaitForTailscaleSyncWithPeerCount(0) | ||||
| 	assertNoErrSync(t, err) | ||||
| 
 | ||||
| 	for _, client := range allClients { | ||||
| 		status, err := client.Status() | ||||
| 		assertNoErr(t, err) | ||||
| 		assert.Equal(t, "NeedsMachineAuth", status.BackendState) | ||||
| 		assert.Len(t, status.Peers(), 0) | ||||
| 	} | ||||
| 
 | ||||
| 	headscale, err := scenario.Headscale() | ||||
| 	assertNoErr(t, err) | ||||
| 
 | ||||
| 	var allNodes []*v1.Node | ||||
| 	err = executeAndUnmarshal( | ||||
| 		headscale, | ||||
| 		[]string{ | ||||
| 			"headscale", | ||||
| 			"nodes", | ||||
| 			"list", | ||||
| 			"--output", | ||||
| 			"json", | ||||
| 		}, | ||||
| 		&allNodes, | ||||
| 	) | ||||
| 	assert.Nil(t, err) | ||||
| 
 | ||||
| 	for _, node := range allNodes { | ||||
| 		_, err = headscale.Execute([]string{ | ||||
| 			"headscale", "nodes", "approve", "--identifier", fmt.Sprintf("%d", node.Id), | ||||
| 		}) | ||||
| 		assertNoErr(t, err) | ||||
| 	} | ||||
| 
 | ||||
| 	for _, client := range allClients { | ||||
| 		err = client.Logout() | ||||
| 		if err != nil { | ||||
| 			t.Fatalf("failed to logout client %s: %s", client.Hostname(), err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	err = scenario.WaitForTailscaleLogout() | ||||
| 	assertNoErrLogout(t, err) | ||||
| 
 | ||||
| 	t.Logf("all clients logged out") | ||||
| 
 | ||||
| 	for userName := range spec { | ||||
| 		err = scenario.runTailscaleUp(userName, headscale.GetEndpoint(), true) | ||||
| 		if err != nil { | ||||
| 			t.Fatalf("failed to run tailscale up: %s", err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	t.Logf("all clients logged in again") | ||||
| 
 | ||||
| 	allClients, err = scenario.ListTailscaleClients() | ||||
| 	assertNoErrListClients(t, err) | ||||
| 
 | ||||
| 	allIps, err := scenario.ListTailscaleClientsIPs() | ||||
| 	assertNoErrListClientIPs(t, err) | ||||
| 
 | ||||
| 	err = scenario.WaitForTailscaleSync() | ||||
| 	assertNoErrSync(t, err) | ||||
| 
 | ||||
| 	//assertClientsState(t, allClients)
 | ||||
| 
 | ||||
| 	allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string { | ||||
| 		return x.String() | ||||
| 	}) | ||||
| 
 | ||||
| 	success := pingAllHelper(t, allClients, allAddrs) | ||||
| 	t.Logf("before expire: %d successful pings out of %d", success, len(allClients)*len(allIps)) | ||||
| 
 | ||||
| 	for _, client := range allClients { | ||||
| 		status, err := client.Status() | ||||
| 		assertNoErr(t, err) | ||||
| 
 | ||||
| 		// Assert that we have the original count - self
 | ||||
| 		assert.Len(t, status.Peers(), len(MustTestVersions)-1) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (s *AuthApprovalScenario) CreateHeadscaleEnv( | ||||
| 	users map[string]int, | ||||
| 	opts ...hsic.Option, | ||||
| ) error { | ||||
| 	headscale, err := s.Headscale(opts...) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	err = headscale.WaitForRunning() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	for userName, clientCount := range users { | ||||
| 		log.Printf("creating user %s with %d clients", userName, clientCount) | ||||
| 		err = s.CreateUser(userName) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		err = s.CreateTailscaleNodesInUser(userName, "all", clientCount) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		err = s.runTailscaleUp(userName, headscale.GetEndpoint(), false) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (s *AuthApprovalScenario) runTailscaleUp( | ||||
| 	userStr, loginServer string, | ||||
| 	withApproved bool, | ||||
| ) error { | ||||
| 	log.Printf("running tailscale up for user %s", userStr) | ||||
| 	if user, ok := s.users[userStr]; ok { | ||||
| 		for _, client := range user.Clients { | ||||
| 			c := client | ||||
| 			user.joinWaitGroup.Go(func() error { | ||||
| 				loginURL, err := c.LoginWithURL(loginServer) | ||||
| 				if err != nil { | ||||
| 					log.Printf("failed to run tailscale up (%s): %s", c.Hostname(), err) | ||||
| 
 | ||||
| 					return err | ||||
| 				} | ||||
| 
 | ||||
| 				err = s.runHeadscaleRegister(userStr, loginURL) | ||||
| 				if err != nil { | ||||
| 					log.Printf("failed to register client (%s): %s", c.Hostname(), err) | ||||
| 
 | ||||
| 					return err | ||||
| 				} | ||||
| 
 | ||||
| 				return nil | ||||
| 			}) | ||||
| 
 | ||||
| 			if withApproved { | ||||
| 				err := client.WaitForRunning() | ||||
| 				if err != nil { | ||||
| 					log.Printf("error waiting for client %s to be approval: %s", client.Hostname(), err) | ||||
| 				} | ||||
| 			} else { | ||||
| 				err := client.WaitForNeedsApprove() | ||||
| 				if err != nil { | ||||
| 					log.Printf("error waiting for client %s to be approval: %s", client.Hostname(), err) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if err := user.joinWaitGroup.Wait(); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		for _, client := range user.Clients { | ||||
| 			if withApproved { | ||||
| 				err := client.WaitForRunning() | ||||
| 				if err != nil { | ||||
| 					return fmt.Errorf("%s failed to up tailscale node: %w", client.Hostname(), err) | ||||
| 				} | ||||
| 			} else { | ||||
| 				err := client.WaitForNeedsApprove() | ||||
| 				if err != nil { | ||||
| 					return fmt.Errorf("%s failed to up tailscale node: %w", client.Hostname(), err) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	return fmt.Errorf("failed to up tailscale node: %w", errNoUserAvailable) | ||||
| } | ||||
| 
 | ||||
| func (s *AuthApprovalScenario) runHeadscaleRegister(userStr string, loginURL *url.URL) error { | ||||
| 	headscale, err := s.Headscale() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	log.Printf("loginURL: %s", loginURL) | ||||
| 	loginURL.Host = fmt.Sprintf("%s:8080", headscale.GetIP()) | ||||
| 	loginURL.Scheme = "http" | ||||
| 
 | ||||
| 	httpClient := &http.Client{} | ||||
| 	ctx := context.Background() | ||||
| 	req, _ := http.NewRequestWithContext(ctx, http.MethodGet, loginURL.String(), nil) | ||||
| 	resp, err := httpClient.Do(req) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	body, err := io.ReadAll(resp.Body) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	defer resp.Body.Close() | ||||
| 
 | ||||
| 	// see api.go HTML template
 | ||||
| 	codeSep := strings.Split(string(body), "</code>") | ||||
| 	if len(codeSep) != 2 { | ||||
| 		return errParseAuthPage | ||||
| 	} | ||||
| 
 | ||||
| 	keySep := strings.Split(codeSep[0], "key ") | ||||
| 	if len(keySep) != 2 { | ||||
| 		return errParseAuthPage | ||||
| 	} | ||||
| 	key := keySep[1] | ||||
| 	log.Printf("registering node %s", key) | ||||
| 
 | ||||
| 	if headscale, err := s.Headscale(); err == nil { | ||||
| 		_, err = headscale.Execute( | ||||
| 			[]string{"headscale", "nodes", "register", "--user", userStr, "--key", key}, | ||||
| 		) | ||||
| 		if err != nil { | ||||
| 			log.Printf("failed to register node: %s", err) | ||||
| 
 | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	return fmt.Errorf("failed to find headscale: %w", errNoHeadscaleAvailable) | ||||
| } | ||||
| @ -389,6 +389,62 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) { | ||||
| 	assert.Len(t, listedPreAuthKeys, 3) | ||||
| } | ||||
| 
 | ||||
| func TestPreAuthKeyCommandPreApproved(t *testing.T) { | ||||
| 	IntegrationSkip(t) | ||||
| 	t.Parallel() | ||||
| 
 | ||||
| 	user := "pre-auth-key-pre-approved-user" | ||||
| 
 | ||||
| 	scenario, err := NewScenario(dockertestMaxWait()) | ||||
| 	assertNoErr(t, err) | ||||
| 	defer scenario.ShutdownAssertNoPanics(t) | ||||
| 
 | ||||
| 	spec := map[string]int{ | ||||
| 		user: 0, | ||||
| 	} | ||||
| 
 | ||||
| 	err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clipakresueeph")) | ||||
| 	assertNoErr(t, err) | ||||
| 
 | ||||
| 	headscale, err := scenario.Headscale() | ||||
| 	assertNoErr(t, err) | ||||
| 
 | ||||
| 	var preAuthGeneralKey v1.PreAuthKey | ||||
| 	err = executeAndUnmarshal( | ||||
| 		headscale, | ||||
| 		[]string{ | ||||
| 			"headscale", | ||||
| 			"preauthkeys", | ||||
| 			"--user", | ||||
| 			user, | ||||
| 			"create", | ||||
| 			"--output", | ||||
| 			"json", | ||||
| 		}, | ||||
| 		&preAuthGeneralKey, | ||||
| 	) | ||||
| 	assertNoErr(t, err) | ||||
| 	assert.False(t, preAuthGeneralKey.GetPreApproved()) | ||||
| 
 | ||||
| 	var preAuthPreApprovedKey v1.PreAuthKey | ||||
| 	err = executeAndUnmarshal( | ||||
| 		headscale, | ||||
| 		[]string{ | ||||
| 			"headscale", | ||||
| 			"preauthkeys", | ||||
| 			"--user", | ||||
| 			user, | ||||
| 			"create", | ||||
| 			"--pre-approved", | ||||
| 			"--output", | ||||
| 			"json", | ||||
| 		}, | ||||
| 		&preAuthPreApprovedKey, | ||||
| 	) | ||||
| 	assertNoErr(t, err) | ||||
| 	assert.True(t, preAuthPreApprovedKey.GetPreApproved()) | ||||
| } | ||||
| 
 | ||||
| func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) { | ||||
| 	IntegrationSkip(t) | ||||
| 	t.Parallel() | ||||
|  | ||||
| @ -16,7 +16,7 @@ type ControlServer interface { | ||||
| 	GetEndpoint() string | ||||
| 	WaitForRunning() error | ||||
| 	CreateUser(user string) error | ||||
| 	CreateAuthKey(user string, reusable bool, ephemeral bool) (*v1.PreAuthKey, error) | ||||
| 	CreateAuthKey(user string, reusable bool, preApproved bool, ephemeral bool) (*v1.PreAuthKey, error) | ||||
| 	ListNodesInUser(user string) ([]*v1.Node, error) | ||||
| 	GetCert() []byte | ||||
| 	GetHostname() string | ||||
|  | ||||
| @ -251,7 +251,7 @@ func (s *EmbeddedDERPServerScenario) CreateHeadscaleEnv( | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		key, err := s.CreatePreAuthKey(userName, true, false) | ||||
| 		key, err := s.CreatePreAuthKey(userName, true, true, false) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| @ -175,7 +175,7 @@ func TestAuthKeyLogoutAndRelogin(t *testing.T) { | ||||
| 			} | ||||
| 
 | ||||
| 			for userName := range spec { | ||||
| 				key, err := scenario.CreatePreAuthKey(userName, true, false) | ||||
| 				key, err := scenario.CreatePreAuthKey(userName, true, true, false) | ||||
| 				if err != nil { | ||||
| 					t.Fatalf("failed to create pre-auth key for user %s: %s", userName, err) | ||||
| 				} | ||||
| @ -274,7 +274,7 @@ func testEphemeralWithOptions(t *testing.T, opts ...hsic.Option) { | ||||
| 			t.Fatalf("failed to create tailscale nodes in user %s: %s", userName, err) | ||||
| 		} | ||||
| 
 | ||||
| 		key, err := scenario.CreatePreAuthKey(userName, true, true) | ||||
| 		key, err := scenario.CreatePreAuthKey(userName, true, true, true) | ||||
| 		if err != nil { | ||||
| 			t.Fatalf("failed to create pre-auth key for user %s: %s", userName, err) | ||||
| 		} | ||||
| @ -364,7 +364,7 @@ func TestEphemeral2006DeletedTooQuickly(t *testing.T) { | ||||
| 			t.Fatalf("failed to create tailscale nodes in user %s: %s", userName, err) | ||||
| 		} | ||||
| 
 | ||||
| 		key, err := scenario.CreatePreAuthKey(userName, true, true) | ||||
| 		key, err := scenario.CreatePreAuthKey(userName, true, true, true) | ||||
| 		if err != nil { | ||||
| 			t.Fatalf("failed to create pre-auth key for user %s: %s", userName, err) | ||||
| 		} | ||||
|  | ||||
| @ -211,6 +211,13 @@ func WithTuning(batchTimeout time.Duration, mapSessionChanSize int) Option { | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // WithManualApproveNewNode allows devices to access the network only after manual approval
 | ||||
| func WithManualApproveNewNode() Option { | ||||
| 	return func(hsic *HeadscaleInContainer) { | ||||
| 		hsic.env["HEADSCALE_NODE_MANAGEMENT_MANUAL_APPROVE_NEW_NODE"] = "true" | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func WithTimezone(timezone string) Option { | ||||
| 	return func(hsic *HeadscaleInContainer) { | ||||
| 		hsic.env["TZ"] = timezone | ||||
| @ -660,6 +667,7 @@ func (t *HeadscaleInContainer) CreateUser( | ||||
| func (t *HeadscaleInContainer) CreateAuthKey( | ||||
| 	user string, | ||||
| 	reusable bool, | ||||
| 	preApproved bool, | ||||
| 	ephemeral bool, | ||||
| ) (*v1.PreAuthKey, error) { | ||||
| 	command := []string{ | ||||
| @ -678,6 +686,10 @@ func (t *HeadscaleInContainer) CreateAuthKey( | ||||
| 		command = append(command, "--reusable") | ||||
| 	} | ||||
| 
 | ||||
| 	if preApproved { | ||||
| 		command = append(command, "--pre-approved") | ||||
| 	} | ||||
| 
 | ||||
| 	if ephemeral { | ||||
| 		command = append(command, "--ephemeral") | ||||
| 	} | ||||
|  | ||||
| @ -291,10 +291,11 @@ func (s *Scenario) Headscale(opts ...hsic.Option) (ControlServer, error) { | ||||
| func (s *Scenario) CreatePreAuthKey( | ||||
| 	user string, | ||||
| 	reusable bool, | ||||
| 	preApproved bool, | ||||
| 	ephemeral bool, | ||||
| ) (*v1.PreAuthKey, error) { | ||||
| 	if headscale, err := s.Headscale(); err == nil { | ||||
| 		key, err := headscale.CreateAuthKey(user, reusable, ephemeral) | ||||
| 		key, err := headscale.CreateAuthKey(user, reusable, preApproved, ephemeral) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("failed to create user: %w", err) | ||||
| 		} | ||||
| @ -503,7 +504,7 @@ func (s *Scenario) CreateHeadscaleEnv( | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		key, err := s.CreatePreAuthKey(userName, true, false) | ||||
| 		key, err := s.CreatePreAuthKey(userName, true, true, false) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| @ -61,7 +61,7 @@ func TestHeadscale(t *testing.T) { | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("create-auth-key", func(t *testing.T) { | ||||
| 		_, err := scenario.CreatePreAuthKey(user, true, false) | ||||
| 		_, err := scenario.CreatePreAuthKey(user, true, true, false) | ||||
| 		if err != nil { | ||||
| 			t.Fatalf("failed to create preauthkey: %s", err) | ||||
| 		} | ||||
| @ -153,7 +153,7 @@ func TestTailscaleNodesJoiningHeadcale(t *testing.T) { | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("join-headscale", func(t *testing.T) { | ||||
| 		key, err := scenario.CreatePreAuthKey(user, true, false) | ||||
| 		key, err := scenario.CreatePreAuthKey(user, true, true, false) | ||||
| 		if err != nil { | ||||
| 			t.Fatalf("failed to create preauthkey: %s", err) | ||||
| 		} | ||||
|  | ||||
| @ -32,6 +32,7 @@ type TailscaleClient interface { | ||||
| 	Netmap() (*netmap.NetworkMap, error) | ||||
| 	Netcheck() (*netcheck.Report, error) | ||||
| 	WaitForNeedsLogin() error | ||||
| 	WaitForNeedsApprove() error | ||||
| 	WaitForRunning() error | ||||
| 	WaitForPeers(expected int) error | ||||
| 	Ping(hostnameOrIP string, opts ...tsic.PingOption) error | ||||
|  | ||||
| @ -772,6 +772,28 @@ func (t *TailscaleInContainer) WaitForNeedsLogin() error { | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // WaitForNeedsApprove blocks until the Tailscale (tailscaled) instance is logged in
 | ||||
| // and gets approved.
 | ||||
| func (t *TailscaleInContainer) WaitForNeedsApprove() error { | ||||
| 	return t.pool.Retry(func() error { | ||||
| 		status, err := t.Status() | ||||
| 		if err != nil { | ||||
| 			return errTailscaleStatus(t.hostname, err) | ||||
| 		} | ||||
| 
 | ||||
| 		// ipnstate.Status.CurrentTailnet was added in Tailscale 1.22.0
 | ||||
| 		// https://github.com/tailscale/tailscale/pull/3865
 | ||||
| 		//
 | ||||
| 		// Before that, we can check the BackendState to see if the
 | ||||
| 		// tailscaled daemon is connected to the control system.
 | ||||
| 		if status.BackendState == "NeedsMachineAuth" { | ||||
| 			return nil | ||||
| 		} | ||||
| 
 | ||||
| 		return errTailscaledNotReadyForLogin | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // WaitForRunning blocks until the Tailscale (tailscaled) instance is logged in
 | ||||
| // and ready to be used.
 | ||||
| func (t *TailscaleInContainer) WaitForRunning() error { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user