fix webauth + autoapprove routes (#2528)

* types/node: add helper funcs for node tags

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* types/node: add DebugString method for node

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* policy/v2: add String func to AutoApprover interface

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* policy/v2: simplify, use slices.Contains

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* policy/v2: debug, use nodes.DebugString

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* policy/v1: fix potential nil pointer in NodeCanApproveRoute

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* policy/v1: slices.Contains

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* integration/tsic: fix diff in login commands

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* integration: fix webauth running with wrong scenario

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* integration: move common oidc opts to func

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* integration: require node count, more verbose

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* auth: remove uneffective route approve

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* .github/workflows: fmt

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* integration/tsic: add id func

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* integration: remove call that might be nil

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* integration: test autoapprovers against web/authkey x group/tag/user

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* integration: unique network id per scenario

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* Revert "integration: move common oidc opts to func"

This reverts commit 7e9d165d4a900c304f1083b665f1a24a26e06e55.

* remove cmd

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* integration: clean docker images between runs in ci

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* integration: run autoapprove test against differnt policy modes

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* integration/tsic: append, not overrwrite extra login args

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* .github/workflows: remove polv2

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
Kristoffer Dalby 2025-04-30 08:54:04 +03:00 committed by GitHub
parent 57861507ab
commit f1206328dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 732 additions and 401 deletions

View File

@ -7,6 +7,8 @@ import (
"os" "os"
"sync" "sync"
"slices"
"github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/types"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
@ -145,13 +147,7 @@ func (pm *PolicyManager) NodeCanHaveTag(node *types.Node, tag string) bool {
tags, invalid := pm.pol.TagsOfNode(pm.users, node) tags, invalid := pm.pol.TagsOfNode(pm.users, node)
log.Debug().Strs("authorised_tags", tags).Strs("unauthorised_tags", invalid).Uint64("node.id", node.ID.Uint64()).Msg("tags provided by policy") log.Debug().Strs("authorised_tags", tags).Strs("unauthorised_tags", invalid).Uint64("node.id", node.ID.Uint64()).Msg("tags provided by policy")
for _, t := range tags { return slices.Contains(tags, tag)
if t == tag {
return true
}
}
return false
} }
func (pm *PolicyManager) NodeCanApproveRoute(node *types.Node, route netip.Prefix) bool { func (pm *PolicyManager) NodeCanApproveRoute(node *types.Node, route netip.Prefix) bool {
@ -174,7 +170,7 @@ func (pm *PolicyManager) NodeCanApproveRoute(node *types.Node, route netip.Prefi
} }
// approvedIPs should contain all of node's IPs if it matches the rule, so check for first // approvedIPs should contain all of node's IPs if it matches the rule, so check for first
if ips.Contains(*node.IPv4) { if ips != nil && ips.Contains(*node.IPv4) {
return true return true
} }
} }

View File

@ -7,6 +7,8 @@ import (
"strings" "strings"
"sync" "sync"
"slices"
"github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/types"
"go4.org/netipx" "go4.org/netipx"
"tailscale.com/net/tsaddr" "tailscale.com/net/tsaddr"
@ -174,10 +176,8 @@ func (pm *PolicyManager) NodeCanHaveTag(node *types.Node, tag string) bool {
defer pm.mu.Unlock() defer pm.mu.Unlock()
if ips, ok := pm.tagOwnerMap[Tag(tag)]; ok { if ips, ok := pm.tagOwnerMap[Tag(tag)]; ok {
for _, nodeAddr := range node.IPs() { if slices.ContainsFunc(node.IPs(), ips.Contains) {
if ips.Contains(nodeAddr) { return true
return true
}
} }
} }
@ -196,10 +196,8 @@ func (pm *PolicyManager) NodeCanApproveRoute(node *types.Node, route netip.Prefi
// where there is an exact entry, e.g. 10.0.0.0/8, then // where there is an exact entry, e.g. 10.0.0.0/8, then
// check and return quickly // check and return quickly
if _, ok := pm.autoApproveMap[route]; ok { if _, ok := pm.autoApproveMap[route]; ok {
for _, nodeAddr := range node.IPs() { if slices.ContainsFunc(node.IPs(), pm.autoApproveMap[route].Contains) {
if pm.autoApproveMap[route].Contains(nodeAddr) { return true
return true
}
} }
} }
@ -220,10 +218,8 @@ func (pm *PolicyManager) NodeCanApproveRoute(node *types.Node, route netip.Prefi
// Check if prefix is larger (so containing) and then overlaps // Check if prefix is larger (so containing) and then overlaps
// the route to see if the node can approve a subset of an autoapprover // the route to see if the node can approve a subset of an autoapprover
if prefix.Bits() <= route.Bits() && prefix.Overlaps(route) { if prefix.Bits() <= route.Bits() && prefix.Overlaps(route) {
for _, nodeAddr := range node.IPs() { if slices.ContainsFunc(node.IPs(), approveAddrs.Contains) {
if approveAddrs.Contains(nodeAddr) { return true
return true
}
} }
} }
} }
@ -279,5 +275,8 @@ func (pm *PolicyManager) DebugString() string {
} }
} }
sb.WriteString("\n\n")
sb.WriteString(pm.nodes.DebugString())
return sb.String() return sb.String()
} }

