Compare commits

..

2 Commits

Author SHA1 Message Date
Kristoffer Dalby
94048a96e7 Run on correct change
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-04 10:32:57 +01:00
Kristoffer Dalby
a617edadf5 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 10:32:22 +01:00
48 changed files with 510 additions and 1207 deletions

View File

@@ -13,13 +13,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
uses: tj-actions/changed-files@v14.1
with:
files: |
*.nix

View File

@@ -9,7 +9,7 @@ jobs:
add-contributors:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Delete upstream contributor branch
# Allow continue on failure to account for when the
# upstream branch is deleted or does not exist.

View File

@@ -7,13 +7,13 @@ jobs:
golangci-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
uses: tj-actions/changed-files@v14.1
with:
files: |
*.nix

View File

@@ -12,11 +12,11 @@ jobs:
runs-on: ubuntu-18.04 # due to CGO we need to user an older version
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v3
uses: actions/setup-go@v2
with:
go-version: 1.19.0
@@ -37,7 +37,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up Docker Buildx
@@ -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
@@ -99,7 +100,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up Docker Buildx
@@ -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@v2
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

@@ -16,7 +16,7 @@ jobs:
APP_ID: ${{ secrets.RENOVATEBOT_APP_ID }}
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v2.0.0
- name: Self-hosted Renovate
uses: renovatebot/github-action@v31.81.3

View File

@@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
with:
fetch-depth: 2
@@ -18,7 +18,7 @@ jobs:
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
uses: tj-actions/changed-files@v14.1
with:
files: |
*.nix

View File

@@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
with:
fetch-depth: 2
@@ -18,7 +18,7 @@ jobs:
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
uses: tj-actions/changed-files@v14.1
with:
files: |
*.nix

View File

@@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
with:
fetch-depth: 2
@@ -18,7 +18,7 @@ jobs:
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
uses: tj-actions/changed-files@v14.1
with:
files: |
*.nix

View File

@@ -13,7 +13,7 @@ jobs:
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
uses: tj-actions/changed-files@v14.1
with:
files: |
*.nix

View File

@@ -7,13 +7,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
uses: tj-actions/changed-files@v14.1
with:
files: |
*.nix

View File

@@ -1,8 +1,6 @@
---
run:
timeout: 10m
build-tags:
- ts2019
issues:
skip-dirs:
@@ -28,7 +26,6 @@ linters:
- ireturn
- execinquery
- exhaustruct
- nolintlint
# We should strive to enable these:
- wrapcheck
@@ -36,9 +33,6 @@ linters:
- makezero
- maintidx
# Limits the methods of an interface to 10. We have more in integration tests
- interfacebloat
# We might want to enable this, but it might be a lot of work
- cyclop
- nestif

View File

@@ -20,8 +20,6 @@ builds:
- -mod=readonly
ldflags:
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
tags:
- ts2019
- id: darwin-arm64
main: ./cmd/headscale/headscale.go
@@ -36,8 +34,6 @@ builds:
- -mod=readonly
ldflags:
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
tags:
- ts2019
- id: linux-amd64
mod_timestamp: "{{ .CommitTimestamp }}"
@@ -50,8 +46,6 @@ builds:
main: ./cmd/headscale/headscale.go
ldflags:
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
tags:
- ts2019
- id: linux-arm64
mod_timestamp: "{{ .CommitTimestamp }}"
@@ -64,8 +58,6 @@ builds:
main: ./cmd/headscale/headscale.go
ldflags:
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
tags:
- ts2019
archives:
- id: golang-cross

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
@@ -25,8 +24,6 @@
- Remove `ip_prefix` configuration option and warning [#899](https://github.com/juanfont/headscale/pull/899)
- 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)

View File

@@ -9,7 +9,7 @@ RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go install -tags ts2019 -ldflags="-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$VERSION" -a ./cmd/headscale
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

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

@@ -9,7 +9,7 @@ RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go install -tags ts2019 -ldflags="-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$VERSION" -a ./cmd/headscale
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 test -e /go/bin/headscale
# Debug image

View File

@@ -10,8 +10,6 @@ ifeq ($(filter $(GOOS), openbsd netbsd soloaris plan9), )
else
endif
TAGS = -tags ts2019
# GO_SOURCES = $(wildcard *.go)
# PROTO_SOURCES = $(wildcard **/*.proto)
GO_SOURCES = $(call rwildcard,,*.go)
@@ -19,12 +17,12 @@ PROTO_SOURCES = $(call rwildcard,,*.proto)
build:
nix build
GOOS=$(GOOS) CGO_ENABLED=0 go build -trimpath $(pieflags) -mod=readonly -ldflags "-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$(version)" cmd/headscale/headscale.go
dev: lint test build
test:
@go test $(TAGS) -short -coverprofile=coverage.out ./...
@go test -short -coverprofile=coverage.out ./...
test_integration: test_integration_cli test_integration_derp test_integration_oidc test_integration_v2_general
@@ -36,7 +34,7 @@ test_integration_cli:
-v ~/.cache/hs-integration-go:/go \
-v $$PWD:$$PWD -w $$PWD \
-v /var/run/docker.sock:/var/run/docker.sock golang:1 \
go test $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationCLI ./...
go test -failfast -timeout 30m -count=1 -run IntegrationCLI ./...
test_integration_derp:
docker network rm $$(docker network ls --filter name=headscale --quiet) || true
@@ -46,7 +44,7 @@ test_integration_derp:
-v ~/.cache/hs-integration-go:/go \
-v $$PWD:$$PWD -w $$PWD \
-v /var/run/docker.sock:/var/run/docker.sock golang:1 \
go test $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationDERP ./...
go test -failfast -timeout 30m -count=1 -run IntegrationDERP ./...
test_integration_oidc:
docker network rm $$(docker network ls --filter name=headscale --quiet) || true
@@ -56,7 +54,7 @@ test_integration_oidc:
-v ~/.cache/hs-integration-go:/go \
-v $$PWD:$$PWD -w $$PWD \
-v /var/run/docker.sock:/var/run/docker.sock golang:1 \
go test $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationOIDC ./...
go test -failfast -timeout 30m -count=1 -run IntegrationOIDC ./...
test_integration_v2_general:
docker run \
@@ -66,7 +64,7 @@ test_integration_v2_general:
-v $$PWD:$$PWD -w $$PWD/integration \
-v /var/run/docker.sock:/var/run/docker.sock \
golang:1 \
go test $(TAGS) -failfast ./... -timeout 60m -parallel 6
go test ./... -timeout 60m -parallel 6
coverprofile_func:
go tool cover -func=coverage.out

