mirror of
https://github.com/juanfont/headscale.git
synced 2025-08-17 15:47:30 +00:00
Compare commits
48 Commits
warn-again
...
duplicate-
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e48b7d0b22 | ||
![]() |
7bcdc08bc9 | ||
![]() |
a82a603db6 | ||
![]() |
f49930c514 | ||
![]() |
2baeb79aa0 | ||
![]() |
b3f78a209a | ||
![]() |
5e6868a858 | ||
![]() |
5caf848f94 | ||
![]() |
3e097123bf | ||
![]() |
74447b02e8 | ||
![]() |
20e96de963 | ||
![]() |
7c765fb3dc | ||
![]() |
dcc246c869 | ||
![]() |
cf7767d8f9 | ||
![]() |
61c578f82b | ||
![]() |
6950ff7841 | ||
![]() |
e65ce17f7b | ||
![]() |
b190ec8edc | ||
![]() |
c39085911f | ||
![]() |
3c20d2a178 | ||
![]() |
9187e4287c | ||
![]() |
2b7bcb77a5 | ||
![]() |
97a909866d | ||
![]() |
feeb5d334b | ||
![]() |
a840a2e6ee | ||
![]() |
4183345020 | ||
![]() |
50fb7ad6ce | ||
![]() |
88a9f4b44c | ||
![]() |
00fbd8dd93 | ||
![]() |
ce587d2421 | ||
![]() |
e1eb30084d | ||
![]() |
673638afe7 | ||
![]() |
da48cf64b3 | ||
![]() |
385fd93e73 | ||
![]() |
26edf24477 | ||
![]() |
83a538cc95 | ||
![]() |
cffa040474 | ||
![]() |
727d95b477 | ||
![]() |
640bb94119 | ||
![]() |
0f65918a25 | ||
![]() |
3ac2e0b253 | ||
![]() |
b322cdf251 | ||
![]() |
e128796b59 | ||
![]() |
6d669c6b9c | ||
![]() |
8dadb045cf | ||
![]() |
9f6e546522 | ||
![]() |
9714900db9 | ||
![]() |
cb25f0d650 |
33
.github/workflows/build.yml
vendored
33
.github/workflows/build.yml
vendored
@@ -8,9 +8,14 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions: write-all
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -32,10 +37,34 @@ jobs:
|
|||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
- name: Run build
|
- name: Run build
|
||||||
|
id: build
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: nix build
|
run: |
|
||||||
|
nix build |& tee build-result
|
||||||
|
BUILD_STATUS="${PIPESTATUS[0]}"
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v2
|
OLD_HASH=$(cat build-result | grep specified: | awk -F ':' '{print $2}' | sed 's/ //g')
|
||||||
|
NEW_HASH=$(cat build-result | grep got: | awk -F ':' '{print $2}' | sed 's/ //g')
|
||||||
|
|
||||||
|
echo "OLD_HASH=$OLD_HASH" >> $GITHUB_OUTPUT
|
||||||
|
echo "NEW_HASH=$NEW_HASH" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
exit $BUILD_STATUS
|
||||||
|
|
||||||
|
- name: Nix gosum diverging
|
||||||
|
uses: actions/github-script@v6
|
||||||
|
if: failure() && steps.build.outcome == 'failure'
|
||||||
|
with:
|
||||||
|
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||||
|
script: |
|
||||||
|
github.rest.pulls.createReviewComment({
|
||||||
|
pull_number: context.issue.number,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body: 'Nix build failed with wrong gosum, please update "vendorSha256" (${{ steps.build.outputs.OLD_HASH }}) for the "headscale" package in flake.nix with the new SHA: ${{ steps.build.outputs.NEW_HASH }}'
|
||||||
|
})
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
with:
|
with:
|
||||||
name: headscale-linux
|
name: headscale-linux
|
||||||
|
8
.github/workflows/lint.yml
vendored
8
.github/workflows/lint.yml
vendored
@@ -3,6 +3,10 @@ name: Lint
|
|||||||
|
|
||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
golangci-lint:
|
golangci-lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -26,7 +30,7 @@ jobs:
|
|||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
uses: golangci/golangci-lint-action@v2
|
uses: golangci/golangci-lint-action@v2
|
||||||
with:
|
with:
|
||||||
version: v1.49.0
|
version: v1.51.2
|
||||||
|
|
||||||
# Only block PRs on new problems.
|
# Only block PRs on new problems.
|
||||||
# If this is not enabled, we will end up having PRs
|
# If this is not enabled, we will end up having PRs
|
||||||
@@ -59,7 +63,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Prettify code
|
- name: Prettify code
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
uses: creyD/prettier_action@v4.0
|
uses: creyD/prettier_action@v4.3
|
||||||
with:
|
with:
|
||||||
prettier_options: >-
|
prettier_options: >-
|
||||||
--check **/*.{ts,js,md,yaml,yml,sass,css,scss,html}
|
--check **/*.{ts,js,md,yaml,yml,sass,css,scss,html}
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
||||||
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
||||||
|
|
||||||
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestAuthKeyLogoutAndRelogin
|
|||||||
|
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -26,8 +29,8 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v16
|
- uses: cachix/install-nix-action@v18
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
@@ -38,6 +41,7 @@ jobs:
|
|||||||
--name headscale-test-suite \
|
--name headscale-test-suite \
|
||||||
--volume $PWD:$PWD -w $PWD/integration \
|
--volume $PWD:$PWD -w $PWD/integration \
|
||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go test ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
@@ -45,3 +49,9 @@ jobs:
|
|||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
-parallel 1 \
|
-parallel 1 \
|
||||||
-run "^TestAuthKeyLogoutAndRelogin$"
|
-run "^TestAuthKeyLogoutAndRelogin$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
||||||
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
||||||
|
|
||||||
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestAuthWebFlowAuthenticationPingAll
|
|||||||
|
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -26,8 +29,8 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v16
|
- uses: cachix/install-nix-action@v18
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
@@ -38,6 +41,7 @@ jobs:
|
|||||||
--name headscale-test-suite \
|
--name headscale-test-suite \
|
||||||
--volume $PWD:$PWD -w $PWD/integration \
|
--volume $PWD:$PWD -w $PWD/integration \
|
||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go test ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
@@ -45,3 +49,9 @@ jobs:
|
|||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
-parallel 1 \
|
-parallel 1 \
|
||||||
-run "^TestAuthWebFlowAuthenticationPingAll$"
|
-run "^TestAuthWebFlowAuthenticationPingAll$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
||||||
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
||||||
|
|
||||||
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestAuthWebFlowLogoutAndRelogin
|
|||||||
|
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -26,8 +29,8 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v16
|
- uses: cachix/install-nix-action@v18
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
@@ -38,6 +41,7 @@ jobs:
|
|||||||
--name headscale-test-suite \
|
--name headscale-test-suite \
|
||||||
--volume $PWD:$PWD -w $PWD/integration \
|
--volume $PWD:$PWD -w $PWD/integration \
|
||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go test ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
@@ -45,3 +49,9 @@ jobs:
|
|||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
-parallel 1 \
|
-parallel 1 \
|
||||||
-run "^TestAuthWebFlowLogoutAndRelogin$"
|
-run "^TestAuthWebFlowLogoutAndRelogin$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
||||||
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
||||||
|
|
||||||
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestCreateTailscale
|
|||||||
|
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -26,8 +29,8 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v16
|
- uses: cachix/install-nix-action@v18
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
@@ -38,6 +41,7 @@ jobs:
|
|||||||
--name headscale-test-suite \
|
--name headscale-test-suite \
|
||||||
--volume $PWD:$PWD -w $PWD/integration \
|
--volume $PWD:$PWD -w $PWD/integration \
|
||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go test ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
@@ -45,3 +49,9 @@ jobs:
|
|||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
-parallel 1 \
|
-parallel 1 \
|
||||||
-run "^TestCreateTailscale$"
|
-run "^TestCreateTailscale$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
||||||
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
||||||
|
|
||||||
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestEnablingRoutes
|
|||||||
|
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -26,8 +29,8 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v16
|
- uses: cachix/install-nix-action@v18
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
@@ -38,6 +41,7 @@ jobs:
|
|||||||
--name headscale-test-suite \
|
--name headscale-test-suite \
|
||||||
--volume $PWD:$PWD -w $PWD/integration \
|
--volume $PWD:$PWD -w $PWD/integration \
|
||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go test ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
@@ -45,3 +49,9 @@ jobs:
|
|||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
-parallel 1 \
|
-parallel 1 \
|
||||||
-run "^TestEnablingRoutes$"
|
-run "^TestEnablingRoutes$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
||||||
|
@@ -1,11 +1,14 @@
|
|||||||
|
|
||||||
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
||||||
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
||||||
|
|
||||||
name: Integration Test v2 - TestNamespaceCommand
|
name: Integration Test v2 - TestEphemeral
|
||||||
|
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -26,8 +29,8 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v16
|
- uses: cachix/install-nix-action@v18
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
@@ -38,10 +41,17 @@ jobs:
|
|||||||
--name headscale-test-suite \
|
--name headscale-test-suite \
|
||||||
--volume $PWD:$PWD -w $PWD/integration \
|
--volume $PWD:$PWD -w $PWD/integration \
|
||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go test ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
-parallel 1 \
|
-parallel 1 \
|
||||||
-run "^TestNamespaceCommand$"
|
-run "^TestEphemeral$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
@@ -1,11 +1,14 @@
|
|||||||
|
|
||||||
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
||||||
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
||||||
|
|
||||||
name: Integration Test v2 - TestOIDCExpireNodes
|
name: Integration Test v2 - TestExpireNode
|
||||||
|
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -26,8 +29,8 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v16
|
- uses: cachix/install-nix-action@v18
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
@@ -38,10 +41,17 @@ jobs:
|
|||||||
--name headscale-test-suite \
|
--name headscale-test-suite \
|
||||||
--volume $PWD:$PWD -w $PWD/integration \
|
--volume $PWD:$PWD -w $PWD/integration \
|
||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go test ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
-parallel 1 \
|
-parallel 1 \
|
||||||
-run "^TestOIDCExpireNodes$"
|
-run "^TestExpireNode$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
||||||
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
||||||
|
|
||||||
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestHeadscale
|
|||||||
|
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -26,8 +29,8 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v16
|
- uses: cachix/install-nix-action@v18
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
@@ -38,6 +41,7 @@ jobs:
|
|||||||
--name headscale-test-suite \
|
--name headscale-test-suite \
|
||||||
--volume $PWD:$PWD -w $PWD/integration \
|
--volume $PWD:$PWD -w $PWD/integration \
|
||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go test ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
@@ -45,3 +49,9 @@ jobs:
|
|||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
-parallel 1 \
|
-parallel 1 \
|
||||||
-run "^TestHeadscale$"
|
-run "^TestHeadscale$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
||||||
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
||||||
|
|
||||||
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestOIDCAuthenticationPingAll
|
|||||||
|
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -26,8 +29,8 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v16
|
- uses: cachix/install-nix-action@v18
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
@@ -38,6 +41,7 @@ jobs:
|
|||||||
--name headscale-test-suite \
|
--name headscale-test-suite \
|
||||||
--volume $PWD:$PWD -w $PWD/integration \
|
--volume $PWD:$PWD -w $PWD/integration \
|
||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go test ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
@@ -45,3 +49,9 @@ jobs:
|
|||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
-parallel 1 \
|
-parallel 1 \
|
||||||
-run "^TestOIDCAuthenticationPingAll$"
|
-run "^TestOIDCAuthenticationPingAll$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
||||||
|
57
.github/workflows/test-integration-v2-TestOIDCExpireNodesBasedOnTokenExpiry.yaml
vendored
Normal file
57
.github/workflows/test-integration-v2-TestOIDCExpireNodesBasedOnTokenExpiry.yaml
vendored
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
||||||
|
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
||||||
|
|
||||||
|
name: Integration Test v2 - TestOIDCExpireNodesBasedOnTokenExpiry
|
||||||
|
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- name: Get changed files
|
||||||
|
id: changed-files
|
||||||
|
uses: tj-actions/changed-files@v34
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
*.nix
|
||||||
|
go.*
|
||||||
|
**/*.go
|
||||||
|
integration_test/
|
||||||
|
config-example.yaml
|
||||||
|
|
||||||
|
- uses: cachix/install-nix-action@v18
|
||||||
|
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
|
- name: Run general integration tests
|
||||||
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
run: |
|
||||||
|
nix develop --command -- docker run \
|
||||||
|
--tty --rm \
|
||||||
|
--volume ~/.cache/hs-integration-go:/go \
|
||||||
|
--name headscale-test-suite \
|
||||||
|
--volume $PWD:$PWD -w $PWD/integration \
|
||||||
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
|
golang:1 \
|
||||||
|
go test ./... \
|
||||||
|
-tags ts2019 \
|
||||||
|
-failfast \
|
||||||
|
-timeout 120m \
|
||||||
|
-parallel 1 \
|
||||||
|
-run "^TestOIDCExpireNodesBasedOnTokenExpiry$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
||||||
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
||||||
|
|
||||||
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestPingAllByHostname
|
|||||||
|
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -26,8 +29,8 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v16
|
- uses: cachix/install-nix-action@v18
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
@@ -38,6 +41,7 @@ jobs:
|
|||||||
--name headscale-test-suite \
|
--name headscale-test-suite \
|
||||||
--volume $PWD:$PWD -w $PWD/integration \
|
--volume $PWD:$PWD -w $PWD/integration \
|
||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go test ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
@@ -45,3 +49,9 @@ jobs:
|
|||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
-parallel 1 \
|
-parallel 1 \
|
||||||
-run "^TestPingAllByHostname$"
|
-run "^TestPingAllByHostname$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
||||||
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
||||||
|
|
||||||
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestPingAllByIP
|
|||||||
|
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -26,8 +29,8 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v16
|
- uses: cachix/install-nix-action@v18
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
@@ -38,6 +41,7 @@ jobs:
|
|||||||
--name headscale-test-suite \
|
--name headscale-test-suite \
|
||||||
--volume $PWD:$PWD -w $PWD/integration \
|
--volume $PWD:$PWD -w $PWD/integration \
|
||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go test ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
@@ -45,3 +49,9 @@ jobs:
|
|||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
-parallel 1 \
|
-parallel 1 \
|
||||||
-run "^TestPingAllByIP$"
|
-run "^TestPingAllByIP$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
||||||
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
||||||
|
|
||||||
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestPreAuthKeyCommand
|
|||||||
|
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -26,8 +29,8 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v16
|
- uses: cachix/install-nix-action@v18
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
@@ -38,6 +41,7 @@ jobs:
|
|||||||
--name headscale-test-suite \
|
--name headscale-test-suite \
|
||||||
--volume $PWD:$PWD -w $PWD/integration \
|
--volume $PWD:$PWD -w $PWD/integration \
|
||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go test ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
@@ -45,3 +49,9 @@ jobs:
|
|||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
-parallel 1 \
|
-parallel 1 \
|
||||||
-run "^TestPreAuthKeyCommand$"
|
-run "^TestPreAuthKeyCommand$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
||||||
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
||||||
|
|
||||||
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestPreAuthKeyCommandReusableEphemeral
|
|||||||
|
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -26,8 +29,8 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v16
|
- uses: cachix/install-nix-action@v18
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
@@ -38,6 +41,7 @@ jobs:
|
|||||||
--name headscale-test-suite \
|
--name headscale-test-suite \
|
||||||
--volume $PWD:$PWD -w $PWD/integration \
|
--volume $PWD:$PWD -w $PWD/integration \
|
||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go test ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
@@ -45,3 +49,9 @@ jobs:
|
|||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
-parallel 1 \
|
-parallel 1 \
|
||||||
-run "^TestPreAuthKeyCommandReusableEphemeral$"
|
-run "^TestPreAuthKeyCommandReusableEphemeral$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
||||||
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
||||||
|
|
||||||
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestPreAuthKeyCommandWithoutExpiry
|
|||||||
|
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -26,8 +29,8 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v16
|
- uses: cachix/install-nix-action@v18
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
@@ -38,6 +41,7 @@ jobs:
|
|||||||
--name headscale-test-suite \
|
--name headscale-test-suite \
|
||||||
--volume $PWD:$PWD -w $PWD/integration \
|
--volume $PWD:$PWD -w $PWD/integration \
|
||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go test ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
@@ -45,3 +49,9 @@ jobs:
|
|||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
-parallel 1 \
|
-parallel 1 \
|
||||||
-run "^TestPreAuthKeyCommandWithoutExpiry$"
|
-run "^TestPreAuthKeyCommandWithoutExpiry$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
||||||
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
||||||
|
|
||||||
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestResolveMagicDNS
|
|||||||
|
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -26,8 +29,8 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v16
|
- uses: cachix/install-nix-action@v18
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
@@ -38,6 +41,7 @@ jobs:
|
|||||||
--name headscale-test-suite \
|
--name headscale-test-suite \
|
||||||
--volume $PWD:$PWD -w $PWD/integration \
|
--volume $PWD:$PWD -w $PWD/integration \
|
||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go test ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
@@ -45,3 +49,9 @@ jobs:
|
|||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
-parallel 1 \
|
-parallel 1 \
|
||||||
-run "^TestResolveMagicDNS$"
|
-run "^TestResolveMagicDNS$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
||||||
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
||||||
|
|
||||||
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestSSHIsBlockedInACL
|
|||||||
|
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -26,8 +29,8 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v16
|
- uses: cachix/install-nix-action@v18
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
@@ -38,6 +41,7 @@ jobs:
|
|||||||
--name headscale-test-suite \
|
--name headscale-test-suite \
|
||||||
--volume $PWD:$PWD -w $PWD/integration \
|
--volume $PWD:$PWD -w $PWD/integration \
|
||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go test ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
@@ -45,3 +49,9 @@ jobs:
|
|||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
-parallel 1 \
|
-parallel 1 \
|
||||||
-run "^TestSSHIsBlockedInACL$"
|
-run "^TestSSHIsBlockedInACL$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
||||||
|
57
.github/workflows/test-integration-v2-TestSSHMultipleUsersAllToAll.yaml
vendored
Normal file
57
.github/workflows/test-integration-v2-TestSSHMultipleUsersAllToAll.yaml
vendored
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
||||||
|
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
||||||
|
|
||||||
|
name: Integration Test v2 - TestSSHMultipleUsersAllToAll
|
||||||
|
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- name: Get changed files
|
||||||
|
id: changed-files
|
||||||
|
uses: tj-actions/changed-files@v34
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
*.nix
|
||||||
|
go.*
|
||||||
|
**/*.go
|
||||||
|
integration_test/
|
||||||
|
config-example.yaml
|
||||||
|
|
||||||
|
- uses: cachix/install-nix-action@v18
|
||||||
|
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
|
- name: Run general integration tests
|
||||||
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
run: |
|
||||||
|
nix develop --command -- docker run \
|
||||||
|
--tty --rm \
|
||||||
|
--volume ~/.cache/hs-integration-go:/go \
|
||||||
|
--name headscale-test-suite \
|
||||||
|
--volume $PWD:$PWD -w $PWD/integration \
|
||||||
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
|
golang:1 \
|
||||||
|
go test ./... \
|
||||||
|
-tags ts2019 \
|
||||||
|
-failfast \
|
||||||
|
-timeout 120m \
|
||||||
|
-parallel 1 \
|
||||||
|
-run "^TestSSHMultipleUsersAllToAll$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
||||||
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
||||||
|
|
||||||
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestSSHNoSSHConfigured
|
|||||||
|
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -26,8 +29,8 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v16
|
- uses: cachix/install-nix-action@v18
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
@@ -38,6 +41,7 @@ jobs:
|
|||||||
--name headscale-test-suite \
|
--name headscale-test-suite \
|
||||||
--volume $PWD:$PWD -w $PWD/integration \
|
--volume $PWD:$PWD -w $PWD/integration \
|
||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go test ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
@@ -45,3 +49,9 @@ jobs:
|
|||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
-parallel 1 \
|
-parallel 1 \
|
||||||
-run "^TestSSHNoSSHConfigured$"
|
-run "^TestSSHNoSSHConfigured$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
||||||
|
@@ -1,11 +1,14 @@
|
|||||||
|
|
||||||
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
||||||
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
||||||
|
|
||||||
name: Integration Test v2 - TestSSHOneNamespaceAllToAll
|
name: Integration Test v2 - TestSSHOneUserAllToAll
|
||||||
|
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -26,8 +29,8 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v16
|
- uses: cachix/install-nix-action@v18
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
@@ -38,10 +41,17 @@ jobs:
|
|||||||
--name headscale-test-suite \
|
--name headscale-test-suite \
|
||||||
--volume $PWD:$PWD -w $PWD/integration \
|
--volume $PWD:$PWD -w $PWD/integration \
|
||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go test ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
-parallel 1 \
|
-parallel 1 \
|
||||||
-run "^TestSSHOneNamespaceAllToAll$"
|
-run "^TestSSHOneUserAllToAll$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
@@ -1,47 +0,0 @@
|
|||||||
|
|
||||||
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
|
||||||
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
|
||||||
|
|
||||||
name: Integration Test v2 - TestSSNamespaceOnlyIsolation
|
|
||||||
|
|
||||||
on: [pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 2
|
|
||||||
|
|
||||||
- name: Get changed files
|
|
||||||
id: changed-files
|
|
||||||
uses: tj-actions/changed-files@v34
|
|
||||||
with:
|
|
||||||
files: |
|
|
||||||
*.nix
|
|
||||||
go.*
|
|
||||||
**/*.go
|
|
||||||
integration_test/
|
|
||||||
config-example.yaml
|
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v16
|
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
run: |
|
|
||||||
nix develop --command -- docker run \
|
|
||||||
--tty --rm \
|
|
||||||
--volume ~/.cache/hs-integration-go:/go \
|
|
||||||
--name headscale-test-suite \
|
|
||||||
--volume $PWD:$PWD -w $PWD/integration \
|
|
||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
|
||||||
golang:1 \
|
|
||||||
go test ./... \
|
|
||||||
-tags ts2019 \
|
|
||||||
-failfast \
|
|
||||||
-timeout 120m \
|
|
||||||
-parallel 1 \
|
|
||||||
-run "^TestSSNamespaceOnlyIsolation$"
|
|
57
.github/workflows/test-integration-v2-TestSSUserOnlyIsolation.yaml
vendored
Normal file
57
.github/workflows/test-integration-v2-TestSSUserOnlyIsolation.yaml
vendored
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
||||||
|
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
||||||
|
|
||||||
|
name: Integration Test v2 - TestSSUserOnlyIsolation
|
||||||
|
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- name: Get changed files
|
||||||
|
id: changed-files
|
||||||
|
uses: tj-actions/changed-files@v34
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
*.nix
|
||||||
|
go.*
|
||||||
|
**/*.go
|
||||||
|
integration_test/
|
||||||
|
config-example.yaml
|
||||||
|
|
||||||
|
- uses: cachix/install-nix-action@v18
|
||||||
|
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
|
- name: Run general integration tests
|
||||||
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
run: |
|
||||||
|
nix develop --command -- docker run \
|
||||||
|
--tty --rm \
|
||||||
|
--volume ~/.cache/hs-integration-go:/go \
|
||||||
|
--name headscale-test-suite \
|
||||||
|
--volume $PWD:$PWD -w $PWD/integration \
|
||||||
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
|
golang:1 \
|
||||||
|
go test ./... \
|
||||||
|
-tags ts2019 \
|
||||||
|
-failfast \
|
||||||
|
-timeout 120m \
|
||||||
|
-parallel 1 \
|
||||||
|
-run "^TestSSUserOnlyIsolation$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
||||||
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
||||||
|
|
||||||
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestTaildrop
|
|||||||
|
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -26,8 +29,8 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v16
|
- uses: cachix/install-nix-action@v18
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
@@ -38,6 +41,7 @@ jobs:
|
|||||||
--name headscale-test-suite \
|
--name headscale-test-suite \
|
||||||
--volume $PWD:$PWD -w $PWD/integration \
|
--volume $PWD:$PWD -w $PWD/integration \
|
||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go test ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
@@ -45,3 +49,9 @@ jobs:
|
|||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
-parallel 1 \
|
-parallel 1 \
|
||||||
-run "^TestTaildrop$"
|
-run "^TestTaildrop$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
||||||
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
||||||
|
|
||||||
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestTailscaleNodesJoiningHeadcale
|
|||||||
|
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -26,8 +29,8 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v16
|
- uses: cachix/install-nix-action@v18
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
@@ -38,6 +41,7 @@ jobs:
|
|||||||
--name headscale-test-suite \
|
--name headscale-test-suite \
|
||||||
--volume $PWD:$PWD -w $PWD/integration \
|
--volume $PWD:$PWD -w $PWD/integration \
|
||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go test ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
@@ -45,3 +49,9 @@ jobs:
|
|||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
-parallel 1 \
|
-parallel 1 \
|
||||||
-run "^TestTailscaleNodesJoiningHeadcale$"
|
-run "^TestTailscaleNodesJoiningHeadcale$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
||||||
|
@@ -1,11 +1,14 @@
|
|||||||
|
|
||||||
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
||||||
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
||||||
|
|
||||||
name: Integration Test v2 - TestSSHMultipleNamespacesAllToAll
|
name: Integration Test v2 - TestUserCommand
|
||||||
|
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -26,8 +29,8 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v16
|
- uses: cachix/install-nix-action@v18
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
@@ -38,10 +41,17 @@ jobs:
|
|||||||
--name headscale-test-suite \
|
--name headscale-test-suite \
|
||||||
--volume $PWD:$PWD -w $PWD/integration \
|
--volume $PWD:$PWD -w $PWD/integration \
|
||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go test ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
-parallel 1 \
|
-parallel 1 \
|
||||||
-run "^TestSSHMultipleNamespacesAllToAll$"
|
-run "^TestUserCommand$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -2,6 +2,10 @@ name: Tests
|
|||||||
|
|
||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -27,6 +27,7 @@ derp.yaml
|
|||||||
.idea
|
.idea
|
||||||
|
|
||||||
test_output/
|
test_output/
|
||||||
|
control_logs/
|
||||||
|
|
||||||
# Nix build output
|
# Nix build output
|
||||||
result
|
result
|
||||||
|
23
CHANGELOG.md
23
CHANGELOG.md
@@ -1,13 +1,32 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
## 0.19.0 (2022-11-26)
|
## 0.21.0 (2023-xx-xx)
|
||||||
|
|
||||||
|
### changes
|
||||||
|
|
||||||
|
- Adding "configtest" CLI command.
|
||||||
|
|
||||||
|
## 0.20.0 (2023-02-03)
|
||||||
|
|
||||||
|
### changes
|
||||||
|
|
||||||
|
- Fix wrong behaviour in exit nodes [#1159](https://github.com/juanfont/headscale/pull/1159)
|
||||||
|
- Align behaviour of `dns_config.restricted_nameservers` to tailscale [#1162](https://github.com/juanfont/headscale/pull/1162)
|
||||||
|
- Make OpenID Connect authenticated client expiry time configurable [#1191](https://github.com/juanfont/headscale/pull/1191)
|
||||||
|
- defaults to 180 days like Tailscale SaaS
|
||||||
|
- adds option to use the expiry time from the OpenID token for the node (see config-example.yaml)
|
||||||
|
- Set ControlTime in Map info sent to nodes [#1195](https://github.com/juanfont/headscale/pull/1195)
|
||||||
|
- Populate Tags field on Node updates sent [#1195](https://github.com/juanfont/headscale/pull/1195)
|
||||||
|
|
||||||
|
## 0.19.0 (2023-01-29)
|
||||||
|
|
||||||
### BREAKING
|
### BREAKING
|
||||||
|
|
||||||
- Rename Namespace to User [#1144](https://github.com/juanfont/headscale/pull/1144)
|
- Rename Namespace to User [#1144](https://github.com/juanfont/headscale/pull/1144)
|
||||||
- **BACKUP your database before upgrading**
|
- **BACKUP your database before upgrading**
|
||||||
|
- Command line flags previously taking `--namespace` or `-n` will now require `--user` or `-u`
|
||||||
|
|
||||||
## 0.18.0 (2022-01-14)
|
## 0.18.0 (2023-01-14)
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
|
@@ -81,6 +81,11 @@ one of the maintainers.
|
|||||||
|
|
||||||
Please have a look at the documentation under [`docs/`](docs/).
|
Please have a look at the documentation under [`docs/`](docs/).
|
||||||
|
|
||||||
|
## Talks
|
||||||
|
|
||||||
|
- Fosdem 2023 (video): [Headscale: How we are using integration testing to reimplement Tailscale](https://fosdem.org/2023/schedule/event/goheadscale/)
|
||||||
|
- presented by Juan Font Alonso and Kristoffer Dalby
|
||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
|
|
||||||
1. We have nothing to do with Tailscale, or Tailscale Inc.
|
1. We have nothing to do with Tailscale, or Tailscale Inc.
|
||||||
|
11
acls.go
11
acls.go
@@ -150,7 +150,11 @@ func (h *Headscale) UpdateACLRules() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateACLRules(machines []Machine, aclPolicy ACLPolicy, stripEmaildomain bool) ([]tailcfg.FilterRule, error) {
|
func generateACLRules(
|
||||||
|
machines []Machine,
|
||||||
|
aclPolicy ACLPolicy,
|
||||||
|
stripEmaildomain bool,
|
||||||
|
) ([]tailcfg.FilterRule, error) {
|
||||||
rules := []tailcfg.FilterRule{}
|
rules := []tailcfg.FilterRule{}
|
||||||
|
|
||||||
for index, acl := range aclPolicy.ACLs {
|
for index, acl := range aclPolicy.ACLs {
|
||||||
@@ -160,7 +164,7 @@ func generateACLRules(machines []Machine, aclPolicy ACLPolicy, stripEmaildomain
|
|||||||
|
|
||||||
srcIPs := []string{}
|
srcIPs := []string{}
|
||||||
for innerIndex, src := range acl.Sources {
|
for innerIndex, src := range acl.Sources {
|
||||||
srcs, err := generateACLPolicySrcIP(machines, aclPolicy, src, stripEmaildomain)
|
srcs, err := generateACLPolicySrc(machines, aclPolicy, src, stripEmaildomain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
Msgf("Error parsing ACL %d, Source %d", index, innerIndex)
|
Msgf("Error parsing ACL %d, Source %d", index, innerIndex)
|
||||||
@@ -311,7 +315,7 @@ func sshCheckAction(duration string) (*tailcfg.SSHAction, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateACLPolicySrcIP(
|
func generateACLPolicySrc(
|
||||||
machines []Machine,
|
machines []Machine,
|
||||||
aclPolicy ACLPolicy,
|
aclPolicy ACLPolicy,
|
||||||
src string,
|
src string,
|
||||||
@@ -427,6 +431,7 @@ func parseProtocol(protocol string) ([]int, bool, error) {
|
|||||||
// - a user
|
// - a user
|
||||||
// - a group
|
// - a group
|
||||||
// - a tag
|
// - a tag
|
||||||
|
// - a host
|
||||||
// and transform these in IPAddresses.
|
// and transform these in IPAddresses.
|
||||||
func expandAlias(
|
func expandAlias(
|
||||||
machines []Machine,
|
machines []Machine,
|
||||||
|
17
acls_test.go
17
acls_test.go
@@ -1041,7 +1041,7 @@ func Test_expandAlias(t *testing.T) {
|
|||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "simple host",
|
name: "simple host by ip",
|
||||||
args: args{
|
args: args{
|
||||||
alias: "10.0.0.1",
|
alias: "10.0.0.1",
|
||||||
machines: []Machine{},
|
machines: []Machine{},
|
||||||
@@ -1051,6 +1051,21 @@ func Test_expandAlias(t *testing.T) {
|
|||||||
want: []string{"10.0.0.1"},
|
want: []string{"10.0.0.1"},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "simple host by hostname alias",
|
||||||
|
args: args{
|
||||||
|
alias: "testy",
|
||||||
|
machines: []Machine{},
|
||||||
|
aclPolicy: ACLPolicy{
|
||||||
|
Hosts: Hosts{
|
||||||
|
"testy": netip.MustParsePrefix("10.0.0.132/32"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stripEmailDomain: true,
|
||||||
|
},
|
||||||
|
want: []string{"10.0.0.132/32"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "simple CIDR",
|
name: "simple CIDR",
|
||||||
args: args{
|
args: args{
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
package headscale
|
package headscale
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
)
|
)
|
||||||
@@ -55,16 +57,46 @@ func (h *Headscale) generateMapResponse(
|
|||||||
peers,
|
peers,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
resp := tailcfg.MapResponse{
|
resp := tailcfg.MapResponse{
|
||||||
KeepAlive: false,
|
KeepAlive: false,
|
||||||
Node: node,
|
Node: node,
|
||||||
Peers: nodePeers,
|
|
||||||
DNSConfig: dnsConfig,
|
// TODO: Only send if updated
|
||||||
Domain: h.cfg.BaseDomain,
|
|
||||||
PacketFilter: h.aclRules,
|
|
||||||
SSHPolicy: h.sshPolicy,
|
|
||||||
DERPMap: h.DERPMap,
|
DERPMap: h.DERPMap,
|
||||||
|
|
||||||
|
// TODO: Only send if updated
|
||||||
|
Peers: nodePeers,
|
||||||
|
|
||||||
|
// TODO(kradalby): Implement:
|
||||||
|
// https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L1351-L1374
|
||||||
|
// PeersChanged
|
||||||
|
// PeersRemoved
|
||||||
|
// PeersChangedPatch
|
||||||
|
// PeerSeenChange
|
||||||
|
// OnlineChange
|
||||||
|
|
||||||
|
// TODO: Only send if updated
|
||||||
|
DNSConfig: dnsConfig,
|
||||||
|
|
||||||
|
// TODO: Only send if updated
|
||||||
|
Domain: h.cfg.BaseDomain,
|
||||||
|
|
||||||
|
// Do not instruct clients to collect services, we do not
|
||||||
|
// support or do anything with them
|
||||||
|
CollectServices: "false",
|
||||||
|
|
||||||
|
// TODO: Only send if updated
|
||||||
|
PacketFilter: h.aclRules,
|
||||||
|
|
||||||
UserProfiles: profiles,
|
UserProfiles: profiles,
|
||||||
|
|
||||||
|
// TODO: Only send if updated
|
||||||
|
SSHPolicy: h.sshPolicy,
|
||||||
|
|
||||||
|
ControlTime: &now,
|
||||||
|
|
||||||
Debug: &tailcfg.Debug{
|
Debug: &tailcfg.Debug{
|
||||||
DisableLogTail: !h.cfg.LogTail.Enabled,
|
DisableLogTail: !h.cfg.LogTail.Enabled,
|
||||||
RandomizeClientPort: h.cfg.RandomizeClientPort,
|
RandomizeClientPort: h.cfg.RandomizeClientPort,
|
||||||
|
5
app.go
5
app.go
@@ -521,7 +521,7 @@ func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *mux.Router {
|
|||||||
apiRouter.Use(h.httpAuthenticationMiddleware)
|
apiRouter.Use(h.httpAuthenticationMiddleware)
|
||||||
apiRouter.PathPrefix("/v1/").HandlerFunc(grpcMux.ServeHTTP)
|
apiRouter.PathPrefix("/v1/").HandlerFunc(grpcMux.ServeHTTP)
|
||||||
|
|
||||||
router.PathPrefix("/").HandlerFunc(stdoutHandler)
|
router.PathPrefix("/").HandlerFunc(notFoundHandler)
|
||||||
|
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
@@ -957,7 +957,7 @@ func (h *Headscale) getLastStateChange(users ...User) time.Time {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func stdoutHandler(
|
func notFoundHandler(
|
||||||
writer http.ResponseWriter,
|
writer http.ResponseWriter,
|
||||||
req *http.Request,
|
req *http.Request,
|
||||||
) {
|
) {
|
||||||
@@ -969,6 +969,7 @@ func stdoutHandler(
|
|||||||
Interface("url", req.URL).
|
Interface("url", req.URL).
|
||||||
Bytes("body", body).
|
Bytes("body", body).
|
||||||
Msg("Request did not match")
|
Msg("Request did not match")
|
||||||
|
writer.WriteHeader(http.StatusNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func readOrCreatePrivateKey(path string) (*key.MachinePrivate, error) {
|
func readOrCreatePrivateKey(path string) (*key.MachinePrivate, error) {
|
||||||
|
@@ -7,19 +7,29 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
githubWorkflowPath = "../../.github/workflows/"
|
||||||
jobFileNameTemplate = `test-integration-v2-%s.yaml`
|
jobFileNameTemplate = `test-integration-v2-%s.yaml`
|
||||||
jobTemplate = template.Must(template.New("jobTemplate").Parse(`
|
jobTemplate = template.Must(
|
||||||
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
template.New("jobTemplate").
|
||||||
|
Parse(`# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
||||||
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
||||||
|
|
||||||
name: Integration Test v2 - {{.Name}}
|
name: Integration Test v2 - {{.Name}}
|
||||||
|
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: {{ "${{ github.workflow }}-$${{ github.head_ref || github.run_id }}" }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -40,8 +50,8 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v16
|
- uses: cachix/install-nix-action@v18
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: {{ "${{ env.ACT }}" }} || steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
@@ -52,6 +62,7 @@ jobs:
|
|||||||
--name headscale-test-suite \
|
--name headscale-test-suite \
|
||||||
--volume $PWD:$PWD -w $PWD/integration \
|
--volume $PWD:$PWD -w $PWD/integration \
|
||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go test ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
@@ -59,43 +70,81 @@ jobs:
|
|||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
-parallel 1 \
|
-parallel 1 \
|
||||||
-run "^{{.Name}}$"
|
-run "^{{.Name}}$"
|
||||||
`))
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
||||||
|
`),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
const workflowFilePerm = 0o600
|
const workflowFilePerm = 0o600
|
||||||
|
|
||||||
|
func removeTests() {
|
||||||
|
glob := fmt.Sprintf(jobFileNameTemplate, "*")
|
||||||
|
|
||||||
|
files, err := filepath.Glob(filepath.Join(githubWorkflowPath, glob))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to find test files")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
err := os.Remove(file)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to remove: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func findTests() []string {
|
||||||
|
rgBin, err := exec.LookPath("rg")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to find rg (ripgrep) binary")
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"--regexp", "func (Test.+)\\(.*",
|
||||||
|
"../../integration/",
|
||||||
|
"--replace", "$1",
|
||||||
|
"--sort", "path",
|
||||||
|
"--no-line-number",
|
||||||
|
"--no-filename",
|
||||||
|
"--no-heading",
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("executing: %s %s", rgBin, strings.Join(args, " "))
|
||||||
|
|
||||||
|
ripgrep := exec.Command(
|
||||||
|
rgBin,
|
||||||
|
args...,
|
||||||
|
)
|
||||||
|
|
||||||
|
result, err := ripgrep.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("out: %s", result)
|
||||||
|
log.Fatalf("failed to run ripgrep: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := strings.Split(string(result), "\n")
|
||||||
|
tests = tests[:len(tests)-1]
|
||||||
|
|
||||||
|
return tests
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
type testConfig struct {
|
type testConfig struct {
|
||||||
Name string
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(kradalby): automatic fetch tests at runtime
|
tests := findTests()
|
||||||
tests := []string{
|
|
||||||
"TestAuthKeyLogoutAndRelogin",
|
removeTests()
|
||||||
"TestAuthWebFlowAuthenticationPingAll",
|
|
||||||
"TestAuthWebFlowLogoutAndRelogin",
|
|
||||||
"TestCreateTailscale",
|
|
||||||
"TestEnablingRoutes",
|
|
||||||
"TestHeadscale",
|
|
||||||
"TestUserCommand",
|
|
||||||
"TestOIDCAuthenticationPingAll",
|
|
||||||
"TestOIDCExpireNodes",
|
|
||||||
"TestPingAllByHostname",
|
|
||||||
"TestPingAllByIP",
|
|
||||||
"TestPreAuthKeyCommand",
|
|
||||||
"TestPreAuthKeyCommandReusableEphemeral",
|
|
||||||
"TestPreAuthKeyCommandWithoutExpiry",
|
|
||||||
"TestResolveMagicDNS",
|
|
||||||
"TestSSHIsBlockedInACL",
|
|
||||||
"TestSSHMultipleUsersAllToAll",
|
|
||||||
"TestSSHNoSSHConfigured",
|
|
||||||
"TestSSHOneUserAllToAll",
|
|
||||||
"TestSSUserOnlyIsolation",
|
|
||||||
"TestTaildrop",
|
|
||||||
"TestTailscaleNodesJoiningHeadcale",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
|
log.Printf("generating workflow for %s", test)
|
||||||
|
|
||||||
var content bytes.Buffer
|
var content bytes.Buffer
|
||||||
|
|
||||||
if err := jobTemplate.Execute(&content, testConfig{
|
if err := jobTemplate.Execute(&content, testConfig{
|
||||||
@@ -104,9 +153,9 @@ func main() {
|
|||||||
log.Fatalf("failed to render template: %s", err)
|
log.Fatalf("failed to render template: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
path := "../../.github/workflows/" + fmt.Sprintf(jobFileNameTemplate, test)
|
testPath := path.Join(githubWorkflowPath, fmt.Sprintf(jobFileNameTemplate, test))
|
||||||
|
|
||||||
err := os.WriteFile(path, content.Bytes(), workflowFilePerm)
|
err := os.WriteFile(testPath, content.Bytes(), workflowFilePerm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to write github job: %s", err)
|
log.Fatalf("failed to write github job: %s", err)
|
||||||
}
|
}
|
||||||
|
22
cmd/headscale/cli/configtest.go
Normal file
22
cmd/headscale/cli/configtest.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(configTestCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
var configTestCmd = &cobra.Command{
|
||||||
|
Use: "configtest",
|
||||||
|
Short: "Test the configuration.",
|
||||||
|
Long: "Run a test of the configuration and exit.",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
_, err := getHeadscaleApp()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Caller().Err(err).Msg("Error initializing")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
@@ -27,7 +27,13 @@ func init() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("")
|
log.Fatal().Err(err).Msg("")
|
||||||
}
|
}
|
||||||
createNodeCmd.Flags().StringP("user", "n", "", "User")
|
createNodeCmd.Flags().StringP("user", "u", "", "User")
|
||||||
|
|
||||||
|
createNodeCmd.Flags().StringP("namespace", "n", "", "User")
|
||||||
|
createNodeNamespaceFlag := createNodeCmd.Flags().Lookup("namespace")
|
||||||
|
createNodeNamespaceFlag.Deprecated = deprecateNamespaceMessage
|
||||||
|
createNodeNamespaceFlag.Hidden = true
|
||||||
|
|
||||||
err = createNodeCmd.MarkFlagRequired("user")
|
err = createNodeCmd.MarkFlagRequired("user")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("")
|
log.Fatal().Err(err).Msg("")
|
||||||
|
@@ -19,11 +19,23 @@ import (
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(nodeCmd)
|
rootCmd.AddCommand(nodeCmd)
|
||||||
listNodesCmd.Flags().StringP("user", "n", "", "Filter by user")
|
listNodesCmd.Flags().StringP("user", "u", "", "Filter by user")
|
||||||
listNodesCmd.Flags().BoolP("tags", "t", false, "Show tags")
|
listNodesCmd.Flags().BoolP("tags", "t", false, "Show tags")
|
||||||
|
|
||||||
|
listNodesCmd.Flags().StringP("namespace", "n", "", "User")
|
||||||
|
listNodesNamespaceFlag := listNodesCmd.Flags().Lookup("namespace")
|
||||||
|
listNodesNamespaceFlag.Deprecated = deprecateNamespaceMessage
|
||||||
|
listNodesNamespaceFlag.Hidden = true
|
||||||
|
|
||||||
nodeCmd.AddCommand(listNodesCmd)
|
nodeCmd.AddCommand(listNodesCmd)
|
||||||
|
|
||||||
registerNodeCmd.Flags().StringP("user", "n", "", "User")
|
registerNodeCmd.Flags().StringP("user", "u", "", "User")
|
||||||
|
|
||||||
|
registerNodeCmd.Flags().StringP("namespace", "n", "", "User")
|
||||||
|
registerNodeNamespaceFlag := registerNodeCmd.Flags().Lookup("namespace")
|
||||||
|
registerNodeNamespaceFlag.Deprecated = deprecateNamespaceMessage
|
||||||
|
registerNodeNamespaceFlag.Hidden = true
|
||||||
|
|
||||||
err := registerNodeCmd.MarkFlagRequired("user")
|
err := registerNodeCmd.MarkFlagRequired("user")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf(err.Error())
|
log.Fatalf(err.Error())
|
||||||
@@ -63,7 +75,12 @@ func init() {
|
|||||||
log.Fatalf(err.Error())
|
log.Fatalf(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
moveNodeCmd.Flags().StringP("user", "n", "", "New user")
|
moveNodeCmd.Flags().StringP("user", "u", "", "New user")
|
||||||
|
|
||||||
|
moveNodeCmd.Flags().StringP("namespace", "n", "", "User")
|
||||||
|
moveNodeNamespaceFlag := moveNodeCmd.Flags().Lookup("namespace")
|
||||||
|
moveNodeNamespaceFlag.Deprecated = deprecateNamespaceMessage
|
||||||
|
moveNodeNamespaceFlag.Hidden = true
|
||||||
|
|
||||||
err = moveNodeCmd.MarkFlagRequired("user")
|
err = moveNodeCmd.MarkFlagRequired("user")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -20,7 +20,13 @@ const (
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(preauthkeysCmd)
|
rootCmd.AddCommand(preauthkeysCmd)
|
||||||
preauthkeysCmd.PersistentFlags().StringP("user", "n", "", "User")
|
preauthkeysCmd.PersistentFlags().StringP("user", "u", "", "User")
|
||||||
|
|
||||||
|
preauthkeysCmd.PersistentFlags().StringP("namespace", "n", "", "User")
|
||||||
|
pakNamespaceFlag := preauthkeysCmd.PersistentFlags().Lookup("namespace")
|
||||||
|
pakNamespaceFlag.Deprecated = deprecateNamespaceMessage
|
||||||
|
pakNamespaceFlag.Hidden = true
|
||||||
|
|
||||||
err := preauthkeysCmd.MarkPersistentFlagRequired("user")
|
err := preauthkeysCmd.MarkPersistentFlagRequired("user")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("")
|
log.Fatal().Err(err).Msg("")
|
||||||
|
@@ -12,10 +12,15 @@ import (
|
|||||||
"github.com/tcnksm/go-latest"
|
"github.com/tcnksm/go-latest"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
deprecateNamespaceMessage = "use --user"
|
||||||
|
)
|
||||||
|
|
||||||
var cfgFile string = ""
|
var cfgFile string = ""
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
if len(os.Args) > 1 && (os.Args[1] == "version" || os.Args[1] == "mockoidc" || os.Args[1] == "completion") {
|
if len(os.Args) > 1 &&
|
||||||
|
(os.Args[1] == "version" || os.Args[1] == "mockoidc" || os.Args[1] == "completion") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -3,8 +3,10 @@ package cli
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"net/netip"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/juanfont/headscale"
|
||||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||||
"github.com/pterm/pterm"
|
"github.com/pterm/pterm"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -218,6 +220,19 @@ func routesToPtables(routes []*v1.Route) pterm.TableData {
|
|||||||
tableData := pterm.TableData{{"ID", "Machine", "Prefix", "Advertised", "Enabled", "Primary"}}
|
tableData := pterm.TableData{{"ID", "Machine", "Prefix", "Advertised", "Enabled", "Primary"}}
|
||||||
|
|
||||||
for _, route := range routes {
|
for _, route := range routes {
|
||||||
|
var isPrimaryStr string
|
||||||
|
prefix, err := netip.ParsePrefix(route.Prefix)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error parsing prefix %s: %s", route.Prefix, err)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if prefix == headscale.ExitRouteV4 || prefix == headscale.ExitRouteV6 {
|
||||||
|
isPrimaryStr = "-"
|
||||||
|
} else {
|
||||||
|
isPrimaryStr = strconv.FormatBool(route.IsPrimary)
|
||||||
|
}
|
||||||
|
|
||||||
tableData = append(tableData,
|
tableData = append(tableData,
|
||||||
[]string{
|
[]string{
|
||||||
strconv.FormatUint(route.Id, Base10),
|
strconv.FormatUint(route.Id, Base10),
|
||||||
@@ -225,7 +240,7 @@ func routesToPtables(routes []*v1.Route) pterm.TableData {
|
|||||||
route.Prefix,
|
route.Prefix,
|
||||||
strconv.FormatBool(route.Advertised),
|
strconv.FormatBool(route.Advertised),
|
||||||
strconv.FormatBool(route.Enabled),
|
strconv.FormatBool(route.Enabled),
|
||||||
strconv.FormatBool(route.IsPrimary),
|
isPrimaryStr,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -27,7 +27,7 @@ const (
|
|||||||
var userCmd = &cobra.Command{
|
var userCmd = &cobra.Command{
|
||||||
Use: "users",
|
Use: "users",
|
||||||
Short: "Manage the users of Headscale",
|
Short: "Manage the users of Headscale",
|
||||||
Aliases: []string{"user", "namespace", "ns"},
|
Aliases: []string{"user", "namespace", "namespaces", "ns"},
|
||||||
}
|
}
|
||||||
|
|
||||||
var createUserCmd = &cobra.Command{
|
var createUserCmd = &cobra.Command{
|
||||||
|
@@ -282,27 +282,38 @@ unix_socket_permission: "0770"
|
|||||||
# client_secret_path: "${CREDENTIALS_DIRECTORY}/oidc_client_secret"
|
# client_secret_path: "${CREDENTIALS_DIRECTORY}/oidc_client_secret"
|
||||||
# # client_secret and client_secret_path are mutually exclusive.
|
# # client_secret and client_secret_path are mutually exclusive.
|
||||||
#
|
#
|
||||||
# Customize the scopes used in the OIDC flow, defaults to "openid", "profile" and "email" and add custom query
|
# # The amount of time from a node is authenticated with OpenID until it
|
||||||
# parameters to the Authorize Endpoint request. Scopes default to "openid", "profile" and "email".
|
# # expires and needs to reauthenticate.
|
||||||
|
# # Setting the value to "0" will mean no expiry.
|
||||||
|
# expiry: 180d
|
||||||
|
#
|
||||||
|
# # Use the expiry from the token received from OpenID when the user logged
|
||||||
|
# # in, this will typically lead to frequent need to reauthenticate and should
|
||||||
|
# # only been enabled if you know what you are doing.
|
||||||
|
# # Note: enabling this will cause `oidc.expiry` to be ignored.
|
||||||
|
# use_expiry_from_token: false
|
||||||
|
#
|
||||||
|
# # Customize the scopes used in the OIDC flow, defaults to "openid", "profile" and "email" and add custom query
|
||||||
|
# # parameters to the Authorize Endpoint request. Scopes default to "openid", "profile" and "email".
|
||||||
#
|
#
|
||||||
# scope: ["openid", "profile", "email", "custom"]
|
# scope: ["openid", "profile", "email", "custom"]
|
||||||
# extra_params:
|
# extra_params:
|
||||||
# domain_hint: example.com
|
# domain_hint: example.com
|
||||||
#
|
#
|
||||||
# List allowed principal domains and/or users. If an authenticated user's domain is not in this list, the
|
# # List allowed principal domains and/or users. If an authenticated user's domain is not in this list, the
|
||||||
# authentication request will be rejected.
|
# # authentication request will be rejected.
|
||||||
#
|
#
|
||||||
# allowed_domains:
|
# allowed_domains:
|
||||||
# - example.com
|
# - example.com
|
||||||
# Groups from keycloak have a leading '/'
|
# # Note: Groups from keycloak have a leading '/'
|
||||||
# allowed_groups:
|
# allowed_groups:
|
||||||
# - /headscale
|
# - /headscale
|
||||||
# allowed_users:
|
# allowed_users:
|
||||||
# - alice@example.com
|
# - alice@example.com
|
||||||
#
|
#
|
||||||
# If `strip_email_domain` is set to `true`, the domain part of the username email address will be removed.
|
# # If `strip_email_domain` is set to `true`, the domain part of the username email address will be removed.
|
||||||
# This will transform `first-name.last-name@example.com` to the user `first-name.last-name`
|
# # This will transform `first-name.last-name@example.com` to the user `first-name.last-name`
|
||||||
# If `strip_email_domain` is set to `false` the domain part will NOT be removed resulting to the following
|
# # If `strip_email_domain` is set to `false` the domain part will NOT be removed resulting to the following
|
||||||
# user: `first-name.last-name.example.com`
|
# user: `first-name.last-name.example.com`
|
||||||
#
|
#
|
||||||
# strip_email_domain: true
|
# strip_email_domain: true
|
||||||
|
36
config.go
36
config.go
@@ -11,6 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
|
"github.com/prometheus/common/model"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
@@ -25,9 +26,14 @@ const (
|
|||||||
|
|
||||||
JSONLogFormat = "json"
|
JSONLogFormat = "json"
|
||||||
TextLogFormat = "text"
|
TextLogFormat = "text"
|
||||||
|
|
||||||
|
defaultOIDCExpiryTime = 180 * 24 * time.Hour // 180 Days
|
||||||
|
maxDuration time.Duration = 1<<63 - 1
|
||||||
)
|
)
|
||||||
|
|
||||||
var errOidcMutuallyExclusive = errors.New("oidc_client_secret and oidc_client_secret_path are mutually exclusive")
|
var errOidcMutuallyExclusive = errors.New(
|
||||||
|
"oidc_client_secret and oidc_client_secret_path are mutually exclusive",
|
||||||
|
)
|
||||||
|
|
||||||
// Config contains the initial Headscale configuration.
|
// Config contains the initial Headscale configuration.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@@ -101,6 +107,8 @@ type OIDCConfig struct {
|
|||||||
AllowedUsers []string
|
AllowedUsers []string
|
||||||
AllowedGroups []string
|
AllowedGroups []string
|
||||||
StripEmaildomain bool
|
StripEmaildomain bool
|
||||||
|
Expiry time.Duration
|
||||||
|
UseExpiryFromToken bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type DERPConfig struct {
|
type DERPConfig struct {
|
||||||
@@ -180,6 +188,8 @@ func LoadConfig(path string, isFile bool) error {
|
|||||||
viper.SetDefault("oidc.scope", []string{oidc.ScopeOpenID, "profile", "email"})
|
viper.SetDefault("oidc.scope", []string{oidc.ScopeOpenID, "profile", "email"})
|
||||||
viper.SetDefault("oidc.strip_email_domain", true)
|
viper.SetDefault("oidc.strip_email_domain", true)
|
||||||
viper.SetDefault("oidc.only_start_if_oidc_is_available", true)
|
viper.SetDefault("oidc.only_start_if_oidc_is_available", true)
|
||||||
|
viper.SetDefault("oidc.expiry", "180d")
|
||||||
|
viper.SetDefault("oidc.use_expiry_from_token", false)
|
||||||
|
|
||||||
viper.SetDefault("logtail.enabled", false)
|
viper.SetDefault("logtail.enabled", false)
|
||||||
viper.SetDefault("randomize_client_port", false)
|
viper.SetDefault("randomize_client_port", false)
|
||||||
@@ -411,8 +421,8 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if viper.IsSet("dns_config.restricted_nameservers") {
|
if viper.IsSet("dns_config.restricted_nameservers") {
|
||||||
if len(dnsConfig.Resolvers) > 0 {
|
|
||||||
dnsConfig.Routes = make(map[string][]*dnstype.Resolver)
|
dnsConfig.Routes = make(map[string][]*dnstype.Resolver)
|
||||||
|
domains := []string{}
|
||||||
restrictedDNS := viper.GetStringMapStringSlice(
|
restrictedDNS := viper.GetStringMapStringSlice(
|
||||||
"dns_config.restricted_nameservers",
|
"dns_config.restricted_nameservers",
|
||||||
)
|
)
|
||||||
@@ -434,11 +444,9 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
dnsConfig.Routes[domain] = restrictedResolvers
|
dnsConfig.Routes[domain] = restrictedResolvers
|
||||||
|
domains = append(domains, domain)
|
||||||
}
|
}
|
||||||
} else {
|
dnsConfig.Domains = domains
|
||||||
log.Warn().
|
|
||||||
Msg("Warning: dns_config.restricted_nameservers is set, but no nameservers are configured. Ignoring restricted_nameservers.")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if viper.IsSet("dns_config.domains") {
|
if viper.IsSet("dns_config.domains") {
|
||||||
@@ -603,6 +611,22 @@ func GetHeadscaleConfig() (*Config, error) {
|
|||||||
AllowedUsers: viper.GetStringSlice("oidc.allowed_users"),
|
AllowedUsers: viper.GetStringSlice("oidc.allowed_users"),
|
||||||
AllowedGroups: viper.GetStringSlice("oidc.allowed_groups"),
|
AllowedGroups: viper.GetStringSlice("oidc.allowed_groups"),
|
||||||
StripEmaildomain: viper.GetBool("oidc.strip_email_domain"),
|
StripEmaildomain: viper.GetBool("oidc.strip_email_domain"),
|
||||||
|
Expiry: func() time.Duration {
|
||||||
|
// if set to 0, we assume no expiry
|
||||||
|
if value := viper.GetString("oidc.expiry"); value == "0" {
|
||||||
|
return maxDuration
|
||||||
|
} else {
|
||||||
|
expiry, err := model.ParseDuration(value)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Msg("failed to parse oidc.expiry, defaulting back to 180 days")
|
||||||
|
|
||||||
|
return defaultOIDCExpiryTime
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Duration(expiry)
|
||||||
|
}
|
||||||
|
}(),
|
||||||
|
UseExpiryFromToken: viper.GetBool("oidc.use_expiry_from_token"),
|
||||||
},
|
},
|
||||||
|
|
||||||
LogTail: logConfig,
|
LogTail: logConfig,
|
||||||
|
3
db.go
3
db.go
@@ -48,6 +48,9 @@ func (h *Headscale) initDB() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = db.Migrator().RenameColumn(&Machine{}, "namespace_id", "user_id")
|
||||||
|
_ = db.Migrator().RenameColumn(&PreAuthKey{}, "namespace_id", "user_id")
|
||||||
|
|
||||||
_ = db.Migrator().RenameColumn(&Machine{}, "ip_address", "ip_addresses")
|
_ = db.Migrator().RenameColumn(&Machine{}, "ip_address", "ip_addresses")
|
||||||
_ = db.Migrator().RenameColumn(&Machine{}, "name", "hostname")
|
_ = db.Migrator().RenameColumn(&Machine{}, "name", "hostname")
|
||||||
|
|
||||||
|
@@ -157,14 +157,14 @@ func (h *Headscale) DERPHandler(
|
|||||||
|
|
||||||
if !fastStart {
|
if !fastStart {
|
||||||
pubKey := h.privateKey.Public()
|
pubKey := h.privateKey.Public()
|
||||||
pubKeyStr := pubKey.UntypedHexString() //nolint
|
pubKeyStr, _ := pubKey.MarshalText() //nolint
|
||||||
fmt.Fprintf(conn, "HTTP/1.1 101 Switching Protocols\r\n"+
|
fmt.Fprintf(conn, "HTTP/1.1 101 Switching Protocols\r\n"+
|
||||||
"Upgrade: DERP\r\n"+
|
"Upgrade: DERP\r\n"+
|
||||||
"Connection: Upgrade\r\n"+
|
"Connection: Upgrade\r\n"+
|
||||||
"Derp-Version: %v\r\n"+
|
"Derp-Version: %v\r\n"+
|
||||||
"Derp-Public-Key: %s\r\n\r\n",
|
"Derp-Public-Key: %s\r\n\r\n",
|
||||||
derp.ProtocolVersion,
|
derp.ProtocolVersion,
|
||||||
pubKeyStr)
|
string(pubKeyStr))
|
||||||
}
|
}
|
||||||
|
|
||||||
h.DERPServer.tailscaleDERP.Accept(req.Context(), netConn, conn, netConn.RemoteAddr().String())
|
h.DERPServer.tailscaleDERP.Accept(req.Context(), netConn, conn, netConn.RemoteAddr().String())
|
||||||
|
31
docs/oidc.md
31
docs/oidc.md
@@ -139,3 +139,34 @@ oidc:
|
|||||||
# Optional: Force the Azure AD account picker
|
# Optional: Force the Azure AD account picker
|
||||||
prompt: select_account
|
prompt: select_account
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Google OAuth Example
|
||||||
|
|
||||||
|
In order to integrate Headscale with Google, you'll need to have a [Google Cloud Console](https://console.cloud.google.com) account.
|
||||||
|
|
||||||
|
Google OAuth has a [verification process](https://support.google.com/cloud/answer/9110914?hl=en) if you need to have users authenticate who are outside of your domain. If you only need to authenticate users from your domain name (ie `@example.com`), you don't need to go through the verification process.
|
||||||
|
|
||||||
|
However if you don't have a domain, or need to add users outside of your domain, you can manually add emails via Google Console.
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
1. Go to [Google Console](https://console.cloud.google.com) and login or create an account if you don't have one.
|
||||||
|
2. Create a project (if you don't already have one).
|
||||||
|
3. On the left hand menu, go to `APIs and services` -> `Credentials`
|
||||||
|
4. Click `Create Credentials` -> `OAuth client ID`
|
||||||
|
5. Under `Application Type`, choose `Web Application`
|
||||||
|
6. For `Name`, enter whatever you like
|
||||||
|
7. Under `Authorised redirect URIs`, use `https://example.com/oidc/callback`, replacing example.com with your Headscale URL.
|
||||||
|
8. Click `Save` at the bottom of the form
|
||||||
|
9. Take note of the `Client ID` and `Client secret`, you can also download it for reference if you need it.
|
||||||
|
10. Edit your headscale config, under `oidc`, filling in your `client_id` and `client_secret`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
oidc:
|
||||||
|
issuer: "https://accounts.google.com"
|
||||||
|
client_id: ""
|
||||||
|
client_secret: ""
|
||||||
|
scope: ["openid", "profile", "email"]
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also use `allowed_domains` and `allowed_users` to restrict the users who can authenticate.
|
||||||
|
@@ -115,6 +115,17 @@ For a slightly more complex configuration which utilizes Docker containers to ma
|
|||||||
|
|
||||||
## Apache
|
## Apache
|
||||||
|
|
||||||
Apache is NOT supported. It will not work. Apache [overwrites](https://github.com/svn2github/apache-httpd/blob/82779fce1be478e2333afc9fef86d34f88db718b/modules/proxy/mod_proxy_wstunnel.c#L354) the custom upgrade header of thw WebSockets connection, which is required for the Tailscale TS2021 protocol.
|
The following minimal Apache config will proxy traffic to the Headscale instance on `<IP:PORT>`. Note that `upgrade=any` is required as a parameter for `ProxyPass` so that WebSockets traffic whose `Upgrade` header value is not equal to `WebSocket` (i. e. Tailscale Control Protocol) is forwarded correctly. See the [Apache docs](https://httpd.apache.org/docs/2.4/mod/mod_proxy_wstunnel.html) for more information on this.
|
||||||
|
|
||||||
Please use any other reverse proxy.
|
```
|
||||||
|
<VirtualHost *:443>
|
||||||
|
ServerName <YOUR_SERVER_NAME>
|
||||||
|
|
||||||
|
ProxyPreserveHost On
|
||||||
|
ProxyPass / http://<IP:PORT>/ upgrade=any
|
||||||
|
|
||||||
|
SSLEngine On
|
||||||
|
SSLCertificateFile <PATH_TO_CERT>
|
||||||
|
SSLCertificateKeyFile <PATH_CERT_KEY>
|
||||||
|
</VirtualHost>
|
||||||
|
```
|
||||||
|
@@ -138,6 +138,7 @@ NoNewPrivileges=yes
|
|||||||
PrivateTmp=yes
|
PrivateTmp=yes
|
||||||
ProtectSystem=strict
|
ProtectSystem=strict
|
||||||
ProtectHome=yes
|
ProtectHome=yes
|
||||||
|
WorkingDirectory=/var/lib/headscale
|
||||||
ReadWritePaths=/var/lib/headscale /var/run/headscale
|
ReadWritePaths=/var/lib/headscale /var/run/headscale
|
||||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||||
RuntimeDirectory=headscale
|
RuntimeDirectory=headscale
|
||||||
|
@@ -10,17 +10,14 @@ describing how to make `headscale` run properly in a server environment.
|
|||||||
|
|
||||||
1. Install from ports (Not Recommend)
|
1. Install from ports (Not Recommend)
|
||||||
|
|
||||||
As of OpenBSD 7.1, there's a headscale in ports collection, however, it's severely outdated(v0.12.4).
|
As of OpenBSD 7.2, there's a headscale in ports collection, however, it's severely outdated(v0.12.4).
|
||||||
You can install it via `pkg_add headscale`.
|
You can install it via `pkg_add headscale`.
|
||||||
|
|
||||||
2. Install from source on OpenBSD 7.1
|
2. Install from source on OpenBSD 7.2
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# Install prerequistes
|
# Install prerequistes
|
||||||
# 1. go v1.19+: headscale newer than 0.17 needs go 1.19+ to compile
|
pkg_add go
|
||||||
# 2. gmake: Makefile in the headscale repo is written in GNU make syntax
|
|
||||||
pkg_add -D snap go
|
|
||||||
pkg_add gmake
|
|
||||||
|
|
||||||
git clone https://github.com/juanfont/headscale.git
|
git clone https://github.com/juanfont/headscale.git
|
||||||
|
|
||||||
@@ -33,7 +30,7 @@ latestTag=$(git describe --tags `git rev-list --tags --max-count=1`)
|
|||||||
|
|
||||||
git checkout $latestTag
|
git checkout $latestTag
|
||||||
|
|
||||||
gmake build
|
go build -ldflags="-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$latestTag" github.com/juanfont/headscale
|
||||||
|
|
||||||
# make it executable
|
# make it executable
|
||||||
chmod a+x headscale
|
chmod a+x headscale
|
||||||
|
48
flake.nix
48
flake.nix
@@ -6,36 +6,39 @@
|
|||||||
flake-utils.url = "github:numtide/flake-utils";
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = {
|
outputs =
|
||||||
self,
|
{ self
|
||||||
nixpkgs,
|
, nixpkgs
|
||||||
flake-utils,
|
, flake-utils
|
||||||
...
|
, ...
|
||||||
}: let
|
}:
|
||||||
|
let
|
||||||
headscaleVersion =
|
headscaleVersion =
|
||||||
if (self ? shortRev)
|
if (self ? shortRev)
|
||||||
then self.shortRev
|
then self.shortRev
|
||||||
else "dev";
|
else "dev";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
overlay = _: prev: let
|
overlay = _: prev:
|
||||||
|
let
|
||||||
pkgs = nixpkgs.legacyPackages.${prev.system};
|
pkgs = nixpkgs.legacyPackages.${prev.system};
|
||||||
in rec {
|
in
|
||||||
|
rec {
|
||||||
headscale = pkgs.buildGo119Module rec {
|
headscale = pkgs.buildGo119Module rec {
|
||||||
pname = "headscale";
|
pname = "headscale";
|
||||||
version = headscaleVersion;
|
version = headscaleVersion;
|
||||||
src = pkgs.lib.cleanSource self;
|
src = pkgs.lib.cleanSource self;
|
||||||
|
|
||||||
tags = ["ts2019"];
|
tags = [ "ts2019" ];
|
||||||
|
|
||||||
# Only run unit tests when testing a build
|
# Only run unit tests when testing a build
|
||||||
checkFlags = ["-short"];
|
checkFlags = [ "-short" ];
|
||||||
|
|
||||||
# When updating go.mod or go.sum, a new sha will need to be calculated,
|
# When updating go.mod or go.sum, a new sha will need to be calculated,
|
||||||
# update this if you have a mismatch after doing a change to thos files.
|
# update this if you have a mismatch after doing a change to thos files.
|
||||||
vendorSha256 = "sha256-SuKT+b8g6xEK15ry2IAmpS/vwDG+zJqK9nfsWpHNXuU=";
|
vendorSha256 = "sha256-QkBBzyKthDnjPCYtGDeQtW46FyWfScRczBQEhAdKHbs=";
|
||||||
|
|
||||||
ldflags = ["-s" "-w" "-X github.com/juanfont/headscale/cmd/headscale/cli.Version=v${version}"];
|
ldflags = [ "-s" "-w" "-X github.com/juanfont/headscale/cmd/headscale/cli.Version=v${version}" ];
|
||||||
};
|
};
|
||||||
|
|
||||||
golines = pkgs.buildGoModule rec {
|
golines = pkgs.buildGoModule rec {
|
||||||
@@ -51,7 +54,7 @@
|
|||||||
|
|
||||||
vendorSha256 = "sha256-rxYuzn4ezAxaeDhxd8qdOzt+CKYIh03A9zKNdzILq18=";
|
vendorSha256 = "sha256-rxYuzn4ezAxaeDhxd8qdOzt+CKYIh03A9zKNdzILq18=";
|
||||||
|
|
||||||
nativeBuildInputs = [pkgs.installShellFiles];
|
nativeBuildInputs = [ pkgs.installShellFiles ];
|
||||||
};
|
};
|
||||||
|
|
||||||
golangci-lint = prev.golangci-lint.override {
|
golangci-lint = prev.golangci-lint.override {
|
||||||
@@ -73,19 +76,20 @@
|
|||||||
|
|
||||||
vendorSha256 = "sha256-dGdnDuRbwg8fU7uB5GaHEWa/zI3w06onqjturvooJQA=";
|
vendorSha256 = "sha256-dGdnDuRbwg8fU7uB5GaHEWa/zI3w06onqjturvooJQA=";
|
||||||
|
|
||||||
nativeBuildInputs = [pkgs.installShellFiles];
|
nativeBuildInputs = [ pkgs.installShellFiles ];
|
||||||
|
|
||||||
subPackages = ["protoc-gen-grpc-gateway" "protoc-gen-openapiv2"];
|
subPackages = [ "protoc-gen-grpc-gateway" "protoc-gen-openapiv2" ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// flake-utils.lib.eachDefaultSystem
|
// flake-utils.lib.eachDefaultSystem
|
||||||
(system: let
|
(system:
|
||||||
|
let
|
||||||
pkgs = import nixpkgs {
|
pkgs = import nixpkgs {
|
||||||
overlays = [self.overlay];
|
overlays = [ self.overlay ];
|
||||||
inherit system;
|
inherit system;
|
||||||
};
|
};
|
||||||
buildDeps = with pkgs; [git go_1_19 gnumake];
|
buildDeps = with pkgs; [ git go_1_19 gnumake ];
|
||||||
devDeps = with pkgs;
|
devDeps = with pkgs;
|
||||||
buildDeps
|
buildDeps
|
||||||
++ [
|
++ [
|
||||||
@@ -93,6 +97,7 @@
|
|||||||
golines
|
golines
|
||||||
nodePackages.prettier
|
nodePackages.prettier
|
||||||
goreleaser
|
goreleaser
|
||||||
|
gotestsum
|
||||||
|
|
||||||
# Protobuf dependencies
|
# Protobuf dependencies
|
||||||
protobuf
|
protobuf
|
||||||
@@ -112,10 +117,11 @@
|
|||||||
headscale-docker = pkgs.dockerTools.buildLayeredImage {
|
headscale-docker = pkgs.dockerTools.buildLayeredImage {
|
||||||
name = "headscale";
|
name = "headscale";
|
||||||
tag = headscaleVersion;
|
tag = headscaleVersion;
|
||||||
contents = [pkgs.headscale];
|
contents = [ pkgs.headscale ];
|
||||||
config.Entrypoint = [(pkgs.headscale + "/bin/headscale")];
|
config.Entrypoint = [ (pkgs.headscale + "/bin/headscale") ];
|
||||||
};
|
};
|
||||||
in rec {
|
in
|
||||||
|
rec {
|
||||||
# `nix develop`
|
# `nix develop`
|
||||||
devShell = pkgs.mkShell {
|
devShell = pkgs.mkShell {
|
||||||
buildInputs = devDeps;
|
buildInputs = devDeps;
|
||||||
|
112
go.mod
112
go.mod
@@ -6,50 +6,52 @@ require (
|
|||||||
github.com/AlecAivazis/survey/v2 v2.3.6
|
github.com/AlecAivazis/survey/v2 v2.3.6
|
||||||
github.com/ccding/go-stun/stun v0.0.0-20200514191101-4dc67bcdb029
|
github.com/ccding/go-stun/stun v0.0.0-20200514191101-4dc67bcdb029
|
||||||
github.com/cenkalti/backoff/v4 v4.2.0
|
github.com/cenkalti/backoff/v4 v4.2.0
|
||||||
github.com/coreos/go-oidc/v3 v3.4.0
|
github.com/coreos/go-oidc/v3 v3.5.0
|
||||||
|
github.com/davecgh/go-spew v1.1.1
|
||||||
github.com/deckarep/golang-set/v2 v2.1.0
|
github.com/deckarep/golang-set/v2 v2.1.0
|
||||||
github.com/efekarakus/termcolor v1.0.1
|
github.com/efekarakus/termcolor v1.0.1
|
||||||
github.com/glebarez/sqlite v1.5.0
|
github.com/glebarez/sqlite v1.7.0
|
||||||
github.com/gofrs/uuid v4.3.1+incompatible
|
github.com/gofrs/uuid v4.4.0+incompatible
|
||||||
github.com/gorilla/mux v1.8.0
|
github.com/gorilla/mux v1.8.0
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
|
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.14.0
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.1
|
||||||
github.com/klauspost/compress v1.15.12
|
github.com/klauspost/compress v1.15.15
|
||||||
github.com/oauth2-proxy/mockoidc v0.0.0-20220308204021-b9169deeb282
|
github.com/oauth2-proxy/mockoidc v0.0.0-20220308204021-b9169deeb282
|
||||||
github.com/ory/dockertest/v3 v3.9.1
|
github.com/ory/dockertest/v3 v3.9.1
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
github.com/philip-bui/grpc-zerolog v1.0.1
|
github.com/philip-bui/grpc-zerolog v1.0.1
|
||||||
github.com/prometheus/client_golang v1.14.0
|
github.com/prometheus/client_golang v1.14.0
|
||||||
github.com/prometheus/common v0.37.0
|
github.com/prometheus/common v0.40.0
|
||||||
github.com/pterm/pterm v0.12.50
|
github.com/pterm/pterm v0.12.54
|
||||||
github.com/puzpuzpuz/xsync/v2 v2.4.0
|
github.com/puzpuzpuz/xsync/v2 v2.4.0
|
||||||
github.com/rs/zerolog v1.28.0
|
github.com/rs/zerolog v1.29.0
|
||||||
|
github.com/samber/lo v1.37.0
|
||||||
github.com/spf13/cobra v1.6.1
|
github.com/spf13/cobra v1.6.1
|
||||||
github.com/spf13/viper v1.14.0
|
github.com/spf13/viper v1.15.0
|
||||||
github.com/stretchr/testify v1.8.1
|
github.com/stretchr/testify v1.8.2
|
||||||
github.com/tailscale/hujson v0.0.0-20220630195928-54599719472f
|
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a
|
||||||
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
|
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
|
||||||
go4.org/netipx v0.0.0-20220925034521-797b0c90d8ab
|
go4.org/netipx v0.0.0-20230125063823-8449b0a6169f
|
||||||
golang.org/x/crypto v0.3.0
|
golang.org/x/crypto v0.6.0
|
||||||
golang.org/x/net v0.2.0
|
golang.org/x/net v0.7.0
|
||||||
golang.org/x/oauth2 v0.2.0
|
golang.org/x/oauth2 v0.5.0
|
||||||
golang.org/x/sync v0.1.0
|
golang.org/x/sync v0.1.0
|
||||||
google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd
|
google.golang.org/genproto v0.0.0-20230223222841-637eb2293923
|
||||||
google.golang.org/grpc v1.51.0
|
google.golang.org/grpc v1.53.0
|
||||||
google.golang.org/protobuf v1.28.1
|
google.golang.org/protobuf v1.28.1
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
gorm.io/driver/postgres v1.4.5
|
gorm.io/driver/postgres v1.4.8
|
||||||
gorm.io/gorm v1.24.2
|
gorm.io/gorm v1.24.5
|
||||||
tailscale.com v1.34.0
|
tailscale.com v1.36.2
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
atomicgo.dev/cursor v0.1.1 // indirect
|
atomicgo.dev/cursor v0.1.1 // indirect
|
||||||
atomicgo.dev/keyboard v0.2.8 // indirect
|
atomicgo.dev/keyboard v0.2.9 // indirect
|
||||||
filippo.io/edwards25519 v1.0.0 // indirect
|
filippo.io/edwards25519 v1.0.0 // indirect
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.0 // indirect
|
github.com/Microsoft/go-winio v0.6.0 // indirect
|
||||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
||||||
github.com/akutz/memconn v0.1.0 // indirect
|
github.com/akutz/memconn v0.1.0 // indirect
|
||||||
@@ -58,16 +60,18 @@ require (
|
|||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
github.com/containerd/console v1.0.3 // indirect
|
github.com/containerd/console v1.0.3 // indirect
|
||||||
github.com/containerd/continuity v0.3.0 // indirect
|
github.com/containerd/continuity v0.3.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/docker/cli v23.0.1+incompatible // indirect
|
||||||
github.com/docker/cli v20.10.21+incompatible // indirect
|
github.com/docker/docker v23.0.1+incompatible // indirect
|
||||||
github.com/docker/docker v20.10.21+incompatible // indirect
|
|
||||||
github.com/docker/go-connections v0.4.0 // indirect
|
github.com/docker/go-connections v0.4.0 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
|
||||||
github.com/glebarez/go-sqlite v1.19.5 // indirect
|
github.com/glebarez/go-sqlite v1.20.3 // indirect
|
||||||
|
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||||
|
github.com/golang/glog v1.0.0 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
github.com/google/go-cmp v0.5.9 // indirect
|
github.com/google/go-cmp v0.5.9 // indirect
|
||||||
@@ -81,71 +85,69 @@ require (
|
|||||||
github.com/hdevalence/ed25519consensus v0.1.0 // indirect
|
github.com/hdevalence/ed25519consensus v0.1.0 // indirect
|
||||||
github.com/imdario/mergo v0.3.13 // indirect
|
github.com/imdario/mergo v0.3.13 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
|
||||||
github.com/jackc/pgconn v1.13.0 // indirect
|
|
||||||
github.com/jackc/pgio v1.0.0 // indirect
|
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgproto3/v2 v2.3.1 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
github.com/jackc/pgx/v5 v5.3.0 // indirect
|
||||||
github.com/jackc/pgtype v1.13.0 // indirect
|
|
||||||
github.com/jackc/pgx/v4 v4.17.2 // indirect
|
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/josharian/native v1.0.0 // indirect
|
github.com/josharian/native v1.1.0 // indirect
|
||||||
github.com/jsimonetti/rtnetlink v1.3.0 // indirect
|
github.com/jsimonetti/rtnetlink v1.3.1 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||||
github.com/kr/pretty v0.3.0 // indirect
|
github.com/kr/pretty v0.3.1 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
|
github.com/lib/pq v1.10.7 // indirect
|
||||||
github.com/lithammer/fuzzysearch v1.1.5 // indirect
|
github.com/lithammer/fuzzysearch v1.1.5 // indirect
|
||||||
github.com/magiconair/properties v1.8.6 // indirect
|
github.com/magiconair/properties v1.8.7 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||||
github.com/mdlayher/netlink v1.7.0 // indirect
|
github.com/mdlayher/netlink v1.7.1 // indirect
|
||||||
github.com/mdlayher/socket v0.4.0 // indirect
|
github.com/mdlayher/socket v0.4.0 // indirect
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/moby/term v0.0.0-20221128092401-c43b287e0e0f // indirect
|
github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.1.0-rc2 // indirect
|
github.com/opencontainers/image-spec v1.1.0-rc2 // indirect
|
||||||
github.com/opencontainers/runc v1.1.4 // indirect
|
github.com/opencontainers/runc v1.1.4 // indirect
|
||||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
|
||||||
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
|
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_model v0.3.0 // indirect
|
github.com/prometheus/client_model v0.3.0 // indirect
|
||||||
github.com/prometheus/procfs v0.8.0 // indirect
|
github.com/prometheus/procfs v0.9.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rivo/uniseg v0.4.3 // indirect
|
github.com/rivo/uniseg v0.4.4 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 // indirect
|
github.com/rogpeppe/go-internal v1.9.0 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||||
github.com/spf13/afero v1.9.3 // indirect
|
github.com/spf13/afero v1.9.4 // indirect
|
||||||
github.com/spf13/cast v1.5.0 // indirect
|
github.com/spf13/cast v1.5.0 // indirect
|
||||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/subosito/gotenv v1.4.1 // indirect
|
github.com/subosito/gotenv v1.4.2 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect
|
go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect
|
||||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 // indirect
|
golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 // indirect
|
||||||
golang.org/x/mod v0.7.0 // indirect
|
golang.org/x/mod v0.8.0 // indirect
|
||||||
golang.org/x/sys v0.3.0 // indirect
|
golang.org/x/sys v0.5.0 // indirect
|
||||||
golang.org/x/term v0.2.0 // indirect
|
golang.org/x/term v0.5.0 // indirect
|
||||||
golang.org/x/text v0.5.0 // indirect
|
golang.org/x/text v0.7.0 // indirect
|
||||||
golang.org/x/time v0.3.0 // indirect
|
golang.org/x/time v0.3.0 // indirect
|
||||||
golang.org/x/tools v0.3.0 // indirect
|
golang.org/x/tools v0.6.0 // indirect
|
||||||
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
|
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||||
modernc.org/libc v1.21.5 // indirect
|
modernc.org/libc v1.22.2 // indirect
|
||||||
modernc.org/mathutil v1.5.0 // indirect
|
modernc.org/mathutil v1.5.0 // indirect
|
||||||
modernc.org/memory v1.5.0 // indirect
|
modernc.org/memory v1.5.0 // indirect
|
||||||
modernc.org/sqlite v1.20.0 // indirect
|
modernc.org/sqlite v1.20.3 // indirect
|
||||||
nhooyr.io/websocket v1.8.7 // indirect
|
nhooyr.io/websocket v1.8.7 // indirect
|
||||||
)
|
)
|
||||||
|
@@ -9,8 +9,17 @@ Headscale's test framework and the current set of scenarios are defined in this
|
|||||||
|
|
||||||
Tests are located in files ending with `_test.go` and the framework are located in the rest.
|
Tests are located in files ending with `_test.go` and the framework are located in the rest.
|
||||||
|
|
||||||
|
## Running integration tests locally
|
||||||
|
|
||||||
|
The easiest way to run tests locally is to use `[act](INSERT LINK)`, a local GitHub Actions runner:
|
||||||
|
|
||||||
|
```
|
||||||
|
act pull_request -W .github/workflows/test-integration-v2-TestPingAllByIP.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, the `docker run` command in each GitHub workflow file can be used.
|
||||||
|
|
||||||
## Running integration tests on GitHub Actions
|
## Running integration tests on GitHub Actions
|
||||||
|
|
||||||
Each test currently runs as a separate workflows in GitHub actions, to add new test, add
|
Each test currently runs as a separate workflows in GitHub actions, to add new test, run
|
||||||
the new test to the list in `../cmd/gh-action-integration-generator/main.go` and run
|
|
||||||
`go generate` inside `../cmd/gh-action-integration-generator/` and commit the result.
|
`go generate` inside `../cmd/gh-action-integration-generator/` and commit the result.
|
||||||
|
@@ -19,6 +19,7 @@ import (
|
|||||||
"github.com/juanfont/headscale/integration/hsic"
|
"github.com/juanfont/headscale/integration/hsic"
|
||||||
"github.com/ory/dockertest/v3"
|
"github.com/ory/dockertest/v3"
|
||||||
"github.com/ory/dockertest/v3/docker"
|
"github.com/ory/dockertest/v3/docker"
|
||||||
|
"github.com/samber/lo"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -91,7 +92,11 @@ func TestOIDCAuthenticationPingAll(t *testing.T) {
|
|||||||
t.Errorf("failed wait for tailscale clients to be in sync: %s", err)
|
t.Errorf("failed wait for tailscale clients to be in sync: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
success := pingAll(t, allClients, allIps)
|
allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string {
|
||||||
|
return x.String()
|
||||||
|
})
|
||||||
|
|
||||||
|
success := pingAllHelper(t, allClients, allAddrs)
|
||||||
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
|
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
|
||||||
|
|
||||||
err = scenario.Shutdown()
|
err = scenario.Shutdown()
|
||||||
@@ -100,7 +105,7 @@ func TestOIDCAuthenticationPingAll(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOIDCExpireNodes(t *testing.T) {
|
func TestOIDCExpireNodesBasedOnTokenExpiry(t *testing.T) {
|
||||||
IntegrationSkip(t)
|
IntegrationSkip(t)
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -129,6 +134,7 @@ func TestOIDCExpireNodes(t *testing.T) {
|
|||||||
"HEADSCALE_OIDC_CLIENT_ID": oidcConfig.ClientID,
|
"HEADSCALE_OIDC_CLIENT_ID": oidcConfig.ClientID,
|
||||||
"HEADSCALE_OIDC_CLIENT_SECRET": oidcConfig.ClientSecret,
|
"HEADSCALE_OIDC_CLIENT_SECRET": oidcConfig.ClientSecret,
|
||||||
"HEADSCALE_OIDC_STRIP_EMAIL_DOMAIN": fmt.Sprintf("%t", oidcConfig.StripEmaildomain),
|
"HEADSCALE_OIDC_STRIP_EMAIL_DOMAIN": fmt.Sprintf("%t", oidcConfig.StripEmaildomain),
|
||||||
|
"HEADSCALE_OIDC_USE_EXPIRY_FROM_TOKEN": "1",
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(
|
err = scenario.CreateHeadscaleEnv(
|
||||||
@@ -156,7 +162,11 @@ func TestOIDCExpireNodes(t *testing.T) {
|
|||||||
t.Errorf("failed wait for tailscale clients to be in sync: %s", err)
|
t.Errorf("failed wait for tailscale clients to be in sync: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
success := pingAll(t, allClients, allIps)
|
allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string {
|
||||||
|
return x.String()
|
||||||
|
})
|
||||||
|
|
||||||
|
success := pingAllHelper(t, allClients, allAddrs)
|
||||||
t.Logf("%d successful pings out of %d (before expiry)", success, len(allClients)*len(allIps))
|
t.Logf("%d successful pings out of %d (before expiry)", success, len(allClients)*len(allIps))
|
||||||
|
|
||||||
// await all nodes being logged out after OIDC token expiry
|
// await all nodes being logged out after OIDC token expiry
|
||||||
@@ -278,7 +288,10 @@ func (s *AuthOIDCScenario) runMockOIDC(accessTTL time.Duration) (*headscale.OIDC
|
|||||||
log.Printf("headscale mock oidc is ready for tests at %s", hostEndpoint)
|
log.Printf("headscale mock oidc is ready for tests at %s", hostEndpoint)
|
||||||
|
|
||||||
return &headscale.OIDCConfig{
|
return &headscale.OIDCConfig{
|
||||||
Issuer: fmt.Sprintf("http://%s/oidc", net.JoinHostPort(s.mockOIDC.GetIPInNetwork(s.network), strconv.Itoa(port))),
|
Issuer: fmt.Sprintf(
|
||||||
|
"http://%s/oidc",
|
||||||
|
net.JoinHostPort(s.mockOIDC.GetIPInNetwork(s.network), strconv.Itoa(port)),
|
||||||
|
),
|
||||||
ClientID: "superclient",
|
ClientID: "superclient",
|
||||||
ClientSecret: "supersecret",
|
ClientSecret: "supersecret",
|
||||||
StripEmaildomain: true,
|
StripEmaildomain: true,
|
||||||
@@ -355,24 +368,6 @@ func (s *AuthOIDCScenario) runTailscaleUp(
|
|||||||
return fmt.Errorf("failed to up tailscale node: %w", errNoUserAvailable)
|
return fmt.Errorf("failed to up tailscale node: %w", errNoUserAvailable)
|
||||||
}
|
}
|
||||||
|
|
||||||
func pingAll(t *testing.T, clients []TailscaleClient, ips []netip.Addr) int {
|
|
||||||
t.Helper()
|
|
||||||
success := 0
|
|
||||||
|
|
||||||
for _, client := range clients {
|
|
||||||
for _, ip := range ips {
|
|
||||||
err := client.Ping(ip.String())
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to ping %s from %s: %s", ip, client.Hostname(), err)
|
|
||||||
} else {
|
|
||||||
success++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return success
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AuthOIDCScenario) Shutdown() error {
|
func (s *AuthOIDCScenario) Shutdown() error {
|
||||||
err := s.pool.Purge(s.mockOIDC)
|
err := s.pool.Purge(s.mockOIDC)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -13,6 +13,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/juanfont/headscale/integration/hsic"
|
"github.com/juanfont/headscale/integration/hsic"
|
||||||
|
"github.com/samber/lo"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errParseAuthPage = errors.New("failed to parse auth page")
|
var errParseAuthPage = errors.New("failed to parse auth page")
|
||||||
@@ -59,18 +60,11 @@ func TestAuthWebFlowAuthenticationPingAll(t *testing.T) {
|
|||||||
t.Errorf("failed wait for tailscale clients to be in sync: %s", err)
|
t.Errorf("failed wait for tailscale clients to be in sync: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
success := 0
|
allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string {
|
||||||
for _, client := range allClients {
|
return x.String()
|
||||||
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++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
success := pingAllHelper(t, allClients, allAddrs)
|
||||||
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
|
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
|
||||||
|
|
||||||
err = scenario.Shutdown()
|
err = scenario.Shutdown()
|
||||||
@@ -117,18 +111,11 @@ func TestAuthWebFlowLogoutAndRelogin(t *testing.T) {
|
|||||||
t.Errorf("failed wait for tailscale clients to be in sync: %s", err)
|
t.Errorf("failed wait for tailscale clients to be in sync: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
success := 0
|
allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string {
|
||||||
for _, client := range allClients {
|
return x.String()
|
||||||
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++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
success := pingAllHelper(t, allClients, allAddrs)
|
||||||
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
|
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
|
||||||
|
|
||||||
clientIPs := make(map[TailscaleClient][]netip.Addr)
|
clientIPs := make(map[TailscaleClient][]netip.Addr)
|
||||||
@@ -175,18 +162,11 @@ func TestAuthWebFlowLogoutAndRelogin(t *testing.T) {
|
|||||||
t.Errorf("failed to get clients: %s", err)
|
t.Errorf("failed to get clients: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
success = 0
|
allAddrs = lo.Map(allIps, func(x netip.Addr, index int) string {
|
||||||
for _, client := range allClients {
|
return x.String()
|
||||||
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++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
success = pingAllHelper(t, allClients, allAddrs)
|
||||||
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
|
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
|
||||||
|
|
||||||
for _, client := range allClients {
|
for _, client := range allClients {
|
||||||
@@ -211,7 +191,12 @@ func TestAuthWebFlowLogoutAndRelogin(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
t.Errorf("IPs changed for client %s. Used to be %v now %v", client.Hostname(), clientIPs[client], ips)
|
t.Errorf(
|
||||||
|
"IPs changed for client %s. Used to be %v now %v",
|
||||||
|
client.Hostname(),
|
||||||
|
clientIPs[client],
|
||||||
|
ips,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -335,7 +320,7 @@ func (s *AuthWebFlowScenario) runHeadscaleRegister(userStr string, loginURL *url
|
|||||||
|
|
||||||
if headscale, err := s.Headscale(); err == nil {
|
if headscale, err := s.Headscale(); err == nil {
|
||||||
_, err = headscale.Execute(
|
_, err = headscale.Execute(
|
||||||
[]string{"headscale", "-n", userStr, "nodes", "register", "--key", key},
|
[]string{"headscale", "nodes", "register", "--user", userStr, "--key", key},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("failed to register node: %s", err)
|
log.Printf("failed to register node: %s", err)
|
||||||
|
@@ -99,7 +99,7 @@ func TestUserCommand(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(
|
assert.Equal(
|
||||||
t,
|
t,
|
||||||
[]string{"user1", "newname"},
|
[]string{"newname", "user1"},
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
type ControlServer interface {
|
type ControlServer interface {
|
||||||
Shutdown() error
|
Shutdown() error
|
||||||
|
SaveLog(string) error
|
||||||
Execute(command []string) (string, error)
|
Execute(command []string) (string, error)
|
||||||
GetHealthEndpoint() string
|
GetHealthEndpoint() string
|
||||||
GetEndpoint() string
|
GetEndpoint() string
|
||||||
|
68
integration/dockertestutil/logs.go
Normal file
68
integration/dockertestutil/logs.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package dockertestutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/ory/dockertest/v3"
|
||||||
|
"github.com/ory/dockertest/v3/docker"
|
||||||
|
)
|
||||||
|
|
||||||
|
const filePerm = 0o644
|
||||||
|
|
||||||
|
func SaveLog(
|
||||||
|
pool *dockertest.Pool,
|
||||||
|
resource *dockertest.Resource,
|
||||||
|
basePath string,
|
||||||
|
) error {
|
||||||
|
err := os.MkdirAll(basePath, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
|
||||||
|
err = pool.Client.Logs(
|
||||||
|
docker.LogsOptions{
|
||||||
|
Context: context.TODO(),
|
||||||
|
Container: resource.Container.ID,
|
||||||
|
OutputStream: &stdout,
|
||||||
|
ErrorStream: &stderr,
|
||||||
|
Tail: "all",
|
||||||
|
RawTerminal: false,
|
||||||
|
Stdout: true,
|
||||||
|
Stderr: true,
|
||||||
|
Follow: false,
|
||||||
|
Timestamps: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Saving logs for %s to %s\n", resource.Container.Name, basePath)
|
||||||
|
|
||||||
|
err = os.WriteFile(
|
||||||
|
path.Join(basePath, resource.Container.Name+".stdout.log"),
|
||||||
|
stdout.Bytes(),
|
||||||
|
filePerm,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(
|
||||||
|
path.Join(basePath, resource.Container.Name+".stderr.log"),
|
||||||
|
stderr.Bytes(),
|
||||||
|
filePerm,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@@ -1,15 +1,19 @@
|
|||||||
package integration
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||||
"github.com/juanfont/headscale/integration/hsic"
|
"github.com/juanfont/headscale/integration/hsic"
|
||||||
"github.com/juanfont/headscale/integration/tsic"
|
"github.com/juanfont/headscale/integration/tsic"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPingAllByIP(t *testing.T) {
|
func TestPingAllByIP(t *testing.T) {
|
||||||
@@ -46,19 +50,11 @@ func TestPingAllByIP(t *testing.T) {
|
|||||||
t.Errorf("failed wait for tailscale clients to be in sync: %s", err)
|
t.Errorf("failed wait for tailscale clients to be in sync: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
success := 0
|
allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string {
|
||||||
|
return x.String()
|
||||||
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++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
success := pingAllHelper(t, allClients, allAddrs)
|
||||||
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
|
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
|
||||||
|
|
||||||
err = scenario.Shutdown()
|
err = scenario.Shutdown()
|
||||||
@@ -148,18 +144,11 @@ func TestAuthKeyLogoutAndRelogin(t *testing.T) {
|
|||||||
t.Errorf("failed to get clients: %s", err)
|
t.Errorf("failed to get clients: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
success := 0
|
allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string {
|
||||||
for _, client := range allClients {
|
return x.String()
|
||||||
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++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
success := pingAllHelper(t, allClients, allAddrs)
|
||||||
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
|
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
|
||||||
|
|
||||||
for _, client := range allClients {
|
for _, client := range allClients {
|
||||||
@@ -184,7 +173,12 @@ func TestAuthKeyLogoutAndRelogin(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
t.Errorf("IPs changed for client %s. Used to be %v now %v", client.Hostname(), clientIPs[client], ips)
|
t.Errorf(
|
||||||
|
"IPs changed for client %s. Used to be %v now %v",
|
||||||
|
client.Hostname(),
|
||||||
|
clientIPs[client],
|
||||||
|
ips,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -253,18 +247,11 @@ func TestEphemeral(t *testing.T) {
|
|||||||
t.Errorf("failed to get clients: %s", err)
|
t.Errorf("failed to get clients: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
success := 0
|
allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string {
|
||||||
for _, client := range allClients {
|
return x.String()
|
||||||
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++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
success := pingAllHelper(t, allClients, allAddrs)
|
||||||
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
|
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
|
||||||
|
|
||||||
for _, client := range allClients {
|
for _, client := range allClients {
|
||||||
@@ -335,18 +322,7 @@ func TestPingAllByHostname(t *testing.T) {
|
|||||||
t.Errorf("failed to get FQDNs: %s", err)
|
t.Errorf("failed to get FQDNs: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
success := 0
|
success := pingAllHelper(t, allClients, allHostnames)
|
||||||
|
|
||||||
for _, client := range allClients {
|
|
||||||
for _, hostname := range allHostnames {
|
|
||||||
err := client.Ping(hostname)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to ping %s from %s: %s", hostname, client.Hostname(), err)
|
|
||||||
} else {
|
|
||||||
success++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allClients))
|
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allClients))
|
||||||
|
|
||||||
@@ -582,3 +558,93 @@ func TestResolveMagicDNS(t *testing.T) {
|
|||||||
t.Errorf("failed to tear down scenario: %s", err)
|
t.Errorf("failed to tear down scenario: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExpireNode(t *testing.T) {
|
||||||
|
IntegrationSkip(t)
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
scenario, err := NewScenario()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to create scenario: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
spec := map[string]int{
|
||||||
|
"user1": len(TailscaleVersions),
|
||||||
|
}
|
||||||
|
|
||||||
|
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("expirenode"))
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string {
|
||||||
|
return x.String()
|
||||||
|
})
|
||||||
|
|
||||||
|
success := pingAllHelper(t, allClients, allAddrs)
|
||||||
|
t.Logf("before expire: %d successful pings out of %d", success, len(allClients)*len(allIps))
|
||||||
|
|
||||||
|
for _, client := range allClients {
|
||||||
|
status, err := client.Status()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Assert that we have the original count - self
|
||||||
|
assert.Len(t, status.Peers(), len(TailscaleVersions)-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
headscale, err := scenario.Headscale()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// TODO(kradalby): This is Headscale specific and would not play nicely
|
||||||
|
// with other implementations of the ControlServer interface
|
||||||
|
result, err := headscale.Execute([]string{
|
||||||
|
"headscale", "nodes", "expire", "--identifier", "0", "--output", "json",
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
var machine v1.Machine
|
||||||
|
err = json.Unmarshal([]byte(result), &machine)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
time.Sleep(30 * time.Second)
|
||||||
|
|
||||||
|
// Verify that the expired not is no longer present in the Peer list
|
||||||
|
// of connected nodes.
|
||||||
|
for _, client := range allClients {
|
||||||
|
status, err := client.Status()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
for _, peerKey := range status.Peers() {
|
||||||
|
peerStatus := status.Peer[peerKey]
|
||||||
|
|
||||||
|
peerPublicKey := strings.TrimPrefix(peerStatus.PublicKey.String(), "nodekey:")
|
||||||
|
|
||||||
|
assert.NotEqual(t, machine.NodeKey, peerPublicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
if client.Hostname() != machine.Name {
|
||||||
|
// Assert that we have the original count - self - expired node
|
||||||
|
assert.Len(t, status.Peers(), len(TailscaleVersions)-2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = scenario.Shutdown()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to tear down scenario: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -41,6 +41,8 @@ type fileInContainer struct {
|
|||||||
contents []byte
|
contents []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HeadscaleInContainer is an implementation of ControlServer which
|
||||||
|
// sets up a Headscale instance inside a container.
|
||||||
type HeadscaleInContainer struct {
|
type HeadscaleInContainer struct {
|
||||||
hostname string
|
hostname string
|
||||||
|
|
||||||
@@ -57,8 +59,12 @@ type HeadscaleInContainer struct {
|
|||||||
filesInContainer []fileInContainer
|
filesInContainer []fileInContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Option represent optional settings that can be given to a
|
||||||
|
// Headscale instance.
|
||||||
type Option = func(c *HeadscaleInContainer)
|
type Option = func(c *HeadscaleInContainer)
|
||||||
|
|
||||||
|
// WithACLPolicy adds a headscale.ACLPolicy policy to the
|
||||||
|
// HeadscaleInContainer instance.
|
||||||
func WithACLPolicy(acl *headscale.ACLPolicy) Option {
|
func WithACLPolicy(acl *headscale.ACLPolicy) Option {
|
||||||
return func(hsic *HeadscaleInContainer) {
|
return func(hsic *HeadscaleInContainer) {
|
||||||
// TODO(kradalby): Move somewhere appropriate
|
// TODO(kradalby): Move somewhere appropriate
|
||||||
@@ -68,6 +74,7 @@ func WithACLPolicy(acl *headscale.ACLPolicy) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithTLS creates certificates and enables HTTPS.
|
||||||
func WithTLS() Option {
|
func WithTLS() Option {
|
||||||
return func(hsic *HeadscaleInContainer) {
|
return func(hsic *HeadscaleInContainer) {
|
||||||
cert, key, err := createCertificate()
|
cert, key, err := createCertificate()
|
||||||
@@ -84,6 +91,8 @@ func WithTLS() Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithConfigEnv takes a map of environment variables that
|
||||||
|
// can be used to override Headscale configuration.
|
||||||
func WithConfigEnv(configEnv map[string]string) Option {
|
func WithConfigEnv(configEnv map[string]string) Option {
|
||||||
return func(hsic *HeadscaleInContainer) {
|
return func(hsic *HeadscaleInContainer) {
|
||||||
for key, value := range configEnv {
|
for key, value := range configEnv {
|
||||||
@@ -92,12 +101,15 @@ func WithConfigEnv(configEnv map[string]string) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithPort sets the port on where to run Headscale.
|
||||||
func WithPort(port int) Option {
|
func WithPort(port int) Option {
|
||||||
return func(hsic *HeadscaleInContainer) {
|
return func(hsic *HeadscaleInContainer) {
|
||||||
hsic.port = port
|
hsic.port = port
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithTestName sets a name for the test, this will be reflected
|
||||||
|
// in the Docker container name.
|
||||||
func WithTestName(testName string) Option {
|
func WithTestName(testName string) Option {
|
||||||
return func(hsic *HeadscaleInContainer) {
|
return func(hsic *HeadscaleInContainer) {
|
||||||
hash, _ := headscale.GenerateRandomStringDNSSafe(hsicHashLength)
|
hash, _ := headscale.GenerateRandomStringDNSSafe(hsicHashLength)
|
||||||
@@ -107,6 +119,8 @@ func WithTestName(testName string) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithHostnameAsServerURL sets the Headscale ServerURL based on
|
||||||
|
// the Hostname.
|
||||||
func WithHostnameAsServerURL() Option {
|
func WithHostnameAsServerURL() Option {
|
||||||
return func(hsic *HeadscaleInContainer) {
|
return func(hsic *HeadscaleInContainer) {
|
||||||
hsic.env["HEADSCALE_SERVER_URL"] = fmt.Sprintf("http://%s",
|
hsic.env["HEADSCALE_SERVER_URL"] = fmt.Sprintf("http://%s",
|
||||||
@@ -116,6 +130,7 @@ func WithHostnameAsServerURL() Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithFileInContainer adds a file to the container at the given path.
|
||||||
func WithFileInContainer(path string, contents []byte) Option {
|
func WithFileInContainer(path string, contents []byte) Option {
|
||||||
return func(hsic *HeadscaleInContainer) {
|
return func(hsic *HeadscaleInContainer) {
|
||||||
hsic.filesInContainer = append(hsic.filesInContainer,
|
hsic.filesInContainer = append(hsic.filesInContainer,
|
||||||
@@ -126,6 +141,7 @@ func WithFileInContainer(path string, contents []byte) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New returns a new HeadscaleInContainer instance.
|
||||||
func New(
|
func New(
|
||||||
pool *dockertest.Pool,
|
pool *dockertest.Pool,
|
||||||
network *dockertest.Network,
|
network *dockertest.Network,
|
||||||
@@ -244,10 +260,19 @@ func (t *HeadscaleInContainer) hasTLS() bool {
|
|||||||
return len(t.tlsCert) != 0 && len(t.tlsKey) != 0
|
return len(t.tlsCert) != 0 && len(t.tlsKey) != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shutdown stops and cleans up the Headscale container.
|
||||||
func (t *HeadscaleInContainer) Shutdown() error {
|
func (t *HeadscaleInContainer) Shutdown() error {
|
||||||
return t.pool.Purge(t.container)
|
return t.pool.Purge(t.container)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SaveLog saves the current stdout log of the container to a path
|
||||||
|
// on the host system.
|
||||||
|
func (t *HeadscaleInContainer) SaveLog(path string) error {
|
||||||
|
return dockertestutil.SaveLog(t.pool, t.container, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute runs a command inside the Headscale container and returns the
|
||||||
|
// result of stdout as a string.
|
||||||
func (t *HeadscaleInContainer) Execute(
|
func (t *HeadscaleInContainer) Execute(
|
||||||
command []string,
|
command []string,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
@@ -269,18 +294,23 @@ func (t *HeadscaleInContainer) Execute(
|
|||||||
return stdout, nil
|
return stdout, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetIP returns the docker container IP as a string.
|
||||||
func (t *HeadscaleInContainer) GetIP() string {
|
func (t *HeadscaleInContainer) GetIP() string {
|
||||||
return t.container.GetIPInNetwork(t.network)
|
return t.container.GetIPInNetwork(t.network)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPort returns the docker container port as a string.
|
||||||
func (t *HeadscaleInContainer) GetPort() string {
|
func (t *HeadscaleInContainer) GetPort() string {
|
||||||
return fmt.Sprintf("%d", t.port)
|
return fmt.Sprintf("%d", t.port)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetHealthEndpoint returns a health endpoint for the HeadscaleInContainer
|
||||||
|
// instance.
|
||||||
func (t *HeadscaleInContainer) GetHealthEndpoint() string {
|
func (t *HeadscaleInContainer) GetHealthEndpoint() string {
|
||||||
return fmt.Sprintf("%s/health", t.GetEndpoint())
|
return fmt.Sprintf("%s/health", t.GetEndpoint())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetEndpoint returns the Headscale endpoint for the HeadscaleInContainer.
|
||||||
func (t *HeadscaleInContainer) GetEndpoint() string {
|
func (t *HeadscaleInContainer) GetEndpoint() string {
|
||||||
hostEndpoint := fmt.Sprintf("%s:%d",
|
hostEndpoint := fmt.Sprintf("%s:%d",
|
||||||
t.GetIP(),
|
t.GetIP(),
|
||||||
@@ -293,14 +323,18 @@ func (t *HeadscaleInContainer) GetEndpoint() string {
|
|||||||
return fmt.Sprintf("http://%s", hostEndpoint)
|
return fmt.Sprintf("http://%s", hostEndpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCert returns the public certificate of the HeadscaleInContainer.
|
||||||
func (t *HeadscaleInContainer) GetCert() []byte {
|
func (t *HeadscaleInContainer) GetCert() []byte {
|
||||||
return t.tlsCert
|
return t.tlsCert
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetHostname returns the hostname of the HeadscaleInContainer.
|
||||||
func (t *HeadscaleInContainer) GetHostname() string {
|
func (t *HeadscaleInContainer) GetHostname() string {
|
||||||
return t.hostname
|
return t.hostname
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WaitForReady blocks until the Headscale instance is ready to
|
||||||
|
// serve clients.
|
||||||
func (t *HeadscaleInContainer) WaitForReady() error {
|
func (t *HeadscaleInContainer) WaitForReady() error {
|
||||||
url := t.GetHealthEndpoint()
|
url := t.GetHealthEndpoint()
|
||||||
|
|
||||||
@@ -328,6 +362,7 @@ func (t *HeadscaleInContainer) WaitForReady() error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateUser adds a new user to the Headscale instance.
|
||||||
func (t *HeadscaleInContainer) CreateUser(
|
func (t *HeadscaleInContainer) CreateUser(
|
||||||
user string,
|
user string,
|
||||||
) error {
|
) error {
|
||||||
@@ -345,6 +380,8 @@ func (t *HeadscaleInContainer) CreateUser(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateAuthKey creates a new "authorisation key" for a User that can be used
|
||||||
|
// to authorise a TailscaleClient with the Headscale instance.
|
||||||
func (t *HeadscaleInContainer) CreateAuthKey(
|
func (t *HeadscaleInContainer) CreateAuthKey(
|
||||||
user string,
|
user string,
|
||||||
reusable bool,
|
reusable bool,
|
||||||
@@ -388,6 +425,8 @@ func (t *HeadscaleInContainer) CreateAuthKey(
|
|||||||
return &preAuthKey, nil
|
return &preAuthKey, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListMachinesInUser list the TailscaleClients (Machine, Headscale internal representation)
|
||||||
|
// associated with a user.
|
||||||
func (t *HeadscaleInContainer) ListMachinesInUser(
|
func (t *HeadscaleInContainer) ListMachinesInUser(
|
||||||
user string,
|
user string,
|
||||||
) ([]*v1.Machine, error) {
|
) ([]*v1.Machine, error) {
|
||||||
@@ -411,6 +450,7 @@ func (t *HeadscaleInContainer) ListMachinesInUser(
|
|||||||
return nodes, nil
|
return nodes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WriteFile save file inside the Headscale container.
|
||||||
func (t *HeadscaleInContainer) WriteFile(path string, data []byte) error {
|
func (t *HeadscaleInContainer) WriteFile(path string, data []byte) error {
|
||||||
return integrationutil.WriteFileToContainer(t.pool, t.container, path, data)
|
return integrationutil.WriteFileToContainer(t.pool, t.container, path, data)
|
||||||
}
|
}
|
||||||
|
@@ -26,13 +26,15 @@ const (
|
|||||||
var (
|
var (
|
||||||
errNoHeadscaleAvailable = errors.New("no headscale available")
|
errNoHeadscaleAvailable = errors.New("no headscale available")
|
||||||
errNoUserAvailable = errors.New("no user available")
|
errNoUserAvailable = errors.New("no user available")
|
||||||
|
errNoClientFound = errors.New("client not found")
|
||||||
|
|
||||||
// Tailscale started adding TS2021 support in CapabilityVersion>=28 (v1.24.0), but
|
// Tailscale started adding TS2021 support in CapabilityVersion>=28 (v1.24.0), but
|
||||||
// proper support in Headscale was only added for CapabilityVersion>=39 clients (v1.30.0).
|
// proper support in Headscale was only added for CapabilityVersion>=39 clients (v1.30.0).
|
||||||
tailscaleVersions2021 = []string{
|
tailscaleVersions2021 = []string{
|
||||||
"head",
|
"head",
|
||||||
"unstable",
|
"unstable",
|
||||||
"1.34.0",
|
"1.36.0",
|
||||||
|
"1.34.2",
|
||||||
"1.32.3",
|
"1.32.3",
|
||||||
"1.30.2",
|
"1.30.2",
|
||||||
}
|
}
|
||||||
@@ -55,12 +57,23 @@ var (
|
|||||||
// "1.8.7",
|
// "1.8.7",
|
||||||
// }.
|
// }.
|
||||||
|
|
||||||
|
// TailscaleVersions represents a list of Tailscale versions the suite
|
||||||
|
// uses to test compatibility with the ControlServer.
|
||||||
|
//
|
||||||
|
// The list contains two special cases, "head" and "unstable" which
|
||||||
|
// points to the current tip of Tailscale's main branch and the latest
|
||||||
|
// released unstable version.
|
||||||
|
//
|
||||||
|
// The rest of the version represents Tailscale versions that can be
|
||||||
|
// found in Tailscale's apt repository.
|
||||||
TailscaleVersions = append(
|
TailscaleVersions = append(
|
||||||
tailscaleVersions2021,
|
tailscaleVersions2021,
|
||||||
tailscaleVersions2019...,
|
tailscaleVersions2019...,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// User represents a User in the ControlServer and a map of TailscaleClient's
|
||||||
|
// associated with the User.
|
||||||
type User struct {
|
type User struct {
|
||||||
Clients map[string]TailscaleClient
|
Clients map[string]TailscaleClient
|
||||||
|
|
||||||
@@ -69,6 +82,10 @@ type User struct {
|
|||||||
syncWaitGroup sync.WaitGroup
|
syncWaitGroup sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scenario is a representation of an environment with one ControlServer and
|
||||||
|
// one or more User's and its associated TailscaleClients.
|
||||||
|
// A Scenario is intended to simplify setting up a new testcase for testing
|
||||||
|
// a ControlServer with TailscaleClients.
|
||||||
// TODO(kradalby): make control server configurable, test correctness with Tailscale SaaS.
|
// TODO(kradalby): make control server configurable, test correctness with Tailscale SaaS.
|
||||||
type Scenario struct {
|
type Scenario struct {
|
||||||
// TODO(kradalby): support multiple headcales for later, currently only
|
// TODO(kradalby): support multiple headcales for later, currently only
|
||||||
@@ -83,6 +100,8 @@ type Scenario struct {
|
|||||||
headscaleLock sync.Mutex
|
headscaleLock sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewScenario creates a test Scenario which can be used to bootstraps a ControlServer with
|
||||||
|
// a set of Users and TailscaleClients.
|
||||||
func NewScenario() (*Scenario, error) {
|
func NewScenario() (*Scenario, error) {
|
||||||
hash, err := headscale.GenerateRandomStringDNSSafe(scenarioHashLength)
|
hash, err := headscale.GenerateRandomStringDNSSafe(scenarioHashLength)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -123,9 +142,21 @@ func NewScenario() (*Scenario, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shutdown shuts down and cleans up all the containers (ControlServer, TailscaleClient)
|
||||||
|
// and networks associated with it.
|
||||||
|
// In addition, it will save the logs of the ControlServer to `/tmp/control` in the
|
||||||
|
// environment running the tests.
|
||||||
func (s *Scenario) Shutdown() error {
|
func (s *Scenario) Shutdown() error {
|
||||||
s.controlServers.Range(func(_ string, control ControlServer) bool {
|
s.controlServers.Range(func(_ string, control ControlServer) bool {
|
||||||
err := control.Shutdown()
|
err := control.SaveLog("/tmp/control")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(
|
||||||
|
"Failed to save log from control: %s",
|
||||||
|
fmt.Errorf("failed to save log from control: %w", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = control.Shutdown()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf(
|
log.Printf(
|
||||||
"Failed to shut down control: %s",
|
"Failed to shut down control: %s",
|
||||||
@@ -158,6 +189,7 @@ func (s *Scenario) Shutdown() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Users returns the name of all users associated with the Scenario.
|
||||||
func (s *Scenario) Users() []string {
|
func (s *Scenario) Users() []string {
|
||||||
users := make([]string, 0)
|
users := make([]string, 0)
|
||||||
for user := range s.users {
|
for user := range s.users {
|
||||||
@@ -170,6 +202,9 @@ func (s *Scenario) Users() []string {
|
|||||||
/// Headscale related stuff
|
/// Headscale related stuff
|
||||||
// Note: These functions assume that there is a _single_ headscale instance for now
|
// Note: These functions assume that there is a _single_ headscale instance for now
|
||||||
|
|
||||||
|
// Headscale returns a ControlServer instance based on hsic (HeadscaleInContainer)
|
||||||
|
// If the Scenario already has an instance, the pointer to the running container
|
||||||
|
// will be return, otherwise a new instance will be created.
|
||||||
// TODO(kradalby): make port and headscale configurable, multiple instances support?
|
// TODO(kradalby): make port and headscale configurable, multiple instances support?
|
||||||
func (s *Scenario) Headscale(opts ...hsic.Option) (ControlServer, error) {
|
func (s *Scenario) Headscale(opts ...hsic.Option) (ControlServer, error) {
|
||||||
s.headscaleLock.Lock()
|
s.headscaleLock.Lock()
|
||||||
@@ -194,7 +229,13 @@ func (s *Scenario) Headscale(opts ...hsic.Option) (ControlServer, error) {
|
|||||||
return headscale, nil
|
return headscale, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Scenario) CreatePreAuthKey(user string, reusable bool, ephemeral bool) (*v1.PreAuthKey, error) {
|
// CreatePreAuthKey creates a "pre authentorised key" to be created in the
|
||||||
|
// Headscale instance on behalf of the Scenario.
|
||||||
|
func (s *Scenario) CreatePreAuthKey(
|
||||||
|
user string,
|
||||||
|
reusable bool,
|
||||||
|
ephemeral bool,
|
||||||
|
) (*v1.PreAuthKey, error) {
|
||||||
if headscale, err := s.Headscale(); err == nil {
|
if headscale, err := s.Headscale(); err == nil {
|
||||||
key, err := headscale.CreateAuthKey(user, reusable, ephemeral)
|
key, err := headscale.CreateAuthKey(user, reusable, ephemeral)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -207,6 +248,8 @@ func (s *Scenario) CreatePreAuthKey(user string, reusable bool, ephemeral bool)
|
|||||||
return nil, fmt.Errorf("failed to create user: %w", errNoHeadscaleAvailable)
|
return nil, fmt.Errorf("failed to create user: %w", errNoHeadscaleAvailable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateUser creates a User to be created in the
|
||||||
|
// Headscale instance on behalf of the Scenario.
|
||||||
func (s *Scenario) CreateUser(user string) error {
|
func (s *Scenario) CreateUser(user string) error {
|
||||||
if headscale, err := s.Headscale(); err == nil {
|
if headscale, err := s.Headscale(); err == nil {
|
||||||
err := headscale.CreateUser(user)
|
err := headscale.CreateUser(user)
|
||||||
@@ -226,6 +269,8 @@ func (s *Scenario) CreateUser(user string) error {
|
|||||||
|
|
||||||
/// Client related stuff
|
/// Client related stuff
|
||||||
|
|
||||||
|
// CreateTailscaleNodesInUser creates and adds a new TailscaleClient to a
|
||||||
|
// User in the Scenario.
|
||||||
func (s *Scenario) CreateTailscaleNodesInUser(
|
func (s *Scenario) CreateTailscaleNodesInUser(
|
||||||
userStr string,
|
userStr string,
|
||||||
requestedVersion string,
|
requestedVersion string,
|
||||||
@@ -286,6 +331,8 @@ func (s *Scenario) CreateTailscaleNodesInUser(
|
|||||||
return fmt.Errorf("failed to add tailscale node: %w", errNoUserAvailable)
|
return fmt.Errorf("failed to add tailscale node: %w", errNoUserAvailable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RunTailscaleUp will log in all of the TailscaleClients associated with a
|
||||||
|
// User to the given ControlServer (by URL).
|
||||||
func (s *Scenario) RunTailscaleUp(
|
func (s *Scenario) RunTailscaleUp(
|
||||||
userStr, loginServer, authKey string,
|
userStr, loginServer, authKey string,
|
||||||
) error {
|
) error {
|
||||||
@@ -314,6 +361,8 @@ func (s *Scenario) RunTailscaleUp(
|
|||||||
return fmt.Errorf("failed to up tailscale node: %w", errNoUserAvailable)
|
return fmt.Errorf("failed to up tailscale node: %w", errNoUserAvailable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CountTailscale returns the total number of TailscaleClients in a Scenario.
|
||||||
|
// This is the sum of Users x TailscaleClients.
|
||||||
func (s *Scenario) CountTailscale() int {
|
func (s *Scenario) CountTailscale() int {
|
||||||
count := 0
|
count := 0
|
||||||
|
|
||||||
@@ -324,6 +373,8 @@ func (s *Scenario) CountTailscale() int {
|
|||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WaitForTailscaleSync blocks execution until all the TailscaleClient reports
|
||||||
|
// to have all other TailscaleClients present in their netmap.NetworkMap.
|
||||||
func (s *Scenario) WaitForTailscaleSync() error {
|
func (s *Scenario) WaitForTailscaleSync() error {
|
||||||
tsCount := s.CountTailscale()
|
tsCount := s.CountTailscale()
|
||||||
|
|
||||||
@@ -344,7 +395,7 @@ func (s *Scenario) WaitForTailscaleSync() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateHeadscaleEnv is a conventient method returning a set up Headcale
|
// CreateHeadscaleEnv is a conventient method returning a complete Headcale
|
||||||
// test environment with nodes of all versions, joined to the server with X
|
// test environment with nodes of all versions, joined to the server with X
|
||||||
// users.
|
// users.
|
||||||
func (s *Scenario) CreateHeadscaleEnv(
|
func (s *Scenario) CreateHeadscaleEnv(
|
||||||
@@ -382,6 +433,8 @@ func (s *Scenario) CreateHeadscaleEnv(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetIPs returns all netip.Addr of TailscaleClients associated with a User
|
||||||
|
// in a Scenario.
|
||||||
func (s *Scenario) GetIPs(user string) ([]netip.Addr, error) {
|
func (s *Scenario) GetIPs(user string) ([]netip.Addr, error) {
|
||||||
var ips []netip.Addr
|
var ips []netip.Addr
|
||||||
if ns, ok := s.users[user]; ok {
|
if ns, ok := s.users[user]; ok {
|
||||||
@@ -399,6 +452,7 @@ func (s *Scenario) GetIPs(user string) ([]netip.Addr, error) {
|
|||||||
return ips, fmt.Errorf("failed to get ips: %w", errNoUserAvailable)
|
return ips, fmt.Errorf("failed to get ips: %w", errNoUserAvailable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetIPs returns all TailscaleClients associated with a User in a Scenario.
|
||||||
func (s *Scenario) GetClients(user string) ([]TailscaleClient, error) {
|
func (s *Scenario) GetClients(user string) ([]TailscaleClient, error) {
|
||||||
var clients []TailscaleClient
|
var clients []TailscaleClient
|
||||||
if ns, ok := s.users[user]; ok {
|
if ns, ok := s.users[user]; ok {
|
||||||
@@ -412,6 +466,8 @@ func (s *Scenario) GetClients(user string) ([]TailscaleClient, error) {
|
|||||||
return clients, fmt.Errorf("failed to get clients: %w", errNoUserAvailable)
|
return clients, fmt.Errorf("failed to get clients: %w", errNoUserAvailable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListTailscaleClients returns a list of TailscaleClients given the Users
|
||||||
|
// passed as parameters.
|
||||||
func (s *Scenario) ListTailscaleClients(users ...string) ([]TailscaleClient, error) {
|
func (s *Scenario) ListTailscaleClients(users ...string) ([]TailscaleClient, error) {
|
||||||
var allClients []TailscaleClient
|
var allClients []TailscaleClient
|
||||||
|
|
||||||
@@ -431,6 +487,28 @@ func (s *Scenario) ListTailscaleClients(users ...string) ([]TailscaleClient, err
|
|||||||
return allClients, nil
|
return allClients, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindTailscaleClientByIP returns a TailscaleClient associated with an IP address
|
||||||
|
// if it exists.
|
||||||
|
func (s *Scenario) FindTailscaleClientByIP(ip netip.Addr) (TailscaleClient, error) {
|
||||||
|
clients, err := s.ListTailscaleClients()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, client := range clients {
|
||||||
|
ips, _ := client.IPs()
|
||||||
|
for _, ip2 := range ips {
|
||||||
|
if ip == ip2 {
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errNoClientFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListTailscaleClientsIPs returns a list of netip.Addr based on Users
|
||||||
|
// passed as parameters.
|
||||||
func (s *Scenario) ListTailscaleClientsIPs(users ...string) ([]netip.Addr, error) {
|
func (s *Scenario) ListTailscaleClientsIPs(users ...string) ([]netip.Addr, error) {
|
||||||
var allIps []netip.Addr
|
var allIps []netip.Addr
|
||||||
|
|
||||||
@@ -450,6 +528,8 @@ func (s *Scenario) ListTailscaleClientsIPs(users ...string) ([]netip.Addr, error
|
|||||||
return allIps, nil
|
return allIps, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListTailscaleClientsIPs returns a list of FQDN based on Users
|
||||||
|
// passed as parameters.
|
||||||
func (s *Scenario) ListTailscaleClientsFQDNs(users ...string) ([]string, error) {
|
func (s *Scenario) ListTailscaleClientsFQDNs(users ...string) ([]string, error) {
|
||||||
allFQDNs := make([]string, 0)
|
allFQDNs := make([]string, 0)
|
||||||
|
|
||||||
@@ -470,6 +550,8 @@ func (s *Scenario) ListTailscaleClientsFQDNs(users ...string) ([]string, error)
|
|||||||
return allFQDNs, nil
|
return allFQDNs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WaitForTailscaleLogout blocks execution until all TailscaleClients have
|
||||||
|
// logged out of the ControlServer.
|
||||||
func (s *Scenario) WaitForTailscaleLogout() {
|
func (s *Scenario) WaitForTailscaleLogout() {
|
||||||
for _, user := range s.users {
|
for _, user := range s.users {
|
||||||
for _, client := range user.Clients {
|
for _, client := range user.Clients {
|
||||||
|
@@ -4,6 +4,7 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/juanfont/headscale/integration/tsic"
|
||||||
"tailscale.com/ipn/ipnstate"
|
"tailscale.com/ipn/ipnstate"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -22,6 +23,6 @@ type TailscaleClient interface {
|
|||||||
WaitForReady() error
|
WaitForReady() error
|
||||||
WaitForLogout() error
|
WaitForLogout() error
|
||||||
WaitForPeers(expected int) error
|
WaitForPeers(expected int) error
|
||||||
Ping(hostnameOrIP string) error
|
Ping(hostnameOrIP string, opts ...tsic.PingOption) error
|
||||||
ID() string
|
ID() string
|
||||||
}
|
}
|
||||||
|
@@ -7,7 +7,9 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/cenkalti/backoff/v4"
|
"github.com/cenkalti/backoff/v4"
|
||||||
"github.com/juanfont/headscale"
|
"github.com/juanfont/headscale"
|
||||||
@@ -20,6 +22,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
tsicHashLength = 6
|
tsicHashLength = 6
|
||||||
|
defaultPingCount = 10
|
||||||
dockerContextPath = "../."
|
dockerContextPath = "../."
|
||||||
headscaleCertPath = "/usr/local/share/ca-certificates/headscale.crt"
|
headscaleCertPath = "/usr/local/share/ca-certificates/headscale.crt"
|
||||||
)
|
)
|
||||||
@@ -33,6 +36,8 @@ var (
|
|||||||
errTailscaleNotLoggedOut = errors.New("tailscale not logged out")
|
errTailscaleNotLoggedOut = errors.New("tailscale not logged out")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TailscaleInContainer is an implementation of TailscaleClient which
|
||||||
|
// sets up a Tailscale instance inside a container.
|
||||||
type TailscaleInContainer struct {
|
type TailscaleInContainer struct {
|
||||||
version string
|
version string
|
||||||
hostname string
|
hostname string
|
||||||
@@ -49,16 +54,25 @@ type TailscaleInContainer struct {
|
|||||||
headscaleCert []byte
|
headscaleCert []byte
|
||||||
headscaleHostname string
|
headscaleHostname string
|
||||||
withSSH bool
|
withSSH bool
|
||||||
|
withTags []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Option represent optional settings that can be given to a
|
||||||
|
// Tailscale instance.
|
||||||
type Option = func(c *TailscaleInContainer)
|
type Option = func(c *TailscaleInContainer)
|
||||||
|
|
||||||
|
// WithHeadscaleTLS takes the certificate of the Headscale instance
|
||||||
|
// and adds it to the trusted surtificate of the Tailscale container.
|
||||||
func WithHeadscaleTLS(cert []byte) Option {
|
func WithHeadscaleTLS(cert []byte) Option {
|
||||||
return func(tsic *TailscaleInContainer) {
|
return func(tsic *TailscaleInContainer) {
|
||||||
tsic.headscaleCert = cert
|
tsic.headscaleCert = cert
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithOrCreateNetwork sets the Docker container network to use with
|
||||||
|
// the Tailscale instance, if the parameter is nil, a new network,
|
||||||
|
// isolating the TailscaleClient, will be created. If a network is
|
||||||
|
// passed, the Tailscale instance will join the given network.
|
||||||
func WithOrCreateNetwork(network *dockertest.Network) Option {
|
func WithOrCreateNetwork(network *dockertest.Network) Option {
|
||||||
return func(tsic *TailscaleInContainer) {
|
return func(tsic *TailscaleInContainer) {
|
||||||
if network != nil {
|
if network != nil {
|
||||||
@@ -79,18 +93,29 @@ func WithOrCreateNetwork(network *dockertest.Network) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithHeadscaleName set the name of the headscale instance,
|
||||||
|
// mostly useful in combination with TLS and WithHeadscaleTLS.
|
||||||
func WithHeadscaleName(hsName string) Option {
|
func WithHeadscaleName(hsName string) Option {
|
||||||
return func(tsic *TailscaleInContainer) {
|
return func(tsic *TailscaleInContainer) {
|
||||||
tsic.headscaleHostname = hsName
|
tsic.headscaleHostname = hsName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithTags associates the given tags to the Tailscale instance.
|
||||||
|
func WithTags(tags []string) Option {
|
||||||
|
return func(tsic *TailscaleInContainer) {
|
||||||
|
tsic.withTags = tags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithSSH enables SSH for the Tailscale instance.
|
||||||
func WithSSH() Option {
|
func WithSSH() Option {
|
||||||
return func(tsic *TailscaleInContainer) {
|
return func(tsic *TailscaleInContainer) {
|
||||||
tsic.withSSH = true
|
tsic.withSSH = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New returns a new TailscaleInContainer instance.
|
||||||
func New(
|
func New(
|
||||||
pool *dockertest.Pool,
|
pool *dockertest.Pool,
|
||||||
version string,
|
version string,
|
||||||
@@ -172,22 +197,29 @@ func (t *TailscaleInContainer) hasTLS() bool {
|
|||||||
return len(t.headscaleCert) != 0
|
return len(t.headscaleCert) != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shutdown stops and cleans up the Tailscale container.
|
||||||
func (t *TailscaleInContainer) Shutdown() error {
|
func (t *TailscaleInContainer) Shutdown() error {
|
||||||
return t.pool.Purge(t.container)
|
return t.pool.Purge(t.container)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hostname returns the hostname of the Tailscale instance.
|
||||||
func (t *TailscaleInContainer) Hostname() string {
|
func (t *TailscaleInContainer) Hostname() string {
|
||||||
return t.hostname
|
return t.hostname
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Version returns the running Tailscale version of the instance.
|
||||||
func (t *TailscaleInContainer) Version() string {
|
func (t *TailscaleInContainer) Version() string {
|
||||||
return t.version
|
return t.version
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ID returns the Docker container ID of the TailscaleInContainer
|
||||||
|
// instance.
|
||||||
func (t *TailscaleInContainer) ID() string {
|
func (t *TailscaleInContainer) ID() string {
|
||||||
return t.container.Container.ID
|
return t.container.Container.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Execute runs a command inside the Tailscale container and returns the
|
||||||
|
// result of stdout as a string.
|
||||||
func (t *TailscaleInContainer) Execute(
|
func (t *TailscaleInContainer) Execute(
|
||||||
command []string,
|
command []string,
|
||||||
) (string, string, error) {
|
) (string, string, error) {
|
||||||
@@ -213,6 +245,8 @@ func (t *TailscaleInContainer) Execute(
|
|||||||
return stdout, stderr, nil
|
return stdout, stderr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Up runs the login routine on the given Tailscale instance.
|
||||||
|
// This login mechanism uses the authorised key for authentication.
|
||||||
func (t *TailscaleInContainer) Up(
|
func (t *TailscaleInContainer) Up(
|
||||||
loginServer, authKey string,
|
loginServer, authKey string,
|
||||||
) error {
|
) error {
|
||||||
@@ -231,6 +265,12 @@ func (t *TailscaleInContainer) Up(
|
|||||||
command = append(command, "--ssh")
|
command = append(command, "--ssh")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(t.withTags) > 0 {
|
||||||
|
command = append(command,
|
||||||
|
fmt.Sprintf(`--advertise-tags=%s`, strings.Join(t.withTags, ",")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
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 fmt.Errorf("failed to join tailscale client: %w", err)
|
||||||
}
|
}
|
||||||
@@ -238,6 +278,8 @@ func (t *TailscaleInContainer) Up(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Up runs the login routine on the given Tailscale instance.
|
||||||
|
// This login mechanism uses web + command line flow for authentication.
|
||||||
func (t *TailscaleInContainer) UpWithLoginURL(
|
func (t *TailscaleInContainer) UpWithLoginURL(
|
||||||
loginServer string,
|
loginServer string,
|
||||||
) (*url.URL, error) {
|
) (*url.URL, error) {
|
||||||
@@ -270,6 +312,7 @@ func (t *TailscaleInContainer) UpWithLoginURL(
|
|||||||
return loginURL, nil
|
return loginURL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Logout runs the logout routine on the given Tailscale instance.
|
||||||
func (t *TailscaleInContainer) Logout() error {
|
func (t *TailscaleInContainer) Logout() error {
|
||||||
_, _, err := t.Execute([]string{"tailscale", "logout"})
|
_, _, err := t.Execute([]string{"tailscale", "logout"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -279,6 +322,7 @@ func (t *TailscaleInContainer) Logout() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IPs returns the netip.Addr of the Tailscale instance.
|
||||||
func (t *TailscaleInContainer) IPs() ([]netip.Addr, error) {
|
func (t *TailscaleInContainer) IPs() ([]netip.Addr, error) {
|
||||||
if t.ips != nil && len(t.ips) != 0 {
|
if t.ips != nil && len(t.ips) != 0 {
|
||||||
return t.ips, nil
|
return t.ips, nil
|
||||||
@@ -311,6 +355,7 @@ func (t *TailscaleInContainer) IPs() ([]netip.Addr, error) {
|
|||||||
return ips, nil
|
return ips, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Status returns the ipnstate.Status of the Tailscale instance.
|
||||||
func (t *TailscaleInContainer) Status() (*ipnstate.Status, error) {
|
func (t *TailscaleInContainer) Status() (*ipnstate.Status, error) {
|
||||||
command := []string{
|
command := []string{
|
||||||
"tailscale",
|
"tailscale",
|
||||||
@@ -332,6 +377,7 @@ func (t *TailscaleInContainer) Status() (*ipnstate.Status, error) {
|
|||||||
return &status, err
|
return &status, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FQDN returns the FQDN as a string of the Tailscale instance.
|
||||||
func (t *TailscaleInContainer) FQDN() (string, error) {
|
func (t *TailscaleInContainer) FQDN() (string, error) {
|
||||||
if t.fqdn != "" {
|
if t.fqdn != "" {
|
||||||
return t.fqdn, nil
|
return t.fqdn, nil
|
||||||
@@ -345,6 +391,8 @@ func (t *TailscaleInContainer) FQDN() (string, error) {
|
|||||||
return status.Self.DNSName, nil
|
return status.Self.DNSName, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WaitForReady blocks until the Tailscale (tailscaled) instance is ready
|
||||||
|
// to login or be used.
|
||||||
func (t *TailscaleInContainer) WaitForReady() error {
|
func (t *TailscaleInContainer) WaitForReady() error {
|
||||||
return t.pool.Retry(func() error {
|
return t.pool.Retry(func() error {
|
||||||
status, err := t.Status()
|
status, err := t.Status()
|
||||||
@@ -360,6 +408,7 @@ func (t *TailscaleInContainer) WaitForReady() error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WaitForLogout blocks until the Tailscale instance has logged out.
|
||||||
func (t *TailscaleInContainer) WaitForLogout() error {
|
func (t *TailscaleInContainer) WaitForLogout() error {
|
||||||
return t.pool.Retry(func() error {
|
return t.pool.Retry(func() error {
|
||||||
status, err := t.Status()
|
status, err := t.Status()
|
||||||
@@ -375,6 +424,8 @@ func (t *TailscaleInContainer) WaitForLogout() error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WaitForPeers blocks until N number of peers is present in the
|
||||||
|
// Peer list of the Tailscale instance.
|
||||||
func (t *TailscaleInContainer) WaitForPeers(expected int) error {
|
func (t *TailscaleInContainer) WaitForPeers(expected int) error {
|
||||||
return t.pool.Retry(func() error {
|
return t.pool.Retry(func() error {
|
||||||
status, err := t.Status()
|
status, err := t.Status()
|
||||||
@@ -390,17 +441,65 @@ func (t *TailscaleInContainer) WaitForPeers(expected int) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
// PingOption repreent optional settings that can be given
|
||||||
|
// to ping another host.
|
||||||
|
PingOption = func(args *pingArgs)
|
||||||
|
|
||||||
|
pingArgs struct {
|
||||||
|
timeout time.Duration
|
||||||
|
count int
|
||||||
|
direct bool
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// WithPingTimeout sets the timeout for the ping command.
|
||||||
|
func WithPingTimeout(timeout time.Duration) PingOption {
|
||||||
|
return func(args *pingArgs) {
|
||||||
|
args.timeout = timeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPingCount sets the count of pings to attempt.
|
||||||
|
func WithPingCount(count int) PingOption {
|
||||||
|
return func(args *pingArgs) {
|
||||||
|
args.count = count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPingUntilDirect decides if the ping should only succeed
|
||||||
|
// if a direct connection is established or if successful
|
||||||
|
// DERP ping is sufficient.
|
||||||
|
func WithPingUntilDirect(direct bool) PingOption {
|
||||||
|
return func(args *pingArgs) {
|
||||||
|
args.direct = direct
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ping executes the Tailscale ping command and pings a hostname
|
||||||
|
// or IP. It accepts a series of PingOption.
|
||||||
// TODO(kradalby): Make multiping, go routine magic.
|
// TODO(kradalby): Make multiping, go routine magic.
|
||||||
func (t *TailscaleInContainer) Ping(hostnameOrIP string) error {
|
func (t *TailscaleInContainer) Ping(hostnameOrIP string, opts ...PingOption) error {
|
||||||
return t.pool.Retry(func() error {
|
args := pingArgs{
|
||||||
command := []string{
|
timeout: time.Second,
|
||||||
"tailscale", "ping",
|
count: defaultPingCount,
|
||||||
"--timeout=1s",
|
direct: true,
|
||||||
"--c=10",
|
|
||||||
"--until-direct=true",
|
|
||||||
hostnameOrIP,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&args)
|
||||||
|
}
|
||||||
|
|
||||||
|
command := []string{
|
||||||
|
"tailscale", "ping",
|
||||||
|
fmt.Sprintf("--timeout=%s", args.timeout),
|
||||||
|
fmt.Sprintf("--c=%d", args.count),
|
||||||
|
fmt.Sprintf("--until-direct=%s", strconv.FormatBool(args.direct)),
|
||||||
|
}
|
||||||
|
|
||||||
|
command = append(command, hostnameOrIP)
|
||||||
|
|
||||||
|
return t.pool.Retry(func() error {
|
||||||
result, _, err := t.Execute(command)
|
result, _, err := t.Execute(command)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf(
|
log.Printf(
|
||||||
@@ -421,6 +520,7 @@ func (t *TailscaleInContainer) Ping(hostnameOrIP string) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WriteFile save file inside the Tailscale container.
|
||||||
func (t *TailscaleInContainer) WriteFile(path string, data []byte) error {
|
func (t *TailscaleInContainer) WriteFile(path string, data []byte) error {
|
||||||
return integrationutil.WriteFileToContainer(t.pool, t.container, path, data)
|
return integrationutil.WriteFileToContainer(t.pool, t.container, path, data)
|
||||||
}
|
}
|
||||||
|
48
integration/utils.go
Normal file
48
integration/utils.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func pingAllHelper(t *testing.T, clients []TailscaleClient, addrs []string) int {
|
||||||
|
t.Helper()
|
||||||
|
success := 0
|
||||||
|
|
||||||
|
for _, client := range clients {
|
||||||
|
for _, addr := range addrs {
|
||||||
|
err := client.Ping(addr)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to ping %s from %s: %s", addr, client.Hostname(), err)
|
||||||
|
} else {
|
||||||
|
success++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return success
|
||||||
|
}
|
||||||
|
|
||||||
|
// pingAllNegativeHelper is intended to have 1 or more nodes timeing out from the ping,
|
||||||
|
// it counts failures instead of successes.
|
||||||
|
// func pingAllNegativeHelper(t *testing.T, clients []TailscaleClient, addrs []string) int {
|
||||||
|
// t.Helper()
|
||||||
|
// failures := 0
|
||||||
|
//
|
||||||
|
// timeout := 100
|
||||||
|
// count := 3
|
||||||
|
//
|
||||||
|
// for _, client := range clients {
|
||||||
|
// for _, addr := range addrs {
|
||||||
|
// err := client.Ping(
|
||||||
|
// addr,
|
||||||
|
// tsic.WithPingTimeout(time.Duration(timeout)*time.Millisecond),
|
||||||
|
// tsic.WithPingCount(count),
|
||||||
|
// )
|
||||||
|
// if err != nil {
|
||||||
|
// failures++
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return failures
|
||||||
|
// }
|
@@ -24,7 +24,6 @@ type IntegrationCLITestSuite struct {
|
|||||||
pool dockertest.Pool
|
pool dockertest.Pool
|
||||||
network dockertest.Network
|
network dockertest.Network
|
||||||
headscale dockertest.Resource
|
headscale dockertest.Resource
|
||||||
env []string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIntegrationCLITestSuite(t *testing.T) {
|
func TestIntegrationCLITestSuite(t *testing.T) {
|
||||||
|
@@ -9,7 +9,6 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||||
@@ -32,7 +31,8 @@ var (
|
|||||||
tailscaleVersions = []string{
|
tailscaleVersions = []string{
|
||||||
"head",
|
"head",
|
||||||
"unstable",
|
"unstable",
|
||||||
"1.34.0",
|
"1.36.2",
|
||||||
|
"1.34.2",
|
||||||
"1.32.3",
|
"1.32.3",
|
||||||
"1.30.2",
|
"1.30.2",
|
||||||
"1.28.0",
|
"1.28.0",
|
||||||
@@ -47,11 +47,6 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
type TestUser struct {
|
|
||||||
count int
|
|
||||||
tailscales map[string]dockertest.Resource
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExecuteCommandConfig struct {
|
type ExecuteCommandConfig struct {
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
}
|
}
|
||||||
@@ -201,38 +196,6 @@ func getDockerBuildOptions(version string) *dockertest.BuildOptions {
|
|||||||
return tailscaleBuildOptions
|
return tailscaleBuildOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
func getIPs(
|
|
||||||
tailscales map[string]dockertest.Resource,
|
|
||||||
) (map[string][]netip.Addr, error) {
|
|
||||||
ips := make(map[string][]netip.Addr)
|
|
||||||
for hostname, tailscale := range tailscales {
|
|
||||||
command := []string{"tailscale", "ip"}
|
|
||||||
|
|
||||||
result, _, err := ExecuteCommand(
|
|
||||||
&tailscale,
|
|
||||||
command,
|
|
||||||
[]string{},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, address := range strings.Split(result, "\n") {
|
|
||||||
address = strings.TrimSuffix(address, "\n")
|
|
||||||
if len(address) < 1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ip, err := netip.ParseAddr(address)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ips[hostname] = append(ips[hostname], ip)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ips, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDNSNames(
|
func getDNSNames(
|
||||||
headscale *dockertest.Resource,
|
headscale *dockertest.Resource,
|
||||||
) ([]string, error) {
|
) ([]string, error) {
|
||||||
@@ -266,43 +229,6 @@ func getDNSNames(
|
|||||||
return hostnames, nil
|
return hostnames, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMagicFQDN(
|
|
||||||
headscale *dockertest.Resource,
|
|
||||||
) ([]string, error) {
|
|
||||||
listAllResult, _, err := ExecuteCommand(
|
|
||||||
headscale,
|
|
||||||
[]string{
|
|
||||||
"headscale",
|
|
||||||
"nodes",
|
|
||||||
"list",
|
|
||||||
"--output",
|
|
||||||
"json",
|
|
||||||
},
|
|
||||||
[]string{},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var listAll []v1.Machine
|
|
||||||
err = json.Unmarshal([]byte(listAllResult), &listAll)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
hostnames := make([]string, len(listAll))
|
|
||||||
|
|
||||||
for index := range listAll {
|
|
||||||
hostnames[index] = fmt.Sprintf(
|
|
||||||
"%s.%s.headscale.net",
|
|
||||||
listAll[index].GetGivenName(),
|
|
||||||
listAll[index].GetUser().GetName(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return hostnames, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetEnvStr(key string) (string, error) {
|
func GetEnvStr(key string) (string, error) {
|
||||||
v := os.Getenv(key)
|
v := os.Getenv(key)
|
||||||
if v == "" {
|
if v == "" {
|
||||||
|
@@ -382,7 +382,7 @@ func (s *IntegrationDERPTestSuite) saveLog(
|
|||||||
|
|
||||||
err = os.WriteFile(
|
err = os.WriteFile(
|
||||||
path.Join(basePath, resource.Container.Name+".stdout.log"),
|
path.Join(basePath, resource.Container.Name+".stdout.log"),
|
||||||
[]byte(stdout.String()),
|
stderr.Bytes(),
|
||||||
0o644,
|
0o644,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -391,7 +391,7 @@ func (s *IntegrationDERPTestSuite) saveLog(
|
|||||||
|
|
||||||
err = os.WriteFile(
|
err = os.WriteFile(
|
||||||
path.Join(basePath, resource.Container.Name+".stderr.log"),
|
path.Join(basePath, resource.Container.Name+".stderr.log"),
|
||||||
[]byte(stdout.String()),
|
stderr.Bytes(),
|
||||||
0o644,
|
0o644,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -37,12 +37,14 @@ logtail:
|
|||||||
enabled: false
|
enabled: false
|
||||||
metrics_listen_addr: 127.0.0.1:19090
|
metrics_listen_addr: 127.0.0.1:19090
|
||||||
oidc:
|
oidc:
|
||||||
|
expiry: 180d
|
||||||
only_start_if_oidc_is_available: true
|
only_start_if_oidc_is_available: true
|
||||||
scope:
|
scope:
|
||||||
- openid
|
- openid
|
||||||
- profile
|
- profile
|
||||||
- email
|
- email
|
||||||
strip_email_domain: true
|
strip_email_domain: true
|
||||||
|
use_expiry_from_token: false
|
||||||
private_key_path: private.key
|
private_key_path: private.key
|
||||||
noise:
|
noise:
|
||||||
private_key_path: noise_private.key
|
private_key_path: noise_private.key
|
||||||
|
@@ -36,12 +36,14 @@ logtail:
|
|||||||
enabled: false
|
enabled: false
|
||||||
metrics_listen_addr: 127.0.0.1:19090
|
metrics_listen_addr: 127.0.0.1:19090
|
||||||
oidc:
|
oidc:
|
||||||
|
expiry: 180d
|
||||||
only_start_if_oidc_is_available: true
|
only_start_if_oidc_is_available: true
|
||||||
scope:
|
scope:
|
||||||
- openid
|
- openid
|
||||||
- profile
|
- profile
|
||||||
- email
|
- email
|
||||||
strip_email_domain: true
|
strip_email_domain: true
|
||||||
|
use_expiry_from_token: false
|
||||||
private_key_path: private.key
|
private_key_path: private.key
|
||||||
noise:
|
noise:
|
||||||
private_key_path: noise_private.key
|
private_key_path: noise_private.key
|
||||||
|
@@ -37,12 +37,14 @@ logtail:
|
|||||||
enabled: false
|
enabled: false
|
||||||
metrics_listen_addr: 127.0.0.1:9090
|
metrics_listen_addr: 127.0.0.1:9090
|
||||||
oidc:
|
oidc:
|
||||||
|
expiry: 180d
|
||||||
only_start_if_oidc_is_available: true
|
only_start_if_oidc_is_available: true
|
||||||
scope:
|
scope:
|
||||||
- openid
|
- openid
|
||||||
- profile
|
- profile
|
||||||
- email
|
- email
|
||||||
strip_email_domain: true
|
strip_email_domain: true
|
||||||
|
use_expiry_from_token: false
|
||||||
private_key_path: private.key
|
private_key_path: private.key
|
||||||
noise:
|
noise:
|
||||||
private_key_path: noise_private.key
|
private_key_path: noise_private.key
|
||||||
|
29
machine.go
29
machine.go
@@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/samber/lo"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
@@ -738,30 +739,39 @@ func (h *Headscale) toNode(
|
|||||||
|
|
||||||
online := machine.isOnline()
|
online := machine.isOnline()
|
||||||
|
|
||||||
|
tags, _ := getTags(h.aclPolicy, machine, h.cfg.OIDC.StripEmaildomain)
|
||||||
|
tags = lo.Uniq(append(tags, machine.ForcedTags...))
|
||||||
|
|
||||||
node := tailcfg.Node{
|
node := tailcfg.Node{
|
||||||
ID: tailcfg.NodeID(machine.ID), // this is the actual ID
|
ID: tailcfg.NodeID(machine.ID), // this is the actual ID
|
||||||
StableID: tailcfg.StableNodeID(
|
StableID: tailcfg.StableNodeID(
|
||||||
strconv.FormatUint(machine.ID, Base10),
|
strconv.FormatUint(machine.ID, Base10),
|
||||||
), // in headscale, unlike tailcontrol server, IDs are permanent
|
), // in headscale, unlike tailcontrol server, IDs are permanent
|
||||||
Name: hostname,
|
Name: hostname,
|
||||||
|
|
||||||
User: tailcfg.UserID(machine.UserID),
|
User: tailcfg.UserID(machine.UserID),
|
||||||
|
|
||||||
Key: nodeKey,
|
Key: nodeKey,
|
||||||
KeyExpiry: keyExpiry,
|
KeyExpiry: keyExpiry,
|
||||||
|
|
||||||
Machine: machineKey,
|
Machine: machineKey,
|
||||||
DiscoKey: discoKey,
|
DiscoKey: discoKey,
|
||||||
Addresses: addrs,
|
Addresses: addrs,
|
||||||
AllowedIPs: allowedIPs,
|
AllowedIPs: allowedIPs,
|
||||||
PrimaryRoutes: primaryPrefixes,
|
|
||||||
Endpoints: machine.Endpoints,
|
Endpoints: machine.Endpoints,
|
||||||
DERP: derp,
|
DERP: derp,
|
||||||
|
|
||||||
Online: &online,
|
|
||||||
Hostinfo: hostInfo.View(),
|
Hostinfo: hostInfo.View(),
|
||||||
Created: machine.CreatedAt,
|
Created: machine.CreatedAt,
|
||||||
LastSeen: machine.LastSeen,
|
|
||||||
|
|
||||||
|
Tags: tags,
|
||||||
|
|
||||||
|
PrimaryRoutes: primaryPrefixes,
|
||||||
|
|
||||||
|
LastSeen: machine.LastSeen,
|
||||||
|
Online: &online,
|
||||||
KeepAlive: true,
|
KeepAlive: true,
|
||||||
MachineAuthorized: !machine.isExpired(),
|
MachineAuthorized: !machine.isExpired(),
|
||||||
|
|
||||||
Capabilities: []string{
|
Capabilities: []string{
|
||||||
tailcfg.CapabilityFileSharing,
|
tailcfg.CapabilityFileSharing,
|
||||||
tailcfg.CapabilityAdmin,
|
tailcfg.CapabilityAdmin,
|
||||||
@@ -1047,8 +1057,8 @@ func (h *Headscale) IsRoutesEnabled(machine *Machine, routeStr string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnableRoutes enables new routes based on a list of new routes.
|
// enableRoutes enables new routes based on a list of new routes.
|
||||||
func (h *Headscale) EnableRoutes(machine *Machine, routeStrs ...string) error {
|
func (h *Headscale) enableRoutes(machine *Machine, routeStrs ...string) error {
|
||||||
newRoutes := make([]netip.Prefix, len(routeStrs))
|
newRoutes := make([]netip.Prefix, len(routeStrs))
|
||||||
for index, routeStr := range routeStrs {
|
for index, routeStr := range routeStrs {
|
||||||
route, err := netip.ParsePrefix(routeStr)
|
route, err := netip.ParsePrefix(routeStr)
|
||||||
@@ -1112,7 +1122,8 @@ func (h *Headscale) EnableAutoApprovedRoutes(machine *Machine) error {
|
|||||||
routes := []Route{}
|
routes := []Route{}
|
||||||
err := h.db.
|
err := h.db.
|
||||||
Preload("Machine").
|
Preload("Machine").
|
||||||
Where("machine_id = ? AND advertised = true AND enabled = false", machine.ID).Find(&routes).Error
|
Where("machine_id = ? AND advertised = true AND enabled = false", machine.ID).
|
||||||
|
Find(&routes).Error
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
log.Error().
|
log.Error().
|
||||||
Caller().
|
Caller().
|
||||||
@@ -1126,7 +1137,9 @@ func (h *Headscale) EnableAutoApprovedRoutes(machine *Machine) error {
|
|||||||
approvedRoutes := []Route{}
|
approvedRoutes := []Route{}
|
||||||
|
|
||||||
for _, advertisedRoute := range routes {
|
for _, advertisedRoute := range routes {
|
||||||
routeApprovers, err := h.aclPolicy.AutoApprovers.GetRouteApprovers(netip.Prefix(advertisedRoute.Prefix))
|
routeApprovers, err := h.aclPolicy.AutoApprovers.GetRouteApprovers(
|
||||||
|
netip.Prefix(advertisedRoute.Prefix),
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).
|
log.Err(err).
|
||||||
Str("advertisedRoute", advertisedRoute.String()).
|
Str("advertisedRoute", advertisedRoute.String()).
|
||||||
|
22
oidc.go
22
oidc.go
@@ -27,7 +27,9 @@ const (
|
|||||||
errOIDCAllowedDomains = Error("authenticated principal does not match any allowed domain")
|
errOIDCAllowedDomains = Error("authenticated principal does not match any allowed domain")
|
||||||
errOIDCAllowedGroups = Error("authenticated principal is not in any allowed group")
|
errOIDCAllowedGroups = Error("authenticated principal is not in any allowed group")
|
||||||
errOIDCAllowedUsers = Error("authenticated principal does not match any allowed user")
|
errOIDCAllowedUsers = Error("authenticated principal does not match any allowed user")
|
||||||
errOIDCInvalidMachineState = Error("requested machine state key expired before authorisation completed")
|
errOIDCInvalidMachineState = Error(
|
||||||
|
"requested machine state key expired before authorisation completed",
|
||||||
|
)
|
||||||
errOIDCNodeKeyMissing = Error("could not get node key from cache")
|
errOIDCNodeKeyMissing = Error("could not get node key from cache")
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -68,6 +70,14 @@ func (h *Headscale) initOIDC() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Headscale) determineTokenExpiration(idTokenExpiration time.Time) time.Time {
|
||||||
|
if h.cfg.OIDC.UseExpiryFromToken {
|
||||||
|
return idTokenExpiration
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Now().Add(h.cfg.OIDC.Expiry)
|
||||||
|
}
|
||||||
|
|
||||||
// RegisterOIDC redirects to the OIDC provider for authentication
|
// RegisterOIDC redirects to the OIDC provider for authentication
|
||||||
// Puts NodeKey in cache so the callback can retrieve it using the oidc state param
|
// Puts NodeKey in cache so the callback can retrieve it using the oidc state param
|
||||||
// Listens in /oidc/register/:nKey.
|
// Listens in /oidc/register/:nKey.
|
||||||
@@ -193,6 +203,7 @@ func (h *Headscale) OIDCCallback(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
idTokenExpiry := h.determineTokenExpiration(idToken.Expiry)
|
||||||
|
|
||||||
// TODO: we can use userinfo at some point to grab additional information about the user (groups membership, etc)
|
// TODO: we can use userinfo at some point to grab additional information about the user (groups membership, etc)
|
||||||
// userInfo, err := oidcProvider.UserInfo(context.Background(), oauth2.StaticTokenSource(oauth2Token))
|
// userInfo, err := oidcProvider.UserInfo(context.Background(), oauth2.StaticTokenSource(oauth2Token))
|
||||||
@@ -218,7 +229,12 @@ func (h *Headscale) OIDCCallback(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
nodeKey, machineExists, err := h.validateMachineForOIDCCallback(writer, state, claims, idToken.Expiry)
|
nodeKey, machineExists, err := h.validateMachineForOIDCCallback(
|
||||||
|
writer,
|
||||||
|
state,
|
||||||
|
claims,
|
||||||
|
idTokenExpiry,
|
||||||
|
)
|
||||||
if err != nil || machineExists {
|
if err != nil || machineExists {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -236,7 +252,7 @@ func (h *Headscale) OIDCCallback(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.registerMachineForOIDCCallback(writer, user, nodeKey, idToken.Expiry); err != nil {
|
if err := h.registerMachineForOIDCCallback(writer, user, nodeKey, idTokenExpiry); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -3,9 +3,11 @@ package headscale
|
|||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/klauspost/compress/zstd"
|
"github.com/klauspost/compress/zstd"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
"tailscale.com/smallzstd"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/key"
|
"tailscale.com/types/key"
|
||||||
)
|
)
|
||||||
@@ -103,8 +105,7 @@ func (h *Headscale) marshalMapResponse(
|
|||||||
|
|
||||||
var respBody []byte
|
var respBody []byte
|
||||||
if compression == ZstdCompression {
|
if compression == ZstdCompression {
|
||||||
encoder, _ := zstd.NewWriter(nil)
|
respBody = zstdEncode(jsonBody)
|
||||||
respBody = encoder.EncodeAll(jsonBody, nil)
|
|
||||||
if !isNoise { // if legacy protocol
|
if !isNoise { // if legacy protocol
|
||||||
respBody = h.privateKey.SealTo(machineKey, respBody)
|
respBody = h.privateKey.SealTo(machineKey, respBody)
|
||||||
}
|
}
|
||||||
@@ -122,3 +123,28 @@ func (h *Headscale) marshalMapResponse(
|
|||||||
|
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func zstdEncode(in []byte) []byte {
|
||||||
|
encoder, ok := zstdEncoderPool.Get().(*zstd.Encoder)
|
||||||
|
if !ok {
|
||||||
|
panic("invalid type in sync pool")
|
||||||
|
}
|
||||||
|
out := encoder.EncodeAll(in, nil)
|
||||||
|
_ = encoder.Close()
|
||||||
|
zstdEncoderPool.Put(encoder)
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
var zstdEncoderPool = &sync.Pool{
|
||||||
|
New: func() any {
|
||||||
|
encoder, err := smallzstd.NewEncoder(
|
||||||
|
nil,
|
||||||
|
zstd.WithEncoderLevel(zstd.SpeedFastest))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return encoder
|
||||||
|
},
|
||||||
|
}
|
||||||
|
@@ -90,7 +90,14 @@ func (h *Headscale) EnableRoute(id uint64) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return h.EnableRoutes(&route.Machine, netip.Prefix(route.Prefix).String())
|
// Tailscale requires both IPv4 and IPv6 exit routes to
|
||||||
|
// be enabled at the same time, as per
|
||||||
|
// https://github.com/juanfont/headscale/issues/804#issuecomment-1399314002
|
||||||
|
if route.isExitRoute() {
|
||||||
|
return h.enableRoutes(&route.Machine, ExitRouteV4.String(), ExitRouteV6.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.enableRoutes(&route.Machine, netip.Prefix(route.Prefix).String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) DisableRoute(id uint64) error {
|
func (h *Headscale) DisableRoute(id uint64) error {
|
||||||
|
@@ -46,10 +46,10 @@ func (s *Suite) TestGetRoutes(c *check.C) {
|
|||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
c.Assert(len(advertisedRoutes), check.Equals, 1)
|
c.Assert(len(advertisedRoutes), check.Equals, 1)
|
||||||
|
|
||||||
err = app.EnableRoutes(&machine, "192.168.0.0/24")
|
err = app.enableRoutes(&machine, "192.168.0.0/24")
|
||||||
c.Assert(err, check.NotNil)
|
c.Assert(err, check.NotNil)
|
||||||
|
|
||||||
err = app.EnableRoutes(&machine, "10.0.0.0/24")
|
err = app.enableRoutes(&machine, "10.0.0.0/24")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,10 +102,10 @@ func (s *Suite) TestGetEnableRoutes(c *check.C) {
|
|||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
c.Assert(len(noEnabledRoutes), check.Equals, 0)
|
c.Assert(len(noEnabledRoutes), check.Equals, 0)
|
||||||
|
|
||||||
err = app.EnableRoutes(&machine, "192.168.0.0/24")
|
err = app.enableRoutes(&machine, "192.168.0.0/24")
|
||||||
c.Assert(err, check.NotNil)
|
c.Assert(err, check.NotNil)
|
||||||
|
|
||||||
err = app.EnableRoutes(&machine, "10.0.0.0/24")
|
err = app.enableRoutes(&machine, "10.0.0.0/24")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
enabledRoutes, err := app.GetEnabledRoutes(&machine)
|
enabledRoutes, err := app.GetEnabledRoutes(&machine)
|
||||||
@@ -113,14 +113,14 @@ func (s *Suite) TestGetEnableRoutes(c *check.C) {
|
|||||||
c.Assert(len(enabledRoutes), check.Equals, 1)
|
c.Assert(len(enabledRoutes), check.Equals, 1)
|
||||||
|
|
||||||
// Adding it twice will just let it pass through
|
// Adding it twice will just let it pass through
|
||||||
err = app.EnableRoutes(&machine, "10.0.0.0/24")
|
err = app.enableRoutes(&machine, "10.0.0.0/24")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
enableRoutesAfterDoubleApply, err := app.GetEnabledRoutes(&machine)
|
enableRoutesAfterDoubleApply, err := app.GetEnabledRoutes(&machine)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
c.Assert(len(enableRoutesAfterDoubleApply), check.Equals, 1)
|
c.Assert(len(enableRoutesAfterDoubleApply), check.Equals, 1)
|
||||||
|
|
||||||
err = app.EnableRoutes(&machine, "150.0.10.0/25")
|
err = app.enableRoutes(&machine, "150.0.10.0/25")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
enabledRoutesWithAdditionalRoute, err := app.GetEnabledRoutes(&machine)
|
enabledRoutesWithAdditionalRoute, err := app.GetEnabledRoutes(&machine)
|
||||||
@@ -167,10 +167,10 @@ func (s *Suite) TestIsUniquePrefix(c *check.C) {
|
|||||||
err = app.processMachineRoutes(&machine1)
|
err = app.processMachineRoutes(&machine1)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
err = app.EnableRoutes(&machine1, route.String())
|
err = app.enableRoutes(&machine1, route.String())
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
err = app.EnableRoutes(&machine1, route2.String())
|
err = app.enableRoutes(&machine1, route2.String())
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
hostInfo2 := tailcfg.Hostinfo{
|
hostInfo2 := tailcfg.Hostinfo{
|
||||||
@@ -192,7 +192,7 @@ func (s *Suite) TestIsUniquePrefix(c *check.C) {
|
|||||||
err = app.processMachineRoutes(&machine2)
|
err = app.processMachineRoutes(&machine2)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
err = app.EnableRoutes(&machine2, route2.String())
|
err = app.enableRoutes(&machine2, route2.String())
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
enabledRoutes1, err := app.GetEnabledRoutes(&machine1)
|
enabledRoutes1, err := app.GetEnabledRoutes(&machine1)
|
||||||
@@ -254,10 +254,10 @@ func (s *Suite) TestSubnetFailover(c *check.C) {
|
|||||||
err = app.processMachineRoutes(&machine1)
|
err = app.processMachineRoutes(&machine1)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
err = app.EnableRoutes(&machine1, prefix.String())
|
err = app.enableRoutes(&machine1, prefix.String())
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
err = app.EnableRoutes(&machine1, prefix2.String())
|
err = app.enableRoutes(&machine1, prefix2.String())
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
err = app.handlePrimarySubnetFailover()
|
err = app.handlePrimarySubnetFailover()
|
||||||
@@ -291,7 +291,7 @@ func (s *Suite) TestSubnetFailover(c *check.C) {
|
|||||||
err = app.processMachineRoutes(&machine2)
|
err = app.processMachineRoutes(&machine2)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
err = app.EnableRoutes(&machine2, prefix2.String())
|
err = app.enableRoutes(&machine2, prefix2.String())
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
err = app.handlePrimarySubnetFailover()
|
err = app.handlePrimarySubnetFailover()
|
||||||
@@ -339,7 +339,7 @@ func (s *Suite) TestSubnetFailover(c *check.C) {
|
|||||||
err = app.processMachineRoutes(&machine2)
|
err = app.processMachineRoutes(&machine2)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
err = app.EnableRoutes(&machine2, prefix.String())
|
err = app.enableRoutes(&machine2, prefix.String())
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
err = app.handlePrimarySubnetFailover()
|
err = app.handlePrimarySubnetFailover()
|
||||||
@@ -413,18 +413,24 @@ func (s *Suite) TestAllowedIPRoutes(c *check.C) {
|
|||||||
err = app.processMachineRoutes(&machine1)
|
err = app.processMachineRoutes(&machine1)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
err = app.EnableRoutes(&machine1, prefix.String())
|
err = app.enableRoutes(&machine1, prefix.String())
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
// We do not enable this one on purpose to test that it is not enabled
|
// We do not enable this one on purpose to test that it is not enabled
|
||||||
// err = app.EnableRoutes(&machine1, prefix2.String())
|
// err = app.enableRoutes(&machine1, prefix2.String())
|
||||||
// c.Assert(err, check.IsNil)
|
// c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
err = app.EnableRoutes(&machine1, prefixExitNodeV4.String())
|
routes, err := app.GetMachineRoutes(&machine1)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
for _, route := range routes {
|
||||||
|
if route.isExitRoute() {
|
||||||
|
err = app.EnableRoute(uint64(route.ID))
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
err = app.EnableRoutes(&machine1, prefixExitNodeV6.String())
|
// We only enable one exit route, so we can test that both are enabled
|
||||||
c.Assert(err, check.IsNil)
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err = app.handlePrimarySubnetFailover()
|
err = app.handlePrimarySubnetFailover()
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
@@ -4,11 +4,27 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Document</title>
|
<title>headscale - Apple</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 40px auto;
|
||||||
|
max-width: 800px;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #444;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-family: Sans-serif;
|
||||||
|
}
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3 {
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<h1>headscale</h1>
|
<h1>headscale: Apple configuration</h1>
|
||||||
<h2>Recent Tailscale versions (1.34.0 and higher)</h2>
|
<h2>Recent Tailscale versions (1.34.0 and higher)</h2>
|
||||||
<p>
|
<p>
|
||||||
Tailscale added Fast User Switching in version 1.34 and you can now use
|
Tailscale added Fast User Switching in version 1.34 and you can now use
|
||||||
|
@@ -4,11 +4,27 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Document</title>
|
<title>headscale - Windows</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 40px auto;
|
||||||
|
max-width: 800px;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #444;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-family: Sans-serif;
|
||||||
|
}
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3 {
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<h1>headscale</h1>
|
<h1>headscale: Windows configuration</h1>
|
||||||
<h2>Recent Tailscale versions (1.34.0 and higher)</h2>
|
<h2>Recent Tailscale versions (1.34.0 and higher)</h2>
|
||||||
<p>
|
<p>
|
||||||
Tailscale added Fast User Switching in version 1.34 and you can now use
|
Tailscale added Fast User Switching in version 1.34 and you can now use
|
||||||
|
Reference in New Issue
Block a user