mirror of
https://github.com/juanfont/headscale.git
synced 2024-11-29 18:33:05 +00:00
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
|
@ -5,25 +5,20 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/juanfont/headscale/hscontrol/policy"
|
|
||||||
"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/patrickmn/go-cache"
|
"github.com/patrickmn/go-cache"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/samber/lo"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"tailscale.com/tailcfg"
|
|
||||||
"tailscale.com/types/key"
|
"tailscale.com/types/key"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MachineGivenNameHashLength = 8
|
MachineGivenNameHashLength = 8
|
||||||
MachineGivenNameTrimSize = 2
|
MachineGivenNameTrimSize = 2
|
||||||
MaxHostnameLength = 255
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -33,7 +28,6 @@ var (
|
||||||
"machine not found in registration cache",
|
"machine not found in registration cache",
|
||||||
)
|
)
|
||||||
ErrCouldNotConvertMachineInterface = errors.New("failed to convert machine interface")
|
ErrCouldNotConvertMachineInterface = errors.New("failed to convert machine interface")
|
||||||
ErrHostnameTooLong = errors.New("hostname too long")
|
|
||||||
ErrDifferentRegisteredUser = errors.New(
|
ErrDifferentRegisteredUser = errors.New(
|
||||||
"machine was previously registered with a different user",
|
"machine was previously registered with a different user",
|
||||||
)
|
)
|
||||||
|
@ -471,7 +465,7 @@ func (hsdb *HSDatabase) RegisterMachine(machine types.Machine,
|
||||||
log.Trace().
|
log.Trace().
|
||||||
Caller().
|
Caller().
|
||||||
Str("machine", machine.Hostname).
|
Str("machine", machine.Hostname).
|
||||||
Str("ip", strings.Join(ips.ToStringSlice(), ",")).
|
Str("ip", strings.Join(ips.StringSlice(), ",")).
|
||||||
Msg("Machine registered with the database")
|
Msg("Machine registered with the database")
|
||||||
|
|
||||||
return &machine, nil
|
return &machine, nil
|
||||||
|
@ -785,169 +779,3 @@ func (hsdb *HSDatabase) ExpireExpiredMachines(lastChange time.Time) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hsdb *HSDatabase) TailNodes(
|
|
||||||
machines types.Machines,
|
|
||||||
pol *policy.ACLPolicy,
|
|
||||||
dnsConfig *tailcfg.DNSConfig,
|
|
||||||
) ([]*tailcfg.Node, error) {
|
|
||||||
nodes := make([]*tailcfg.Node, len(machines))
|
|
||||||
|
|
||||||
for index, machine := range machines {
|
|
||||||
node, err := hsdb.TailNode(machine, pol, dnsConfig)
|
|
||||||
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 (hsdb *HSDatabase) TailNode(
|
|
||||||
machine types.Machine,
|
|
||||||
pol *policy.ACLPolicy,
|
|
||||||
dnsConfig *tailcfg.DNSConfig,
|
|
||||||
) (*tailcfg.Node, error) {
|
|
||||||
var nodeKey key.NodePublic
|
|
||||||
err := nodeKey.UnmarshalText([]byte(util.NodePublicKeyEnsurePrefix(machine.NodeKey)))
|
|
||||||
if err != nil {
|
|
||||||
log.Trace().
|
|
||||||
Caller().
|
|
||||||
Str("node_key", machine.NodeKey).
|
|
||||||
Msgf("Failed to parse node public key from hex")
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("failed to parse node public key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var machineKey key.MachinePublic
|
|
||||||
// MachineKey is only used in the legacy protocol
|
|
||||||
if machine.MachineKey != "" {
|
|
||||||
err = machineKey.UnmarshalText(
|
|
||||||
[]byte(util.MachinePublicKeyEnsurePrefix(machine.MachineKey)),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse machine public key: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var discoKey key.DiscoPublic
|
|
||||||
if machine.DiscoKey != "" {
|
|
||||||
err := discoKey.UnmarshalText(
|
|
||||||
[]byte(util.DiscoPublicKeyEnsurePrefix(machine.DiscoKey)),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse disco public key: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
discoKey = key.DiscoPublic{}
|
|
||||||
}
|
|
||||||
|
|
||||||
addrs := []netip.Prefix{}
|
|
||||||
for _, machineAddress := range machine.IPAddresses {
|
|
||||||
ip := netip.PrefixFrom(machineAddress, machineAddress.BitLen())
|
|
||||||
addrs = append(addrs, ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
allowedIPs := append(
|
|
||||||
[]netip.Prefix{},
|
|
||||||
addrs...) // we append the node own IP, as it is required by the clients
|
|
||||||
|
|
||||||
primaryRoutes, err := hsdb.GetMachinePrimaryRoutes(&machine)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
primaryPrefixes := primaryRoutes.Prefixes()
|
|
||||||
|
|
||||||
machineRoutes, err := hsdb.GetMachineRoutes(&machine)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, route := range machineRoutes {
|
|
||||||
if route.Enabled && (route.IsPrimary || 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{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var hostname string
|
|
||||||
if dnsConfig != nil && dnsConfig.Proxied { // MagicDNS
|
|
||||||
hostname = fmt.Sprintf(
|
|
||||||
"%s.%s.%s",
|
|
||||||
machine.GivenName,
|
|
||||||
machine.User.Name,
|
|
||||||
hsdb.baseDomain,
|
|
||||||
)
|
|
||||||
if len(hostname) > MaxHostnameLength {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
"hostname %q is too long it cannot except 255 ASCII chars: %w",
|
|
||||||
hostname,
|
|
||||||
ErrHostnameTooLong,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
hostname = machine.GivenName
|
|
||||||
}
|
|
||||||
|
|
||||||
hostInfo := machine.GetHostInfo()
|
|
||||||
|
|
||||||
online := machine.IsOnline()
|
|
||||||
|
|
||||||
tags, _ := pol.GetTagsOfMachine(machine, hsdb.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
|
|
||||||
}
|
|
||||||
|
|
|
@ -69,27 +69,11 @@ func NewMapper(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Mapper) fullMapResponse(
|
func (m *Mapper) tempWrap(
|
||||||
mapRequest tailcfg.MapRequest,
|
mapRequest tailcfg.MapRequest,
|
||||||
machine *types.Machine,
|
machine *types.Machine,
|
||||||
pol *policy.ACLPolicy,
|
pol *policy.ACLPolicy,
|
||||||
) (*tailcfg.MapResponse, error) {
|
) (*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)
|
peers, err := m.db.ListPeers(machine)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
|
@ -100,7 +84,39 @@ func (m Mapper) fullMapResponse(
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -109,38 +125,31 @@ func (m Mapper) fullMapResponse(
|
||||||
peers = policy.FilterMachinesByACL(machine, peers, rules)
|
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(
|
dnsConfig := generateDNSConfig(
|
||||||
m.dnsCfg,
|
dnsCfg,
|
||||||
m.baseDomain,
|
baseDomain,
|
||||||
*machine,
|
*machine,
|
||||||
peers,
|
peers,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
tailPeers, err := tailNodes(peers, pol, dnsCfg, baseDomain, stripEmailDomain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
resp := tailcfg.MapResponse{
|
resp := tailcfg.MapResponse{
|
||||||
KeepAlive: false,
|
KeepAlive: false,
|
||||||
Node: node,
|
Node: tailnode,
|
||||||
|
|
||||||
// TODO: Only send if updated
|
// TODO: Only send if updated
|
||||||
DERPMap: m.derpMap,
|
DERPMap: derpMap,
|
||||||
|
|
||||||
// TODO: Only send if updated
|
// TODO: Only send if updated
|
||||||
Peers: nodePeers,
|
Peers: tailPeers,
|
||||||
|
|
||||||
// TODO(kradalby): Implement:
|
// TODO(kradalby): Implement:
|
||||||
// https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L1351-L1374
|
// https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L1351-L1374
|
||||||
|
@ -154,7 +163,7 @@ func (m Mapper) fullMapResponse(
|
||||||
DNSConfig: dnsConfig,
|
DNSConfig: dnsConfig,
|
||||||
|
|
||||||
// TODO: Only send if updated
|
// TODO: Only send if updated
|
||||||
Domain: m.baseDomain,
|
Domain: baseDomain,
|
||||||
|
|
||||||
// Do not instruct clients to collect services, we do not
|
// Do not instruct clients to collect services, we do not
|
||||||
// support or do anything with them
|
// support or do anything with them
|
||||||
|
@ -171,8 +180,8 @@ func (m Mapper) fullMapResponse(
|
||||||
ControlTime: &now,
|
ControlTime: &now,
|
||||||
|
|
||||||
Debug: &tailcfg.Debug{
|
Debug: &tailcfg.Debug{
|
||||||
DisableLogTail: !m.logtail,
|
DisableLogTail: !logtail,
|
||||||
RandomizeClientPort: m.randomClientPort,
|
RandomizeClientPort: randomClientPort,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -283,7 +292,7 @@ func (m Mapper) CreateMapResponse(
|
||||||
machine *types.Machine,
|
machine *types.Machine,
|
||||||
pol *policy.ACLPolicy,
|
pol *policy.ACLPolicy,
|
||||||
) ([]byte, error) {
|
) ([]byte, error) {
|
||||||
mapResponse, err := m.fullMapResponse(mapRequest, machine, pol)
|
mapResponse, err := m.tempWrap(mapRequest, machine, pol)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,7 +124,7 @@ func TestDNSConfigMapResponse(t *testing.T) {
|
||||||
)
|
)
|
||||||
|
|
||||||
if diff := cmp.Diff(tt.want, got, cmpopts.EquateEmpty()); diff != "" {
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1332,7 +1332,7 @@ func Test_expandAlias(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if diff := cmp.Diff(test.want, got); diff != "" {
|
if diff := cmp.Diff(test.want, got); diff != "" {
|
||||||
t.Errorf("expandAlias() = %v, want %v", got, test.want)
|
t.Errorf("expandAlias() unexpected result (-want +got):\n%s", diff)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1711,7 +1711,7 @@ func TestACLPolicy_generateFilterRules(t *testing.T) {
|
||||||
|
|
||||||
if diff := cmp.Diff(tt.want, got); diff != "" {
|
if diff := cmp.Diff(tt.want, got); diff != "" {
|
||||||
log.Trace().Interface("got", got).Msg("result")
|
log.Trace().Interface("got", got).Msg("result")
|
||||||
t.Errorf("ACLgenerateFilterRules() = %v, want %v", got, tt.want)
|
t.Errorf("ACLgenerateFilterRules() unexpected result (-want +got):\n%s", diff)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -516,7 +516,7 @@ func (h *Headscale) handleAuthKeyCommon(
|
||||||
Str("func", "handleAuthKeyCommon").
|
Str("func", "handleAuthKeyCommon").
|
||||||
Bool("noise", isNoise).
|
Bool("noise", isNoise).
|
||||||
Str("machine", registerRequest.Hostinfo.Hostname).
|
Str("machine", registerRequest.Hostinfo.Hostname).
|
||||||
Str("ips", strings.Join(machine.IPAddresses.ToStringSlice(), ", ")).
|
Str("ips", strings.Join(machine.IPAddresses.StringSlice(), ", ")).
|
||||||
Msg("Successfully authenticated via AuthKey")
|
Msg("Successfully authenticated via AuthKey")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,17 +10,23 @@ import (
|
||||||
|
|
||||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||||
"github.com/juanfont/headscale/hscontrol/policy/matcher"
|
"github.com/juanfont/headscale/hscontrol/policy/matcher"
|
||||||
|
"github.com/juanfont/headscale/hscontrol/util"
|
||||||
"go4.org/netipx"
|
"go4.org/netipx"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
|
"tailscale.com/types/key"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// TODO(kradalby): Move out of here when we got circdeps under control.
|
// TODO(kradalby): Move out of here when we got circdeps under control.
|
||||||
keepAliveInterval = 60 * time.Second
|
keepAliveInterval = 60 * time.Second
|
||||||
|
MaxHostnameLength = 255
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrMachineAddressesInvalid = errors.New("failed to parse machine addresses")
|
var (
|
||||||
|
ErrMachineAddressesInvalid = errors.New("failed to parse machine addresses")
|
||||||
|
ErrHostnameTooLong = errors.New("hostname too long")
|
||||||
|
)
|
||||||
|
|
||||||
// Machine is a Headscale client.
|
// Machine is a Headscale client.
|
||||||
type Machine struct {
|
type Machine struct {
|
||||||
|
@ -73,7 +79,7 @@ type (
|
||||||
|
|
||||||
type MachineAddresses []netip.Addr
|
type MachineAddresses []netip.Addr
|
||||||
|
|
||||||
func (ma MachineAddresses) ToStringSlice() []string {
|
func (ma MachineAddresses) StringSlice() []string {
|
||||||
strSlice := make([]string, 0, len(ma))
|
strSlice := make([]string, 0, len(ma))
|
||||||
for _, addr := range ma {
|
for _, addr := range ma {
|
||||||
strSlice = append(strSlice, addr.String())
|
strSlice = append(strSlice, addr.String())
|
||||||
|
@ -125,7 +131,7 @@ func (ma *MachineAddresses) Scan(destination interface{}) error {
|
||||||
|
|
||||||
// Value return json value, implement driver.Valuer interface.
|
// Value return json value, implement driver.Valuer interface.
|
||||||
func (ma MachineAddresses) Value() (driver.Value, error) {
|
func (ma MachineAddresses) Value() (driver.Value, error) {
|
||||||
addresses := strings.Join(ma.ToStringSlice(), ",")
|
addresses := strings.Join(ma.StringSlice(), ",")
|
||||||
|
|
||||||
return addresses, nil
|
return addresses, nil
|
||||||
}
|
}
|
||||||
|
@ -201,7 +207,7 @@ func (machine *Machine) Proto() *v1.Machine {
|
||||||
|
|
||||||
NodeKey: machine.NodeKey,
|
NodeKey: machine.NodeKey,
|
||||||
DiscoKey: machine.DiscoKey,
|
DiscoKey: machine.DiscoKey,
|
||||||
IpAddresses: machine.IPAddresses.ToStringSlice(),
|
IpAddresses: machine.IPAddresses.StringSlice(),
|
||||||
Name: machine.Hostname,
|
Name: machine.Hostname,
|
||||||
GivenName: machine.GivenName,
|
GivenName: machine.GivenName,
|
||||||
User: machine.User.Proto(),
|
User: machine.User.Proto(),
|
||||||
|
@ -240,6 +246,70 @@ func (machine *Machine) GetHostInfo() tailcfg.Hostinfo {
|
||||||
return tailcfg.Hostinfo(machine.HostInfo)
|
return tailcfg.Hostinfo(machine.HostInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (machine *Machine) GetFQDN(dnsConfig *tailcfg.DNSConfig, baseDomain string) (string, error) {
|
||||||
|
var hostname string
|
||||||
|
if dnsConfig != nil && dnsConfig.Proxied { // MagicDNS
|
||||||
|
hostname = fmt.Sprintf(
|
||||||
|
"%s.%s.%s",
|
||||||
|
machine.GivenName,
|
||||||
|
machine.User.Name,
|
||||||
|
baseDomain,
|
||||||
|
)
|
||||||
|
if len(hostname) > MaxHostnameLength {
|
||||||
|
return "", fmt.Errorf(
|
||||||
|
"hostname %q is too long it cannot except 255 ASCII chars: %w",
|
||||||
|
hostname,
|
||||||
|
ErrHostnameTooLong,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hostname = machine.GivenName
|
||||||
|
}
|
||||||
|
|
||||||
|
return hostname, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (machine *Machine) MachinePublicKey() (key.MachinePublic, error) {
|
||||||
|
var machineKey key.MachinePublic
|
||||||
|
|
||||||
|
if machine.MachineKey != "" {
|
||||||
|
err := machineKey.UnmarshalText(
|
||||||
|
[]byte(util.MachinePublicKeyEnsurePrefix(machine.MachineKey)),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return key.MachinePublic{}, fmt.Errorf("failed to parse machine public key: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return machineKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (machine *Machine) DiscoPublicKey() (key.DiscoPublic, error) {
|
||||||
|
var discoKey key.DiscoPublic
|
||||||
|
if machine.DiscoKey != "" {
|
||||||
|
err := discoKey.UnmarshalText(
|
||||||
|
[]byte(util.DiscoPublicKeyEnsurePrefix(machine.DiscoKey)),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return key.DiscoPublic{}, fmt.Errorf("failed to parse disco public key: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
discoKey = key.DiscoPublic{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return discoKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (machine *Machine) NodePublicKey() (key.NodePublic, error) {
|
||||||
|
var nodeKey key.NodePublic
|
||||||
|
err := nodeKey.UnmarshalText([]byte(util.NodePublicKeyEnsurePrefix(machine.NodeKey)))
|
||||||
|
if err != nil {
|
||||||
|
return key.NodePublic{}, fmt.Errorf("failed to parse node public key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodeKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (machine Machine) String() string {
|
func (machine Machine) String() string {
|
||||||
return machine.Hostname
|
return machine.Hostname
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue