mirror of
https://github.com/juanfont/headscale.git
synced 2024-12-24 00:37:52 +00:00
Merge branch 'main' into oidc-refactoring
This commit is contained in:
commit
8a9fe1da4b
18
README.md
18
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
|
||||
|
||||
|
@ -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)
|
||||
|
6
db.go
6
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
19
docs/android-client.md
Normal file
19
docs/android-client.md
Normal 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.
|
24
machine.go
24
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
2
oidc.go
2
oidc.go
@ -551,7 +551,7 @@ func (h *Headscale) findOrCreateNewNamespaceForOIDCCallback(
|
||||
namespaceName string,
|
||||
) (*Namespace, error) {
|
||||
namespace, err := h.GetNamespace(namespaceName)
|
||||
if errors.Is(err, errNamespaceNotFound) {
|
||||
if errors.Is(err, ErrNamespaceNotFound) {
|
||||
namespace, err = h.CreateNamespace(namespaceName)
|
||||
|
||||
if err != nil {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
8
utils.go
8
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 {
|
||||
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user