mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-28 10:51:44 +01:00 
			
		
		
		
	fix auto approver on register and new policy (#2506)
* fix issue auto approve route on register bug This commit fixes an issue where routes where not approved on a node during registration. This cause the auto approval to require the node to readvertise the routes. Fixes #2497 Fixes #2485 Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * hsic: only set db policy if exist Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * policy: calculate changed based on policy and filter v1 is a bit simpler than v2, it does not pre calculate the auto approver map and we cannot tell if it is changed. Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> --------- Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
		
							parent
							
								
									e3521be705
								
							
						
					
					
						commit
						5a18e91317
					
				@ -66,12 +66,11 @@ jobs:
 | 
				
			|||||||
          - Test2118DeletingOnlineNodePanics
 | 
					          - Test2118DeletingOnlineNodePanics
 | 
				
			||||||
          - TestEnablingRoutes
 | 
					          - TestEnablingRoutes
 | 
				
			||||||
          - TestHASubnetRouterFailover
 | 
					          - TestHASubnetRouterFailover
 | 
				
			||||||
          - TestEnableDisableAutoApprovedRoute
 | 
					 | 
				
			||||||
          - TestAutoApprovedSubRoute2068
 | 
					 | 
				
			||||||
          - TestSubnetRouteACL
 | 
					          - TestSubnetRouteACL
 | 
				
			||||||
          - TestEnablingExitRoutes
 | 
					          - TestEnablingExitRoutes
 | 
				
			||||||
          - TestSubnetRouterMultiNetwork
 | 
					          - TestSubnetRouterMultiNetwork
 | 
				
			||||||
          - TestSubnetRouterMultiNetworkExitNode
 | 
					          - TestSubnetRouterMultiNetworkExitNode
 | 
				
			||||||
 | 
					          - TestAutoApproveMultiNetwork
 | 
				
			||||||
          - TestHeadscale
 | 
					          - TestHeadscale
 | 
				
			||||||
          - TestTailscaleNodesJoiningHeadcale
 | 
					          - TestTailscaleNodesJoiningHeadcale
 | 
				
			||||||
          - TestSSHOneUserToAll
 | 
					          - TestSSHOneUserToAll
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										3
									
								
								.github/workflows/test-integration.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/workflows/test-integration.yaml
									
									
									
									
										vendored
									
									
								
							@ -66,12 +66,11 @@ jobs:
 | 
				
			|||||||
          - Test2118DeletingOnlineNodePanics
 | 
					          - Test2118DeletingOnlineNodePanics
 | 
				
			||||||
          - TestEnablingRoutes
 | 
					          - TestEnablingRoutes
 | 
				
			||||||
          - TestHASubnetRouterFailover
 | 
					          - TestHASubnetRouterFailover
 | 
				
			||||||
          - TestEnableDisableAutoApprovedRoute
 | 
					 | 
				
			||||||
          - TestAutoApprovedSubRoute2068
 | 
					 | 
				
			||||||
          - TestSubnetRouteACL
 | 
					          - TestSubnetRouteACL
 | 
				
			||||||
          - TestEnablingExitRoutes
 | 
					          - TestEnablingExitRoutes
 | 
				
			||||||
          - TestSubnetRouterMultiNetwork
 | 
					          - TestSubnetRouterMultiNetwork
 | 
				
			||||||
          - TestSubnetRouterMultiNetworkExitNode
 | 
					          - TestSubnetRouterMultiNetworkExitNode
 | 
				
			||||||
 | 
					          - TestAutoApproveMultiNetwork
 | 
				
			||||||
          - TestHeadscale
 | 
					          - TestHeadscale
 | 
				
			||||||
          - TestTailscaleNodesJoiningHeadcale
 | 
					          - TestTailscaleNodesJoiningHeadcale
 | 
				
			||||||
          - TestSSHOneUserToAll
 | 
					          - TestSSHOneUserToAll
 | 
				
			||||||
 | 
				
			|||||||
@ -866,6 +866,11 @@ func (h *Headscale) Serve() error {
 | 
				
			|||||||
					log.Info().
 | 
										log.Info().
 | 
				
			||||||
						Msg("ACL policy successfully reloaded, notifying nodes of change")
 | 
											Msg("ACL policy successfully reloaded, notifying nodes of change")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										err = h.autoApproveNodes()
 | 
				
			||||||
 | 
										if err != nil {
 | 
				
			||||||
 | 
											log.Error().Err(err).Msg("failed to approve routes after new policy")
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					ctx := types.NotifyCtx(context.Background(), "acl-sighup", "na")
 | 
										ctx := types.NotifyCtx(context.Background(), "acl-sighup", "na")
 | 
				
			||||||
					h.nodeNotifier.NotifyAll(ctx, types.UpdateFull())
 | 
										h.nodeNotifier.NotifyAll(ctx, types.UpdateFull())
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
@ -1166,3 +1171,36 @@ func (h *Headscale) loadPolicyManager() error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	return errOut
 | 
						return errOut
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// autoApproveNodes mass approves routes on all nodes. It is _only_ intended for
 | 
				
			||||||
 | 
					// use when the policy is replaced. It is not sending or reporting any changes
 | 
				
			||||||
 | 
					// or updates as we send full updates after replacing the policy.
 | 
				
			||||||
 | 
					// TODO(kradalby): This is kind of messy, maybe this is another +1
 | 
				
			||||||
 | 
					// for an event bus. See example comments here.
 | 
				
			||||||
 | 
					func (h *Headscale) autoApproveNodes() error {
 | 
				
			||||||
 | 
						err := h.db.Write(func(tx *gorm.DB) error {
 | 
				
			||||||
 | 
							nodes, err := db.ListNodes(tx)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for _, node := range nodes {
 | 
				
			||||||
 | 
								changed := policy.AutoApproveRoutes(h.polMan, node)
 | 
				
			||||||
 | 
								if changed {
 | 
				
			||||||
 | 
									err = tx.Save(node).Error
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										return err
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									h.primaryRoutes.SetRoutes(node.ID, node.SubnetRoutes()...)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("auto approving routes for nodes: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -10,6 +10,7 @@ import (
 | 
				
			|||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/juanfont/headscale/hscontrol/db"
 | 
						"github.com/juanfont/headscale/hscontrol/db"
 | 
				
			||||||
 | 
						"github.com/juanfont/headscale/hscontrol/policy"
 | 
				
			||||||
	"github.com/juanfont/headscale/hscontrol/types"
 | 
						"github.com/juanfont/headscale/hscontrol/types"
 | 
				
			||||||
	"github.com/juanfont/headscale/hscontrol/util"
 | 
						"github.com/juanfont/headscale/hscontrol/util"
 | 
				
			||||||
	"gorm.io/gorm"
 | 
						"gorm.io/gorm"
 | 
				
			||||||
@ -212,6 +213,9 @@ func (h *Headscale) handleRegisterWithAuthKey(
 | 
				
			|||||||
		nodeToRegister.Expiry = ®Req.Expiry
 | 
							nodeToRegister.Expiry = ®Req.Expiry
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Ensure any auto approved routes are handled before saving.
 | 
				
			||||||
 | 
						policy.AutoApproveRoutes(h.polMan, &nodeToRegister)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ipv4, ipv6, err := h.ipAlloc.Next()
 | 
						ipv4, ipv6, err := h.ipAlloc.Next()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("allocating IPs: %w", err)
 | 
							return nil, fmt.Errorf("allocating IPs: %w", err)
 | 
				
			||||||
@ -266,7 +270,7 @@ func (h *Headscale) handleRegisterInteractive(
 | 
				
			|||||||
		return nil, fmt.Errorf("generating registration ID: %w", err)
 | 
							return nil, fmt.Errorf("generating registration ID: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	newNode := types.RegisterNode{
 | 
						nodeToRegister := types.RegisterNode{
 | 
				
			||||||
		Node: types.Node{
 | 
							Node: types.Node{
 | 
				
			||||||
			Hostname:   regReq.Hostinfo.Hostname,
 | 
								Hostname:   regReq.Hostinfo.Hostname,
 | 
				
			||||||
			MachineKey: machineKey,
 | 
								MachineKey: machineKey,
 | 
				
			||||||
@ -278,12 +282,15 @@ func (h *Headscale) handleRegisterInteractive(
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if !regReq.Expiry.IsZero() {
 | 
						if !regReq.Expiry.IsZero() {
 | 
				
			||||||
		newNode.Node.Expiry = ®Req.Expiry
 | 
							nodeToRegister.Node.Expiry = ®Req.Expiry
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Ensure any auto approved routes are handled before saving.
 | 
				
			||||||
 | 
						policy.AutoApproveRoutes(h.polMan, &nodeToRegister.Node)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	h.registrationCache.Set(
 | 
						h.registrationCache.Set(
 | 
				
			||||||
		registrationId,
 | 
							registrationId,
 | 
				
			||||||
		newNode,
 | 
							nodeToRegister,
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return &tailcfg.RegisterResponse{
 | 
						return &tailcfg.RegisterResponse{
 | 
				
			||||||
 | 
				
			|||||||
@ -739,6 +739,11 @@ func (api headscaleV1APIServer) SetPolicy(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// Only send update if the packet filter has changed.
 | 
						// Only send update if the packet filter has changed.
 | 
				
			||||||
	if changed {
 | 
						if changed {
 | 
				
			||||||
 | 
							err = api.h.autoApproveNodes()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		ctx := types.NotifyCtx(context.Background(), "acl-update", "na")
 | 
							ctx := types.NotifyCtx(context.Background(), "acl-update", "na")
 | 
				
			||||||
		api.h.nodeNotifier.NotifyAll(ctx, types.UpdateFull())
 | 
							api.h.nodeNotifier.NotifyAll(ctx, types.UpdateFull())
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -53,14 +53,15 @@ func NewPolicyManager(polB []byte, users []types.User, nodes types.Nodes) (*Poli
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type PolicyManager struct {
 | 
					type PolicyManager struct {
 | 
				
			||||||
	mu  sync.Mutex
 | 
						mu      sync.Mutex
 | 
				
			||||||
	pol *ACLPolicy
 | 
						pol     *ACLPolicy
 | 
				
			||||||
 | 
						polHash deephash.Sum
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	users []types.User
 | 
						users []types.User
 | 
				
			||||||
	nodes types.Nodes
 | 
						nodes types.Nodes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	filterHash deephash.Sum
 | 
					 | 
				
			||||||
	filter     []tailcfg.FilterRule
 | 
						filter     []tailcfg.FilterRule
 | 
				
			||||||
 | 
						filterHash deephash.Sum
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// updateLocked updates the filter rules based on the current policy and nodes.
 | 
					// updateLocked updates the filter rules based on the current policy and nodes.
 | 
				
			||||||
@ -71,13 +72,16 @@ func (pm *PolicyManager) updateLocked() (bool, error) {
 | 
				
			|||||||
		return false, fmt.Errorf("compiling filter rules: %w", err)
 | 
							return false, fmt.Errorf("compiling filter rules: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						polHash := deephash.Hash(pm.pol)
 | 
				
			||||||
	filterHash := deephash.Hash(&filter)
 | 
						filterHash := deephash.Hash(&filter)
 | 
				
			||||||
	if filterHash == pm.filterHash {
 | 
					
 | 
				
			||||||
 | 
						if polHash == pm.polHash && filterHash == pm.filterHash {
 | 
				
			||||||
		return false, nil
 | 
							return false, nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	pm.filter = filter
 | 
						pm.filter = filter
 | 
				
			||||||
	pm.filterHash = filterHash
 | 
						pm.filterHash = filterHash
 | 
				
			||||||
 | 
						pm.polHash = polHash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return true, nil
 | 
						return true, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,6 @@
 | 
				
			|||||||
package integration
 | 
					package integration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"encoding/json"
 | 
					 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"net/netip"
 | 
						"net/netip"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
@ -9,6 +8,7 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	"github.com/google/go-cmp/cmp"
 | 
						"github.com/google/go-cmp/cmp"
 | 
				
			||||||
	policyv1 "github.com/juanfont/headscale/hscontrol/policy/v1"
 | 
						policyv1 "github.com/juanfont/headscale/hscontrol/policy/v1"
 | 
				
			||||||
 | 
						"github.com/juanfont/headscale/hscontrol/types"
 | 
				
			||||||
	"github.com/juanfont/headscale/integration/hsic"
 | 
						"github.com/juanfont/headscale/integration/hsic"
 | 
				
			||||||
	"github.com/juanfont/headscale/integration/tsic"
 | 
						"github.com/juanfont/headscale/integration/tsic"
 | 
				
			||||||
	"github.com/stretchr/testify/assert"
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
@ -1033,9 +1033,7 @@ func TestPolicyUpdateWhileRunningWithCLIInDatabase(t *testing.T) {
 | 
				
			|||||||
			tsic.WithDockerWorkdir("/"),
 | 
								tsic.WithDockerWorkdir("/"),
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		hsic.WithTestName("policyreload"),
 | 
							hsic.WithTestName("policyreload"),
 | 
				
			||||||
		hsic.WithConfigEnv(map[string]string{
 | 
							hsic.WithPolicyMode(types.PolicyModeDB),
 | 
				
			||||||
			"HEADSCALE_POLICY_MODE": "database",
 | 
					 | 
				
			||||||
		}),
 | 
					 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
	require.NoError(t, err)
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1086,24 +1084,7 @@ func TestPolicyUpdateWhileRunningWithCLIInDatabase(t *testing.T) {
 | 
				
			|||||||
		Hosts: policyv1.Hosts{},
 | 
							Hosts: policyv1.Hosts{},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	pBytes, _ := json.Marshal(p)
 | 
						err = headscale.SetPolicy(&p)
 | 
				
			||||||
 | 
					 | 
				
			||||||
	policyFilePath := "/etc/headscale/policy.json"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	err = headscale.WriteFile(policyFilePath, pBytes)
 | 
					 | 
				
			||||||
	require.NoError(t, err)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// No policy is present at this time.
 | 
					 | 
				
			||||||
	// Add a new policy from a file.
 | 
					 | 
				
			||||||
	_, err = headscale.Execute(
 | 
					 | 
				
			||||||
		[]string{
 | 
					 | 
				
			||||||
			"headscale",
 | 
					 | 
				
			||||||
			"policy",
 | 
					 | 
				
			||||||
			"set",
 | 
					 | 
				
			||||||
			"-f",
 | 
					 | 
				
			||||||
			policyFilePath,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
	require.NoError(t, err)
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Get the current policy and check
 | 
						// Get the current policy and check
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,7 @@ import (
 | 
				
			|||||||
	"net/netip"
 | 
						"net/netip"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
 | 
						v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
 | 
				
			||||||
 | 
						policyv1 "github.com/juanfont/headscale/hscontrol/policy/v1"
 | 
				
			||||||
	"github.com/ory/dockertest/v3"
 | 
						"github.com/ory/dockertest/v3"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -24,4 +25,5 @@ type ControlServer interface {
 | 
				
			|||||||
	ApproveRoutes(uint64, []netip.Prefix) (*v1.Node, error)
 | 
						ApproveRoutes(uint64, []netip.Prefix) (*v1.Node, error)
 | 
				
			||||||
	GetCert() []byte
 | 
						GetCert() []byte
 | 
				
			||||||
	GetHostname() string
 | 
						GetHostname() string
 | 
				
			||||||
 | 
						SetPolicy(*policyv1.ACLPolicy) error
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -71,6 +71,7 @@ type HeadscaleInContainer struct {
 | 
				
			|||||||
	filesInContainer []fileInContainer
 | 
						filesInContainer []fileInContainer
 | 
				
			||||||
	postgres         bool
 | 
						postgres         bool
 | 
				
			||||||
	policyV2         bool
 | 
						policyV2         bool
 | 
				
			||||||
 | 
						policyMode       types.PolicyMode
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Option represent optional settings that can be given to a
 | 
					// Option represent optional settings that can be given to a
 | 
				
			||||||
@ -195,6 +196,14 @@ func WithPolicyV2() Option {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// WithPolicy sets the policy mode for headscale
 | 
				
			||||||
 | 
					func WithPolicyMode(mode types.PolicyMode) Option {
 | 
				
			||||||
 | 
						return func(hsic *HeadscaleInContainer) {
 | 
				
			||||||
 | 
							hsic.policyMode = mode
 | 
				
			||||||
 | 
							hsic.env["HEADSCALE_POLICY_MODE"] = string(mode)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// WithIPAllocationStrategy sets the tests IP Allocation strategy.
 | 
					// WithIPAllocationStrategy sets the tests IP Allocation strategy.
 | 
				
			||||||
func WithIPAllocationStrategy(strategy types.IPAllocationStrategy) Option {
 | 
					func WithIPAllocationStrategy(strategy types.IPAllocationStrategy) Option {
 | 
				
			||||||
	return func(hsic *HeadscaleInContainer) {
 | 
						return func(hsic *HeadscaleInContainer) {
 | 
				
			||||||
@ -286,6 +295,7 @@ func New(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		env:              DefaultConfigEnv(),
 | 
							env:              DefaultConfigEnv(),
 | 
				
			||||||
		filesInContainer: []fileInContainer{},
 | 
							filesInContainer: []fileInContainer{},
 | 
				
			||||||
 | 
							policyMode:       types.PolicyModeFile,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, opt := range opts {
 | 
						for _, opt := range opts {
 | 
				
			||||||
@ -412,14 +422,9 @@ func New(
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if hsic.aclPolicy != nil {
 | 
						if hsic.aclPolicy != nil {
 | 
				
			||||||
		data, err := json.Marshal(hsic.aclPolicy)
 | 
							err = hsic.writePolicy(hsic.aclPolicy)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return nil, fmt.Errorf("failed to marshal ACL Policy to JSON: %w", err)
 | 
								return nil, fmt.Errorf("writing policy: %w", err)
 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		err = hsic.WriteFile(aclPolicyPath, data)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return nil, fmt.Errorf("failed to write ACL policy to container: %w", err)
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -441,6 +446,15 @@ func New(
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Load the database from policy file on repeat until it succeeds,
 | 
				
			||||||
 | 
						// this is done as the container sleeps before starting headscale.
 | 
				
			||||||
 | 
						if hsic.aclPolicy != nil && hsic.policyMode == types.PolicyModeDB {
 | 
				
			||||||
 | 
							err := pool.Retry(hsic.reloadDatabasePolicy)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("loading database policy on startup: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return hsic, nil
 | 
						return hsic, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -822,6 +836,116 @@ func (t *HeadscaleInContainer) ListUsers() ([]*v1.User, error) {
 | 
				
			|||||||
	return users, nil
 | 
						return users, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *HeadscaleInContainer) SetPolicy(pol *policyv1.ACLPolicy) error {
 | 
				
			||||||
 | 
						err := h.writePolicy(pol)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("writing policy file: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch h.policyMode {
 | 
				
			||||||
 | 
						case types.PolicyModeDB:
 | 
				
			||||||
 | 
							err := h.reloadDatabasePolicy()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("reloading database policy: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						case types.PolicyModeFile:
 | 
				
			||||||
 | 
							err := h.Reload()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("reloading policy file: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							panic("policy mode is not valid: " + h.policyMode)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *HeadscaleInContainer) reloadDatabasePolicy() error {
 | 
				
			||||||
 | 
						_, err := h.Execute(
 | 
				
			||||||
 | 
							[]string{
 | 
				
			||||||
 | 
								"headscale",
 | 
				
			||||||
 | 
								"policy",
 | 
				
			||||||
 | 
								"set",
 | 
				
			||||||
 | 
								"-f",
 | 
				
			||||||
 | 
								aclPolicyPath,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("setting policy with db command: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *HeadscaleInContainer) writePolicy(pol *policyv1.ACLPolicy) error {
 | 
				
			||||||
 | 
						pBytes, err := json.Marshal(pol)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("marshalling pol: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = h.WriteFile(aclPolicyPath, pBytes)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("writing policy to headscale container: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *HeadscaleInContainer) PID() (int, error) {
 | 
				
			||||||
 | 
						cmd := []string{"bash", "-c", `ps aux | grep headscale | grep -v grep | awk '{print $2}'`}
 | 
				
			||||||
 | 
						output, err := h.Execute(cmd)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return 0, fmt.Errorf("failed to execute command: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						lines := strings.TrimSpace(output)
 | 
				
			||||||
 | 
						if lines == "" {
 | 
				
			||||||
 | 
							return 0, os.ErrNotExist // No output means no process found
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						pids := make([]int, 0, len(lines))
 | 
				
			||||||
 | 
						for _, line := range strings.Split(lines, "\n") {
 | 
				
			||||||
 | 
							line = strings.TrimSpace(line)
 | 
				
			||||||
 | 
							if line == "" {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							pidInt, err := strconv.Atoi(line)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return 0, fmt.Errorf("parsing PID: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// We dont care about the root pid for the container
 | 
				
			||||||
 | 
							if pidInt == 1 {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							pids = append(pids, pidInt)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch len(pids) {
 | 
				
			||||||
 | 
						case 0:
 | 
				
			||||||
 | 
							return 0, os.ErrNotExist
 | 
				
			||||||
 | 
						case 1:
 | 
				
			||||||
 | 
							return pids[0], nil
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							return 0, fmt.Errorf("multiple headscale processes running")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Reload sends a SIGHUP to the headscale process to reload internals,
 | 
				
			||||||
 | 
					// for example Policy from file.
 | 
				
			||||||
 | 
					func (h *HeadscaleInContainer) Reload() error {
 | 
				
			||||||
 | 
						pid, err := h.PID()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("getting headscale PID: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_, err = h.Execute([]string{"kill", "-HUP", strconv.Itoa(pid)})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("reloading headscale with HUP: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ApproveRoutes approves routes for a node.
 | 
					// ApproveRoutes approves routes for a node.
 | 
				
			||||||
func (t *HeadscaleInContainer) ApproveRoutes(id uint64, routes []netip.Prefix) (*v1.Node, error) {
 | 
					func (t *HeadscaleInContainer) ApproveRoutes(id uint64, routes []netip.Prefix) (*v1.Node, error) {
 | 
				
			||||||
	command := []string{
 | 
						command := []string{
 | 
				
			||||||
 | 
				
			|||||||
@ -13,6 +13,7 @@ import (
 | 
				
			|||||||
	"github.com/google/go-cmp/cmp/cmpopts"
 | 
						"github.com/google/go-cmp/cmp/cmpopts"
 | 
				
			||||||
	v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
 | 
						v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
 | 
				
			||||||
	policyv1 "github.com/juanfont/headscale/hscontrol/policy/v1"
 | 
						policyv1 "github.com/juanfont/headscale/hscontrol/policy/v1"
 | 
				
			||||||
 | 
						"github.com/juanfont/headscale/hscontrol/types"
 | 
				
			||||||
	"github.com/juanfont/headscale/hscontrol/util"
 | 
						"github.com/juanfont/headscale/hscontrol/util"
 | 
				
			||||||
	"github.com/juanfont/headscale/integration/hsic"
 | 
						"github.com/juanfont/headscale/integration/hsic"
 | 
				
			||||||
	"github.com/juanfont/headscale/integration/tsic"
 | 
						"github.com/juanfont/headscale/integration/tsic"
 | 
				
			||||||
@ -768,178 +769,6 @@ func TestHASubnetRouterFailover(t *testing.T) {
 | 
				
			|||||||
	assertTracerouteViaIP(t, tr, subRouter2.MustIPv4())
 | 
						assertTracerouteViaIP(t, tr, subRouter2.MustIPv4())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestEnableDisableAutoApprovedRoute(t *testing.T) {
 | 
					 | 
				
			||||||
	IntegrationSkip(t)
 | 
					 | 
				
			||||||
	t.Parallel()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	expectedRoutes := "172.0.0.0/24"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	spec := ScenarioSpec{
 | 
					 | 
				
			||||||
		NodesPerUser: 1,
 | 
					 | 
				
			||||||
		Users:        []string{"user1"},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	scenario, err := NewScenario(spec)
 | 
					 | 
				
			||||||
	require.NoErrorf(t, err, "failed to create scenario: %s", err)
 | 
					 | 
				
			||||||
	defer scenario.ShutdownAssertNoPanics(t)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	err = scenario.CreateHeadscaleEnv([]tsic.Option{
 | 
					 | 
				
			||||||
		tsic.WithTags([]string{"tag:approve"}),
 | 
					 | 
				
			||||||
		tsic.WithAcceptRoutes(),
 | 
					 | 
				
			||||||
	}, hsic.WithTestName("clienableroute"), hsic.WithACLPolicy(
 | 
					 | 
				
			||||||
		&policyv1.ACLPolicy{
 | 
					 | 
				
			||||||
			ACLs: []policyv1.ACL{
 | 
					 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					Action:       "accept",
 | 
					 | 
				
			||||||
					Sources:      []string{"*"},
 | 
					 | 
				
			||||||
					Destinations: []string{"*:*"},
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
			TagOwners: map[string][]string{
 | 
					 | 
				
			||||||
				"tag:approve": {"user1@"},
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
			AutoApprovers: policyv1.AutoApprovers{
 | 
					 | 
				
			||||||
				Routes: map[string][]string{
 | 
					 | 
				
			||||||
					expectedRoutes: {"tag:approve"},
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	))
 | 
					 | 
				
			||||||
	assertNoErrHeadscaleEnv(t, err)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	allClients, err := scenario.ListTailscaleClients()
 | 
					 | 
				
			||||||
	assertNoErrListClients(t, err)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	err = scenario.WaitForTailscaleSync()
 | 
					 | 
				
			||||||
	assertNoErrSync(t, err)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	headscale, err := scenario.Headscale()
 | 
					 | 
				
			||||||
	assertNoErrGetHeadscale(t, err)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	subRouter1 := allClients[0]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Initially advertise route
 | 
					 | 
				
			||||||
	command := []string{
 | 
					 | 
				
			||||||
		"tailscale",
 | 
					 | 
				
			||||||
		"set",
 | 
					 | 
				
			||||||
		"--advertise-routes=" + expectedRoutes,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	_, _, err = subRouter1.Execute(command)
 | 
					 | 
				
			||||||
	require.NoErrorf(t, err, "failed to advertise route: %s", err)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	time.Sleep(10 * time.Second)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	nodes, err := headscale.ListNodes()
 | 
					 | 
				
			||||||
	require.NoError(t, err)
 | 
					 | 
				
			||||||
	assert.Len(t, nodes, 1)
 | 
					 | 
				
			||||||
	assertNodeRouteCount(t, nodes[0], 1, 1, 1)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Stop advertising route
 | 
					 | 
				
			||||||
	command = []string{
 | 
					 | 
				
			||||||
		"tailscale",
 | 
					 | 
				
			||||||
		"set",
 | 
					 | 
				
			||||||
		`--advertise-routes=`,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	_, _, err = subRouter1.Execute(command)
 | 
					 | 
				
			||||||
	require.NoErrorf(t, err, "failed to remove advertised route: %s", err)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	time.Sleep(10 * time.Second)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	nodes, err = headscale.ListNodes()
 | 
					 | 
				
			||||||
	require.NoError(t, err)
 | 
					 | 
				
			||||||
	assert.Len(t, nodes, 1)
 | 
					 | 
				
			||||||
	assertNodeRouteCount(t, nodes[0], 0, 1, 0)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Advertise route again
 | 
					 | 
				
			||||||
	command = []string{
 | 
					 | 
				
			||||||
		"tailscale",
 | 
					 | 
				
			||||||
		"set",
 | 
					 | 
				
			||||||
		"--advertise-routes=" + expectedRoutes,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	_, _, err = subRouter1.Execute(command)
 | 
					 | 
				
			||||||
	require.NoErrorf(t, err, "failed to advertise route: %s", err)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	time.Sleep(10 * time.Second)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	nodes, err = headscale.ListNodes()
 | 
					 | 
				
			||||||
	require.NoError(t, err)
 | 
					 | 
				
			||||||
	assert.Len(t, nodes, 1)
 | 
					 | 
				
			||||||
	assertNodeRouteCount(t, nodes[0], 1, 1, 1)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestAutoApprovedSubRoute2068(t *testing.T) {
 | 
					 | 
				
			||||||
	IntegrationSkip(t)
 | 
					 | 
				
			||||||
	t.Parallel()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	expectedRoutes := "10.42.7.0/24"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	user := "user1"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	spec := ScenarioSpec{
 | 
					 | 
				
			||||||
		NodesPerUser: 1,
 | 
					 | 
				
			||||||
		Users:        []string{user},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	scenario, err := NewScenario(spec)
 | 
					 | 
				
			||||||
	require.NoErrorf(t, err, "failed to create scenario: %s", err)
 | 
					 | 
				
			||||||
	defer scenario.ShutdownAssertNoPanics(t)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	err = scenario.CreateHeadscaleEnv([]tsic.Option{
 | 
					 | 
				
			||||||
		tsic.WithTags([]string{"tag:approve"}),
 | 
					 | 
				
			||||||
		tsic.WithAcceptRoutes(),
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
		hsic.WithTestName("clienableroute"),
 | 
					 | 
				
			||||||
		hsic.WithEmbeddedDERPServerOnly(),
 | 
					 | 
				
			||||||
		hsic.WithTLS(),
 | 
					 | 
				
			||||||
		hsic.WithACLPolicy(
 | 
					 | 
				
			||||||
			&policyv1.ACLPolicy{
 | 
					 | 
				
			||||||
				ACLs: []policyv1.ACL{
 | 
					 | 
				
			||||||
					{
 | 
					 | 
				
			||||||
						Action:       "accept",
 | 
					 | 
				
			||||||
						Sources:      []string{"*"},
 | 
					 | 
				
			||||||
						Destinations: []string{"*:*"},
 | 
					 | 
				
			||||||
					},
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
				TagOwners: map[string][]string{
 | 
					 | 
				
			||||||
					"tag:approve": {user + "@"},
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
				AutoApprovers: policyv1.AutoApprovers{
 | 
					 | 
				
			||||||
					Routes: map[string][]string{
 | 
					 | 
				
			||||||
						"10.42.0.0/16": {"tag:approve"},
 | 
					 | 
				
			||||||
					},
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		))
 | 
					 | 
				
			||||||
	assertNoErrHeadscaleEnv(t, err)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	allClients, err := scenario.ListTailscaleClients()
 | 
					 | 
				
			||||||
	assertNoErrListClients(t, err)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	err = scenario.WaitForTailscaleSync()
 | 
					 | 
				
			||||||
	assertNoErrSync(t, err)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	headscale, err := scenario.Headscale()
 | 
					 | 
				
			||||||
	assertNoErrGetHeadscale(t, err)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	subRouter1 := allClients[0]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Initially advertise route
 | 
					 | 
				
			||||||
	command := []string{
 | 
					 | 
				
			||||||
		"tailscale",
 | 
					 | 
				
			||||||
		"set",
 | 
					 | 
				
			||||||
		"--advertise-routes=" + expectedRoutes,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	_, _, err = subRouter1.Execute(command)
 | 
					 | 
				
			||||||
	require.NoErrorf(t, err, "failed to advertise route: %s", err)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	time.Sleep(10 * time.Second)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	nodes, err := headscale.ListNodes()
 | 
					 | 
				
			||||||
	require.NoError(t, err)
 | 
					 | 
				
			||||||
	assert.Len(t, nodes, 1)
 | 
					 | 
				
			||||||
	assertNodeRouteCount(t, nodes[0], 1, 1, 1)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// TestSubnetRouteACL verifies that Subnet routes are distributed
 | 
					// TestSubnetRouteACL verifies that Subnet routes are distributed
 | 
				
			||||||
// as expected when ACLs are activated.
 | 
					// as expected when ACLs are activated.
 | 
				
			||||||
// It implements the issue from
 | 
					// It implements the issue from
 | 
				
			||||||
@ -1390,7 +1219,6 @@ func TestSubnetRouterMultiNetwork(t *testing.T) {
 | 
				
			|||||||
	assertTracerouteViaIP(t, tr, user1c.MustIPv4())
 | 
						assertTracerouteViaIP(t, tr, user1c.MustIPv4())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// TestSubnetRouterMultiNetworkExitNode
 | 
					 | 
				
			||||||
func TestSubnetRouterMultiNetworkExitNode(t *testing.T) {
 | 
					func TestSubnetRouterMultiNetworkExitNode(t *testing.T) {
 | 
				
			||||||
	IntegrationSkip(t)
 | 
						IntegrationSkip(t)
 | 
				
			||||||
	t.Parallel()
 | 
						t.Parallel()
 | 
				
			||||||
@ -1469,10 +1297,7 @@ func TestSubnetRouterMultiNetworkExitNode(t *testing.T) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Enable route
 | 
						// Enable route
 | 
				
			||||||
	_, err = headscale.ApproveRoutes(
 | 
						_, err = headscale.ApproveRoutes(nodes[0].Id, []netip.Prefix{tsaddr.AllIPv4()})
 | 
				
			||||||
		nodes[0].Id,
 | 
					 | 
				
			||||||
		[]netip.Prefix{tsaddr.AllIPv4()},
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
	require.NoError(t, err)
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	time.Sleep(5 * time.Second)
 | 
						time.Sleep(5 * time.Second)
 | 
				
			||||||
@ -1524,6 +1349,380 @@ func TestSubnetRouterMultiNetworkExitNode(t *testing.T) {
 | 
				
			|||||||
	require.NoError(t, err)
 | 
						require.NoError(t, err)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TestAutoApproveMultiNetwork tests auto approving of routes
 | 
				
			||||||
 | 
					// by setting up two networks where network1 has three subnet
 | 
				
			||||||
 | 
					// routers:
 | 
				
			||||||
 | 
					// - routerUsernet1: advertising the docker network
 | 
				
			||||||
 | 
					// - routerSubRoute: advertising a subroute, a /24 inside a auto approved /16
 | 
				
			||||||
 | 
					// - routeExitNode: advertising an exit node
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Each router is tested step by step through the following scenarios
 | 
				
			||||||
 | 
					//   - Policy is set to auto approve the nodes route
 | 
				
			||||||
 | 
					//   - Node advertises route and it is verified that it is auto approved and sent to nodes
 | 
				
			||||||
 | 
					//   - Policy is changed to _not_ auto approve the route
 | 
				
			||||||
 | 
					//   - Verify that peers can still see the node
 | 
				
			||||||
 | 
					//   - Disable route, making it unavailable
 | 
				
			||||||
 | 
					//   - Verify that peers can no longer use node
 | 
				
			||||||
 | 
					//   - Policy is changed back to auto approve route, check that routes already existing is approved.
 | 
				
			||||||
 | 
					//   - Verify that routes can now be seen by peers.
 | 
				
			||||||
 | 
					func TestAutoApproveMultiNetwork(t *testing.T) {
 | 
				
			||||||
 | 
						IntegrationSkip(t)
 | 
				
			||||||
 | 
						t.Parallel()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						spec := ScenarioSpec{
 | 
				
			||||||
 | 
							NodesPerUser: 3,
 | 
				
			||||||
 | 
							Users:        []string{"user1", "user2"},
 | 
				
			||||||
 | 
							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"},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rootRoute := netip.MustParsePrefix("10.42.0.0/16")
 | 
				
			||||||
 | 
						subRoute := netip.MustParsePrefix("10.42.7.0/24")
 | 
				
			||||||
 | 
						notApprovedRoute := netip.MustParsePrefix("192.168.0.0/24")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						scenario, err := NewScenario(spec)
 | 
				
			||||||
 | 
						require.NoErrorf(t, err, "failed to create scenario: %s", err)
 | 
				
			||||||
 | 
						defer scenario.ShutdownAssertNoPanics(t)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						pol := &policyv1.ACLPolicy{
 | 
				
			||||||
 | 
							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"},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = scenario.CreateHeadscaleEnv([]tsic.Option{
 | 
				
			||||||
 | 
							tsic.WithAcceptRoutes(),
 | 
				
			||||||
 | 
							tsic.WithTags([]string{"tag:approve"}),
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
							hsic.WithTestName("clienableroute"),
 | 
				
			||||||
 | 
							hsic.WithEmbeddedDERPServerOnly(),
 | 
				
			||||||
 | 
							hsic.WithTLS(),
 | 
				
			||||||
 | 
							hsic.WithACLPolicy(pol),
 | 
				
			||||||
 | 
							hsic.WithPolicyMode(types.PolicyModeDB),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						assertNoErrHeadscaleEnv(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						allClients, err := scenario.ListTailscaleClients()
 | 
				
			||||||
 | 
						assertNoErrListClients(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = scenario.WaitForTailscaleSync()
 | 
				
			||||||
 | 
						assertNoErrSync(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						headscale, err := scenario.Headscale()
 | 
				
			||||||
 | 
						assertNoErrGetHeadscale(t, err)
 | 
				
			||||||
 | 
						assert.NotNil(t, headscale)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						route, err := scenario.SubnetOfNetwork("usernet1")
 | 
				
			||||||
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Set the route of usernet1 to be autoapproved
 | 
				
			||||||
 | 
						pol.AutoApprovers.Routes[route.String()] = []string{"tag:approve"}
 | 
				
			||||||
 | 
						err = headscale.SetPolicy(pol)
 | 
				
			||||||
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						services, err := scenario.Services("usernet1")
 | 
				
			||||||
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
						require.Len(t, services, 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						usernet1, err := scenario.Network("usernet1")
 | 
				
			||||||
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						web := services[0]
 | 
				
			||||||
 | 
						webip := netip.MustParseAddr(web.GetIPInNetwork(usernet1))
 | 
				
			||||||
 | 
						weburl := fmt.Sprintf("http://%s/etc/hostname", webip)
 | 
				
			||||||
 | 
						t.Logf("webservice: %s, %s", webip.String(), weburl)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Sort nodes by ID
 | 
				
			||||||
 | 
						sort.SliceStable(allClients, func(i, j int) bool {
 | 
				
			||||||
 | 
							statusI := allClients[i].MustStatus()
 | 
				
			||||||
 | 
							statusJ := allClients[j].MustStatus()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return statusI.Self.ID < statusJ.Self.ID
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// This is ok because the scenario makes users in order, so the three first
 | 
				
			||||||
 | 
						// nodes, which are subnet routes, will be created first, and the last user
 | 
				
			||||||
 | 
						// will be created with the second.
 | 
				
			||||||
 | 
						routerUsernet1 := allClients[0]
 | 
				
			||||||
 | 
						routerSubRoute := allClients[1]
 | 
				
			||||||
 | 
						routerExitNode := allClients[2]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						client := allClients[3]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Advertise the route for the dockersubnet of user1
 | 
				
			||||||
 | 
						command := []string{
 | 
				
			||||||
 | 
							"tailscale",
 | 
				
			||||||
 | 
							"set",
 | 
				
			||||||
 | 
							"--advertise-routes=" + route.String(),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						_, _, err = routerUsernet1.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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 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 {
 | 
				
			||||||
 | 
								requirePeerSubnetRoutes(t, peerStatus, nil)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						url := fmt.Sprintf("http://%s/etc/hostname", webip)
 | 
				
			||||||
 | 
						t.Logf("url from %s to %s", client.Hostname(), url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						result, err := client.Curl(url)
 | 
				
			||||||
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
						assert.Len(t, result, 13)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tr, err := client.Traceroute(webip)
 | 
				
			||||||
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
						assertTracerouteViaIP(t, tr, routerUsernet1.MustIPv4())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Remove the auto approval from the policy, any routes already enabled should be allowed.
 | 
				
			||||||
 | 
						delete(pol.AutoApprovers.Routes, route.String())
 | 
				
			||||||
 | 
						err = headscale.SetPolicy(pol)
 | 
				
			||||||
 | 
						require.NoError(t, 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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 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 {
 | 
				
			||||||
 | 
								requirePeerSubnetRoutes(t, peerStatus, nil)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						url = fmt.Sprintf("http://%s/etc/hostname", webip)
 | 
				
			||||||
 | 
						t.Logf("url from %s to %s", client.Hostname(), url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						result, err = client.Curl(url)
 | 
				
			||||||
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
						assert.Len(t, result, 13)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tr, err = client.Traceroute(webip)
 | 
				
			||||||
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
						assertTracerouteViaIP(t, tr, routerUsernet1.MustIPv4())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Disable the route, making it unavailable since it is no longer auto-approved
 | 
				
			||||||
 | 
						_, err = headscale.ApproveRoutes(
 | 
				
			||||||
 | 
							nodes[0].GetId(),
 | 
				
			||||||
 | 
							[]netip.Prefix{},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						require.NoError(t, 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, 0, 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 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]
 | 
				
			||||||
 | 
							requirePeerSubnetRoutes(t, peerStatus, nil)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Add the route back to the auto approver in the policy, the route should
 | 
				
			||||||
 | 
						// now become available again.
 | 
				
			||||||
 | 
						pol.AutoApprovers.Routes[route.String()] = []string{"tag:approve"}
 | 
				
			||||||
 | 
						err = headscale.SetPolicy(pol)
 | 
				
			||||||
 | 
						require.NoError(t, 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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 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" {
 | 
				
			||||||
 | 
								require.NotNil(t, peerStatus.PrimaryRoutes)
 | 
				
			||||||
 | 
								assert.Contains(t, peerStatus.PrimaryRoutes.AsSlice(), *route)
 | 
				
			||||||
 | 
								requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{*route})
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								requirePeerSubnetRoutes(t, peerStatus, nil)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						url = fmt.Sprintf("http://%s/etc/hostname", webip)
 | 
				
			||||||
 | 
						t.Logf("url from %s to %s", client.Hostname(), url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						result, err = client.Curl(url)
 | 
				
			||||||
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
						assert.Len(t, result, 13)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tr, err = client.Traceroute(webip)
 | 
				
			||||||
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
						assertTracerouteViaIP(t, tr, routerUsernet1.MustIPv4())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Advertise and validate a subnet of an auto approved route, /24 inside the
 | 
				
			||||||
 | 
						// auto approved /16.
 | 
				
			||||||
 | 
						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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 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 == "2" {
 | 
				
			||||||
 | 
								assert.Contains(t, peerStatus.PrimaryRoutes.AsSlice(), subRoute)
 | 
				
			||||||
 | 
								requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{subRoute})
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								requirePeerSubnetRoutes(t, peerStatus, nil)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Advertise a not approved route will not end up anywhere
 | 
				
			||||||
 | 
						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, 0)
 | 
				
			||||||
 | 
						assertNodeRouteCount(t, nodes[2], 0, 0, 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 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 {
 | 
				
			||||||
 | 
								requirePeerSubnetRoutes(t, peerStatus, nil)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Exit routes are also automatically approved
 | 
				
			||||||
 | 
						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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						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)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func assertTracerouteViaIP(t *testing.T, tr util.Traceroute, ip netip.Addr) {
 | 
					func assertTracerouteViaIP(t *testing.T, tr util.Traceroute, ip netip.Addr) {
 | 
				
			||||||
	t.Helper()
 | 
						t.Helper()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user