Skip to content

Commit

Permalink
Merge branch 'main' into docs-acl-modifications
Browse files Browse the repository at this point in the history
  • Loading branch information
kradalby committed Feb 18, 2022
2 parents 55d746d + 7d5e6d3 commit 397b6fc
Show file tree
Hide file tree
Showing 55 changed files with 2,863 additions and 629 deletions.
9 changes: 9 additions & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
* @juanfont @kradalby

*.md @ohdearaugustin
*.yml @ohdearaugustin
*.yaml @ohdearaugustin
Dockerfile* @ohdearaugustin
.goreleaser.yaml @ohdearaugustin
/docs/ @ohdearaugustin
/.github/workflows/ @ohdearaugustin
2 changes: 2 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ko_fi: kradalby
github: [kradalby]
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
if: steps.changed-files.outputs.any_changed == 'true'
uses: actions/setup-go@v2
with:
go-version: "1.17"
go-version: "1.17.7"

- name: Install dependencies
if: steps.changed-files.outputs.any_changed == 'true'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.17
go-version: 1.17.7

- name: Install dependencies
run: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
if: steps.changed-files.outputs.any_changed == 'true'
uses: actions/setup-go@v2
with:
go-version: "1.17"
go-version: "1.17.7"

- name: Run Integration tests
if: steps.changed-files.outputs.any_changed == 'true'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
if: steps.changed-files.outputs.any_changed == 'true'
uses: actions/setup-go@v2
with:
go-version: "1.17"
go-version: "1.17.7"

- name: Install dependencies
if: steps.changed-files.outputs.any_changed == 'true'
Expand Down
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@

**TBD (TBD):**

**0.13.0 (2022-xx-xx):**
**0.13.0 (2022-02-18):**

**Features**:

- Add IPv6 support to the prefix assigned to namespaces
- Add API Key support
- Enable remote control of `headscale` via CLI [docs](docs/remote-cli.md)
- Enable HTTP API (beta, subject to change)

**Changes**:

