Compare commits

..

38 Commits

Author SHA1 Message Date
Juan Font
613d29478d Return the correct error on cache miss 2022-11-14 16:50:43 +01:00
Juan Font
4034fbc6e9 OIDC code cleanup and harmonize with regular web auth 2022-11-14 16:50:43 +01:00
Kristoffer Dalby
46df219ed3 Add testname identifier to hs container
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-14 16:50:28 +01:00
Kristoffer Dalby
835288d864 Remove unused variable
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-14 16:50:28 +01:00
Kristoffer Dalby
93d56362af Lock and unify headscale start/get method
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-14 16:50:28 +01:00
Kristoffer Dalby
4799859be0 Fix renamed method
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-14 16:50:28 +01:00
Kristoffer Dalby
8e44596171 less verbose command output
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-14 16:50:28 +01:00
Kristoffer Dalby
d479234058 Split ts versions into 2019/2021 for dedicated tests later
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-14 16:50:28 +01:00
Kristoffer Dalby
3fc5866de0 Remove duplicate function
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-14 16:50:28 +01:00
Kristoffer Dalby
f3c40086ac Make TLS setup work automatically
This commit injects the per-test-generated tls certs into the tailscale
container and makes sure all can ping all. It does not test any of the
DERP isolation yet.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-14 16:50:28 +01:00
Kristoffer Dalby
09ed21edd8 Remove duplicate function
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-14 16:50:28 +01:00
Kristoffer Dalby
456479eaa1 Rename and move wait for headscale
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-14 16:50:28 +01:00
Kristoffer Dalby
cb87852825 Add nolint to gosec stuff that doesnt matter because test
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-14 16:50:28 +01:00
Kristoffer Dalby
69440058bb Clean up cert function
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-14 16:50:28 +01:00
Kristoffer Dalby
9bc6ac0f35 Make TLS setup work automatically
This commit injects the per-test-generated tls certs into the tailscale
container and makes sure all can ping all. It does not test any of the
DERP isolation yet.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-14 16:50:28 +01:00
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
47 changed files with 1200 additions and 421 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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -13,7 +13,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

@@ -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,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 test -e /go/bin/headscale
# Debug image

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

View File

