Merge branch 'main' into oidc-refactoring

This commit is contained in:
Juan Font 2022-08-09 20:29:02 +02:00 committed by GitHub
commit 8a9fe1da4b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 86 additions and 67 deletions

View file

@ -68,13 +68,13 @@ one of the maintainers.
## Client OS support ## Client OS support
| OS | Supports headscale | | OS | Supports headscale |
| ------- | ----------------------------------------------------------------------------------------------------------------- | | ------- | --------------------------------------------------------- |
| Linux | Yes | | Linux | Yes |
| OpenBSD | Yes | | OpenBSD | Yes |
| FreeBSD | Yes | | FreeBSD | Yes |
| macOS | Yes (see `/apple` on your headscale for more information) | | macOS | Yes (see `/apple` on your headscale for more information) |
| Windows | Yes [docs](./docs/windows-client.md) | | Windows | Yes [docs](./docs/windows-client.md) |
| Android | [You need to compile the client yourself](https://github.com/juanfont/headscale/issues/58#issuecomment-885255270) | | Android | Yes [docs](./docs/android-client.md) |
| iOS | Not yet | | iOS | Not yet |
## Running headscale ## Running headscale

View file

@ -14,7 +14,7 @@ const (
apiPrefixLength = 7 apiPrefixLength = 7
apiKeyLength = 32 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 // 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) { func (h *Headscale) ValidateAPIKey(keyStr string) (bool, error) {
prefix, hash, found := strings.Cut(keyStr, ".") prefix, hash, found := strings.Cut(keyStr, ".")
if !found { if !found {
return false, errAPIKeyFailedToParse return false, ErrAPIKeyFailedToParse
} }
key, err := h.GetAPIKey(prefix) key, err := h.GetAPIKey(prefix)

6
db.go
View file

@ -248,7 +248,7 @@ func (hi *HostInfo) Scan(destination interface{}) error {
return json.Unmarshal([]byte(value), hi) return json.Unmarshal([]byte(value), hi)
default: 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) return json.Unmarshal([]byte(value), i)
default: 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) return json.Unmarshal([]byte(value), i)
default: default:
return fmt.Errorf("%w: unexpected data type %T", errMachineAddressesInvalid, destination) return fmt.Errorf("%w: unexpected data type %T", ErrMachineAddressesInvalid, destination)
} }
} }

View file

@ -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. or YAML. Check the [test ACLs](../tests/acls) for further information.
When registering the servers we will need to add the flag 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 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 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 and only valid tags are applied. A tag is valid if the namespace that is

19
docs/android-client.md Normal file
View file

@ -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.

View file

@ -18,14 +18,14 @@ import (
) )
const ( const (
errMachineNotFound = Error("machine not found") ErrMachineNotFound = Error("machine not found")
errMachineRouteIsNotAvailable = Error("route is not available on machine") ErrMachineRouteIsNotAvailable = Error("route is not available on machine")
errMachineAddressesInvalid = Error("failed to parse machine addresses") ErrMachineAddressesInvalid = Error("failed to parse machine addresses")
errMachineNotFoundRegistrationCache = Error( ErrMachineNotFoundRegistrationCache = Error(
"machine not found in registration cache", "machine not found in registration cache",
) )
errCouldNotConvertMachineInterface = Error("failed to convert machine interface") ErrCouldNotConvertMachineInterface = Error("failed to convert machine interface")
errHostnameTooLong = Error("Hostname too long") ErrHostnameTooLong = Error("Hostname too long")
MachineGivenNameHashLength = 8 MachineGivenNameHashLength = 8
MachineGivenNameTrimSize = 2 MachineGivenNameTrimSize = 2
) )
@ -112,7 +112,7 @@ func (ma *MachineAddresses) Scan(destination interface{}) error {
return nil return nil
default: 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. // GetMachineByID finds a Machine by ID and returns the Machine struct.
@ -635,7 +635,7 @@ func (machine Machine) toNode(
return nil, fmt.Errorf( return nil, fmt.Errorf(
"hostname %q is too long it cannot except 255 ASCII chars: %w", "hostname %q is too long it cannot except 255 ASCII chars: %w",
hostname, hostname,
errHostnameTooLong, ErrHostnameTooLong,
) )
} }
} else { } else {
@ -785,11 +785,11 @@ func (h *Headscale) RegisterMachineFromAuthCallback(
return machine, err return machine, err
} else { } 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. // 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( return fmt.Errorf(
"route (%s) is not available on node %s: %w", "route (%s) is not available on node %s: %w",
machine.Hostname, machine.Hostname,
newRoute, errMachineRouteIsNotAvailable, newRoute, ErrMachineRouteIsNotAvailable,
) )
} }
} }

