1
0
mirror of https://github.com/juanfont/headscale.git synced 2025-08-01 13:46:49 +02:00

wire through primare router

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
Kristoffer Dalby 2025-02-14 15:56:39 +01:00
parent d3bce2d7ff
commit a48843bfd0
No known key found for this signature in database
8 changed files with 100 additions and 66 deletions

View File

@ -572,7 +572,7 @@ func (h *Headscale) Serve() error {
// Fetch an initial DERP Map before we start serving
h.DERPMap = derp.GetDERPMap(h.cfg.DERP)
h.mapper = mapper.NewMapper(h.db, h.cfg, h.DERPMap, h.nodeNotifier, h.polMan)
h.mapper = mapper.NewMapper(h.db, h.cfg, h.DERPMap, h.nodeNotifier, h.polMan, h.primaryRoutes)
if h.cfg.DERP.ServerEnabled {
// When embedded DERP is enabled we always need a STUN server

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(),
@ -166,6 +170,7 @@ func (m *Mapper) fullMapResponse(
resp,
true, // full change
m.polMan,
m.primary,
node,
capVer,
peers,
@ -271,6 +276,7 @@ func (m *Mapper) PeerChangedResponse(
&resp,
false, // partial change
m.polMan,
m.primary,
node,
mapRequest.Version,
changedNodes,
@ -299,7 +305,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 +452,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 +506,7 @@ func appendPeerChanges(
fullChange bool,
polMan policy.PolicyManager,
primary *routes.PrimaryRoutes,
node *types.Node,
capVer tailcfg.CapabilityVersion,
changed types.Nodes,
@ -522,7 +529,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,10 +6,10 @@ 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"
@ -159,11 +159,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",
),
@ -194,10 +194,10 @@ func Test_fullMapResponse(t *testing.T) {
}
tailMini := &tailcfg.Node{
ID: 0,
StableID: "0",
ID: 1,
StableID: "1",
Name: "mini",
User: 0,
User: tailcfg.UserID(user1.ID),
Key: mustNK(
"nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
),
@ -214,12 +214,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,
@ -231,7 +236,7 @@ func Test_fullMapResponse(t *testing.T) {
}
peer1 := &types.Node{
ID: 1,
ID: 2,
MachineKey: mustMK(
"mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
),
@ -244,8 +249,8 @@ 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,
@ -254,9 +259,10 @@ func Test_fullMapResponse(t *testing.T) {
}
tailPeer1 := &tailcfg.Node{
ID: 1,
StableID: "1",
ID: 2,
StableID: "2",
Name: "peer1",
User: tailcfg.UserID(user2.ID),
Key: mustNK(
"nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
),
@ -274,7 +280,6 @@ func Test_fullMapResponse(t *testing.T) {
Hostinfo: hiview(tailcfg.Hostinfo{}),
Created: created,
Tags: []string{},
PrimaryRoutes: []netip.Prefix{},
LastSeen: &lastSeen,
MachineAuthorized: true,
@ -285,29 +290,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{},
CreatedAt: created,
}
tests := []struct {
name string
pol *policy.ACLPolicy
@ -349,7 +331,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{
@ -383,9 +365,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,
},
@ -395,6 +380,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",
@ -406,7 +394,6 @@ func Test_fullMapResponse(t *testing.T) {
node: mini,
peers: types.Nodes{
peer1,
peer2,
},
derpMap: &tailcfg.DERPMap{},
cfg: &types.Config{
@ -434,7 +421,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{},
@ -449,6 +437,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.RegisterRoutes(tt.node.ID, tt.node.SubnetRoutes()...)
for _, peer := range tt.peers {
primary.RegisterRoutes(peer.ID, peer.SubnetRoutes()...)
}
mappy := NewMapper(
nil,
@ -456,6 +450,7 @@ func Test_fullMapResponse(t *testing.T) {
tt.derpMap,
nil,
polMan,
primary,
)
got, err := mappy.fullMapResponse(
@ -470,8 +465,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()
@ -94,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,

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{
@ -146,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{},
@ -174,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.RegisterRoutes(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.RegisterRoutes(2, netip.MustParsePrefix("192.168.0.0/24"))
got, err := tailNode(
tt.node,
0,
polMan,
primary,
cfg,
)
@ -236,6 +249,7 @@ func TestNodeExpiry(t *testing.T) {
node,
0,
&policy.PolicyManagerV1{},
nil,
&types.Config{},
)
if err != nil {

View File

@ -208,6 +208,7 @@ func (pol *ACLPolicy) CompileFilterRules(
users,
alias,
)
if err != nil {
return nil, err
}
@ -243,6 +244,7 @@ func (pol *ACLPolicy) CompileFilterRules(
// ReduceFilterRules takes a node and a set of rules and removes all rules and destinations
// that are not relevant to that particular node.
func ReduceFilterRules(node *types.Node, rules []tailcfg.FilterRule) []tailcfg.FilterRule {
// TODO(kradalby): Make this nil and not alloc unless needed
ret := []tailcfg.FilterRule{}
for _, rule := range rules {
@ -264,13 +266,11 @@ func ReduceFilterRules(node *types.Node, rules []tailcfg.FilterRule) []tailcfg.F
// If the node exposes routes, ensure they are note removed
// when the filters are reduced.
if node.Hostinfo != nil {
if len(node.Hostinfo.RoutableIPs) > 0 {
for _, routableIP := range node.Hostinfo.RoutableIPs {
if expanded.OverlapsPrefix(routableIP) {
dests = append(dests, dest)
continue DEST_LOOP
}
if len(node.SubnetRoutes()) > 0 {
for _, routableIP := range node.SubnetRoutes() {
if expanded.OverlapsPrefix(routableIP) {
dests = append(dests, dest)
continue DEST_LOOP
}
}
}

View File

@ -6,7 +6,6 @@ import (
"net/netip"
"slices"
"strconv"
"strings"
"time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
@ -108,9 +107,7 @@ type Node struct {
IsOnline *bool `gorm:"-"`
}
type (
Nodes []*Node
)
type Nodes []*Node
// GivenNameHasBeenChanged returns whether the `givenName` can be automatically changed based on the `Hostname` of the node.
func (node *Node) GivenNameHasBeenChanged() bool {

View File

@ -74,3 +74,21 @@ func TailMapResponseToString(resp tailcfg.MapResponse) string {
TailNodesToString(resp.Peers),
)
}
func TailcfgFilterRulesToString(rules []tailcfg.FilterRule) string {
var sb strings.Builder
for index, rule := range rules {
sb.WriteString(fmt.Sprintf(`
{
SrcIPs: %v
DstIPs: %v
}
`, rule.SrcIPs, rule.DstPorts))
if index < len(rules)-1 {
sb.WriteString(", ")
}
}
return fmt.Sprintf("[ %s ](%d)", sb.String(), len(rules))
}