Merge branch 'juanfont:main' into main

This commit is contained in:
Kedas 2024-07-13 13:11:13 -07:00 committed by GitHub
commit 1fc14790f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 288 additions and 118 deletions

View file

@ -40,6 +40,10 @@ after improving the test harness as part of adopting [#1460](https://github.com/
- Prefixes are now defined per v4 and v6 range. [#1756](https://github.com/juanfont/headscale/pull/1756) - Prefixes are now defined per v4 and v6 range. [#1756](https://github.com/juanfont/headscale/pull/1756)
- `ip_prefixes` option is now `prefixes.v4` and `prefixes.v6` - `ip_prefixes` option is now `prefixes.v4` and `prefixes.v6`
- `prefixes.allocation` can be set to assign IPs at `sequential` or `random`. [#1869](https://github.com/juanfont/headscale/pull/1869) - `prefixes.allocation` can be set to assign IPs at `sequential` or `random`. [#1869](https://github.com/juanfont/headscale/pull/1869)
- MagicDNS domains no longer contain usernames []()
- This is in preperation to fix Headscales implementation of tags which currently does not correctly remove the link between a tagged device and a user. As tagged devices will not have a user, this will require a change to the DNS generation, removing the username, see [#1369](https://github.com/juanfont/headscale/issues/1369) for more information.
- `use_username_in_magic_dns` can be used to turn this behaviour on again, but note that this option _will be removed_ when tags are fixed.
- This option brings Headscales behaviour in line with Tailscale.
### Changes ### Changes
@ -58,6 +62,8 @@ after improving the test harness as part of adopting [#1460](https://github.com/
- Log available update as warning [#1877](https://github.com/juanfont/headscale/pull/1877) - Log available update as warning [#1877](https://github.com/juanfont/headscale/pull/1877)
- Add `autogroup:internet` to Policy [#1917](https://github.com/juanfont/headscale/pull/1917) - Add `autogroup:internet` to Policy [#1917](https://github.com/juanfont/headscale/pull/1917)
- Restore foreign keys and add constraints [#1562](https://github.com/juanfont/headscale/pull/1562) - Restore foreign keys and add constraints [#1562](https://github.com/juanfont/headscale/pull/1562)
- Make registration page easier to use on mobile devices
- Make write-ahead-log default on and configurable for SQLite [#1985](https://github.com/juanfont/headscale/pull/1985)
## 0.22.3 (2023-05-12) ## 0.22.3 (2023-05-12)

View file

@ -145,6 +145,10 @@ database:
sqlite: sqlite:
path: /var/lib/headscale/db.sqlite path: /var/lib/headscale/db.sqlite
# Enable WAL mode for SQLite. This is recommended for production environments.
# https://www.sqlite.org/wal.html
write_ahead_log: true
# # Postgres config # # Postgres config
# postgres: # postgres:
# # If using a Unix socket to connect to Postgres, set the socket path in the 'host' field and leave 'port' blank. # # If using a Unix socket to connect to Postgres, set the socket path in the 'host' field and leave 'port' blank.
@ -268,6 +272,15 @@ dns_config:
# Only works if there is at least a nameserver defined. # Only works if there is at least a nameserver defined.
magic_dns: true magic_dns: true
# DEPRECATED
# Use the username as part of the DNS name for nodes, with this option enabled:
# node1.username.example.com
# while when this is disabled:
# node1.example.com
# This is a legacy option as Headscale has have this wrongly implemented
# while in upstream Tailscale, the username is not included.
use_username_in_magic_dns: false
# Defines the base domain to create the hostnames for MagicDNS. # Defines the base domain to create the hostnames for MagicDNS.
# `base_domain` must be a FQDNs, without the trailing dot. # `base_domain` must be a FQDNs, without the trailing dot.
# The FQDN of the hosts will be # The FQDN of the hosts will be

View file

@ -12,8 +12,8 @@ Ensure that the installed version is at least 1.30.0, as that is the first relea
## Configuring the headscale URL ## Configuring the headscale URL
After opening the app, the kebab menu icon (three dots) on the top bar on the right must be repeatedly opened and closed until the _Change server_ option appears in the menu. This is where you can enter your headscale URL. After opening the app:
A screen recording of this process can be seen in the `tailscale-android` PR which implemented this functionality: <https://github.com/tailscale/tailscale-android/pull/55> - Open setting and go into account settings
- In the kebab menu icon (three dots) on the top bar on the right select “Use an alternate server”
After saving and restarting the app, selecting the regular _Sign in_ option (non-SSO) should open up the headscale authentication page. - Enter your server URL and follow the instructions

View file

@ -8,7 +8,7 @@ hide:
`headscale` is an open source, self-hosted implementation of the Tailscale control server. `headscale` is an open source, self-hosted implementation of the Tailscale control server.
This page contains the documentation for the latest version of headscale. Please also check our [FAQ](/faq/). This page contains the documentation for the latest version of headscale. Please also check our [FAQ](faq.md).
Join our [Discord](https://discord.gg/c84AZQhmpx) server for a chat and community support. Join our [Discord](https://discord.gg/c84AZQhmpx) server for a chat and community support.

View file

@ -15,6 +15,10 @@ The reverse proxy MUST be configured to support WebSockets, as it is needed for
WebSockets support is required when using the headscale embedded DERP server. In this case, you will also need to expose the UDP port used for STUN (by default, udp/3478). Please check our [config-example.yaml](https://github.com/juanfont/headscale/blob/main/config-example.yaml). WebSockets support is required when using the headscale embedded DERP server. In this case, you will also need to expose the UDP port used for STUN (by default, udp/3478). Please check our [config-example.yaml](https://github.com/juanfont/headscale/blob/main/config-example.yaml).
### Cloudflare
Running headscale behind a cloudflare proxy or cloudflare tunnel is not supported and will not work as Cloudflare does not support WebSocket POSTs as required by the Tailscale protocol. See [this issue](https://github.com/juanfont/headscale/issues/1468)
### TLS ### TLS
Headscale can be configured not to use TLS, leaving it to the reverse proxy to handle. Add the following configuration values to your headscale config file. Headscale can be configured not to use TLS, leaving it to the reverse proxy to handle. Add the following configuration values to your headscale config file.

View file

@ -57,7 +57,7 @@ describing how to make `headscale` run properly in a server environment.
touch /etc/headscale/config.yaml touch /etc/headscale/config.yaml
``` ```
**(Strongly Recommended)** Download a copy of the [example configuration][config-example.yaml](https://github.com/juanfont/headscale/blob/main/config-example.yaml) from the headscale repository. **(Strongly Recommended)** Download a copy of the [example configuration](https://github.com/juanfont/headscale/blob/main/config-example.yaml) from the headscale repository.
1. Start the headscale server: 1. Start the headscale server:

View file

@ -93,7 +93,7 @@ describing how to make `headscale` run properly in a server environment.
touch /etc/headscale/config.yaml touch /etc/headscale/config.yaml
``` ```
**(Strongly Recommended)** Download a copy of the [example configuration][config-example.yaml](https://github.com/juanfont/headscale/blob/main/config-example.yaml) from the headscale repository. **(Strongly Recommended)** Download a copy of the [example configuration](https://github.com/juanfont/headscale/blob/main/config-example.yaml) from the headscale repository.
1. Start the headscale server: 1. Start the headscale server:

View file

@ -20,11 +20,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1716062047, "lastModified": 1720181791,
"narHash": "sha256-OhysviwHQz4p2HZL4g7XGMLoUbWMjkMr/ogaR3VUYNA=", "narHash": "sha256-i4vJL12/AdyuQuviMMd1Hk2tsGt02hDNhA0Zj1m16N8=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "02923630b89aa1ab36ef8e422501a6f4fd4b2016", "rev": "4284c2b73c8bce4b46a6adf23e16d9e2ec8da4bb",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -319,14 +319,8 @@ func NewHeadscaleDatabase(
// no longer used. // no longer used.
ID: "202402151347", ID: "202402151347",
Migrate: func(tx *gorm.DB) error { Migrate: func(tx *gorm.DB) error {
err := tx.Migrator().DropColumn(&types.Node{}, "last_successful_update") _ = tx.Migrator().DropColumn(&types.Node{}, "last_successful_update")
if err != nil && strings.Contains(err.Error(), `of relation "nodes" does not exist`) {
return nil return nil
} else {
return err
}
return err
}, },
Rollback: func(tx *gorm.DB) error { Rollback: func(tx *gorm.DB) error {
return nil return nil
@ -440,13 +434,29 @@ func openDB(cfg types.DatabaseConfig) (*gorm.DB, error) {
Msg("Opening database") Msg("Opening database")
db, err := gorm.Open( db, err := gorm.Open(
sqlite.Open(cfg.Sqlite.Path+"?_synchronous=1&_journal_mode=WAL"), sqlite.Open(cfg.Sqlite.Path),
&gorm.Config{ &gorm.Config{
Logger: dbLogger, Logger: dbLogger,
}, },
) )
db.Exec("PRAGMA foreign_keys=ON") if err := db.Exec(`
PRAGMA foreign_keys=ON;
PRAGMA busy_timeout=10000;
PRAGMA auto_vacuum=INCREMENTAL;
PRAGMA synchronous=NORMAL;
`).Error; err != nil {
return nil, fmt.Errorf("enabling foreign keys: %w", err)
}
if cfg.Sqlite.WriteAheadLog {
if err := db.Exec(`
PRAGMA journal_mode=WAL;
PRAGMA wal_autocheckpoint=0;
`).Error; err != nil {
return nil, fmt.Errorf("setting WAL mode: %w", err)
}
}
// The pure Go SQLite library does not handle locking in // The pure Go SQLite library does not handle locking in
// the same way as the C based one and we cant use the gorm // the same way as the C based one and we cant use the gorm

View file

@ -215,7 +215,7 @@ func SetTags(
return nil return nil
} }
newTags := types.StringList{} var newTags types.StringList
for _, tag := range tags { for _, tag := range tags {
if !util.StringOrPrefixListContains(newTags, tag) { if !util.StringOrPrefixListContains(newTags, tag) {
newTags = append(newTags, tag) newTags = append(newTags, tag)
@ -452,7 +452,7 @@ func GetAdvertisedRoutes(tx *gorm.DB, node *types.Node) ([]netip.Prefix, error)
return nil, fmt.Errorf("getting advertised routes for node(%d): %w", node.ID, err) return nil, fmt.Errorf("getting advertised routes for node(%d): %w", node.ID, err)
} }
prefixes := []netip.Prefix{} var prefixes []netip.Prefix
for _, route := range routes { for _, route := range routes {
prefixes = append(prefixes, netip.Prefix(route.Prefix)) prefixes = append(prefixes, netip.Prefix(route.Prefix))
} }
@ -478,7 +478,7 @@ func GetEnabledRoutes(tx *gorm.DB, node *types.Node) ([]netip.Prefix, error) {
return nil, fmt.Errorf("getting enabled routes for node(%d): %w", node.ID, err) return nil, fmt.Errorf("getting enabled routes for node(%d): %w", node.ID, err)
} }
prefixes := []netip.Prefix{} var prefixes []netip.Prefix
for _, route := range routes { for _, route := range routes {
prefixes = append(prefixes, netip.Prefix(route.Prefix)) prefixes = append(prefixes, netip.Prefix(route.Prefix))
} }

View file

@ -222,7 +222,7 @@ func DeleteRoute(
return nil, err return nil, err
} }
routesToDelete := types.Routes{} var routesToDelete types.Routes
for _, r := range routes { for _, r := range routes {
if r.IsExitRoute() { if r.IsExitRoute() {
routesToDelete = append(routesToDelete, r) routesToDelete = append(routesToDelete, r)
@ -623,7 +623,7 @@ func EnableAutoApprovedRoutes(
log.Trace().Interface("routes", routes).Msg("routes for autoapproving") log.Trace().Interface("routes", routes).Msg("routes for autoapproving")
approvedRoutes := types.Routes{} var approvedRoutes types.Routes
for _, advertisedRoute := range routes { for _, advertisedRoute := range routes {
if advertisedRoute.Enabled { if advertisedRoute.Enabled {

View file

@ -81,7 +81,7 @@ func mergeDERPMaps(derpMaps []*tailcfg.DERPMap) *tailcfg.DERPMap {
} }
func GetDERPMap(cfg types.DERPConfig) *tailcfg.DERPMap { func GetDERPMap(cfg types.DERPConfig) *tailcfg.DERPMap {
derpMaps := make([]*tailcfg.DERPMap, 0) var derpMaps []*tailcfg.DERPMap
for _, path := range cfg.Paths { for _, path := range cfg.Paths {
log.Debug(). log.Debug().

View file

@ -143,6 +143,18 @@ var registerWebAPITemplate = template.Must(
<html> <html>
<head> <head>
<title>Registration - Headscale</title> <title>Registration - Headscale</title>
<meta name=viewport content="width=device-width, initial-scale=1">
<style>
body {
font-family: sans;
}
code {
display: block;
padding: 20px;
border: 1px solid #bbb;
background-color: #eee;
}
</style>
</head> </head>
<body> <body>
<h1>headscale</h1> <h1>headscale</h1>
@ -150,7 +162,7 @@ var registerWebAPITemplate = template.Must(
<p> <p>
Run the command below in the headscale server to add this machine to your network: Run the command below in the headscale server to add this machine to your network:
</p> </p>
<pre><code>headscale nodes register --user USERNAME --key {{.Key}}</code></pre> <code>headscale nodes register --user USERNAME --key {{.Key}}</code>
</body> </body>
</html> </html>
`)) `))

View file

@ -102,7 +102,7 @@ func generateUserProfiles(
userMap[peer.User.Name] = peer.User // not worth checking if already is there userMap[peer.User.Name] = peer.User // not worth checking if already is there
} }
profiles := []tailcfg.UserProfile{} var profiles []tailcfg.UserProfile
for _, user := range userMap { for _, user := range userMap {
displayName := user.Name displayName := user.Name
@ -122,15 +122,20 @@ func generateUserProfiles(
} }
func generateDNSConfig( func generateDNSConfig(
base *tailcfg.DNSConfig, cfg *types.Config,
baseDomain string, baseDomain string,
node *types.Node, node *types.Node,
peers types.Nodes, peers types.Nodes,
) *tailcfg.DNSConfig { ) *tailcfg.DNSConfig {
dnsConfig := base.Clone() if cfg.DNSConfig == nil {
return nil
}
dnsConfig := cfg.DNSConfig.Clone()
// if MagicDNS is enabled // if MagicDNS is enabled
if base != nil && base.Proxied { if dnsConfig.Proxied {
if cfg.DNSUserNameInMagicDNS {
// Only inject the Search Domain of the current user // Only inject the Search Domain of the current user
// shared nodes should use their full FQDN // shared nodes should use their full FQDN
dnsConfig.Domains = append( dnsConfig.Domains = append(
@ -151,8 +156,7 @@ func generateDNSConfig(
dnsRoute := fmt.Sprintf("%v.%v", user.Name, baseDomain) dnsRoute := fmt.Sprintf("%v.%v", user.Name, baseDomain)
dnsConfig.Routes[dnsRoute] = nil dnsConfig.Routes[dnsRoute] = nil
} }
} else { }
dnsConfig = base
} }
addNextDNSMetadata(dnsConfig.Resolvers, node) addNextDNSMetadata(dnsConfig.Resolvers, node)
@ -568,7 +572,7 @@ func appendPeerChanges(
profiles := generateUserProfiles(node, changed, cfg.BaseDomain) profiles := generateUserProfiles(node, changed, cfg.BaseDomain)
dnsConfig := generateDNSConfig( dnsConfig := generateDNSConfig(
cfg.DNSConfig, cfg,
cfg.BaseDomain, cfg.BaseDomain,
node, node,
peers, peers,

View file

@ -127,7 +127,10 @@ func TestDNSConfigMapResponse(t *testing.T) {
} }
got := generateDNSConfig( got := generateDNSConfig(
&dnsConfigOrig, &types.Config{
DNSConfig: &dnsConfigOrig,
DNSUserNameInMagicDNS: true,
},
baseDomain, baseDomain,
nodeInShared1, nodeInShared1,
peersOfNodeInShared1, peersOfNodeInShared1,

View file

@ -77,7 +77,7 @@ func tailNode(
keyExpiry = time.Time{} keyExpiry = time.Time{}
} }
hostname, err := node.GetFQDN(cfg.DNSConfig, cfg.BaseDomain) hostname, err := node.GetFQDN(cfg, cfg.BaseDomain)
if err != nil { if err != nil {
return nil, fmt.Errorf("tailNode, failed to create FQDN: %s", err) return nil, fmt.Errorf("tailNode, failed to create FQDN: %s", err)
} }

View file

@ -3,6 +3,7 @@ package notifier
import ( import (
"context" "context"
"net/netip" "net/netip"
"sort"
"testing" "testing"
"time" "time"
@ -221,6 +222,11 @@ func TestBatcher(t *testing.T) {
// We will call flush manually for the tests, // We will call flush manually for the tests,
// so do not run the worker. // so do not run the worker.
BatchChangeDelay: time.Hour, BatchChangeDelay: time.Hour,
// Since we do not load the config, we wont get the
// default, so set it manually so we dont time out
// and have flakes.
NotifierSendTimeout: time.Second,
}, },
}) })
@ -241,6 +247,16 @@ func TestBatcher(t *testing.T) {
got = append(got, out) got = append(got, out)
} }
// Make the inner order stable for comparison.
for _, u := range got {
sort.Slice(u.ChangeNodes, func(i, j int) bool {
return u.ChangeNodes[i] < u.ChangeNodes[j]
})
sort.Slice(u.ChangePatches, func(i, j int) bool {
return u.ChangePatches[i].NodeID < u.ChangePatches[j].NodeID
})
}
if diff := cmp.Diff(tt.want, got, util.Comparers...); diff != "" { if diff := cmp.Diff(tt.want, got, util.Comparers...); diff != "" {
t.Errorf("batcher() unexpected result (-want +got):\n%s", diff) t.Errorf("batcher() unexpected result (-want +got):\n%s", diff)
} }

View file

@ -180,14 +180,14 @@ func (pol *ACLPolicy) CompileFilterRules(
return tailcfg.FilterAllowAll, nil return tailcfg.FilterAllowAll, nil
} }
rules := []tailcfg.FilterRule{} var rules []tailcfg.FilterRule
for index, acl := range pol.ACLs { for index, acl := range pol.ACLs {
if acl.Action != "accept" { if acl.Action != "accept" {
return nil, ErrInvalidAction return nil, ErrInvalidAction
} }
srcIPs := []string{} var srcIPs []string
for srcIndex, src := range acl.Sources { for srcIndex, src := range acl.Sources {
srcs, err := pol.expandSource(src, nodes) srcs, err := pol.expandSource(src, nodes)
if err != nil { if err != nil {
@ -221,7 +221,7 @@ func (pol *ACLPolicy) CompileFilterRules(
return nil, err return nil, err
} }
dests := []tailcfg.NetPortRange{} var dests []tailcfg.NetPortRange
for _, dest := range expanded.Prefixes() { for _, dest := range expanded.Prefixes() {
for _, port := range *ports { for _, port := range *ports {
pr := tailcfg.NetPortRange{ pr := tailcfg.NetPortRange{
@ -251,8 +251,7 @@ func ReduceFilterRules(node *types.Node, rules []tailcfg.FilterRule) []tailcfg.F
for _, rule := range rules { for _, rule := range rules {
// record if the rule is actually relevant for the given node. // record if the rule is actually relevant for the given node.
dests := []tailcfg.NetPortRange{} var dests []tailcfg.NetPortRange
DEST_LOOP: DEST_LOOP:
for _, dest := range rule.DstPorts { for _, dest := range rule.DstPorts {
expanded, err := util.ParseIPSet(dest.IP, nil) expanded, err := util.ParseIPSet(dest.IP, nil)
@ -301,7 +300,7 @@ func (pol *ACLPolicy) CompileSSHPolicy(
return nil, nil return nil, nil
} }
rules := []*tailcfg.SSHRule{} var rules []*tailcfg.SSHRule
acceptAction := tailcfg.SSHAction{ acceptAction := tailcfg.SSHAction{
Message: "", Message: "",
@ -533,8 +532,7 @@ func (pol *ACLPolicy) expandSource(
return []string{}, err return []string{}, err
} }
prefixes := []string{} var prefixes []string
for _, prefix := range ipSet.Prefixes() { for _, prefix := range ipSet.Prefixes() {
prefixes = append(prefixes, prefix.String()) prefixes = append(prefixes, prefix.String())
} }
@ -615,8 +613,8 @@ func excludeCorrectlyTaggedNodes(
nodes types.Nodes, nodes types.Nodes,
user string, user string,
) types.Nodes { ) types.Nodes {
out := types.Nodes{} var out types.Nodes
tags := []string{} var tags []string
for tag := range aclPolicy.TagOwners { for tag := range aclPolicy.TagOwners {
owners, _ := expandOwnersFromTag(aclPolicy, user) owners, _ := expandOwnersFromTag(aclPolicy, user)
ns := append(owners, user) ns := append(owners, user)
@ -661,7 +659,7 @@ func expandPorts(portsStr string, isWild bool) (*[]tailcfg.PortRange, error) {
return nil, ErrWildcardIsNeeded return nil, ErrWildcardIsNeeded
} }
ports := []tailcfg.PortRange{} var ports []tailcfg.PortRange
for _, portStr := range strings.Split(portsStr, ",") { for _, portStr := range strings.Split(portsStr, ",") {
log.Trace().Msgf("parsing portstring: %s", portStr) log.Trace().Msgf("parsing portstring: %s", portStr)
rang := strings.Split(portStr, "-") rang := strings.Split(portStr, "-")
@ -737,7 +735,7 @@ func expandOwnersFromTag(
func (pol *ACLPolicy) expandUsersFromGroup( func (pol *ACLPolicy) expandUsersFromGroup(
group string, group string,
) ([]string, error) { ) ([]string, error) {
users := []string{} var users []string
log.Trace().Caller().Interface("pol", pol).Msg("test") log.Trace().Caller().Interface("pol", pol).Msg("test")
aclGroups, ok := pol.Groups[group] aclGroups, ok := pol.Groups[group]
if !ok { if !ok {
@ -772,7 +770,7 @@ func (pol *ACLPolicy) expandIPsFromGroup(
group string, group string,
nodes types.Nodes, nodes types.Nodes,
) (*netipx.IPSet, error) { ) (*netipx.IPSet, error) {
build := netipx.IPSetBuilder{} var build netipx.IPSetBuilder
users, err := pol.expandUsersFromGroup(group) users, err := pol.expandUsersFromGroup(group)
if err != nil { if err != nil {
@ -792,7 +790,7 @@ func (pol *ACLPolicy) expandIPsFromTag(
alias string, alias string,
nodes types.Nodes, nodes types.Nodes,
) (*netipx.IPSet, error) { ) (*netipx.IPSet, error) {
build := netipx.IPSetBuilder{} var build netipx.IPSetBuilder
// check for forced tags // check for forced tags
for _, node := range nodes { for _, node := range nodes {
@ -841,7 +839,7 @@ func (pol *ACLPolicy) expandIPsFromUser(
user string, user string,
nodes types.Nodes, nodes types.Nodes,
) (*netipx.IPSet, error) { ) (*netipx.IPSet, error) {
build := netipx.IPSetBuilder{} var build netipx.IPSetBuilder
filteredNodes := filterNodesByUser(nodes, user) filteredNodes := filterNodesByUser(nodes, user)
filteredNodes = excludeCorrectlyTaggedNodes(pol, filteredNodes, user) filteredNodes = excludeCorrectlyTaggedNodes(pol, filteredNodes, user)
@ -866,7 +864,7 @@ func (pol *ACLPolicy) expandIPsFromSingleIP(
matches := nodes.FilterByIP(ip) matches := nodes.FilterByIP(ip)
build := netipx.IPSetBuilder{} var build netipx.IPSetBuilder
build.Add(ip) build.Add(ip)
for _, node := range matches { for _, node := range matches {
@ -881,7 +879,7 @@ func (pol *ACLPolicy) expandIPsFromIPPrefix(
nodes types.Nodes, nodes types.Nodes,
) (*netipx.IPSet, error) { ) (*netipx.IPSet, error) {
log.Trace().Str("prefix", prefix.String()).Msg("expandAlias got prefix") log.Trace().Str("prefix", prefix.String()).Msg("expandAlias got prefix")
build := netipx.IPSetBuilder{} var build netipx.IPSetBuilder
build.AddPrefix(prefix) build.AddPrefix(prefix)
// This is suboptimal and quite expensive, but if we only add the prefix, we will miss all the relevant IPv6 // This is suboptimal and quite expensive, but if we only add the prefix, we will miss all the relevant IPv6
@ -931,8 +929,8 @@ func isAutoGroup(str string) bool {
func (pol *ACLPolicy) TagsOfNode( func (pol *ACLPolicy) TagsOfNode(
node *types.Node, node *types.Node,
) ([]string, []string) { ) ([]string, []string) {
validTags := make([]string, 0) var validTags []string
invalidTags := make([]string, 0) var invalidTags []string
// TODO(kradalby): Why is this sometimes nil? coming from tailNode? // TODO(kradalby): Why is this sometimes nil? coming from tailNode?
if node == nil { if node == nil {
@ -973,7 +971,7 @@ func (pol *ACLPolicy) TagsOfNode(
} }
func filterNodesByUser(nodes types.Nodes, user string) types.Nodes { func filterNodesByUser(nodes types.Nodes, user string) types.Nodes {
out := types.Nodes{} var out types.Nodes
for _, node := range nodes { for _, node := range nodes {
if node.User.Name == user { if node.User.Name == user {
out = append(out, node) out = append(out, node)
@ -989,7 +987,7 @@ func FilterNodesByACL(
nodes types.Nodes, nodes types.Nodes,
filter []tailcfg.FilterRule, filter []tailcfg.FilterRule,
) types.Nodes { ) types.Nodes {
result := types.Nodes{} var result types.Nodes
for index, peer := range nodes { for index, peer := range nodes {
if peer.ID == node.ID { if peer.ID == node.ID {

View file

@ -943,7 +943,7 @@ func Test_listNodesInUser(t *testing.T) {
}, },
user: "mickael", user: "mickael",
}, },
want: types.Nodes{}, want: nil,
}, },
} }
for _, test := range tests { for _, test := range tests {
@ -1645,7 +1645,7 @@ func TestACLPolicy_generateFilterRules(t *testing.T) {
name: "no-policy", name: "no-policy",
field: field{}, field: field{},
args: args{}, args: args{},
want: []tailcfg.FilterRule{}, want: nil,
wantErr: false, wantErr: false,
}, },
{ {
@ -2896,7 +2896,7 @@ func Test_getFilteredByACLPeers(t *testing.T) {
User: types.User{Name: "marc"}, User: types.User{Name: "marc"},
}, },
}, },
want: types.Nodes{}, want: nil,
}, },
{ {
// Investigating 699 // Investigating 699
@ -3426,7 +3426,7 @@ func TestSSHRules(t *testing.T) {
}, },
}, },
}, },
want: &tailcfg.SSHPolicy{Rules: []*tailcfg.SSHRule{}}, want: &tailcfg.SSHPolicy{Rules: nil},
}, },
} }

View file

@ -64,6 +64,7 @@ type Config struct {
ACMEEmail string ACMEEmail string
DNSConfig *tailcfg.DNSConfig DNSConfig *tailcfg.DNSConfig
DNSUserNameInMagicDNS bool
UnixSocket string UnixSocket string
UnixSocketPermission fs.FileMode UnixSocketPermission fs.FileMode
@ -82,6 +83,7 @@ type Config struct {
type SqliteConfig struct { type SqliteConfig struct {
Path string Path string
WriteAheadLog bool
} }
type PostgresConfig struct { type PostgresConfig struct {
@ -205,6 +207,7 @@ func LoadConfig(path string, isFile bool) error {
viper.SetDefault("dns_config", nil) viper.SetDefault("dns_config", nil)
viper.SetDefault("dns_config.override_local_dns", true) viper.SetDefault("dns_config.override_local_dns", true)
viper.SetDefault("dns_config.use_username_in_magic_dns", false)
viper.SetDefault("derp.server.enabled", false) viper.SetDefault("derp.server.enabled", false)
viper.SetDefault("derp.server.stun.enabled", true) viper.SetDefault("derp.server.stun.enabled", true)
@ -224,6 +227,8 @@ func LoadConfig(path string, isFile bool) error {
viper.SetDefault("database.postgres.max_idle_conns", 10) viper.SetDefault("database.postgres.max_idle_conns", 10)
viper.SetDefault("database.postgres.conn_max_idle_time_secs", 3600) viper.SetDefault("database.postgres.conn_max_idle_time_secs", 3600)
viper.SetDefault("database.sqlite.write_ahead_log", true)
viper.SetDefault("oidc.scope", []string{oidc.ScopeOpenID, "profile", "email"}) viper.SetDefault("oidc.scope", []string{oidc.ScopeOpenID, "profile", "email"})
viper.SetDefault("oidc.strip_email_domain", true) viper.SetDefault("oidc.strip_email_domain", true)
viper.SetDefault("oidc.only_start_if_oidc_is_available", true) viper.SetDefault("oidc.only_start_if_oidc_is_available", true)
@ -463,6 +468,7 @@ func GetDatabaseConfig() DatabaseConfig {
Path: util.AbsolutePathFromConfigPath( Path: util.AbsolutePathFromConfigPath(
viper.GetString("database.sqlite.path"), viper.GetString("database.sqlite.path"),
), ),
WriteAheadLog: viper.GetBool("database.sqlite.write_ahead_log"),
}, },
Postgres: PostgresConfig{ Postgres: PostgresConfig{
Host: viper.GetString("database.postgres.host"), Host: viper.GetString("database.postgres.host"),
@ -556,16 +562,6 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) {
dnsConfig.Domains = domains dnsConfig.Domains = domains
} }
if viper.IsSet("dns_config.domains") {
domains := viper.GetStringSlice("dns_config.domains")
if len(dnsConfig.Resolvers) > 0 {
dnsConfig.Domains = domains
} else if domains != nil {
log.Warn().
Msg("Warning: dns_config.domains is set, but no nameservers are configured. Ignoring domains.")
}
}
if viper.IsSet("dns_config.extra_records") { if viper.IsSet("dns_config.extra_records") {
var extraRecords []tailcfg.DNSRecord var extraRecords []tailcfg.DNSRecord
@ -591,8 +587,18 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) {
baseDomain = "headscale.net" // does not really matter when MagicDNS is not enabled baseDomain = "headscale.net" // does not really matter when MagicDNS is not enabled
} }
log.Trace().Interface("dns_config", dnsConfig).Msg("DNS configuration loaded") if !viper.GetBool("dns_config.use_username_in_magic_dns") {
dnsConfig.Domains = []string{baseDomain}
} else {
log.Warn().Msg("DNS: Usernames in DNS has been deprecated, this option will be remove in future versions")
log.Warn().Msg("DNS: see 0.23.0 changelog for more information.")
}
if domains := viper.GetStringSlice("dns_config.domains"); len(domains) > 0 {
dnsConfig.Domains = append(dnsConfig.Domains, domains...)
}
log.Trace().Interface("dns_config", dnsConfig).Msg("DNS configuration loaded")
return dnsConfig, baseDomain return dnsConfig, baseDomain
} }
@ -736,6 +742,7 @@ func GetHeadscaleConfig() (*Config, error) {
TLS: GetTLSConfig(), TLS: GetTLSConfig(),
DNSConfig: dnsConfig, DNSConfig: dnsConfig,
DNSUserNameInMagicDNS: viper.GetBool("dns_config.use_username_in_magic_dns"),
ACMEEmail: viper.GetString("tls.acme_email"), ACMEEmail: viper.GetString("tls.acme_email"),
ACMEURL: viper.GetString("tls.acme_url"), ACMEURL: viper.GetString("tls.acme_url"),

View file

@ -394,13 +394,20 @@ func (node *Node) Proto() *v1.Node {
return nodeProto return nodeProto
} }
func (node *Node) GetFQDN(dnsConfig *tailcfg.DNSConfig, baseDomain string) (string, error) { func (node *Node) GetFQDN(cfg *Config, baseDomain string) (string, error) {
var hostname string var hostname string
if dnsConfig != nil && dnsConfig.Proxied { // MagicDNS if cfg.DNSConfig != nil && cfg.DNSConfig.Proxied { // MagicDNS
if node.GivenName == "" { if node.GivenName == "" {
return "", fmt.Errorf("failed to create valid FQDN: %w", ErrNodeHasNoGivenName) return "", fmt.Errorf("failed to create valid FQDN: %w", ErrNodeHasNoGivenName)
} }
hostname = fmt.Sprintf(
"%s.%s",
node.GivenName,
baseDomain,
)
if cfg.DNSUserNameInMagicDNS {
if node.User.Name == "" { if node.User.Name == "" {
return "", fmt.Errorf("failed to create valid FQDN: %w", ErrNodeUserHasNoName) return "", fmt.Errorf("failed to create valid FQDN: %w", ErrNodeUserHasNoName)
} }
@ -411,6 +418,8 @@ func (node *Node) GetFQDN(dnsConfig *tailcfg.DNSConfig, baseDomain string) (stri
node.User.Name, node.User.Name,
baseDomain, baseDomain,
) )
}
if len(hostname) > MaxHostnameLength { if len(hostname) > MaxHostnameLength {
return "", fmt.Errorf( return "", fmt.Errorf(
"failed to create valid FQDN (%s): %w", "failed to create valid FQDN (%s): %w",

View file

@ -126,11 +126,87 @@ func TestNodeFQDN(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
node Node node Node
dns tailcfg.DNSConfig cfg Config
domain string domain string
want string want string
wantErr string wantErr string
}{ }{
{
name: "all-set-with-username",
node: Node{
GivenName: "test",
User: User{
Name: "user",
},
},
cfg: Config{
DNSConfig: &tailcfg.DNSConfig{
Proxied: true,
},
DNSUserNameInMagicDNS: true,
},
domain: "example.com",
want: "test.user.example.com",
},
{
name: "no-given-name-with-username",
node: Node{
User: User{
Name: "user",
},
},
cfg: Config{
DNSConfig: &tailcfg.DNSConfig{
Proxied: true,
},
DNSUserNameInMagicDNS: true,
},
domain: "example.com",
wantErr: "failed to create valid FQDN: node has no given name",
},
{
name: "no-user-name-with-username",
node: Node{
GivenName: "test",
User: User{},
},
cfg: Config{
DNSConfig: &tailcfg.DNSConfig{
Proxied: true,
},
DNSUserNameInMagicDNS: true,
},
domain: "example.com",
wantErr: "failed to create valid FQDN: node user has no name",
},
{
name: "no-magic-dns-with-username",
node: Node{
GivenName: "test",
User: User{
Name: "user",
},
},
cfg: Config{
DNSConfig: &tailcfg.DNSConfig{
Proxied: false,
},
DNSUserNameInMagicDNS: true,
},
domain: "example.com",
want: "test",
},
{
name: "no-dnsconfig-with-username",
node: Node{
GivenName: "test",
User: User{
Name: "user",
},
},
domain: "example.com",
want: "test",
},
{ {
name: "all-set", name: "all-set",
node: Node{ node: Node{
@ -139,11 +215,14 @@ func TestNodeFQDN(t *testing.T) {
Name: "user", Name: "user",
}, },
}, },
dns: tailcfg.DNSConfig{ cfg: Config{
DNSConfig: &tailcfg.DNSConfig{
Proxied: true, Proxied: true,
}, },
DNSUserNameInMagicDNS: false,
},
domain: "example.com", domain: "example.com",
want: "test.user.example.com", want: "test.example.com",
}, },
{ {
name: "no-given-name", name: "no-given-name",
@ -152,9 +231,12 @@ func TestNodeFQDN(t *testing.T) {
Name: "user", Name: "user",
}, },
}, },
dns: tailcfg.DNSConfig{ cfg: Config{
DNSConfig: &tailcfg.DNSConfig{
Proxied: true, Proxied: true,
}, },
DNSUserNameInMagicDNS: false,
},
domain: "example.com", domain: "example.com",
wantErr: "failed to create valid FQDN: node has no given name", wantErr: "failed to create valid FQDN: node has no given name",
}, },
@ -164,11 +246,14 @@ func TestNodeFQDN(t *testing.T) {
GivenName: "test", GivenName: "test",
User: User{}, User: User{},
}, },
dns: tailcfg.DNSConfig{ cfg: Config{
DNSConfig: &tailcfg.DNSConfig{
Proxied: true, Proxied: true,
}, },
DNSUserNameInMagicDNS: false,
},
domain: "example.com", domain: "example.com",
wantErr: "failed to create valid FQDN: node user has no name", want: "test.example.com",
}, },
{ {
name: "no-magic-dns", name: "no-magic-dns",
@ -178,9 +263,12 @@ func TestNodeFQDN(t *testing.T) {
Name: "user", Name: "user",
}, },
}, },
dns: tailcfg.DNSConfig{ cfg: Config{
DNSConfig: &tailcfg.DNSConfig{
Proxied: false, Proxied: false,
}, },
DNSUserNameInMagicDNS: false,
},
domain: "example.com", domain: "example.com",
want: "test", want: "test",
}, },
@ -199,7 +287,7 @@ func TestNodeFQDN(t *testing.T) {
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
got, err := tc.node.GetFQDN(&tc.dns, tc.domain) got, err := tc.node.GetFQDN(&tc.cfg, tc.domain)
if (err != nil) && (err.Error() != tc.wantErr) { if (err != nil) && (err.Error() != tc.wantErr) {
t.Errorf("GetFQDN() error = %s, wantErr %s", err, tc.wantErr) t.Errorf("GetFQDN() error = %s, wantErr %s", err, tc.wantErr)