remove DB dependency of tailNode conversion, add test
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
parent
bce8427423
commit
5bad48a24e
8 changed files with 462 additions and 221 deletions
|
@ -69,27 +69,11 @@ func NewMapper(
|
|||
}
|
||||
}
|
||||
|
||||
func (m Mapper) fullMapResponse(
|
||||
func (m *Mapper) tempWrap(
|
||||
mapRequest tailcfg.MapRequest,
|
||||
machine *types.Machine,
|
||||
pol *policy.ACLPolicy,
|
||||
) (*tailcfg.MapResponse, error) {
|
||||
log.Trace().
|
||||
Caller().
|
||||
Str("machine", mapRequest.Hostinfo.Hostname).
|
||||
Msg("Creating Map response")
|
||||
|
||||
// TODO(kradalby): Decouple this from DB?
|
||||
node, err := m.db.TailNode(*machine, pol, m.dnsCfg)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Cannot convert to node")
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
peers, err := m.db.ListPeers(machine)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
|
@ -100,7 +84,39 @@ func (m Mapper) fullMapResponse(
|
|||
return nil, err
|
||||
}
|
||||
|
||||
rules, sshPolicy, err := policy.GenerateFilterRules(pol, peers, m.stripEmailDomain)
|
||||
return fullMapResponse(
|
||||
mapRequest,
|
||||
pol,
|
||||
machine,
|
||||
peers,
|
||||
m.stripEmailDomain,
|
||||
m.baseDomain,
|
||||
m.dnsCfg,
|
||||
m.derpMap,
|
||||
m.logtail,
|
||||
m.randomClientPort,
|
||||
)
|
||||
}
|
||||
|
||||
func fullMapResponse(
|
||||
mapRequest tailcfg.MapRequest,
|
||||
pol *policy.ACLPolicy,
|
||||
machine *types.Machine,
|
||||
peers types.Machines,
|
||||
|
||||
stripEmailDomain bool,
|
||||
baseDomain string,
|
||||
dnsCfg *tailcfg.DNSConfig,
|
||||
derpMap *tailcfg.DERPMap,
|
||||
logtail bool,
|
||||
randomClientPort bool,
|
||||
) (*tailcfg.MapResponse, error) {
|
||||
tailnode, err := tailNode(*machine, pol, dnsCfg, baseDomain, stripEmailDomain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rules, sshPolicy, err := policy.GenerateFilterRules(pol, peers, stripEmailDomain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -109,38 +125,31 @@ func (m Mapper) fullMapResponse(
|
|||
peers = policy.FilterMachinesByACL(machine, peers, rules)
|
||||
}
|
||||
|
||||
profiles := generateUserProfiles(machine, peers, m.baseDomain)
|
||||
profiles := generateUserProfiles(machine, peers, baseDomain)
|
||||
|
||||
// TODO(kradalby): Decouple this from DB?
|
||||
nodePeers, err := m.db.TailNodes(peers, pol, m.dnsCfg)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Failed to convert peers to Tailscale nodes")
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO(kradalby): Shold this mutation happen before TailNode(s) is called?
|
||||
dnsConfig := generateDNSConfig(
|
||||
m.dnsCfg,
|
||||
m.baseDomain,
|
||||
dnsCfg,
|
||||
baseDomain,
|
||||
*machine,
|
||||
peers,
|
||||
)
|
||||
|
||||
tailPeers, err := tailNodes(peers, pol, dnsCfg, baseDomain, stripEmailDomain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
resp := tailcfg.MapResponse{
|
||||
KeepAlive: false,
|
||||
Node: node,
|
||||
Node: tailnode,
|
||||
|
||||
// TODO: Only send if updated
|
||||
DERPMap: m.derpMap,
|
||||
DERPMap: derpMap,
|
||||
|
||||
// TODO: Only send if updated
|
||||
Peers: nodePeers,
|
||||
Peers: tailPeers,
|
||||
|
||||
// TODO(kradalby): Implement:
|
||||
// https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L1351-L1374
|
||||
|
@ -154,7 +163,7 @@ func (m Mapper) fullMapResponse(
|
|||
DNSConfig: dnsConfig,
|
||||
|
||||
// TODO: Only send if updated
|
||||
Domain: m.baseDomain,
|
||||
Domain: baseDomain,
|
||||
|
||||
// Do not instruct clients to collect services, we do not
|
||||
// support or do anything with them
|
||||
|
@ -171,8 +180,8 @@ func (m Mapper) fullMapResponse(
|
|||
ControlTime: &now,
|
||||
|
||||
Debug: &tailcfg.Debug{
|
||||
DisableLogTail: !m.logtail,
|
||||
RandomizeClientPort: m.randomClientPort,
|
||||
DisableLogTail: !logtail,
|
||||
RandomizeClientPort: randomClientPort,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -283,7 +292,7 @@ func (m Mapper) CreateMapResponse(
|
|||
machine *types.Machine,
|
||||
pol *policy.ACLPolicy,
|
||||
) ([]byte, error) {
|
||||
mapResponse, err := m.fullMapResponse(mapRequest, machine, pol)
|
||||
mapResponse, err := m.tempWrap(mapRequest, machine, pol)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -124,7 +124,7 @@ func TestDNSConfigMapResponse(t *testing.T) {
|
|||
)
|
||||
|
||||
if diff := cmp.Diff(tt.want, got, cmpopts.EquateEmpty()); diff != "" {
|
||||
t.Errorf("expandAlias() = %v, want %v", got, tt.want)
|
||||
t.Errorf("expandAlias() unexpected result (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
151
hscontrol/mapper/tail.go
Normal file
151
hscontrol/mapper/tail.go
Normal file
|
@ -0,0 +1,151 @@
|
|||
package mapper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/juanfont/headscale/hscontrol/policy"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"github.com/samber/lo"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func tailNodes(
|
||||
machines types.Machines,
|
||||
pol *policy.ACLPolicy,
|
||||
dnsConfig *tailcfg.DNSConfig,
|
||||
baseDomain string,
|
||||
stripEmailDomain bool,
|
||||
) ([]*tailcfg.Node, error) {
|
||||
nodes := make([]*tailcfg.Node, len(machines))
|
||||
|
||||
for index, machine := range machines {
|
||||
node, err := tailNode(
|
||||
machine,
|
||||
pol,
|
||||
dnsConfig,
|
||||
baseDomain,
|
||||
stripEmailDomain,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nodes[index] = node
|
||||
}
|
||||
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
// tailNode converts a Machine into a Tailscale Node. includeRoutes is false for shared nodes
|
||||
// as per the expected behaviour in the official SaaS.
|
||||
func tailNode(
|
||||
machine types.Machine,
|
||||
pol *policy.ACLPolicy,
|
||||
dnsConfig *tailcfg.DNSConfig,
|
||||
baseDomain string,
|
||||
stripEmailDomain bool,
|
||||
) (*tailcfg.Node, error) {
|
||||
nodeKey, err := machine.NodePublicKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// MachineKey is only used in the legacy protocol
|
||||
machineKey, err := machine.MachinePublicKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
discoKey, err := machine.DiscoPublicKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addrs := machine.IPAddresses.Prefixes()
|
||||
|
||||
allowedIPs := append(
|
||||
[]netip.Prefix{},
|
||||
addrs...) // we append the node own IP, as it is required by the clients
|
||||
|
||||
primaryPrefixes := []netip.Prefix{}
|
||||
|
||||
for _, route := range machine.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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var derp string
|
||||
if machine.HostInfo.NetInfo != nil {
|
||||
derp = fmt.Sprintf("127.3.3.40:%d", machine.HostInfo.NetInfo.PreferredDERP)
|
||||
} else {
|
||||
derp = "127.3.3.40:0" // Zero means disconnected or unknown.
|
||||
}
|
||||
|
||||
var keyExpiry time.Time
|
||||
if machine.Expiry != nil {
|
||||
keyExpiry = *machine.Expiry
|
||||
} else {
|
||||
keyExpiry = time.Time{}
|
||||
}
|
||||
|
||||
hostname, err := machine.GetFQDN(dnsConfig, baseDomain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hostInfo := machine.GetHostInfo()
|
||||
|
||||
online := machine.IsOnline()
|
||||
|
||||
tags, _ := pol.GetTagsOfMachine(machine, stripEmailDomain)
|
||||
tags = lo.Uniq(append(tags, machine.ForcedTags...))
|
||||
|
||||
node := tailcfg.Node{
|
||||
ID: tailcfg.NodeID(machine.ID), // this is the actual ID
|
||||
StableID: tailcfg.StableNodeID(
|
||||
strconv.FormatUint(machine.ID, util.Base10),
|
||||
), // in headscale, unlike tailcontrol server, IDs are permanent
|
||||
Name: hostname,
|
||||
|
||||
User: tailcfg.UserID(machine.UserID),
|
||||
|
||||
Key: nodeKey,
|
||||
KeyExpiry: keyExpiry,
|
||||
|
||||
Machine: machineKey,
|
||||
DiscoKey: discoKey,
|
||||
Addresses: addrs,
|
||||
AllowedIPs: allowedIPs,
|
||||
Endpoints: machine.Endpoints,
|
||||
DERP: derp,
|
||||
Hostinfo: hostInfo.View(),
|
||||
Created: machine.CreatedAt,
|
||||
|
||||
Tags: tags,
|
||||
|
||||
PrimaryRoutes: primaryPrefixes,
|
||||
|
||||
LastSeen: machine.LastSeen,
|
||||
Online: &online,
|
||||
KeepAlive: true,
|
||||
MachineAuthorized: !machine.IsExpired(),
|
||||
|
||||
Capabilities: []string{
|
||||
tailcfg.CapabilityFileSharing,
|
||||
tailcfg.CapabilityAdmin,
|
||||
tailcfg.CapabilitySSH,
|
||||
},
|
||||
}
|
||||
|
||||
return &node, nil
|
||||
}
|
183
hscontrol/mapper/tail_test.go
Normal file
183
hscontrol/mapper/tail_test.go
Normal file
|
@ -0,0 +1,183 @@
|
|||
package mapper
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/juanfont/headscale/hscontrol/policy"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
func TestTailNode(t *testing.T) {
|
||||
mustNK := func(str string) key.NodePublic {
|
||||
var k key.NodePublic
|
||||
_ = k.UnmarshalText([]byte(str))
|
||||
|
||||
return k
|
||||
}
|
||||
|
||||
mustDK := func(str string) key.DiscoPublic {
|
||||
var k key.DiscoPublic
|
||||
_ = k.UnmarshalText([]byte(str))
|
||||
|
||||
return k
|
||||
}
|
||||
|
||||
mustMK := func(str string) key.MachinePublic {
|
||||
var k key.MachinePublic
|
||||
_ = k.UnmarshalText([]byte(str))
|
||||
|
||||
return k
|
||||
}
|
||||
|
||||
hiview := func(hoin tailcfg.Hostinfo) tailcfg.HostinfoView {
|
||||
return hoin.View()
|
||||
}
|
||||
|
||||
created := time.Date(2009, time.November, 10, 23, 0, 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)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
machine types.Machine
|
||||
pol *policy.ACLPolicy
|
||||
dnsConfig *tailcfg.DNSConfig
|
||||
baseDomain string
|
||||
stripEmailDomain bool
|
||||
want *tailcfg.Node
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty-machine",
|
||||
machine: types.Machine{},
|
||||
pol: &policy.ACLPolicy{},
|
||||
dnsConfig: &tailcfg.DNSConfig{},
|
||||
baseDomain: "",
|
||||
stripEmailDomain: false,
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "minimal-machine",
|
||||
machine: types.Machine{
|
||||
ID: 0,
|
||||
MachineKey: "mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
|
||||
NodeKey: "nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
|
||||
DiscoKey: "discokey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
|
||||
IPAddresses: []netip.Addr{
|
||||
netip.MustParseAddr("100.64.0.1"),
|
||||
},
|
||||
Hostname: "mini",
|
||||
GivenName: "mini",
|
||||
UserID: 0,
|
||||
User: types.User{
|
||||
Name: "mini",
|
||||
},
|
||||
ForcedTags: []string{},
|
||||
AuthKeyID: 0,
|
||||
AuthKey: &types.PreAuthKey{},
|
||||
LastSeen: &lastSeen,
|
||||
Expiry: &expire,
|
||||
HostInfo: types.HostInfo{},
|
||||
Endpoints: []string{},
|
||||
Routes: []types.Route{
|
||||
{
|
||||
Prefix: types.IPPrefix(netip.MustParsePrefix("0.0.0.0/0")),
|
||||
Advertised: true,
|
||||
Enabled: true,
|
||||
IsPrimary: false,
|
||||
},
|
||||
{
|
||||
Prefix: types.IPPrefix(netip.MustParsePrefix("192.168.0.0/24")),
|
||||
Advertised: true,
|
||||
Enabled: true,
|
||||
IsPrimary: true,
|
||||
},
|
||||
},
|
||||
CreatedAt: created,
|
||||
},
|
||||
pol: &policy.ACLPolicy{},
|
||||
dnsConfig: &tailcfg.DNSConfig{},
|
||||
baseDomain: "",
|
||||
stripEmailDomain: false,
|
||||
want: &tailcfg.Node{
|
||||
ID: 0,
|
||||
StableID: "0",
|
||||
Name: "mini",
|
||||
|
||||
User: 0,
|
||||
|
||||
Key: mustNK(
|
||||
"nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
|
||||
),
|
||||
KeyExpiry: expire,
|
||||
|
||||
Machine: mustMK(
|
||||
"mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
|
||||
),
|
||||
DiscoKey: mustDK(
|
||||
"discokey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
|
||||
),
|
||||
Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")},
|
||||
AllowedIPs: []netip.Prefix{
|
||||
netip.MustParsePrefix("100.64.0.1/32"),
|
||||
netip.MustParsePrefix("0.0.0.0/0"),
|
||||
netip.MustParsePrefix("192.168.0.0/24"),
|
||||
},
|
||||
Endpoints: []string{},
|
||||
DERP: "127.3.3.40:0",
|
||||
Hostinfo: hiview(tailcfg.Hostinfo{}),
|
||||
Created: created,
|
||||
|
||||
Tags: []string{},
|
||||
|
||||
PrimaryRoutes: []netip.Prefix{
|
||||
netip.MustParsePrefix("192.168.0.0/24"),
|
||||
},
|
||||
|
||||
LastSeen: &lastSeen,
|
||||
Online: new(bool),
|
||||
KeepAlive: true,
|
||||
MachineAuthorized: true,
|
||||
|
||||
Capabilities: []string{
|
||||
tailcfg.CapabilityFileSharing,
|
||||
tailcfg.CapabilityAdmin,
|
||||
tailcfg.CapabilitySSH,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
// TODO: Add tests to check other aspects of the node conversion:
|
||||
// - With tags and policy
|
||||
// - dnsconfig and basedomain
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tailNode(
|
||||
tt.machine,
|
||||
tt.pol,
|
||||
tt.dnsConfig,
|
||||
tt.baseDomain,
|
||||
tt.stripEmailDomain,
|
||||
)
|
||||
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("tailNode() error = %v, wantErr %v", err, tt.wantErr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(tt.want, got, cmpopts.EquateEmpty()); diff != "" {
|
||||
t.Errorf("tailNode() unexpected result (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue