Compare commits

...

33 Commits

Author SHA1 Message Date
Juan Font Alonso
89ff5c83d2 Add web flow auth integration tests 2022-11-14 08:47:02 +01:00
Juan Font Alonso
0a47d694be Return the real port of the container 2022-11-14 08:47:02 +01:00
Juan Font Alonso
73c84d4f6a Print hostname of the machine registered 2022-11-14 08:47:02 +01:00
Juan Font Alonso
a9251d6652 Fixed linter issues 2022-11-13 22:33:41 +01:00
Juan Font Alonso
f9c44f11d6 Added method to run tailscale up without authkey 2022-11-13 22:33:41 +01:00
Juan Font Alonso
1f8bd24a0d Return stderr in tsic.Execute 2022-11-13 22:33:41 +01:00
Juan Font Alonso
7bf2eb3d71 Update Tailscale interface with new Execute signature 2022-11-13 22:33:41 +01:00
Juan Font Alonso
f5a5437917 disable interfacebloat linter 2022-11-13 18:30:00 +01:00
Juan Font Alonso
9989657c0f Wait for tailscale client to be ready after tailscale up 2022-11-13 18:30:00 +01:00
Juan Font Alonso
cb2790984f 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 18:30:00 +01:00
Juan Font Alonso
18c0009a51 Fix oidc.go linting issues
Signed-off-by: Juan Font Alonso <juanfontalonso@gmail.com>
2022-11-13 15:42:54 +01:00
Juan Font Alonso
d038df2a88 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 15:42:54 +01:00
Mesar Hameed
d8e9d95a3b config-example.yaml: fix typos and improve english. 2022-11-10 15:52:57 +00:00
Grigoriy Mikhalkin
0e405c7ce0 remove private key constant errors from NewHeadscale 2022-11-10 15:35:22 +00:00
Anton Schubert
21f0e089b6 fix noise mapResponse updates, fixes #838 2022-11-10 14:44:44 +00:00
kyra
cfda804726 Provide LoginName when registering with pre-auth key 2022-11-06 19:09:52 +01:00
github-actions[bot]
d6b383dd2f docs(README): update contributors 2022-11-05 16:03:17 +01:00
LiuHanCheng
07f92e647c fix bug in #912 (#914) 2022-11-05 09:07:22 +01:00
LiuHanCheng
bf87b33292 feat: add information to the /apple page for the macOS standalone client user (#915)
Co-authored-by: Kristoffer Dalby <kristoffer@dalby.cc>
2022-11-04 12:27:23 +01:00
Kristoffer Dalby
527b580f5e Add build flag to enable TS2019 (#928) 2022-11-04 11:26:33 +01:00
Kristoffer Dalby
c31328a54a Fix bitrotted versions in gh ci
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-04 10:46:23 +01:00
Kristoffer Dalby
b2c0e37122 Run on correct change
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-04 10:35:19 +01:00
Kristoffer Dalby
889223e35f 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:35:19 +01:00
Kristoffer Dalby
6e83b7f06b Give workflows better names
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-03 14:59:15 +01:00
Kristoffer Dalby
31d427b655 Run more tests in parallel
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-03 13:31:51 +01:00
Kristoffer Dalby
d8c856e602 Add basic accept all acl to all test as example
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-03 12:53:00 +01:00
Kristoffer Dalby
aad4c90fe6 Add options to hsic, ACL and env overrides
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-03 12:53:00 +01:00
Kristoffer Dalby
4f9fe93146 golangci-lint --fix
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-03 12:53:00 +01:00
Kristoffer Dalby
96fe6aa3a1 Remove unused func, comment out configobject way
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-03 12:53:00 +01:00
Kristoffer Dalby
947e961a3a Write headcsale config file from code, not depend on directory
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-03 12:53:00 +01:00
Kristoffer Dalby
43731cad2e Add helper function to add files to hs container
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-03 12:53:00 +01:00
Kristoffer Dalby
ac15b21720 Remove tab from YAML
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-03 12:53:00 +01:00
Kristoffer Dalby
dfc03a6124 Ditch stupid distroless image for debug/test
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-03 12:53:00 +01:00
44 changed files with 910 additions and 280 deletions

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
---
name: release
name: Release
on:
push:
@@ -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@v2
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v3
with:
go-version: 1.19.0
@@ -37,7 +37,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Docker Buildx
@@ -100,7 +100,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Docker Buildx
@@ -166,7 +166,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Docker Buildx

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,24 +1,19 @@
name: CI
name: Integration Test v2 - kradalby
on: [pull_request]
jobs:
integration-test-v2-general:
runs-on: ubuntu-latest
integration-test-v2-kradalby:
runs-on: [self-hosted, linux, x64, nixos, docker]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
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
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
@@ -27,9 +22,6 @@ jobs:
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_general

View File

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

View File

@@ -1,6 +1,8 @@
---
run:
timeout: 10m
build-tags:
- ts2019
issues:
skip-dirs:
@@ -26,6 +28,7 @@ linters:
- ireturn
- execinquery
- exhaustruct
- nolintlint
# We should strive to enable these:
- wrapcheck
@@ -33,6 +36,9 @@ 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,6 +20,8 @@ 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
@@ -34,6 +36,8 @@ 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 }}"
@@ -46,6 +50,8 @@ 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 }}"
@@ -58,6 +64,8 @@ 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

