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

node attributes begin

This commit is contained in:
Niels Bouma 2024-12-28 11:07:38 +01:00
parent b81420bef1
commit cd0c88f332
7 changed files with 114 additions and 14 deletions

View File

@ -115,6 +115,7 @@ func generateUserProfiles(
func generateDNSConfig(
cfg *types.Config,
node *types.Node,
nodeAttrs []string,
) *tailcfg.DNSConfig {
if cfg.TailcfgDNSConfig == nil {
return nil
@ -122,7 +123,7 @@ func generateDNSConfig(
dnsConfig := cfg.TailcfgDNSConfig.Clone()
addNextDNSMetadata(dnsConfig.Resolvers, node)
addNextDNSMetadata(dnsConfig.Resolvers, node, nodeAttrs)
return dnsConfig
}
@ -134,9 +135,21 @@ func generateDNSConfig(
//
// This will produce a resolver like:
// `https://dns.nextdns.io/<nextdns-id>?device_name=node-name&device_model=linux&device_ip=100.64.0.1`
func addNextDNSMetadata(resolvers []*dnstype.Resolver, node *types.Node) {
func addNextDNSMetadata(resolvers []*dnstype.Resolver, node *types.Node, nodeAttrs []string) {
for _, resolver := range resolvers {
if strings.HasPrefix(resolver.Addr, nextDNSDoHPrefix) {
idx := slices.IndexFunc(nodeAttrs, func(item string) bool { return strings.HasPrefix(item, "nextdns:") && item != "nextdns:no-device-info" })
if idx != -1 {
nextDNSProfile := strings.Split(nodeAttrs[idx], ":")[1]
resolver.Addr = fmt.Sprintf("%s/%s", nextDNSDoHPrefix, nextDNSProfile)
}
if slices.Contains(nodeAttrs, "nextdns:no-device-info") {
continue
}
attrs := url.Values{
"device_name": []string{node.Hostname},
"device_model": []string{node.Hostinfo.OS},
@ -158,7 +171,13 @@ func (m *Mapper) fullMapResponse(
peers types.Nodes,
capVer tailcfg.CapabilityVersion,
) (*tailcfg.MapResponse, error) {
resp, err := m.baseWithConfigMapResponse(node, capVer)
nodeAttrs, err := m.polMan.NodeAttributes(node)
if err != nil {
return nil, err
}
resp, err := m.baseWithConfigMapResponse(node, capVer, nodeAttrs)
if err != nil {
return nil, err
}
@ -171,6 +190,7 @@ func (m *Mapper) fullMapResponse(
capVer,
peers,
m.cfg,
nodeAttrs,
)
if err != nil {
return nil, err
@ -206,7 +226,13 @@ func (m *Mapper) ReadOnlyMapResponse(
node *types.Node,
messages ...string,
) ([]byte, error) {
resp, err := m.baseWithConfigMapResponse(node, mapRequest.Version)
nodeAttrs, err := m.polMan.NodeAttributes(node)
if err != nil {
return nil, err
}
resp, err := m.baseWithConfigMapResponse(node, mapRequest.Version, nodeAttrs)
if err != nil {
return nil, err
}
@ -268,6 +294,11 @@ func (m *Mapper) PeerChangedResponse(
}
}
nodeAttrs, err := m.polMan.NodeAttributes(node)
if err != nil {
return nil, err
}
err = appendPeerChanges(
&resp,
false, // partial change
@ -276,6 +307,7 @@ func (m *Mapper) PeerChangedResponse(
mapRequest.Version,
changedNodes,
m.cfg,
nodeAttrs,
)
if err != nil {
return nil, err
@ -300,7 +332,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.cfg, nodeAttrs)
if err != nil {
return nil, err
}
@ -444,10 +476,11 @@ func (m *Mapper) baseMapResponse() tailcfg.MapResponse {
func (m *Mapper) baseWithConfigMapResponse(
node *types.Node,
capVer tailcfg.CapabilityVersion,
nodeAttrs []string,
) (*tailcfg.MapResponse, error) {
resp := m.baseMapResponse()
tailnode, err := tailNode(node, capVer, m.polMan, m.cfg)
tailnode, err := tailNode(node, capVer, m.polMan, m.cfg, nodeAttrs)
if err != nil {
return nil, err
}
@ -505,6 +538,7 @@ func appendPeerChanges(
capVer tailcfg.CapabilityVersion,
changed types.Nodes,
cfg *types.Config,
attrs []string,
) error {
filter := polMan.Filter()
@ -521,7 +555,7 @@ func appendPeerChanges(
profiles := generateUserProfiles(node, changed)
dnsConfig := generateDNSConfig(cfg, node)
dnsConfig := generateDNSConfig(cfg, node, attrs)
tailPeers, err := tailNodes(changed, capVer, polMan, cfg)
if err != nil {

View File

@ -120,6 +120,7 @@ func TestDNSConfigMapResponse(t *testing.T) {
TailcfgDNSConfig: &dnsConfigOrig,
},
nodeInShared1,
[]string{},
)
if diff := cmp.Diff(tt.want, got, cmpopts.EquateEmpty()); diff != "" {

View File

@ -20,11 +20,18 @@ func tailNodes(
tNodes := make([]*tailcfg.Node, len(nodes))
for index, node := range nodes {
nodeAttrs, err := polMan.NodeAttributes(node)
if err != nil {
return nil, err
}
node, err := tailNode(
node,
capVer,
polMan,
cfg,
nodeAttrs,
)
if err != nil {
return nil, err
@ -42,6 +49,7 @@ func tailNode(
capVer tailcfg.CapabilityVersion,
polMan policy.PolicyManager,
cfg *types.Config,
nodeAttrs []string,
) (*tailcfg.Node, error) {
addrs := node.Prefixes()
@ -124,6 +132,10 @@ func tailNode(
tNode.CapMap[tailcfg.NodeAttrRandomizeClientPort] = []tailcfg.RawMessage{}
}
for _, nodeAttr := range nodeAttrs {
tNode.CapMap[tailcfg.NodeCapability(nodeAttr)] = []tailcfg.RawMessage{}
}
if node.IsOnline == nil || !*node.IsOnline {
// LastSeen is only set when node is
// not connected to the control server.

View File

@ -195,6 +195,7 @@ func TestTailNode(t *testing.T) {
0,
polMan,
cfg,
[]string{},
)
if (err != nil) != tt.wantErr {
@ -248,6 +249,7 @@ func TestNodeExpiry(t *testing.T) {
0,
&policy.PolicyManagerV1{},
&types.Config{},
[]string{},
)
if err != nil {
t.Fatalf("nodeExpiry() error = %v", err)

View File

@ -442,6 +442,42 @@ func (pol *ACLPolicy) CompileSSHPolicy(
}, nil
}
func (pol *ACLPolicy) GetAttributesForNode(
node *types.Node,
users []types.User,
peers types.Nodes,
) ([]string, error) {
if pol == nil {
return nil, nil
}
var attributes []string
for _, nodeAttr := range pol.NodeAttributes {
var dest netipx.IPSetBuilder
for _, target := range nodeAttr.Targets {
expanded, err := pol.ExpandAlias(append(peers, node), users, target)
if err != nil {
return nil, err
}
dest.AddSet(expanded)
}
destSet, err := dest.IPSet()
if err != nil {
return nil, err
}
if !node.InIPSet(destSet) {
continue
}
attributes = append(attributes, nodeAttr.Attributes...)
}
return attributes, nil
}
// ipSetAll returns a function that iterates over all the IPs in the IPSet.
func ipSetAll(ipSet *netipx.IPSet) iter.Seq[netip.Addr] {
return func(yield func(netip.Addr) bool) {

View File

@ -10,13 +10,14 @@ import (
// ACLPolicy represents a Tailscale ACL Policy.
type ACLPolicy struct {
Groups Groups `json:"groups"`
Hosts Hosts `json:"hosts"`
TagOwners TagOwners `json:"tagOwners"`
ACLs []ACL `json:"acls"`
Tests []ACLTest `json:"tests"`
AutoApprovers AutoApprovers `json:"autoApprovers"`
SSHs []SSH `json:"ssh"`
Groups Groups `json:"groups"`
Hosts Hosts `json:"hosts"`
TagOwners TagOwners `json:"tagOwners"`
ACLs []ACL `json:"acls"`
Tests []ACLTest `json:"tests"`
AutoApprovers AutoApprovers `json:"autoApprovers"`
SSHs []SSH `json:"ssh"`
NodeAttributes []NodeAttributes `json:"nodeAttrs"`
}
// ACL is a basic rule for the ACL Policy.
@ -50,6 +51,12 @@ type AutoApprovers struct {
ExitNode []string `json:"exitNode"`
}
// NodeAttributes is for applying additional attributes to specific devices
type NodeAttributes struct {
Targets []string `json:"target"`
Attributes []string `json:"attr"`
}
// SSH controls who can ssh into which machines.
type SSH struct {
Action string `json:"action"`

View File

@ -17,6 +17,7 @@ import (
type PolicyManager interface {
Filter() []tailcfg.FilterRule
SSHPolicy(*types.Node) (*tailcfg.SSHPolicy, error)
NodeAttributes(node *types.Node) ([]string, error)
Tags(*types.Node) []string
ApproversForRoute(netip.Prefix) []string
ExpandAlias(string) (*netipx.IPSet, error)
@ -140,6 +141,13 @@ func (pm *PolicyManagerV1) SetPolicy(polB []byte) (bool, error) {
return pm.updateLocked()
}
func (pm *PolicyManagerV1) NodeAttributes(node *types.Node) ([]string, error) {
pm.mu.Lock()
defer pm.mu.Unlock()
return pm.pol.GetAttributesForNode(node, pm.users, pm.nodes)
}
// SetUsers updates the users in the policy manager and updates the filter rules.
func (pm *PolicyManagerV1) SetUsers(users []types.User) (bool, error) {
pm.mu.Lock()