Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/workflows/test-integration-policyv2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,11 @@ jobs:
- Test2118DeletingOnlineNodePanics
- TestEnablingRoutes
- TestHASubnetRouterFailover
- TestEnableDisableAutoApprovedRoute
- TestAutoApprovedSubRoute2068
- TestSubnetRouteACL
- TestEnablingExitRoutes
- TestSubnetRouterMultiNetwork
- TestSubnetRouterMultiNetworkExitNode
- TestAutoApproveMultiNetwork
- TestHeadscale
- TestTailscaleNodesJoiningHeadcale
- TestSSHOneUserToAll
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/test-integration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,11 @@ jobs:
- Test2118DeletingOnlineNodePanics
- TestEnablingRoutes
- TestHASubnetRouterFailover
- TestEnableDisableAutoApprovedRoute
- TestAutoApprovedSubRoute2068
- TestSubnetRouteACL
- TestEnablingExitRoutes
- TestSubnetRouterMultiNetwork
- TestSubnetRouterMultiNetworkExitNode
- TestAutoApproveMultiNetwork
- TestHeadscale
- TestTailscaleNodesJoiningHeadcale
- TestSSHOneUserToAll
Expand Down
38 changes: 38 additions & 0 deletions hscontrol/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,11 @@ func (h *Headscale) Serve() error {
log.Info().
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")
h.nodeNotifier.NotifyAll(ctx, types.UpdateFull())
}
Expand Down Expand Up @@ -1166,3 +1171,36 @@ func (h *Headscale) loadPolicyManager() error {

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
}
13 changes: 10 additions & 3 deletions hscontrol/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"time"

"github.com/juanfont/headscale/hscontrol/db"
"github.com/juanfont/headscale/hscontrol/policy"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util"
"gorm.io/gorm"
Expand Down Expand Up @@ -212,6 +213,9 @@ func (h *Headscale) handleRegisterWithAuthKey(
nodeToRegister.Expiry = &regReq.Expiry
}

// Ensure any auto approved routes are handled before saving.
policy.AutoApproveRoutes(h.polMan, &nodeToRegister)

ipv4, ipv6, err := h.ipAlloc.Next()
if err != nil {
return nil, fmt.Errorf("allocating IPs: %w", err)
Expand Down Expand Up @@ -266,7 +270,7 @@ func (h *Headscale) handleRegisterInteractive(
return nil, fmt.Errorf("generating registration ID: %w", err)
}

newNode := types.RegisterNode{
nodeToRegister := types.RegisterNode{
Node: types.Node{
Hostname: regReq.Hostinfo.Hostname,
MachineKey: machineKey,
Expand All @@ -278,12 +282,15 @@ func (h *Headscale) handleRegisterInteractive(
}

if !regReq.Expiry.IsZero() {
newNode.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(
registrationId,
newNode,
nodeToRegister,
)

return &tailcfg.RegisterResponse{
Expand Down
5 changes: 5 additions & 0 deletions hscontrol/grpcv1.go
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,11 @@ func (api headscaleV1APIServer) SetPolicy(

// Only send update if the packet filter has changed.
if changed {
err = api.h.autoApproveNodes()
if err != nil {
return nil, err
}

ctx := types.NotifyCtx(context.Background(), "acl-update", "na")
api.h.nodeNotifier.NotifyAll(ctx, types.UpdateFull())
}
Expand Down
12 changes: 8 additions & 4 deletions hscontrol/policy/v1/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,15 @@ func NewPolicyManager(polB []byte, users []types.User, nodes types.Nodes) (*Poli
}

type PolicyManager struct {
mu sync.Mutex
pol *ACLPolicy
mu sync.Mutex
pol *ACLPolicy
polHash deephash.Sum

users []types.User
nodes types.Nodes

filterHash deephash.Sum
filter []tailcfg.FilterRule
filterHash deephash.Sum
}

// updateLocked updates the filter rules based on the current policy and nodes.
Expand All @@ -71,13 +72,16 @@ func (pm *PolicyManager) updateLocked() (bool, error) {
return false, fmt.Errorf("compiling filter rules: %w", err)
}

polHash := deephash.Hash(pm.pol)
filterHash := deephash.Hash(&filter)
if filterHash == pm.filterHash {

if polHash == pm.polHash && filterHash == pm.filterHash {
return false, nil
}

pm.filter = filter
pm.filterHash = filterHash
pm.polHash = polHash

return true, nil
}
Expand Down
25 changes: 3 additions & 22 deletions integration/acl_test.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package integration

import (
"encoding/json"
"fmt"
"net/netip"
"strings"
"testing"

"github.com/google/go-cmp/cmp"
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/tsic"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -1033,9 +1033,7 @@ func TestPolicyUpdateWhileRunningWithCLIInDatabase(t *testing.T) {
tsic.WithDockerWorkdir("/"),
},
hsic.WithTestName("policyreload"),
hsic.WithConfigEnv(map[string]string{
"HEADSCALE_POLICY_MODE": "database",
}),
hsic.WithPolicyMode(types.PolicyModeDB),
)
require.NoError(t, err)

Expand Down Expand Up @@ -1086,24 +1084,7 @@ func TestPolicyUpdateWhileRunningWithCLIInDatabase(t *testing.T) {
Hosts: policyv1.Hosts{},
}

pBytes, _ := json.Marshal(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,
},
)
err = headscale.SetPolicy(&p)
require.NoError(t, err)

// Get the current policy and check
Expand Down
2 changes: 2 additions & 0 deletions integration/control.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"net/netip"

v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
policyv1 "github.com/juanfont/headscale/hscontrol/policy/v1"
"github.com/ory/dockertest/v3"
)

Expand All @@ -24,4 +25,5 @@ type ControlServer interface {
ApproveRoutes(uint64, []netip.Prefix) (*v1.Node, error)
GetCert() []byte
GetHostname() string
SetPolicy(*policyv1.ACLPolicy) error
}
Loading
Loading