View file

@ -16,10 +16,10 @@ import (
) )
const ( const (
errNamespaceExists = Error("Namespace already exists") ErrNamespaceExists = Error("Namespace already exists")
errNamespaceNotFound = Error("Namespace not found") ErrNamespaceNotFound = Error("Namespace not found")
errNamespaceNotEmptyOfNodes = Error("Namespace not empty: node(s) found") ErrNamespaceNotEmptyOfNodes = Error("Namespace not empty: node(s) found")
errInvalidNamespaceName = Error("Invalid namespace name") ErrInvalidNamespaceName = Error("Invalid namespace name")
) )
const ( const (
@ -47,7 +47,7 @@ func (h *Headscale) CreateNamespace(name string) (*Namespace, error) {
} }
namespace := Namespace{} namespace := Namespace{}
if err := h.db.Where("name = ?", name).First(&namespace).Error; err == nil { if err := h.db.Where("name = ?", name).First(&namespace).Error; err == nil {
return nil, errNamespaceExists return nil, ErrNamespaceExists
} }
namespace.Name = name namespace.Name = name
if err := h.db.Create(&namespace).Error; err != nil { 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 { func (h *Headscale) DestroyNamespace(name string) error {
namespace, err := h.GetNamespace(name) namespace, err := h.GetNamespace(name)
if err != nil { if err != nil {
return errNamespaceNotFound return ErrNamespaceNotFound
} }
machines, err := h.ListMachinesInNamespace(name) machines, err := h.ListMachinesInNamespace(name)
@ -75,7 +75,7 @@ func (h *Headscale) DestroyNamespace(name string) error {
return err return err
} }
if len(machines) > 0 { if len(machines) > 0 {
return errNamespaceNotEmptyOfNodes return ErrNamespaceNotEmptyOfNodes
} }
keys, err := h.ListPreAuthKeys(name) keys, err := h.ListPreAuthKeys(name)
@ -110,9 +110,9 @@ func (h *Headscale) RenameNamespace(oldName, newName string) error {
} }
_, err = h.GetNamespace(newName) _, err = h.GetNamespace(newName)
if err == nil { if err == nil {
return errNamespaceExists return ErrNamespaceExists
} }
if !errors.Is(err, errNamespaceNotFound) { if !errors.Is(err, ErrNamespaceNotFound) {
return err return err
} }
@ -132,7 +132,7 @@ func (h *Headscale) GetNamespace(name string) (*Namespace, error) {
result.Error, result.Error,
gorm.ErrRecordNotFound, gorm.ErrRecordNotFound,
) { ) {
return nil, errNamespaceNotFound return nil, ErrNamespaceNotFound
} }
return &namespace, nil return &namespace, nil
@ -272,7 +272,7 @@ func NormalizeToFQDNRules(name string, stripEmailDomain bool) (string, error) {
return "", fmt.Errorf( return "", fmt.Errorf(
"label %v is more than 63 chars: %w", "label %v is more than 63 chars: %w",
elt, elt,
errInvalidNamespaceName, ErrInvalidNamespaceName,
) )
} }
} }
@ -285,21 +285,21 @@ func CheckForFQDNRules(name string) error {
return fmt.Errorf( return fmt.Errorf(
"DNS segment must not be over 63 chars. %v doesn't comply with this rule: %w", "DNS segment must not be over 63 chars. %v doesn't comply with this rule: %w",
name, name,
errInvalidNamespaceName, ErrInvalidNamespaceName,
) )
} }
if strings.ToLower(name) != name { if strings.ToLower(name) != name {
return fmt.Errorf( return fmt.Errorf(
"DNS segment should be lowercase. %v doesn't comply with this rule: %w", "DNS segment should be lowercase. %v doesn't comply with this rule: %w",
name, name,
errInvalidNamespaceName, ErrInvalidNamespaceName,
) )
} }
if invalidCharsInNamespaceRegex.MatchString(name) { if invalidCharsInNamespaceRegex.MatchString(name) {
return fmt.Errorf( 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", "DNS segment should only be composed of lowercase ASCII letters numbers, hyphen and dots. %v doesn't comply with theses rules: %w",
name, name,
errInvalidNamespaceName, ErrInvalidNamespaceName,
) )
} }