View File

@@ -424,13 +424,6 @@ make build
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/CNLHC>
<img src=https://avatars.githubusercontent.com/u/21005146?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=LiuHanCheng/>
<br />
<sub style="font-size:14px"><b>LiuHanCheng</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/pvinis>
<img src=https://avatars.githubusercontent.com/u/100233?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Pavlos Vinieratos/>
@@ -446,7 +439,7 @@ make build
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/ratsclub>
<a href=https://github.com/vtrf>
<img src=https://avatars.githubusercontent.com/u/25647735?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Victor Freire/>
<br />
<sub style="font-size:14px"><b>Victor Freire</b></sub>
@@ -466,8 +459,6 @@ make build
<sub style="font-size:14px"><b>thomas</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/aberoham>
<img src=https://avatars.githubusercontent.com/u/586805?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Abraham Ingersoll/>
@@ -475,13 +466,8 @@ make build
<sub style="font-size:14px"><b>Abraham Ingersoll</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/puzpuzpuz>
<img src=https://avatars.githubusercontent.com/u/37772591?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Andrei Pechkurov/>
<br />
<sub style="font-size:14px"><b>Andrei Pechkurov</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/apognu>
<img src=https://avatars.githubusercontent.com/u/3017182?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Antoine POPINEAU/>
@@ -510,8 +496,6 @@ make build
<sub style="font-size:14px"><b>Bryan Stenson</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/yangchuansheng>
<img src=https://avatars.githubusercontent.com/u/15308462?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt= Carson Yang/>
@@ -526,6 +510,8 @@ make build
<sub style="font-size:14px"><b>kundel</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/fkr>
<img src=https://avatars.githubusercontent.com/u/51063?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Felix Kronlage-Dammers/>
@@ -554,8 +540,6 @@ make build
<sub style="font-size:14px"><b>Jim Tittsler</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/ShadowJonathan>
<img src=https://avatars.githubusercontent.com/u/22740616?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jonathan de Jong/>
@@ -570,6 +554,8 @@ make build
<sub style="font-size:14px"><b>Pierre Carru</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/Donran>
<img src=https://avatars.githubusercontent.com/u/4838348?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Pontus N/>
@@ -593,13 +579,11 @@ make build
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/renovate-bot>
<img src=https://avatars.githubusercontent.com/u/25180681?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Mend Renovate/>
<img src=https://avatars.githubusercontent.com/u/25180681?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=WhiteSource Renovate/>
<br />
<sub style="font-size:14px"><b>Mend Renovate</b></sub>
<sub style="font-size:14px"><b>WhiteSource Renovate</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/ryanfowler>
<img src=https://avatars.githubusercontent.com/u/2668821?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Ryan Fowler/>
@@ -614,6 +598,8 @@ make build
<sub style="font-size:14px"><b>Shaanan Cohney</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/stefanvanburen>
<img src=https://avatars.githubusercontent.com/u/622527?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Stefan VanBuren/>
@@ -642,8 +628,6 @@ make build
<sub style="font-size:14px"><b>Teteros</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/gitter-badger>
<img src=https://avatars.githubusercontent.com/u/8518239?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=The Gitter Badger/>
@@ -658,6 +642,8 @@ make build
<sub style="font-size:14px"><b>Tianon Gravi</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/thetillhoff>
<img src=https://avatars.githubusercontent.com/u/25052289?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Till Hoffmann/>
@@ -686,8 +672,6 @@ make build
<sub style="font-size:14px"><b>Yujie Xia</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/zekker6>
<img src=https://avatars.githubusercontent.com/u/1367798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Zakhar Bessarab/>
@@ -702,6 +686,8 @@ make build
<sub style="font-size:14px"><b>Zhiyuan Zheng</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/Bpazy>
<img src=https://avatars.githubusercontent.com/u/9838749?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Ziyuan Han/>
@@ -730,8 +716,6 @@ make build
<sub style="font-size:14px"><b>ignoramous</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/lion24>
<img src=https://avatars.githubusercontent.com/u/1382102?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=sharkonet/>
@@ -746,6 +730,8 @@ make build
<sub style="font-size:14px"><b>pernila</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/phpmalik>
<img src=https://avatars.githubusercontent.com/u/26834645?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=phpmalik/>

17
app.go
View File

