Compare commits

..

29 Commits

Author SHA1 Message Date
Juan Font
70e08462b3 Merge branch 'main' into web-auth-flow-tests 2022-11-13 16:51:53 +01:00
Juan Font Alonso
a231ece825 Added ts2019 buildtag to CI config
Otherwise we are getting utils.go:119:6: `decode` is unused (deadcode)

Signed-off-by: Juan Font Alonso <juanfontalonso@gmail.com>
2022-11-13 13:34:08 +01:00
Juan Font Alonso
ae43d82a33 Fixed gofumpt linting 2022-11-13 13:18:00 +01:00
Juan Font Alonso
ad3c36fd07 Wait for the tailscale clients to be ready in the web auth CreateHeadscaleEnv
Signed-off-by: Juan Font Alonso <juanfontalonso@gmail.com>
2022-11-13 13:13:29 +01:00
Juan Font Alonso
f176503448 Added WaitForReady() to Tailscale interface
When using running `tailscale up` in the AuthKey flow process, the tailscale client immediately enters PollMap after registration - avoiding a race condition.

When using the web auth (up -> go to the Control website -> CLI `register`) the client is polling checking if it has been authorized. If we immediately ask for the client IP, as done in CreateHeadscaleEnv() we might have the client in NotReady status.

This method provides a way to wait for the client to be ready.

Signed-off-by: Juan Font Alonso <juanfontalonso@gmail.com>
2022-11-13 13:13:29 +01:00
Juan Font Alonso
f7ad88aa08 Print the name of the registered machine 2022-11-13 13:13:29 +01:00
Juan Font Alonso
f63d22655c Fixed conflict 2022-11-13 13:13:29 +01:00
Juan Font Alonso
89c468fc43 Added makefile and .github 2022-11-13 13:13:29 +01:00
Kristoffer Dalby
b0fda6b216 Merge branch 'main' into web-auth-flow-tests 2022-11-10 16:15:17 +00:00
Juan Font Alonso
154fb59bdb Run auth test in main actions file 2022-11-04 16:14:57 +01:00
Juan Font
d3e9703fb5 Update integration/auth_web_flow_test.go
Co-authored-by: Kristoffer Dalby <kristoffer@dalby.cc>
2022-11-04 16:11:10 +01:00
Juan Font
7ce3f8c7d1 Update Makefile
Co-authored-by: Kristoffer Dalby <kristoffer@dalby.cc>
2022-11-04 16:10:59 +01:00
Kristoffer Dalby
58c8633cc1 Merge branch 'main' into web-auth-flow-tests 2022-11-04 13:05:39 +01:00
Juan Font Alonso
b3f5af30a4 Linting fixes + disable interfacebloat linter 2022-11-04 11:41:54 +01:00
Kristoffer Dalby
9f64ac8a33 Fix bitrotted versions in gh ci
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-04 11:41:54 +01:00
Kristoffer Dalby
aa1cc05cfb Run on correct change
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-04 11:41:54 +01:00
Kristoffer Dalby
670ef9a93e Add experimental kradalby gh runner
Remove old v2 runner in favour of self-hosted

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-04 11:41:54 +01:00
Juan Font Alonso
987abcfdce More linting fixes 2022-11-04 00:27:00 +01:00
Juan Font Alonso
c70f5696dc Linting fixes 2022-11-04 00:23:20 +01:00
Juan Font Alonso
825e88311e Renamed integration tests for auth 2022-11-04 00:16:19 +01:00
Juan Font Alonso
bbc8cb11da Added makefile and .github 2022-11-04 00:11:34 +01:00
Juan Font Alonso
3a6ef6bece Work in progress for web auth flow 2022-11-04 00:10:36 +01:00
Juan Font Alonso
b2dc480f22 Return the real port of the container 2022-11-04 00:05:01 +01:00
Juan Font Alonso
5d7eae46f8 Always attempt to parse login url 2022-11-04 00:04:04 +01:00
Juan Font Alonso
45cb0f3fa3 Typo 2022-11-03 23:59:06 +01:00
Juan Font Alonso
658478cba3 Add web flow auth basic integration test 2022-11-03 17:40:09 +01:00
Juan Font Alonso
ec90e9d716 Update Tailscale interface with new Execute signature 2022-11-03 17:00:23 +01:00
Juan Font Alonso
181f1eeb4f Added method to run tailscale up without authkey 2022-11-03 16:56:19 +01:00
Juan Font Alonso
e270cf6d20 Return stderr in tsic.Execute 2022-11-03 16:50:20 +01:00
17 changed files with 341 additions and 576 deletions

View File

@@ -65,6 +65,7 @@ jobs:
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest
type=sha
- name: Login to DockerHub
uses: docker/login-action@v1
@@ -124,12 +125,13 @@ jobs:
${{ secrets.DOCKERHUB_USERNAME }}/headscale
ghcr.io/${{ github.repository_owner }}/headscale
flavor: |
suffix=-debug,onlatest=true
latest=false
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
type=semver,pattern={{version}}-debug
type=semver,pattern={{major}}.{{minor}}-debug
type=semver,pattern={{major}}-debug
type=raw,value=latest-debug
type=sha,suffix=-debug
- name: Login to DockerHub
uses: docker/login-action@v1
with:
@@ -159,3 +161,69 @@ jobs:
run: |
rm -rf /tmp/.buildx-cache-debug
mv /tmp/.buildx-cache-debug-new /tmp/.buildx-cache-debug
docker-alpine-release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Set up QEMU for multiple platforms
uses: docker/setup-qemu-action@master
with:
platforms: arm64,amd64
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache-alpine
key: ${{ runner.os }}-buildx-alpine-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-alpine-
- name: Docker meta
id: meta-alpine
uses: docker/metadata-action@v3
with:
# list of Docker images to use as base name for tags
images: |
${{ secrets.DOCKERHUB_USERNAME }}/headscale
ghcr.io/${{ github.repository_owner }}/headscale
flavor: |
latest=false
tags: |
type=semver,pattern={{version}}-alpine
type=semver,pattern={{major}}.{{minor}}-alpine
type=semver,pattern={{major}}-alpine
type=raw,value=latest-alpine
type=sha,suffix=-alpine
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
push: true
context: .
file: Dockerfile.alpine
tags: ${{ steps.meta-alpine.outputs.tags }}
labels: ${{ steps.meta-alpine.outputs.labels }}
platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache-alpine
cache-to: type=local,dest=/tmp/.buildx-cache-alpine-new
build-args: |
VERSION=${{ steps.meta-alpine.outputs.version }}
- name: Prepare cache for next build
run: |
rm -rf /tmp/.buildx-cache-alpine
mv /tmp/.buildx-cache-alpine-new /tmp/.buildx-cache-alpine

