1
0
mirror of https://github.com/juanfont/headscale.git synced 2025-06-10 01:17:20 +02:00
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
Kristoffer Dalby 2025-04-11 12:40:43 +02:00
parent d5fdbf16c2
commit 1b318c389e
No known key found for this signature in database
10 changed files with 537 additions and 398 deletions

View File

@ -1,74 +1,79 @@
--- ---
run: version: "2"
timeout: 10m
build-tags:
- ts2019
issues:
skip-dirs:
- gen
linters: linters:
enable-all: true default: all
disable: disable:
- revive - cyclop
- lll - depguard
- gofmt - dupl
- exhaustruct
- funlen
- gochecknoglobals - gochecknoglobals
- gochecknoinits - gochecknoinits
- gocognit - gocognit
- funlen
- tagliatelle
- godox - godox
- ireturn
- execinquery
- exhaustruct
- nolintlint
- musttag # causes issues with imported libs
- depguard
- exportloopref
- tenv
# We should strive to enable these:
- wrapcheck
- dupl
- makezero
- maintidx
# Limits the methods of an interface to 10. We have more in integration tests
- interfacebloat - interfacebloat
- ireturn
# We might want to enable this, but it might be a lot of work - lll
- cyclop - maintidx
- makezero
- musttag
- nestif - nestif
- wsl # might be incompatible with gofumpt - nolintlint
- testpackage
- paralleltest - paralleltest
- revive
- tagliatelle
- testpackage
- wrapcheck
- wsl
settings:
gocritic:
disabled-checks:
- appendAssign
- ifElseChain
nlreturn:
block-size: 4
varnamelen:
ignore-names:
- err
- db
- id
- ip
- ok
- c
- tt
- tx
- rx
- sb
- wg
- pr
- p
- p2
ignore-type-assert-ok: true
ignore-map-index-ok: true
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
- gen
linters-settings: formatters:
varnamelen: enable:
ignore-type-assert-ok: true - gci
ignore-map-index-ok: true - gofmt
ignore-names: - gofumpt
- err - goimports
- db exclusions:
- id generated: lax
- ip paths:
- ok - third_party$
- c - builtin$
- tt - examples$
- tx - gen
- rx
- sb
- wg
- pr
- p
- p2
gocritic:
disabled-checks:
- appendAssign
# TODO(kradalby): Remove this
- ifElseChain
nlreturn:
block-size: 4

View File

