mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-11-04 01:51:04 +01:00 
			
		
		
		
	Merge pull request #801 from juanfont/oidc-integration-testing
Add integration tests for OIDC authentication
This commit is contained in:
		
						commit
						bd6282d1e3
					
				
							
								
								
									
										9
									
								
								.github/workflows/test-integration.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.github/workflows/test-integration.yml
									
									
									
									
										vendored
									
									
								
							@ -48,6 +48,15 @@ jobs:
 | 
			
		||||
          retry_on: error
 | 
			
		||||
          command: nix develop --command -- make test_integration_derp
 | 
			
		||||
 | 
			
		||||
      - name: Run OIDC integration tests
 | 
			
		||||
        if: steps.changed-files.outputs.any_changed == 'true'
 | 
			
		||||
        uses: nick-fields/retry@v2
 | 
			
		||||
        with:
 | 
			
		||||
          timeout_minutes: 240
 | 
			
		||||
          max_attempts: 5
 | 
			
		||||
          retry_on: error
 | 
			
		||||
          command: nix develop --command -- make test_integration_oidc
 | 
			
		||||
 | 
			
		||||
      - name: Run general integration tests
 | 
			
		||||
        if: steps.changed-files.outputs.any_changed == 'true'
 | 
			
		||||
        uses: nick-fields/retry@v2
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										5
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								Makefile
									
									
									
									
									
								
							@ -24,7 +24,7 @@ dev: lint test build
 | 
			
		||||
test:
 | 
			
		||||
	@go test -coverprofile=coverage.out ./...
 | 
			
		||||
 | 
			
		||||
test_integration: test_integration_cli test_integration_derp test_integration_general
 | 
			
		||||
test_integration: test_integration_cli test_integration_derp test_integration_oidc test_integration_general
 | 
			
		||||
 | 
			
		||||
test_integration_cli:
 | 
			
		||||
	go test -failfast -tags integration_cli,integration -timeout 30m -count=1 ./...
 | 
			
		||||
@ -35,6 +35,9 @@ test_integration_derp:
 | 
			
		||||
test_integration_general:
 | 
			
		||||
	go test -failfast -tags integration_general,integration -timeout 30m -count=1 ./...
 | 
			
		||||
 | 
			
		||||
test_integration_oidc:
 | 
			
		||||
	go test -failfast -tags integration_oidc,integration -timeout 30m -count=1 ./...
 | 
			
		||||
 | 
			
		||||