View file

@ -26,7 +26,7 @@ func (s *Suite) TestCreateAndDestroyNamespace(c *check.C) {
func (s *Suite) TestDestroyNamespaceErrors(c *check.C) { func (s *Suite) TestDestroyNamespaceErrors(c *check.C) {
err := app.DestroyNamespace("test") err := app.DestroyNamespace("test")
c.Assert(err, check.Equals, errNamespaceNotFound) c.Assert(err, check.Equals, ErrNamespaceNotFound)
namespace, err := app.CreateNamespace("test") namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@ -60,7 +60,7 @@ func (s *Suite) TestDestroyNamespaceErrors(c *check.C) {
app.db.Save(&machine) app.db.Save(&machine)
err = app.DestroyNamespace("test") err = app.DestroyNamespace("test")
c.Assert(err, check.Equals, errNamespaceNotEmptyOfNodes) c.Assert(err, check.Equals, ErrNamespaceNotEmptyOfNodes)
} }
func (s *Suite) TestRenameNamespace(c *check.C) { func (s *Suite) TestRenameNamespace(c *check.C) {
@ -76,20 +76,20 @@ func (s *Suite) TestRenameNamespace(c *check.C) {
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetNamespace("test") _, err = app.GetNamespace("test")
c.Assert(err, check.Equals, errNamespaceNotFound) c.Assert(err, check.Equals, ErrNamespaceNotFound)
_, err = app.GetNamespace("test-renamed") _, err = app.GetNamespace("test-renamed")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
err = app.RenameNamespace("test-does-not-exit", "test") 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") namespaceTest2, err := app.CreateNamespace("test2")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(namespaceTest2.Name, check.Equals, "test2") c.Assert(namespaceTest2.Name, check.Equals, "test2")
err = app.RenameNamespace("test2", "test-renamed") 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) { 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) c.Assert(machine.Namespace.Name, check.Equals, newNamespace.Name)
err = app.SetMachineNamespace(&machine, "non-existing-namespace") 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) err = app.SetMachineNamespace(&machine, newNamespace.Name)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)

View file

@ -551,7 +551,7 @@ func (h *Headscale) findOrCreateNewNamespaceForOIDCCallback(
namespaceName string, namespaceName string,
) (*Namespace, error) { ) (*Namespace, error) {
namespace, err := h.GetNamespace(namespaceName) namespace, err := h.GetNamespace(namespaceName)
if errors.Is(err, errNamespaceNotFound) { if errors.Is(err, ErrNamespaceNotFound) {
namespace, err = h.CreateNamespace(namespaceName) namespace, err = h.CreateNamespace(namespaceName)
if err != nil { if err != nil {

View file

@ -14,10 +14,10 @@ import (
) )
const ( const (
errPreAuthKeyNotFound = Error("AuthKey not found") ErrPreAuthKeyNotFound = Error("AuthKey not found")
errPreAuthKeyExpired = Error("AuthKey expired") ErrPreAuthKeyExpired = Error("AuthKey expired")
errSingleUseAuthKeyHasBeenUsed = Error("AuthKey has already been used") ErrSingleUseAuthKeyHasBeenUsed = Error("AuthKey has already been used")
errNamespaceMismatch = Error("namespace mismatch") ErrNamespaceMismatch = Error("namespace mismatch")
) )
// PreAuthKey describes a pre-authorization key usable in a particular namespace. // 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 { if pak.Namespace.Name != namespace {
return nil, errNamespaceMismatch return nil, ErrNamespaceMismatch
} }
return pak, nil return pak, nil
@ -135,11 +135,11 @@ func (h *Headscale) checkKeyValidity(k string) (*PreAuthKey, error) {
result.Error, result.Error,
gorm.ErrRecordNotFound, gorm.ErrRecordNotFound,
) { ) {
return nil, errPreAuthKeyNotFound return nil, ErrPreAuthKeyNotFound
} }
if pak.Expiration != nil && pak.Expiration.Before(time.Now()) { 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 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 { if len(machines) != 0 || pak.Used {
return nil, errSingleUseAuthKeyHasBeenUsed return nil, ErrSingleUseAuthKeyHasBeenUsed
} }
return &pak, nil return &pak, nil

View file

@ -44,13 +44,13 @@ func (*Suite) TestExpiredPreAuthKey(c *check.C) {
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
key, err := app.checkKeyValidity(pak.Key) key, err := app.checkKeyValidity(pak.Key)
c.Assert(err, check.Equals, errPreAuthKeyExpired) c.Assert(err, check.Equals, ErrPreAuthKeyExpired)
c.Assert(key, check.IsNil) c.Assert(key, check.IsNil)
} }
func (*Suite) TestPreAuthKeyDoesNotExist(c *check.C) { func (*Suite) TestPreAuthKeyDoesNotExist(c *check.C) {
key, err := app.checkKeyValidity("potatoKey") key, err := app.checkKeyValidity("potatoKey")
c.Assert(err, check.Equals, errPreAuthKeyNotFound) c.Assert(err, check.Equals, ErrPreAuthKeyNotFound)
c.Assert(key, check.IsNil) c.Assert(key, check.IsNil)
} }
@ -86,7 +86,7 @@ func (*Suite) TestAlreadyUsedKey(c *check.C) {
app.db.Save(&machine) app.db.Save(&machine)
key, err := app.checkKeyValidity(pak.Key) key, err := app.checkKeyValidity(pak.Key)
c.Assert(err, check.Equals, errSingleUseAuthKeyHasBeenUsed) c.Assert(err, check.Equals, ErrSingleUseAuthKeyHasBeenUsed)
c.Assert(key, check.IsNil) c.Assert(key, check.IsNil)
} }
@ -174,7 +174,7 @@ func (*Suite) TestExpirePreauthKey(c *check.C) {
c.Assert(pak.Expiration, check.NotNil) c.Assert(pak.Expiration, check.NotNil)
key, err := app.checkKeyValidity(pak.Key) key, err := app.checkKeyValidity(pak.Key)
c.Assert(err, check.Equals, errPreAuthKeyExpired) c.Assert(err, check.Equals, ErrPreAuthKeyExpired)
c.Assert(key, check.IsNil) c.Assert(key, check.IsNil)
} }
@ -188,5 +188,5 @@ func (*Suite) TestNotReusableMarkedAsUsed(c *check.C) {
app.db.Save(&pak) app.db.Save(&pak)
_, err = app.checkKeyValidity(pak.Key) _, err = app.checkKeyValidity(pak.Key)
c.Assert(err, check.Equals, errSingleUseAuthKeyHasBeenUsed) c.Assert(err, check.Equals, ErrSingleUseAuthKeyHasBeenUsed)
} }

