Redo route code (#2422)

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
Kristoffer Dalby 2025-02-26 07:22:55 -08:00 committed by GitHub
parent 16868190c8
commit 7891378f57
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 2977 additions and 6251 deletions

View file

@ -18,6 +18,7 @@ import (
"github.com/juanfont/headscale/hscontrol/db"
"github.com/juanfont/headscale/hscontrol/notifier"
"github.com/juanfont/headscale/hscontrol/policy"
"github.com/juanfont/headscale/hscontrol/routes"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/klauspost/compress/zstd"
@ -56,6 +57,7 @@ type Mapper struct {
derpMap *tailcfg.DERPMap
notif *notifier.Notifier
polMan policy.PolicyManager
primary *routes.PrimaryRoutes
uid string
created time.Time
@ -73,6 +75,7 @@ func NewMapper(
derpMap *tailcfg.DERPMap,
notif *notifier.Notifier,
polMan policy.PolicyManager,
primary *routes.PrimaryRoutes,
) *Mapper {
uid, _ := util.GenerateRandomStringDNSSafe(mapperIDLength)
@ -82,6 +85,7 @@ func NewMapper(
derpMap: derpMap,
notif: notif,
polMan: polMan,
primary: primary,
uid: uid,
created: time.Now(),
@ -97,15 +101,22 @@ func generateUserProfiles(
node *types.Node,
peers types.Nodes,
) []tailcfg.UserProfile {
userMap := make(map[uint]types.User)
userMap[node.User.ID] = node.User
userMap := make(map[uint]*types.User)
ids := make([]uint, 0, len(userMap))
userMap[node.User.ID] = &node.User
ids = append(ids, node.User.ID)
for _, peer := range peers {
userMap[peer.User.ID] = peer.User // not worth checking if already is there
userMap[peer.User.ID] = &peer.User
ids = append(ids, peer.User.ID)
}
slices.Sort(ids)
slices.Compact(ids)
var profiles []tailcfg.UserProfile
for _, user := range userMap {
profiles = append(profiles, user.TailscaleUserProfile())
for _, id := range ids {
if userMap[id] != nil {
profiles = append(profiles, userMap[id].TailscaleUserProfile())
}
}
return profiles
@ -166,6 +177,7 @@ func (m *Mapper) fullMapResponse(
resp,
true, // full change
m.polMan,
m.primary,
node,
capVer,
peers,
@ -271,6 +283,7 @@ func (m *Mapper) PeerChangedResponse(
&resp,
false, // partial change
m.polMan,
m.primary,
node,
mapRequest.Version,
changedNodes,
@ -299,7 +312,7 @@ func (m *Mapper) PeerChangedResponse(
// Add the node itself, it might have changed, and particularly
// if there are no patches or changes, this is a self update.
tailnode, err := tailNode(node, mapRequest.Version, m.polMan, m.cfg)
tailnode, err := tailNode(node, mapRequest.Version, m.polMan, m.primary, m.cfg)
if err != nil {
return nil, err
}
@ -446,7 +459,7 @@ func (m *Mapper) baseWithConfigMapResponse(
) (*tailcfg.MapResponse, error) {
resp := m.baseMapResponse()
tailnode, err := tailNode(node, capVer, m.polMan, m.cfg)
tailnode, err := tailNode(node, capVer, m.polMan, m.primary, m.cfg)
if err != nil {
return nil, err
}
@ -500,6 +513,7 @@ func appendPeerChanges(
fullChange bool,
polMan policy.PolicyManager,
primary *routes.PrimaryRoutes,
node *types.Node,
capVer tailcfg.CapabilityVersion,
changed types.Nodes,
@ -522,7 +536,7 @@ func appendPeerChanges(
dnsConfig := generateDNSConfig(cfg, node)
tailPeers, err := tailNodes(changed, capVer, polMan, cfg)
tailPeers, err := tailNodes(changed, capVer, polMan, primary, cfg)
if err != nil {
return err
}

View file

@ -6,12 +6,11 @@ import (
"testing"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/juanfont/headscale/hscontrol/policy"
"github.com/juanfont/headscale/hscontrol/routes"
"github.com/juanfont/headscale/hscontrol/types"
"gopkg.in/check.v1"
"gorm.io/gorm"
"tailscale.com/net/tsaddr"
"tailscale.com/tailcfg"
@ -24,51 +23,6 @@ var iap = func(ipStr string) *netip.Addr {
return &ip
}
func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
mach := func(hostname, username string, userid uint) *types.Node {
return &types.Node{
Hostname: hostname,
UserID: userid,
User: types.User{
Model: gorm.Model{
ID: userid,
},
Name: username,
},
}
}
nodeInShared1 := mach("test_get_shared_nodes_1", "user1", 1)
nodeInShared2 := mach("test_get_shared_nodes_2", "user2", 2)
nodeInShared3 := mach("test_get_shared_nodes_3", "user3", 3)
node2InShared1 := mach("test_get_shared_nodes_4", "user1", 1)
userProfiles := generateUserProfiles(
nodeInShared1,
types.Nodes{
nodeInShared2, nodeInShared3, node2InShared1,
},
)
c.Assert(len(userProfiles), check.Equals, 3)
users := []string{
"user1", "user2", "user3",
}
for _, user := range users {
found := false
for _, userProfile := range userProfiles {
if userProfile.DisplayName == user {
found = true
break
}
}
c.Assert(found, check.Equals, true)
}
}
func TestDNSConfigMapResponse(t *testing.T) {
tests := []struct {
magicDNS bool
@ -159,11 +113,11 @@ func Test_fullMapResponse(t *testing.T) {
lastSeen := time.Date(2009, time.November, 10, 23, 9, 0, 0, time.UTC)
expire := time.Date(2500, time.November, 11, 23, 0, 0, 0, time.UTC)
user1 := types.User{Model: gorm.Model{ID: 0}, Name: "mini"}
user2 := types.User{Model: gorm.Model{ID: 1}, Name: "peer2"}
user1 := types.User{Model: gorm.Model{ID: 1}, Name: "user1"}
user2 := types.User{Model: gorm.Model{ID: 2}, Name: "user2"}
mini := &types.Node{
ID: 0,
ID: 1,
MachineKey: mustMK(
"mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
),
@ -182,35 +136,22 @@ func Test_fullMapResponse(t *testing.T) {
AuthKey: &types.PreAuthKey{},
LastSeen: &lastSeen,
Expiry: &expire,
Hostinfo: &tailcfg.Hostinfo{},
Routes: []types.Route{
{
Prefix: tsaddr.AllIPv4(),
Advertised: true,
Enabled: true,
IsPrimary: false,
},
{
Prefix: netip.MustParsePrefix("192.168.0.0/24"),
Advertised: true,
Enabled: true,
IsPrimary: true,
},
{
Prefix: netip.MustParsePrefix("172.0.0.0/10"),
Advertised: true,
Enabled: false,
IsPrimary: true,
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{
tsaddr.AllIPv4(),
netip.MustParsePrefix("192.168.0.0/24"),
netip.MustParsePrefix("172.0.0.0/10"),
},
},
CreatedAt: created,
ApprovedRoutes: []netip.Prefix{tsaddr.AllIPv4(), netip.MustParsePrefix("192.168.0.0/24")},
CreatedAt: created,
}
tailMini := &tailcfg.Node{
ID: 0,
StableID: "0",
ID: 1,
StableID: "1",
Name: "mini",
User: 0,
User: tailcfg.UserID(user1.ID),
Key: mustNK(
"nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
),
@ -227,12 +168,17 @@ func Test_fullMapResponse(t *testing.T) {
tsaddr.AllIPv4(),
netip.MustParsePrefix("192.168.0.0/24"),
},
HomeDERP: 0,
LegacyDERPString: "127.3.3.40:0",
Hostinfo: hiview(tailcfg.Hostinfo{}),
HomeDERP: 0,
LegacyDERPString: "127.3.3.40:0",
Hostinfo: hiview(tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{
tsaddr.AllIPv4(),
netip.MustParsePrefix("192.168.0.0/24"),
netip.MustParsePrefix("172.0.0.0/10"),
},
}),
Created: created,
Tags: []string{},
PrimaryRoutes: []netip.Prefix{netip.MustParsePrefix("192.168.0.0/24")},
LastSeen: &lastSeen,
MachineAuthorized: true,
@ -244,7 +190,7 @@ func Test_fullMapResponse(t *testing.T) {
}
peer1 := &types.Node{
ID: 1,
ID: 2,
MachineKey: mustMK(
"mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
),
@ -257,20 +203,20 @@ func Test_fullMapResponse(t *testing.T) {
IPv4: iap("100.64.0.2"),
Hostname: "peer1",
GivenName: "peer1",
UserID: user1.ID,
User: user1,
UserID: user2.ID,
User: user2,
ForcedTags: []string{},
LastSeen: &lastSeen,
Expiry: &expire,
Hostinfo: &tailcfg.Hostinfo{},
Routes: []types.Route{},
CreatedAt: created,
}
tailPeer1 := &tailcfg.Node{
ID: 1,
StableID: "1",
ID: 2,
StableID: "2",
Name: "peer1",
User: tailcfg.UserID(user2.ID),
Key: mustNK(
"nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
),
@ -288,7 +234,6 @@ func Test_fullMapResponse(t *testing.T) {
Hostinfo: hiview(tailcfg.Hostinfo{}),
Created: created,
Tags: []string{},
PrimaryRoutes: []netip.Prefix{},
LastSeen: &lastSeen,
MachineAuthorized: true,
@ -299,30 +244,6 @@ func Test_fullMapResponse(t *testing.T) {
},
}
peer2 := &types.Node{
ID: 2,
MachineKey: mustMK(
"mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
),
NodeKey: mustNK(
"nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
),
DiscoKey: mustDK(
"discokey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
),
IPv4: iap("100.64.0.3"),
Hostname: "peer2",
GivenName: "peer2",
UserID: user2.ID,
User: user2,
ForcedTags: []string{},
LastSeen: &lastSeen,
Expiry: &expire,
Hostinfo: &tailcfg.Hostinfo{},
Routes: []types.Route{},
CreatedAt: created,
}
tests := []struct {
name string
pol *policy.ACLPolicy
@ -364,7 +285,7 @@ func Test_fullMapResponse(t *testing.T) {
Domain: "",
CollectServices: "false",
PacketFilter: []tailcfg.FilterRule{},
UserProfiles: []tailcfg.UserProfile{{LoginName: "mini", DisplayName: "mini"}},
UserProfiles: []tailcfg.UserProfile{{ID: tailcfg.UserID(user1.ID), LoginName: "user1", DisplayName: "user1"}},
SSHPolicy: &tailcfg.SSHPolicy{Rules: []*tailcfg.SSHRule{}},
ControlTime: &time.Time{},
Debug: &tailcfg.Debug{
@ -398,9 +319,12 @@ func Test_fullMapResponse(t *testing.T) {
Domain: "",
CollectServices: "false",
PacketFilter: []tailcfg.FilterRule{},
UserProfiles: []tailcfg.UserProfile{{LoginName: "mini", DisplayName: "mini"}},
SSHPolicy: &tailcfg.SSHPolicy{Rules: []*tailcfg.SSHRule{}},
ControlTime: &time.Time{},
UserProfiles: []tailcfg.UserProfile{
{ID: tailcfg.UserID(user1.ID), LoginName: "user1", DisplayName: "user1"},
{ID: tailcfg.UserID(user2.ID), LoginName: "user2", DisplayName: "user2"},
},
SSHPolicy: &tailcfg.SSHPolicy{Rules: []*tailcfg.SSHRule{}},
ControlTime: &time.Time{},
Debug: &tailcfg.Debug{
DisableLogTail: true,
},
@ -410,6 +334,9 @@ func Test_fullMapResponse(t *testing.T) {
{
name: "with-pol-map-response",
pol: &policy.ACLPolicy{
Hosts: policy.Hosts{
"mini": netip.MustParsePrefix("100.64.0.1/32"),
},
ACLs: []policy.ACL{
{
Action: "accept",
@ -421,7 +348,6 @@ func Test_fullMapResponse(t *testing.T) {
node: mini,
peers: types.Nodes{
peer1,
peer2,
},
derpMap: &tailcfg.DERPMap{},
cfg: &types.Config{
@ -449,7 +375,8 @@ func Test_fullMapResponse(t *testing.T) {
},
},
UserProfiles: []tailcfg.UserProfile{
{LoginName: "mini", DisplayName: "mini"},
{ID: tailcfg.UserID(user1.ID), LoginName: "user1", DisplayName: "user1"},
{ID: tailcfg.UserID(user2.ID), LoginName: "user2", DisplayName: "user2"},
},
SSHPolicy: &tailcfg.SSHPolicy{Rules: []*tailcfg.SSHRule{}},
ControlTime: &time.Time{},
@ -464,6 +391,12 @@ func Test_fullMapResponse(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
polMan, _ := policy.NewPolicyManagerForTest(tt.pol, []types.User{user1, user2}, append(tt.peers, tt.node))
primary := routes.New()
primary.SetRoutes(tt.node.ID, tt.node.SubnetRoutes()...)
for _, peer := range tt.peers {
primary.SetRoutes(peer.ID, peer.SubnetRoutes()...)
}
mappy := NewMapper(
nil,
@ -471,6 +404,7 @@ func Test_fullMapResponse(t *testing.T) {
tt.derpMap,
nil,
polMan,
primary,
)
got, err := mappy.fullMapResponse(
@ -485,8 +419,6 @@ func Test_fullMapResponse(t *testing.T) {
return
}
spew.Dump(got)
if diff := cmp.Diff(
tt.want,
got,

View file

@ -6,6 +6,7 @@ import (
"time"
"github.com/juanfont/headscale/hscontrol/policy"
"github.com/juanfont/headscale/hscontrol/routes"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/samber/lo"
"tailscale.com/tailcfg"
@ -15,6 +16,7 @@ func tailNodes(
nodes types.Nodes,
capVer tailcfg.CapabilityVersion,
polMan policy.PolicyManager,
primary *routes.PrimaryRoutes,
cfg *types.Config,
) ([]*tailcfg.Node, error) {
tNodes := make([]*tailcfg.Node, len(nodes))
@ -24,6 +26,7 @@ func tailNodes(
node,
capVer,
polMan,
primary,
cfg,
)
if err != nil {
@ -41,6 +44,7 @@ func tailNode(
node *types.Node,
capVer tailcfg.CapabilityVersion,
polMan policy.PolicyManager,
primary *routes.PrimaryRoutes,
cfg *types.Config,
) (*tailcfg.Node, error) {
addrs := node.Prefixes()
@ -49,17 +53,8 @@ func tailNode(
[]netip.Prefix{},
addrs...) // we append the node own IP, as it is required by the clients
primaryPrefixes := []netip.Prefix{}
for _, route := range node.Routes {
if route.Enabled {
if route.IsPrimary {
allowedIPs = append(allowedIPs, netip.Prefix(route.Prefix))
primaryPrefixes = append(primaryPrefixes, netip.Prefix(route.Prefix))
} else if route.IsExitRoute() {
allowedIPs = append(allowedIPs, netip.Prefix(route.Prefix))
}
}
for _, route := range node.SubnetRoutes() {
allowedIPs = append(allowedIPs, netip.Prefix(route))
}
var derp int
@ -103,6 +98,7 @@ func tailNode(
Machine: node.MachineKey,
DiscoKey: node.DiscoKey,
Addresses: addrs,
PrimaryRoutes: primary.PrimaryRoutes(node.ID),
AllowedIPs: allowedIPs,
Endpoints: node.Endpoints,
HomeDERP: derp,
@ -114,8 +110,6 @@ func tailNode(
Tags: tags,
PrimaryRoutes: primaryPrefixes,
MachineAuthorized: !node.IsExpired(),
Expired: node.IsExpired(),
}

View file

@ -9,6 +9,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/juanfont/headscale/hscontrol/policy"
"github.com/juanfont/headscale/hscontrol/routes"
"github.com/juanfont/headscale/hscontrol/types"
"tailscale.com/net/tsaddr"
"tailscale.com/tailcfg"
@ -72,7 +73,6 @@ func TestTailNode(t *testing.T) {
LegacyDERPString: "127.3.3.40:0",
Hostinfo: hiview(tailcfg.Hostinfo{}),
Tags: []string{},
PrimaryRoutes: []netip.Prefix{},
MachineAuthorized: true,
CapMap: tailcfg.NodeCapMap{
@ -107,28 +107,15 @@ func TestTailNode(t *testing.T) {
AuthKey: &types.PreAuthKey{},
LastSeen: &lastSeen,
Expiry: &expire,
Hostinfo: &tailcfg.Hostinfo{},
Routes: []types.Route{
{
Prefix: tsaddr.AllIPv4(),
Advertised: true,
Enabled: true,
IsPrimary: false,
},
{
Prefix: netip.MustParsePrefix("192.168.0.0/24"),
Advertised: true,
Enabled: true,
IsPrimary: true,
},
{
Prefix: netip.MustParsePrefix("172.0.0.0/10"),
Advertised: true,
Enabled: false,
IsPrimary: true,
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{
tsaddr.AllIPv4(),
netip.MustParsePrefix("192.168.0.0/24"),
netip.MustParsePrefix("172.0.0.0/10"),
},
},
CreatedAt: created,
ApprovedRoutes: []netip.Prefix{tsaddr.AllIPv4(), netip.MustParsePrefix("192.168.0.0/24")},
CreatedAt: created,
},
pol: &policy.ACLPolicy{},
dnsConfig: &tailcfg.DNSConfig{},
@ -159,8 +146,14 @@ func TestTailNode(t *testing.T) {
},
HomeDERP: 0,
LegacyDERPString: "127.3.3.40:0",
Hostinfo: hiview(tailcfg.Hostinfo{}),
Created: created,
Hostinfo: hiview(tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{
tsaddr.AllIPv4(),
netip.MustParsePrefix("192.168.0.0/24"),
netip.MustParsePrefix("172.0.0.0/10"),
},
}),
Created: created,
Tags: []string{},
@ -187,15 +180,22 @@ func TestTailNode(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
polMan, _ := policy.NewPolicyManagerForTest(tt.pol, []types.User{}, types.Nodes{tt.node})
primary := routes.New()
cfg := &types.Config{
BaseDomain: tt.baseDomain,
TailcfgDNSConfig: tt.dnsConfig,
RandomizeClientPort: false,
}
_ = primary.SetRoutes(tt.node.ID, tt.node.SubnetRoutes()...)
// This is a hack to avoid having a second node to test the primary route.
// This should be baked into the test case proper if it is extended in the future.
_ = primary.SetRoutes(2, netip.MustParsePrefix("192.168.0.0/24"))
got, err := tailNode(
tt.node,
0,
polMan,
primary,
cfg,
)
@ -249,6 +249,7 @@ func TestNodeExpiry(t *testing.T) {
node,
0,
&policy.PolicyManagerV1{},
nil,
&types.Config{},
)
if err != nil {