@@ -24,6 +24,7 @@
- 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)
## 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 -ldflags="-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$VERSION" -a ./cmd/headscale
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 strip /go/bin/headscale
RUN test -e /go/bin/headscale

View File

@@ -9,11 +9,11 @@ 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 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 test -e /go/bin/headscale
# Debug image
FROM gcr.io/distroless/base-debian11:debug
FROM docker.io/golang:1.19.0-bullseye
COPY --from=build /go/bin/headscale /bin/headscale
ENV TZ UTC

View File

@@ -10,6 +10,8 @@ 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)
@@ -17,12 +19,12 @@ PROTO_SOURCES = $(call rwildcard,,*.proto)
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
nix build
dev: lint test build
test:
@go test -short -coverprofile=coverage.out ./...
@go test $(TAGS) -short -coverprofile=coverage.out ./...
test_integration: test_integration_cli test_integration_derp test_integration_oidc test_integration_v2_general
@@ -34,7 +36,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 -failfast -timeout 30m -count=1 -run IntegrationCLI ./...
go test $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationCLI ./...
test_integration_derp:
docker network rm $$(docker network ls --filter name=headscale --quiet) || true
@@ -44,7 +46,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 -failfast -timeout 30m -count=1 -run IntegrationDERP ./...
go test $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationDERP ./...
test_integration_oidc:
docker network rm $$(docker network ls --filter name=headscale --quiet) || true
@@ -54,7 +56,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 -failfast -timeout 30m -count=1 -run IntegrationOIDC ./...
go test $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationOIDC ./...
test_integration_v2_general:
docker run \
@@ -64,7 +66,7 @@ test_integration_v2_general:
-v $$PWD:$$PWD -w $$PWD/integration \
-v /var/run/docker.sock:/var/run/docker.sock \
golang:1 \
go test ./... -timeout 60m
go test $(TAGS) -failfast ./... -timeout 60m -parallel 6
coverprofile_func:
go tool cover -func=coverage.out

View File

