diff --git a/CHANGELOG.md b/CHANGELOG.md index 03516fd6..fced0b6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) - `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) +- 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 @@ -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) - 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) +- 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) diff --git a/config-example.yaml b/config-example.yaml index 7c303e9f..442b30b3 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -145,6 +145,10 @@ database: 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: # # 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. 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. # `base_domain` must be a FQDNs, without the trailing dot. # The FQDN of the hosts will be diff --git a/docs/android-client.md b/docs/android-client.md index d4f8129c..21dd8d21 100644 --- a/docs/android-client.md +++ b/docs/android-client.md @@ -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 -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: - -After saving and restarting the app, selecting the regular _Sign in_ option (non-SSO) should open up the headscale authentication page. +- 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” +- Enter your server URL and follow the instructions diff --git a/docs/index.md b/docs/index.md index d13339d8..f0b8bb00 100644 --- a/docs/index.md +++ b/docs/index.md @@ -8,7 +8,7 @@ hide: `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. diff --git a/docs/reverse-proxy.md b/docs/reverse-proxy.md index c6fd4b16..23c61c26 100644 --- a/docs/reverse-proxy.md +++ b/docs/reverse-proxy.md @@ -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). +### 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 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. diff --git a/docs/running-headscale-linux-manual.md b/docs/running-headscale-linux-manual.md index 4108208f..3651c892 100644 --- a/docs/running-headscale-linux-manual.md +++ b/docs/running-headscale-linux-manual.md @@ -57,7 +57,7 @@ describing how to make `headscale` run properly in a server environment. 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: diff --git a/docs/running-headscale-openbsd.md b/docs/running-headscale-openbsd.md index e1d8d83f..72c7bf79 100644 --- a/docs/running-headscale-openbsd.md +++ b/docs/running-headscale-openbsd.md @@ -93,7 +93,7 @@ describing how to make `headscale` run properly in a server environment. 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: diff --git a/flake.lock b/flake.lock index 351c657c..6de98223 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1716062047, - "narHash": "sha256-OhysviwHQz4p2HZL4g7XGMLoUbWMjkMr/ogaR3VUYNA=", + "lastModified": 1720181791, + "narHash": "sha256-i4vJL12/AdyuQuviMMd1Hk2tsGt02hDNhA0Zj1m16N8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "02923630b89aa1ab36ef8e422501a6f4fd4b2016", + "rev": "4284c2b73c8bce4b46a6adf23e16d9e2ec8da4bb", "type": "github" }, "original": { diff --git a/hscontrol/db/db.go b/hscontrol/db/db.go index a30939c1..69994d02 100644 --- a/hscontrol/db/db.go +++ b/hscontrol/db/db.go @@ -92,7 +92,7 @@ func NewHeadscaleDatabase( RenameColumn(&types.Node{}, "nickname", "given_name") dbConn.Model(&types.Node{}).Where("auth_key_id = ?", 0).Update("auth_key_id", nil) - // If the Node table has a column for registered, + // If the Node table has a column for registered, // find all occourences of "false" and drop them. Then // remove the column. if tx.Migrator().HasColumn(&types.Node{}, "registered") { @@ -319,14 +319,8 @@ func NewHeadscaleDatabase( // no longer used. ID: "202402151347", Migrate: func(tx *gorm.DB) error { - err := tx.Migrator().DropColumn(&types.Node{}, "last_successful_update") - if err != nil && strings.Contains(err.Error(), `of relation "nodes" does not exist`) { - return nil - } else { - return err - } - - return err + _ = tx.Migrator().DropColumn(&types.Node{}, "last_successful_update") + return nil }, Rollback: func(tx *gorm.DB) error { return nil @@ -440,13 +434,29 @@ func openDB(cfg types.DatabaseConfig) (*gorm.DB, error) { Msg("Opening database") db, err := gorm.Open( - sqlite.Open(cfg.Sqlite.Path+"?_synchronous=1&_journal_mode=WAL"), + sqlite.Open(cfg.Sqlite.Path), &gorm.Config{ 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 same way as the C based one and we cant use the gorm diff --git a/hscontrol/db/node.go b/hscontrol/db/node.go index c675dc7c..e36d6ed1 100644 --- a/hscontrol/db/node.go +++ b/hscontrol/db/node.go @@ -215,7 +215,7 @@ func SetTags( return nil } - newTags := types.StringList{} + var newTags types.StringList for _, tag := range tags { if !util.StringOrPrefixListContains(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) } - prefixes := []netip.Prefix{} + var prefixes []netip.Prefix for _, route := range routes { 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) } - prefixes := []netip.Prefix{} + var prefixes []netip.Prefix for _, route := range routes { prefixes = append(prefixes, netip.Prefix(route.Prefix)) } diff --git a/hscontrol/db/routes.go b/hscontrol/db/routes.go index 74b2b4b7..3b897190 100644 --- a/hscontrol/db/routes.go +++ b/hscontrol/db/routes.go @@ -222,7 +222,7 @@ func DeleteRoute( return nil, err } - routesToDelete := types.Routes{} + var routesToDelete types.Routes for _, r := range routes { if r.IsExitRoute() { routesToDelete = append(routesToDelete, r) @@ -623,7 +623,7 @@ func EnableAutoApprovedRoutes( log.Trace().Interface("routes", routes).Msg("routes for autoapproving") - approvedRoutes := types.Routes{} + var approvedRoutes types.Routes for _, advertisedRoute := range routes { if advertisedRoute.Enabled { diff --git a/hscontrol/derp/derp.go b/hscontrol/derp/derp.go index 80ec520d..3afcb4ea 100644 --- a/hscontrol/derp/derp.go +++ b/hscontrol/derp/derp.go @@ -81,7 +81,7 @@ func mergeDERPMaps(derpMaps []*tailcfg.DERPMap) *tailcfg.DERPMap { } func GetDERPMap(cfg types.DERPConfig) *tailcfg.DERPMap { - derpMaps := make([]*tailcfg.DERPMap, 0) + var derpMaps []*tailcfg.DERPMap for _, path := range cfg.Paths { log.Debug(). diff --git a/hscontrol/handlers.go b/hscontrol/handlers.go index a6bbd1b8..6efe1984 100644 --- a/hscontrol/handlers.go +++ b/hscontrol/handlers.go @@ -143,6 +143,18 @@ var registerWebAPITemplate = template.Must( Registration - Headscale + +

headscale

@@ -150,7 +162,7 @@ var registerWebAPITemplate = template.Must(

Run the command below in the headscale server to add this machine to your network:

-
headscale nodes register --user USERNAME --key {{.Key}}
+ headscale nodes register --user USERNAME --key {{.Key}} `)) diff --git a/hscontrol/mapper/mapper.go b/hscontrol/mapper/mapper.go index d4f4392a..adc49669 100644 --- a/hscontrol/mapper/mapper.go +++ b/hscontrol/mapper/mapper.go @@ -102,7 +102,7 @@ func generateUserProfiles( userMap[peer.User.Name] = peer.User // not worth checking if already is there } - profiles := []tailcfg.UserProfile{} + var profiles []tailcfg.UserProfile for _, user := range userMap { displayName := user.Name @@ -122,37 +122,41 @@ func generateUserProfiles( } func generateDNSConfig( - base *tailcfg.DNSConfig, + cfg *types.Config, baseDomain string, node *types.Node, peers types.Nodes, ) *tailcfg.DNSConfig { - dnsConfig := base.Clone() + if cfg.DNSConfig == nil { + return nil + } + + dnsConfig := cfg.DNSConfig.Clone() // if MagicDNS is enabled - if base != nil && base.Proxied { - // Only inject the Search Domain of the current user - // shared nodes should use their full FQDN - dnsConfig.Domains = append( - dnsConfig.Domains, - fmt.Sprintf( - "%s.%s", - node.User.Name, - baseDomain, - ), - ) + if dnsConfig.Proxied { + if cfg.DNSUserNameInMagicDNS { + // Only inject the Search Domain of the current user + // shared nodes should use their full FQDN + dnsConfig.Domains = append( + dnsConfig.Domains, + fmt.Sprintf( + "%s.%s", + node.User.Name, + baseDomain, + ), + ) - userSet := mapset.NewSet[types.User]() - userSet.Add(node.User) - for _, p := range peers { - userSet.Add(p.User) + userSet := mapset.NewSet[types.User]() + userSet.Add(node.User) + for _, p := range peers { + 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) @@ -568,7 +572,7 @@ func appendPeerChanges( profiles := generateUserProfiles(node, changed, cfg.BaseDomain) dnsConfig := generateDNSConfig( - cfg.DNSConfig, + cfg, cfg.BaseDomain, node, peers, diff --git a/hscontrol/mapper/mapper_test.go b/hscontrol/mapper/mapper_test.go index 2ba3d031..be48c6fa 100644 --- a/hscontrol/mapper/mapper_test.go +++ b/hscontrol/mapper/mapper_test.go @@ -127,7 +127,10 @@ func TestDNSConfigMapResponse(t *testing.T) { } got := generateDNSConfig( - &dnsConfigOrig, + &types.Config{ + DNSConfig: &dnsConfigOrig, + DNSUserNameInMagicDNS: true, + }, baseDomain, nodeInShared1, peersOfNodeInShared1, @@ -187,9 +190,9 @@ func Test_fullMapResponse(t *testing.T) { UserID: 0, User: types.User{Name: "mini"}, ForcedTags: []string{}, - AuthKey: &types.PreAuthKey{}, - LastSeen: &lastSeen, - Expiry: &expire, + AuthKey: &types.PreAuthKey{}, + LastSeen: &lastSeen, + Expiry: &expire, Hostinfo: &tailcfg.Hostinfo{}, Routes: []types.Route{ { diff --git a/hscontrol/mapper/tail.go b/hscontrol/mapper/tail.go index ac39d35e..92fbed81 100644 --- a/hscontrol/mapper/tail.go +++ b/hscontrol/mapper/tail.go @@ -77,7 +77,7 @@ func tailNode( keyExpiry = time.Time{} } - hostname, err := node.GetFQDN(cfg.DNSConfig, cfg.BaseDomain) + hostname, err := node.GetFQDN(cfg, cfg.BaseDomain) if err != nil { return nil, fmt.Errorf("tailNode, failed to create FQDN: %s", err) } diff --git a/hscontrol/notifier/notifier_test.go b/hscontrol/notifier/notifier_test.go index 8841a46d..c41e0039 100644 --- a/hscontrol/notifier/notifier_test.go +++ b/hscontrol/notifier/notifier_test.go @@ -3,6 +3,7 @@ package notifier import ( "context" "net/netip" + "sort" "testing" "time" @@ -221,6 +222,11 @@ func TestBatcher(t *testing.T) { // We will call flush manually for the tests, // so do not run the worker. 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) } + // 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 != "" { t.Errorf("batcher() unexpected result (-want +got):\n%s", diff) } diff --git a/hscontrol/policy/acls.go b/hscontrol/policy/acls.go index 1196995d..9dde401b 100644 --- a/hscontrol/policy/acls.go +++ b/hscontrol/policy/acls.go @@ -180,14 +180,14 @@ func (pol *ACLPolicy) CompileFilterRules( return tailcfg.FilterAllowAll, nil } - rules := []tailcfg.FilterRule{} + var rules []tailcfg.FilterRule for index, acl := range pol.ACLs { if acl.Action != "accept" { return nil, ErrInvalidAction } - srcIPs := []string{} + var srcIPs []string for srcIndex, src := range acl.Sources { srcs, err := pol.expandSource(src, nodes) if err != nil { @@ -221,7 +221,7 @@ func (pol *ACLPolicy) CompileFilterRules( return nil, err } - dests := []tailcfg.NetPortRange{} + var dests []tailcfg.NetPortRange for _, dest := range expanded.Prefixes() { for _, port := range *ports { pr := tailcfg.NetPortRange{ @@ -251,8 +251,7 @@ func ReduceFilterRules(node *types.Node, rules []tailcfg.FilterRule) []tailcfg.F for _, rule := range rules { // record if the rule is actually relevant for the given node. - dests := []tailcfg.NetPortRange{} - + var dests []tailcfg.NetPortRange DEST_LOOP: for _, dest := range rule.DstPorts { expanded, err := util.ParseIPSet(dest.IP, nil) @@ -301,7 +300,7 @@ func (pol *ACLPolicy) CompileSSHPolicy( return nil, nil } - rules := []*tailcfg.SSHRule{} + var rules []*tailcfg.SSHRule acceptAction := tailcfg.SSHAction{ Message: "", @@ -533,8 +532,7 @@ func (pol *ACLPolicy) expandSource( return []string{}, err } - prefixes := []string{} - + var prefixes []string for _, prefix := range ipSet.Prefixes() { prefixes = append(prefixes, prefix.String()) } @@ -615,8 +613,8 @@ func excludeCorrectlyTaggedNodes( nodes types.Nodes, user string, ) types.Nodes { - out := types.Nodes{} - tags := []string{} + var out types.Nodes + var tags []string for tag := range aclPolicy.TagOwners { owners, _ := expandOwnersFromTag(aclPolicy, user) ns := append(owners, user) @@ -661,7 +659,7 @@ func expandPorts(portsStr string, isWild bool) (*[]tailcfg.PortRange, error) { return nil, ErrWildcardIsNeeded } - ports := []tailcfg.PortRange{} + var ports []tailcfg.PortRange for _, portStr := range strings.Split(portsStr, ",") { log.Trace().Msgf("parsing portstring: %s", portStr) rang := strings.Split(portStr, "-") @@ -737,7 +735,7 @@ func expandOwnersFromTag( func (pol *ACLPolicy) expandUsersFromGroup( group string, ) ([]string, error) { - users := []string{} + var users []string log.Trace().Caller().Interface("pol", pol).Msg("test") aclGroups, ok := pol.Groups[group] if !ok { @@ -772,7 +770,7 @@ func (pol *ACLPolicy) expandIPsFromGroup( group string, nodes types.Nodes, ) (*netipx.IPSet, error) { - build := netipx.IPSetBuilder{} + var build netipx.IPSetBuilder users, err := pol.expandUsersFromGroup(group) if err != nil { @@ -792,7 +790,7 @@ func (pol *ACLPolicy) expandIPsFromTag( alias string, nodes types.Nodes, ) (*netipx.IPSet, error) { - build := netipx.IPSetBuilder{} + var build netipx.IPSetBuilder // check for forced tags for _, node := range nodes { @@ -841,7 +839,7 @@ func (pol *ACLPolicy) expandIPsFromUser( user string, nodes types.Nodes, ) (*netipx.IPSet, error) { - build := netipx.IPSetBuilder{} + var build netipx.IPSetBuilder filteredNodes := filterNodesByUser(nodes, user) filteredNodes = excludeCorrectlyTaggedNodes(pol, filteredNodes, user) @@ -866,7 +864,7 @@ func (pol *ACLPolicy) expandIPsFromSingleIP( matches := nodes.FilterByIP(ip) - build := netipx.IPSetBuilder{} + var build netipx.IPSetBuilder build.Add(ip) for _, node := range matches { @@ -881,7 +879,7 @@ func (pol *ACLPolicy) expandIPsFromIPPrefix( nodes types.Nodes, ) (*netipx.IPSet, error) { log.Trace().Str("prefix", prefix.String()).Msg("expandAlias got prefix") - build := netipx.IPSetBuilder{} + var build netipx.IPSetBuilder build.AddPrefix(prefix) // 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( node *types.Node, ) ([]string, []string) { - validTags := make([]string, 0) - invalidTags := make([]string, 0) + var validTags []string + var invalidTags []string // TODO(kradalby): Why is this sometimes nil? coming from tailNode? if node == nil { @@ -973,7 +971,7 @@ func (pol *ACLPolicy) TagsOfNode( } func filterNodesByUser(nodes types.Nodes, user string) types.Nodes { - out := types.Nodes{} + var out types.Nodes for _, node := range nodes { if node.User.Name == user { out = append(out, node) @@ -989,7 +987,7 @@ func FilterNodesByACL( nodes types.Nodes, filter []tailcfg.FilterRule, ) types.Nodes { - result := types.Nodes{} + var result types.Nodes for index, peer := range nodes { if peer.ID == node.ID { diff --git a/hscontrol/policy/acls_test.go b/hscontrol/policy/acls_test.go index b0cafe10..c1e7ae08 100644 --- a/hscontrol/policy/acls_test.go +++ b/hscontrol/policy/acls_test.go @@ -943,7 +943,7 @@ func Test_listNodesInUser(t *testing.T) { }, user: "mickael", }, - want: types.Nodes{}, + want: nil, }, } for _, test := range tests { @@ -1645,7 +1645,7 @@ func TestACLPolicy_generateFilterRules(t *testing.T) { name: "no-policy", field: field{}, args: args{}, - want: []tailcfg.FilterRule{}, + want: nil, wantErr: false, }, { @@ -2896,7 +2896,7 @@ func Test_getFilteredByACLPeers(t *testing.T) { User: types.User{Name: "marc"}, }, }, - want: types.Nodes{}, + want: nil, }, { // Investigating 699 @@ -3426,7 +3426,7 @@ func TestSSHRules(t *testing.T) { }, }, }, - want: &tailcfg.SSHPolicy{Rules: []*tailcfg.SSHRule{}}, + want: &tailcfg.SSHPolicy{Rules: nil}, }, } diff --git a/hscontrol/types/config.go b/hscontrol/types/config.go index 7e944f81..664eafb1 100644 --- a/hscontrol/types/config.go +++ b/hscontrol/types/config.go @@ -63,7 +63,8 @@ type Config struct { ACMEURL string ACMEEmail string - DNSConfig *tailcfg.DNSConfig + DNSConfig *tailcfg.DNSConfig + DNSUserNameInMagicDNS bool UnixSocket string UnixSocketPermission fs.FileMode @@ -81,7 +82,8 @@ type Config struct { } type SqliteConfig struct { - Path string + Path string + WriteAheadLog bool } type PostgresConfig struct { @@ -205,6 +207,7 @@ func LoadConfig(path string, isFile bool) error { viper.SetDefault("dns_config", nil) 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.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.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.strip_email_domain", true) viper.SetDefault("oidc.only_start_if_oidc_is_available", true) @@ -463,6 +468,7 @@ func GetDatabaseConfig() DatabaseConfig { Path: util.AbsolutePathFromConfigPath( viper.GetString("database.sqlite.path"), ), + WriteAheadLog: viper.GetBool("database.sqlite.write_ahead_log"), }, Postgres: PostgresConfig{ Host: viper.GetString("database.postgres.host"), @@ -556,16 +562,6 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) { 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") { 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 } - 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 } @@ -735,7 +741,8 @@ func GetHeadscaleConfig() (*Config, error) { TLS: GetTLSConfig(), - DNSConfig: dnsConfig, + DNSConfig: dnsConfig, + DNSUserNameInMagicDNS: viper.GetBool("dns_config.use_username_in_magic_dns"), ACMEEmail: viper.GetString("tls.acme_email"), ACMEURL: viper.GetString("tls.acme_url"), diff --git a/hscontrol/types/node.go b/hscontrol/types/node.go index 3ccadc38..6bee5c42 100644 --- a/hscontrol/types/node.go +++ b/hscontrol/types/node.go @@ -394,23 +394,32 @@ func (node *Node) Proto() *v1.Node { 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 - if dnsConfig != nil && dnsConfig.Proxied { // MagicDNS + if cfg.DNSConfig != nil && cfg.DNSConfig.Proxied { // MagicDNS if node.GivenName == "" { 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( - "%s.%s.%s", + "%s.%s", node.GivenName, - node.User.Name, 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 { return "", fmt.Errorf( "failed to create valid FQDN (%s): %w", diff --git a/hscontrol/types/node_test.go b/hscontrol/types/node_test.go index 157be89e..85857c3a 100644 --- a/hscontrol/types/node_test.go +++ b/hscontrol/types/node_test.go @@ -126,11 +126,87 @@ func TestNodeFQDN(t *testing.T) { tests := []struct { name string node Node - dns tailcfg.DNSConfig + cfg Config domain string want 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", node: Node{ @@ -139,11 +215,14 @@ func TestNodeFQDN(t *testing.T) { Name: "user", }, }, - dns: tailcfg.DNSConfig{ - Proxied: true, + cfg: Config{ + DNSConfig: &tailcfg.DNSConfig{ + Proxied: true, + }, + DNSUserNameInMagicDNS: false, }, domain: "example.com", - want: "test.user.example.com", + want: "test.example.com", }, { name: "no-given-name", @@ -152,8 +231,11 @@ func TestNodeFQDN(t *testing.T) { Name: "user", }, }, - dns: tailcfg.DNSConfig{ - Proxied: true, + cfg: Config{ + DNSConfig: &tailcfg.DNSConfig{ + Proxied: true, + }, + DNSUserNameInMagicDNS: false, }, domain: "example.com", wantErr: "failed to create valid FQDN: node has no given name", @@ -164,11 +246,14 @@ func TestNodeFQDN(t *testing.T) { GivenName: "test", User: User{}, }, - dns: tailcfg.DNSConfig{ - Proxied: true, + cfg: Config{ + DNSConfig: &tailcfg.DNSConfig{ + Proxied: true, + }, + DNSUserNameInMagicDNS: false, }, - domain: "example.com", - wantErr: "failed to create valid FQDN: node user has no name", + domain: "example.com", + want: "test.example.com", }, { name: "no-magic-dns", @@ -178,8 +263,11 @@ func TestNodeFQDN(t *testing.T) { Name: "user", }, }, - dns: tailcfg.DNSConfig{ - Proxied: false, + cfg: Config{ + DNSConfig: &tailcfg.DNSConfig{ + Proxied: false, + }, + DNSUserNameInMagicDNS: false, }, domain: "example.com", want: "test", @@ -199,7 +287,7 @@ func TestNodeFQDN(t *testing.T) { for _, tc := range tests { 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) { t.Errorf("GetFQDN() error = %s, wantErr %s", err, tc.wantErr)