coverprofile_func:
 | 
			
		||||
	go tool cover -func=coverage.out
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,7 @@ func (h *Headscale) generateMapResponse(
 | 
			
		||||
		Str("func", "generateMapResponse").
 | 
			
		||||
		Str("machine", mapRequest.Hostinfo.Hostname).
 | 
			
		||||
		Msg("Creating Map response")
 | 
			
		||||
	node, err := machine.toNode(h.cfg.BaseDomain, h.cfg.DNSConfig, true)
 | 
			
		||||
	node, err := machine.toNode(h.cfg.BaseDomain, h.cfg.DNSConfig)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error().
 | 
			
		||||
			Caller().
 | 
			
		||||
@ -37,7 +37,7 @@ func (h *Headscale) generateMapResponse(
 | 
			
		||||
 | 
			
		||||
	profiles := getMapResponseUserProfiles(*machine, peers)
 | 
			
		||||
 | 
			
		||||
	nodePeers, err := peers.toNodes(h.cfg.BaseDomain, h.cfg.DNSConfig, true)
 | 
			
		||||
	nodePeers, err := peers.toNodes(h.cfg.BaseDomain, h.cfg.DNSConfig)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error().
 | 
			
		||||
			Caller().
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										100
									
								
								cmd/headscale/cli/mockoidc.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								cmd/headscale/cli/mockoidc.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,100 @@
 | 
			
		||||
package cli
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/oauth2-proxy/mockoidc"
 | 
			
		||||
	"github.com/rs/zerolog/log"
 | 
			
		||||
	"github.com/spf13/cobra"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	errMockOidcClientIDNotDefined     = Error("MOCKOIDC_CLIENT_ID not defined")
 | 
			
		||||
	errMockOidcClientSecretNotDefined = Error("MOCKOIDC_CLIENT_SECRET not defined")
 | 
			
		||||
	errMockOidcPortNotDefined         = Error("MOCKOIDC_PORT not defined")
 | 
			
		||||
	accessTTL                         = 10 * time.Minute
 | 
			
		||||
	refreshTTL                        = 60 * time.Minute
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	rootCmd.AddCommand(mockOidcCmd)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var mockOidcCmd = &cobra.Command{
 | 
			
		||||
	Use:   "mockoidc",
 | 
			
		||||
	Short: "Runs a mock OIDC server for testing",
 | 
			
		||||
	Long:  "This internal command runs a OpenID Connect for testing purposes",
 | 
			
		||||
	Run: func(cmd *cobra.Command, args []string) {
 | 
			
		||||
		err := mockOIDC()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Error().Err(err).Msgf("Error running mock OIDC server")
 | 
			
		||||
			os.Exit(1)
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func mockOIDC() error {
 | 
			
		||||
	clientID := os.Getenv("MOCKOIDC_CLIENT_ID")
 | 
			
		||||
	if clientID == "" {
 | 
			
		||||
		return errMockOidcClientIDNotDefined
 | 
			
		||||
	}
 | 
			
		||||
	clientSecret := os.Getenv("MOCKOIDC_CLIENT_SECRET")
 | 
			
		||||
	if clientSecret == "" {
 | 
			
		||||
		return errMockOidcClientSecretNotDefined
 | 
			
		||||
	}
 | 
			
		||||
	portStr := os.Getenv("MOCKOIDC_PORT")
 | 
			
		||||
	if portStr == "" {
 | 
			
		||||
		return errMockOidcPortNotDefined
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	port, err := strconv.Atoi(portStr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	mock, err := getMockOIDC(clientID, clientSecret)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	listener, err := net.Listen("tcp", fmt.Sprintf("mockoidc:%d", port))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = mock.Start(listener, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	log.Info().Msgf("Mock OIDC server listening on %s", listener.Addr().String())
 | 
			
		||||
	log.Info().Msgf("Issuer: %s", mock.Issuer())
 | 
			
		||||
	c := make(chan struct{})
 | 
			
		||||
	<-c
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getMockOIDC(clientID string, clientSecret string) (*mockoidc.MockOIDC, error) {
 | 
			
		||||
	keypair, err := mockoidc.NewKeypair(nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	mock := mockoidc.MockOIDC{
 | 
			
		||||
		ClientID:                      clientID,
 | 
			
		||||
		ClientSecret:                  clientSecret,
 | 
			
		||||
		AccessTTL:                     accessTTL,
 | 
			
		||||
		RefreshTTL:                    refreshTTL,
 | 
			
		||||
		CodeChallengeMethodsSupported: []string{"plain", "S256"},
 | 
			
		||||
		Keypair:                       keypair,
 | 
			
		||||
		SessionStore:                  mockoidc.NewSessionStore(),
 | 
			
		||||
		UserQueue:                     &mockoidc.UserQueue{},
 | 
			
		||||
		ErrorQueue:                    &mockoidc.ErrorQueue{},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &mock, nil
 | 
			
		||||
}
 | 
			
		||||
@ -15,6 +15,10 @@ import (
 | 
			
		||||
var cfgFile string = ""
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	if len(os.Args) > 1 && os.Args[1] == "version" || os.Args[1] == "mockoidc" {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cobra.OnInitialize(initConfig)
 | 
			
		||||
	rootCmd.PersistentFlags().
 | 
			
		||||
		StringVarP(&cfgFile, "config", "c", "", "config file (default is /etc/headscale/config.yaml)")
 | 
			
		||||
 | 
			
		||||
@ -24,7 +24,7 @@
 | 
			
		||||
 | 
			
		||||
              # When updating go.mod or go.sum, a new sha will need to be calculated,
 | 
			
		||||
              # update this if you have a mismatch after doing a change to thos files.
 | 
			
		||||
              vendorSha256 = "sha256-kc8EU+TkwRlsKM2+ljm/88aWe5h2QMgd/ZGPSgdd9QQ=";
 | 
			
		||||
              vendorSha256 = "sha256-DosFCSiQ5FURbIrt4NcPGkExc84t2MGMqe9XLxNHdIM=";
 | 
			
		||||
 | 
			
		||||
              ldflags = [ "-s" "-w" "-X github.com/juanfont/headscale/cmd/headscale/cli.Version=v${version}" ];
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
@ -129,7 +129,7 @@ func (s *IntegrationCLITestSuite) HandleStats(
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *IntegrationCLITestSuite) createNamespace(name string) (*v1.Namespace, error) {
 | 
			
		||||
	result, err := ExecuteCommand(
 | 
			
		||||
	result, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -172,7 +172,7 @@ func (s *IntegrationCLITestSuite) TestNamespaceCommand() {
 | 
			
		||||
	assert.Equal(s.T(), names[2], namespaces[2].Name)
 | 
			
		||||
 | 
			
		||||
	// Test list namespaces
 | 
			
		||||
	listResult, err := ExecuteCommand(
 | 
			
		||||
	listResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -194,7 +194,7 @@ func (s *IntegrationCLITestSuite) TestNamespaceCommand() {
 | 
			
		||||
	assert.Equal(s.T(), names[2], listedNamespaces[2].Name)
 | 
			
		||||
 | 
			
		||||
	// Test rename namespace
 | 
			
		||||
	renameResult, err := ExecuteCommand(
 | 
			
		||||
	renameResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -216,7 +216,7 @@ func (s *IntegrationCLITestSuite) TestNamespaceCommand() {
 | 
			
		||||
	assert.Equal(s.T(), renamedNamespace.Name, "newname")
 | 
			
		||||
 | 
			
		||||
	// Test list after rename namespaces
 | 
			
		||||
	listAfterRenameResult, err := ExecuteCommand(
 | 
			
		||||
	listAfterRenameResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -247,7 +247,7 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommand() {
 | 
			
		||||
	assert.Nil(s.T(), err)
 | 
			
		||||
 | 
			
		||||
	for i := 0; i < count; i++ {
 | 
			
		||||
		preAuthResult, err := ExecuteCommand(
 | 
			
		||||
		preAuthResult, _, err := ExecuteCommand(
 | 
			
		||||
			&s.headscale,
 | 
			
		||||
			[]string{
 | 
			
		||||
				"headscale",
 | 
			
		||||
@ -275,7 +275,7 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommand() {
 | 
			
		||||
	assert.Len(s.T(), keys, 5)
 | 
			
		||||
 | 
			
		||||
	// Test list of keys
 | 
			
		||||
	listResult, err := ExecuteCommand(
 | 
			
		||||
	listResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -335,7 +335,7 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommand() {
 | 
			
		||||
 | 
			
		||||
	// Expire three keys
 | 
			
		||||
	for i := 0; i < 3; i++ {
 | 
			
		||||
		_, err := ExecuteCommand(
 | 
			
		||||
		_, _, err := ExecuteCommand(
 | 
			
		||||
			&s.headscale,
 | 
			
		||||
			[]string{
 | 
			
		||||
				"headscale",
 | 
			
		||||
@ -351,7 +351,7 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommand() {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Test list pre auth keys after expire
 | 
			
		||||
	listAfterExpireResult, err := ExecuteCommand(
 | 
			
		||||
	listAfterExpireResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -396,7 +396,7 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommandWithoutExpiry() {
 | 
			
		||||
	namespace, err := s.createNamespace("pre-auth-key-without-exp-namespace")
 | 
			
		||||
	assert.Nil(s.T(), err)
 | 
			
		||||
 | 
			
		||||
	preAuthResult, err := ExecuteCommand(
 | 
			
		||||
	preAuthResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -417,7 +417,7 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommandWithoutExpiry() {
 | 
			
		||||
	assert.Nil(s.T(), err)
 | 
			
		||||
 | 
			
		||||
	// Test list of keys
 | 
			
		||||
	listResult, err := ExecuteCommand(
 | 
			
		||||
	listResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -449,7 +449,7 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommandReusableEphemeral() {
 | 
			
		||||
	namespace, err := s.createNamespace("pre-auth-key-reus-ephm-namespace")
 | 
			
		||||
	assert.Nil(s.T(), err)
 | 
			
		||||
 | 
			
		||||
	preAuthReusableResult, err := ExecuteCommand(
 | 
			
		||||
	preAuthReusableResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -472,7 +472,7 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommandReusableEphemeral() {
 | 
			
		||||
	assert.True(s.T(), preAuthReusableKey.GetReusable())
 | 
			
		||||
	assert.False(s.T(), preAuthReusableKey.GetEphemeral())
 | 
			
		||||
 | 
			
		||||
	preAuthEphemeralResult, err := ExecuteCommand(
 | 
			
		||||
	preAuthEphemeralResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -514,7 +514,7 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommandReusableEphemeral() {
 | 
			
		||||
	// assert.NotNil(s.T(), err)
 | 
			
		||||
 | 
			
		||||
	// Test list of keys
 | 
			
		||||
	listResult, err := ExecuteCommand(
 | 
			
		||||
	listResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -548,7 +548,7 @@ func (s *IntegrationCLITestSuite) TestNodeTagCommand() {
 | 
			
		||||
	assert.Nil(s.T(), err)
 | 
			
		||||
 | 
			
		||||
	for index, machineKey := range machineKeys {
 | 
			
		||||
		_, err := ExecuteCommand(
 | 
			
		||||
		_, _, err := ExecuteCommand(
 | 
			
		||||
			&s.headscale,
 | 
			
		||||
			[]string{
 | 
			
		||||
				"headscale",
 | 
			
		||||
@ -567,7 +567,7 @@ func (s *IntegrationCLITestSuite) TestNodeTagCommand() {
 | 
			
		||||
		)
 | 
			
		||||
		assert.Nil(s.T(), err)
 | 
			
		||||
 | 
			
		||||
		machineResult, err := ExecuteCommand(
 | 
			
		||||
		machineResult, _, err := ExecuteCommand(
 | 
			
		||||
			&s.headscale,
 | 
			
		||||
			[]string{
 | 
			
		||||
				"headscale",
 | 
			
		||||
@ -592,7 +592,7 @@ func (s *IntegrationCLITestSuite) TestNodeTagCommand() {
 | 
			
		||||
	}
 | 
			
		||||
	assert.Len(s.T(), machines, len(machineKeys))
 | 
			
		||||
 | 
			
		||||
	addTagResult, err := ExecuteCommand(
 | 
			
		||||
	addTagResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -612,7 +612,7 @@ func (s *IntegrationCLITestSuite) TestNodeTagCommand() {
 | 
			
		||||
	assert.Equal(s.T(), []string{"tag:test"}, machine.ForcedTags)
 | 
			
		||||
 | 
			
		||||
	// try to set a wrong tag and retrieve the error
 | 
			
		||||
	wrongTagResult, err := ExecuteCommand(
 | 
			
		||||
	wrongTagResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -634,7 +634,7 @@ func (s *IntegrationCLITestSuite) TestNodeTagCommand() {
 | 
			
		||||
	assert.Contains(s.T(), errorOutput.Error, "tag must start with the string 'tag:'")
 | 
			
		||||
 | 
			
		||||
	// Test list all nodes after added seconds
 | 
			
		||||
	listAllResult, err := ExecuteCommand(
 | 
			
		||||
	listAllResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -684,7 +684,7 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
 | 
			
		||||
	assert.Nil(s.T(), err)
 | 
			
		||||
 | 
			
		||||
	for index, machineKey := range machineKeys {
 | 
			
		||||
		_, err := ExecuteCommand(
 | 
			
		||||
		_, _, err := ExecuteCommand(
 | 
			
		||||
			&s.headscale,
 | 
			
		||||
			[]string{
 | 
			
		||||
				"headscale",
 | 
			
		||||
@ -703,7 +703,7 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
 | 
			
		||||
		)
 | 
			
		||||
		assert.Nil(s.T(), err)
 | 
			
		||||
 | 
			
		||||
		machineResult, err := ExecuteCommand(
 | 
			
		||||
		machineResult, _, err := ExecuteCommand(
 | 
			
		||||
			&s.headscale,
 | 
			
		||||
			[]string{
 | 
			
		||||
				"headscale",
 | 
			
		||||
@ -730,7 +730,7 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
 | 
			
		||||
	assert.Len(s.T(), machines, len(machineKeys))
 | 
			
		||||
 | 
			
		||||
	// Test list all nodes after added seconds
 | 
			
		||||
	listAllResult, err := ExecuteCommand(
 | 
			
		||||
	listAllResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -769,7 +769,7 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
 | 
			
		||||
	assert.Nil(s.T(), err)
 | 
			
		||||
 | 
			
		||||
	for index, machineKey := range otherNamespaceMachineKeys {
 | 
			
		||||
		_, err := ExecuteCommand(
 | 
			
		||||
		_, _, err := ExecuteCommand(
 | 
			
		||||
			&s.headscale,
 | 
			
		||||
			[]string{
 | 
			
		||||
				"headscale",
 | 
			
		||||
@ -788,7 +788,7 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
 | 
			
		||||
		)
 | 
			
		||||
		assert.Nil(s.T(), err)
 | 
			
		||||
 | 
			
		||||
		machineResult, err := ExecuteCommand(
 | 
			
		||||
		machineResult, _, err := ExecuteCommand(
 | 
			
		||||
			&s.headscale,
 | 
			
		||||
			[]string{
 | 
			
		||||
				"headscale",
 | 
			
		||||
@ -815,7 +815,7 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
 | 
			
		||||
	assert.Len(s.T(), otherNamespaceMachines, len(otherNamespaceMachineKeys))
 | 
			
		||||
 | 
			
		||||
	// Test list all nodes after added otherNamespace
 | 
			
		||||
	listAllWithotherNamespaceResult, err := ExecuteCommand(
 | 
			
		||||
	listAllWithotherNamespaceResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -845,7 +845,7 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
 | 
			
		||||
	assert.Equal(s.T(), "otherNamespace-machine-2", listAllWithotherNamespace[6].Name)
 | 
			
		||||
 | 
			
		||||
	// Test list all nodes after added otherNamespace
 | 
			
		||||
	listOnlyotherNamespaceMachineNamespaceResult, err := ExecuteCommand(
 | 
			
		||||
	listOnlyotherNamespaceMachineNamespaceResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -884,7 +884,7 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	// Delete a machines
 | 
			
		||||
	_, err = ExecuteCommand(
 | 
			
		||||
	_, _, err = ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -902,7 +902,7 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
 | 
			
		||||
	assert.Nil(s.T(), err)
 | 
			
		||||
 | 
			
		||||
	// Test: list main namespace after machine is deleted
 | 
			
		||||
	listOnlyMachineNamespaceAfterDeleteResult, err := ExecuteCommand(
 | 
			
		||||
	listOnlyMachineNamespaceAfterDeleteResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -943,7 +943,7 @@ func (s *IntegrationCLITestSuite) TestNodeExpireCommand() {
 | 
			
		||||
	assert.Nil(s.T(), err)
 | 
			
		||||
 | 
			
		||||
	for index, machineKey := range machineKeys {
 | 
			
		||||
		_, err := ExecuteCommand(
 | 
			
		||||
		_, _, err := ExecuteCommand(
 | 
			
		||||
			&s.headscale,
 | 
			
		||||
			[]string{
 | 
			
		||||
				"headscale",
 | 
			
		||||
@ -962,7 +962,7 @@ func (s *IntegrationCLITestSuite) TestNodeExpireCommand() {
 | 
			
		||||
		)
 | 
			
		||||
		assert.Nil(s.T(), err)
 | 
			
		||||
 | 
			
		||||
		machineResult, err := ExecuteCommand(
 | 
			
		||||
		machineResult, _, err := ExecuteCommand(
 | 
			
		||||
			&s.headscale,
 | 
			
		||||
			[]string{
 | 
			
		||||
				"headscale",
 | 
			
		||||
@ -988,7 +988,7 @@ func (s *IntegrationCLITestSuite) TestNodeExpireCommand() {
 | 
			
		||||
 | 
			
		||||
	assert.Len(s.T(), machines, len(machineKeys))
 | 
			
		||||
 | 
			
		||||
	listAllResult, err := ExecuteCommand(
 | 
			
		||||
	listAllResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -1014,7 +1014,7 @@ func (s *IntegrationCLITestSuite) TestNodeExpireCommand() {
 | 
			
		||||
	assert.True(s.T(), listAll[4].Expiry.AsTime().IsZero())
 | 
			
		||||
 | 
			
		||||
	for i := 0; i < 3; i++ {
 | 
			
		||||
		_, err := ExecuteCommand(
 | 
			
		||||
		_, _, err := ExecuteCommand(
 | 
			
		||||
			&s.headscale,
 | 
			
		||||
			[]string{
 | 
			
		||||
				"headscale",
 | 
			
		||||
@ -1028,7 +1028,7 @@ func (s *IntegrationCLITestSuite) TestNodeExpireCommand() {
 | 
			
		||||
		assert.Nil(s.T(), err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	listAllAfterExpiryResult, err := ExecuteCommand(
 | 
			
		||||
	listAllAfterExpiryResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -1070,7 +1070,7 @@ func (s *IntegrationCLITestSuite) TestNodeRenameCommand() {
 | 
			
		||||
	assert.Nil(s.T(), err)
 | 
			
		||||
 | 
			
		||||
	for index, machineKey := range machineKeys {
 | 
			
		||||
		_, err := ExecuteCommand(
 | 
			
		||||
		_, _, err := ExecuteCommand(
 | 
			
		||||
			&s.headscale,
 | 
			
		||||
			[]string{
 | 
			
		||||
				"headscale",
 | 
			
		||||
@ -1089,7 +1089,7 @@ func (s *IntegrationCLITestSuite) TestNodeRenameCommand() {
 | 
			
		||||
		)
 | 
			
		||||
		assert.Nil(s.T(), err)
 | 
			
		||||
 | 
			
		||||
		machineResult, err := ExecuteCommand(
 | 
			
		||||
		machineResult, _, err := ExecuteCommand(
 | 
			
		||||
			&s.headscale,
 | 
			
		||||
			[]string{
 | 
			
		||||
				"headscale",
 | 
			
		||||
@ -1115,7 +1115,7 @@ func (s *IntegrationCLITestSuite) TestNodeRenameCommand() {
 | 
			
		||||
 | 
			
		||||
	assert.Len(s.T(), machines, len(machineKeys))
 | 
			
		||||
 | 
			
		||||
	listAllResult, err := ExecuteCommand(
 | 
			
		||||
	listAllResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -1141,7 +1141,7 @@ func (s *IntegrationCLITestSuite) TestNodeRenameCommand() {
 | 
			
		||||
	assert.Contains(s.T(), listAll[4].GetGivenName(), "machine-5")
 | 
			
		||||
 | 
			
		||||
	for i := 0; i < 3; i++ {
 | 
			
		||||
		_, err := ExecuteCommand(
 | 
			
		||||
		_, _, err := ExecuteCommand(
 | 
			
		||||
			&s.headscale,
 | 
			
		||||
			[]string{
 | 
			
		||||
				"headscale",
 | 
			
		||||
@ -1156,7 +1156,7 @@ func (s *IntegrationCLITestSuite) TestNodeRenameCommand() {
 | 
			
		||||
		assert.Nil(s.T(), err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	listAllAfterRenameResult, err := ExecuteCommand(
 | 
			
		||||
	listAllAfterRenameResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -1182,7 +1182,7 @@ func (s *IntegrationCLITestSuite) TestNodeRenameCommand() {
 | 
			
		||||
	assert.Contains(s.T(), listAllAfterRename[4].GetGivenName(), "machine-5")
 | 
			
		||||
 | 
			
		||||
	// Test failure for too long names
 | 
			
		||||
	result, err := ExecuteCommand(
 | 
			
		||||
	result, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -1197,7 +1197,7 @@ func (s *IntegrationCLITestSuite) TestNodeRenameCommand() {
 | 
			
		||||
	assert.Nil(s.T(), err)
 | 
			
		||||
	assert.Contains(s.T(), result, "not be over 63 chars")
 | 
			
		||||
 | 
			
		||||
	listAllAfterRenameAttemptResult, err := ExecuteCommand(
 | 
			
		||||
	listAllAfterRenameAttemptResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -1233,7 +1233,7 @@ func (s *IntegrationCLITestSuite) TestRouteCommand() {
 | 
			
		||||
	// Randomly generated machine keys
 | 
			
		||||
	machineKey := "9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe"
 | 
			
		||||
 | 
			
		||||
	_, err = ExecuteCommand(
 | 
			
		||||
	_, _, err = ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -1256,7 +1256,7 @@ func (s *IntegrationCLITestSuite) TestRouteCommand() {
 | 
			
		||||
	)
 | 
			
		||||
	assert.Nil(s.T(), err)
 | 
			
		||||
 | 
			
		||||
	machineResult, err := ExecuteCommand(
 | 
			
		||||
	machineResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -1280,7 +1280,7 @@ func (s *IntegrationCLITestSuite) TestRouteCommand() {
 | 
			
		||||
	assert.Equal(s.T(), uint64(1), machine.Id)
 | 
			
		||||
	assert.Equal(s.T(), "route-machine", machine.Name)
 | 
			
		||||
 | 
			
		||||
	listAllResult, err := ExecuteCommand(
 | 
			
		||||
	listAllResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -1305,7 +1305,7 @@ func (s *IntegrationCLITestSuite) TestRouteCommand() {
 | 
			
		||||
 | 
			
		||||
	assert.Empty(s.T(), listAll.EnabledRoutes)
 | 
			
		||||
 | 
			
		||||
	enableTwoRoutesResult, err := ExecuteCommand(
 | 
			
		||||
	enableTwoRoutesResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -1337,7 +1337,7 @@ func (s *IntegrationCLITestSuite) TestRouteCommand() {
 | 
			
		||||
	assert.Contains(s.T(), enableTwoRoutes.EnabledRoutes, "192.168.1.0/24")
 | 
			
		||||
 | 
			
		||||
	// Enable only one route, effectively disabling one of the routes
 | 
			
		||||
	enableOneRouteResult, err := ExecuteCommand(
 | 
			
		||||
	enableOneRouteResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -1366,7 +1366,7 @@ func (s *IntegrationCLITestSuite) TestRouteCommand() {
 | 
			
		||||
	assert.Contains(s.T(), enableOneRoute.EnabledRoutes, "10.0.0.0/8")
 | 
			
		||||
 | 
			
		||||
	// Enable only one route, effectively disabling one of the routes
 | 
			
		||||
	failEnableNonAdvertisedRoute, err := ExecuteCommand(
 | 
			
		||||
	failEnableNonAdvertisedRoute, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -1390,7 +1390,7 @@ func (s *IntegrationCLITestSuite) TestRouteCommand() {
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	// Enable all routes on host
 | 
			
		||||
	enableAllRouteResult, err := ExecuteCommand(
 | 
			
		||||
	enableAllRouteResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -1425,7 +1425,7 @@ func (s *IntegrationCLITestSuite) TestApiKeyCommand() {
 | 
			
		||||
	keys := make([]string, count)
 | 
			
		||||
 | 
			
		||||
	for i := 0; i < count; i++ {
 | 
			
		||||
		apiResult, err := ExecuteCommand(
 | 
			
		||||
		apiResult, _, err := ExecuteCommand(
 | 
			
		||||
			&s.headscale,
 | 
			
		||||
			[]string{
 | 
			
		||||
				"headscale",
 | 
			
		||||
@ -1451,7 +1451,7 @@ func (s *IntegrationCLITestSuite) TestApiKeyCommand() {
 | 
			
		||||
	assert.Len(s.T(), keys, 5)
 | 
			
		||||
 | 
			
		||||
	// Test list of keys
 | 
			
		||||
	listResult, err := ExecuteCommand(
 | 
			
		||||
	listResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -1513,7 +1513,7 @@ func (s *IntegrationCLITestSuite) TestApiKeyCommand() {
 | 
			
		||||
 | 
			
		||||
	// Expire three keys
 | 
			
		||||
	for i := 0; i < 3; i++ {
 | 
			
		||||
		_, err := ExecuteCommand(
 | 
			
		||||
		_, _, err := ExecuteCommand(
 | 
			
		||||
			&s.headscale,
 | 
			
		||||
			[]string{
 | 
			
		||||
				"headscale",
 | 
			
		||||
@ -1530,7 +1530,7 @@ func (s *IntegrationCLITestSuite) TestApiKeyCommand() {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Test list pre auth keys after expire
 | 
			
		||||
	listAfterExpireResult, err := ExecuteCommand(
 | 
			
		||||
	listAfterExpireResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -1573,7 +1573,7 @@ func (s *IntegrationCLITestSuite) TestNodeMoveCommand() {
 | 
			
		||||
	// Randomly generated machine key
 | 
			
		||||
	machineKey := "688411b767663479632d44140f08a9fde87383adc7cdeb518f62ce28a17ef0aa"
 | 
			
		||||
 | 
			
		||||
	_, err = ExecuteCommand(
 | 
			
		||||
	_, _, err = ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -1592,7 +1592,7 @@ func (s *IntegrationCLITestSuite) TestNodeMoveCommand() {
 | 
			
		||||
	)
 | 
			
		||||
	assert.Nil(s.T(), err)
 | 
			
		||||
 | 
			
		||||
	machineResult, err := ExecuteCommand(
 | 
			
		||||
	machineResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -1619,7 +1619,7 @@ func (s *IntegrationCLITestSuite) TestNodeMoveCommand() {
 | 
			
		||||
 | 
			
		||||
	machineId := fmt.Sprintf("%d", machine.Id)
 | 
			
		||||
 | 
			
		||||
	moveToNewNSResult, err := ExecuteCommand(
 | 
			
		||||
	moveToNewNSResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -1641,7 +1641,7 @@ func (s *IntegrationCLITestSuite) TestNodeMoveCommand() {
 | 
			
		||||
 | 
			
		||||
	assert.Equal(s.T(), machine.Namespace, newNamespace)
 | 
			
		||||
 | 
			
		||||
	listAllNodesResult, err := ExecuteCommand(
 | 
			
		||||
	listAllNodesResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -1664,7 +1664,7 @@ func (s *IntegrationCLITestSuite) TestNodeMoveCommand() {
 | 
			
		||||
	assert.Equal(s.T(), allNodes[0].Namespace, machine.Namespace)
 | 
			
		||||
	assert.Equal(s.T(), allNodes[0].Namespace, newNamespace)
 | 
			
		||||
 | 
			
		||||
	moveToNonExistingNSResult, err := ExecuteCommand(
 | 
			
		||||
	moveToNonExistingNSResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -1688,7 +1688,7 @@ func (s *IntegrationCLITestSuite) TestNodeMoveCommand() {
 | 
			
		||||
	)
 | 
			
		||||
	assert.Equal(s.T(), machine.Namespace, newNamespace)
 | 
			
		||||
 | 
			
		||||
	moveToOldNSResult, err := ExecuteCommand(
 | 
			
		||||
	moveToOldNSResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -1710,7 +1710,7 @@ func (s *IntegrationCLITestSuite) TestNodeMoveCommand() {
 | 
			
		||||
 | 
			
		||||
	assert.Equal(s.T(), machine.Namespace, oldNamespace)
 | 
			
		||||
 | 
			
		||||
	moveToSameNSResult, err := ExecuteCommand(
 | 
			
		||||
	moveToSameNSResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -1742,7 +1742,7 @@ func (s *IntegrationCLITestSuite) TestLoadConfigFromCommand() {
 | 
			
		||||
	altEnvConfig, err := os.ReadFile("integration_test/etc/alt-env-config.dump.gold.yaml")
 | 
			
		||||
	assert.Nil(s.T(), err)
 | 
			
		||||
 | 
			
		||||
	_, err = ExecuteCommand(
 | 
			
		||||
	_, _, err = ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -1757,7 +1757,7 @@ func (s *IntegrationCLITestSuite) TestLoadConfigFromCommand() {
 | 
			
		||||
 | 
			
		||||
	assert.YAMLEq(s.T(), string(defaultConfig), string(defaultDumpConfig))
 | 
			
		||||
 | 
			
		||||
	_, err = ExecuteCommand(
 | 
			
		||||
	_, _, err = ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -1774,7 +1774,7 @@ func (s *IntegrationCLITestSuite) TestLoadConfigFromCommand() {
 | 
			
		||||
 | 
			
		||||
	assert.YAMLEq(s.T(), string(altConfig), string(altDumpConfig))
 | 
			
		||||
 | 
			
		||||
	_, err = ExecuteCommand(
 | 
			
		||||
	_, _, err = ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -1791,7 +1791,7 @@ func (s *IntegrationCLITestSuite) TestLoadConfigFromCommand() {
 | 
			
		||||
 | 
			
		||||
	assert.YAMLEq(s.T(), string(altEnvConfig), string(altEnvDumpConfig))
 | 
			
		||||
 | 
			
		||||
	_, err = ExecuteCommand(
 | 
			
		||||
	_, _, err = ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
 | 
			
		||||
@ -68,7 +68,7 @@ func ExecuteCommand(
 | 
			
		||||
	cmd []string,
 | 
			
		||||
	env []string,
 | 
			
		||||
	options ...ExecuteCommandOption,
 | 
			
		||||
) (string, error) {
 | 
			
		||||
) (string, string, error) {
 | 
			
		||||
	var stdout bytes.Buffer
 | 
			
		||||
	var stderr bytes.Buffer
 | 
			
		||||
 | 
			
		||||
@ -78,7 +78,7 @@ func ExecuteCommand(
 | 
			
		||||
 | 
			
		||||
	for _, opt := range options {
 | 
			
		||||
		if err := opt(&execConfig); err != nil {
 | 
			
		||||
			return "", fmt.Errorf("execute-command/options: %w", err)
 | 
			
		||||
			return "", "", fmt.Errorf("execute-command/options: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -107,7 +107,7 @@ func ExecuteCommand(
 | 
			
		||||
	select {
 | 
			
		||||
	case res := <-resultChan:
 | 
			
		||||
		if res.err != nil {
 | 
			
		||||
			return "", res.err
 | 
			
		||||
			return stdout.String(), stderr.String(), res.err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if res.exitCode != 0 {
 | 
			
		||||
@ -115,13 +115,13 @@ func ExecuteCommand(
 | 
			
		||||
			fmt.Println("stdout: ", stdout.String())
 | 
			
		||||
			fmt.Println("stderr: ", stderr.String())
 | 
			
		||||
 | 
			
		||||
			return "", fmt.Errorf("command failed with: %s", stderr.String())
 | 
			
		||||
			return stdout.String(), stderr.String(), fmt.Errorf("command failed with: %s", stderr.String())
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return stdout.String(), nil
 | 
			
		||||
		return stdout.String(), stderr.String(), nil
 | 
			
		||||
	case <-time.After(execConfig.timeout):
 | 
			
		||||
 | 
			
		||||
		return "", fmt.Errorf("command timed out after %s", execConfig.timeout)
 | 
			
		||||
		return stdout.String(), stderr.String(), fmt.Errorf("command timed out after %s", execConfig.timeout)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -200,7 +200,7 @@ func getIPs(
 | 
			
		||||
	for hostname, tailscale := range tailscales {
 | 
			
		||||
		command := []string{"tailscale", "ip"}
 | 
			
		||||
 | 
			
		||||
		result, err := ExecuteCommand(
 | 
			
		||||
		result, _, err := ExecuteCommand(
 | 
			
		||||
			&tailscale,
 | 
			
		||||
			command,
 | 
			
		||||
			[]string{},
 | 
			
		||||
@ -228,7 +228,7 @@ func getIPs(
 | 
			
		||||
func getDNSNames(
 | 
			
		||||
	headscale *dockertest.Resource,
 | 
			
		||||
) ([]string, error) {
 | 
			
		||||
	listAllResult, err := ExecuteCommand(
 | 
			
		||||
	listAllResult, _, err := ExecuteCommand(
 | 
			
		||||
		headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -261,7 +261,7 @@ func getDNSNames(
 | 
			
		||||
func getMagicFQDN(
 | 
			
		||||
	headscale *dockertest.Resource,
 | 
			
		||||
) ([]string, error) {
 | 
			
		||||
	listAllResult, err := ExecuteCommand(
 | 
			
		||||
	listAllResult, _, err := ExecuteCommand(
 | 
			
		||||
		headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
 | 
			
		||||
@ -187,7 +187,7 @@ func (s *IntegrationDERPTestSuite) SetupSuite() {
 | 
			
		||||
	log.Println("headscale container is ready for embedded DERP tests")
 | 
			
		||||
 | 
			
		||||
	log.Printf("Creating headscale namespace: %s\n", namespaceName)
 | 
			
		||||
	result, err := ExecuteCommand(
 | 
			
		||||
	result, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{"headscale", "namespaces", "create", namespaceName},
 | 
			
		||||
		[]string{},
 | 
			
		||||
@ -196,7 +196,7 @@ func (s *IntegrationDERPTestSuite) SetupSuite() {
 | 
			
		||||
	assert.Nil(s.T(), err)
 | 
			
		||||
 | 
			
		||||
	log.Printf("Creating pre auth key for %s\n", namespaceName)
 | 
			
		||||
	preAuthResult, err := ExecuteCommand(
 | 
			
		||||
	preAuthResult, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{
 | 
			
		||||
			"headscale",
 | 
			
		||||
@ -259,7 +259,7 @@ func (s *IntegrationDERPTestSuite) Join(
 | 
			
		||||
 | 
			
		||||
	log.Println("Join command:", command)
 | 
			
		||||
	log.Printf("Running join command for %s\n", hostname)
 | 
			
		||||
	_, err := ExecuteCommand(
 | 
			
		||||
	_, _, err := ExecuteCommand(
 | 
			
		||||
		&tailscale,
 | 
			
		||||
		command,
 | 
			
		||||
		[]string{},
 | 
			
		||||
@ -414,7 +414,7 @@ func (s *IntegrationDERPTestSuite) TestPingAllPeersByHostname() {
 | 
			
		||||
					peername,
 | 
			
		||||
				)
 | 
			
		||||
				log.Println(command)
 | 
			
		||||
				result, err := ExecuteCommand(
 | 
			
		||||
				result, _, err := ExecuteCommand(
 | 
			
		||||
					&tailscale,
 | 
			
		||||
					command,
 | 
			
		||||
					[]string{},
 | 
			
		||||
 | 
			
		||||
@ -163,7 +163,7 @@ func (s *IntegrationTestSuite) Join(
 | 
			
		||||
 | 
			
		||||
	log.Println("Join command:", command)
 | 
			
		||||
	log.Printf("Running join command for %s\n", hostname)
 | 
			
		||||
	_, err := ExecuteCommand(
 | 
			
		||||
	_, _, err := ExecuteCommand(
 | 
			
		||||
		&tailscale,
 | 
			
		||||
		command,
 | 
			
		||||
		[]string{},
 | 
			
		||||
@ -305,7 +305,7 @@ func (s *IntegrationTestSuite) SetupSuite() {
 | 
			
		||||
 | 
			
		||||
	for namespace, scales := range s.namespaces {
 | 
			
		||||
		log.Printf("Creating headscale namespace: %s\n", namespace)
 | 
			
		||||
		result, err := ExecuteCommand(
 | 
			
		||||
		result, _, err := ExecuteCommand(
 | 
			
		||||
			&s.headscale,
 | 
			
		||||
			[]string{"headscale", "namespaces", "create", namespace},
 | 
			
		||||
			[]string{},
 | 
			
		||||
@ -314,7 +314,7 @@ func (s *IntegrationTestSuite) SetupSuite() {
 | 
			
		||||
		assert.Nil(s.T(), err)
 | 
			
		||||
 | 
			
		||||
		log.Printf("Creating pre auth key for %s\n", namespace)
 | 
			
		||||
		preAuthResult, err := ExecuteCommand(
 | 
			
		||||
		preAuthResult, _, err := ExecuteCommand(
 | 
			
		||||
			&s.headscale,
 | 
			
		||||
			[]string{
 | 
			
		||||
				"headscale",
 | 
			
		||||
@ -386,7 +386,7 @@ func (s *IntegrationTestSuite) HandleStats(
 | 
			
		||||
func (s *IntegrationTestSuite) TestListNodes() {
 | 
			
		||||
	for namespace, scales := range s.namespaces {
 | 
			
		||||
		log.Println("Listing nodes")
 | 
			
		||||
		result, err := ExecuteCommand(
 | 
			
		||||
		result, _, err := ExecuteCommand(
 | 
			
		||||
			&s.headscale,
 | 
			
		||||
			[]string{"headscale", "--namespace", namespace, "nodes", "list"},
 | 
			
		||||
			[]string{},
 | 
			
		||||
@ -518,7 +518,7 @@ func (s *IntegrationTestSuite) TestPingAllPeersByAddress() {
 | 
			
		||||
								peername,
 | 
			
		||||
								ip,
 | 
			
		||||
							)
 | 
			
		||||
							result, err := ExecuteCommand(
 | 
			
		||||
							result, _, err := ExecuteCommand(
 | 
			
		||||
								&tailscale,
 | 
			
		||||
								command,
 | 
			
		||||
								[]string{},
 | 
			
		||||
@ -552,7 +552,7 @@ func (s *IntegrationTestSuite) TestTailDrop() {
 | 
			
		||||
 | 
			
		||||
		for hostname, tailscale := range scales.tailscales {
 | 
			
		||||
			command := []string{"touch", fmt.Sprintf("/tmp/file_from_%s", hostname)}
 | 
			
		||||
			_, err := ExecuteCommand(
 | 
			
		||||
			_, _, err := ExecuteCommand(
 | 
			
		||||
				&tailscale,
 | 
			
		||||
				command,
 | 
			
		||||
				[]string{},
 | 
			
		||||
@ -586,7 +586,7 @@ func (s *IntegrationTestSuite) TestTailDrop() {
 | 
			
		||||
							hostname,
 | 
			
		||||
							peername,
 | 
			
		||||
						)
 | 
			
		||||
						_, err := ExecuteCommand(
 | 
			
		||||
						_, _, err := ExecuteCommand(
 | 
			
		||||
							&tailscale,
 | 
			
		||||
							command,
 | 
			
		||||
							[]string{},
 | 
			
		||||
@ -606,7 +606,7 @@ func (s *IntegrationTestSuite) TestTailDrop() {
 | 
			
		||||
				"get",
 | 
			
		||||
				"/tmp/",
 | 
			
		||||
			}
 | 
			
		||||
			_, err := ExecuteCommand(
 | 
			
		||||
			_, _, err := ExecuteCommand(
 | 
			
		||||
				&tailscale,
 | 
			
		||||
				command,
 | 
			
		||||
				[]string{},
 | 
			
		||||
@ -628,7 +628,7 @@ func (s *IntegrationTestSuite) TestTailDrop() {
 | 
			
		||||
						peername,
 | 
			
		||||
						ip,
 | 
			
		||||
					)
 | 
			
		||||
					result, err := ExecuteCommand(
 | 
			
		||||
					result, _, err := ExecuteCommand(
 | 
			
		||||
						&tailscale,
 | 
			
		||||
						command,
 | 
			
		||||
						[]string{},
 | 
			
		||||
@ -672,7 +672,7 @@ func (s *IntegrationTestSuite) TestPingAllPeersByHostname() {
 | 
			
		||||
						hostname,
 | 
			
		||||
						peername,
 | 
			
		||||
					)
 | 
			
		||||
					result, err := ExecuteCommand(
 | 
			
		||||
					result, _, err := ExecuteCommand(
 | 
			
		||||
						&tailscale,
 | 
			
		||||
						command,
 | 
			
		||||
						[]string{},
 | 
			
		||||
@ -724,7 +724,7 @@ func (s *IntegrationTestSuite) TestMagicDNS() {
 | 
			
		||||
							peername,
 | 
			
		||||
							hostname,
 | 
			
		||||
						)
 | 
			
		||||
						result, err := ExecuteCommand(
 | 
			
		||||
						result, _, err := ExecuteCommand(
 | 
			
		||||
							&tailscale,
 | 
			
		||||
							command,
 | 
			
		||||
							[]string{},
 | 
			
		||||
@ -757,7 +757,7 @@ func getAPIURLs(
 | 
			
		||||
			"/run/tailscale/tailscaled.sock",
 | 
			
		||||
			"http://localhost/localapi/v0/file-targets",
 | 
			
		||||
		}
 | 
			
		||||
		result, err := ExecuteCommand(
 | 
			
		||||
		result, _, err := ExecuteCommand(
 | 
			
		||||
			&tailscale,
 | 
			
		||||
			command,
 | 
			
		||||
			[]string{},
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										506
									
								
								integration_oidc_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										506
									
								
								integration_oidc_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,506 @@
 | 
			
		||||
//go:build integration_oidc
 | 
			
		||||
 | 
			
		||||
package headscale
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"crypto/tls"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"testing"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/ory/dockertest/v3"
 | 
			
		||||
	"github.com/ory/dockertest/v3/docker"
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"github.com/stretchr/testify/suite"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	oidcHeadscaleHostname = "headscale"
 | 
			
		||||
	oidcNamespaceName     = "oidcnamespace"
 | 
			
		||||
	totalOidcContainers   = 3
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type IntegrationOIDCTestSuite struct {
 | 
			
		||||
	suite.Suite
 | 
			
		||||
	stats *suite.SuiteInformation
 | 
			
		||||
 | 
			
		||||
	pool      dockertest.Pool
 | 
			
		||||
	network   dockertest.Network
 | 
			
		||||
	headscale dockertest.Resource
 | 
			
		||||
	mockOidc  dockertest.Resource
 | 
			
		||||
	saveLogs  bool
 | 
			
		||||
 | 
			
		||||
	tailscales    map[string]dockertest.Resource
 | 
			
		||||
	joinWaitGroup sync.WaitGroup
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestOIDCIntegrationTestSuite(t *testing.T) {
 | 
			
		||||
	saveLogs, err := GetEnvBool("HEADSCALE_INTEGRATION_SAVE_LOG")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		saveLogs = false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	s := new(IntegrationOIDCTestSuite)
 | 
			
		||||
 | 
			
		||||
	s.tailscales = make(map[string]dockertest.Resource)
 | 
			
		||||
	s.saveLogs = saveLogs
 | 
			
		||||
 | 
			
		||||
	suite.Run(t, s)
 | 
			
		||||
 | 
			
		||||
	// HandleStats, which allows us to check if we passed and save logs
 | 
			
		||||
	// is called after TearDown, so we cannot tear down containers before
 | 
			
		||||
	// we have potentially saved the logs.
 | 
			
		||||
	if s.saveLogs {
 | 
			
		||||
		for _, tailscale := range s.tailscales {
 | 
			
		||||
			if err := s.pool.Purge(&tailscale); err != nil {
 | 
			
		||||
				log.Printf("Could not purge resource: %s\n", err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !s.stats.Passed() {
 | 
			
		||||
			err := s.saveLog(&s.headscale, "test_output")
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Printf("Could not save log: %s\n", err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := s.pool.Purge(&s.mockOidc); err != nil {
 | 
			
		||||
			log.Printf("Could not purge resource: %s\n", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := s.pool.Purge(&s.headscale); err != nil {
 | 
			
		||||
			t.Logf("Could not purge resource: %s\n", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := s.network.Close(); err != nil {
 | 
			
		||||
			log.Printf("Could not close network: %s\n", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *IntegrationOIDCTestSuite) SetupSuite() {
 | 
			
		||||
	if ppool, err := dockertest.NewPool(""); err == nil {
 | 
			
		||||
		s.pool = *ppool
 | 
			
		||||
	} else {
 | 
			
		||||
		s.FailNow(fmt.Sprintf("Could not connect to docker: %s", err), "")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if pnetwork, err := s.pool.CreateNetwork("headscale-test"); err == nil {
 | 
			
		||||
		s.network = *pnetwork
 | 
			
		||||
	} else {
 | 
			
		||||
		s.FailNow(fmt.Sprintf("Could not create network: %s", err), "")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Create does not give us an updated version of the resource, so we need to
 | 
			
		||||
	// get it again.
 | 
			
		||||
	networks, err := s.pool.NetworksByName("headscale-test")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		s.FailNow(fmt.Sprintf("Could not get network: %s", err), "")
 | 
			
		||||
	}
 | 
			
		||||
	s.network = networks[0]
 | 
			
		||||
 | 
			
		||||
	log.Printf("Network config: %v", s.network.Network.IPAM.Config[0])
 | 
			
		||||
 | 
			
		||||
	s.Suite.T().Log("Setting up mock OIDC")
 | 
			
		||||
	mockOidcOptions := &dockertest.RunOptions{
 | 
			
		||||
		Name:         "mockoidc",
 | 
			
		||||
		Hostname:     "mockoidc",
 | 
			
		||||
		Cmd:          []string{"headscale", "mockoidc"},
 | 
			
		||||
		ExposedPorts: []string{"10000/tcp"},
 | 
			
		||||
		Networks:     []*dockertest.Network{&s.network},
 | 
			
		||||
		PortBindings: map[docker.Port][]docker.PortBinding{
 | 
			
		||||
			"10000/tcp": {{HostPort: "10000"}},
 | 
			
		||||
		},
 | 
			
		||||
		Env: []string{
 | 
			
		||||
			"MOCKOIDC_PORT=10000",
 | 
			
		||||
			"MOCKOIDC_CLIENT_ID=superclient",
 | 
			
		||||
			"MOCKOIDC_CLIENT_SECRET=supersecret",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	headscaleBuildOptions := &dockertest.BuildOptions{
 | 
			
		||||
		Dockerfile: "Dockerfile.debug",
 | 
			
		||||
		ContextDir: ".",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if pmockoidc, err := s.pool.BuildAndRunWithBuildOptions(
 | 
			
		||||
		headscaleBuildOptions,
 | 
			
		||||
		mockOidcOptions,
 | 
			
		||||
		DockerRestartPolicy); err == nil {
 | 
			
		||||
		s.mockOidc = *pmockoidc
 | 
			
		||||
	} else {
 | 
			
		||||
		s.FailNow(fmt.Sprintf("Could not start mockOIDC container: %s", err), "")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	oidcCfg := fmt.Sprintf(`
 | 
			
		||||
oidc:
 | 
			
		||||
  issuer: http://%s:10000/oidc
 | 
			
		||||
  client_id: superclient
 | 
			
		||||
  client_secret: supersecret
 | 
			
		||||
  strip_email_domain: true`, s.mockOidc.GetIPInNetwork(&s.network))
 | 
			
		||||
 | 
			
		||||
	currentPath, err := os.Getwd()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		s.FailNow(fmt.Sprintf("Could not determine current path: %s", err), "")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	baseConfig, err := os.ReadFile(
 | 
			
		||||
		path.Join(currentPath, "integration_test/etc_oidc/base_config.yaml"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		s.FailNow(fmt.Sprintf("Could not read base config: %s", err), "")
 | 
			
		||||
	}
 | 
			
		||||
	config := string(baseConfig) + oidcCfg
 | 
			
		||||
 | 
			
		||||
	log.Println(config)
 | 
			
		||||
 | 
			
		||||
	configPath := path.Join(currentPath, "integration_test/etc_oidc/config.yaml")
 | 
			
		||||
	err = os.WriteFile(configPath, []byte(config), 0644)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		s.FailNow(fmt.Sprintf("Could not write config: %s", err), "")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	headscaleOptions := &dockertest.RunOptions{
 | 
			
		||||
		Name:     oidcHeadscaleHostname,
 | 
			
		||||
		Networks: []*dockertest.Network{&s.network},
 | 
			
		||||
		Mounts: []string{
 | 
			
		||||
			path.Join(currentPath,
 | 
			
		||||
				"integration_test/etc_oidc:/etc/headscale",
 | 
			
		||||
			),
 | 
			
		||||
		},
 | 
			
		||||
		Cmd:          []string{"headscale", "serve"},
 | 
			
		||||
		ExposedPorts: []string{"8443/tcp", "3478/udp"},
 | 
			
		||||
		PortBindings: map[docker.Port][]docker.PortBinding{
 | 
			
		||||
			"8443/tcp": {{HostPort: "8443"}},
 | 
			
		||||
			"3478/udp": {{HostPort: "3478"}},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = s.pool.RemoveContainerByName(oidcHeadscaleHostname)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		s.FailNow(
 | 
			
		||||
			fmt.Sprintf(
 | 
			
		||||
				"Could not remove existing container before building test: %s",
 | 
			
		||||
				err,
 | 
			
		||||
			),
 | 
			
		||||
			"",
 | 
			
		||||
		)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	s.Suite.T().Logf("Creating headscale container for OIDC integration tests")
 | 
			
		||||
	if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
 | 
			
		||||
		s.headscale = *pheadscale
 | 
			
		||||
	} else {
 | 
			
		||||
		s.FailNow(fmt.Sprintf("Could not start headscale container: %s", err), "")
 | 
			
		||||
	}
 | 
			
		||||
	s.Suite.T().Logf("Created headscale container for embedded OIDC tests")
 | 
			
		||||
 | 
			
		||||
	s.Suite.T().Logf("Creating tailscale containers for embedded OIDC tests")
 | 
			
		||||
 | 
			
		||||
	for i := 0; i < totalOidcContainers; i++ {
 | 
			
		||||
		version := tailscaleVersions[i%len(tailscaleVersions)]
 | 
			
		||||
		hostname, container := s.tailscaleContainer(
 | 
			
		||||
			fmt.Sprint(i),
 | 
			
		||||
			version,
 | 
			
		||||
		)
 | 
			
		||||
		s.tailscales[hostname] = *container
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	s.Suite.T().Logf("Waiting for headscale to be ready for embedded OIDC tests")
 | 
			
		||||
	hostEndpoint := fmt.Sprintf("localhost:%s", s.headscale.GetPort("8443/tcp"))
 | 
			
		||||
 | 
			
		||||
	if err := s.pool.Retry(func() error {
 | 
			
		||||
		url := fmt.Sprintf("https://%s/health", hostEndpoint)
 | 
			
		||||
		insecureTransport := http.DefaultTransport.(*http.Transport).Clone()
 | 
			
		||||
		insecureTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
 | 
			
		||||
		client := &http.Client{Transport: insecureTransport}
 | 
			
		||||
		resp, err := client.Get(url)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Printf("headscale for embedded OIDC tests is not ready: %s\n", err)
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if resp.StatusCode != http.StatusOK {
 | 
			
		||||
			return fmt.Errorf("status code not OK")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return nil
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		// TODO(kradalby): If we cannot access headscale, or any other fatal error during
 | 
			
		||||
		// test setup, we need to abort and tear down. However, testify does not seem to
 | 
			
		||||
		// support that at the moment:
 | 
			
		||||
		// https://github.com/stretchr/testify/issues/849
 | 
			
		||||
		return // fmt.Errorf("Could not connect to headscale: %s", err)
 | 
			
		||||
	}
 | 
			
		||||
	s.Suite.T().Log("headscale container is ready for embedded OIDC tests")
 | 
			
		||||
 | 
			
		||||
	s.Suite.T().Logf("Creating headscale namespace: %s\n", oidcNamespaceName)
 | 
			
		||||
	result, _, err := ExecuteCommand(
 | 
			
		||||
		&s.headscale,
 | 
			
		||||
		[]string{"headscale", "namespaces", "create", oidcNamespaceName},
 | 
			
		||||
		[]string{},
 | 
			
		||||
	)
 | 
			
		||||
	log.Println("headscale create namespace result: ", result)
 | 
			
		||||
	assert.Nil(s.T(), err)
 | 
			
		||||
 | 
			
		||||
	headscaleEndpoint := fmt.Sprintf(
 | 
			
		||||
		"https://headscale:%s",
 | 
			
		||||
		s.headscale.GetPort("8443/tcp"),
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	log.Printf(
 | 
			
		||||
		"Joining tailscale containers to headscale at %s\n",
 | 
			
		||||
		headscaleEndpoint,
 | 
			
		||||
	)
 | 
			
		||||
	for hostname, tailscale := range s.tailscales {
 | 
			
		||||
		s.joinWaitGroup.Add(1)
 | 
			
		||||
		go s.AuthenticateOIDC(headscaleEndpoint, hostname, tailscale)
 | 
			
		||||
 | 
			
		||||
		// TODO(juan): Workaround for https://github.com/juanfont/headscale/issues/814
 | 
			
		||||
		time.Sleep(1 * time.Second)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	s.joinWaitGroup.Wait()
 | 
			
		||||
 | 
			
		||||
	// The nodes need a bit of time to get their updated maps from headscale
 | 
			
		||||
	// TODO: See if we can have a more deterministic wait here.
 | 
			
		||||
	time.Sleep(60 * time.Second)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *IntegrationOIDCTestSuite) AuthenticateOIDC(
 | 
			
		||||
	endpoint, hostname string,
 | 
			
		||||
	tailscale dockertest.Resource,
 | 
			
		||||
) {
 | 
			
		||||
	defer s.joinWaitGroup.Done()
 | 
			
		||||
 | 
			
		||||
	loginURL, err := s.joinOIDC(endpoint, hostname, tailscale)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		s.FailNow(fmt.Sprintf("Could not join OIDC node: %s", err), "")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	insecureTransport := &http.Transport{
 | 
			
		||||
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
 | 
			
		||||
	}
 | 
			
		||||
	client := &http.Client{Transport: insecureTransport}
 | 
			
		||||
	resp, err := client.Get(loginURL.String())
 | 
			
		||||
	assert.Nil(s.T(), err)
 | 
			
		||||
 | 
			
		||||
	body, err := io.ReadAll(resp.Body)
 | 
			
		||||
	assert.Nil(s.T(), err)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		s.FailNow(fmt.Sprintf("Could not read login page: %s", err), "")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Printf("Login page for %s: %s", hostname, string(body))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *IntegrationOIDCTestSuite) joinOIDC(
 | 
			
		||||
	endpoint, hostname string,
 | 
			
		||||
	tailscale dockertest.Resource,
 | 
			
		||||
) (*url.URL, error) {
 | 
			
		||||
 | 
			
		||||
	command := []string{
 | 
			
		||||
		"tailscale",
 | 
			
		||||
		"up",
 | 
			
		||||
		"-login-server",
 | 
			
		||||
		endpoint,
 | 
			
		||||
		"--hostname",
 | 
			
		||||
		hostname,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Println("Join command:", command)
 | 
			
		||||
	log.Printf("Running join command for %s\n", hostname)
 | 
			
		||||
	_, stderr, _ := ExecuteCommand(
 | 
			
		||||
		&tailscale,
 | 
			
		||||
		command,
 | 
			
		||||
		[]string{},
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	// This piece of code just gets the login URL out of the stderr of the tailscale client.
 | 
			
		||||
	// See https://github.com/tailscale/tailscale/blob/main/cmd/tailscale/cli/up.go#L584.
 | 
			
		||||
	urlStr := strings.ReplaceAll(stderr, "\nTo authenticate, visit:\n\n\t", "")
 | 
			
		||||
	urlStr = strings.TrimSpace(urlStr)
 | 
			
		||||
 | 
			
		||||
	// parse URL
 | 
			
		||||
	loginUrl, err := url.Parse(urlStr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Printf("Could not parse login URL: %s", err)
 | 
			
		||||
		log.Printf("Original join command result: %s", stderr)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return loginUrl, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *IntegrationOIDCTestSuite) tailscaleContainer(
 | 
			
		||||
	identifier, version string,
 | 
			
		||||
) (string, *dockertest.Resource) {
 | 
			
		||||
	tailscaleBuildOptions := getDockerBuildOptions(version)
 | 
			
		||||
 | 
			
		||||
	hostname := fmt.Sprintf(
 | 
			
		||||
		"tailscale-%s-%s",
 | 
			
		||||
		strings.Replace(version, ".", "-", -1),
 | 
			
		||||
		identifier,
 | 
			
		||||
	)
 | 
			
		||||
	tailscaleOptions := &dockertest.RunOptions{
 | 
			
		||||
		Name:     hostname,
 | 
			
		||||
		Networks: []*dockertest.Network{&s.network},
 | 
			
		||||
		Cmd: []string{
 | 
			
		||||
			"tailscaled", "--tun=tsdev",
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		// expose the host IP address, so we can access it from inside the container
 | 
			
		||||
		ExtraHosts: []string{
 | 
			
		||||
			"host.docker.internal:host-gateway",
 | 
			
		||||
			"headscale:host-gateway",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pts, err := s.pool.BuildAndRunWithBuildOptions(
 | 
			
		||||
		tailscaleBuildOptions,
 | 
			
		||||
		tailscaleOptions,
 | 
			
		||||
		DockerRestartPolicy,
 | 
			
		||||
		DockerAllowLocalIPv6,
 | 
			
		||||
		DockerAllowNetworkAdministration,
 | 
			
		||||
	)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatalf("Could not start tailscale container version %s: %s", version, err)
 | 
			
		||||
	}
 | 
			
		||||
	log.Printf("Created %s container\n", hostname)
 | 
			
		||||
 | 
			
		||||
	return hostname, pts
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *IntegrationOIDCTestSuite) TearDownSuite() {
 | 
			
		||||
	if !s.saveLogs {
 | 
			
		||||
		for _, tailscale := range s.tailscales {
 | 
			
		||||
			if err := s.pool.Purge(&tailscale); err != nil {
 | 
			
		||||
				log.Printf("Could not purge resource: %s\n", err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := s.pool.Purge(&s.headscale); err != nil {
 | 
			
		||||
			log.Printf("Could not purge resource: %s\n", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := s.pool.Purge(&s.mockOidc); err != nil {
 | 
			
		||||
			log.Printf("Could not purge resource: %s\n", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := s.network.Close(); err != nil {
 | 
			
		||||
			log.Printf("Could not close network: %s\n", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *IntegrationOIDCTestSuite) HandleStats(
 | 
			
		||||
	suiteName string,
 | 
			
		||||
	stats *suite.SuiteInformation,
 | 
			
		||||
) {
 | 
			
		||||
	s.stats = stats
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *IntegrationOIDCTestSuite) saveLog(
 | 
			
		||||
	resource *dockertest.Resource,
 | 
			
		||||
	basePath string,
 | 
			
		||||
) error {
 | 
			
		||||
	err := os.MkdirAll(basePath, os.ModePerm)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var stdout bytes.Buffer
 | 
			
		||||
	var stderr bytes.Buffer
 | 
			
		||||
 | 
			
		||||
	err = s.pool.Client.Logs(
 | 
			
		||||
		docker.LogsOptions{
 | 
			
		||||
			Context:      context.TODO(),
 | 
			
		||||
			Container:    resource.Container.ID,
 | 
			
		||||
			OutputStream: &stdout,
 | 
			
		||||
			ErrorStream:  &stderr,
 | 
			
		||||
			Tail:         "all",
 | 
			
		||||
			RawTerminal:  false,
 | 
			
		||||
			Stdout:       true,
 | 
			
		||||
			Stderr:       true,
 | 
			
		||||
			Follow:       false,
 | 
			
		||||
			Timestamps:   false,
 | 
			
		||||
		},
 | 
			
		||||
	)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Printf("Saving logs for %s to %s\n", resource.Container.Name, basePath)
 | 
			
		||||
 | 
			
		||||
	err = os.WriteFile(
 | 
			
		||||
		path.Join(basePath, resource.Container.Name+".stdout.log"),
 | 
			
		||||
		[]byte(stdout.String()),
 | 
			
		||||
		0o644,
 | 
			
		||||
	)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = os.WriteFile(
 | 
			
		||||
		path.Join(basePath, resource.Container.Name+".stderr.log"),
 | 
			
		||||
		[]byte(stdout.String()),
 | 
			
		||||
		0o644,
 | 
			
		||||
	)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *IntegrationOIDCTestSuite) TestPingAllPeersByAddress() {
 | 
			
		||||
	for hostname, tailscale := range s.tailscales {
 | 
			
		||||
		ips, err := getIPs(s.tailscales)
 | 
			
		||||
		assert.Nil(s.T(), err)
 | 
			
		||||
		for peername, peerIPs := range ips {
 | 
			
		||||
			for i, ip := range peerIPs {
 | 
			
		||||
				// We currently cant ping ourselves, so skip that.
 | 
			
		||||
				if peername == hostname {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				s.T().
 | 
			
		||||
					Run(fmt.Sprintf("%s-%s-%d", hostname, peername, i), func(t *testing.T) {
 | 
			
		||||
						// We are only interested in "direct ping" which means what we
 | 
			
		||||
						// might need a couple of more attempts before reaching the node.
 | 
			
		||||
						command := []string{
 | 
			
		||||
							"tailscale", "ping",
 | 
			
		||||
							"--timeout=1s",
 | 
			
		||||
							"--c=10",
 | 
			
		||||
							"--until-direct=true",
 | 
			
		||||
							ip.String(),
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						log.Printf(
 | 
			
		||||
							"Pinging from %s to %s (%s)\n",
 | 
			
		||||
							hostname,
 | 
			
		||||
							peername,
 | 
			
		||||
							ip,
 | 
			
		||||
						)
 | 
			
		||||
						stdout, stderr, err := ExecuteCommand(
 | 
			
		||||
							&tailscale,
 | 
			
		||||
							command,
 | 
			
		||||
							[]string{},
 | 
			
		||||
						)
 | 
			
		||||
						assert.Nil(t, err)
 | 
			
		||||
						log.Printf("result for %s: stdout: %s, stderr: %s\n", hostname, stdout, stderr)
 | 
			
		||||
						assert.Contains(t, stdout, "pong")
 | 
			
		||||
					})
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								integration_test/etc_oidc/base_config.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								integration_test/etc_oidc/base_config.yaml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
			
		||||
log_level: trace
 | 
			
		||||
acl_policy_path: ""
 | 
			
		||||
db_type: sqlite3
 | 
			
		||||
ephemeral_node_inactivity_timeout: 30m
 | 
			
		||||
node_update_check_interval: 10s
 | 
			
		||||
ip_prefixes:
 | 
			
		||||
  - fd7a:115c:a1e0::/48
 | 
			
		||||
  - 100.64.0.0/10
 | 
			
		||||
db_path: /tmp/integration_test_db.sqlite3
 | 
			
		||||
private_key_path: private.key
 | 
			
		||||
noise:
 | 
			
		||||
  private_key_path: noise_private.key
 | 
			
		||||
listen_addr: 0.0.0.0:8443
 | 
			
		||||
server_url: https://localhost:8443
 | 
			
		||||
tls_cert_path: "/etc/headscale/tls/server.crt"
 | 
			
		||||
tls_key_path: "/etc/headscale/tls/server.key"
 | 
			
		||||
tls_client_auth_mode: disabled
 | 
			
		||||
derp:
 | 
			
		||||
  urls:
 | 
			
		||||
    - https://controlplane.tailscale.com/derpmap/default
 | 
			
		||||
  auto_update_enabled: true
 | 
			
		||||
  update_frequency: 1m
 | 
			
		||||
							
								
								
									
										22
									
								
								integration_test/etc_oidc/tls/server.crt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								integration_test/etc_oidc/tls/server.crt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
			
		||||
 | 
			
		||||
-----BEGIN CERTIFICATE-----
 | 
			
		||||
MIIC8jCCAdqgAwIBAgIULbu+UbSTMG/LtxooLLh7BgSEyqEwDQYJKoZIhvcNAQEL
 | 
			
		||||
BQAwFDESMBAGA1UEAwwJaGVhZHNjYWxlMCAXDTIyMDMwNTE2NDgwM1oYDzI1MjEx
 | 
			
		||||
MTA0MTY0ODAzWjAUMRIwEAYDVQQDDAloZWFkc2NhbGUwggEiMA0GCSqGSIb3DQEB
 | 
			
		||||
AQUAA4IBDwAwggEKAoIBAQDqcfpToLZUF0rlNwXkkt3lbyw4Cl4TJdx36o2PKaOK
 | 
			
		||||
U+tze/IjRsCWeMwrcR1o9TNZcxsD+c2J48D1WATuQJlMeg+2UJXGaTGRKkkbPMy3
 | 
			
		||||
5m7AFf/Q16UEOgm2NYjZaQ8faRGIMYURG/6sXmNeETJvBixpBev9yKJuVXgqHNS4
 | 
			
		||||
NpEkNwdOCuAZXrmw0HCbiusawJOay4tFvhH14rav8Uimonl8UTNVXufMzyUOuoaQ
 | 
			
		||||
TGflmzYX3hIoswRnTPlIWFoqObvx2Q8H+of3uQJXy0m8I6OrIoXLNxnqYMfFls79
 | 
			
		||||
9SYgVc2jPsCbh5fwyRbx2Hof7sIZ1K/mNgxJRG1E3ZiLAgMBAAGjOjA4MBQGA1Ud
 | 
			
		||||
EQQNMAuCCWhlYWRzY2FsZTALBgNVHQ8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUH
 | 
			
		||||
AwEwDQYJKoZIhvcNAQELBQADggEBANGlVN7NCsJaKz0k0nhlRGK+tcxn2p1PXN/i
 | 
			
		||||
Iy+JX8ahixPC4ocRwOhrXgb390ZXLLwq08HrWYRB/Wi1VUzCp5d8dVxvrR43dJ+v
 | 
			
		||||
L2EOBiIKgcu2C3pWW1qRR46/EoXUU9kSH2VNBvIhNufi32kEOidoDzxtQf6qVCoF
 | 
			
		||||
guUt1JkAqrynv1UvR/2ZRM/WzM/oJ8qfECwrwDxyYhkqU5Z5jCWg0C6kPIBvNdzt
 | 
			
		||||
B0eheWS+ZxVwkePTR4e17kIafwknth3lo+orxVrq/xC+OVM1bGrt2ZyD64ZvEqQl
 | 
			
		||||
w6kgbzBdLScAQptWOFThwhnJsg0UbYKimZsnYmjVEuN59TJv92M=
 | 
			
		||||
-----END CERTIFICATE-----
 | 
			
		||||
 | 
			
		||||
(Expires on Nov  4 16:48:03 2521 GMT)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										28
									
								
								integration_test/etc_oidc/tls/server.key
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								integration_test/etc_oidc/tls/server.key
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
			
		||||
-----BEGIN PRIVATE KEY-----
 | 
			
		||||
MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDqcfpToLZUF0rl
 | 
			
		||||
NwXkkt3lbyw4Cl4TJdx36o2PKaOKU+tze/IjRsCWeMwrcR1o9TNZcxsD+c2J48D1
 | 
			
		||||
WATuQJlMeg+2UJXGaTGRKkkbPMy35m7AFf/Q16UEOgm2NYjZaQ8faRGIMYURG/6s
 | 
			
		||||
XmNeETJvBixpBev9yKJuVXgqHNS4NpEkNwdOCuAZXrmw0HCbiusawJOay4tFvhH1
 | 
			
		||||
4rav8Uimonl8UTNVXufMzyUOuoaQTGflmzYX3hIoswRnTPlIWFoqObvx2Q8H+of3
 | 
			
		||||
uQJXy0m8I6OrIoXLNxnqYMfFls799SYgVc2jPsCbh5fwyRbx2Hof7sIZ1K/mNgxJ
 | 
			
		||||
RG1E3ZiLAgMBAAECggEBALu1Ni/u5Qy++YA8ZcN0s6UXNdhItLmv/q0kZuLQ+9et
 | 
			
		||||
CT8VZfFInLndTdsaXenDKLHdryunviFA8SV+q7P2lMbek+Xs735EiyMnMBFWxLIZ
 | 
			
		||||
FWNGOeQERGL19QCmLEOmEi2b+iWJQHlKaMWpbPXL3w11a+lKjIBNO4ALfoJ5QveZ
 | 
			
		||||
cGMKsJdm/mpqBvLeNeh2eAFk3Gp6sT1g80Ge8NkgyzFBNIqnut0eerM15kPTc6Qz
 | 
			
		||||
12JLaOXUuV3PrcB4PN4nOwrTDg88GDNOQtc1Pc9r4nOHyLfr8X7QEtj1wXSwmOuK
 | 
			
		||||
d6ynMnAmoxVA9wEnupLbil1bzohRzpsTpkmDruYaBEECgYEA/Z09I8D6mt2NVqIE
 | 
			
		||||
KyvLjBK39ijSV9r3/lvB2Ple2OOL5YQEd+yTrIFy+3zdUnDgD1zmNnXjmjvHZ9Lc
 | 
			
		||||
IFf2o06AF84QLNB5gLPdDQkGNFdDqUxljBrfAfE3oANmPS/B0SijMGOOOiDO2FtO
 | 
			
		||||
xl1nfRr78mswuRs9awoUWCdNRKUCgYEA7KaTYKIQW/FEjw9lshp74q5vbn6zoXF5
 | 
			
		||||
7N8VkwI+bBVNvRbM9XZ8qhfgRdu9eXs5oL/N4mSYY54I8fA//pJ0Z2vpmureMm1V
 | 
			
		||||
mL5WBUmSD9DIbAchoK+sRiQhVmNMBQC6cHMABA7RfXvBeGvWrm9pKCS6ZLgLjkjp
 | 
			
		||||
PsmAcaXQcW8CgYEA2inAxljjOwUK6FNGsrxhxIT1qtNC3kCGxE+6WSNq67gSR8Vg
 | 
			
		||||
8qiX//T7LEslOB3RIGYRwxd2St7RkgZZRZllmOWWWuPwFhzf6E7RAL2akLvggGov
 | 
			
		||||
kG4tGEagSw2hjVDfsUT73ExHtMk0Jfmlsg33UC8+PDLpHtLH6qQpDAwC8+ECgYEA
 | 
			
		||||
o+AqOIWhvHmT11l7O915Ip1WzvZwYADbxLsrDnVEUsZh4epTHjvh0kvcY6PqTqCV
 | 
			
		||||
ZIrOANNWb811Nkz/k8NJVoD08PFp0xPBbZeIq/qpachTsfMyRzq/mobUiyUR9Hjv
 | 
			
		||||
ooUQYr78NOApNsG+lWbTNBhS9wI4BlzZIECbcJe5g4MCgYEAndRoy8S+S0Hx/S8a
 | 
			
		||||
O3hzXeDmivmgWqn8NVD4AKOovpkz4PaIVVQbAQkiNfAx8/DavPvjEKAbDezJ4ECV
 | 
			
		||||
j7IsOWtDVI7pd6eF9fTcECwisrda8aUoiOap8AQb48153Vx+g2N4Vy3uH0xJs4cz
 | 
			
		||||
TDALZPOBg8VlV+HEFDP43sp9Bf0=
 | 
			
		||||
-----END PRIVATE KEY-----
 | 
			
		||||
@ -573,12 +573,11 @@ func (machines MachinesP) String() string {
 | 
			
		||||
func (machines Machines) toNodes(
 | 
			
		||||
	baseDomain string,
 | 
			
		||||
	dnsConfig *tailcfg.DNSConfig,
 | 
			
		||||
	includeRoutes bool,
 | 
			
		||||
) ([]*tailcfg.Node, error) {
 | 
			
		||||
	nodes := make([]*tailcfg.Node, len(machines))
 | 
			
		||||
 | 
			
		||||
	for index, machine := range machines {
 | 
			
		||||
		node, err := machine.toNode(baseDomain, dnsConfig, includeRoutes)
 | 
			
		||||
		node, err := machine.toNode(baseDomain, dnsConfig)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
@ -594,7 +593,6 @@ func (machines Machines) toNodes(
 | 
			
		||||
func (machine Machine) toNode(
 | 
			
		||||
	baseDomain string,
 | 
			
		||||
	dnsConfig *tailcfg.DNSConfig,
 | 
			
		||||
	includeRoutes bool,
 | 
			
		||||
) (*tailcfg.Node, error) {
 | 
			
		||||
	var nodeKey key.NodePublic
 | 
			
		||||
	err := nodeKey.UnmarshalText([]byte(NodePublicKeyEnsurePrefix(machine.NodeKey)))
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user