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( nodes types.Nodes, capVer tailcfg.CapabilityVersion, pol *policy.ACLPolicy, dnsConfig *tailcfg.DNSConfig, baseDomain string, randomClientPort bool, ) ([]*tailcfg.Node, error) { tNodes := make([]*tailcfg.Node, len(nodes)) for index, node := range nodes { node, err := tailNode( node, capVer, pol, dnsConfig, baseDomain, randomClientPort, ) if err != nil { return nil, err } tNodes[index] = node } return tNodes, nil } // tailNode converts a Node into a Tailscale Node. includeRoutes is false for shared nodes // as per the expected behaviour in the official SaaS. func tailNode( node *types.Node, capVer tailcfg.CapabilityVersion, pol *policy.ACLPolicy, dnsConfig *tailcfg.DNSConfig, baseDomain string, randomClientPort bool, ) (*tailcfg.Node, error) { addrs := node.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 node.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 node.Hostinfo != nil && node.Hostinfo.NetInfo != nil { derp = fmt.Sprintf("127.3.3.40:%d", node.Hostinfo.NetInfo.PreferredDERP) } else { derp = "127.3.3.40:0" // Zero means disconnected or unknown. } var keyExpiry time.Time if node.Expiry != nil { keyExpiry = *node.Expiry } else { keyExpiry = time.Time{} } hostname, err := node.GetFQDN(dnsConfig, baseDomain) if err != nil { return nil, fmt.Errorf("tailNode, failed to create FQDN: %s", err) } tags, _ := pol.TagsOfNode(node) tags = lo.Uniq(append(tags, node.ForcedTags...)) tNode := tailcfg.Node{ ID: tailcfg.NodeID(node.ID), // this is the actual ID StableID: tailcfg.StableNodeID( strconv.FormatUint(node.ID, util.Base10), ), // in headscale, unlike tailcontrol server, IDs are permanent Name: hostname, Cap: capVer, User: tailcfg.UserID(node.UserID), Key: node.NodeKey, KeyExpiry: keyExpiry, Machine: node.MachineKey, DiscoKey: node.DiscoKey, Addresses: addrs, AllowedIPs: allowedIPs, Endpoints: node.Endpoints, DERP: derp, Hostinfo: node.Hostinfo.View(), Created: node.CreatedAt, Online: node.IsOnline, Tags: tags, PrimaryRoutes: primaryPrefixes, MachineAuthorized: !node.IsExpired(), Expired: node.IsExpired(), } // - 74: 2023-09-18: Client understands NodeCapMap if capVer >= 74 { tNode.CapMap = tailcfg.NodeCapMap{ tailcfg.CapabilityFileSharing: []tailcfg.RawMessage{}, tailcfg.CapabilityAdmin: []tailcfg.RawMessage{}, tailcfg.CapabilitySSH: []tailcfg.RawMessage{}, } if randomClientPort { tNode.CapMap[tailcfg.NodeAttrRandomizeClientPort] = []tailcfg.RawMessage{} } } else { tNode.Capabilities = []tailcfg.NodeCapability{ tailcfg.CapabilityFileSharing, tailcfg.CapabilityAdmin, tailcfg.CapabilitySSH, } if randomClientPort { tNode.Capabilities = append(tNode.Capabilities, tailcfg.NodeAttrRandomizeClientPort) } } // - 72: 2023-08-23: TS-2023-006 UPnP issue fixed; UPnP can now be used again if capVer < 72 { tNode.Capabilities = append(tNode.Capabilities, tailcfg.NodeAttrDisableUPnP) } if node.IsOnline == nil || !*node.IsOnline { // LastSeen is only set when node is // not connected to the control server. tNode.LastSeen = node.LastSeen } return &tNode, nil }