- `ip_prefix` is now superseded by `ip_prefixes` in the configuration [#208](https://github.com/juanfont/headscale/pull/208)
- Upgrade `tailscale` (1.20.4) and other dependencies to latest [#314](https://github.com/juanfont/headscale/pull/314)
- fix swapped machine<->namespace labels in `/metrics` [#312](https://github.com/juanfont/headscale/pull/312)
- remove key-value based update mechanism for namespace changes [#316](https://github.com/juanfont/headscale/pull/316)

**0.12.4 (2022-01-29):**

Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Builder image
FROM docker.io/golang:1.17.1-bullseye AS build
FROM docker.io/golang:1.17.7-bullseye AS build
ENV GOPATH /go
WORKDIR /go/src/headscale

Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.alpine
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Builder image
FROM docker.io/golang:1.17.1-alpine AS build
FROM docker.io/golang:1.17.7-alpine AS build
ENV GOPATH /go
WORKDIR /go/src/headscale

Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.debug
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Builder image
FROM docker.io/golang:1.17.1-bullseye AS build
FROM docker.io/golang:1.17.7-bullseye AS build
ENV GOPATH /go
WORKDIR /go/src/headscale

Expand Down
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,20 @@ Join our [Discord](https://discord.gg/XcQxk2VHjx) server for a chat.

## Overview

Tailscale is [a modern VPN](https://tailscale.com/) built on top of [Wireguard](https://www.wireguard.com/). It [works like an overlay network](https://tailscale.com/blog/how-tailscale-works/) between the computers of your networks - using all kinds of [NAT traversal sorcery](https://tailscale.com/blog/how-nat-traversal-works/).
Tailscale is [a modern VPN](https://tailscale.com/) built on top of [Wireguard](https://www.wireguard.com/). It [works like an overlay network](https://tailscale.com/blog/how-tailscale-works/) between the computers of your networks - using [NAT traversal](https://tailscale.com/blog/how-nat-traversal-works/).

Everything in Tailscale is Open Source, except the GUI clients for proprietary OS (Windows and macOS/iOS), and the 'coordination/control server'.

The control server works as an exchange point of Wireguard public keys for the nodes in the Tailscale network. It also assigns the IP addresses of the clients, creates the boundaries between each user, enables sharing machines between users, and exposes the advertised routes of your nodes.

headscale implements this coordination server.

## Support

If you like `headscale` and find it useful, there is sponsorship and donation buttons available in the repo.

If you would like to sponsor features, bugs or prioritisation, reach out to one of the maintainers.

## Status

- [x] Base functionality (nodes can communicate with each other)
Expand All @@ -42,11 +48,11 @@ headscale implements this coordination server.
| Linux | Yes |
| OpenBSD | Yes |
| macOS | Yes (see `/apple` on your headscale for more information) |
| Windows | Yes |
| 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 |

## Roadmap 🤷
## Roadmap

Suggestions/PRs welcomed!

Expand Down
164 changes: 164 additions & 0 deletions api_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package headscale

import (
"fmt"
"strings"
"time"

v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"golang.org/x/crypto/bcrypt"
"google.golang.org/protobuf/types/known/timestamppb"
)

const (
apiPrefixLength = 7
apiKeyLength = 32
apiKeyParts = 2

errAPIKeyFailedToParse = Error("Failed to parse ApiKey")
)

// APIKey describes the datamodel for API keys used to remotely authenticate with
// headscale.
type APIKey struct {
ID uint64 `gorm:"primary_key"`
Prefix string `gorm:"uniqueIndex"`
Hash []byte

CreatedAt *time.Time
Expiration *time.Time
LastSeen *time.Time
}

// CreateAPIKey creates a new ApiKey in a namespace, and returns it.
func (h *Headscale) CreateAPIKey(
expiration *time.Time,
) (string, *APIKey, error) {
prefix, err := GenerateRandomStringURLSafe(apiPrefixLength)
if err != nil {
return "", nil, err
}

toBeHashed, err := GenerateRandomStringURLSafe(apiKeyLength)
if err != nil {
return "", nil, err
}

// Key to return to user, this will only be visible _once_
keyStr := prefix + "." + toBeHashed

hash, err := bcrypt.GenerateFromPassword([]byte(toBeHashed), bcrypt.DefaultCost)
if err != nil {
return "", nil, err
}

key := APIKey{
Prefix: prefix,
Hash: hash,
Expiration: expiration,
}
h.db.Save(&key)

return keyStr, &key, nil
}

// ListAPIKeys returns the list of ApiKeys for a namespace.
func (h *Headscale) ListAPIKeys() ([]APIKey, error) {
keys := []APIKey{}
if err := h.db.Find(&keys).Error; err != nil {
return nil, err
}

return keys, nil
}

// GetAPIKey returns a ApiKey for a given key.
func (h *Headscale) GetAPIKey(prefix string) (*APIKey, error) {
key := APIKey{}
if result := h.db.First(&key, "prefix = ?", prefix); result.Error != nil {
return nil, result.Error
}

return &key, nil
}

// GetAPIKeyByID returns a ApiKey for a given id.
func (h *Headscale) GetAPIKeyByID(id uint64) (*APIKey, error) {
key := APIKey{}
if result := h.db.Find(&APIKey{ID: id}).First(&key); result.Error != nil {
return nil, result.Error
}

return &key, nil
}

// DestroyAPIKey destroys a ApiKey. Returns error if the ApiKey
// does not exist.
func (h *Headscale) DestroyAPIKey(key APIKey) error {
if result := h.db.Unscoped().Delete(key); result.Error != nil {
return result.Error
}

return nil
}

// ExpireAPIKey marks a ApiKey as expired.
func (h *Headscale) ExpireAPIKey(key *APIKey) error {
if err := h.db.Model(&key).Update("Expiration", time.Now()).Error; err != nil {
return err
}

return nil
}

func (h *Headscale) ValidateAPIKey(keyStr string) (bool, error) {
prefix, hash, err := splitAPIKey(keyStr)
if err != nil {
return false, fmt.Errorf("failed to validate api key: %w", err)
}

key, err := h.GetAPIKey(prefix)
if err != nil {
return false, fmt.Errorf("failed to validate api key: %w", err)
}

if key.Expiration.Before(time.Now()) {
return false, nil
}

if err := bcrypt.CompareHashAndPassword(key.Hash, []byte(hash)); err != nil {
return false, err
}

return true, nil
}

func splitAPIKey(key string) (string, string, error) {
parts := strings.Split(key, ".")
if len(parts) != apiKeyParts {
return "", "", errAPIKeyFailedToParse
}

return parts[0], parts[1], nil
}

func (key *APIKey) toProto() *v1.ApiKey {
protoKey := v1.ApiKey{
Id: key.ID,
Prefix: key.Prefix,
}

if key.Expiration != nil {
protoKey.Expiration = timestamppb.New(*key.Expiration)
}

if key.CreatedAt != nil {
protoKey.CreatedAt = timestamppb.New(*key.CreatedAt)
}

if key.LastSeen != nil {
protoKey.LastSeen = timestamppb.New(*key.LastSeen)
}

return &protoKey
}
89 changes: 89 additions & 0 deletions api_key_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package headscale

import (
"time"

"gopkg.in/check.v1"
)

func (*Suite) TestCreateAPIKey(c *check.C) {
apiKeyStr, apiKey, err := app.CreateAPIKey(nil)
c.Assert(err, check.IsNil)
c.Assert(apiKey, check.NotNil)

// Did we get a valid key?
c.Assert(apiKey.Prefix, check.NotNil)
c.Assert(apiKey.Hash, check.NotNil)
c.Assert(apiKeyStr, check.Not(check.Equals), "")

_, err = app.ListAPIKeys()
c.Assert(err, check.IsNil)

keys, err := app.ListAPIKeys()
c.Assert(err, check.IsNil)
c.Assert(len(keys), check.Equals, 1)
}

func (*Suite) TestAPIKeyDoesNotExist(c *check.C) {
key, err := app.GetAPIKey("does-not-exist")
c.Assert(err, check.NotNil)
c.Assert(key, check.IsNil)
}

func (*Suite) TestValidateAPIKeyOk(c *check.C) {
nowPlus2 := time.Now().Add(2 * time.Hour)
apiKeyStr, apiKey, err := app.CreateAPIKey(&nowPlus2)
c.Assert(err, check.IsNil)
c.Assert(apiKey, check.NotNil)

valid, err := app.ValidateAPIKey(apiKeyStr)
c.Assert(err, check.IsNil)
c.Assert(valid, check.Equals, true)
}

func (*Suite) TestValidateAPIKeyNotOk(c *check.C) {
nowMinus2 := time.Now().Add(time.Duration(-2) * time.Hour)
apiKeyStr, apiKey, err := app.CreateAPIKey(&nowMinus2)
c.Assert(err, check.IsNil)
c.Assert(apiKey, check.NotNil)

valid, err := app.ValidateAPIKey(apiKeyStr)
c.Assert(err, check.IsNil)
c.Assert(valid, check.Equals, false)

now := time.Now()
apiKeyStrNow, apiKey, err := app.CreateAPIKey(&now)
c.Assert(err, check.IsNil)
c.Assert(apiKey, check.NotNil)

validNow, err := app.ValidateAPIKey(apiKeyStrNow)
c.Assert(err, check.IsNil)
c.Assert(validNow, check.Equals, false)

validSilly, err := app.ValidateAPIKey("nota.validkey")
c.Assert(err, check.NotNil)
c.Assert(validSilly, check.Equals, false)

validWithErr, err := app.ValidateAPIKey("produceerrorkey")
c.Assert(err, check.NotNil)
c.Assert(validWithErr, check.Equals, false)
}

func (*Suite) TestExpireAPIKey(c *check.C) {
nowPlus2 := time.Now().Add(2 * time.Hour)
apiKeyStr, apiKey, err := app.CreateAPIKey(&nowPlus2)
c.Assert(err, check.IsNil)
c.Assert(apiKey, check.NotNil)

valid, err := app.ValidateAPIKey(apiKeyStr)
c.Assert(err, check.IsNil)
c.Assert(valid, check.Equals, true)

err = app.ExpireAPIKey(apiKey)
c.Assert(err, check.IsNil)
c.Assert(apiKey.Expiration, check.NotNil)

notValid, err := app.ValidateAPIKey(apiKeyStr)
c.Assert(err, check.IsNil)
c.Assert(notValid, check.Equals, false)
}

0 comments on commit 397b6fc

Please sign in to comment.