diff --git a/hscontrol/db/machine.go b/hscontrol/db/machine.go index 74cf75aa..849ce0c5 100644 --- a/hscontrol/db/machine.go +++ b/hscontrol/db/machine.go @@ -5,25 +5,20 @@ import ( "fmt" "net/netip" "sort" - "strconv" "strings" "time" - "github.com/juanfont/headscale/hscontrol/policy" "github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/util" "github.com/patrickmn/go-cache" "github.com/rs/zerolog/log" - "github.com/samber/lo" "gorm.io/gorm" - "tailscale.com/tailcfg" "tailscale.com/types/key" ) const ( MachineGivenNameHashLength = 8 MachineGivenNameTrimSize = 2 - MaxHostnameLength = 255 ) var ( @@ -33,7 +28,6 @@ var ( "machine not found in registration cache", ) ErrCouldNotConvertMachineInterface = errors.New("failed to convert machine interface") - ErrHostnameTooLong = errors.New("hostname too long") ErrDifferentRegisteredUser = errors.New( "machine was previously registered with a different user", ) @@ -471,7 +465,7 @@ func (hsdb *HSDatabase) RegisterMachine(machine types.Machine, log.Trace(). Caller(). Str("machine", machine.Hostname). - Str("ip", strings.Join(ips.ToStringSlice(), ",")). + Str("ip", strings.Join(ips.StringSlice(), ",")). Msg("Machine registered with the database") 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 -} diff --git a/hscontrol/mapper/mapper.go b/hscontrol/mapper/mapper.go index 5dfa9499..68db7c5d 100644 --- a/hscontrol/mapper/mapper.go +++ b/hscontrol/mapper/mapper.go @@ -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 } diff --git a/hscontrol/mapper/mapper_test.go b/hscontrol/mapper/mapper_test.go index a5d65c9a..e593eaab 100644 --- a/hscontrol/mapper/mapper_test.go +++ b/hscontrol/mapper/mapper_test.go @@ -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) } }) } diff --git a/hscontrol/mapper/tail.go b/hscontrol/mapper/tail.go new file mode 100644 index 00000000..a6118648 --- /dev/null +++ b/hscontrol/mapper/tail.go @@ -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 +} diff --git a/hscontrol/mapper/tail_test.go b/hscontrol/mapper/tail_test.go new file mode 100644 index 00000000..a8a3f40b --- /dev/null +++ b/hscontrol/mapper/tail_test.go @@ -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) + } + }) + } +} diff --git a/hscontrol/policy/acls_test.go b/hscontrol/policy/acls_test.go index f6c5e107..e9b99ce9 100644 --- a/hscontrol/policy/acls_test.go +++ b/hscontrol/policy/acls_test.go @@ -1332,7 +1332,7 @@ func Test_expandAlias(t *testing.T) { return } 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 != "" { 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) } }) } diff --git a/hscontrol/protocol_common.go b/hscontrol/protocol_common.go index c0ba9240..85d18941 100644 --- a/hscontrol/protocol_common.go +++ b/hscontrol/protocol_common.go @@ -516,7 +516,7 @@ func (h *Headscale) handleAuthKeyCommon( Str("func", "handleAuthKeyCommon"). Bool("noise", isNoise). Str("machine", registerRequest.Hostinfo.Hostname). - Str("ips", strings.Join(machine.IPAddresses.ToStringSlice(), ", ")). + Str("ips", strings.Join(machine.IPAddresses.StringSlice(), ", ")). Msg("Successfully authenticated via AuthKey") } diff --git a/hscontrol/types/machine.go b/hscontrol/types/machine.go index 562d7d6f..15f71b7d 100644 --- a/hscontrol/types/machine.go +++ b/hscontrol/types/machine.go @@ -10,17 +10,23 @@ import ( v1 "github.com/juanfont/headscale/gen/go/headscale/v1" "github.com/juanfont/headscale/hscontrol/policy/matcher" + "github.com/juanfont/headscale/hscontrol/util" "go4.org/netipx" "google.golang.org/protobuf/types/known/timestamppb" "tailscale.com/tailcfg" + "tailscale.com/types/key" ) const ( // TODO(kradalby): Move out of here when we got circdeps under control. 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. type Machine struct { @@ -73,7 +79,7 @@ type ( type MachineAddresses []netip.Addr -func (ma MachineAddresses) ToStringSlice() []string { +func (ma MachineAddresses) StringSlice() []string { strSlice := make([]string, 0, len(ma)) for _, addr := range ma { strSlice = append(strSlice, addr.String()) @@ -125,7 +131,7 @@ func (ma *MachineAddresses) Scan(destination interface{}) error { // Value return json value, implement driver.Valuer interface. func (ma MachineAddresses) Value() (driver.Value, error) { - addresses := strings.Join(ma.ToStringSlice(), ",") + addresses := strings.Join(ma.StringSlice(), ",") return addresses, nil } @@ -201,7 +207,7 @@ func (machine *Machine) Proto() *v1.Machine { NodeKey: machine.NodeKey, DiscoKey: machine.DiscoKey, - IpAddresses: machine.IPAddresses.ToStringSlice(), + IpAddresses: machine.IPAddresses.StringSlice(), Name: machine.Hostname, GivenName: machine.GivenName, User: machine.User.Proto(), @@ -240,6 +246,70 @@ func (machine *Machine) GetHostInfo() tailcfg.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 { return machine.Hostname }