View File

@ -162,6 +162,10 @@ func (g Group) CanBeAutoApprover() bool {
return true return true
} }
func (g Group) String() string {
return string(g)
}
func (g Group) Resolve(p *Policy, users types.Users, nodes types.Nodes) (*netipx.IPSet, error) { func (g Group) Resolve(p *Policy, users types.Users, nodes types.Nodes) (*netipx.IPSet, error) {
var ips netipx.IPSetBuilder var ips netipx.IPSetBuilder
var errs []error var errs []error
@ -235,6 +239,10 @@ func (t Tag) CanBeAutoApprover() bool {
return true return true
} }
func (t Tag) String() string {
return string(t)
}
// Host is a string that represents a hostname. // Host is a string that represents a hostname.
type Host string type Host string
@ -590,6 +598,7 @@ func unmarshalPointer[T any](
type AutoApprover interface { type AutoApprover interface {
CanBeAutoApprover() bool CanBeAutoApprover() bool
UnmarshalJSON([]byte) error UnmarshalJSON([]byte) error
String() string
} }
type AutoApprovers []AutoApprover type AutoApprovers []AutoApprover

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"net/netip" "net/netip"
"slices" "slices"
"sort"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -194,19 +195,26 @@ func (node *Node) IsTagged() bool {
// Currently, this function only handles tags set // Currently, this function only handles tags set
// via CLI ("forced tags" and preauthkeys) // via CLI ("forced tags" and preauthkeys)
func (node *Node) HasTag(tag string) bool { func (node *Node) HasTag(tag string) bool {
if slices.Contains(node.ForcedTags, tag) { return slices.Contains(node.Tags(), tag)
return true }
}
if node.AuthKey != nil && slices.Contains(node.AuthKey.Tags, tag) { func (node *Node) Tags() []string {
return true var tags []string
if node.AuthKey != nil {
tags = append(tags, node.AuthKey.Tags...)
} }
// TODO(kradalby): Figure out how tagging should work // TODO(kradalby): Figure out how tagging should work
// and hostinfo.requestedtags. // and hostinfo.requestedtags.
// Do this in other work. // Do this in other work.
// #2417
return false tags = append(tags, node.ForcedTags...)
sort.Strings(tags)
tags = slices.Compact(tags)
return tags
} }
func (node *Node) RequestTags() []string { func (node *Node) RequestTags() []string {
@ -549,3 +557,25 @@ func (nodes Nodes) IDMap() map[NodeID]*Node {
return ret return ret
} }
func (nodes Nodes) DebugString() string {
var sb strings.Builder
sb.WriteString("Nodes:\n")
for _, node := range nodes {
sb.WriteString(node.DebugString())
sb.WriteString("\n")
}
return sb.String()
}
func (node Node) DebugString() string {
var sb strings.Builder
fmt.Fprintf(&sb, "%s(%s):\n", node.Hostname, node.ID)
fmt.Fprintf(&sb, "\tUser: %s (%d, %q)\n", node.User.Display(), node.User.ID, node.User.Username())
fmt.Fprintf(&sb, "\tTags: %v\n", node.Tags())
fmt.Fprintf(&sb, "\tIPs: %v\n", node.IPs())
fmt.Fprintf(&sb, "\tApprovedRoutes: %v\n", node.ApprovedRoutes)
fmt.Fprintf(&sb, "\tSubnetRoutes: %v\n", node.SubnetRoutes())
sb.WriteString("\n")
return sb.String()
}

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"net/netip" "net/netip"
"net/url" "net/url"
"os"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
@ -173,3 +174,15 @@ func ParseTraceroute(output string) (Traceroute, error) {
return result, nil return result, nil
} }
func IsCI() bool {
if _, ok := os.LookupEnv("CI"); ok {
return true
}
if _, ok := os.LookupEnv("GITHUB_RUN_ID"); ok {
return true
}
return false
}

View File

@ -1054,7 +1054,7 @@ func TestPolicyUpdateWhileRunningWithCLIInDatabase(t *testing.T) {
// Initially all nodes can reach each other // Initially all nodes can reach each other
for _, client := range all { for _, client := range all {
for _, peer := range all { for _, peer := range all {
if client.ID() == peer.ID() { if client.ContainerID() == peer.ContainerID() {
continue continue
} }

View File

@ -442,7 +442,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
assertNoErr(t, err) assertNoErr(t, err)
assert.Len(t, listUsers, 0) assert.Len(t, listUsers, 0)
ts, err := scenario.CreateTailscaleNode("unstable", tsic.WithNetwork(scenario.networks[TestDefaultNetwork])) ts, err := scenario.CreateTailscaleNode("unstable", tsic.WithNetwork(scenario.networks[scenario.testDefaultNetwork]))
assertNoErr(t, err) assertNoErr(t, err)
u, err := ts.LoginWithURL(headscale.GetEndpoint()) u, err := ts.LoginWithURL(headscale.GetEndpoint())

View File

@ -26,7 +26,7 @@ func TestAuthWebFlowAuthenticationPingAll(t *testing.T) {
} }
defer scenario.ShutdownAssertNoPanics(t) defer scenario.ShutdownAssertNoPanics(t)
err = scenario.CreateHeadscaleEnv( err = scenario.CreateHeadscaleEnvWithLoginURL(
nil, nil,
hsic.WithTestName("webauthping"), hsic.WithTestName("webauthping"),
hsic.WithEmbeddedDERPServerOnly(), hsic.WithEmbeddedDERPServerOnly(),
@ -66,7 +66,7 @@ func TestAuthWebFlowLogoutAndRelogin(t *testing.T) {
assertNoErr(t, err) assertNoErr(t, err)
defer scenario.ShutdownAssertNoPanics(t) defer scenario.ShutdownAssertNoPanics(t)
err = scenario.CreateHeadscaleEnv( err = scenario.CreateHeadscaleEnvWithLoginURL(
nil, nil,
hsic.WithTestName("weblogout"), hsic.WithTestName("weblogout"),
hsic.WithTLS(), hsic.WithTLS(),

View File

@ -6,6 +6,7 @@ import (
"log" "log"
"net" "net"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/ory/dockertest/v3" "github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker" "github.com/ory/dockertest/v3/docker"
) )
@ -105,3 +106,23 @@ func CleanUnreferencedNetworks(pool *dockertest.Pool) error {
return nil return nil
} }
// CleanImagesInCI removes images if running in CI.
func CleanImagesInCI(pool *dockertest.Pool) error {
if !util.IsCI() {
log.Println("Skipping image cleanup outside of CI")
return nil
}
images, err := pool.Client.ListImages(docker.ListImagesOptions{})
if err != nil {
return fmt.Errorf("getting images: %w", err)
}
for _, image := range images {
log.Printf("removing image: %s, %v", image.ID, image.RepoTags)
_ = pool.Client.RemoveImage(image.ID)
}
return nil
}

View File

@ -138,7 +138,7 @@ func testEphemeralWithOptions(t *testing.T, opts ...hsic.Option) {
t.Fatalf("failed to create user %s: %s", userName, err) t.Fatalf("failed to create user %s: %s", userName, err)
} }
err = scenario.CreateTailscaleNodesInUser(userName, "all", spec.NodesPerUser, tsic.WithNetwork(scenario.networks[TestDefaultNetwork])) err = scenario.CreateTailscaleNodesInUser(userName, "all", spec.NodesPerUser, tsic.WithNetwork(scenario.networks[scenario.testDefaultNetwork]))
if err != nil { if err != nil {
t.Fatalf("failed to create tailscale nodes in user %s: %s", userName, err) t.Fatalf("failed to create tailscale nodes in user %s: %s", userName, err)
} }
@ -216,7 +216,7 @@ func TestEphemeral2006DeletedTooQuickly(t *testing.T) {
t.Fatalf("failed to create user %s: %s", userName, err) t.Fatalf("failed to create user %s: %s", userName, err)
} }
err = scenario.CreateTailscaleNodesInUser(userName, "all", spec.NodesPerUser, tsic.WithNetwork(scenario.networks[TestDefaultNetwork])) err = scenario.CreateTailscaleNodesInUser(userName, "all", spec.NodesPerUser, tsic.WithNetwork(scenario.networks[scenario.testDefaultNetwork]))
if err != nil { if err != nil {
t.Fatalf("failed to create tailscale nodes in user %s: %s", userName, err) t.Fatalf("failed to create tailscale nodes in user %s: %s", userName, err)
} }

File diff suppressed because it is too large Load Diff

View File

@ -109,6 +109,9 @@ type Scenario struct {
spec ScenarioSpec spec ScenarioSpec
userToNetwork map[string]*dockertest.Network userToNetwork map[string]*dockertest.Network
testHashPrefix string
testDefaultNetwork string
} }
// ScenarioSpec describes the users, nodes, and network topology to // ScenarioSpec describes the users, nodes, and network topology to
@ -150,11 +153,8 @@ type ScenarioSpec struct {
MaxWait time.Duration MaxWait time.Duration
} }
var TestHashPrefix = "hs-" + util.MustGenerateRandomStringDNSSafe(scenarioHashLength) func (s *Scenario) prefixedNetworkName(name string) string {
var TestDefaultNetwork = TestHashPrefix + "-default" return s.testHashPrefix + "-" + name
func prefixedNetworkName(name string) string {
return TestHashPrefix + "-" + name
} }
// NewScenario creates a test Scenario which can be used to bootstraps a ControlServer with // NewScenario creates a test Scenario which can be used to bootstraps a ControlServer with
@ -169,6 +169,7 @@ func NewScenario(spec ScenarioSpec) (*Scenario, error) {
// This might be a no op, but it is worth a try as we sometime // This might be a no op, but it is worth a try as we sometime
// dont clean up nicely after ourselves. // dont clean up nicely after ourselves.
dockertestutil.CleanUnreferencedNetworks(pool) dockertestutil.CleanUnreferencedNetworks(pool)
dockertestutil.CleanImagesInCI(pool)
if spec.MaxWait == 0 { if spec.MaxWait == 0 {
pool.MaxWait = dockertestMaxWait() pool.MaxWait = dockertestMaxWait()
@ -176,18 +177,22 @@ func NewScenario(spec ScenarioSpec) (*Scenario, error) {
pool.MaxWait = spec.MaxWait pool.MaxWait = spec.MaxWait
} }
testHashPrefix := "hs-" + util.MustGenerateRandomStringDNSSafe(scenarioHashLength)
s := &Scenario{ s := &Scenario{
controlServers: xsync.NewMapOf[string, ControlServer](), controlServers: xsync.NewMapOf[string, ControlServer](),
users: make(map[string]*User), users: make(map[string]*User),
pool: pool, pool: pool,
spec: spec, spec: spec,
testHashPrefix: testHashPrefix,
testDefaultNetwork: testHashPrefix + "-default",
} }
var userToNetwork map[string]*dockertest.Network var userToNetwork map[string]*dockertest.Network
if spec.Networks != nil || len(spec.Networks) != 0 { if spec.Networks != nil || len(spec.Networks) != 0 {
for name, users := range s.spec.Networks { for name, users := range s.spec.Networks {
networkName := TestHashPrefix + "-" + name networkName := testHashPrefix + "-" + name
network, err := s.AddNetwork(networkName) network, err := s.AddNetwork(networkName)
if err != nil { if err != nil {
return nil, err return nil, err
@ -201,7 +206,7 @@ func NewScenario(spec ScenarioSpec) (*Scenario, error) {
} }
} }
} else { } else {
_, err := s.AddNetwork(TestDefaultNetwork) _, err := s.AddNetwork(s.testDefaultNetwork)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -213,7 +218,7 @@ func NewScenario(spec ScenarioSpec) (*Scenario, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
mak.Set(&s.extraServices, prefixedNetworkName(network), append(s.extraServices[prefixedNetworkName(network)], svc)) mak.Set(&s.extraServices, s.prefixedNetworkName(network), append(s.extraServices[s.prefixedNetworkName(network)], svc))
} }
} }
@ -261,7 +266,7 @@ func (s *Scenario) Networks() []*dockertest.Network {
} }
func (s *Scenario) Network(name string) (*dockertest.Network, error) { func (s *Scenario) Network(name string) (*dockertest.Network, error) {
net, ok := s.networks[prefixedNetworkName(name)] net, ok := s.networks[s.prefixedNetworkName(name)]
if !ok { if !ok {
return nil, fmt.Errorf("no network named: %s", name) return nil, fmt.Errorf("no network named: %s", name)
} }
@ -270,7 +275,7 @@ func (s *Scenario) Network(name string) (*dockertest.Network, error) {
} }
func (s *Scenario) SubnetOfNetwork(name string) (*netip.Prefix, error) { func (s *Scenario) SubnetOfNetwork(name string) (*netip.Prefix, error) {
net, ok := s.networks[prefixedNetworkName(name)] net, ok := s.networks[s.prefixedNetworkName(name)]
if !ok { if !ok {
return nil, fmt.Errorf("no network named: %s", name) return nil, fmt.Errorf("no network named: %s", name)
} }
@ -288,7 +293,7 @@ func (s *Scenario) SubnetOfNetwork(name string) (*netip.Prefix, error) {
} }
func (s *Scenario) Services(name string) ([]*dockertest.Resource, error) { func (s *Scenario) Services(name string) ([]*dockertest.Resource, error) {
res, ok := s.extraServices[prefixedNetworkName(name)] res, ok := s.extraServices[s.prefixedNetworkName(name)]
if !ok { if !ok {
return nil, fmt.Errorf("no network named: %s", name) return nil, fmt.Errorf("no network named: %s", name)
} }
@ -298,6 +303,7 @@ func (s *Scenario) Services(name string) ([]*dockertest.Resource, error) {
func (s *Scenario) ShutdownAssertNoPanics(t *testing.T) { func (s *Scenario) ShutdownAssertNoPanics(t *testing.T) {
defer dockertestutil.CleanUnreferencedNetworks(s.pool) defer dockertestutil.CleanUnreferencedNetworks(s.pool)
defer dockertestutil.CleanImagesInCI(s.pool)
s.controlServers.Range(func(_ string, control ControlServer) bool { s.controlServers.Range(func(_ string, control ControlServer) bool {
stdoutPath, stderrPath, err := control.Shutdown() stdoutPath, stderrPath, err := control.Shutdown()
@ -493,8 +499,7 @@ func (s *Scenario) CreateTailscaleNode(
) )
if err != nil { if err != nil {
return nil, fmt.Errorf( return nil, fmt.Errorf(
"failed to create tailscale (%s) node: %w", "failed to create tailscale node: %w",
tsClient.Hostname(),
err, err,
) )
} }
@ -707,7 +712,7 @@ func (s *Scenario) createHeadscaleEnv(
if s.userToNetwork != nil { if s.userToNetwork != nil {
opts = append(tsOpts, tsic.WithNetwork(s.userToNetwork[user])) opts = append(tsOpts, tsic.WithNetwork(s.userToNetwork[user]))
} else { } else {
opts = append(tsOpts, tsic.WithNetwork(s.networks[TestDefaultNetwork])) opts = append(tsOpts, tsic.WithNetwork(s.networks[s.testDefaultNetwork]))
} }
err = s.CreateTailscaleNodesInUser(user, "all", s.spec.NodesPerUser, opts...) err = s.CreateTailscaleNodesInUser(user, "all", s.spec.NodesPerUser, opts...)
@ -1181,7 +1186,7 @@ func Webservice(s *Scenario, networkName string) (*dockertest.Resource, error) {
hostname := fmt.Sprintf("hs-webservice-%s", hash) hostname := fmt.Sprintf("hs-webservice-%s", hash)
network, ok := s.networks[prefixedNetworkName(networkName)] network, ok := s.networks[s.prefixedNetworkName(networkName)]
if !ok { if !ok {
return nil, fmt.Errorf("network does not exist: %s", networkName) return nil, fmt.Errorf("network does not exist: %s", networkName)
} }

View File

@ -111,7 +111,7 @@ func TestTailscaleNodesJoiningHeadcale(t *testing.T) {
}) })
t.Run("create-tailscale", func(t *testing.T) { t.Run("create-tailscale", func(t *testing.T) {
err := scenario.CreateTailscaleNodesInUser(user, "unstable", count, tsic.WithNetwork(scenario.networks[TestDefaultNetwork])) err := scenario.CreateTailscaleNodesInUser(user, "unstable", count, tsic.WithNetwork(scenario.networks[scenario.testDefaultNetwork]))
if err != nil { if err != nil {
t.Fatalf("failed to add tailscale nodes: %s", err) t.Fatalf("failed to add tailscale nodes: %s", err)
} }

View File

@ -410,7 +410,7 @@ func assertSSHHostname(t *testing.T, client TailscaleClient, peer TailscaleClien
result, _, err := doSSH(t, client, peer) result, _, err := doSSH(t, client, peer)
assertNoErr(t, err) assertNoErr(t, err)
assertContains(t, peer.ID(), strings.ReplaceAll(result, "\n", "")) assertContains(t, peer.ContainerID(), strings.ReplaceAll(result, "\n", ""))
} }
func assertSSHPermissionDenied(t *testing.T, client TailscaleClient, peer TailscaleClient) { func assertSSHPermissionDenied(t *testing.T, client TailscaleClient, peer TailscaleClient) {

View File

@ -5,6 +5,7 @@ import (
"net/netip" "net/netip"
"net/url" "net/url"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util" "github.com/juanfont/headscale/hscontrol/util"
"github.com/juanfont/headscale/integration/dockertestutil" "github.com/juanfont/headscale/integration/dockertestutil"
"github.com/juanfont/headscale/integration/tsic" "github.com/juanfont/headscale/integration/tsic"
@ -43,7 +44,8 @@ type TailscaleClient interface {
Ping(hostnameOrIP string, opts ...tsic.PingOption) error Ping(hostnameOrIP string, opts ...tsic.PingOption) error
Curl(url string, opts ...tsic.CurlOption) (string, error) Curl(url string, opts ...tsic.CurlOption) (string, error)
Traceroute(netip.Addr) (util.Traceroute, error) Traceroute(netip.Addr) (util.Traceroute, error)
ID() string ContainerID() string
MustID() types.NodeID
ReadFile(path string) ([]byte, error) ReadFile(path string) ([]byte, error)
// FailingPeersAsString returns a formatted-ish multi-line-string of peers in the client // FailingPeersAsString returns a formatted-ish multi-line-string of peers in the client

View File

@ -18,6 +18,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util" "github.com/juanfont/headscale/hscontrol/util"
"github.com/juanfont/headscale/integration/dockertestutil" "github.com/juanfont/headscale/integration/dockertestutil"
"github.com/juanfont/headscale/integration/integrationutil" "github.com/juanfont/headscale/integration/integrationutil"
@ -194,7 +195,7 @@ func WithBuildTag(tag string) Option {
// as part of the Login function. // as part of the Login function.
func WithExtraLoginArgs(args []string) Option { func WithExtraLoginArgs(args []string) Option {
return func(tsic *TailscaleInContainer) { return func(tsic *TailscaleInContainer) {
tsic.extraLoginArgs = args tsic.extraLoginArgs = append(tsic.extraLoginArgs, args...)
} }
} }
@ -383,7 +384,7 @@ func (t *TailscaleInContainer) Version() string {
// ID returns the Docker container ID of the TailscaleInContainer // ID returns the Docker container ID of the TailscaleInContainer
// instance. // instance.
func (t *TailscaleInContainer) ID() string { func (t *TailscaleInContainer) ContainerID() string {
return t.container.Container.ID return t.container.Container.ID
} }
@ -426,20 +427,21 @@ func (t *TailscaleInContainer) Logs(stdout, stderr io.Writer) error {
) )
} }
// Up runs the login routine on the given Tailscale instance. func (t *TailscaleInContainer) buildLoginCommand(
// This login mechanism uses the authorised key for authentication.
func (t *TailscaleInContainer) Login(
loginServer, authKey string, loginServer, authKey string,
) error { ) []string {
command := []string{ command := []string{
"tailscale", "tailscale",
"up", "up",
"--login-server=" + loginServer, "--login-server=" + loginServer,
"--authkey=" + authKey,
"--hostname=" + t.hostname, "--hostname=" + t.hostname,
fmt.Sprintf("--accept-routes=%t", t.withAcceptRoutes), fmt.Sprintf("--accept-routes=%t", t.withAcceptRoutes),
} }
if authKey != "" {
command = append(command, "--authkey="+authKey)
}
if t.extraLoginArgs != nil { if t.extraLoginArgs != nil {
command = append(command, t.extraLoginArgs...) command = append(command, t.extraLoginArgs...)
} }
@ -458,6 +460,16 @@ func (t *TailscaleInContainer) Login(
) )
} }
return command
}
// Login runs the login routine on the given Tailscale instance.
// This login mechanism uses the authorised key for authentication.
func (t *TailscaleInContainer) Login(
loginServer, authKey string,
) error {
command := t.buildLoginCommand(loginServer, authKey)
if _, _, err := t.Execute(command, dockertestutil.ExecuteCommandTimeout(dockerExecuteTimeout)); err != nil { if _, _, err := t.Execute(command, dockertestutil.ExecuteCommandTimeout(dockerExecuteTimeout)); err != nil {
return fmt.Errorf( return fmt.Errorf(
"%s failed to join tailscale client (%s): %w", "%s failed to join tailscale client (%s): %w",
@ -475,17 +487,7 @@ func (t *TailscaleInContainer) Login(
func (t *TailscaleInContainer) LoginWithURL( func (t *TailscaleInContainer) LoginWithURL(
loginServer string, loginServer string,
) (loginURL *url.URL, err error) { ) (loginURL *url.URL, err error) {
command := []string{ command := t.buildLoginCommand(loginServer, "")
"tailscale",
"up",
"--login-server=" + loginServer,
"--hostname=" + t.hostname,
"--accept-routes=false",
}
if t.extraLoginArgs != nil {
command = append(command, t.extraLoginArgs...)
}
stdout, stderr, err := t.Execute(command) stdout, stderr, err := t.Execute(command)
if errors.Is(err, errTailscaleNotLoggedIn) { if errors.Is(err, errTailscaleNotLoggedIn) {
@ -646,7 +648,7 @@ func (t *TailscaleInContainer) Status(save ...bool) (*ipnstate.Status, error) {
return &status, err return &status, err
} }
// Status returns the ipnstate.Status of the Tailscale instance. // MustStatus returns the ipnstate.Status of the Tailscale instance.
func (t *TailscaleInContainer) MustStatus() *ipnstate.Status { func (t *TailscaleInContainer) MustStatus() *ipnstate.Status {
status, err := t.Status() status, err := t.Status()
if err != nil { if err != nil {
@ -656,6 +658,21 @@ func (t *TailscaleInContainer) MustStatus() *ipnstate.Status {
return status return status
} }
// MustID returns the ID of the Tailscale instance.
func (t *TailscaleInContainer) MustID() types.NodeID {
status, err := t.Status()
if err != nil {
panic(err)
}
id, err := strconv.ParseUint(string(status.Self.ID), 10, 64)
if err != nil {
panic(fmt.Sprintf("failed to parse ID: %s", err))
}
return types.NodeID(id)
}
// Netmap returns the current Netmap (netmap.NetworkMap) of the Tailscale instance. // Netmap returns the current Netmap (netmap.NetworkMap) of the Tailscale instance.
// Only works with Tailscale 1.56 and newer. // Only works with Tailscale 1.56 and newer.
// Panics if version is lower then minimum. // Panics if version is lower then minimum.

View File

@ -5,7 +5,6 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io" "io"
"os"
"strings" "strings"
"sync" "sync"
"testing" "testing"
@ -344,22 +343,10 @@ func isSelfClient(client TailscaleClient, addr string) bool {
return false return false
} }
func isCI() bool {
if _, ok := os.LookupEnv("CI"); ok {
return true
}
if _, ok := os.LookupEnv("GITHUB_RUN_ID"); ok {
return true
}
return false
}
func dockertestMaxWait() time.Duration { func dockertestMaxWait() time.Duration {
wait := 120 * time.Second //nolint wait := 120 * time.Second //nolint
if isCI() { if util.IsCI() {
wait = 300 * time.Second //nolint wait = 300 * time.Second //nolint
} }