@@ -13,4 +13,7 @@ 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,6 +6,7 @@ import (
"testing"
"time"
"github.com/juanfont/headscale/integration/hsic"
"github.com/rs/zerolog/log"
)
@@ -22,7 +23,7 @@ func TestPingAllByIP(t *testing.T) {
"namespace2": len(TailscaleVersions),
}
err = scenario.CreateHeadscaleEnv(spec)
err = scenario.CreateHeadscaleEnv(spec, hsic.WithTestName("pingallbyip"))
if err != nil {
t.Errorf("failed to create headscale environment: %s", err)
}
@@ -77,7 +78,7 @@ func TestPingAllByHostname(t *testing.T) {
"namespace4": len(TailscaleVersions) - 1,
}
err = scenario.CreateHeadscaleEnv(spec)
err = scenario.CreateHeadscaleEnv(spec, hsic.WithTestName("pingallbyname"))
if err != nil {
t.Errorf("failed to create headscale environment: %s", err)
}
@@ -144,7 +145,7 @@ func TestTaildrop(t *testing.T) {
"taildrop": len(TailscaleVersions) - 1,
}
err = scenario.CreateHeadscaleEnv(spec)
err = scenario.CreateHeadscaleEnv(spec, hsic.WithTestName("taildrop"))
if err != nil {
t.Errorf("failed to create headscale environment: %s", err)
}
@@ -168,7 +169,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 +194,7 @@ func TestTaildrop(t *testing.T) {
client.Hostname(),
peer.Hostname(),
)
_, err := client.Execute(command)
_, _, err := client.Execute(command)
return err
})
@@ -214,7 +215,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 +235,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)
}
@@ -271,7 +272,7 @@ func TestResolveMagicDNS(t *testing.T) {
"magicdns2": len(TailscaleVersions) - 1,
}
err = scenario.CreateHeadscaleEnv(spec)
err = scenario.CreateHeadscaleEnv(spec, hsic.WithTestName("magicdns"))
if err != nil {
t.Errorf("failed to create headscale environment: %s", err)
}
@@ -306,7 +307,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,52 +1,83 @@
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"
"path/filepath"
"time"
"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"
hsicHashLength = 6
dockerContextPath = "../."
aclPolicyPath = "/etc/headscale/acl.hujson"
tlsCertPath = "/etc/headscale/tls.cert"
tlsKeyPath = "/etc/headscale/tls.key"
headscaleDefaultPort = 8080
)
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{}
@@ -59,9 +90,23 @@ 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) {
@@ -71,11 +116,10 @@ func New(
}
hostname := fmt.Sprintf("hs-%s", hash)
portProto := fmt.Sprintf("%d/tcp", port)
hsic := &HeadscaleInContainer{
hostname: hostname,
port: port,
port: headscaleDefaultPort,
pool: pool,
network: network,
@@ -85,9 +129,9 @@ func New(
opt(hsic)
}
if hsic.aclPolicy != nil {
hsic.env = append(hsic.env, fmt.Sprintf("HEADSCALE_ACL_POLICY_PATH=%s", aclPolicyPath))
}
log.Println("NAME: ", hsic.hostname)
portProto := fmt.Sprintf("%d/tcp", hsic.port)
headscaleBuildOptions := &dockertest.BuildOptions{
Dockerfile: "Dockerfile.debug",
@@ -95,7 +139,7 @@ func New(
}
runOptions := &dockertest.RunOptions{
Name: hostname,
Name: hsic.hostname,
ExposedPorts: []string{portProto},
Networks: []*dockertest.Network{network},
// Cmd: []string{"headscale", "serve"},
@@ -108,7 +152,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(hostname)
err = pool.RemoveContainerByName(hsic.hostname)
if err != nil {
return nil, err
}
@@ -123,7 +167,7 @@ func New(
if err != nil {
return nil, fmt.Errorf("could not start headscale container: %w", err)
}
log.Printf("Created %s container\n", hostname)
log.Printf("Created %s container\n", hsic.hostname)
hsic.container = container
@@ -144,9 +188,25 @@ 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)
}
@@ -154,8 +214,6 @@ 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,
@@ -164,11 +222,11 @@ func (t *HeadscaleInContainer) Execute(
if err != nil {
log.Printf("command stderr: %s\n", stderr)
return "", err
}
if stdout != "" {
log.Printf("command stdout: %s\n", stdout)
}
if stdout != "" {
log.Printf("command stdout: %s\n", stdout)
return "", err
}
return stdout, nil
@@ -179,17 +237,11 @@ 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 {
hostEndpoint := fmt.Sprintf("%s:%d",
t.GetIP(),
t.port)
return fmt.Sprintf("http://%s/health", hostEndpoint)
return fmt.Sprintf("%s/health", t.GetEndpoint())
}
func (t *HeadscaleInContainer) GetEndpoint() string {
@@ -197,17 +249,39 @@ 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 := http.Get(url) //nolint
resp, err := client.Get(url) //nolint
if err != nil {
log.Printf("ready err: %s", err)
return fmt.Errorf("headscale is not ready: %w", err)
}
@@ -294,55 +368,87 @@ func (t *HeadscaleInContainer) ListMachinesInNamespace(
}
func (t *HeadscaleInContainer) WriteFile(path string, data []byte) error {
dirPath, fileName := filepath.Split(path)
return integrationutil.WriteFileToContainer(t.pool, t.container, path, data)
}
file := bytes.NewReader(data)
//nolint
func createCertificate() ([]byte, []byte, error) {
// From:
// https://shaneutt.com/blog/golang-ca-and-signed-cert-go/
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()),
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,
}
caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return nil, nil, 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)
if err != nil {
return nil, nil, err
}
certBytes, err := x509.CreateCertificate(
rand.Reader,
cert,
ca,
&certPrivKey.PublicKey,
caPrivKey,
)
if err != nil {
return err
return nil, nil, err
}
return nil
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
}

View File

@@ -0,0 +1,77 @@
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,36 +15,46 @@ 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")
TailscaleVersions = []string{
tailscaleVersions2021 = []string{
"head",
"unstable",
"1.32.1",
"1.30.2",
"1.28.0",
"1.26.2",
}
tailscaleVersions2019 = []string{
"1.24.2",
"1.22.2",
"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 {
@@ -59,12 +69,14 @@ type Namespace struct {
type Scenario struct {
// TODO(kradalby): support multiple headcales for later, currently only
// use one.
controlServers map[string]ControlServer
controlServers *xsync.MapOf[string, ControlServer]
namespaces map[string]*Namespace
pool *dockertest.Pool
network *dockertest.Network
headscaleLock sync.Mutex
}
func NewScenario() (*Scenario, error) {
@@ -99,7 +111,7 @@ func NewScenario() (*Scenario, error) {
}
return &Scenario{
controlServers: make(map[string]ControlServer),
controlServers: xsync.NewMapOf[ControlServer](),
namespaces: make(map[string]*Namespace),
pool: pool,
@@ -108,12 +120,17 @@ func NewScenario() (*Scenario, error) {
}
func (s *Scenario) Shutdown() error {
for _, control := range s.controlServers {
s.controlServers.Range(func(_ string, control ControlServer) bool {
err := control.Shutdown()
if err != nil {
return fmt.Errorf("failed to tear down control: %w", err)
log.Printf(
"Failed to shut down control: %s",
fmt.Errorf("failed to tear down control: %w", err),
)
}
}
return true
})
for namespaceName, namespace := range s.namespaces {
for _, client := range namespace.Clients {
@@ -150,36 +167,31 @@ 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) 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 fmt.Errorf("failed to create headscale container: %w", err)
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
}
s.controlServers["headscale"] = headscale
headscale, err := hsic.New(s.pool, s.network, opts...)
if err != nil {
return nil, fmt.Errorf("failed to create headscale container: %w", err)
}
return nil
}
err = headscale.WaitForReady()
if err != nil {
return nil, fmt.Errorf("failed reach headscale container: %w", err)
}
func (s *Scenario) Headscale() *hsic.HeadscaleInContainer {
//nolint
return s.controlServers["headscale"].(*hsic.HeadscaleInContainer)
s.controlServers.Store("headscale", headscale)
return headscale, nil
}
func (s *Scenario) CreatePreAuthKey(namespace string) (*v1.PreAuthKey, error) {
if headscale, ok := s.controlServers["headscale"]; ok {
if headscale, err := s.Headscale(); err == nil {
key, err := headscale.CreateAuthKey(namespace)
if err != nil {
return nil, fmt.Errorf("failed to create namespace: %w", err)
@@ -192,7 +204,7 @@ func (s *Scenario) CreatePreAuthKey(namespace string) (*v1.PreAuthKey, error) {
}
func (s *Scenario) CreateNamespace(namespace string) error {
if headscale, ok := s.controlServers["headscale"]; ok {
if headscale, err := s.Headscale(); err == nil {
err := headscale.CreateNamespace(namespace)
if err != nil {
return fmt.Errorf("failed to create namespace: %w", err)
@@ -222,16 +234,36 @@ 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)
tsClient, err := tsic.New(
s.pool,
version,
s.network,
tsic.WithHeadscaleTLS(cert),
tsic.WithHeadscaleName(hostname),
)
if err != nil {
// return fmt.Errorf("failed to add tailscale node: %w", err)
log.Printf("failed to add tailscale node: %s", 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)
}
namespace.Clients[tsClient.Hostname()] = tsClient
@@ -258,7 +290,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
@@ -300,13 +338,8 @@ 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) error {
err := s.StartHeadscale()
if err != nil {
return err
}
err = s.Headscale().WaitForReady()
func (s *Scenario) CreateHeadscaleEnv(namespaces map[string]int, opts ...hsic.Option) error {
headscale, err := s.Headscale(opts...)
if err != nil {
return err
}
@@ -327,7 +360,7 @@ func (s *Scenario) CreateHeadscaleEnv(namespaces map[string]int) error {
return err
}
err = s.RunTailscaleUp(namespaceName, s.Headscale().GetEndpoint(), key.GetKey())
err = s.RunTailscaleUp(namespaceName, 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) {
err = scenario.StartHeadscale()
headscale, err := scenario.Headscale()
if err != nil {
t.Errorf("failed to create start headcale: %s", err)
}
err = scenario.Headscale().WaitForReady()
err = headscale.WaitForReady()
if err != nil {
t.Errorf("headscale failed to become ready: %s", err)
}
@@ -117,12 +117,11 @@ func TestTailscaleNodesJoiningHeadcale(t *testing.T) {
}
t.Run("start-headscale", func(t *testing.T) {
err = scenario.StartHeadscale()
headscale, err := scenario.Headscale()
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)
@@ -157,7 +156,16 @@ func TestTailscaleNodesJoiningHeadcale(t *testing.T) {
t.Errorf("failed to create preauthkey: %s", err)
}
err = scenario.RunTailscaleUp(namespace, scenario.Headscale().GetEndpoint(), key.GetKey())
headscale, err := scenario.Headscale()
if err != nil {
t.Errorf("failed to create start headcale: %s", err)
}
err = scenario.RunTailscaleUp(
namespace,
headscale.GetEndpoint(),
key.GetKey(),
)
if err != nil {
t.Errorf("failed to login: %s", err)
}

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,11 +6,13 @@ 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"
@@ -19,12 +21,15 @@ 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")
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 {
@@ -38,12 +43,51 @@ 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 {
@@ -52,20 +96,38 @@ func New(
hostname := fmt.Sprintf("ts-%s-%s", strings.ReplaceAll(version, ".", "-"), hash)
// 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
// }
tsic := &TailscaleInContainer{
version: version,
hostname: hostname,
pool: pool,
network: network,
}
for _, opt := range opts {
opt(tsic)
}
tailscaleOptions := &dockertest.RunOptions{
Name: hostname,
Networks: []*dockertest.Network{network},
Cmd: []string{
"tailscaled", "--tun=tsdev",
// Cmd: []string{
// "tailscaled", "--tun=tsdev",
// },
Entrypoint: []string{
"/bin/bash",
"-c",
"/bin/sleep 3 ; update-ca-certificates ; 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.
@@ -86,14 +148,20 @@ func New(
}
log.Printf("Created %s container\n", hostname)
return &TailscaleInContainer{
version: version,
hostname: hostname,
tsic.container = container
pool: pool,
container: container,
network: network,
}, nil
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
}
func (t *TailscaleInContainer) Shutdown() error {
@@ -110,9 +178,7 @@ func (t *TailscaleInContainer) Version() string {
func (t *TailscaleInContainer) Execute(
command []string,
) (string, error) {
log.Println("command", command)
log.Printf("running command for %s\n", t.hostname)
) (string, string, error) {
stdout, stderr, err := dockertestutil.ExecuteCommand(
t.container,
command,
@@ -126,13 +192,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 +215,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 +266,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 +293,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 +320,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 +361,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",
@@ -268,6 +381,10 @@ 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{
"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 {

97
oidc.go
View File

@@ -76,20 +76,52 @@ 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)
return
}
log.Trace().
Caller().
Str("node_key", nodeKeyStr).
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")
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().
@@ -103,7 +135,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, nodeKeyStr, registerCacheExpiration)
h.registrationCache.Set(stateStr, NodePublicKeyStripPrefix(nodeKey), registerCacheExpiration)
// Add any extra parameter provided in the configuration to the Authorize Endpoint request
extras := make([]oauth2.AuthCodeOption, 0, len(h.cfg.OIDC.ExtraParams))
@@ -405,8 +437,8 @@ func (h *Headscale) validateMachineForOIDCCallback(
claims *IDTokenClaims,
) (*key.NodePublic, bool, error) {
// retrieve machinekey from state cache
machineKeyIf, machineKeyFound := h.registrationCache.Get(state)
if !machineKeyFound {
nodeKeyIf, nodeKeyFound := h.registrationCache.Get(state)
if !nodeKeyFound {
log.Error().
Msg("requested machine state key expired before authorisation completed")
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
@@ -419,20 +451,38 @@ func (h *Headscale) validateMachineForOIDCCallback(
Msg("Failed to write response")
}
return nil, false, errOIDCInvalidMachineState
return nil, false, errOIDCNodeKeyMissing
}
var nodeKey key.NodePublic
nodeKeyFromCache, nodeKeyOK := machineKeyIf.(string)
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
}
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 public key"))
_, werr := writer.Write([]byte("could not parse node public key"))
if werr != nil {
log.Error().
Caller().
@@ -443,21 +493,6 @@ 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
@@ -604,10 +639,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())