@ -285,9 +285,6 @@ func (h *Headscale) handleRegisterInteractive(
nodeToRegister.Node.Expiry = &regReq.Expiry nodeToRegister.Node.Expiry = &regReq.Expiry
} }
// Ensure any auto approved routes are handled before saving.
policy.AutoApproveRoutes(h.polMan, &nodeToRegister.Node)
h.registrationCache.Set( h.registrationCache.Set(
registrationId, registrationId,
nodeToRegister, nodeToRegister,

View File

@ -6,6 +6,7 @@ import (
"github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util" "github.com/juanfont/headscale/hscontrol/util"
"github.com/rs/zerolog/log"
"github.com/samber/lo" "github.com/samber/lo"
"tailscale.com/net/tsaddr" "tailscale.com/net/tsaddr"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
@ -87,9 +88,13 @@ func AutoApproveRoutes(pm PolicyManager, node *types.Node) bool {
if pm == nil { if pm == nil {
return false return false
} }
log.Trace().Msgf("AUTO APPROVE: node %s %d", node.Hostname, node.ID)
var newApproved []netip.Prefix var newApproved []netip.Prefix
for _, route := range node.AnnouncedRoutes() { for _, route := range node.AnnouncedRoutes() {
log.Trace().Msgf("AUTO APPROVE: node %s %d, checking %s", node.Hostname, node.ID, route.String())
if pm.NodeCanApproveRoute(node, route) { if pm.NodeCanApproveRoute(node, route) {
log.Trace().Msgf("AUTO APPROVE: node %s %d, checking %s, %v", node.Hostname, node.ID, route.String(), true)
newApproved = append(newApproved, route) newApproved = append(newApproved, route)
} }
} }

View File

@ -7,6 +7,9 @@ import (
"os" "os"
"sync" "sync"
"slices"
"github.com/davecgh/go-spew/spew"
"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 +148,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 {
@ -163,18 +160,27 @@ func (pm *PolicyManager) NodeCanApproveRoute(node *types.Node, route netip.Prefi
defer pm.mu.Unlock() defer pm.mu.Unlock()
approvers, _ := pm.pol.AutoApprovers.GetRouteApprovers(route) approvers, _ := pm.pol.AutoApprovers.GetRouteApprovers(route)
log.Trace().Msgf("AUTO APPROVE: node %d, checking %s, approvers: %v", node.ID, route.String(), approvers)
for _, approvedAlias := range approvers { for _, approvedAlias := range approvers {
if approvedAlias == node.User.Username() { if approvedAlias == node.User.Username() {
return true return true
} else { } else {
log.Trace().Msgf("AUTO APPROVE: node %d, checking %s, expanding: %s", node.ID, route.String(), approvedAlias)
ips, err := pm.pol.ExpandAlias(pm.nodes, pm.users, approvedAlias) ips, err := pm.pol.ExpandAlias(pm.nodes, pm.users, approvedAlias)
if err != nil { if err != nil {
return false return false
} }
log.Trace().Msgf("AUTO APPROVE: node %d, checking %s, ips: %v", node.ID, route.String(), ips.Prefixes())
log.Trace().Msgf("AUTO APPROVE: node %d, checking %s, contains? %v", node.ID, route.String(), node.IPs())
// log.Trace().Msgf("AUTO APPROVE: node %d, checking %s, users %v", node.ID, route.String(), pm.users)
// log.Trace().Msgf("AUTO APPROVE: node %d, checking %s, nodes %v", node.ID, route.String(), pm.nodes)
spew.Dump(pm.users)
spew.Dump(pm.nodes)
// 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,7 +7,10 @@ import (
"strings" "strings"
"sync" "sync"
"slices"
"github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/types"
"github.com/rs/zerolog/log"
"go4.org/netipx" "go4.org/netipx"
"tailscale.com/net/tsaddr" "tailscale.com/net/tsaddr"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
@ -83,10 +86,12 @@ func (pm *PolicyManager) updateLocked() (bool, error) {
pm.tagOwnerMap = tagMap pm.tagOwnerMap = tagMap
pm.tagOwnerMapHash = tagOwnerMapHash pm.tagOwnerMapHash = tagOwnerMapHash
log.Printf("AUTO APP: BUILDING AUTO APProvers")
autoMap, err := resolveAutoApprovers(pm.pol, pm.users, pm.nodes) autoMap, err := resolveAutoApprovers(pm.pol, pm.users, pm.nodes)
if err != nil { if err != nil {
return false, fmt.Errorf("resolving auto approvers map: %w", err) return false, fmt.Errorf("resolving auto approvers map: %w", err)
} }
log.Printf("AUTO APP: BUILDING AUTO APProvers DONE")
autoApproveMapHash := deephash.Hash(&autoMap) autoApproveMapHash := deephash.Hash(&autoMap)
autoApproveChanged := autoApproveMapHash != pm.autoApproveMapHash autoApproveChanged := autoApproveMapHash != pm.autoApproveMapHash
@ -174,10 +179,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
}
} }
} }
@ -192,14 +195,14 @@ func (pm *PolicyManager) NodeCanApproveRoute(node *types.Node, route netip.Prefi
pm.mu.Lock() pm.mu.Lock()
defer pm.mu.Unlock() defer pm.mu.Unlock()
log.Debug().Msg(pm.DebugString())
// The fast path is that a node requests to approve a prefix // The fast path is that a node requests to approve a prefix
// 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 +223,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 +280,8 @@ func (pm *PolicyManager) DebugString() string {
} }
} }
sb.WriteString("\n\n")
sb.WriteString(pm.nodes.DebugString())
return sb.String() return sb.String()
} }

View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log"
"net/netip" "net/netip"
"strings" "strings"
"time" "time"
@ -160,6 +161,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
@ -209,15 +214,24 @@ func (t Tag) Resolve(p *Policy, users types.Users, nodes types.Nodes) (*netipx.I
return nil, err return nil, err
} }
log.Printf("AUTO APP: TAGMAP: %+v", tagMap)
for tag, ips := range tagMap {
log.Printf("AUTO APP: TAG %s, %v", tag, ips.Prefixes())
}
for _, node := range nodes { for _, node := range nodes {
if node.HasTag(string(t)) { if node.HasTag(string(t)) {
log.Printf("AUTO APP: NODE %d %q HAS TAG %s", node.ID, node.Hostname, t)
node.AppendToIPSet(&ips) node.AppendToIPSet(&ips)
} }
// TODO(kradalby): remove as part of #2417, see comment above // TODO(kradalby): remove as part of #2417, see comment above
if tagMap != nil { if tagMap != nil {
log.Printf("AUTO APP: NODE %d %q CHECKING REQUESTED TAGS: %v", node.ID, node.Hostname, node.Hostinfo.RequestTags)
if tagips, ok := tagMap[t]; ok && node.InIPSet(tagips) && node.Hostinfo != nil { if tagips, ok := tagMap[t]; ok && node.InIPSet(tagips) && node.Hostinfo != nil {
log.Printf("AUTO APP: NODE %d %q CHECKING tagips %v", node.ID, node.Hostname, tagips.Prefixes())
for _, tag := range node.Hostinfo.RequestTags { for _, tag := range node.Hostinfo.RequestTags {
log.Printf("AUTO APP: NODE %d %q CHECKING requested tag %s", node.ID, node.Hostname, tag)
if tag == string(t) { if tag == string(t) {
node.AppendToIPSet(&ips) node.AppendToIPSet(&ips)
} }
@ -233,12 +247,16 @@ 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
func (h Host) Validate() error { func (h Host) Validate() error {
if isHost(string(h)) { if isHost(string(h)) {
fmt.Errorf("Hostname %q is invalid", h) return fmt.Errorf("Hostname %q is invalid", h)
} }
return nil return nil
} }
@ -591,6 +609,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
@ -826,6 +845,7 @@ func resolveAutoApprovers(p *Policy, users types.Users, nodes types.Nodes) (map[
} }
// If it does not resolve, that means the autoApprover is not associated with any IP addresses. // If it does not resolve, that means the autoApprover is not associated with any IP addresses.
ips, _ := aa.Resolve(p, users, nodes) ips, _ := aa.Resolve(p, users, nodes)
log.Printf("AUTO APP RESOLVED: tag: %q pref: %s ips: %v", autoApprover.String(), prefix, ips.Prefixes())
routes[prefix].AddSet(ips) routes[prefix].AddSet(ips)
} }
} }

View File

@ -462,6 +462,7 @@ func (m *mapSession) handleEndpointUpdate() {
// auto approved. Any change here is not important as any // auto approved. Any change here is not important as any
// actual state change will be detected when the route manager // actual state change will be detected when the route manager
// is updated. // is updated.
log.Trace().Msgf("AUTO APPROVE ROUTES CHANGE")
policy.AutoApproveRoutes(m.h.polMan, m.node) policy.AutoApproveRoutes(m.h.polMan, m.node)
// Update the routes of the given node in the route manager to // Update the routes of the given node in the route manager to

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"net/netip" "net/netip"
"slices" "slices"
"sort"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -190,19 +191,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 {
@ -404,9 +412,9 @@ func (node *Node) SubnetRoutes() []netip.Prefix {
return routes return routes
} }
func (node *Node) String() string { // func (node *Node) String() string {
return node.Hostname // return node.Hostname
} // }
// PeerChangeFromMapRequest takes a MapRequest and compares it to the node // PeerChangeFromMapRequest takes a MapRequest and compares it to the node
// to produce a PeerChange struct that can be used to updated the node and // to produce a PeerChange struct that can be used to updated the node and
@ -526,15 +534,15 @@ func (node *Node) ApplyPeerChange(change *tailcfg.PeerChange) {
node.LastSeen = change.LastSeen node.LastSeen = change.LastSeen
} }
func (nodes Nodes) String() string { // func (nodes Nodes) String() string {
temp := make([]string, len(nodes)) // temp := make([]string, len(nodes))
for index, node := range nodes { // for index, node := range nodes {
temp[index] = node.Hostname // temp[index] = node.Hostname
} // }
return fmt.Sprintf("[ %s ](%d)", strings.Join(temp, ", "), len(temp)) // return fmt.Sprintf("[ %s ](%d)", strings.Join(temp, ", "), len(temp))
} // }
func (nodes Nodes) IDMap() map[NodeID]*Node { func (nodes Nodes) IDMap() map[NodeID]*Node {
ret := map[NodeID]*Node{} ret := map[NodeID]*Node{}
@ -545,3 +553,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

@ -19,6 +19,20 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func oidcHSICOpts(s *Scenario) []hsic.Option {
oidcMap := map[string]string{
"HEADSCALE_OIDC_ISSUER": s.mockOIDC.Issuer(),
"HEADSCALE_OIDC_CLIENT_ID": s.mockOIDC.ClientID(),
"CREDENTIALS_DIRECTORY_TEST": "/tmp",
"HEADSCALE_OIDC_CLIENT_SECRET_PATH": "${CREDENTIALS_DIRECTORY_TEST}/hs_client_oidc_secret",
}
return []hsic.Option{
hsic.WithConfigEnv(oidcMap),
hsic.WithFileInContainer("/tmp/hs_client_oidc_secret", []byte(s.mockOIDC.ClientSecret())),
hsic.WithTLS(),
}
}
func TestOIDCAuthenticationPingAll(t *testing.T) { func TestOIDCAuthenticationPingAll(t *testing.T) {
IntegrationSkip(t) IntegrationSkip(t)
t.Parallel() t.Parallel()
@ -40,19 +54,9 @@ func TestOIDCAuthenticationPingAll(t *testing.T) {
defer scenario.ShutdownAssertNoPanics(t) defer scenario.ShutdownAssertNoPanics(t)
oidcMap := map[string]string{
"HEADSCALE_OIDC_ISSUER": scenario.mockOIDC.Issuer(),
"HEADSCALE_OIDC_CLIENT_ID": scenario.mockOIDC.ClientID(),
"CREDENTIALS_DIRECTORY_TEST": "/tmp",
"HEADSCALE_OIDC_CLIENT_SECRET_PATH": "${CREDENTIALS_DIRECTORY_TEST}/hs_client_oidc_secret",
}
err = scenario.CreateHeadscaleEnvWithLoginURL( err = scenario.CreateHeadscaleEnvWithLoginURL(
nil, nil,
hsic.WithTestName("oidcauthping"), append(oidcHSICOpts(scenario), hsic.WithTestName("oidcauthping"))...,
hsic.WithConfigEnv(oidcMap),
hsic.WithTLS(),
hsic.WithFileInContainer("/tmp/hs_client_oidc_secret", []byte(scenario.mockOIDC.ClientSecret())),
) )
assertNoErrHeadscaleEnv(t, err) assertNoErrHeadscaleEnv(t, err)

View File

@ -1367,359 +1367,426 @@ func TestSubnetRouterMultiNetworkExitNode(t *testing.T) {
// - Verify that routes can now be seen by peers. // - Verify that routes can now be seen by peers.
func TestAutoApproveMultiNetwork(t *testing.T) { func TestAutoApproveMultiNetwork(t *testing.T) {
IntegrationSkip(t) IntegrationSkip(t)
t.Parallel()
spec := ScenarioSpec{ tests := []struct {
NodesPerUser: 3, name string
Users: []string{"user1", "user2"}, spec ScenarioSpec
Networks: map[string][]string{ withURL bool
"usernet1": {"user1"}, withOIDC bool
"usernet2": {"user2"}, }{
}, {
ExtraService: map[string][]extraServiceFunc{ name: "authkey",
"usernet1": {Webservice}, spec: ScenarioSpec{
}, NodesPerUser: 3,
// We build the head image with curl and traceroute, so only use Users: []string{"user1", "user2"},
// that for this test. Networks: map[string][]string{
Versions: []string{"head"}, "usernet1": {"user1"},
} "usernet2": {"user2"},
},
rootRoute := netip.MustParsePrefix("10.42.0.0/16") ExtraService: map[string][]extraServiceFunc{
subRoute := netip.MustParsePrefix("10.42.7.0/24") "usernet1": {Webservice},
notApprovedRoute := netip.MustParsePrefix("192.168.0.0/24") },
// We build the head image with curl and traceroute, so only use
scenario, err := NewScenario(spec) // that for this test.
require.NoErrorf(t, err, "failed to create scenario: %s", err) Versions: []string{"head"},
defer scenario.ShutdownAssertNoPanics(t)
pol := &policyv1.ACLPolicy{
ACLs: []policyv1.ACL{
{
Action: "accept",
Sources: []string{"*"},
Destinations: []string{"*:*"},
}, },
}, },
TagOwners: map[string][]string{ {
"tag:approve": {"user1@"}, name: "webauth",
}, spec: ScenarioSpec{
AutoApprovers: policyv1.AutoApprovers{ NodesPerUser: 3,
Routes: map[string][]string{ Users: []string{"user1", "user2"},
rootRoute.String(): {"tag:approve"}, Networks: map[string][]string{
"usernet1": {"user1"},
"usernet2": {"user2"},
},
ExtraService: map[string][]extraServiceFunc{
"usernet1": {Webservice},
},
// We build the head image with curl and traceroute, so only use
// that for this test.
Versions: []string{"head"},
}, },
ExitNode: []string{"tag:approve"}, withURL: true,
}, },
// TODO(kradalby): multinetwork isnt really working on the oidc
// {
// name: "oidc",
// spec: ScenarioSpec{
// NodesPerUser: 3,
// Users: []string{"user1", "user2"},
// OIDCUsers: []mockoidc.MockUser{
// oidcMockUser("user1", false),
// oidcMockUser("user1", false),
// oidcMockUser("user1", false),
// oidcMockUser("user2", false),
// oidcMockUser("user2", false),
// oidcMockUser("user2", false),
// },
// Networks: map[string][]string{
// "usernet1": {"user1"},
// "usernet2": {"user2"},
// },
// ExtraService: map[string][]extraServiceFunc{
// "usernet1": {Webservice},
// },
// // We build the head image with curl and traceroute, so only use
// // that for this test.
// Versions: []string{"head"},
// },
// withURL: true,
// withOIDC: true,
// },
} }
err = scenario.CreateHeadscaleEnv([]tsic.Option{ for _, tt := range tests {
tsic.WithAcceptRoutes(), t.Run(tt.name, func(t *testing.T) {
tsic.WithTags([]string{"tag:approve"}), rootRoute := netip.MustParsePrefix("10.42.0.0/16")
}, subRoute := netip.MustParsePrefix("10.42.7.0/24")
hsic.WithTestName("clienableroute"), notApprovedRoute := netip.MustParsePrefix("192.168.0.0/24")
hsic.WithEmbeddedDERPServerOnly(),
hsic.WithTLS(),
hsic.WithACLPolicy(pol),
hsic.WithPolicyMode(types.PolicyModeDB),
)
assertNoErrHeadscaleEnv(t, err)
allClients, err := scenario.ListTailscaleClients() scenario, err := NewScenario(tt.spec)
assertNoErrListClients(t, err) require.NoErrorf(t, err, "failed to create scenario: %s", err)
// defer scenario.ShutdownAssertNoPanics(t)
err = scenario.WaitForTailscaleSync() pol := &policyv1.ACLPolicy{
assertNoErrSync(t, err) ACLs: []policyv1.ACL{
{
Action: "accept",
Sources: []string{"*"},
Destinations: []string{"*:*"},
},
},
TagOwners: map[string][]string{
"tag:approve": {"user1@"},
},
AutoApprovers: policyv1.AutoApprovers{
Routes: map[string][]string{
rootRoute.String(): {"tag:approve"},
},
ExitNode: []string{"tag:approve"},
},
}
headscale, err := scenario.Headscale() opts := []hsic.Option{
assertNoErrGetHeadscale(t, err) hsic.WithTestName("clienableroute"),
assert.NotNil(t, headscale) hsic.WithEmbeddedDERPServerOnly(),
hsic.WithTLS(),
hsic.WithACLPolicy(pol),
hsic.WithPolicyMode(types.PolicyModeDB),
}
route, err := scenario.SubnetOfNetwork("usernet1") if tt.withOIDC {
require.NoError(t, err) opts = append(opts, oidcHSICOpts(scenario)...)
}
// Set the route of usernet1 to be autoapproved err = scenario.createHeadscaleEnv(tt.withURL, []tsic.Option{
pol.AutoApprovers.Routes[route.String()] = []string{"tag:approve"} tsic.WithAcceptRoutes(),
err = headscale.SetPolicy(pol) tsic.WithTags([]string{"tag:approve"}),
require.NoError(t, err) },
opts...,
)
assertNoErrHeadscaleEnv(t, err)
services, err := scenario.Services("usernet1") allClients, err := scenario.ListTailscaleClients()
require.NoError(t, err) assertNoErrListClients(t, err)
require.Len(t, services, 1)
usernet1, err := scenario.Network("usernet1") err = scenario.WaitForTailscaleSync()
require.NoError(t, err) assertNoErrSync(t, err)
web := services[0] headscale, err := scenario.Headscale()
webip := netip.MustParseAddr(web.GetIPInNetwork(usernet1)) assertNoErrGetHeadscale(t, err)
weburl := fmt.Sprintf("http://%s/etc/hostname", webip) assert.NotNil(t, headscale)
t.Logf("webservice: %s, %s", webip.String(), weburl)
// Sort nodes by ID route, err := scenario.SubnetOfNetwork("usernet1")
sort.SliceStable(allClients, func(i, j int) bool { require.NoError(t, err)
statusI := allClients[i].MustStatus()
statusJ := allClients[j].MustStatus()
return statusI.Self.ID < statusJ.Self.ID // Set the route of usernet1 to be autoapproved
}) pol.AutoApprovers.Routes[route.String()] = []string{"tag:approve"}
err = headscale.SetPolicy(pol)
require.NoError(t, err)
// This is ok because the scenario makes users in order, so the three first services, err := scenario.Services("usernet1")
// nodes, which are subnet routes, will be created first, and the last user require.NoError(t, err)
// will be created with the second. require.Len(t, services, 1)
routerUsernet1 := allClients[0]
routerSubRoute := allClients[1]
routerExitNode := allClients[2]
client := allClients[3] usernet1, err := scenario.Network("usernet1")
require.NoError(t, err)
// Advertise the route for the dockersubnet of user1 web := services[0]
command := []string{ webip := netip.MustParseAddr(web.GetIPInNetwork(usernet1))
"tailscale", weburl := fmt.Sprintf("http://%s/etc/hostname", webip)
"set", t.Logf("webservice: %s, %s", webip.String(), weburl)
"--advertise-routes=" + route.String(),
}
_, _, err = routerUsernet1.Execute(command)
require.NoErrorf(t, err, "failed to advertise route: %s", err)
time.Sleep(5 * time.Second) // Sort nodes by ID
sort.SliceStable(allClients, func(i, j int) bool {
statusI := allClients[i].MustStatus()
statusJ := allClients[j].MustStatus()
// These route should auto approve, so the node is expected to have a route return statusI.Self.ID < statusJ.Self.ID
// for all counts. })
nodes, err := headscale.ListNodes()
require.NoError(t, err)
assertNodeRouteCount(t, nodes[0], 1, 1, 1)
// Verify that the routes have been sent to the client. // This is ok because the scenario makes users in order, so the three first
status, err := client.Status() // nodes, which are subnet routes, will be created first, and the last user
require.NoError(t, err) // will be created with the second.
routerUsernet1 := allClients[0]
routerSubRoute := allClients[1]
routerExitNode := allClients[2]
for _, peerKey := range status.Peers() { client := allClients[3]
peerStatus := status.Peer[peerKey]
if peerStatus.ID == "1" { // Advertise the route for the dockersubnet of user1
assert.Contains(t, peerStatus.PrimaryRoutes.AsSlice(), *route) command := []string{
requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{*route}) "tailscale",
} else { "set",
requirePeerSubnetRoutes(t, peerStatus, nil) "--advertise-routes=" + route.String(),
} }
} _, _, err = routerUsernet1.Execute(command)
require.NoErrorf(t, err, "failed to advertise route: %s", err)
url := fmt.Sprintf("http://%s/etc/hostname", webip) time.Sleep(5 * time.Second)
t.Logf("url from %s to %s", client.Hostname(), url)
result, err := client.Curl(url) // These route should auto approve, so the node is expected to have a route
require.NoError(t, err) // for all counts.
assert.Len(t, result, 13) nodes, err := headscale.ListNodes()
require.NoError(t, err)
assertNodeRouteCount(t, nodes[0], 1, 1, 1)
tr, err := client.Traceroute(webip) // Verify that the routes have been sent to the client.
require.NoError(t, err) status, err := client.Status()
assertTracerouteViaIP(t, tr, routerUsernet1.MustIPv4()) require.NoError(t, err)
// Remove the auto approval from the policy, any routes already enabled should be allowed. for _, peerKey := range status.Peers() {
delete(pol.AutoApprovers.Routes, route.String()) peerStatus := status.Peer[peerKey]
err = headscale.SetPolicy(pol)
require.NoError(t, err)
time.Sleep(5 * time.Second) if peerStatus.ID == "1" {
assert.Contains(t, peerStatus.PrimaryRoutes.AsSlice(), *route)
requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{*route})
} else {
requirePeerSubnetRoutes(t, peerStatus, nil)
}
}
// These route should auto approve, so the node is expected to have a route url := fmt.Sprintf("http://%s/etc/hostname", webip)
// for all counts. t.Logf("url from %s to %s", client.Hostname(), url)
nodes, err = headscale.ListNodes()
require.NoError(t, err)
assertNodeRouteCount(t, nodes[0], 1, 1, 1)
// Verify that the routes have been sent to the client. result, err := client.Curl(url)
status, err = client.Status() require.NoError(t, err)
require.NoError(t, err) assert.Len(t, result, 13)
for _, peerKey := range status.Peers() { tr, err := client.Traceroute(webip)
peerStatus := status.Peer[peerKey] require.NoError(t, err)
assertTracerouteViaIP(t, tr, routerUsernet1.MustIPv4())
if peerStatus.ID == "1" { // Remove the auto approval from the policy, any routes already enabled should be allowed.
assert.Contains(t, peerStatus.PrimaryRoutes.AsSlice(), *route) delete(pol.AutoApprovers.Routes, route.String())
requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{*route}) err = headscale.SetPolicy(pol)
} else { require.NoError(t, err)
requirePeerSubnetRoutes(t, peerStatus, nil)
}
}
url = fmt.Sprintf("http://%s/etc/hostname", webip) time.Sleep(5 * time.Second)
t.Logf("url from %s to %s", client.Hostname(), url)
result, err = client.Curl(url) // These route should auto approve, so the node is expected to have a route
require.NoError(t, err) // for all counts.
assert.Len(t, result, 13) nodes, err = headscale.ListNodes()
require.NoError(t, err)
assertNodeRouteCount(t, nodes[0], 1, 1, 1)
tr, err = client.Traceroute(webip) // Verify that the routes have been sent to the client.
require.NoError(t, err) status, err = client.Status()
assertTracerouteViaIP(t, tr, routerUsernet1.MustIPv4()) require.NoError(t, err)
// Disable the route, making it unavailable since it is no longer auto-approved for _, peerKey := range status.Peers() {
_, err = headscale.ApproveRoutes( peerStatus := status.Peer[peerKey]
nodes[0].GetId(),
[]netip.Prefix{},
)
require.NoError(t, err)
time.Sleep(5 * time.Second) if peerStatus.ID == "1" {
assert.Contains(t, peerStatus.PrimaryRoutes.AsSlice(), *route)
requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{*route})
} else {
requirePeerSubnetRoutes(t, peerStatus, nil)
}
}
// These route should auto approve, so the node is expected to have a route url = fmt.Sprintf("http://%s/etc/hostname", webip)
// for all counts. t.Logf("url from %s to %s", client.Hostname(), url)
nodes, err = headscale.ListNodes()
require.NoError(t, err)
assertNodeRouteCount(t, nodes[0], 1, 0, 0)
// Verify that the routes have been sent to the client. result, err = client.Curl(url)
status, err = client.Status() require.NoError(t, err)
require.NoError(t, err) assert.Len(t, result, 13)
for _, peerKey := range status.Peers() { tr, err = client.Traceroute(webip)
peerStatus := status.Peer[peerKey] require.NoError(t, err)
requirePeerSubnetRoutes(t, peerStatus, nil) assertTracerouteViaIP(t, tr, routerUsernet1.MustIPv4())
}
// Add the route back to the auto approver in the policy, the route should // Disable the route, making it unavailable since it is no longer auto-approved
// now become available again. _, err = headscale.ApproveRoutes(
pol.AutoApprovers.Routes[route.String()] = []string{"tag:approve"} nodes[0].GetId(),
err = headscale.SetPolicy(pol) []netip.Prefix{},
require.NoError(t, err) )
require.NoError(t, err)
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
// These route should auto approve, so the node is expected to have a route // These route should auto approve, so the node is expected to have a route
// for all counts. // for all counts.
nodes, err = headscale.ListNodes() nodes, err = headscale.ListNodes()
require.NoError(t, err) require.NoError(t, err)
assertNodeRouteCount(t, nodes[0], 1, 1, 1) assertNodeRouteCount(t, nodes[0], 1, 0, 0)
// Verify that the routes have been sent to the client. // Verify that the routes have been sent to the client.
status, err = client.Status() status, err = client.Status()
require.NoError(t, err) require.NoError(t, err)
for _, peerKey := range status.Peers() { for _, peerKey := range status.Peers() {
peerStatus := status.Peer[peerKey] peerStatus := status.Peer[peerKey]
requirePeerSubnetRoutes(t, peerStatus, nil)
}
if peerStatus.ID == "1" { // Add the route back to the auto approver in the policy, the route should
require.NotNil(t, peerStatus.PrimaryRoutes) // now become available again.
assert.Contains(t, peerStatus.PrimaryRoutes.AsSlice(), *route) pol.AutoApprovers.Routes[route.String()] = []string{"tag:approve"}
requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{*route}) err = headscale.SetPolicy(pol)
} else { require.NoError(t, err)
requirePeerSubnetRoutes(t, peerStatus, nil)
}
}
url = fmt.Sprintf("http://%s/etc/hostname", webip) time.Sleep(5 * time.Second)
t.Logf("url from %s to %s", client.Hostname(), url)
result, err = client.Curl(url) // These route should auto approve, so the node is expected to have a route
require.NoError(t, err) // for all counts.
assert.Len(t, result, 13) nodes, err = headscale.ListNodes()
require.NoError(t, err)
assertNodeRouteCount(t, nodes[0], 1, 1, 1)
tr, err = client.Traceroute(webip) // Verify that the routes have been sent to the client.
require.NoError(t, err) status, err = client.Status()
assertTracerouteViaIP(t, tr, routerUsernet1.MustIPv4()) require.NoError(t, err)
// Advertise and validate a subnet of an auto approved route, /24 inside the for _, peerKey := range status.Peers() {
// auto approved /16. peerStatus := status.Peer[peerKey]
command = []string{
"tailscale",
"set",
"--advertise-routes=" + subRoute.String(),
}
_, _, err = routerSubRoute.Execute(command)
require.NoErrorf(t, err, "failed to advertise route: %s", err)
time.Sleep(5 * time.Second) if peerStatus.ID == "1" {
require.NotNil(t, peerStatus.PrimaryRoutes)
assert.Contains(t, peerStatus.PrimaryRoutes.AsSlice(), *route)
requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{*route})
} else {
requirePeerSubnetRoutes(t, peerStatus, nil)
}
}
// These route should auto approve, so the node is expected to have a route url = fmt.Sprintf("http://%s/etc/hostname", webip)
// for all counts. t.Logf("url from %s to %s", client.Hostname(), url)
nodes, err = headscale.ListNodes()
require.NoError(t, err)
assertNodeRouteCount(t, nodes[0], 1, 1, 1)
assertNodeRouteCount(t, nodes[1], 1, 1, 1)
// Verify that the routes have been sent to the client. result, err = client.Curl(url)
status, err = client.Status() require.NoError(t, err)
require.NoError(t, err) assert.Len(t, result, 13)
for _, peerKey := range status.Peers() { tr, err = client.Traceroute(webip)
peerStatus := status.Peer[peerKey] require.NoError(t, err)
assertTracerouteViaIP(t, tr, routerUsernet1.MustIPv4())
if peerStatus.ID == "1" { // Advertise and validate a subnet of an auto approved route, /24 inside the
assert.Contains(t, peerStatus.PrimaryRoutes.AsSlice(), *route) // auto approved /16.
requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{*route}) command = []string{
} else if peerStatus.ID == "2" { "tailscale",
assert.Contains(t, peerStatus.PrimaryRoutes.AsSlice(), subRoute) "set",
requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{subRoute}) "--advertise-routes=" + subRoute.String(),
} else { }
requirePeerSubnetRoutes(t, peerStatus, nil) _, _, err = routerSubRoute.Execute(command)
} require.NoErrorf(t, err, "failed to advertise route: %s", err)
}
// Advertise a not approved route will not end up anywhere time.Sleep(5 * time.Second)
command = []string{
"tailscale",
"set",
"--advertise-routes=" + notApprovedRoute.String(),
}
_, _, err = routerSubRoute.Execute(command)
require.NoErrorf(t, err, "failed to advertise route: %s", err)
time.Sleep(5 * time.Second) // These route should auto approve, so the node is expected to have a route
// for all counts.
nodes, err = headscale.ListNodes()
require.NoError(t, err)
assertNodeRouteCount(t, nodes[0], 1, 1, 1)
assertNodeRouteCount(t, nodes[1], 1, 1, 1)
// These route should auto approve, so the node is expected to have a route // Verify that the routes have been sent to the client.
// for all counts. status, err = client.Status()
nodes, err = headscale.ListNodes() require.NoError(t, err)
require.NoError(t, err)
assertNodeRouteCount(t, nodes[0], 1, 1, 1)
assertNodeRouteCount(t, nodes[1], 1, 1, 0)
assertNodeRouteCount(t, nodes[2], 0, 0, 0)
// Verify that the routes have been sent to the client. for _, peerKey := range status.Peers() {
status, err = client.Status() peerStatus := status.Peer[peerKey]
require.NoError(t, err)
for _, peerKey := range status.Peers() { if peerStatus.ID == "1" {
peerStatus := status.Peer[peerKey] assert.Contains(t, peerStatus.PrimaryRoutes.AsSlice(), *route)
requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{*route})
} else if peerStatus.ID == "2" {
assert.Contains(t, peerStatus.PrimaryRoutes.AsSlice(), subRoute)
requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{subRoute})
} else {
requirePeerSubnetRoutes(t, peerStatus, nil)
}
}
if peerStatus.ID == "1" { // Advertise a not approved route will not end up anywhere
assert.Contains(t, peerStatus.PrimaryRoutes.AsSlice(), *route) command = []string{
requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{*route}) "tailscale",
} else { "set",
requirePeerSubnetRoutes(t, peerStatus, nil) "--advertise-routes=" + notApprovedRoute.String(),
} }
} _, _, err = routerSubRoute.Execute(command)
require.NoErrorf(t, err, "failed to advertise route: %s", err)
// Exit routes are also automatically approved time.Sleep(5 * time.Second)
command = []string{
"tailscale",
"set",
"--advertise-exit-node",
}
_, _, err = routerExitNode.Execute(command)
require.NoErrorf(t, err, "failed to advertise route: %s", err)
time.Sleep(5 * time.Second) // These route should auto approve, so the node is expected to have a route
// for all counts.
nodes, err = headscale.ListNodes()
require.NoError(t, err)
assertNodeRouteCount(t, nodes[0], 1, 1, 1)
assertNodeRouteCount(t, nodes[1], 1, 1, 0)
assertNodeRouteCount(t, nodes[2], 0, 0, 0)
nodes, err = headscale.ListNodes() // Verify that the routes have been sent to the client.
require.NoError(t, err) status, err = client.Status()
assertNodeRouteCount(t, nodes[0], 1, 1, 1) require.NoError(t, err)
assertNodeRouteCount(t, nodes[1], 1, 1, 0)
assertNodeRouteCount(t, nodes[2], 2, 2, 2)
// Verify that the routes have been sent to the client. for _, peerKey := range status.Peers() {
status, err = client.Status() peerStatus := status.Peer[peerKey]
require.NoError(t, err)
for _, peerKey := range status.Peers() { if peerStatus.ID == "1" {
peerStatus := status.Peer[peerKey] assert.Contains(t, peerStatus.PrimaryRoutes.AsSlice(), *route)
requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{*route})
} else {
requirePeerSubnetRoutes(t, peerStatus, nil)
}
}
if peerStatus.ID == "1" { // Exit routes are also automatically approved
assert.Contains(t, peerStatus.PrimaryRoutes.AsSlice(), *route) command = []string{
requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{*route}) "tailscale",
} else if peerStatus.ID == "3" { "set",
requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{tsaddr.AllIPv4(), tsaddr.AllIPv6()}) "--advertise-exit-node",
} else { }
requirePeerSubnetRoutes(t, peerStatus, nil) _, _, err = routerExitNode.Execute(command)
} require.NoErrorf(t, err, "failed to advertise route: %s", err)
time.Sleep(5 * time.Second)
nodes, err = headscale.ListNodes()
require.NoError(t, err)
assertNodeRouteCount(t, nodes[0], 1, 1, 1)
assertNodeRouteCount(t, nodes[1], 1, 1, 0)
assertNodeRouteCount(t, nodes[2], 2, 2, 2)
// Verify that the routes have been sent to the client.
status, err = client.Status()
require.NoError(t, err)
for _, peerKey := range status.Peers() {
peerStatus := status.Peer[peerKey]
if peerStatus.ID == "1" {
assert.Contains(t, peerStatus.PrimaryRoutes.AsSlice(), *route)
requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{*route})
} else if peerStatus.ID == "3" {
requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{tsaddr.AllIPv4(), tsaddr.AllIPv6()})
} else {
requirePeerSubnetRoutes(t, peerStatus, nil)
}
}
})
} }
} }