From 911e6ba6de445092dca7e809477e594133ea9cdc Mon Sep 17 00:00:00 2001
From: Grigoriy Mikhalkin <grigoriymikhalkin@gmail.com>
Date: Fri, 29 Jul 2022 17:35:21 +0200
Subject: [PATCH 1/4] exported API errors

---
 api_key.go           |  4 ++--
 db.go                |  6 +++---
 machine.go           | 24 ++++++++++++------------
 namespaces.go        | 28 ++++++++++++++--------------
 namespaces_test.go   | 12 ++++++------
 oidc.go              |  2 +-
 preauth_keys.go      | 16 ++++++++--------
 preauth_keys_test.go | 10 +++++-----
 routes.go            |  4 ++--
 utils.go             |  8 ++++----
 10 files changed, 57 insertions(+), 57 deletions(-)

diff --git a/api_key.go b/api_key.go
index c1bbce2d..01291d72 100644
--- a/api_key.go
+++ b/api_key.go
@@ -14,7 +14,7 @@ const (
 	apiPrefixLength = 7
 	apiKeyLength    = 32
 
-	errAPIKeyFailedToParse = Error("Failed to parse ApiKey")
+	ErrAPIKeyFailedToParse = Error("Failed to parse ApiKey")
 )
 
 // APIKey describes the datamodel for API keys used to remotely authenticate with
@@ -116,7 +116,7 @@ func (h *Headscale) ExpireAPIKey(key *APIKey) error {
 func (h *Headscale) ValidateAPIKey(keyStr string) (bool, error) {
 	prefix, hash, found := strings.Cut(keyStr, ".")
 	if !found {
-		return false, errAPIKeyFailedToParse
+		return false, ErrAPIKeyFailedToParse
 	}
 
 	key, err := h.GetAPIKey(prefix)
diff --git a/db.go b/db.go
index 5df9c23b..f0a0a598 100644
--- a/db.go
+++ b/db.go
@@ -248,7 +248,7 @@ func (hi *HostInfo) Scan(destination interface{}) error {
 		return json.Unmarshal([]byte(value), hi)
 
 	default:
-		return fmt.Errorf("%w: unexpected data type %T", errMachineAddressesInvalid, destination)
+		return fmt.Errorf("%w: unexpected data type %T", ErrMachineAddressesInvalid, destination)
 	}
 }
 
@@ -270,7 +270,7 @@ func (i *IPPrefixes) Scan(destination interface{}) error {
 		return json.Unmarshal([]byte(value), i)
 
 	default:
-		return fmt.Errorf("%w: unexpected data type %T", errMachineAddressesInvalid, destination)
+		return fmt.Errorf("%w: unexpected data type %T", ErrMachineAddressesInvalid, destination)
 	}
 }
 
@@ -292,7 +292,7 @@ func (i *StringList) Scan(destination interface{}) error {
 		return json.Unmarshal([]byte(value), i)
 
 	default:
-		return fmt.Errorf("%w: unexpected data type %T", errMachineAddressesInvalid, destination)
+		return fmt.Errorf("%w: unexpected data type %T", ErrMachineAddressesInvalid, destination)
 	}
 }
 
diff --git a/machine.go b/machine.go
index dda49020..22be0da1 100644
--- a/machine.go
+++ b/machine.go
@@ -18,14 +18,14 @@ import (
 )
 
 const (
-	errMachineNotFound                  = Error("machine not found")
-	errMachineRouteIsNotAvailable       = Error("route is not available on machine")
-	errMachineAddressesInvalid          = Error("failed to parse machine addresses")
-	errMachineNotFoundRegistrationCache = Error(
+	ErrMachineNotFound                  = Error("machine not found")
+	ErrMachineRouteIsNotAvailable       = Error("route is not available on machine")
+	ErrMachineAddressesInvalid          = Error("failed to parse machine addresses")
+	ErrMachineNotFoundRegistrationCache = Error(
 		"machine not found in registration cache",
 	)
-	errCouldNotConvertMachineInterface = Error("failed to convert machine interface")
-	errHostnameTooLong                 = Error("Hostname too long")
+	ErrCouldNotConvertMachineInterface = Error("failed to convert machine interface")
+	ErrHostnameTooLong                 = Error("Hostname too long")
 	MachineGivenNameHashLength         = 8
 	MachineGivenNameTrimSize           = 2
 )
@@ -112,7 +112,7 @@ func (ma *MachineAddresses) Scan(destination interface{}) error {
 		return nil
 
 	default:
-		return fmt.Errorf("%w: unexpected data type %T", errMachineAddressesInvalid, destination)
+		return fmt.Errorf("%w: unexpected data type %T", ErrMachineAddressesInvalid, destination)
 	}
 }
 
@@ -337,7 +337,7 @@ func (h *Headscale) GetMachine(namespace string, name string) (*Machine, error)
 		}
 	}
 
-	return nil, errMachineNotFound
+	return nil, ErrMachineNotFound
 }
 
 // GetMachineByID finds a Machine by ID and returns the Machine struct.
@@ -635,7 +635,7 @@ func (machine Machine) toNode(
 			return nil, fmt.Errorf(
 				"hostname %q is too long it cannot except 255 ASCII chars: %w",
 				hostname,
-				errHostnameTooLong,
+				ErrHostnameTooLong,
 			)
 		}
 	} else {
@@ -785,11 +785,11 @@ func (h *Headscale) RegisterMachineFromAuthCallback(
 
 			return machine, err
 		} else {
-			return nil, errCouldNotConvertMachineInterface
+			return nil, ErrCouldNotConvertMachineInterface
 		}
 	}
 
-	return nil, errMachineNotFoundRegistrationCache
+	return nil, ErrMachineNotFoundRegistrationCache
 }
 
 // RegisterMachine is executed from the CLI to register a new Machine using its MachineKey.
@@ -877,7 +877,7 @@ func (h *Headscale) EnableRoutes(machine *Machine, routeStrs ...string) error {
 			return fmt.Errorf(
 				"route (%s) is not available on node %s: %w",
 				machine.Hostname,
-				newRoute, errMachineRouteIsNotAvailable,
+				newRoute, ErrMachineRouteIsNotAvailable,
 			)
 		}
 	}
diff --git a/namespaces.go b/namespaces.go
index 0add03ff..ac8913ff 100644
--- a/namespaces.go
+++ b/namespaces.go
@@ -16,10 +16,10 @@ import (
 )
 
 const (
-	errNamespaceExists          = Error("Namespace already exists")
-	errNamespaceNotFound        = Error("Namespace not found")
-	errNamespaceNotEmptyOfNodes = Error("Namespace not empty: node(s) found")
-	errInvalidNamespaceName     = Error("Invalid namespace name")
+	ErrNamespaceExists          = Error("Namespace already exists")
+	ErrNamespaceNotFound        = Error("Namespace not found")
+	ErrNamespaceNotEmptyOfNodes = Error("Namespace not empty: node(s) found")
+	ErrInvalidNamespaceName     = Error("Invalid namespace name")
 )
 
 const (
@@ -47,7 +47,7 @@ func (h *Headscale) CreateNamespace(name string) (*Namespace, error) {
 	}
 	namespace := Namespace{}
 	if err := h.db.Where("name = ?", name).First(&namespace).Error; err == nil {
-		return nil, errNamespaceExists
+		return nil, ErrNamespaceExists
 	}
 	namespace.Name = name
 	if err := h.db.Create(&namespace).Error; err != nil {
@@ -67,7 +67,7 @@ func (h *Headscale) CreateNamespace(name string) (*Namespace, error) {
 func (h *Headscale) DestroyNamespace(name string) error {
 	namespace, err := h.GetNamespace(name)
 	if err != nil {
-		return errNamespaceNotFound
+		return ErrNamespaceNotFound
 	}
 
 	machines, err := h.ListMachinesInNamespace(name)
@@ -75,7 +75,7 @@ func (h *Headscale) DestroyNamespace(name string) error {
 		return err
 	}
 	if len(machines) > 0 {
-		return errNamespaceNotEmptyOfNodes
+		return ErrNamespaceNotEmptyOfNodes
 	}
 
 	keys, err := h.ListPreAuthKeys(name)
@@ -110,9 +110,9 @@ func (h *Headscale) RenameNamespace(oldName, newName string) error {
 	}
 	_, err = h.GetNamespace(newName)
 	if err == nil {
-		return errNamespaceExists
+		return ErrNamespaceExists
 	}
-	if !errors.Is(err, errNamespaceNotFound) {
+	if !errors.Is(err, ErrNamespaceNotFound) {
 		return err
 	}
 
@@ -132,7 +132,7 @@ func (h *Headscale) GetNamespace(name string) (*Namespace, error) {
 		result.Error,
 		gorm.ErrRecordNotFound,
 	) {
-		return nil, errNamespaceNotFound
+		return nil, ErrNamespaceNotFound
 	}
 
 	return &namespace, nil
@@ -272,7 +272,7 @@ func NormalizeToFQDNRules(name string, stripEmailDomain bool) (string, error) {
 			return "", fmt.Errorf(
 				"label %v is more than 63 chars: %w",
 				elt,
-				errInvalidNamespaceName,
+				ErrInvalidNamespaceName,
 			)
 		}
 	}
@@ -285,21 +285,21 @@ func CheckForFQDNRules(name string) error {
 		return fmt.Errorf(
 			"DNS segment must not be over 63 chars. %v doesn't comply with this rule: %w",
 			name,
-			errInvalidNamespaceName,
+			ErrInvalidNamespaceName,
 		)
 	}
 	if strings.ToLower(name) != name {
 		return fmt.Errorf(
 			"DNS segment should be lowercase. %v doesn't comply with this rule: %w",
 			name,
-			errInvalidNamespaceName,
+			ErrInvalidNamespaceName,
 		)
 	}
 	if invalidCharsInNamespaceRegex.MatchString(name) {
 		return fmt.Errorf(
 			"DNS segment should only be composed of lowercase ASCII letters numbers, hyphen and dots. %v doesn't comply with theses rules: %w",
 			name,
-			errInvalidNamespaceName,
+			ErrInvalidNamespaceName,
 		)
 	}
 
diff --git a/namespaces_test.go b/namespaces_test.go
index f8afa157..6f33585f 100644
--- a/namespaces_test.go
+++ b/namespaces_test.go
@@ -26,7 +26,7 @@ func (s *Suite) TestCreateAndDestroyNamespace(c *check.C) {
 
 func (s *Suite) TestDestroyNamespaceErrors(c *check.C) {
 	err := app.DestroyNamespace("test")
-	c.Assert(err, check.Equals, errNamespaceNotFound)
+	c.Assert(err, check.Equals, ErrNamespaceNotFound)
 
 	namespace, err := app.CreateNamespace("test")
 	c.Assert(err, check.IsNil)
@@ -60,7 +60,7 @@ func (s *Suite) TestDestroyNamespaceErrors(c *check.C) {
 	app.db.Save(&machine)
 
 	err = app.DestroyNamespace("test")
-	c.Assert(err, check.Equals, errNamespaceNotEmptyOfNodes)
+	c.Assert(err, check.Equals, ErrNamespaceNotEmptyOfNodes)
 }
 
 func (s *Suite) TestRenameNamespace(c *check.C) {
@@ -76,20 +76,20 @@ func (s *Suite) TestRenameNamespace(c *check.C) {
 	c.Assert(err, check.IsNil)
 
 	_, err = app.GetNamespace("test")
-	c.Assert(err, check.Equals, errNamespaceNotFound)
+	c.Assert(err, check.Equals, ErrNamespaceNotFound)
 
 	_, err = app.GetNamespace("test-renamed")
 	c.Assert(err, check.IsNil)
 
 	err = app.RenameNamespace("test-does-not-exit", "test")
-	c.Assert(err, check.Equals, errNamespaceNotFound)
+	c.Assert(err, check.Equals, ErrNamespaceNotFound)
 
 	namespaceTest2, err := app.CreateNamespace("test2")
 	c.Assert(err, check.IsNil)
 	c.Assert(namespaceTest2.Name, check.Equals, "test2")
 
 	err = app.RenameNamespace("test2", "test-renamed")
-	c.Assert(err, check.Equals, errNamespaceExists)
+	c.Assert(err, check.Equals, ErrNamespaceExists)
 }
 
 func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
@@ -402,7 +402,7 @@ func (s *Suite) TestSetMachineNamespace(c *check.C) {
 	c.Assert(machine.Namespace.Name, check.Equals, newNamespace.Name)
 
 	err = app.SetMachineNamespace(&machine, "non-existing-namespace")
-	c.Assert(err, check.Equals, errNamespaceNotFound)
+	c.Assert(err, check.Equals, ErrNamespaceNotFound)
 
 	err = app.SetMachineNamespace(&machine, newNamespace.Name)
 	c.Assert(err, check.IsNil)
diff --git a/oidc.go b/oidc.go
index 8b5f0242..cd0d9185 100644
--- a/oidc.go
+++ b/oidc.go
@@ -416,7 +416,7 @@ func (h *Headscale) OIDCCallback(
 	log.Debug().Msg("Registering new machine after successful callback")
 
 	namespace, err := h.GetNamespace(namespaceName)
-	if errors.Is(err, errNamespaceNotFound) {
+	if errors.Is(err, ErrNamespaceNotFound) {
 		namespace, err = h.CreateNamespace(namespaceName)
 
 		if err != nil {
diff --git a/preauth_keys.go b/preauth_keys.go
index b32ff636..f120f452 100644
--- a/preauth_keys.go
+++ b/preauth_keys.go
@@ -14,10 +14,10 @@ import (
 )
 
 const (
-	errPreAuthKeyNotFound          = Error("AuthKey not found")
-	errPreAuthKeyExpired           = Error("AuthKey expired")
-	errSingleUseAuthKeyHasBeenUsed = Error("AuthKey has already been used")
-	errNamespaceMismatch           = Error("namespace mismatch")
+	ErrPreAuthKeyNotFound          = Error("AuthKey not found")
+	ErrPreAuthKeyExpired           = Error("AuthKey expired")
+	ErrSingleUseAuthKeyHasBeenUsed = Error("AuthKey has already been used")
+	ErrNamespaceMismatch           = Error("namespace mismatch")
 )
 
 // PreAuthKey describes a pre-authorization key usable in a particular namespace.
@@ -92,7 +92,7 @@ func (h *Headscale) GetPreAuthKey(namespace string, key string) (*PreAuthKey, er
 	}
 
 	if pak.Namespace.Name != namespace {
-		return nil, errNamespaceMismatch
+		return nil, ErrNamespaceMismatch
 	}
 
 	return pak, nil
@@ -135,11 +135,11 @@ func (h *Headscale) checkKeyValidity(k string) (*PreAuthKey, error) {
 		result.Error,
 		gorm.ErrRecordNotFound,
 	) {
-		return nil, errPreAuthKeyNotFound
+		return nil, ErrPreAuthKeyNotFound
 	}
 
 	if pak.Expiration != nil && pak.Expiration.Before(time.Now()) {
-		return nil, errPreAuthKeyExpired
+		return nil, ErrPreAuthKeyExpired
 	}
 
 	if pak.Reusable || pak.Ephemeral { // we don't need to check if has been used before
@@ -152,7 +152,7 @@ func (h *Headscale) checkKeyValidity(k string) (*PreAuthKey, error) {
 	}
 
 	if len(machines) != 0 || pak.Used {
-		return nil, errSingleUseAuthKeyHasBeenUsed
+		return nil, ErrSingleUseAuthKeyHasBeenUsed
 	}
 
 	return &pak, nil
diff --git a/preauth_keys_test.go b/preauth_keys_test.go
index c54c1bf4..cd9c66aa 100644
--- a/preauth_keys_test.go
+++ b/preauth_keys_test.go
@@ -44,13 +44,13 @@ func (*Suite) TestExpiredPreAuthKey(c *check.C) {
 	c.Assert(err, check.IsNil)
 
 	key, err := app.checkKeyValidity(pak.Key)
-	c.Assert(err, check.Equals, errPreAuthKeyExpired)
+	c.Assert(err, check.Equals, ErrPreAuthKeyExpired)
 	c.Assert(key, check.IsNil)
 }
 
 func (*Suite) TestPreAuthKeyDoesNotExist(c *check.C) {
 	key, err := app.checkKeyValidity("potatoKey")
-	c.Assert(err, check.Equals, errPreAuthKeyNotFound)
+	c.Assert(err, check.Equals, ErrPreAuthKeyNotFound)
 	c.Assert(key, check.IsNil)
 }
 
@@ -86,7 +86,7 @@ func (*Suite) TestAlreadyUsedKey(c *check.C) {
 	app.db.Save(&machine)
 
 	key, err := app.checkKeyValidity(pak.Key)
-	c.Assert(err, check.Equals, errSingleUseAuthKeyHasBeenUsed)
+	c.Assert(err, check.Equals, ErrSingleUseAuthKeyHasBeenUsed)
 	c.Assert(key, check.IsNil)
 }
 
@@ -174,7 +174,7 @@ func (*Suite) TestExpirePreauthKey(c *check.C) {
 	c.Assert(pak.Expiration, check.NotNil)
 
 	key, err := app.checkKeyValidity(pak.Key)
-	c.Assert(err, check.Equals, errPreAuthKeyExpired)
+	c.Assert(err, check.Equals, ErrPreAuthKeyExpired)
 	c.Assert(key, check.IsNil)
 }
 
@@ -188,5 +188,5 @@ func (*Suite) TestNotReusableMarkedAsUsed(c *check.C) {
 	app.db.Save(&pak)
 
 	_, err = app.checkKeyValidity(pak.Key)
-	c.Assert(err, check.Equals, errSingleUseAuthKeyHasBeenUsed)
+	c.Assert(err, check.Equals, ErrSingleUseAuthKeyHasBeenUsed)
 }
diff --git a/routes.go b/routes.go
index d062f827..23217ca2 100644
--- a/routes.go
+++ b/routes.go
@@ -7,7 +7,7 @@ import (
 )
 
 const (
-	errRouteIsNotAvailable = Error("route is not available")
+	ErrRouteIsNotAvailable = Error("route is not available")
 )
 
 // Deprecated: use machine function instead
@@ -106,7 +106,7 @@ func (h *Headscale) EnableNodeRoute(
 	}
 
 	if !available {
-		return errRouteIsNotAvailable
+		return ErrRouteIsNotAvailable
 	}
 
 	machine.EnabledRoutes = enabledRoutes
diff --git a/utils.go b/utils.go
index 87930a16..b586f7ec 100644
--- a/utils.go
+++ b/utils.go
@@ -27,8 +27,8 @@ import (
 )
 
 const (
-	errCannotDecryptReponse = Error("cannot decrypt response")
-	errCouldNotAllocateIP   = Error("could not find any suitable IP")
+	ErrCannotDecryptReponse = Error("cannot decrypt response")
+	ErrCouldNotAllocateIP   = Error("could not find any suitable IP")
 
 	// These constants are copied from the upstream tailscale.com/types/key
 	// library, because they are not exported.
@@ -120,7 +120,7 @@ func decode(
 
 	decrypted, ok := privKey.OpenFrom(*pubKey, msg)
 	if !ok {
-		return errCannotDecryptReponse
+		return ErrCannotDecryptReponse
 	}
 
 	if err := json.Unmarshal(decrypted, output); err != nil {
@@ -181,7 +181,7 @@ func (h *Headscale) getAvailableIP(ipPrefix netaddr.IPPrefix) (*netaddr.IP, erro
 
 	for {
 		if !ipPrefix.Contains(ip) {
-			return nil, errCouldNotAllocateIP
+			return nil, ErrCouldNotAllocateIP
 		}
 
 		switch {

From 10d566c94690e475fa187809c09c722d902335a7 Mon Sep 17 00:00:00 2001
From: Rasmus Moorats <xx@nns.ee>
Date: Tue, 2 Aug 2022 09:49:28 +0300
Subject: [PATCH 2/4] add details on how to use the android app

---
 README.md              | 18 +++++++++---------
 docs/android-client.md | 19 +++++++++++++++++++
 2 files changed, 28 insertions(+), 9 deletions(-)
 create mode 100644 docs/android-client.md

diff --git a/README.md b/README.md
index d0887675..0cfed4dd 100644
--- a/README.md
+++ b/README.md
@@ -67,15 +67,15 @@ one of the maintainers.
 
 ## Client OS support
 
-| OS      | Supports headscale                                                                                                |
-| ------- | ----------------------------------------------------------------------------------------------------------------- |
-| Linux   | Yes                                                                                                               |
-| OpenBSD | Yes                                                                                                               |
-| FreeBSD | Yes                                                                                                               |
-| macOS   | Yes (see `/apple` on your headscale for more information)                                                         |
-| Windows | Yes [docs](./docs/windows-client.md)                                                                              |
-| Android | [You need to compile the client yourself](https://github.com/juanfont/headscale/issues/58#issuecomment-885255270) |
-| iOS     | Not yet                                                                                                           |
+| OS      | Supports headscale                                        |
+| ------- | --------------------------------------------------------- |
+| Linux   | Yes                                                       |
+| OpenBSD | Yes                                                       |
+| FreeBSD | Yes                                                       |
+| macOS   | Yes (see `/apple` on your headscale for more information) |
+| Windows | Yes [docs](./docs/windows-client.md)                      |
+| Android | Yes [docs](./docs/android-client.md)                      |
+| iOS     | Not yet                                                   |
 
 ## Running headscale
 
diff --git a/docs/android-client.md b/docs/android-client.md
new file mode 100644
index 00000000..d4f8129c
--- /dev/null
+++ b/docs/android-client.md
@@ -0,0 +1,19 @@
+# Connecting an Android client
+
+## Goal
+
+This documentation has the goal of showing how a user can use the official Android [Tailscale](https://tailscale.com) client with `headscale`.
+
+## Installation
+
+Install the official Tailscale Android client from the [Google Play Store](https://play.google.com/store/apps/details?id=com.tailscale.ipn) or [F-Droid](https://f-droid.org/packages/com.tailscale.ipn/).
+
+Ensure that the installed version is at least 1.30.0, as that is the first release to support custom URLs.
+
+## 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.
+
+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>
+
+After saving and restarting the app, selecting the regular _Sign in_ option (non-SSO) should open up the headscale authentication page.

From cc1343d31d2bd60011dee51f3b6ea125f32812d3 Mon Sep 17 00:00:00 2001
From: Grigoriy Mikhalkin <grigoriymikhalkin@gmail.com>
Date: Fri, 5 Aug 2022 00:00:36 +0200
Subject: [PATCH 3/4] fixed typo in ErrCannotDecryptResponse name

---
 utils.go | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/utils.go b/utils.go
index b586f7ec..b4362535 100644
--- a/utils.go
+++ b/utils.go
@@ -27,8 +27,8 @@ import (
 )
 
 const (
-	ErrCannotDecryptReponse = Error("cannot decrypt response")
-	ErrCouldNotAllocateIP   = Error("could not find any suitable IP")
+	ErrCannotDecryptResponse = Error("cannot decrypt response")
+	ErrCouldNotAllocateIP    = Error("could not find any suitable IP")
 
 	// These constants are copied from the upstream tailscale.com/types/key
 	// library, because they are not exported.
@@ -120,7 +120,7 @@ func decode(
 
 	decrypted, ok := privKey.OpenFrom(*pubKey, msg)
 	if !ok {
-		return ErrCannotDecryptReponse
+		return ErrCannotDecryptResponse
 	}
 
 	if err := json.Unmarshal(decrypted, output); err != nil {

From 2254ac2102ce08d542c6b8d420aebe583352e4e0 Mon Sep 17 00:00:00 2001
From: Steve Malloy <sophware@hotmail.com>
Date: Fri, 5 Aug 2022 15:44:11 -0400
Subject: [PATCH 4/4] typo fixed from advertised to advertise

---
 docs/acls.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/acls.md b/docs/acls.md
index d69ed8fb..148f973f 100644
--- a/docs/acls.md
+++ b/docs/acls.md
@@ -36,7 +36,7 @@ ACLs could be written either on [huJSON](https://github.com/tailscale/hujson)
 or YAML. Check the [test ACLs](../tests/acls) for further information.
 
 When registering the servers we will need to add the flag
-`--advertised-tags=tag:<tag1>,tag:<tag2>`, and the user (namespace) that is
+`--advertise-tags=tag:<tag1>,tag:<tag2>`, and the user (namespace) that is
 registering the server should be allowed to do it. Since anyone can add tags to
 a server they can register, the check of the tags is done on headscale server
 and only valid tags are applied. A tag is valid if the namespace that is