fix search domains and remove username from magicdns (#1987)

This commit is contained in:
Kristoffer Dalby 2024-06-26 13:44:40 +02:00 committed by GitHub
parent 4a34cfc4a6
commit 14a3f94f0c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 183 additions and 63 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

View file

@ -265,6 +265,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

@ -122,37 +122,41 @@ 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 {
// Only inject the Search Domain of the current user if cfg.DNSUserNameInMagicDNS {
// shared nodes should use their full FQDN // Only inject the Search Domain of the current user
dnsConfig.Domains = append( // shared nodes should use their full FQDN
dnsConfig.Domains, dnsConfig.Domains = append(
fmt.Sprintf( dnsConfig.Domains,
"%s.%s", fmt.Sprintf(
node.User.Name, "%s.%s",
baseDomain, node.User.Name,
), baseDomain,
) ),
)
userSet := mapset.NewSet[types.User]() userSet := mapset.NewSet[types.User]()
userSet.Add(node.User) userSet.Add(node.User)
for _, p := range peers { for _, p := range peers {
userSet.Add(p.User) userSet.Add(p.User)
}
for _, user := range userSet.ToSlice() {
dnsRoute := fmt.Sprintf("%v.%v", user.Name, baseDomain)
dnsConfig.Routes[dnsRoute] = nil
}
} }
for _, user := range userSet.ToSlice() {
dnsRoute := fmt.Sprintf("%v.%v", user.Name, baseDomain)
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,
@ -187,9 +190,9 @@ func Test_fullMapResponse(t *testing.T) {
UserID: 0, UserID: 0,
User: types.User{Name: "mini"}, User: types.User{Name: "mini"},
ForcedTags: []string{}, ForcedTags: []string{},
AuthKey: &types.PreAuthKey{}, AuthKey: &types.PreAuthKey{},
LastSeen: &lastSeen, LastSeen: &lastSeen,
Expiry: &expire, Expiry: &expire,
Hostinfo: &tailcfg.Hostinfo{}, Hostinfo: &tailcfg.Hostinfo{},
Routes: []types.Route{ Routes: []types.Route{
{ {

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

@ -63,7 +63,8 @@ type Config struct {
ACMEURL string ACMEURL string
ACMEEmail string ACMEEmail string
DNSConfig *tailcfg.DNSConfig DNSConfig *tailcfg.DNSConfig
DNSUserNameInMagicDNS bool
UnixSocket string UnixSocket string
UnixSocketPermission fs.FileMode UnixSocketPermission fs.FileMode
@ -204,6 +205,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)
@ -540,16 +542,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
@ -575,8 +567,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
} }
@ -719,7 +721,8 @@ 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("acme_email"), ACMEEmail: viper.GetString("acme_email"),
ACMEURL: viper.GetString("acme_url"), ACMEURL: viper.GetString("acme_url"),

View file

@ -394,23 +394,32 @@ 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)
} }
if node.User.Name == "" {
return "", fmt.Errorf("failed to create valid FQDN: %w", ErrNodeUserHasNoName)
}
hostname = fmt.Sprintf( hostname = fmt.Sprintf(
"%s.%s.%s", "%s.%s",
node.GivenName, node.GivenName,
node.User.Name,
baseDomain, baseDomain,
) )
if cfg.DNSUserNameInMagicDNS {
if node.User.Name == "" {
return "", fmt.Errorf("failed to create valid FQDN: %w", ErrNodeUserHasNoName)
}
hostname = fmt.Sprintf(
"%s.%s.%s",
node.GivenName,
node.User.Name,
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{
Proxied: true, DNSConfig: &tailcfg.DNSConfig{
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,8 +231,11 @@ func TestNodeFQDN(t *testing.T) {
Name: "user", Name: "user",
}, },
}, },
dns: tailcfg.DNSConfig{ cfg: Config{
Proxied: true, DNSConfig: &tailcfg.DNSConfig{
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{
Proxied: true, DNSConfig: &tailcfg.DNSConfig{
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,8 +263,11 @@ func TestNodeFQDN(t *testing.T) {
Name: "user", Name: "user",
}, },
}, },
dns: tailcfg.DNSConfig{ cfg: Config{
Proxied: false, DNSConfig: &tailcfg.DNSConfig{
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)