1
0
mirror of https://github.com/juanfont/headscale.git synced 2025-08-05 13:49:57 +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 // Fetch an initial DERP Map before we start serving
h.DERPMap = derp.GetDERPMap(h.cfg.DERP) 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 { if h.cfg.DERP.ServerEnabled {
// When embedded DERP is enabled we always need a STUN server // 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/db"
"github.com/juanfont/headscale/hscontrol/notifier" "github.com/juanfont/headscale/hscontrol/notifier"
"github.com/juanfont/headscale/hscontrol/policy" "github.com/juanfont/headscale/hscontrol/policy"
"github.com/juanfont/headscale/hscontrol/routes"
"github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util" "github.com/juanfont/headscale/hscontrol/util"
"github.com/klauspost/compress/zstd" "github.com/klauspost/compress/zstd"
@ -56,6 +57,7 @@ type Mapper struct {
derpMap *tailcfg.DERPMap derpMap *tailcfg.DERPMap
notif *notifier.Notifier notif *notifier.Notifier
polMan policy.PolicyManager polMan policy.PolicyManager
primary *routes.PrimaryRoutes
uid string uid string
created time.Time created time.Time
@ -73,6 +75,7 @@ func NewMapper(
derpMap *tailcfg.DERPMap, derpMap *tailcfg.DERPMap,
notif *notifier.Notifier, notif *notifier.Notifier,
polMan policy.PolicyManager, polMan policy.PolicyManager,
primary *routes.PrimaryRoutes,
) *Mapper { ) *Mapper {
uid, _ := util.GenerateRandomStringDNSSafe(mapperIDLength) uid, _ := util.GenerateRandomStringDNSSafe(mapperIDLength)
@ -82,6 +85,7 @@ func NewMapper(
derpMap: derpMap, derpMap: derpMap,
notif: notif, notif: notif,
polMan: polMan, polMan: polMan,
primary: primary,
uid: uid, uid: uid,
created: time.Now(), created: time.Now(),
@ -166,6 +170,7 @@ func (m *Mapper) fullMapResponse(
resp, resp,
true, // full change true, // full change
m.polMan, m.polMan,
m.primary,
node, node,
capVer, capVer,
peers, peers,
@ -271,6 +276,7 @@ func (m *Mapper) PeerChangedResponse(
&resp, &resp,
false, // partial change false, // partial change
m.polMan, m.polMan,
m.primary,
node, node,
mapRequest.Version, mapRequest.Version,
changedNodes, changedNodes,
@ -299,7 +305,7 @@ func (m *Mapper) PeerChangedResponse(
// Add the node itself, it might have changed, and particularly // Add the node itself, it might have changed, and particularly
// if there are no patches or changes, this is a self update. // 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 { if err != nil {
return nil, err return nil, err
} }
@ -446,7 +452,7 @@ func (m *Mapper) baseWithConfigMapResponse(
) (*tailcfg.MapResponse, error) { ) (*tailcfg.MapResponse, error) {
resp := m.baseMapResponse() 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 { if err != nil {
return nil, err return nil, err
} }
@ -500,6 +506,7 @@ func appendPeerChanges(
fullChange bool, fullChange bool,
polMan policy.PolicyManager, polMan policy.PolicyManager,
primary *routes.PrimaryRoutes,
node *types.Node, node *types.Node,
capVer tailcfg.CapabilityVersion, capVer tailcfg.CapabilityVersion,
changed types.Nodes, changed types.Nodes,
@ -522,7 +529,7 @@ func appendPeerChanges(
dnsConfig := generateDNSConfig(cfg, node) dnsConfig := generateDNSConfig(cfg, node)
tailPeers, err := tailNodes(changed, capVer, polMan, cfg) tailPeers, err := tailNodes(changed, capVer, polMan, primary, cfg)
if err != nil { if err != nil {
return err return err
} }

View File

@ -6,10 +6,10 @@ import (
"testing" "testing"
"time" "time"
"github.com/davecgh/go-spew/spew"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-cmp/cmp/cmpopts"
"github.com/juanfont/headscale/hscontrol/policy" "github.com/juanfont/headscale/hscontrol/policy"
"github.com/juanfont/headscale/hscontrol/routes"
"github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/types"
"gopkg.in/check.v1" "gopkg.in/check.v1"
"gorm.io/gorm" "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) 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) expire := time.Date(2500, time.November, 11, 23, 0, 0, 0, time.UTC)
user1 := types.User{Model: gorm.Model{ID: 0}, Name: "mini"} user1 := types.User{Model: gorm.Model{ID: 1}, Name: "user1"}
user2 := types.User{Model: gorm.Model{ID: 1}, Name: "peer2"} user2 := types.User{Model: gorm.Model{ID: 2}, Name: "user2"}
mini := &types.Node{ mini := &types.Node{
ID: 0, ID: 1,
MachineKey: mustMK( MachineKey: mustMK(
"mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507", "mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
), ),
@ -194,10 +194,10 @@ func Test_fullMapResponse(t *testing.T) {
} }
tailMini := &tailcfg.Node{ tailMini := &tailcfg.Node{
ID: 0, ID: 1,
StableID: "0", StableID: "1",
Name: "mini", Name: "mini",
User: 0, User: tailcfg.UserID(user1.ID),
Key: mustNK( Key: mustNK(
"nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe", "nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
), ),
@ -214,12 +214,17 @@ func Test_fullMapResponse(t *testing.T) {
tsaddr.AllIPv4(), tsaddr.AllIPv4(),
netip.MustParsePrefix("192.168.0.0/24"), netip.MustParsePrefix("192.168.0.0/24"),
}, },
HomeDERP: 0, HomeDERP: 0,
LegacyDERPString: "127.3.3.40:0", LegacyDERPString: "127.3.3.40:0",
Hostinfo: hiview(tailcfg.Hostinfo{}), 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, Created: created,
Tags: []string{}, Tags: []string{},
PrimaryRoutes: []netip.Prefix{netip.MustParsePrefix("192.168.0.0/24")},
LastSeen: &lastSeen, LastSeen: &lastSeen,
MachineAuthorized: true, MachineAuthorized: true,
@ -231,7 +236,7 @@ func Test_fullMapResponse(t *testing.T) {
} }
peer1 := &types.Node{ peer1 := &types.Node{
ID: 1, ID: 2,
MachineKey: mustMK( MachineKey: mustMK(
"mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507", "mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
), ),
@ -244,8 +249,8 @@ func Test_fullMapResponse(t *testing.T) {
IPv4: iap("100.64.0.2"), IPv4: iap("100.64.0.2"),
Hostname: "peer1", Hostname: "peer1",
GivenName: "peer1", GivenName: "peer1",
UserID: user1.ID, UserID: user2.ID,
User: user1, User: user2,
ForcedTags: []string{}, ForcedTags: []string{},
LastSeen: &lastSeen, LastSeen: &lastSeen,
Expiry: &expire, Expiry: &expire,
@ -254,9 +259,10 @@ func Test_fullMapResponse(t *testing.T) {
} }
tailPeer1 := &tailcfg.Node{ tailPeer1 := &tailcfg.Node{
ID: 1, ID: 2,
StableID: "1", StableID: "2",
Name: "peer1", Name: "peer1",
User: tailcfg.UserID(user2.ID),
Key: mustNK( Key: mustNK(
"nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe", "nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
), ),
@ -274,7 +280,6 @@ func Test_fullMapResponse(t *testing.T) {
Hostinfo: hiview(tailcfg.Hostinfo{}), Hostinfo: hiview(tailcfg.Hostinfo{}),
Created: created, Created: created,
Tags: []string{}, Tags: []string{},
PrimaryRoutes: []netip.Prefix{},
LastSeen: &lastSeen, LastSeen: &lastSeen,
MachineAuthorized: true, 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 { tests := []struct {
name string name string
pol *policy.ACLPolicy pol *policy.ACLPolicy
@ -349,7 +331,7 @@ func Test_fullMapResponse(t *testing.T) {
Domain: "", Domain: "",
CollectServices: "false", CollectServices: "false",
PacketFilter: []tailcfg.FilterRule{}, 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{}}, SSHPolicy: &tailcfg.SSHPolicy{Rules: []*tailcfg.SSHRule{}},
ControlTime: &time.Time{}, ControlTime: &time.Time{},
Debug: &tailcfg.Debug{ Debug: &tailcfg.Debug{
@ -383,9 +365,12 @@ func Test_fullMapResponse(t *testing.T) {
Domain: "", Domain: "",
CollectServices: "false", CollectServices: "false",
PacketFilter: []tailcfg.FilterRule{}, PacketFilter: []tailcfg.FilterRule{},
UserProfiles: []tailcfg.UserProfile{{LoginName: "mini", DisplayName: "mini"}}, UserProfiles: []tailcfg.UserProfile{
SSHPolicy: &tailcfg.SSHPolicy{Rules: []*tailcfg.SSHRule{}}, {ID: tailcfg.UserID(user1.ID), LoginName: "user1", DisplayName: "user1"},
ControlTime: &time.Time{}, {ID: tailcfg.UserID(user2.ID), LoginName: "user2", DisplayName: "user2"},
},
SSHPolicy: &tailcfg.SSHPolicy{Rules: []*tailcfg.SSHRule{}},
ControlTime: &time.Time{},
Debug: &tailcfg.Debug{ Debug: &tailcfg.Debug{
DisableLogTail: true, DisableLogTail: true,
}, },
@ -395,6 +380,9 @@ func Test_fullMapResponse(t *testing.T) {
{ {
name: "with-pol-map-response", name: "with-pol-map-response",
pol: &policy.ACLPolicy{ pol: &policy.ACLPolicy{
Hosts: policy.Hosts{
"mini": netip.MustParsePrefix("100.64.0.1/32"),
},
ACLs: []policy.ACL{ ACLs: []policy.ACL{
{ {
Action: "accept", Action: "accept",
@ -406,7 +394,6 @@ func Test_fullMapResponse(t *testing.T) {
node: mini, node: mini,
peers: types.Nodes{ peers: types.Nodes{
peer1, peer1,
peer2,
}, },
derpMap: &tailcfg.DERPMap{}, derpMap: &tailcfg.DERPMap{},
cfg: &types.Config{ cfg: &types.Config{
@ -434,7 +421,8 @@ func Test_fullMapResponse(t *testing.T) {
}, },
}, },
UserProfiles: []tailcfg.UserProfile{ 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{}}, SSHPolicy: &tailcfg.SSHPolicy{Rules: []*tailcfg.SSHRule{}},
ControlTime: &time.Time{}, ControlTime: &time.Time{},
@ -449,6 +437,12 @@ func Test_fullMapResponse(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
polMan, _ := policy.NewPolicyManagerForTest(tt.pol, []types.User{user1, user2}, append(tt.peers, tt.node)) 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( mappy := NewMapper(
nil, nil,
@ -456,6 +450,7 @@ func Test_fullMapResponse(t *testing.T) {
tt.derpMap, tt.derpMap,
nil, nil,
polMan, polMan,
primary,
) )
got, err := mappy.fullMapResponse( got, err := mappy.fullMapResponse(
@ -470,8 +465,6 @@ func Test_fullMapResponse(t *testing.T) {
return return
} }
spew.Dump(got)
if diff := cmp.Diff( if diff := cmp.Diff(
tt.want, tt.want,
got, got,

View File

@ -6,6 +6,7 @@ import (
"time" "time"
"github.com/juanfont/headscale/hscontrol/policy" "github.com/juanfont/headscale/hscontrol/policy"
"github.com/juanfont/headscale/hscontrol/routes"
"github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/types"
"github.com/samber/lo" "github.com/samber/lo"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
@ -15,6 +16,7 @@ func tailNodes(
nodes types.Nodes, nodes types.Nodes,
capVer tailcfg.CapabilityVersion, capVer tailcfg.CapabilityVersion,
polMan policy.PolicyManager, polMan policy.PolicyManager,
primary *routes.PrimaryRoutes,
cfg *types.Config, cfg *types.Config,
) ([]*tailcfg.Node, error) { ) ([]*tailcfg.Node, error) {
tNodes := make([]*tailcfg.Node, len(nodes)) tNodes := make([]*tailcfg.Node, len(nodes))
@ -24,6 +26,7 @@ func tailNodes(
node, node,
capVer, capVer,
polMan, polMan,
primary,
cfg, cfg,
) )
if err != nil { if err != nil {
@ -41,6 +44,7 @@ func tailNode(
node *types.Node, node *types.Node,
capVer tailcfg.CapabilityVersion, capVer tailcfg.CapabilityVersion,
polMan policy.PolicyManager, polMan policy.PolicyManager,
primary *routes.PrimaryRoutes,
cfg *types.Config, cfg *types.Config,
) (*tailcfg.Node, error) { ) (*tailcfg.Node, error) {
addrs := node.Prefixes() addrs := node.Prefixes()
@ -94,6 +98,7 @@ func tailNode(
Machine: node.MachineKey, Machine: node.MachineKey,
DiscoKey: node.DiscoKey, DiscoKey: node.DiscoKey,
Addresses: addrs, Addresses: addrs,
PrimaryRoutes: primary.PrimaryRoutes(node.ID),
AllowedIPs: allowedIPs, AllowedIPs: allowedIPs,
Endpoints: node.Endpoints, Endpoints: node.Endpoints,
HomeDERP: derp, HomeDERP: derp,

View File

@ -9,6 +9,7 @@ import (
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-cmp/cmp/cmpopts"
"github.com/juanfont/headscale/hscontrol/policy" "github.com/juanfont/headscale/hscontrol/policy"
"github.com/juanfont/headscale/hscontrol/routes"
"github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/types"
"tailscale.com/net/tsaddr" "tailscale.com/net/tsaddr"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
@ -72,7 +73,6 @@ func TestTailNode(t *testing.T) {
LegacyDERPString: "127.3.3.40:0", LegacyDERPString: "127.3.3.40:0",
Hostinfo: hiview(tailcfg.Hostinfo{}), Hostinfo: hiview(tailcfg.Hostinfo{}),
Tags: []string{}, Tags: []string{},
PrimaryRoutes: []netip.Prefix{},
MachineAuthorized: true, MachineAuthorized: true,
CapMap: tailcfg.NodeCapMap{ CapMap: tailcfg.NodeCapMap{
@ -146,8 +146,14 @@ func TestTailNode(t *testing.T) {
}, },
HomeDERP: 0, HomeDERP: 0,
LegacyDERPString: "127.3.3.40:0", LegacyDERPString: "127.3.3.40:0",
Hostinfo: hiview(tailcfg.Hostinfo{}), Hostinfo: hiview(tailcfg.Hostinfo{
Created: created, RoutableIPs: []netip.Prefix{
tsaddr.AllIPv4(),
netip.MustParsePrefix("192.168.0.0/24"),
netip.MustParsePrefix("172.0.0.0/10"),
},
}),
Created: created,
Tags: []string{}, Tags: []string{},
@ -174,15 +180,22 @@ func TestTailNode(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
polMan, _ := policy.NewPolicyManagerForTest(tt.pol, []types.User{}, types.Nodes{tt.node}) polMan, _ := policy.NewPolicyManagerForTest(tt.pol, []types.User{}, types.Nodes{tt.node})
primary := routes.New()
cfg := &types.Config{ cfg := &types.Config{
BaseDomain: tt.baseDomain, BaseDomain: tt.baseDomain,
TailcfgDNSConfig: tt.dnsConfig, TailcfgDNSConfig: tt.dnsConfig,
RandomizeClientPort: false, 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( got, err := tailNode(
tt.node, tt.node,
0, 0,
polMan, polMan,
primary,
cfg, cfg,
) )
@ -236,6 +249,7 @@ func TestNodeExpiry(t *testing.T) {
node, node,
0, 0,
&policy.PolicyManagerV1{}, &policy.PolicyManagerV1{},
nil,
&types.Config{}, &types.Config{},
) )
if err != nil { if err != nil {

View File

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

View File

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

View File

@ -74,3 +74,21 @@ func TailMapResponseToString(resp tailcfg.MapResponse) string {
TailNodesToString(resp.Peers), 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))
}