View file

@ -7,7 +7,7 @@ import (
) )
const ( const (
errRouteIsNotAvailable = Error("route is not available") ErrRouteIsNotAvailable = Error("route is not available")
) )
// Deprecated: use machine function instead // Deprecated: use machine function instead
@ -106,7 +106,7 @@ func (h *Headscale) EnableNodeRoute(
} }
if !available { if !available {
return errRouteIsNotAvailable return ErrRouteIsNotAvailable
} }
machine.EnabledRoutes = enabledRoutes machine.EnabledRoutes = enabledRoutes

View file

@ -27,8 +27,8 @@ import (
) )
const ( const (
errCannotDecryptReponse = Error("cannot decrypt response") ErrCannotDecryptResponse = Error("cannot decrypt response")
errCouldNotAllocateIP = Error("could not find any suitable IP") ErrCouldNotAllocateIP = Error("could not find any suitable IP")
// These constants are copied from the upstream tailscale.com/types/key // These constants are copied from the upstream tailscale.com/types/key
// library, because they are not exported. // library, because they are not exported.
@ -120,7 +120,7 @@ func decode(
decrypted, ok := privKey.OpenFrom(*pubKey, msg) decrypted, ok := privKey.OpenFrom(*pubKey, msg)
if !ok { if !ok {
return errCannotDecryptReponse return ErrCannotDecryptResponse
} }
if err := json.Unmarshal(decrypted, output); err != nil { if err := json.Unmarshal(decrypted, output); err != nil {
@ -181,7 +181,7 @@ func (h *Headscale) getAvailableIP(ipPrefix netaddr.IPPrefix) (*netaddr.IP, erro
for { for {
if !ipPrefix.Contains(ip) { if !ipPrefix.Contains(ip) {
return nil, errCouldNotAllocateIP return nil, ErrCouldNotAllocateIP
} }
switch { switch {