@@ -51,6 +51,12 @@ const (
errUnsupportedLetsEncryptChallengeType = Error(
"unknown value for Lets Encrypt challenge type",
)
ErrFailedPrivateKey = Error("failed to read or create private key")
ErrFailedNoisePrivateKey = Error(
"failed to read or create Noise protocol private key",
)
ErrSamePrivateKeys = Error("private key and noise private key are the same")
)
const (
@@ -125,17 +131,17 @@ func LookupTLSClientAuthMode(mode string) (tls.ClientAuthType, bool) {
func NewHeadscale(cfg *Config) (*Headscale, error) {
privateKey, err := readOrCreatePrivateKey(cfg.PrivateKeyPath)
if err != nil {
return nil, fmt.Errorf("failed to read or create private key: %w", err)
return nil, ErrFailedPrivateKey
}
// TS2021 requires to have a different key from the legacy protocol.
noisePrivateKey, err := readOrCreatePrivateKey(cfg.NoisePrivateKeyPath)
if err != nil {
return nil, fmt.Errorf("failed to read or create Noise protocol private key: %w", err)
return nil, ErrFailedNoisePrivateKey
}
if privateKey.Equal(*noisePrivateKey) {
return nil, fmt.Errorf("private key and noise private key are the same: %w", err)
return nil, ErrSamePrivateKeys
}
var dbString string
@@ -448,8 +454,9 @@ func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *mux.Router {
router.HandleFunc("/health", h.HealthHandler).Methods(http.MethodGet)
router.HandleFunc("/key", h.KeyHandler).Methods(http.MethodGet)
router.HandleFunc("/register/{nkey}", h.RegisterWebAPI).Methods(http.MethodGet)
h.addLegacyHandlers(router)
router.HandleFunc("/machine/{mkey}/map", h.PollNetMapHandler).
Methods(http.MethodPost)
router.HandleFunc("/machine/{mkey}", h.RegistrationHandler).Methods(http.MethodPost)
router.HandleFunc("/oidc/register/{nkey}", h.RegisterOIDC).Methods(http.MethodGet)
router.HandleFunc("/oidc/callback", h.OIDCCallback).Methods(http.MethodGet)
router.HandleFunc("/apple", h.AppleConfigMessage).Methods(http.MethodGet)

View File

@@ -3,7 +3,6 @@ package cli
import (
"fmt"
"github.com/juanfont/headscale"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
@@ -11,7 +10,8 @@ import (
)
const (
errPreAuthKeyMalformed = Error("key is malformed. expected 64 hex characters with `nodekey` prefix")
keyLength = 64
errPreAuthKeyTooShort = Error("key too short, must be 64 hexadecimal characters")
)
// Error is used to compare errors as per https://dave.cheney.net/2016/04/07/constant-errors
@@ -87,8 +87,8 @@ var createNodeCmd = &cobra.Command{
return
}
if !headscale.NodePublicKeyRegex.Match([]byte(machineKey)) {
err = errPreAuthKeyMalformed
if len(machineKey) != keyLength {
err = errPreAuthKeyTooShort
ErrorOutput(
err,
fmt.Sprintf("Error: %s", err),

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, "Machine register", output)
},
}

View File

@@ -35,13 +35,14 @@ grpc_listen_addr: 0.0.0.0:50443
# are doing.
grpc_allow_insecure: false
# Private key used to encrypt the traffic between headscale
# Private key used encrypt the traffic between headscale
# and Tailscale clients.
# The private key file will be autogenerated if it's missing.
# The private key file which will be
# autogenerated if it's missing
private_key_path: /var/lib/headscale/private.key
# The Noise section includes specific configuration for the
# TS2021 Noise protocol
# TS2021 Noise procotol
noise:
# The Noise private key is used to encrypt the
# traffic between headscale and Tailscale clients when
@@ -77,7 +78,7 @@ derp:
region_code: "headscale"
region_name: "Headscale Embedded DERP"
# Listens over UDP at the configured address for STUN connections - to help with NAT traversal.
# Listens in UDP at the configured address for STUN connections to help on NAT traversal.
# When the embedded DERP server is enabled stun_listen_addr MUST be defined.
#
# For more details on how this works, check this great article: https://tailscale.com/blog/how-tailscale-works/
@@ -111,9 +112,9 @@ disable_check_updates: false
# Time before an inactive ephemeral node is deleted?
ephemeral_node_inactivity_timeout: 30m
# Period to check for node updates within the tailnet. A value too low will severely affect
# Period to check for node updates in the tailnet. A value too low will severily affect
# CPU consumption of Headscale. A value too high (over 60s) will cause problems
# for the nodes, as they won't get updates or keep alive messages frequently enough.
# to the nodes, as they won't get updates or keep alive messages in time.
# In case of doubts, do not touch the default 10s.
node_update_check_interval: 10s

View File

@@ -26,8 +26,6 @@
version = headscaleVersion;
src = pkgs.lib.cleanSource self;
tags = ["ts2019"];
# Only run unit tests when testing a build
checkFlags = ["-short"];
@@ -137,7 +135,7 @@
buildInputs = devDeps;
shellHook = ''
export GOFLAGS=-tags="ts2019"
export GOFLAGS=-tags="integration,integration_general,integration_oidc,integration_cli,integration_derp"
'';
};

View File

@@ -1,4 +1,4 @@
// nolint
//nolint
package headscale
import (
@@ -12,7 +12,6 @@ import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
type headscaleV1APIServer struct { // v1.HeadscaleServiceServer
@@ -497,14 +496,9 @@ func (api headscaleV1APIServer) DebugCreateMachine(
HostInfo: HostInfo(hostinfo),
}
nodeKey := key.NodePublic{}
err = nodeKey.UnmarshalText([]byte(request.GetKey()))
if err != nil {
log.Panic().Msg("can not add machine for debug. invalid node key")
}
api.h.registrationCache.Set(
NodePublicKeyStripPrefix(nodeKey),
request.GetKey(),
newMachine,
registerCacheExpiration,
)

View File

@@ -1,15 +0,0 @@
//go:build ts2019
package headscale
import (
"net/http"
"github.com/gorilla/mux"
)
func (h *Headscale) addLegacyHandlers(router *mux.Router) {
router.HandleFunc("/machine/{mkey}/map", h.PollNetMapHandler).
Methods(http.MethodPost)
router.HandleFunc("/machine/{mkey}", h.RegistrationHandler).Methods(http.MethodPost)
}

View File

@@ -1,8 +0,0 @@
//go:build !ts2019
package headscale
import "github.com/gorilla/mux"
func (h *Headscale) addLegacyHandlers(router *mux.Router) {
}

View File

@@ -1,204 +0,0 @@
package integration
import (
"context"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/url"
"strings"
"testing"
"github.com/juanfont/headscale/integration/hsic"
)
var errParseAuthPage = errors.New("failed to parse auth page")
type AuthWebFlowScenario struct {
*Scenario
}
func TestAuthWebFlowAuthenticationPingAll(t *testing.T) {
IntegrationSkip(t)
baseScenario, err := NewScenario()
if err != nil {
t.Errorf("failed to create scenario: %s", err)
}
scenario := AuthWebFlowScenario{
Scenario: baseScenario,
}
spec := map[string]int{
"namespace1": len(TailscaleVersions),
"namespace2": len(TailscaleVersions),
}
err = scenario.CreateHeadscaleEnv(spec, hsic.WithTestName("webauthping"))
if err != nil {
t.Errorf("failed to create headscale environment: %s", err)
}
allClients, err := scenario.ListTailscaleClients()
if err != nil {
t.Errorf("failed to get clients: %s", err)
}
allIps, err := scenario.ListTailscaleClientsIPs()
if err != nil {
t.Errorf("failed to get clients: %s", err)
}
err = scenario.WaitForTailscaleSync()
if err != nil {
t.Errorf("failed wait for tailscale clients to be in sync: %s", err)
}
success := 0
for _, client := range allClients {
for _, ip := range allIps {
err := client.Ping(ip.String())
if err != nil {
t.Errorf("failed to ping %s from %s: %s", ip, client.Hostname(), err)
} else {
success++
}
}
}
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
err = scenario.Shutdown()
if err != nil {
t.Errorf("failed to tear down scenario: %s", err)
}
}
func (s *AuthWebFlowScenario) CreateHeadscaleEnv(
namespaces map[string]int,
opts ...hsic.Option,
) error {
headscale, err := s.Headscale(opts...)
if err != nil {
return err
}
err = headscale.WaitForReady()
if err != nil {
return err
}
for namespaceName, clientCount := range namespaces {
log.Printf("creating namespace %s with %d clients", namespaceName, clientCount)
err = s.CreateNamespace(namespaceName)
if err != nil {
return err
}
err = s.CreateTailscaleNodesInNamespace(namespaceName, "all", clientCount)
if err != nil {
return err
}
err = s.runTailscaleUp(namespaceName, headscale.GetEndpoint())
if err != nil {
return err
}
}
return nil
}
func (s *AuthWebFlowScenario) runTailscaleUp(
namespaceStr, loginServer string,
) error {
log.Printf("running tailscale up for namespace %s", namespaceStr)
if namespace, ok := s.namespaces[namespaceStr]; ok {
for _, client := range namespace.Clients {
namespace.joinWaitGroup.Add(1)
go func(c TailscaleClient) {
defer namespace.joinWaitGroup.Done()
// TODO(juanfont): error handle this
loginURL, err := c.UpWithLoginURL(loginServer)
if err != nil {
log.Printf("failed to run tailscale up: %s", err)
}
err = s.runHeadscaleRegister(namespaceStr, loginURL)
if err != nil {
log.Printf("failed to register client: %s", err)
}
err = c.WaitForReady()
if err != nil {
log.Printf("error waiting for client %s to be ready: %s", c.Hostname(), err)
}
}(client)
}
namespace.joinWaitGroup.Wait()
return nil
}
return fmt.Errorf("failed to up tailscale node: %w", errNoNamespaceAvailable)
}
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.Scheme = "http"
httpClient := &http.Client{}
ctx := context.Background()
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, loginURL.String(), nil)
resp, err := httpClient.Do(req)
if err != nil {
return err
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
defer resp.Body.Close()
// see api.go HTML template
codeSep := strings.Split(string(body), "</code>")
if len(codeSep) != 2 {
return errParseAuthPage
}
keySep := strings.Split(codeSep[0], "key ")
if len(keySep) != 2 {
return errParseAuthPage
}
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 err != nil {
log.Printf("failed to register node: %s", err)
return err
}
return nil
}
return fmt.Errorf("failed to find headscale: %w", errNoHeadscaleAvailable)
}

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)
}
@@ -169,7 +168,7 @@ func TestTaildrop(t *testing.T) {
for _, client := range allClients {
command := []string{"touch", fmt.Sprintf("/tmp/file_from_%s", client.Hostname())}
if _, _, err := client.Execute(command); err != nil {
if _, err := client.Execute(command); err != nil {
t.Errorf("failed to create taildrop file on %s, err: %s", client.Hostname(), err)
}
@@ -194,7 +193,7 @@ func TestTaildrop(t *testing.T) {
client.Hostname(),
peer.Hostname(),
)
_, _, err := client.Execute(command)
_, err := client.Execute(command)
return err
})
@@ -215,7 +214,7 @@ func TestTaildrop(t *testing.T) {
"get",
"/tmp/",
}
if _, _, err := client.Execute(command); err != nil {
if _, err := client.Execute(command); err != nil {
t.Errorf("failed to get taildrop file on %s, err: %s", client.Hostname(), err)
}
@@ -235,7 +234,7 @@ func TestTaildrop(t *testing.T) {
peer.Hostname(),
)
result, _, err := client.Execute(command)
result, err := client.Execute(command)
if err != nil {
t.Errorf("failed to execute command to ls taildrop: %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)
}
@@ -307,7 +306,7 @@ func TestResolveMagicDNS(t *testing.T) {
"tailscale",
"ip", peerFQDN,
}
result, _, err := client.Execute(command)
result, err := client.Execute(command)
if err != nil {
t.Errorf(
"failed to execute resolve/ip command %s from %s: %s",

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
}
@@ -237,11 +179,17 @@ func (t *HeadscaleInContainer) GetIP() string {
}
func (t *HeadscaleInContainer) GetPort() string {
return fmt.Sprintf("%d", t.port)
portProto := fmt.Sprintf("%d/tcp", t.port)
return t.container.GetPort(portProto)
}
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 +197,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 +294,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

@@ -2,7 +2,6 @@ package integration
import (
"net/netip"
"net/url"
"tailscale.com/ipn/ipnstate"
)
@@ -11,13 +10,11 @@ type TailscaleClient interface {
Hostname() string
Shutdown() error
Version() string
Execute(command []string) (string, string, error)
Execute(command []string) (string, error)
Up(loginServer, authKey string) error
UpWithLoginURL(loginServer string) (*url.URL, error)
IPs() ([]netip.Addr, error)
FQDN() (string, error)
Status() (*ipnstate.Status, error)
WaitForReady() error
WaitForPeers(expected int) error
Ping(hostnameOrIP string) error
}

View File

@@ -6,13 +6,11 @@ import (
"fmt"
"log"
"net/netip"
"net/url"
"strings"
"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 +19,12 @@ 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")
)
type TailscaleInContainer struct {
@@ -43,51 +38,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 +52,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 +86,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 {
@@ -178,7 +110,9 @@ func (t *TailscaleInContainer) Version() string {
func (t *TailscaleInContainer) Execute(
command []string,
) (string, string, error) {
) (string, error) {
log.Println("command", command)
log.Printf("running command for %s\n", t.hostname)
stdout, stderr, err := dockertestutil.ExecuteCommand(
t.container,
command,
@@ -192,13 +126,13 @@ func (t *TailscaleInContainer) Execute(
}
if strings.Contains(stderr, "NeedsLogin") {
return stdout, stderr, errTailscaleNotLoggedIn
return "", errTailscaleNotLoggedIn
}
return stdout, stderr, err
return "", err
}
return stdout, stderr, nil
return stdout, nil
}
func (t *TailscaleInContainer) Up(
@@ -215,45 +149,13 @@ func (t *TailscaleInContainer) Up(
t.hostname,
}
if _, _, err := t.Execute(command); err != nil {
if _, err := t.Execute(command); err != nil {
return fmt.Errorf("failed to join tailscale client: %w", err)
}
return nil
}
func (t *TailscaleInContainer) UpWithLoginURL(
loginServer string,
) (*url.URL, error) {
command := []string{
"tailscale",
"up",
"-login-server",
loginServer,
"--hostname",
t.hostname,
}
_, stderr, err := t.Execute(command)
if errors.Is(err, errTailscaleNotLoggedIn) {
return nil, errTailscaleCannotUpWithoutAuthkey
}
urlStr := strings.ReplaceAll(stderr, "\nTo authenticate, visit:\n\n\t", "")
urlStr = strings.TrimSpace(urlStr)
// parse URL
loginURL, err := url.Parse(urlStr)
if err != nil {
log.Printf("Could not parse login URL: %s", err)
log.Printf("Original join command result: %s", stderr)
return nil, err
}
return loginURL, nil
}
func (t *TailscaleInContainer) IPs() ([]netip.Addr, error) {
if t.ips != nil && len(t.ips) != 0 {
return t.ips, nil
@@ -266,7 +168,7 @@ func (t *TailscaleInContainer) IPs() ([]netip.Addr, error) {
"ip",
}
result, _, err := t.Execute(command)
result, err := t.Execute(command)
if err != nil {
return []netip.Addr{}, fmt.Errorf("failed to join tailscale client: %w", err)
}
@@ -293,7 +195,7 @@ func (t *TailscaleInContainer) Status() (*ipnstate.Status, error) {
"--json",
}
result, _, err := t.Execute(command)
result, err := t.Execute(command)
if err != nil {
return nil, fmt.Errorf("failed to execute tailscale status command: %w", err)
}
@@ -320,21 +222,6 @@ func (t *TailscaleInContainer) FQDN() (string, error) {
return status.Self.DNSName, nil
}
func (t *TailscaleInContainer) WaitForReady() error {
return t.pool.Retry(func() error {
status, err := t.Status()
if err != nil {
return fmt.Errorf("failed to fetch tailscale status: %w", err)
}
if status.CurrentTailnet != nil {
return nil
}
return errTailscaleNotConnected
})
}
func (t *TailscaleInContainer) WaitForPeers(expected int) error {
return t.pool.Retry(func() error {
status, err := t.Status()
@@ -361,7 +248,7 @@ func (t *TailscaleInContainer) Ping(hostnameOrIP string) error {
hostnameOrIP,
}
result, _, err := t.Execute(command)
result, err := t.Execute(command)
if err != nil {
log.Printf(
"failed to run ping command from %s to %s, err: %s",
@@ -381,10 +268,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 {

View File

@@ -1,4 +1,4 @@
// nolint
//nolint
package headscale
import (
@@ -558,8 +558,8 @@ func (s *IntegrationCLITestSuite) TestNodeTagCommand() {
assert.Nil(s.T(), err)
machineKeys := []string{
"nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
"nodekey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
"9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
"6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
}
machines := make([]*v1.Machine, len(machineKeys))
assert.Nil(s.T(), err)
@@ -691,11 +691,11 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
// Randomly generated machine keys
machineKeys := []string{
"nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
"nodekey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
"nodekey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
"nodekey:8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1",
"nodekey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
"9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
"6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
"f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
"8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1",
"cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
}
machines := make([]*v1.Machine, len(machineKeys))
assert.Nil(s.T(), err)
@@ -779,8 +779,8 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
assert.Equal(s.T(), "machine-5", listAll[4].Name)
otherNamespaceMachineKeys := []string{
"nodekey:b5b444774186d4217adcec407563a1223929465ee2c68a4da13af0d0185b4f8e",
"nodekey:dc721977ac7415aafa87f7d4574cbe07c6b171834a6d37375782bdc1fb6b3584",
"b5b444774186d4217adcec407563a1223929465ee2c68a4da13af0d0185b4f8e",
"dc721977ac7415aafa87f7d4574cbe07c6b171834a6d37375782bdc1fb6b3584",
}
otherNamespaceMachines := make([]*v1.Machine, len(otherNamespaceMachineKeys))
assert.Nil(s.T(), err)
@@ -950,11 +950,11 @@ func (s *IntegrationCLITestSuite) TestNodeExpireCommand() {
// Randomly generated machine keys
machineKeys := []string{
"nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
"nodekey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
"nodekey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
"nodekey:8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1",
"nodekey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
"9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
"6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
"f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
"8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1",
"cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
}
machines := make([]*v1.Machine, len(machineKeys))
assert.Nil(s.T(), err)
@@ -1077,11 +1077,11 @@ func (s *IntegrationCLITestSuite) TestNodeRenameCommand() {
// Randomly generated machine keys
machineKeys := []string{
"nodekey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
"nodekey:8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1",
"nodekey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
"nodekey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
"nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
"cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
"8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1",
"f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
"6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
"9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
}
machines := make([]*v1.Machine, len(machineKeys))
assert.Nil(s.T(), err)
@@ -1248,7 +1248,7 @@ func (s *IntegrationCLITestSuite) TestRouteCommand() {
assert.Nil(s.T(), err)
// Randomly generated machine keys
machineKey := "nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe"
machineKey := "9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe"
_, _, err = ExecuteCommand(
&s.headscale,
@@ -1588,7 +1588,7 @@ func (s *IntegrationCLITestSuite) TestNodeMoveCommand() {
assert.Nil(s.T(), err)
// Randomly generated machine key
machineKey := "nodekey:688411b767663479632d44140f08a9fde87383adc7cdeb518f62ce28a17ef0aa"
machineKey := "688411b767663479632d44140f08a9fde87383adc7cdeb518f62ce28a17ef0aa"
_, _, err = ExecuteCommand(
&s.headscale,

View File

@@ -1,4 +1,4 @@
// nolint
//nolint
package headscale
import (

View File

@@ -839,13 +839,7 @@ func (h *Headscale) RegisterMachineFromAuthCallback(
namespaceName string,
registrationMethod string,
) (*Machine, error) {
nodeKey := key.NodePublic{}
err := nodeKey.UnmarshalText([]byte(nodeKeyStr))
if err != nil {
return nil, err
}
if machineInterface, ok := h.registrationCache.Get(NodePublicKeyStripPrefix(nodeKey)); ok {
if machineInterface, ok := h.registrationCache.Get(nodeKeyStr); ok {
if registrationMachine, ok := machineInterface.(Machine); ok {
namespace, err := h.GetNamespace(namespaceName)
if err != nil {

102
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
@@ -642,8 +604,10 @@ func (h *Headscale) registerMachineForOIDCCallback(
namespace *Namespace,
nodeKey *key.NodePublic,
) error {
nodeKeyStr := NodePublicKeyStripPrefix(*nodeKey)
if _, err := h.RegisterMachineFromAuthCallback(
nodeKey.String(),
nodeKeyStr,
namespace.Name,
RegisterMethodOIDC,
); err != nil {

View File

@@ -2,7 +2,6 @@ package headscale
import (
"bytes"
_ "embed"
"html/template"
"net/http"
textTemplate "text/template"
@@ -12,18 +11,51 @@ import (
"github.com/rs/zerolog/log"
)
//go:embed templates/apple.html
var appleTemplate string
//go:embed templates/windows.html
var windowsTemplate string
// WindowsConfigMessage shows a simple message in the browser for how to configure the Windows Tailscale client.
func (h *Headscale) WindowsConfigMessage(
writer http.ResponseWriter,
req *http.Request,
) {
winTemplate := template.Must(template.New("windows").Parse(windowsTemplate))
winTemplate := template.Must(template.New("windows").Parse(`
<html>
<body>
<h1>headscale</h1>
<h2>Windows registry configuration</h2>
<p>
This page provides Windows registry information for the official Windows Tailscale client.
<p>
<p>
The registry file will configure Tailscale to use <code>{{.URL}}</code> as its control server.
<p>
<h3>Caution</h3>
<p>You should always download and inspect the registry file before installing it:</p>
<pre><code>curl {{.URL}}/windows/tailscale.reg</code></pre>
<h2>Installation</h2>
<p>Headscale can be set to the default server by running the registry file:</p>
<p>
<a href="/windows/tailscale.reg" download="tailscale.reg">Windows registry file</a>
</p>
<ol>
<li>Download the registry file, then run it</li>
<li>Follow the prompts</li>
<li>Install and run the official windows Tailscale client</li>
<li>When the installation has finished, start Tailscale, and log in by clicking the icon in the system tray</li>
</ol>
<p>Or</p>
<p>Open command prompt with Administrator rights. Issue the following commands to add the required registry entries:</p>
<pre>
<code>REG ADD "HKLM\Software\Tailscale IPN" /v UnattendedMode /t REG_SZ /d always
REG ADD "HKLM\Software\Tailscale IPN" /v LoginURL /t REG_SZ /d "{{.URL}}"</code></pre>
<p>
Restart Tailscale and log in.
<p>
</body>
</html>
`))
config := map[string]interface{}{
"URL": h.cfg.ServerURL,
}
@@ -104,7 +136,55 @@ func (h *Headscale) AppleConfigMessage(
writer http.ResponseWriter,
req *http.Request,
) {
appleTemplate := template.Must(template.New("apple").Parse(appleTemplate))
appleTemplate := template.Must(template.New("apple").Parse(`
<html>
<body>
<h1>headscale</h1>
<h2>Apple configuration profiles</h2>
<p>
This page provides <a href="https://support.apple.com/guide/mdm/mdm-overview-mdmbf9e668/web">configuration profiles</a> for the official Tailscale clients for <a href="https://apps.apple.com/us/app/tailscale/id1470499037?ls=1">iOS</a> and <a href="https://apps.apple.com/ca/app/tailscale/id1475387142?mt=12">macOS</a>.
</p>
<p>
The profiles will configure Tailscale.app to use <code>{{.URL}}</code> as its control server.
</p>
<h3>Caution</h3>
<p>You should always download and inspect the profile before installing it:</p>
<!--
<pre><code>curl {{.URL}}/apple/ios</code></pre>
-->
<pre><code>curl {{.URL}}/apple/macos</code></pre>
<h2>Profiles</h2>
<!--
<h3>iOS</h3>
<p>
<a href="/apple/ios" download="headscale_ios.mobileconfig">iOS profile</a>
</p>
-->
<h3>macOS</h3>
<p>Headscale can be set to the default server by installing a Headscale configuration profile:</p>
<p>
<a href="/apple/macos" download="headscale_macos.mobileconfig">macOS profile</a>
</p>
<ol>
<li>Download the profile, then open it. When it has been opened, there should be a notification that a profile can be installed</li>
<li>Open System Preferences and go to "Profiles"</li>
<li>Find and install the Headscale profile</li>
<li>Restart Tailscale.app and log in</li>
</ol>
<p>Or</p>
<p>Use your terminal to configure the default setting for Tailscale by issuing:</p>
<code>defaults write io.tailscale.ipn.macos ControlURL {{.URL}}</code>
<p>Restart Tailscale.app and log in.</p>
</body>
</html>`))
config := map[string]interface{}{
"URL": h.cfg.ServerURL,
@@ -202,33 +282,24 @@ func (h *Headscale) ApplePlatformConfig(
}
var payload bytes.Buffer
handleMacError := func(ierr error) {
log.Error().
Str("handler", "ApplePlatformConfig").
Err(ierr).
Msg("Could not render Apple macOS template")
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusInternalServerError)
_, err := writer.Write([]byte("Could not render Apple macOS template"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
}
switch platform {
case "macos-standlone":
if err := macosStandloneTemplate.Execute(&payload, platformConfig); err != nil {
handleMacError(err)
case "macos":
if err := macosTemplate.Execute(&payload, platformConfig); err != nil {
log.Error().
Str("handler", "ApplePlatformConfig").
Err(err).
Msg("Could not render Apple macOS template")
return
}
case "macos-app-store":
if err := macosAppStoreTemplate.Execute(&payload, platformConfig); err != nil {
handleMacError(err)
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusInternalServerError)
_, err := writer.Write([]byte("Could not render Apple macOS template"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return
}
@@ -373,7 +444,7 @@ var iosTemplate = textTemplate.Must(textTemplate.New("iosTemplate").Parse(`
</dict>
`))
var macosAppStoreTemplate = template.Must(template.New("macosTemplate").Parse(`
var macosTemplate = template.Must(template.New("macosTemplate").Parse(`
<dict>
<key>PayloadType</key>
<string>io.tailscale.ipn.macos</string>
@@ -385,23 +456,7 @@ var macosAppStoreTemplate = template.Must(template.New("macosTemplate").Parse(`
<integer>1</integer>
<key>PayloadEnabled</key>
<true/>
<key>ControlURL</key>
<string>{{.URL}}</string>
</dict>
`))
var macosStandloneTemplate = template.Must(template.New("macosStandloneTemplate").Parse(`
<dict>
<key>PayloadType</key>
<string>io.tailscale.ipn.macsys</string>
<key>PayloadUUID</key>
<string>{{.UUID}}</string>
<key>PayloadIdentifier</key>
<string>com.github.juanfont.headscale</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PayloadEnabled</key>
<true/>
<key>ControlURL</key>
<string>{{.URL}}</string>
</dict>

View File

@@ -435,10 +435,6 @@ func (h *Headscale) handleAuthKeyCommon(
resp.MachineAuthorized = true
resp.User = *pak.Namespace.toUser()
// Provide LoginName when registering with pre-auth key
// Otherwise it will need to exec `tailscale up` twice to fetch the *LoginName*
resp.Login = *pak.Namespace.toLogin()
respBody, err := h.marshalResponse(resp, machineKey)
if err != nil {
log.Error().
@@ -490,17 +486,16 @@ 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",
strings.TrimSuffix(h.cfg.ServerURL, "/"),
registerRequest.NodeKey,
NodePublicKeyStripPrefix(registerRequest.NodeKey),
)
} else {
resp.AuthURL = fmt.Sprintf("%s/register/%s",
strings.TrimSuffix(h.cfg.ServerURL, "/"),
registerRequest.NodeKey)
NodePublicKeyStripPrefix(registerRequest.NodeKey))
}
respBody, err := h.marshalResponse(resp, machineKey)
@@ -529,7 +524,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,11 +722,11 @@ 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, "/"),
registerRequest.NodeKey)
NodePublicKeyStripPrefix(registerRequest.NodeKey))
}
respBody, err := h.marshalResponse(resp, machineKey)

View File

@@ -451,7 +451,7 @@ func (h *Headscale) pollNetMapStream(
Time("last_successful_update", lastUpdate).
Time("last_state_change", h.getLastStateChange(machine.Namespace)).
Msgf("There has been updates since the last successful update to %s", machine.Hostname)
data, err := h.getMapResponseData(mapRequest, machine, isNoise)
data, err := h.getMapResponseData(mapRequest, machine, false)
if err != nil {
log.Error().
Str("handler", "PollNetMapStream").
@@ -622,7 +622,7 @@ func (h *Headscale) scheduledPollWorker(
defer closeChanWithLog(
keepAliveChan,
fmt.Sprint(ctx.Value(machineNameContextKey)),
"keepAliveChan",
"updateChan",
)
for {

View File

@@ -1,5 +1,3 @@
//go:build ts2019
package headscale
import (

View File

@@ -1,5 +1,3 @@
//go:build ts2019
package headscale
import (

View File

@@ -1,102 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h1>headscale</h1>
<h2>Apple configuration profiles</h2>
<p>
This page provides
<a href="https://support.apple.com/guide/mdm/mdm-overview-mdmbf9e668/web">
configuration profiles
</a>
for the official Tailscale clients for
</p>
<ul>
<li>
<a href="https://apps.apple.com/us/app/tailscale/id1470499037?ls=1"
>iOS</a
>
</li>
<li>
<a href="https://apps.apple.com/ca/app/tailscale/id1475387142?mt=12"
>macOS - AppStore Client</a
>.
</li>
<li>
<a href="https://pkgs.tailscale.com/stable/#macos"
>macOS - Standalone Client</a
>.
</li>
</ul>
<p>
The profiles will configure Tailscale.app to use <code>{{.URL}}</code> as
its control server.
</p>
<h3>Caution</h3>
<p>
You should always download and inspect the profile before installing it:
</p>
<!--
<pre><code>curl {{.URL}}/apple/ios</code></pre>
-->
<pre><code>curl {{.URL}}/apple/macos</code></pre>
<h2>Profiles</h2>
<!--
<h3>iOS</h3>
<p>
<a href="/apple/ios" download="headscale_ios.mobileconfig">iOS profile</a>
</p>
-->
<h3>macOS</h3>
<p>
Headscale can be set to the default server by installing a Headscale
configuration profile:
</p>
<p>
<a href="/apple/macos-app-store" download="headscale_macos.mobileconfig"
>macOS AppStore profile</a
>
<a href="/apple/macos-standalone" download="headscale_macos.mobileconfig"
>macOS Standalone profile</a
>
</p>
<ol>
<li>
Download the profile, then open it. When it has been opened, there
should be a notification that a profile can be installed
</li>
<li>Open System Preferences and go to "Profiles"</li>
<li>Find and install the Headscale profile</li>
<li>Restart Tailscale.app and log in</li>
</ol>
<p>Or</p>
<p>
Use your terminal to configure the default setting for Tailscale by
issuing:
</p>
<ul>
<li>
for app store client:
<code>defaults write io.tailscale.ipn.macos ControlURL {{.URL}}</code>
</li>
<li>
for standlone client:
<code>defaults write io.tailscale.ipn.macsys ControlURL {{.URL}}</code>
</li>
</ul>
<p>Restart Tailscale.app and log in.</p>
</body>
</html>

View File

@@ -1,64 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h1>headscale</h1>
<h2>Windows registry configuration</h2>
<p>
This page provides Windows registry information for the official Windows
Tailscale client.
</p>
<p></p>
<p>
The registry file will configure Tailscale to use <code>{{.URL}}</code> as
its control server.
</p>
<p></p>
<h3>Caution</h3>
<p>
You should always download and inspect the registry file before installing
it:
</p>
<pre><code>curl {{.URL}}/windows/tailscale.reg</code></pre>
<h2>Installation</h2>
<p>
Headscale can be set to the default server by running the registry file:
</p>
<p>
<a href="/windows/tailscale.reg" download="tailscale.reg"
>Windows registry file</a
>
</p>
<ol>
<li>Download the registry file, then run it</li>
<li>Follow the prompts</li>
<li>Install and run the official windows Tailscale client</li>
<li>
When the installation has finished, start Tailscale, and log in by
clicking the icon in the system tray
</li>
</ol>
<p>Or</p>
<p>
Open command prompt with Administrator rights. Issue the following
commands to add the required registry entries:
</p>
<pre>
<code>REG ADD "HKLM\Software\Tailscale IPN" /v UnattendedMode /t REG_SZ /d always
REG ADD "HKLM\Software\Tailscale IPN" /v LoginURL /t REG_SZ /d "{{.URL}}"</code></pre>
<p>Restart Tailscale and log in.</p>
<p></p>
</body>
</html>

View File

@@ -347,7 +347,7 @@ func IsStringInSlice(slice []string, str string) bool {
}
func AbsolutePathFromConfigPath(path string) string {
// If a relative path is provided, prefix it with the directory where
// If a relative path is provided, prefix it with the the directory where
// the config file was found.
if (path != "") && !strings.HasPrefix(path, string(os.PathSeparator)) {
dir, _ := filepath.Split(viper.ConfigFileUsed())