@@ -424,6 +424,13 @@ 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/>
@@ -439,7 +446,7 @@ make build
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/vtrf>
<a href=https://github.com/ratsclub>
<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>
@@ -459,6 +466,8 @@ 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/>
@@ -466,8 +475,13 @@ make build
<sub style="font-size:14px"><b>Abraham Ingersoll</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/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>
<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/>
@@ -496,6 +510,8 @@ 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/>
@@ -510,8 +526,6 @@ 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/>
@@ -540,6 +554,8 @@ 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/>
@@ -554,8 +570,6 @@ 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/>
@@ -579,11 +593,13 @@ 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=WhiteSource 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=Mend Renovate/>
<br />
<sub style="font-size:14px"><b>WhiteSource Renovate</b></sub>
<sub style="font-size:14px"><b>Mend 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/>
@@ -598,8 +614,6 @@ 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/>
@@ -628,6 +642,8 @@ 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/>
@@ -642,8 +658,6 @@ 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/>
@@ -672,6 +686,8 @@ 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/>
@@ -686,8 +702,6 @@ 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/>
@@ -716,6 +730,8 @@ 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/>
@@ -730,8 +746,6 @@ 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,12 +51,6 @@ 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 (
@@ -131,17 +125,17 @@ func LookupTLSClientAuthMode(mode string) (tls.ClientAuthType, bool) {
func NewHeadscale(cfg *Config) (*Headscale, error) {
privateKey, err := readOrCreatePrivateKey(cfg.PrivateKeyPath)
if err != nil {
return nil, ErrFailedPrivateKey
return nil, fmt.Errorf("failed to read or create private key: %w", err)
}
// TS2021 requires to have a different key from the legacy protocol.
noisePrivateKey, err := readOrCreatePrivateKey(cfg.NoisePrivateKeyPath)
if err != nil {
return nil, ErrFailedNoisePrivateKey
return nil, fmt.Errorf("failed to read or create Noise protocol private key: %w", err)
}
if privateKey.Equal(*noisePrivateKey) {
return nil, ErrSamePrivateKeys
return nil, fmt.Errorf("private key and noise private key are the same: %w", err)
}
var dbString string
@@ -454,9 +448,8 @@ 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)
router.HandleFunc("/machine/{mkey}/map", h.PollNetMapHandler).
Methods(http.MethodPost)
router.HandleFunc("/machine/{mkey}", h.RegistrationHandler).Methods(http.MethodPost)
h.addLegacyHandlers(router)
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,6 +3,7 @@ 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"
@@ -10,8 +11,7 @@ import (
)
const (
keyLength = 64
errPreAuthKeyTooShort = Error("key too short, must be 64 hexadecimal characters")
errPreAuthKeyMalformed = Error("key is malformed. expected 64 hex characters with `nodekey` prefix")
)
// 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 len(machineKey) != keyLength {
err = errPreAuthKeyTooShort
if !headscale.NodePublicKeyRegex.Match([]byte(machineKey)) {
err = errPreAuthKeyMalformed
ErrorOutput(
err,
fmt.Sprintf("Error: %s", err),

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
//nolint
// nolint
package headscale
import (
@@ -12,6 +12,7 @@ import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
type headscaleV1APIServer struct { // v1.HeadscaleServiceServer
@@ -496,9 +497,14 @@ 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(
request.GetKey(),
NodePublicKeyStripPrefix(nodeKey),
newMachine,
registerCacheExpiration,
)

15
handler_legacy.go Normal file
View File

@@ -0,0 +1,15 @@
//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)
}

8
handler_placeholder.go Normal file
View File

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

View File

@@ -0,0 +1,192 @@
package integration
import (
"context"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/url"
"strings"
"testing"
)
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)
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) error {
err := s.StartHeadscale()
if err != nil {
return err
}
err = s.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, s.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 {
log.Printf("loginURL: %s", loginURL)
loginURL.Host = fmt.Sprintf("%s:8080", s.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, 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)
return err
}
return nil
}
return fmt.Errorf("failed to find headscale: %w", errNoHeadscaleAvailable)
}

View File

@@ -168,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)
}
@@ -193,7 +193,7 @@ func TestTaildrop(t *testing.T) {
client.Hostname(),
peer.Hostname(),
)
_, err := client.Execute(command)
_, _, err := client.Execute(command)
return err
})
@@ -214,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)
}
@@ -234,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)
}
@@ -306,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

@@ -0,0 +1,97 @@
package hsic
// const (
// defaultEphemeralNodeInactivityTimeout = time.Second * 30
// defaultNodeUpdateCheckInterval = time.Second * 10
// )
// TODO(kradalby): This approach doesnt work because we cannot
// serialise our config object to YAML or JSON.
// func DefaultConfig() headscale.Config {
// derpMap, _ := url.Parse("https://controlplane.tailscale.com/derpmap/default")
//
// config := headscale.Config{
// Log: headscale.LogConfig{
// Level: zerolog.TraceLevel,
// },
// ACL: headscale.GetACLConfig(),
// DBtype: "sqlite3",
// EphemeralNodeInactivityTimeout: defaultEphemeralNodeInactivityTimeout,
// NodeUpdateCheckInterval: defaultNodeUpdateCheckInterval,
// IPPrefixes: []netip.Prefix{
// netip.MustParsePrefix("fd7a:115c:a1e0::/48"),
// netip.MustParsePrefix("100.64.0.0/10"),
// },
// DNSConfig: &tailcfg.DNSConfig{
// Proxied: true,
// Nameservers: []netip.Addr{
// netip.MustParseAddr("127.0.0.11"),
// netip.MustParseAddr("1.1.1.1"),
// },
// Resolvers: []*dnstype.Resolver{
// {
// Addr: "127.0.0.11",
// },
// {
// Addr: "1.1.1.1",
// },
// },
// },
// BaseDomain: "headscale.net",
//
// DBpath: "/tmp/integration_test_db.sqlite3",
//
// PrivateKeyPath: "/tmp/integration_private.key",
// NoisePrivateKeyPath: "/tmp/noise_integration_private.key",
// Addr: "0.0.0.0:8080",
// MetricsAddr: "127.0.0.1:9090",
// ServerURL: "http://headscale:8080",
//
// DERP: headscale.DERPConfig{
// URLs: []url.URL{
// *derpMap,
// },
// AutoUpdate: false,
// UpdateFrequency: 1 * time.Minute,
// },
// }
//
// return config
// }
// TODO: Reuse the actual configuration object above.
func DefaultConfigYAML() string {
yaml := `
log:
level: trace
acl_policy_path: ""
db_type: sqlite3
db_path: /tmp/integration_test_db.sqlite3
ephemeral_node_inactivity_timeout: 30m
node_update_check_interval: 10s
ip_prefixes:
- fd7a:115c:a1e0::/48
- 100.64.0.0/10
dns_config:
base_domain: headscale.net
magic_dns: true
domains: []
nameservers:
- 127.0.0.11
- 1.1.1.1
private_key_path: /tmp/private.key
noise:
private_key_path: /tmp/noise_private.key
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 127.0.0.1:9090
server_url: http://headscale:8080
derp:
urls:
- https://controlplane.tailscale.com/derpmap/default
auto_update_enabled: false
update_frequency: 1m
`
return yaml
}

