mirror of
https://github.com/juanfont/headscale.git
synced 2025-08-24 13:46:53 +02:00
Corrected existing integration tests. New integration tests added
This commit is contained in:
parent
eeed3ab4f5
commit
3aa93bc7f4
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)
|
||||
}
|
@ -390,6 +390,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
|
||||
|
@ -254,7 +254,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
|
||||
}
|
||||
|
@ -176,7 +176,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)
|
||||
}
|
||||
@ -279,7 +279,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)
|
||||
}
|
||||
@ -369,7 +369,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)
|
||||
}
|
||||
|
@ -259,6 +259,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
|
||||
@ -720,6 +727,7 @@ func (t *HeadscaleInContainer) CreateUser(
|
||||
func (t *HeadscaleInContainer) CreateAuthKey(
|
||||
user string,
|
||||
reusable bool,
|
||||
preApproved bool,
|
||||
ephemeral bool,
|
||||
) (*v1.PreAuthKey, error) {
|
||||
command := []string{
|
||||
@ -738,6 +746,10 @@ func (t *HeadscaleInContainer) CreateAuthKey(
|
||||
command = append(command, "--reusable")
|
||||
}
|
||||
|
||||
if preApproved {
|
||||
command = append(command, "--pre-approved")
|
||||
}
|
||||
|
||||
if ephemeral {
|
||||
command = append(command, "--ephemeral")
|
||||
}
|
||||
|
@ -301,10 +301,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)
|
||||
}
|
||||
@ -513,7 +514,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)
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ type TailscaleClient interface {
|
||||
DebugDERPRegion(region string) (*ipnstate.DebugDERPRegionReport, error)
|
||||
Netcheck() (*netcheck.Report, error)
|
||||
WaitForNeedsLogin() error
|
||||
WaitForNeedsApprove() error
|
||||
WaitForRunning() error
|
||||
WaitForPeers(expected int) error
|
||||
Ping(hostnameOrIP string, opts ...tsic.PingOption) error
|
||||
|
@ -848,6 +848,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