View File

@@ -0,0 +1,35 @@
name: Integration Test v2
on: [pull_request]
jobs:
integration-test-v2:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 2
- name: Set Swap Space
uses: pierotofy/set-swap-space@master
with:
swap-size-gb: 10
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v14.1
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v16
if: steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: nix develop --command -- make test_integration_v2_auth_web_flow

View File

@@ -5,7 +5,6 @@
### BREAKING
- Log level option `log_level` was moved to a distinct `log` config section and renamed to `level` [#768](https://github.com/juanfont/headscale/pull/768)
- Removed Alpine Linux container image [#962](https://github.com/juanfont/headscale/pull/962)
### Changes
@@ -26,7 +25,6 @@
- Add `dns_config.override_local_dns` option [#905](https://github.com/juanfont/headscale/pull/905)
- Fix some DNS config issues [#660](https://github.com/juanfont/headscale/issues/660)
- Make it possible to disable TS2019 with build flag [#928](https://github.com/juanfont/headscale/pull/928)
- Fix OIDC registration issues [#960](https://github.com/juanfont/headscale/pull/960) and [#971](https://github.com/juanfont/headscale/pull/971)
## 0.16.4 (2022-08-21)

24
Dockerfile.alpine Normal file
View File

@@ -0,0 +1,24 @@
# Builder image
FROM docker.io/golang:1.19.0-alpine AS build
ARG VERSION=dev
ENV GOPATH /go
WORKDIR /go/src/headscale
COPY go.mod go.sum /go/src/headscale/
RUN apk add gcc musl-dev
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go install -ldflags="-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$VERSION" -a ./cmd/headscale
RUN strip /go/bin/headscale
RUN test -e /go/bin/headscale
# Production image
FROM docker.io/alpine:latest
COPY --from=build /go/bin/headscale /bin/headscale
ENV TZ UTC
EXPOSE 8080/tcp
CMD ["headscale"]

View File

@@ -68,6 +68,17 @@ test_integration_v2_general:
golang:1 \
go test $(TAGS) -failfast ./... -timeout 60m -parallel 6
test_integration_v2_auth_web_flow:
docker run \
-t --rm \
-v ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
-v $$PWD:$$PWD -w $$PWD/integration \
-v /var/run/docker.sock:/var/run/docker.sock \
golang:1 \
go test ./... -timeout 60m -parallel 6 -run TestAuthWebFlow
coverprofile_func:
go tool cover -func=coverage.out

View File

@@ -134,9 +134,7 @@ var registerNodeCmd = &cobra.Command{
return
}
SuccessOutput(
response.Machine,
fmt.Sprintf("Machine %s registered", response.Machine.GivenName), output)
SuccessOutput(response.Machine, fmt.Sprintf("Machine %s registered", response.Machine.GivenName), output)
},
}

View File

@@ -10,8 +10,6 @@ import (
"net/url"
"strings"
"testing"
"github.com/juanfont/headscale/integration/hsic"
)
var errParseAuthPage = errors.New("failed to parse auth page")
@@ -37,7 +35,7 @@ func TestAuthWebFlowAuthenticationPingAll(t *testing.T) {
"namespace2": len(TailscaleVersions),
}
err = scenario.CreateHeadscaleEnv(spec, hsic.WithTestName("webauthping"))
err = scenario.CreateHeadscaleEnv(spec)
if err != nil {
t.Errorf("failed to create headscale environment: %s", err)
}
@@ -78,16 +76,13 @@ func TestAuthWebFlowAuthenticationPingAll(t *testing.T) {
}
}
func (s *AuthWebFlowScenario) CreateHeadscaleEnv(
namespaces map[string]int,
opts ...hsic.Option,
) error {
headscale, err := s.Headscale(opts...)
func (s *AuthWebFlowScenario) CreateHeadscaleEnv(namespaces map[string]int) error {
err := s.StartHeadscale()
if err != nil {
return err
}
err = headscale.WaitForReady()
err = s.Headscale().WaitForReady()
if err != nil {
return err
}
@@ -104,7 +99,7 @@ func (s *AuthWebFlowScenario) CreateHeadscaleEnv(
return err
}
err = s.runTailscaleUp(namespaceName, headscale.GetEndpoint())
err = s.runTailscaleUp(namespaceName, s.Headscale().GetEndpoint())
if err != nil {
return err
}
@@ -150,13 +145,8 @@ func (s *AuthWebFlowScenario) runTailscaleUp(
}
func (s *AuthWebFlowScenario) runHeadscaleRegister(namespaceStr string, loginURL *url.URL) error {
headscale, err := s.Headscale()
if err != nil {
return err
}
log.Printf("loginURL: %s", loginURL)
loginURL.Host = fmt.Sprintf("%s:8080", headscale.GetIP())
loginURL.Host = fmt.Sprintf("%s:8080", s.Headscale().GetIP())
loginURL.Scheme = "http"
httpClient := &http.Client{}
@@ -187,10 +177,8 @@ func (s *AuthWebFlowScenario) runHeadscaleRegister(namespaceStr string, loginURL
key := keySep[1]
log.Printf("registering node %s", key)
if headscale, err := s.Headscale(); err == nil {
_, err = headscale.Execute(
[]string{"headscale", "-n", namespaceStr, "nodes", "register", "--key", key},
)
if headscale, ok := s.controlServers["headscale"]; ok {
_, err = headscale.Execute([]string{"headscale", "-n", namespaceStr, "nodes", "register", "--key", key})
if err != nil {
log.Printf("failed to register node: %s", err)

View File

@@ -7,7 +7,6 @@ import (
"time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/juanfont/headscale/integration/hsic"
"github.com/stretchr/testify/assert"
)
@@ -37,14 +36,11 @@ func TestNamespaceCommand(t *testing.T) {
"namespace2": 0,
}
err = scenario.CreateHeadscaleEnv(spec, hsic.WithTestName("clins"))
assert.NoError(t, err)
headscale, err := scenario.Headscale()
err = scenario.CreateHeadscaleEnv(spec)
assert.NoError(t, err)
var listNamespaces []v1.Namespace
err = executeAndUnmarshal(headscale,
err = executeAndUnmarshal(scenario.Headscale(),
[]string{
"headscale",
"namespaces",
@@ -65,7 +61,7 @@ func TestNamespaceCommand(t *testing.T) {
result,
)
_, err = headscale.Execute(
_, err = scenario.Headscale().Execute(
[]string{
"headscale",
"namespaces",
@@ -79,7 +75,7 @@ func TestNamespaceCommand(t *testing.T) {
assert.NoError(t, err)
var listAfterRenameNamespaces []v1.Namespace
err = executeAndUnmarshal(headscale,
err = executeAndUnmarshal(scenario.Headscale(),
[]string{
"headscale",
"namespaces",
@@ -118,10 +114,7 @@ func TestPreAuthKeyCommand(t *testing.T) {
namespace: 0,
}
err = scenario.CreateHeadscaleEnv(spec, hsic.WithTestName("clipak"))
assert.NoError(t, err)
headscale, err := scenario.Headscale()
err = scenario.CreateHeadscaleEnv(spec)
assert.NoError(t, err)
keys := make([]*v1.PreAuthKey, count)
@@ -130,7 +123,7 @@ func TestPreAuthKeyCommand(t *testing.T) {
for index := 0; index < count; index++ {
var preAuthKey v1.PreAuthKey
err := executeAndUnmarshal(
headscale,
scenario.Headscale(),
[]string{
"headscale",
"preauthkeys",
@@ -156,7 +149,7 @@ func TestPreAuthKeyCommand(t *testing.T) {
var listedPreAuthKeys []v1.PreAuthKey
err = executeAndUnmarshal(
headscale,
scenario.Headscale(),
[]string{
"headscale",
"preauthkeys",
@@ -209,7 +202,7 @@ func TestPreAuthKeyCommand(t *testing.T) {
}
// Test key expiry
_, err = headscale.Execute(
_, err = scenario.Headscale().Execute(
[]string{
"headscale",
"preauthkeys",
@@ -223,7 +216,7 @@ func TestPreAuthKeyCommand(t *testing.T) {
var listedPreAuthKeysAfterExpire []v1.PreAuthKey
err = executeAndUnmarshal(
headscale,
scenario.Headscale(),
[]string{
"headscale",
"preauthkeys",
@@ -258,15 +251,12 @@ func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) {
namespace: 0,
}
err = scenario.CreateHeadscaleEnv(spec, hsic.WithTestName("clipaknaexp"))
assert.NoError(t, err)
headscale, err := scenario.Headscale()
err = scenario.CreateHeadscaleEnv(spec)
assert.NoError(t, err)
var preAuthKey v1.PreAuthKey
err = executeAndUnmarshal(
headscale,
scenario.Headscale(),
[]string{
"headscale",
"preauthkeys",
@@ -283,7 +273,7 @@ func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) {
var listedPreAuthKeys []v1.PreAuthKey
err = executeAndUnmarshal(
headscale,
scenario.Headscale(),
[]string{
"headscale",
"preauthkeys",
@@ -323,15 +313,12 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
namespace: 0,
}
err = scenario.CreateHeadscaleEnv(spec, hsic.WithTestName("clipakresueeph"))
assert.NoError(t, err)
headscale, err := scenario.Headscale()
err = scenario.CreateHeadscaleEnv(spec)
assert.NoError(t, err)
var preAuthReusableKey v1.PreAuthKey
err = executeAndUnmarshal(
headscale,
scenario.Headscale(),
[]string{
"headscale",
"preauthkeys",
@@ -348,7 +335,7 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
var preAuthEphemeralKey v1.PreAuthKey
err = executeAndUnmarshal(
headscale,
scenario.Headscale(),
[]string{
"headscale",
"preauthkeys",
@@ -368,7 +355,7 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
var listedPreAuthKeys []v1.PreAuthKey
err = executeAndUnmarshal(
headscale,
scenario.Headscale(),
[]string{
"headscale",
"preauthkeys",

View File

@@ -13,7 +13,4 @@ type ControlServer interface {
CreateNamespace(namespace string) error
CreateAuthKey(namespace string) (*v1.PreAuthKey, error)
ListMachinesInNamespace(namespace string) ([]*v1.Machine, error)
GetCert() []byte
GetHostname() string
GetIP() string
}

View File

@@ -6,7 +6,6 @@ import (
"testing"
"time"
"github.com/juanfont/headscale/integration/hsic"
"github.com/rs/zerolog/log"
)
@@ -23,7 +22,7 @@ func TestPingAllByIP(t *testing.T) {
"namespace2": len(TailscaleVersions),
}
err = scenario.CreateHeadscaleEnv(spec, hsic.WithTestName("pingallbyip"))
err = scenario.CreateHeadscaleEnv(spec)
if err != nil {
t.Errorf("failed to create headscale environment: %s", err)
}
@@ -78,7 +77,7 @@ func TestPingAllByHostname(t *testing.T) {
"namespace4": len(TailscaleVersions) - 1,
}
err = scenario.CreateHeadscaleEnv(spec, hsic.WithTestName("pingallbyname"))
err = scenario.CreateHeadscaleEnv(spec)
if err != nil {
t.Errorf("failed to create headscale environment: %s", err)
}
@@ -145,7 +144,7 @@ func TestTaildrop(t *testing.T) {
"taildrop": len(TailscaleVersions) - 1,
}
err = scenario.CreateHeadscaleEnv(spec, hsic.WithTestName("taildrop"))
err = scenario.CreateHeadscaleEnv(spec)
if err != nil {
t.Errorf("failed to create headscale environment: %s", err)
}
@@ -272,7 +271,7 @@ func TestResolveMagicDNS(t *testing.T) {
"magicdns2": len(TailscaleVersions) - 1,
}
err = scenario.CreateHeadscaleEnv(spec, hsic.WithTestName("magicdns"))
err = scenario.CreateHeadscaleEnv(spec)
if err != nil {
t.Errorf("failed to create headscale environment: %s", err)
}

View File

@@ -1,83 +1,52 @@
package hsic
import (
"archive/tar"
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"log"
"math/big"
"net"
"net/http"
"time"
"path/filepath"
"github.com/juanfont/headscale"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/juanfont/headscale/integration/dockertestutil"
"github.com/juanfont/headscale/integration/integrationutil"
"github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker"
)
const (
hsicHashLength = 6
dockerContextPath = "../."
aclPolicyPath = "/etc/headscale/acl.hujson"
tlsCertPath = "/etc/headscale/tls.cert"
tlsKeyPath = "/etc/headscale/tls.key"
headscaleDefaultPort = 8080
hsicHashLength = 6
dockerContextPath = "../."
aclPolicyPath = "/etc/headscale/acl.hujson"
)
var errHeadscaleStatusCodeNotOk = errors.New("headscale status code not ok")
type HeadscaleInContainer struct {
hostname string
port int
pool *dockertest.Pool
container *dockertest.Resource
network *dockertest.Network
// optional config
port int
aclPolicy *headscale.ACLPolicy
env []string
tlsCert []byte
tlsKey []byte
}
type Option = func(c *HeadscaleInContainer)
func WithACLPolicy(acl *headscale.ACLPolicy) Option {
return func(hsic *HeadscaleInContainer) {
// TODO(kradalby): Move somewhere appropriate
hsic.env = append(hsic.env, fmt.Sprintf("HEADSCALE_ACL_POLICY_PATH=%s", aclPolicyPath))
hsic.aclPolicy = acl
}
}
func WithTLS() Option {
return func(hsic *HeadscaleInContainer) {
cert, key, err := createCertificate()
if err != nil {
log.Fatalf("failed to create certificates for headscale test: %s", err)
}
// TODO(kradalby): Move somewhere appropriate
hsic.env = append(hsic.env, fmt.Sprintf("HEADSCALE_TLS_CERT_PATH=%s", tlsCertPath))
hsic.env = append(hsic.env, fmt.Sprintf("HEADSCALE_TLS_KEY_PATH=%s", tlsKeyPath))
hsic.env = append(hsic.env, "HEADSCALE_TLS_CLIENT_AUTH_MODE=disabled")
hsic.tlsCert = cert
hsic.tlsKey = key
}
}
func WithConfigEnv(configEnv map[string]string) Option {
return func(hsic *HeadscaleInContainer) {
env := []string{}
@@ -90,23 +59,9 @@ func WithConfigEnv(configEnv map[string]string) Option {
}
}
func WithPort(port int) Option {
return func(hsic *HeadscaleInContainer) {
hsic.port = port
}
}
func WithTestName(testName string) Option {
return func(hsic *HeadscaleInContainer) {
hash, _ := headscale.GenerateRandomStringDNSSafe(hsicHashLength)
hostname := fmt.Sprintf("hs-%s-%s", testName, hash)
hsic.hostname = hostname
}
}
func New(
pool *dockertest.Pool,
port int,
network *dockertest.Network,
opts ...Option,
) (*HeadscaleInContainer, error) {
@@ -116,10 +71,11 @@ func New(
}
hostname := fmt.Sprintf("hs-%s", hash)
portProto := fmt.Sprintf("%d/tcp", port)
hsic := &HeadscaleInContainer{
hostname: hostname,
port: headscaleDefaultPort,
port: port,
pool: pool,
network: network,
@@ -129,9 +85,9 @@ func New(
opt(hsic)
}
log.Println("NAME: ", hsic.hostname)
portProto := fmt.Sprintf("%d/tcp", hsic.port)
if hsic.aclPolicy != nil {
hsic.env = append(hsic.env, fmt.Sprintf("HEADSCALE_ACL_POLICY_PATH=%s", aclPolicyPath))
}
headscaleBuildOptions := &dockertest.BuildOptions{
Dockerfile: "Dockerfile.debug",
@@ -139,7 +95,7 @@ func New(
}
runOptions := &dockertest.RunOptions{
Name: hsic.hostname,
Name: hostname,
ExposedPorts: []string{portProto},
Networks: []*dockertest.Network{network},
// Cmd: []string{"headscale", "serve"},
@@ -152,7 +108,7 @@ func New(
// dockertest isnt very good at handling containers that has already
// been created, this is an attempt to make sure this container isnt
// present.
err = pool.RemoveContainerByName(hsic.hostname)
err = pool.RemoveContainerByName(hostname)
if err != nil {
return nil, err
}
@@ -167,7 +123,7 @@ func New(
if err != nil {
return nil, fmt.Errorf("could not start headscale container: %w", err)
}
log.Printf("Created %s container\n", hsic.hostname)
log.Printf("Created %s container\n", hostname)
hsic.container = container
@@ -188,25 +144,9 @@ func New(
}
}
if hsic.hasTLS() {
err = hsic.WriteFile(tlsCertPath, hsic.tlsCert)
if err != nil {
return nil, fmt.Errorf("failed to write TLS certificate to container: %w", err)
}
err = hsic.WriteFile(tlsKeyPath, hsic.tlsKey)
if err != nil {
return nil, fmt.Errorf("failed to write TLS key to container: %w", err)
}
}
return hsic, nil
}
func (t *HeadscaleInContainer) hasTLS() bool {
return len(t.tlsCert) != 0 && len(t.tlsKey) != 0
}
func (t *HeadscaleInContainer) Shutdown() error {
return t.pool.Purge(t.container)
}
@@ -214,6 +154,8 @@ func (t *HeadscaleInContainer) Shutdown() error {
func (t *HeadscaleInContainer) Execute(
command []string,
) (string, error) {
log.Println("command", command)
log.Printf("running command for %s\n", t.hostname)
stdout, stderr, err := dockertestutil.ExecuteCommand(
t.container,
command,
@@ -222,13 +164,13 @@ func (t *HeadscaleInContainer) Execute(
if err != nil {
log.Printf("command stderr: %s\n", stderr)
if stdout != "" {
log.Printf("command stdout: %s\n", stdout)
}
return "", err
}
if stdout != "" {
log.Printf("command stdout: %s\n", stdout)
}
return stdout, nil
}
@@ -241,7 +183,11 @@ func (t *HeadscaleInContainer) GetPort() string {
}
func (t *HeadscaleInContainer) GetHealthEndpoint() string {
return fmt.Sprintf("%s/health", t.GetEndpoint())
hostEndpoint := fmt.Sprintf("%s:%d",
t.GetIP(),
t.port)
return fmt.Sprintf("http://%s/health", hostEndpoint)
}
func (t *HeadscaleInContainer) GetEndpoint() string {
@@ -249,39 +195,17 @@ func (t *HeadscaleInContainer) GetEndpoint() string {
t.GetIP(),
t.port)
if t.hasTLS() {
return fmt.Sprintf("https://%s", hostEndpoint)
}
return fmt.Sprintf("http://%s", hostEndpoint)
}
func (t *HeadscaleInContainer) GetCert() []byte {
return t.tlsCert
}
func (t *HeadscaleInContainer) GetHostname() string {
return t.hostname
}
func (t *HeadscaleInContainer) WaitForReady() error {
url := t.GetHealthEndpoint()
log.Printf("waiting for headscale to be ready at %s", url)
client := &http.Client{}
if t.hasTLS() {
insecureTransport := http.DefaultTransport.(*http.Transport).Clone() //nolint
insecureTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint
client = &http.Client{Transport: insecureTransport}
}
return t.pool.Retry(func() error {
resp, err := client.Get(url) //nolint
resp, err := http.Get(url) //nolint
if err != nil {
log.Printf("ready err: %s", err)
return fmt.Errorf("headscale is not ready: %w", err)
}
@@ -368,87 +292,55 @@ func (t *HeadscaleInContainer) ListMachinesInNamespace(
}
func (t *HeadscaleInContainer) WriteFile(path string, data []byte) error {
return integrationutil.WriteFileToContainer(t.pool, t.container, path, data)
}
dirPath, fileName := filepath.Split(path)
//nolint
func createCertificate() ([]byte, []byte, error) {
// From:
// https://shaneutt.com/blog/golang-ca-and-signed-cert-go/
file := bytes.NewReader(data)
ca := &x509.Certificate{
SerialNumber: big.NewInt(2019),
Subject: pkix.Name{
Organization: []string{"Headscale testing INC"},
Country: []string{"NL"},
Locality: []string{"Leiden"},
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(30 * time.Minute),
IsCA: true,
ExtKeyUsage: []x509.ExtKeyUsage{
x509.ExtKeyUsageClientAuth,
x509.ExtKeyUsageServerAuth,
},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
buf := bytes.NewBuffer([]byte{})
tarWriter := tar.NewWriter(buf)
header := &tar.Header{
Name: fileName,
Size: file.Size(),
// Mode: int64(stat.Mode()),
// ModTime: stat.ModTime(),
}
caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
err := tarWriter.WriteHeader(header)
if err != nil {
return nil, nil, err
return fmt.Errorf("failed write file header to tar: %w", err)
}
cert := &x509.Certificate{
SerialNumber: big.NewInt(1658),
Subject: pkix.Name{
Organization: []string{"Headscale testing INC"},
Country: []string{"NL"},
Locality: []string{"Leiden"},
},
IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback},
NotBefore: time.Now(),
NotAfter: time.Now().Add(30 * time.Minute),
SubjectKeyId: []byte{1, 2, 3, 4, 6},
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature,
}
certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
_, err = io.Copy(tarWriter, file)
if err != nil {
return nil, nil, err
return fmt.Errorf("failed to copy file to tar: %w", err)
}
certBytes, err := x509.CreateCertificate(
rand.Reader,
cert,
ca,
&certPrivKey.PublicKey,
caPrivKey,
err = tarWriter.Close()
if err != nil {
return fmt.Errorf("failed to close tar: %w", err)
}
log.Printf("tar: %s", buf.String())
// Ensure the directory is present inside the container
_, err = t.Execute([]string{"mkdir", "-p", dirPath})
if err != nil {
return fmt.Errorf("failed to ensure directory: %w", err)
}
err = t.pool.Client.UploadToContainer(
t.container.Container.ID,
docker.UploadToContainerOptions{
NoOverwriteDirNonDir: false,
Path: dirPath,
InputStream: bytes.NewReader(buf.Bytes()),
},
)
if err != nil {
return nil, nil, err
return err
}
certPEM := new(bytes.Buffer)
err = pem.Encode(certPEM, &pem.Block{
Type: "CERTIFICATE",
Bytes: certBytes,
})
if err != nil {
return nil, nil, err
}
certPrivKeyPEM := new(bytes.Buffer)
err = pem.Encode(certPrivKeyPEM, &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey),
})
if err != nil {
return nil, nil, err
}
return certPEM.Bytes(), certPrivKeyPEM.Bytes(), nil
return nil
}

View File

@@ -1,77 +0,0 @@
package integrationutil
import (
"archive/tar"
"bytes"
"fmt"
"io"
"log"
"path/filepath"
"github.com/juanfont/headscale/integration/dockertestutil"
"github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker"
)
func WriteFileToContainer(
pool *dockertest.Pool,
container *dockertest.Resource,
path string,
data []byte,
) error {
dirPath, fileName := filepath.Split(path)
file := bytes.NewReader(data)
buf := bytes.NewBuffer([]byte{})
tarWriter := tar.NewWriter(buf)
header := &tar.Header{
Name: fileName,
Size: file.Size(),
// Mode: int64(stat.Mode()),
// ModTime: stat.ModTime(),
}
err := tarWriter.WriteHeader(header)
if err != nil {
return fmt.Errorf("failed write file header to tar: %w", err)
}
_, err = io.Copy(tarWriter, file)
if err != nil {
return fmt.Errorf("failed to copy file to tar: %w", err)
}
err = tarWriter.Close()
if err != nil {
return fmt.Errorf("failed to close tar: %w", err)
}
log.Printf("tar: %s", buf.String())
// Ensure the directory is present inside the container
_, _, err = dockertestutil.ExecuteCommand(
container,
[]string{"mkdir", "-p", dirPath},
[]string{},
)
if err != nil {
return fmt.Errorf("failed to ensure directory: %w", err)
}
err = pool.Client.UploadToContainer(
container.Container.ID,
docker.UploadToContainerOptions{
NoOverwriteDirNonDir: false,
Path: dirPath,
InputStream: bytes.NewReader(buf.Bytes()),
},
)
if err != nil {
return err
}
return nil
}

View File

@@ -15,28 +15,22 @@ import (
"github.com/juanfont/headscale/integration/hsic"
"github.com/juanfont/headscale/integration/tsic"
"github.com/ory/dockertest/v3"
"github.com/puzpuzpuz/xsync/v2"
)
const (
scenarioHashLength = 6
maxWait = 60 * time.Second
headscalePort = 8080
)
var (
errNoHeadscaleAvailable = errors.New("no headscale available")
errNoNamespaceAvailable = errors.New("no namespace available")
// Tailscale started adding TS2021 support in CapabilityVersion>=28 (v1.24.0), but
// proper support in Headscale was only added for CapabilityVersion>=39 clients (v1.30.0).
tailscaleVersions2021 = []string{
TailscaleVersions = []string{
"head",
"unstable",
"1.32.1",
"1.30.2",
}
tailscaleVersions2019 = []string{
"1.28.0",
"1.26.2",
"1.24.2",
@@ -44,20 +38,13 @@ var (
"1.20.4",
"1.18.2",
"1.16.2",
// These versions seem to fail when fetching from apt.
// "1.14.6",
// "1.12.4",
// "1.10.2",
// "1.8.7",
}
// tailscaleVersionsUnavailable = []string{
// // These versions seem to fail when fetching from apt.
// "1.14.6",
// "1.12.4",
// "1.10.2",
// "1.8.7",
// }.
TailscaleVersions = append(
tailscaleVersions2021,
tailscaleVersions2019...,
)
)
type Namespace struct {
@@ -72,14 +59,12 @@ type Namespace struct {
type Scenario struct {
// TODO(kradalby): support multiple headcales for later, currently only
// use one.
controlServers *xsync.MapOf[string, ControlServer]
controlServers map[string]ControlServer
namespaces map[string]*Namespace
pool *dockertest.Pool
network *dockertest.Network
headscaleLock sync.Mutex
}
func NewScenario() (*Scenario, error) {
@@ -114,7 +99,7 @@ func NewScenario() (*Scenario, error) {
}
return &Scenario{
controlServers: xsync.NewMapOf[ControlServer](),
controlServers: make(map[string]ControlServer),
namespaces: make(map[string]*Namespace),
pool: pool,
@@ -123,17 +108,12 @@ func NewScenario() (*Scenario, error) {
}
func (s *Scenario) Shutdown() error {
s.controlServers.Range(func(_ string, control ControlServer) bool {
for _, control := range s.controlServers {
err := control.Shutdown()
if err != nil {
log.Printf(
"Failed to shut down control: %s",
fmt.Errorf("failed to tear down control: %w", err),
)
return fmt.Errorf("failed to tear down control: %w", err)
}
return true
})
}
for namespaceName, namespace := range s.namespaces {
for _, client := range namespace.Clients {
@@ -170,31 +150,36 @@ func (s *Scenario) Namespaces() []string {
// Note: These functions assume that there is a _single_ headscale instance for now
// TODO(kradalby): make port and headscale configurable, multiple instances support?
func (s *Scenario) Headscale(opts ...hsic.Option) (ControlServer, error) {
s.headscaleLock.Lock()
defer s.headscaleLock.Unlock()
if headscale, ok := s.controlServers.Load("headscale"); ok {
return headscale, nil
}
headscale, err := hsic.New(s.pool, s.network, opts...)
func (s *Scenario) StartHeadscale() error {
headscale, err := hsic.New(s.pool, headscalePort, s.network,
hsic.WithACLPolicy(
&headscale.ACLPolicy{
ACLs: []headscale.ACL{
{
Action: "accept",
Sources: []string{"*"},
Destinations: []string{"*:*"},
},
},
},
),
)
if err != nil {
return nil, fmt.Errorf("failed to create headscale container: %w", err)
return fmt.Errorf("failed to create headscale container: %w", err)
}
err = headscale.WaitForReady()
if err != nil {
return nil, fmt.Errorf("failed reach headscale container: %w", err)
}
s.controlServers["headscale"] = headscale
s.controlServers.Store("headscale", headscale)
return nil
}
return headscale, nil
func (s *Scenario) Headscale() *hsic.HeadscaleInContainer {
//nolint
return s.controlServers["headscale"].(*hsic.HeadscaleInContainer)
}
func (s *Scenario) CreatePreAuthKey(namespace string) (*v1.PreAuthKey, error) {
if headscale, err := s.Headscale(); err == nil {
if headscale, ok := s.controlServers["headscale"]; ok {
key, err := headscale.CreateAuthKey(namespace)
if err != nil {
return nil, fmt.Errorf("failed to create namespace: %w", err)
@@ -207,7 +192,7 @@ func (s *Scenario) CreatePreAuthKey(namespace string) (*v1.PreAuthKey, error) {
}
func (s *Scenario) CreateNamespace(namespace string) error {
if headscale, err := s.Headscale(); err == nil {
if headscale, ok := s.controlServers["headscale"]; ok {
err := headscale.CreateNamespace(namespace)
if err != nil {
return fmt.Errorf("failed to create namespace: %w", err)
@@ -237,36 +222,16 @@ func (s *Scenario) CreateTailscaleNodesInNamespace(
version = TailscaleVersions[i%len(TailscaleVersions)]
}
headscale, err := s.Headscale()
if err != nil {
return fmt.Errorf("failed to create tailscale node: %w", err)
}
cert := headscale.GetCert()
hostname := headscale.GetHostname()
namespace.createWaitGroup.Add(1)
go func() {
defer namespace.createWaitGroup.Done()
// TODO(kradalby): error handle this
tsClient, err := tsic.New(
s.pool,
version,
s.network,
tsic.WithHeadscaleTLS(cert),
tsic.WithHeadscaleName(hostname),
)
tsClient, err := tsic.New(s.pool, version, s.network)
if err != nil {
// return fmt.Errorf("failed to add tailscale node: %w", err)
log.Printf("failed to create tailscale node: %s", err)
}
err = tsClient.WaitForReady()
if err != nil {
// return fmt.Errorf("failed to add tailscale node: %w", err)
log.Printf("failed to wait for tailscaled: %s", err)
log.Printf("failed to add tailscale node: %s", err)
}
namespace.Clients[tsClient.Hostname()] = tsClient
@@ -293,13 +258,7 @@ func (s *Scenario) RunTailscaleUp(
// TODO(kradalby): error handle this
_ = c.Up(loginServer, authKey)
}(client)
err := client.WaitForReady()
if err != nil {
log.Printf("error waiting for client %s to be ready: %s", client.Hostname(), err)
}
}
namespace.joinWaitGroup.Wait()
return nil
@@ -341,8 +300,13 @@ func (s *Scenario) WaitForTailscaleSync() error {
// CreateHeadscaleEnv is a conventient method returning a set up Headcale
// test environment with nodes of all versions, joined to the server with X
// namespaces.
func (s *Scenario) CreateHeadscaleEnv(namespaces map[string]int, opts ...hsic.Option) error {
headscale, err := s.Headscale(opts...)
func (s *Scenario) CreateHeadscaleEnv(namespaces map[string]int) error {
err := s.StartHeadscale()
if err != nil {
return err
}
err = s.Headscale().WaitForReady()
if err != nil {
return err
}
@@ -363,7 +327,7 @@ func (s *Scenario) CreateHeadscaleEnv(namespaces map[string]int, opts ...hsic.Op
return err
}
err = s.RunTailscaleUp(namespaceName, headscale.GetEndpoint(), key.GetKey())
err = s.RunTailscaleUp(namespaceName, s.Headscale().GetEndpoint(), key.GetKey())
if err != nil {
return err
}

View File

@@ -34,12 +34,12 @@ func TestHeadscale(t *testing.T) {
}
t.Run("start-headscale", func(t *testing.T) {
headscale, err := scenario.Headscale()
err = scenario.StartHeadscale()
if err != nil {
t.Errorf("failed to create start headcale: %s", err)
}
err = headscale.WaitForReady()
err = scenario.Headscale().WaitForReady()
if err != nil {
t.Errorf("headscale failed to become ready: %s", err)
}
@@ -117,11 +117,12 @@ func TestTailscaleNodesJoiningHeadcale(t *testing.T) {
}
t.Run("start-headscale", func(t *testing.T) {
headscale, err := scenario.Headscale()
err = scenario.StartHeadscale()
if err != nil {
t.Errorf("failed to create start headcale: %s", err)
}
headscale := scenario.Headscale()
err = headscale.WaitForReady()
if err != nil {
t.Errorf("headscale failed to become ready: %s", err)
@@ -156,16 +157,7 @@ func TestTailscaleNodesJoiningHeadcale(t *testing.T) {
t.Errorf("failed to create preauthkey: %s", err)
}
headscale, err := scenario.Headscale()
if err != nil {
t.Errorf("failed to create start headcale: %s", err)
}
err = scenario.RunTailscaleUp(
namespace,
headscale.GetEndpoint(),
key.GetKey(),
)
err = scenario.RunTailscaleUp(namespace, scenario.Headscale().GetEndpoint(), key.GetKey())
if err != nil {
t.Errorf("failed to login: %s", err)
}

View File

@@ -12,7 +12,6 @@ import (
"github.com/cenkalti/backoff/v4"
"github.com/juanfont/headscale"
"github.com/juanfont/headscale/integration/dockertestutil"
"github.com/juanfont/headscale/integration/integrationutil"
"github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker"
"tailscale.com/ipn/ipnstate"
@@ -21,15 +20,13 @@ import (
const (
tsicHashLength = 6
dockerContextPath = "../."
headscaleCertPath = "/usr/local/share/ca-certificates/headscale.crt"
)
var (
errTailscalePingFailed = errors.New("ping failed")
errTailscaleNotLoggedIn = errors.New("tailscale not logged in")
errTailscaleWrongPeerCount = errors.New("wrong peer count")
errTailscaleCannotUpWithoutAuthkey = errors.New("cannot up without authkey")
errTailscaleNotConnected = errors.New("tailscale not connected")
errTailscalePingFailed = errors.New("ping failed")
errTailscaleNotLoggedIn = errors.New("tailscale not logged in")
errTailscaleWrongPeerCount = errors.New("wrong peer count")
errTailscaleNotConnected = errors.New("tailscale not connected")
)
type TailscaleInContainer struct {
@@ -43,51 +40,12 @@ type TailscaleInContainer struct {
// "cache"
ips []netip.Addr
fqdn string
// optional config
headscaleCert []byte
headscaleHostname string
}
type Option = func(c *TailscaleInContainer)
func WithHeadscaleTLS(cert []byte) Option {
return func(tsic *TailscaleInContainer) {
tsic.headscaleCert = cert
}
}
func WithOrCreateNetwork(network *dockertest.Network) Option {
return func(tsic *TailscaleInContainer) {
if network != nil {
tsic.network = network
return
}
network, err := dockertestutil.GetFirstOrCreateNetwork(
tsic.pool,
fmt.Sprintf("%s-network", tsic.hostname),
)
if err != nil {
log.Fatalf("failed to create network: %s", err)
}
tsic.network = network
}
}
func WithHeadscaleName(hsName string) Option {
return func(tsic *TailscaleInContainer) {
tsic.headscaleHostname = hsName
}
}
func New(
pool *dockertest.Pool,
version string,
network *dockertest.Network,
opts ...Option,
) (*TailscaleInContainer, error) {
hash, err := headscale.GenerateRandomStringDNSSafe(tsicHashLength)
if err != nil {
@@ -96,38 +54,20 @@ func New(
hostname := fmt.Sprintf("ts-%s-%s", strings.ReplaceAll(version, ".", "-"), hash)
tsic := &TailscaleInContainer{
version: version,
hostname: hostname,
pool: pool,
network: network,
}
for _, opt := range opts {
opt(tsic)
}
// TODO(kradalby): figure out why we need to "refresh" the network here.
// network, err = dockertestutil.GetFirstOrCreateNetwork(pool, network.Network.Name)
// if err != nil {
// return nil, err
// }
tailscaleOptions := &dockertest.RunOptions{
Name: hostname,
Networks: []*dockertest.Network{network},
// Cmd: []string{
// "tailscaled", "--tun=tsdev",
// },
Entrypoint: []string{
"/bin/bash",
"-c",
"/bin/sleep 3 ; update-ca-certificates ; tailscaled --tun=tsdev",
Cmd: []string{
"tailscaled", "--tun=tsdev",
},
}
if tsic.headscaleHostname != "" {
tailscaleOptions.ExtraHosts = []string{
"host.docker.internal:host-gateway",
fmt.Sprintf("%s:host-gateway", tsic.headscaleHostname),
}
}
// dockertest isnt very good at handling containers that has already
// been created, this is an attempt to make sure this container isnt
// present.
@@ -148,20 +88,14 @@ func New(
}
log.Printf("Created %s container\n", hostname)
tsic.container = container
return &TailscaleInContainer{
version: version,
hostname: hostname,
if tsic.hasTLS() {
err = tsic.WriteFile(headscaleCertPath, tsic.headscaleCert)
if err != nil {
return nil, fmt.Errorf("failed to write TLS certificate to container: %w", err)
}
}
return tsic, nil
}
func (t *TailscaleInContainer) hasTLS() bool {
return len(t.headscaleCert) != 0
pool: pool,
container: container,
network: network,
}, nil
}
func (t *TailscaleInContainer) Shutdown() error {
@@ -179,6 +113,8 @@ func (t *TailscaleInContainer) Version() string {
func (t *TailscaleInContainer) Execute(
command []string,
) (string, string, error) {
log.Println("command", command)
log.Printf("running command for %s\n", t.hostname)
stdout, stderr, err := dockertestutil.ExecuteCommand(
t.container,
command,
@@ -234,10 +170,7 @@ func (t *TailscaleInContainer) UpWithLoginURL(
t.hostname,
}
_, stderr, err := t.Execute(command)
if errors.Is(err, errTailscaleNotLoggedIn) {
return nil, errTailscaleCannotUpWithoutAuthkey
}
_, stderr, _ := t.Execute(command)
urlStr := strings.ReplaceAll(stderr, "\nTo authenticate, visit:\n\n\t", "")
urlStr = strings.TrimSpace(urlStr)
@@ -381,10 +314,6 @@ func (t *TailscaleInContainer) Ping(hostnameOrIP string) error {
})
}
func (t *TailscaleInContainer) WriteFile(path string, data []byte) error {
return integrationutil.WriteFileToContainer(t.pool, t.container, path, data)
}
func createTailscaleBuildOptions(version string) *dockertest.BuildOptions {
var tailscaleBuildOptions *dockertest.BuildOptions
switch version {

98
oidc.go
View File

@@ -76,55 +76,20 @@ func (h *Headscale) RegisterOIDC(
) {
vars := mux.Vars(req)
nodeKeyStr, ok := vars["nkey"]
if !ok || nodeKeyStr == "" {
log.Error().
Caller().
Msg("Missing node key in URL")
http.Error(writer, "Missing node key in URL", http.StatusBadRequest)
log.Debug().
return
}
log.Trace().
Caller().
Str("node_key", nodeKeyStr).
Bool("ok", ok).
Msg("Received oidc register call")
if !NodePublicKeyRegex.Match([]byte(nodeKeyStr)) {
log.Warn().Str("node_key", nodeKeyStr).Msg("Invalid node key passed to registration url")
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusUnauthorized)
_, err := writer.Write([]byte("Unauthorized"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return
}
// We need to make sure we dont open for XSS style injections, if the parameter that
// is passed as a key is not parsable/validated as a NodePublic key, then fail to render
// the template and log an error.
var nodeKey key.NodePublic
err := nodeKey.UnmarshalText(
[]byte(NodePublicKeyEnsurePrefix(nodeKeyStr)),
)
if !ok || nodeKeyStr == "" || err != nil {
log.Warn().
Err(err).
Msg("Failed to parse incoming nodekey in OIDC registration")
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusBadRequest)
_, err := writer.Write([]byte("Wrong params"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return
}
randomBlob := make([]byte, randomByteSize)
if _, err := rand.Read(randomBlob); err != nil {
log.Error().
@@ -138,7 +103,7 @@ func (h *Headscale) RegisterOIDC(
stateStr := hex.EncodeToString(randomBlob)[:32]
// place the node key into the state cache, so it can be retrieved later
h.registrationCache.Set(stateStr, NodePublicKeyStripPrefix(nodeKey), registerCacheExpiration)
h.registrationCache.Set(stateStr, nodeKeyStr, registerCacheExpiration)
// Add any extra parameter provided in the configuration to the Authorize Endpoint request
extras := make([]oauth2.AuthCodeOption, 0, len(h.cfg.OIDC.ExtraParams))
@@ -440,8 +405,8 @@ func (h *Headscale) validateMachineForOIDCCallback(
claims *IDTokenClaims,
) (*key.NodePublic, bool, error) {
// retrieve machinekey from state cache
nodeKeyIf, nodeKeyFound := h.registrationCache.Get(state)
if !nodeKeyFound {
machineKeyIf, machineKeyFound := h.registrationCache.Get(state)
if !machineKeyFound {
log.Error().
Msg("requested machine state key expired before authorisation completed")
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
@@ -454,38 +419,20 @@ func (h *Headscale) validateMachineForOIDCCallback(
Msg("Failed to write response")
}
return nil, false, errOIDCNodeKeyMissing
}
var nodeKey key.NodePublic
nodeKeyFromCache, nodeKeyOK := nodeKeyIf.(string)
if !nodeKeyOK {
log.Error().
Msg("requested machine state key is not a string")
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusBadRequest)
_, err := writer.Write([]byte("state is invalid"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return nil, false, errOIDCInvalidMachineState
}
var nodeKey key.NodePublic
nodeKeyFromCache, nodeKeyOK := machineKeyIf.(string)
err := nodeKey.UnmarshalText(
[]byte(NodePublicKeyEnsurePrefix(nodeKeyFromCache)),
)
if err != nil {
log.Error().
Str("nodeKey", nodeKeyFromCache).
Bool("nodeKeyOK", nodeKeyOK).
Msg("could not parse node public key")
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusBadRequest)
_, werr := writer.Write([]byte("could not parse node public key"))
_, werr := writer.Write([]byte("could not parse public key"))
if werr != nil {
log.Error().
Caller().
@@ -496,6 +443,21 @@ func (h *Headscale) validateMachineForOIDCCallback(
return nil, false, err
}
if !nodeKeyOK {
log.Error().Msg("could not get node key from cache")
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusInternalServerError)
_, err := writer.Write([]byte("could not get node key from cache"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return nil, false, errOIDCNodeKeyMissing
}
// retrieve machine information if it exist
// The error is not important, because if it does not
// exist, then this is a new machine and we will move

View File

@@ -490,7 +490,6 @@ func (h *Headscale) handleNewMachineCommon(
Bool("noise", machineKey.IsZero()).
Str("machine", registerRequest.Hostinfo.Hostname).
Msg("The node seems to be new, sending auth url")
if h.oauth2Config != nil {
resp.AuthURL = fmt.Sprintf(
"%s/oidc/register/%s",
@@ -529,7 +528,6 @@ func (h *Headscale) handleNewMachineCommon(
log.Info().
Caller().
Bool("noise", machineKey.IsZero()).
Str("AuthURL", resp.AuthURL).
Str("machine", registerRequest.Hostinfo.Hostname).
Msg("Successfully sent auth url")
}
@@ -728,7 +726,7 @@ func (h *Headscale) handleMachineExpiredCommon(
if h.oauth2Config != nil {
resp.AuthURL = fmt.Sprintf("%s/oidc/register/%s",
strings.TrimSuffix(h.cfg.ServerURL, "/"),
registerRequest.NodeKey)
NodePublicKeyStripPrefix(registerRequest.NodeKey))
} else {
resp.AuthURL = fmt.Sprintf("%s/register/%s",
strings.TrimSuffix(h.cfg.ServerURL, "/"),