View File

@@ -1,23 +1,27 @@
package hsic
import (
"archive/tar"
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"path"
"path/filepath"
"github.com/juanfont/headscale"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/juanfont/headscale/integration/dockertestutil"
"github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker"
)
const (
hsicHashLength = 6
dockerContextPath = "../."
aclPolicyPath = "/etc/headscale/acl.hujson"
)
var errHeadscaleStatusCodeNotOk = errors.New("headscale status code not ok")
@@ -29,44 +33,76 @@ type HeadscaleInContainer struct {
pool *dockertest.Pool
container *dockertest.Resource
network *dockertest.Network
// optional config
aclPolicy *headscale.ACLPolicy
env []string
}
type Option = func(c *HeadscaleInContainer)
func WithACLPolicy(acl *headscale.ACLPolicy) Option {
return func(hsic *HeadscaleInContainer) {
hsic.aclPolicy = acl
}
}
func WithConfigEnv(configEnv map[string]string) Option {
return func(hsic *HeadscaleInContainer) {
env := []string{}
for key, value := range configEnv {
env = append(env, fmt.Sprintf("%s=%s", key, value))
}
hsic.env = env
}
}
func New(
pool *dockertest.Pool,
port int,
network *dockertest.Network,
opts ...Option,
) (*HeadscaleInContainer, error) {
hash, err := headscale.GenerateRandomStringDNSSafe(hsicHashLength)
if err != nil {
return nil, err
}
headscaleBuildOptions := &dockertest.BuildOptions{
Dockerfile: "Dockerfile",
ContextDir: dockerContextPath,
}
hostname := fmt.Sprintf("hs-%s", hash)
portProto := fmt.Sprintf("%d/tcp", port)
currentPath, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("could not determine current path: %w", err)
hsic := &HeadscaleInContainer{
hostname: hostname,
port: port,
pool: pool,
network: network,
}
integrationConfigPath := path.Join(currentPath, "..", "integration_test", "etc")
for _, opt := range opts {
opt(hsic)
}
if hsic.aclPolicy != nil {
hsic.env = append(hsic.env, fmt.Sprintf("HEADSCALE_ACL_POLICY_PATH=%s", aclPolicyPath))
}
headscaleBuildOptions := &dockertest.BuildOptions{
Dockerfile: "Dockerfile.debug",
ContextDir: dockerContextPath,
}
runOptions := &dockertest.RunOptions{
Name: hostname,
// TODO(kradalby): Do something clever here, can we ditch the config repo?
// Always generate the config from code?
Mounts: []string{
fmt.Sprintf("%s:/etc/headscale", integrationConfigPath),
},
Name: hostname,
ExposedPorts: []string{portProto},
// TODO(kradalby): WHY do we need to bind these now that we run fully in docker?
Networks: []*dockertest.Network{network},
Cmd: []string{"headscale", "serve"},
Networks: []*dockertest.Network{network},
// Cmd: []string{"headscale", "serve"},
// TODO(kradalby): Get rid of this hack, we currently need to give us some
// to inject the headscale configuration further down.
Entrypoint: []string{"/bin/bash", "-c", "/bin/sleep 3 ; headscale serve"},
Env: hsic.env,
}
// dockertest isnt very good at handling containers that has already
@@ -89,14 +125,26 @@ func New(
}
log.Printf("Created %s container\n", hostname)
return &HeadscaleInContainer{
hostname: hostname,
port: port,
hsic.container = container
pool: pool,
container: container,
network: network,
}, nil
err = hsic.WriteFile("/etc/headscale/config.yaml", []byte(DefaultConfigYAML()))
if err != nil {
return nil, fmt.Errorf("failed to write headscale config to container: %w", err)
}
if hsic.aclPolicy != nil {
data, err := json.Marshal(hsic.aclPolicy)
if err != nil {
return nil, fmt.Errorf("failed to marshal ACL Policy to JSON: %w", err)
}
err = hsic.WriteFile(aclPolicyPath, data)
if err != nil {
return nil, fmt.Errorf("failed to write ACL policy to container: %w", err)
}
}
return hsic, nil
}
func (t *HeadscaleInContainer) Shutdown() error {
@@ -131,9 +179,7 @@ func (t *HeadscaleInContainer) GetIP() string {
}
func (t *HeadscaleInContainer) GetPort() string {
portProto := fmt.Sprintf("%d/tcp", t.port)
return t.container.GetPort(portProto)
return fmt.Sprintf("%d", t.port)
}
func (t *HeadscaleInContainer) GetHealthEndpoint() string {
@@ -244,3 +290,57 @@ func (t *HeadscaleInContainer) ListMachinesInNamespace(
return nodes, nil
}
func (t *HeadscaleInContainer) WriteFile(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 = 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 err
}
return nil
}

View File

@@ -55,8 +55,7 @@ type Namespace struct {
syncWaitGroup sync.WaitGroup
}
// TODO(kradalby): make control server configurable, test test correctness with
// Tailscale SaaS.
// TODO(kradalby): make control server configurable, test correctness with Tailscale SaaS.
type Scenario struct {
// TODO(kradalby): support multiple headcales for later, currently only
// use one.
@@ -152,7 +151,19 @@ func (s *Scenario) Namespaces() []string {
// TODO(kradalby): make port and headscale configurable, multiple instances support?
func (s *Scenario) StartHeadscale() error {
headscale, err := hsic.New(s.pool, headscalePort, s.network)
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 fmt.Errorf("failed to create headscale container: %w", err)
}
@@ -247,7 +258,13 @@ 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

View File

@@ -2,6 +2,7 @@ package integration
import (
"net/netip"
"net/url"
"tailscale.com/ipn/ipnstate"
)
@@ -10,11 +11,13 @@ type TailscaleClient interface {
Hostname() string
Shutdown() error
Version() string
Execute(command []string) (string, error)
Execute(command []string) (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,6 +6,7 @@ import (
"fmt"
"log"
"net/netip"
"net/url"
"strings"
"github.com/cenkalti/backoff/v4"
@@ -22,9 +23,11 @@ const (
)
var (
errTailscalePingFailed = errors.New("ping failed")
errTailscaleNotLoggedIn = errors.New("tailscale not logged in")
errTailscaleWrongPeerCount = errors.New("wrong peer count")
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")
)
type TailscaleInContainer struct {
@@ -110,7 +113,7 @@ func (t *TailscaleInContainer) Version() string {
func (t *TailscaleInContainer) Execute(
command []string,
) (string, error) {
) (string, string, error) {
log.Println("command", command)
log.Printf("running command for %s\n", t.hostname)
stdout, stderr, err := dockertestutil.ExecuteCommand(
@@ -126,13 +129,13 @@ func (t *TailscaleInContainer) Execute(
}
if strings.Contains(stderr, "NeedsLogin") {
return "", errTailscaleNotLoggedIn
return stdout, stderr, errTailscaleNotLoggedIn
}
return "", err
return stdout, stderr, err
}
return stdout, nil
return stdout, stderr, nil
}
func (t *TailscaleInContainer) Up(
@@ -149,13 +152,45 @@ 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
@@ -168,7 +203,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)
}
@@ -195,7 +230,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)
}
@@ -222,6 +257,21 @@ 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()
@@ -248,7 +298,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",

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{
"9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
"6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
"nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
"nodekey: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{
"9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
"6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
"f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
"8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1",
"cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
"nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
"nodekey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
"nodekey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
"nodekey:8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1",
"nodekey: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{
"b5b444774186d4217adcec407563a1223929465ee2c68a4da13af0d0185b4f8e",
"dc721977ac7415aafa87f7d4574cbe07c6b171834a6d37375782bdc1fb6b3584",
"nodekey:b5b444774186d4217adcec407563a1223929465ee2c68a4da13af0d0185b4f8e",
"nodekey: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{
"9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
"6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
"f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
"8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1",
"cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
"nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
"nodekey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
"nodekey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
"nodekey:8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1",
"nodekey: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{
"cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
"8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1",
"f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
"6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
"9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
"nodekey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
"nodekey:8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1",
"nodekey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
"nodekey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
"nodekey: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 := "9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe"
machineKey := "nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe"
_, _, err = ExecuteCommand(
&s.headscale,
@@ -1588,7 +1588,7 @@ func (s *IntegrationCLITestSuite) TestNodeMoveCommand() {
assert.Nil(s.T(), err)
// Randomly generated machine key
machineKey := "688411b767663479632d44140f08a9fde87383adc7cdeb518f62ce28a17ef0aa"
machineKey := "nodekey:688411b767663479632d44140f08a9fde87383adc7cdeb518f62ce28a17ef0aa"
_, _, err = ExecuteCommand(
&s.headscale,

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ package headscale
import (
"bytes"
_ "embed"
"html/template"
"net/http"
textTemplate "text/template"
@@ -11,51 +12,18 @@ 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(`
<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>
`))
winTemplate := template.Must(template.New("windows").Parse(windowsTemplate))
config := map[string]interface{}{
"URL": h.cfg.ServerURL,
}
@@ -136,55 +104,7 @@ func (h *Headscale) AppleConfigMessage(
writer http.ResponseWriter,
req *http.Request,
) {
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>`))
appleTemplate := template.Must(template.New("apple").Parse(appleTemplate))
config := map[string]interface{}{
"URL": h.cfg.ServerURL,
@@ -282,24 +202,33 @@ 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":
if err := macosTemplate.Execute(&payload, platformConfig); err != nil {
log.Error().
Str("handler", "ApplePlatformConfig").
Err(err).
Msg("Could not render Apple macOS template")
case "macos-standlone":
if err := macosStandloneTemplate.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
}
case "macos-app-store":
if err := macosAppStoreTemplate.Execute(&payload, platformConfig); err != nil {
handleMacError(err)
return
}
@@ -444,7 +373,7 @@ var iosTemplate = textTemplate.Must(textTemplate.New("iosTemplate").Parse(`
</dict>
`))
var macosTemplate = template.Must(template.New("macosTemplate").Parse(`
var macosAppStoreTemplate = template.Must(template.New("macosTemplate").Parse(`
<dict>
<key>PayloadType</key>
<string>io.tailscale.ipn.macos</string>
@@ -456,7 +385,23 @@ var macosTemplate = 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,6 +435,10 @@ 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,12 +494,12 @@ func (h *Headscale) handleNewMachineCommon(
resp.AuthURL = fmt.Sprintf(
"%s/oidc/register/%s",
strings.TrimSuffix(h.cfg.ServerURL, "/"),
NodePublicKeyStripPrefix(registerRequest.NodeKey),
registerRequest.NodeKey,
)
} else {
resp.AuthURL = fmt.Sprintf("%s/register/%s",
strings.TrimSuffix(h.cfg.ServerURL, "/"),
NodePublicKeyStripPrefix(registerRequest.NodeKey))
registerRequest.NodeKey)
}
respBody, err := h.marshalResponse(resp, machineKey)
@@ -726,7 +730,7 @@ func (h *Headscale) handleMachineExpiredCommon(
} else {
resp.AuthURL = fmt.Sprintf("%s/register/%s",
strings.TrimSuffix(h.cfg.ServerURL, "/"),
NodePublicKeyStripPrefix(registerRequest.NodeKey))
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, false)
data, err := h.getMapResponseData(mapRequest, machine, isNoise)
if err != nil {
log.Error().
Str("handler", "PollNetMapStream").
@@ -622,7 +622,7 @@ func (h *Headscale) scheduledPollWorker(
defer closeChanWithLog(
keepAliveChan,
fmt.Sprint(ctx.Value(machineNameContextKey)),
"updateChan",
"keepAliveChan",
)
for {

View File

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

View File

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

102
templates/apple.html Normal file
View File

@@ -0,0 +1,102 @@
<!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>

64
templates/windows.html Normal file
View File

@@ -0,0 +1,64 @@
<!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 the directory where
// If a relative path is provided, prefix it with the directory where
// the config file was found.
if (path != "") && !strings.HasPrefix(path, string(os.PathSeparator)) {
dir, _ := filepath.Split(viper.ConfigFileUsed())