mirror of
https://github.com/juanfont/headscale.git
synced 2026-03-09 14:50:03 +01:00
Compare commits
8 Commits
web-auth-f
...
fix-integr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2d1b88b80 | ||
|
|
c0846978ed | ||
|
|
2b683aa0ee | ||
|
|
947095020b | ||
|
|
671620bc8b | ||
|
|
b11c77f692 | ||
|
|
dbc25e992f | ||
|
|
0f3dc9de4e |
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ko_fi: kradalby
|
||||||
|
github: [kradalby]
|
||||||
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -13,13 +13,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v34
|
uses: tj-actions/changed-files@v14.1
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
*.nix
|
*.nix
|
||||||
|
|||||||
2
.github/workflows/contributors.yml
vendored
2
.github/workflows/contributors.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
add-contributors:
|
add-contributors:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v2
|
||||||
- name: Delete upstream contributor branch
|
- name: Delete upstream contributor branch
|
||||||
# Allow continue on failure to account for when the
|
# Allow continue on failure to account for when the
|
||||||
# upstream branch is deleted or does not exist.
|
# upstream branch is deleted or does not exist.
|
||||||
|
|||||||
10
.github/workflows/lint.yml
vendored
10
.github/workflows/lint.yml
vendored
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: Lint
|
name: CI
|
||||||
|
|
||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
|
|
||||||
@@ -7,13 +7,13 @@ jobs:
|
|||||||
golangci-lint:
|
golangci-lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v34
|
uses: tj-actions/changed-files@v14.1
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
*.nix
|
*.nix
|
||||||
@@ -26,7 +26,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.46.1
|
||||||
|
|
||||||
# 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
|
||||||
@@ -70,7 +70,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: bufbuild/buf-setup-action@v1.7.0
|
- uses: bufbuild/buf-setup-action@v0.7.0
|
||||||
- uses: bufbuild/buf-lint-action@v1
|
- uses: bufbuild/buf-lint-action@v1
|
||||||
with:
|
with:
|
||||||
input: "proto"
|
input: "proto"
|
||||||
|
|||||||
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: Release
|
name: release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -12,13 +12,13 @@ jobs:
|
|||||||
runs-on: ubuntu-18.04 # due to CGO we need to user an older version
|
runs-on: ubuntu-18.04 # due to CGO we need to user an older version
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v2
|
||||||
with:
|
with:
|
||||||
go-version: 1.19.0
|
go-version: 1.18.0
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
@@ -100,7 +100,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
@@ -166,7 +166,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
|
|||||||
2
.github/workflows/renovatebot.yml
vendored
2
.github/workflows/renovatebot.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
APP_ID: ${{ secrets.RENOVATEBOT_APP_ID }}
|
APP_ID: ${{ secrets.RENOVATEBOT_APP_ID }}
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v2.0.0
|
||||||
|
|
||||||
- name: Self-hosted Renovate
|
- name: Self-hosted Renovate
|
||||||
uses: renovatebot/github-action@v31.81.3
|
uses: renovatebot/github-action@v31.81.3
|
||||||
|
|||||||
35
.github/workflows/test-integration-cli.yml
vendored
35
.github/workflows/test-integration-cli.yml
vendored
@@ -1,35 +0,0 @@
|
|||||||
name: Integration Test CLI
|
|
||||||
|
|
||||||
on: [pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
integration-test-cli:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 2
|
|
||||||
|
|
||||||
- name: Set Swap Space
|
|
||||||
uses: pierotofy/set-swap-space@master
|
|
||||||
with:
|
|
||||||
swap-size-gb: 10
|
|
||||||
|
|
||||||
- name: Get changed files
|
|
||||||
id: changed-files
|
|
||||||
uses: tj-actions/changed-files@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 CLI integration tests
|
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
run: nix develop --command -- make test_integration_cli
|
|
||||||
35
.github/workflows/test-integration-derp.yml
vendored
35
.github/workflows/test-integration-derp.yml
vendored
@@ -1,35 +0,0 @@
|
|||||||
name: Integration Test DERP
|
|
||||||
|
|
||||||
on: [pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
integration-test-derp:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 2
|
|
||||||
|
|
||||||
- name: Set Swap Space
|
|
||||||
uses: pierotofy/set-swap-space@master
|
|
||||||
with:
|
|
||||||
swap-size-gb: 10
|
|
||||||
|
|
||||||
- name: Get changed files
|
|
||||||
id: changed-files
|
|
||||||
uses: tj-actions/changed-files@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 Embedded DERP server integration tests
|
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
run: nix develop --command -- make test_integration_derp
|
|
||||||
35
.github/workflows/test-integration-oidc.yml
vendored
35
.github/workflows/test-integration-oidc.yml
vendored
@@ -1,35 +0,0 @@
|
|||||||
name: Integration Test OIDC
|
|
||||||
|
|
||||||
on: [pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
integration-test-oidc:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 2
|
|
||||||
|
|
||||||
- name: Set Swap Space
|
|
||||||
uses: pierotofy/set-swap-space@master
|
|
||||||
with:
|
|
||||||
swap-size-gb: 10
|
|
||||||
|
|
||||||
- name: Get changed files
|
|
||||||
id: changed-files
|
|
||||||
uses: tj-actions/changed-files@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 OIDC integration tests
|
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
run: nix develop --command -- make test_integration_oidc
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
name: Integration Test v2
|
|
||||||
|
|
||||||
on: [pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
integration-test-v2:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
fetch-depth: 2
|
|
||||||
|
|
||||||
- name: Set Swap Space
|
|
||||||
uses: pierotofy/set-swap-space@master
|
|
||||||
with:
|
|
||||||
swap-size-gb: 10
|
|
||||||
|
|
||||||
- name: Get changed files
|
|
||||||
id: changed-files
|
|
||||||
uses: tj-actions/changed-files@v14.1
|
|
||||||
with:
|
|
||||||
files: |
|
|
||||||
*.nix
|
|
||||||
go.*
|
|
||||||
**/*.go
|
|
||||||
integration_test/
|
|
||||||
config-example.yaml
|
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v16
|
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
run: nix develop --command -- make test_integration_v2_auth_web_flow
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
name: Integration Test v2 - kradalby
|
|
||||||
|
|
||||||
on: [pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
integration-test-v2-kradalby:
|
|
||||||
runs-on: [self-hosted, linux, x64, nixos, docker]
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
run: nix develop --command -- make test_integration_v2_general
|
|
||||||
52
.github/workflows/test-integration.yml
vendored
Normal file
52
.github/workflows/test-integration.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
integration-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Inspect builder
|
||||||
|
run: |
|
||||||
|
echo "Name: ${{ steps.buildx.outputs.name }}"
|
||||||
|
echo "Endpoint: ${{ steps.buildx.outputs.endpoint }}"
|
||||||
|
echo "Status: ${{ steps.buildx.outputs.status }}"
|
||||||
|
echo "Flags: ${{ steps.buildx.outputs.flags }}"
|
||||||
|
echo "Platforms: ${{ steps.buildx.outputs.platforms }}"
|
||||||
|
|
||||||
|
- name: check buildx version
|
||||||
|
run: docker buildx version
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Get changed files
|
||||||
|
id: changed-files
|
||||||
|
uses: tj-actions/changed-files@v14.1
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
*.nix
|
||||||
|
go.*
|
||||||
|
**/*.go
|
||||||
|
integration_test/
|
||||||
|
config-example.yaml
|
||||||
|
Dockerfile*
|
||||||
|
|
||||||
|
- uses: cachix/install-nix-action@v16
|
||||||
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
|
- name: Run Integration tests
|
||||||
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
uses: nick-fields/retry@v2
|
||||||
|
with:
|
||||||
|
timeout_minutes: 240
|
||||||
|
max_attempts: 5
|
||||||
|
retry_on: error
|
||||||
|
command: nix develop --command -- make test_integration
|
||||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Tests
|
name: CI
|
||||||
|
|
||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
|
|
||||||
@@ -7,13 +7,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v34
|
uses: tj-actions/changed-files@v14.1
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
*.nix
|
*.nix
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
---
|
---
|
||||||
run:
|
run:
|
||||||
timeout: 10m
|
timeout: 10m
|
||||||
build-tags:
|
|
||||||
- ts2019
|
|
||||||
|
|
||||||
issues:
|
issues:
|
||||||
skip-dirs:
|
skip-dirs:
|
||||||
@@ -28,7 +26,6 @@ linters:
|
|||||||
- ireturn
|
- ireturn
|
||||||
- execinquery
|
- execinquery
|
||||||
- exhaustruct
|
- exhaustruct
|
||||||
- nolintlint
|
|
||||||
|
|
||||||
# We should strive to enable these:
|
# We should strive to enable these:
|
||||||
- wrapcheck
|
- wrapcheck
|
||||||
@@ -36,9 +33,6 @@ linters:
|
|||||||
- makezero
|
- makezero
|
||||||
- maintidx
|
- maintidx
|
||||||
|
|
||||||
# Limits the methods of an interface to 10. We have more in integration tests
|
|
||||||
- interfacebloat
|
|
||||||
|
|
||||||
# We might want to enable this, but it might be a lot of work
|
# We might want to enable this, but it might be a lot of work
|
||||||
- cyclop
|
- cyclop
|
||||||
- nestif
|
- nestif
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
before:
|
before:
|
||||||
hooks:
|
hooks:
|
||||||
- go mod tidy -compat=1.19
|
- go mod tidy -compat=1.18
|
||||||
|
|
||||||
release:
|
release:
|
||||||
prerelease: auto
|
prerelease: auto
|
||||||
@@ -20,8 +20,6 @@ builds:
|
|||||||
- -mod=readonly
|
- -mod=readonly
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
|
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
|
||||||
tags:
|
|
||||||
- ts2019
|
|
||||||
|
|
||||||
- id: darwin-arm64
|
- id: darwin-arm64
|
||||||
main: ./cmd/headscale/headscale.go
|
main: ./cmd/headscale/headscale.go
|
||||||
@@ -36,8 +34,6 @@ builds:
|
|||||||
- -mod=readonly
|
- -mod=readonly
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
|
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
|
||||||
tags:
|
|
||||||
- ts2019
|
|
||||||
|
|
||||||
- id: linux-amd64
|
- id: linux-amd64
|
||||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||||
@@ -50,8 +46,6 @@ builds:
|
|||||||
main: ./cmd/headscale/headscale.go
|
main: ./cmd/headscale/headscale.go
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
|
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
|
||||||
tags:
|
|
||||||
- ts2019
|
|
||||||
|
|
||||||
- id: linux-arm64
|
- id: linux-arm64
|
||||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||||
@@ -64,8 +58,6 @@ builds:
|
|||||||
main: ./cmd/headscale/headscale.go
|
main: ./cmd/headscale/headscale.go
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
|
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
|
||||||
tags:
|
|
||||||
- ts2019
|
|
||||||
|
|
||||||
archives:
|
archives:
|
||||||
- id: golang-cross
|
- id: golang-cross
|
||||||
|
|||||||
56
CHANGELOG.md
56
CHANGELOG.md
@@ -1,58 +1,6 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
## 0.17.0 (2022-XX-XX)
|
## 0.17.0 (2022-xx-xx)
|
||||||
|
|
||||||
### BREAKING
|
|
||||||
|
|
||||||
- Log level option `log_level` was moved to a distinct `log` config section and renamed to `level` [#768](https://github.com/juanfont/headscale/pull/768)
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
|
|
||||||
- Added support for Tailscale TS2021 protocol [#738](https://github.com/juanfont/headscale/pull/738)
|
|
||||||
- Add ability to specify config location via env var `HEADSCALE_CONFIG` [#674](https://github.com/juanfont/headscale/issues/674)
|
|
||||||
- Target Go 1.19 for Headscale [#778](https://github.com/juanfont/headscale/pull/778)
|
|
||||||
- Target Tailscale v1.30.0 to build Headscale [#780](https://github.com/juanfont/headscale/pull/780)
|
|
||||||
- Give a warning when running Headscale with reverse proxy improperly configured for WebSockets [#788](https://github.com/juanfont/headscale/pull/788)
|
|
||||||
- Fix subnet routers with Primary Routes [#811](https://github.com/juanfont/headscale/pull/811)
|
|
||||||
- Added support for JSON logs [#653](https://github.com/juanfont/headscale/issues/653)
|
|
||||||
- Sanitise the node key passed to registration url [#823](https://github.com/juanfont/headscale/pull/823)
|
|
||||||
- Add support for generating pre-auth keys with tags [#767](https://github.com/juanfont/headscale/pull/767)
|
|
||||||
- Add support for evaluating `autoApprovers` ACL entries when a machine is registered [#763](https://github.com/juanfont/headscale/pull/763)
|
|
||||||
- Add config flag to allow Headscale to start if OIDC provider is down [#829](https://github.com/juanfont/headscale/pull/829)
|
|
||||||
- Fix prefix length comparison bug in AutoApprovers route evaluation [#862](https://github.com/juanfont/headscale/pull/862)
|
|
||||||
- Random node DNS suffix only applied if names collide in namespace. [#766](https://github.com/juanfont/headscale/issues/766)
|
|
||||||
- Remove `ip_prefix` configuration option and warning [#899](https://github.com/juanfont/headscale/pull/899)
|
|
||||||
- Add `dns_config.override_local_dns` option [#905](https://github.com/juanfont/headscale/pull/905)
|
|
||||||
- Fix some DNS config issues [#660](https://github.com/juanfont/headscale/issues/660)
|
|
||||||
- Make it possible to disable TS2019 with build flag [#928](https://github.com/juanfont/headscale/pull/928)
|
|
||||||
|
|
||||||
## 0.16.4 (2022-08-21)
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
|
|
||||||
- Add ability to connect to PostgreSQL over TLS/SSL [#745](https://github.com/juanfont/headscale/pull/745)
|
|
||||||
- Fix CLI registration of expired machines [#754](https://github.com/juanfont/headscale/pull/754)
|
|
||||||
|
|
||||||
## 0.16.3 (2022-08-17)
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
|
|
||||||
- Fix issue with OIDC authentication [#747](https://github.com/juanfont/headscale/pull/747)
|
|
||||||
|
|
||||||
## 0.16.2 (2022-08-14)
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
|
|
||||||
- Fixed bugs in the client registration process after migration to NodeKey [#735](https://github.com/juanfont/headscale/pull/735)
|
|
||||||
|
|
||||||
## 0.16.1 (2022-08-12)
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
|
|
||||||
- Updated dependencies (including the library that lacked armhf support) [#722](https://github.com/juanfont/headscale/pull/722)
|
|
||||||
- Fix missing group expansion in function `excludeCorretlyTaggedNodes` [#563](https://github.com/juanfont/headscale/issues/563)
|
|
||||||
- Improve registration protocol implementation and switch to NodeKey as main identifier [#725](https://github.com/juanfont/headscale/pull/725)
|
|
||||||
- Add ability to connect to PostgreSQL via unix socket [#734](https://github.com/juanfont/headscale/pull/734)
|
|
||||||
|
|
||||||
## 0.16.0 (2022-07-25)
|
## 0.16.0 (2022-07-25)
|
||||||
|
|
||||||
@@ -162,7 +110,7 @@ This is a part of aligning `headscale`'s behaviour with Tailscale's upstream beh
|
|||||||
- OpenID Connect users will be mapped per namespaces
|
- OpenID Connect users will be mapped per namespaces
|
||||||
- Each user will get its own namespace, created if it does not exist
|
- Each user will get its own namespace, created if it does not exist
|
||||||
- `oidc.domain_map` option has been removed
|
- `oidc.domain_map` option has been removed
|
||||||
- `strip_email_domain` option has been added (see [config-example.yaml](./config-example.yaml))
|
- `strip_email_domain` option has been added (see [config-example.yaml](./config_example.yaml))
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
# Contributor Covenant Code of Conduct
|
|
||||||
|
|
||||||
## Our Pledge
|
|
||||||
|
|
||||||
We as members, contributors, and leaders pledge to make participation
|
|
||||||
in our community a harassment-free experience for everyone, regardless
|
|
||||||
of age, body size, visible or invisible disability, ethnicity, sex
|
|
||||||
characteristics, gender identity and expression, level of experience,
|
|
||||||
education, socio-economic status, nationality, personal appearance,
|
|
||||||
race, religion, or sexual identity and orientation.
|
|
||||||
|
|
||||||
We pledge to act and interact in ways that contribute to an open,
|
|
||||||
welcoming, diverse, inclusive, and healthy community.
|
|
||||||
|
|
||||||
## Our Standards
|
|
||||||
|
|
||||||
Examples of behavior that contributes to a positive environment for
|
|
||||||
our community include:
|
|
||||||
|
|
||||||
- Demonstrating empathy and kindness toward other people
|
|
||||||
- Being respectful of differing opinions, viewpoints, and experiences
|
|
||||||
- Giving and gracefully accepting constructive feedback
|
|
||||||
- Accepting responsibility and apologizing to those affected by our
|
|
||||||
mistakes, and learning from the experience
|
|
||||||
- Focusing on what is best not just for us as individuals, but for the
|
|
||||||
overall community
|
|
||||||
|
|
||||||
Examples of unacceptable behavior include:
|
|
||||||
|
|
||||||
- The use of sexualized language or imagery, and sexual attention or
|
|
||||||
advances of any kind
|
|
||||||
- Trolling, insulting or derogatory comments, and personal or
|
|
||||||
political attacks
|
|
||||||
- Public or private harassment
|
|
||||||
- Publishing others' private information, such as a physical or email
|
|
||||||
address, without their explicit permission
|
|
||||||
- Other conduct which could reasonably be considered inappropriate in
|
|
||||||
a professional setting
|
|
||||||
|
|
||||||
## Enforcement Responsibilities
|
|
||||||
|
|
||||||
Community leaders are responsible for clarifying and enforcing our
|
|
||||||
standards of acceptable behavior and will take appropriate and fair
|
|
||||||
corrective action in response to any behavior that they deem
|
|
||||||
inappropriate, threatening, offensive, or harmful.
|
|
||||||
|
|
||||||
Community leaders have the right and responsibility to remove, edit,
|
|
||||||
or reject comments, commits, code, wiki edits, issues, and other
|
|
||||||
contributions that are not aligned to this Code of Conduct, and will
|
|
||||||
communicate reasons for moderation decisions when appropriate.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
This Code of Conduct applies within all community spaces, and also
|
|
||||||
applies when an individual is officially representing the community in
|
|
||||||
public spaces. Examples of representing our community include using an
|
|
||||||
official e-mail address, posting via an official social media account,
|
|
||||||
or acting as an appointed representative at an online or offline
|
|
||||||
event.
|
|
||||||
|
|
||||||
## Enforcement
|
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior
|
|
||||||
may be reported to the community leaders responsible for enforcement
|
|
||||||
at our Discord channel. All complaints
|
|
||||||
will be reviewed and investigated promptly and fairly.
|
|
||||||
|
|
||||||
All community leaders are obligated to respect the privacy and
|
|
||||||
security of the reporter of any incident.
|
|
||||||
|
|
||||||
## Enforcement Guidelines
|
|
||||||
|
|
||||||
Community leaders will follow these Community Impact Guidelines in
|
|
||||||
determining the consequences for any action they deem in violation of
|
|
||||||
this Code of Conduct:
|
|
||||||
|
|
||||||
### 1. Correction
|
|
||||||
|
|
||||||
**Community Impact**: Use of inappropriate language or other behavior
|
|
||||||
deemed unprofessional or unwelcome in the community.
|
|
||||||
|
|
||||||
**Consequence**: A private, written warning from community leaders,
|
|
||||||
providing clarity around the nature of the violation and an
|
|
||||||
explanation of why the behavior was inappropriate. A public apology
|
|
||||||
may be requested.
|
|
||||||
|
|
||||||
### 2. Warning
|
|
||||||
|
|
||||||
**Community Impact**: A violation through a single incident or series
|
|
||||||
of actions.
|
|
||||||
|
|
||||||
**Consequence**: A warning with consequences for continued
|
|
||||||
behavior. No interaction with the people involved, including
|
|
||||||
unsolicited interaction with those enforcing the Code of Conduct, for
|
|
||||||
a specified period of time. This includes avoiding interactions in
|
|
||||||
community spaces as well as external channels like social
|
|
||||||
media. Violating these terms may lead to a temporary or permanent ban.
|
|
||||||
|
|
||||||
### 3. Temporary Ban
|
|
||||||
|
|
||||||
**Community Impact**: A serious violation of community standards,
|
|
||||||
including sustained inappropriate behavior.
|
|
||||||
|
|
||||||
**Consequence**: A temporary ban from any sort of interaction or
|
|
||||||
public communication with the community for a specified period of
|
|
||||||
time. No public or private interaction with the people involved,
|
|
||||||
including unsolicited interaction with those enforcing the Code of
|
|
||||||
Conduct, is allowed during this period. Violating these terms may lead
|
|
||||||
to a permanent ban.
|
|
||||||
|
|
||||||
### 4. Permanent Ban
|
|
||||||
|
|
||||||
**Community Impact**: Demonstrating a pattern of violation of
|
|
||||||
community standards, including sustained inappropriate behavior,
|
|
||||||
harassment of an individual, or aggression toward or disparagement of
|
|
||||||
classes of individuals.
|
|
||||||
|
|
||||||
**Consequence**: A permanent ban from any sort of public interaction
|
|
||||||
within the community.
|
|
||||||
|
|
||||||
## Attribution
|
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor
|
|
||||||
Covenant][homepage], version 2.0, available at
|
|
||||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
|
||||||
|
|
||||||
Community Impact Guidelines were inspired by [Mozilla's code of
|
|
||||||
conduct enforcement ladder](https://github.com/mozilla/diversity).
|
|
||||||
|
|
||||||
[homepage]: https://www.contributor-covenant.org
|
|
||||||
|
|
||||||
For answers to common questions about this code of conduct, see the
|
|
||||||
FAQ at https://www.contributor-covenant.org/faq. Translations are
|
|
||||||
available at https://www.contributor-covenant.org/translations.
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# Builder image
|
# Builder image
|
||||||
FROM docker.io/golang:1.19.0-bullseye AS build
|
FROM --platform=$BUILDPLATFORM docker.io/golang:1.18.0-bullseye AS build
|
||||||
ARG VERSION=dev
|
ARG VERSION=dev
|
||||||
ENV GOPATH /go
|
ENV GOPATH /go
|
||||||
WORKDIR /go/src/headscale
|
WORKDIR /go/src/headscale
|
||||||
@@ -8,9 +8,8 @@ COPY go.mod go.sum /go/src/headscale/
|
|||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
ARG TARGETOS TARGETARCH
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go install -tags ts2019 -ldflags="-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$VERSION" -a ./cmd/headscale
|
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /go/bin/headscale -ldflags="-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$VERSION" -a ./cmd/headscale
|
||||||
RUN strip /go/bin/headscale
|
|
||||||
RUN test -e /go/bin/headscale
|
RUN test -e /go/bin/headscale
|
||||||
|
|
||||||
# Production image
|
# Production image
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Builder image
|
# Builder image
|
||||||
FROM docker.io/golang:1.19.0-alpine AS build
|
FROM --platform=$BUILDPLATFORM docker.io/golang:1.18.0-alpine AS build
|
||||||
ARG VERSION=dev
|
ARG VERSION=dev
|
||||||
ENV GOPATH /go
|
ENV GOPATH /go
|
||||||
WORKDIR /go/src/headscale
|
WORKDIR /go/src/headscale
|
||||||
@@ -10,8 +10,8 @@ RUN go mod download
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go install -ldflags="-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$VERSION" -a ./cmd/headscale
|
ARG TARGETOS TARGETARCH
|
||||||
RUN strip /go/bin/headscale
|
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /go/bin/headscale -ldflags="-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$VERSION" -a ./cmd/headscale
|
||||||
RUN test -e /go/bin/headscale
|
RUN test -e /go/bin/headscale
|
||||||
|
|
||||||
# Production image
|
# Production image
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Builder image
|
# Builder image
|
||||||
FROM docker.io/golang:1.19.0-bullseye AS build
|
FROM --platform=$BUILDPLATFORM docker.io/golang:1.18.0-bullseye AS build
|
||||||
ARG VERSION=dev
|
ARG VERSION=dev
|
||||||
ENV GOPATH /go
|
ENV GOPATH /go
|
||||||
WORKDIR /go/src/headscale
|
WORKDIR /go/src/headscale
|
||||||
@@ -9,11 +9,12 @@ RUN go mod download
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go install -tags ts2019 -ldflags="-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$VERSION" -a ./cmd/headscale
|
ARG TARGETOS TARGETARCH
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go install -ldflags="-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$VERSION" -a ./cmd/headscale
|
||||||
RUN test -e /go/bin/headscale
|
RUN test -e /go/bin/headscale
|
||||||
|
|
||||||
# Debug image
|
# Debug image
|
||||||
FROM docker.io/golang:1.19.0-bullseye
|
FROM gcr.io/distroless/base-debian11:debug
|
||||||
|
|
||||||
COPY --from=build /go/bin/headscale /bin/headscale
|
COPY --from=build /go/bin/headscale /bin/headscale
|
||||||
ENV TZ UTC
|
ENV TZ UTC
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ RUN apt-get update \
|
|||||||
|
|
||||||
RUN git clone https://github.com/tailscale/tailscale.git
|
RUN git clone https://github.com/tailscale/tailscale.git
|
||||||
|
|
||||||
WORKDIR /go/tailscale
|
WORKDIR tailscale
|
||||||
|
|
||||||
RUN git checkout main
|
|
||||||
|
|
||||||
RUN sh build_dist.sh tailscale.com/cmd/tailscale
|
RUN sh build_dist.sh tailscale.com/cmd/tailscale
|
||||||
RUN sh build_dist.sh tailscale.com/cmd/tailscaled
|
RUN sh build_dist.sh tailscale.com/cmd/tailscaled
|
||||||
|
|||||||
58
Makefile
58
Makefile
@@ -10,8 +10,6 @@ ifeq ($(filter $(GOOS), openbsd netbsd soloaris plan9), )
|
|||||||
else
|
else
|
||||||
endif
|
endif
|
||||||
|
|
||||||
TAGS = -tags ts2019
|
|
||||||
|
|
||||||
# GO_SOURCES = $(wildcard *.go)
|
# GO_SOURCES = $(wildcard *.go)
|
||||||
# PROTO_SOURCES = $(wildcard **/*.proto)
|
# PROTO_SOURCES = $(wildcard **/*.proto)
|
||||||
GO_SOURCES = $(call rwildcard,,*.go)
|
GO_SOURCES = $(call rwildcard,,*.go)
|
||||||
@@ -19,65 +17,21 @@ PROTO_SOURCES = $(call rwildcard,,*.proto)
|
|||||||
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
nix build
|
GOOS=$(GOOS) CGO_ENABLED=0 go build -trimpath $(pieflags) -mod=readonly -ldflags "-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$(version)" cmd/headscale/headscale.go
|
||||||
|
|
||||||
dev: lint test build
|
dev: lint test build
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@go test $(TAGS) -short -coverprofile=coverage.out ./...
|
@go test -coverprofile=coverage.out ./...
|
||||||
|
|
||||||
test_integration: test_integration_cli test_integration_derp test_integration_oidc test_integration_v2_general
|
test_integration:
|
||||||
|
go test -failfast -tags integration -timeout 30m -count=1 ./...
|
||||||
|
|
||||||
test_integration_cli:
|
test_integration_cli:
|
||||||
docker network rm $$(docker network ls --filter name=headscale --quiet) || true
|
go test -tags integration -v integration_cli_test.go integration_common_test.go
|
||||||
docker network create headscale-test || true
|
|
||||||
docker run -t --rm \
|
|
||||||
--network headscale-test \
|
|
||||||
-v ~/.cache/hs-integration-go:/go \
|
|
||||||
-v $$PWD:$$PWD -w $$PWD \
|
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock golang:1 \
|
|
||||||
go test $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationCLI ./...
|
|
||||||
|
|
||||||
test_integration_derp:
|
test_integration_derp:
|
||||||
docker network rm $$(docker network ls --filter name=headscale --quiet) || true
|
go test -tags integration -v integration_embedded_derp_test.go integration_common_test.go
|
||||||
docker network create headscale-test || true
|
|
||||||
docker run -t --rm \
|
|
||||||
--network headscale-test \
|
|
||||||
-v ~/.cache/hs-integration-go:/go \
|
|
||||||
-v $$PWD:$$PWD -w $$PWD \
|
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock golang:1 \
|
|
||||||
go test $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationDERP ./...
|
|
||||||
|
|
||||||
test_integration_oidc:
|
|
||||||
docker network rm $$(docker network ls --filter name=headscale --quiet) || true
|
|
||||||
docker network create headscale-test || true
|
|
||||||
docker run -t --rm \
|
|
||||||
--network headscale-test \
|
|
||||||
-v ~/.cache/hs-integration-go:/go \
|
|
||||||
-v $$PWD:$$PWD -w $$PWD \
|
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock golang:1 \
|
|
||||||
go test $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationOIDC ./...
|
|
||||||
|
|
||||||
test_integration_v2_general:
|
|
||||||
docker run \
|
|
||||||
-t --rm \
|
|
||||||
-v ~/.cache/hs-integration-go:/go \
|
|
||||||
--name headscale-test-suite \
|
|
||||||
-v $$PWD:$$PWD -w $$PWD/integration \
|
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
|
||||||
golang:1 \
|
|
||||||
go test $(TAGS) -failfast ./... -timeout 60m -parallel 6
|
|
||||||
|
|
||||||
|
|
||||||
test_integration_v2_auth_web_flow:
|
|
||||||
docker run \
|
|
||||||
-t --rm \
|
|
||||||
-v ~/.cache/hs-integration-go:/go \
|
|
||||||
--name headscale-test-suite \
|
|
||||||
-v $$PWD:$$PWD -w $$PWD/integration \
|
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
|
||||||
golang:1 \
|
|
||||||
go test ./... -timeout 60m -parallel 6 -run TestAuthWebFlow
|
|
||||||
|
|
||||||
coverprofile_func:
|
coverprofile_func:
|
||||||
go tool cover -func=coverage.out
|
go tool cover -func=coverage.out
|
||||||
|
|||||||
238
README.md
238
README.md
@@ -1,4 +1,4 @@
|
|||||||

|
# headscale
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -67,15 +67,15 @@ one of the maintainers.
|
|||||||
|
|
||||||
## Client OS support
|
## Client OS support
|
||||||
|
|
||||||
| OS | Supports headscale |
|
| OS | Supports headscale |
|
||||||
| ------- | --------------------------------------------------------- |
|
| ------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||||
| Linux | Yes |
|
| Linux | Yes |
|
||||||
| OpenBSD | Yes |
|
| OpenBSD | Yes |
|
||||||
| FreeBSD | Yes |
|
| FreeBSD | Yes |
|
||||||
| macOS | Yes (see `/apple` on your headscale for more information) |
|
| macOS | Yes (see `/apple` on your headscale for more information) |
|
||||||
| Windows | Yes [docs](./docs/windows-client.md) |
|
| Windows | Yes [docs](./docs/windows-client.md) |
|
||||||
| Android | Yes [docs](./docs/android-client.md) |
|
| Android | [You need to compile the client yourself](https://github.com/juanfont/headscale/issues/58#issuecomment-885255270) |
|
||||||
| iOS | Not yet |
|
| iOS | Not yet |
|
||||||
|
|
||||||
## Running headscale
|
## Running headscale
|
||||||
|
|
||||||
@@ -195,15 +195,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Jiang Zhu</b></sub>
|
<sub style="font-size:14px"><b>Jiang Zhu</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
|
||||||
<a href=https://github.com/tsujamin>
|
|
||||||
<img src=https://avatars.githubusercontent.com/u/2435619?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Benjamin Roberts/>
|
|
||||||
<br />
|
|
||||||
<sub style="font-size:14px"><b>Benjamin Roberts</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/reynico>
|
<a href=https://github.com/reynico>
|
||||||
<img src=https://avatars.githubusercontent.com/u/715768?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Nico/>
|
<img src=https://avatars.githubusercontent.com/u/715768?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Nico/>
|
||||||
@@ -211,6 +202,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Nico</b></sub>
|
<sub style="font-size:14px"><b>Nico</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/e-zk>
|
<a href=https://github.com/e-zk>
|
||||||
<img src=https://avatars.githubusercontent.com/u/58356365?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=e-zk/>
|
<img src=https://avatars.githubusercontent.com/u/58356365?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=e-zk/>
|
||||||
@@ -239,6 +232,13 @@ make build
|
|||||||
<sub style="font-size:14px"><b>unreality</b></sub>
|
<sub style="font-size:14px"><b>unreality</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
|
<a href=https://github.com/mpldr>
|
||||||
|
<img src=https://avatars.githubusercontent.com/u/33086936?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Moritz Poldrack/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Moritz Poldrack</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/ohdearaugustin>
|
<a href=https://github.com/ohdearaugustin>
|
||||||
<img src=https://avatars.githubusercontent.com/u/14001491?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ohdearaugustin/>
|
<img src=https://avatars.githubusercontent.com/u/14001491?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ohdearaugustin/>
|
||||||
@@ -248,27 +248,6 @@ make build
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
|
||||||
<a href=https://github.com/mpldr>
|
|
||||||
<img src=https://avatars.githubusercontent.com/u/33086936?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Moritz Poldrack/>
|
|
||||||
<br />
|
|
||||||
<sub style="font-size:14px"><b>Moritz Poldrack</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
|
||||||
<a href=https://github.com/GrigoriyMikhalkin>
|
|
||||||
<img src=https://avatars.githubusercontent.com/u/3637857?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=GrigoriyMikhalkin/>
|
|
||||||
<br />
|
|
||||||
<sub style="font-size:14px"><b>GrigoriyMikhalkin</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
|
||||||
<a href=https://github.com/mike-lloyd03>
|
|
||||||
<img src=https://avatars.githubusercontent.com/u/49411532?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Mike Lloyd/>
|
|
||||||
<br />
|
|
||||||
<sub style="font-size:14px"><b>Mike Lloyd</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/Niek>
|
<a href=https://github.com/Niek>
|
||||||
<img src=https://avatars.githubusercontent.com/u/213140?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Niek van der Maas/>
|
<img src=https://avatars.githubusercontent.com/u/213140?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Niek van der Maas/>
|
||||||
@@ -283,22 +262,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Eugen Biegler</b></sub>
|
<sub style="font-size:14px"><b>Eugen Biegler</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
|
||||||
<a href=https://github.com/617a7a>
|
|
||||||
<img src=https://avatars.githubusercontent.com/u/67651251?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Azz/>
|
|
||||||
<br />
|
|
||||||
<sub style="font-size:14px"><b>Azz</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
|
||||||
<a href=https://github.com/iSchluff>
|
|
||||||
<img src=https://avatars.githubusercontent.com/u/1429641?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Anton Schubert/>
|
|
||||||
<br />
|
|
||||||
<sub style="font-size:14px"><b>Anton Schubert</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/qbit>
|
<a href=https://github.com/qbit>
|
||||||
<img src=https://avatars.githubusercontent.com/u/68368?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Aaron Bieber/>
|
<img src=https://avatars.githubusercontent.com/u/68368?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Aaron Bieber/>
|
||||||
@@ -307,17 +270,10 @@ make build
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/kazauwa>
|
<a href=https://github.com/iSchluff>
|
||||||
<img src=https://avatars.githubusercontent.com/u/12330159?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Igor Perepilitsyn/>
|
<img src=https://avatars.githubusercontent.com/u/1429641?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Anton Schubert/>
|
||||||
<br />
|
<br />
|
||||||
<sub style="font-size:14px"><b>Igor Perepilitsyn</b></sub>
|
<sub style="font-size:14px"><b>Anton Schubert</b></sub>
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
|
||||||
<a href=https://github.com/Aluxima>
|
|
||||||
<img src=https://avatars.githubusercontent.com/u/16262531?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Laurent Marchaud/>
|
|
||||||
<br />
|
|
||||||
<sub style="font-size:14px"><b>Laurent Marchaud</b></sub>
|
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
@@ -328,14 +284,21 @@ make build
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/hdhoang>
|
<a href=https://github.com/GrigoriyMikhalkin>
|
||||||
<img src=https://avatars.githubusercontent.com/u/12537?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=hdhoang/>
|
<img src=https://avatars.githubusercontent.com/u/3637857?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=GrigoriyMikhalkin/>
|
||||||
<br />
|
<br />
|
||||||
<sub style="font-size:14px"><b>hdhoang</b></sub>
|
<sub style="font-size:14px"><b>GrigoriyMikhalkin</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
|
<a href=https://github.com/hdhoang>
|
||||||
|
<img src=https://avatars.githubusercontent.com/u/12537?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Hoàng Đức Hiếu/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Hoàng Đức Hiếu</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/bravechamp>
|
<a href=https://github.com/bravechamp>
|
||||||
<img src=https://avatars.githubusercontent.com/u/48980452?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=bravechamp/>
|
<img src=https://avatars.githubusercontent.com/u/48980452?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=bravechamp/>
|
||||||
@@ -350,13 +313,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Deon Thomas</b></sub>
|
<sub style="font-size:14px"><b>Deon Thomas</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
|
||||||
<a href=https://github.com/madjam002>
|
|
||||||
<img src=https://avatars.githubusercontent.com/u/679137?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jamie Greeff/>
|
|
||||||
<br />
|
|
||||||
<sub style="font-size:14px"><b>Jamie Greeff</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/ChibangLW>
|
<a href=https://github.com/ChibangLW>
|
||||||
<img src=https://avatars.githubusercontent.com/u/22293464?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ChibangLW/>
|
<img src=https://avatars.githubusercontent.com/u/22293464?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ChibangLW/>
|
||||||
@@ -401,13 +357,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Stefan Majer</b></sub>
|
<sub style="font-size:14px"><b>Stefan Majer</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
|
||||||
<a href=https://github.com/kevin1sMe>
|
|
||||||
<img src=https://avatars.githubusercontent.com/u/6886076?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=kevinlin/>
|
|
||||||
<br />
|
|
||||||
<sub style="font-size:14px"><b>kevinlin</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/artemklevtsov>
|
<a href=https://github.com/artemklevtsov>
|
||||||
<img src=https://avatars.githubusercontent.com/u/603798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Artem Klevtsov/>
|
<img src=https://avatars.githubusercontent.com/u/603798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Artem Klevtsov/>
|
||||||
@@ -422,15 +371,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Casey Marshall</b></sub>
|
<sub style="font-size:14px"><b>Casey Marshall</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
|
||||||
<a href=https://github.com/CNLHC>
|
|
||||||
<img src=https://avatars.githubusercontent.com/u/21005146?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=LiuHanCheng/>
|
|
||||||
<br />
|
|
||||||
<sub style="font-size:14px"><b>LiuHanCheng</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/pvinis>
|
<a href=https://github.com/pvinis>
|
||||||
<img src=https://avatars.githubusercontent.com/u/100233?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Pavlos Vinieratos/>
|
<img src=https://avatars.githubusercontent.com/u/100233?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Pavlos Vinieratos/>
|
||||||
@@ -438,6 +378,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Pavlos Vinieratos</b></sub>
|
<sub style="font-size:14px"><b>Pavlos Vinieratos</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/SilverBut>
|
<a href=https://github.com/SilverBut>
|
||||||
<img src=https://avatars.githubusercontent.com/u/6560655?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Silver Bullet/>
|
<img src=https://avatars.githubusercontent.com/u/6560655?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Silver Bullet/>
|
||||||
@@ -445,13 +387,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Silver Bullet</b></sub>
|
<sub style="font-size:14px"><b>Silver Bullet</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
|
||||||
<a href=https://github.com/ratsclub>
|
|
||||||
<img src=https://avatars.githubusercontent.com/u/25647735?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Victor Freire/>
|
|
||||||
<br />
|
|
||||||
<sub style="font-size:14px"><b>Victor Freire</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/lachy2849>
|
<a href=https://github.com/lachy2849>
|
||||||
<img src=https://avatars.githubusercontent.com/u/98844035?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=lachy2849/>
|
<img src=https://avatars.githubusercontent.com/u/98844035?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=lachy2849/>
|
||||||
@@ -466,8 +401,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>thomas</b></sub>
|
<sub style="font-size:14px"><b>thomas</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/aberoham>
|
<a href=https://github.com/aberoham>
|
||||||
<img src=https://avatars.githubusercontent.com/u/586805?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Abraham Ingersoll/>
|
<img src=https://avatars.githubusercontent.com/u/586805?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Abraham Ingersoll/>
|
||||||
@@ -475,13 +408,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Abraham Ingersoll</b></sub>
|
<sub style="font-size:14px"><b>Abraham Ingersoll</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
|
||||||
<a href=https://github.com/puzpuzpuz>
|
|
||||||
<img src=https://avatars.githubusercontent.com/u/37772591?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Andrei Pechkurov/>
|
|
||||||
<br />
|
|
||||||
<sub style="font-size:14px"><b>Andrei Pechkurov</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/apognu>
|
<a href=https://github.com/apognu>
|
||||||
<img src=https://avatars.githubusercontent.com/u/3017182?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Antoine POPINEAU/>
|
<img src=https://avatars.githubusercontent.com/u/3017182?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Antoine POPINEAU/>
|
||||||
@@ -496,6 +422,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Aofei Sheng</b></sub>
|
<sub style="font-size:14px"><b>Aofei Sheng</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/awoimbee>
|
<a href=https://github.com/awoimbee>
|
||||||
<img src=https://avatars.githubusercontent.com/u/22431493?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Arthur Woimbée/>
|
<img src=https://avatars.githubusercontent.com/u/22431493?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Arthur Woimbée/>
|
||||||
@@ -510,8 +438,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Bryan Stenson</b></sub>
|
<sub style="font-size:14px"><b>Bryan Stenson</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/yangchuansheng>
|
<a href=https://github.com/yangchuansheng>
|
||||||
<img src=https://avatars.githubusercontent.com/u/15308462?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt= Carson Yang/>
|
<img src=https://avatars.githubusercontent.com/u/15308462?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt= Carson Yang/>
|
||||||
@@ -540,6 +466,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Felix Yan</b></sub>
|
<sub style="font-size:14px"><b>Felix Yan</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/JJGadgets>
|
<a href=https://github.com/JJGadgets>
|
||||||
<img src=https://avatars.githubusercontent.com/u/5709019?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=JJGadgets/>
|
<img src=https://avatars.githubusercontent.com/u/5709019?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=JJGadgets/>
|
||||||
@@ -547,6 +475,13 @@ make build
|
|||||||
<sub style="font-size:14px"><b>JJGadgets</b></sub>
|
<sub style="font-size:14px"><b>JJGadgets</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
|
<a href=https://github.com/madjam002>
|
||||||
|
<img src=https://avatars.githubusercontent.com/u/679137?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jamie Greeff/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Jamie Greeff</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/jimt>
|
<a href=https://github.com/jimt>
|
||||||
<img src=https://avatars.githubusercontent.com/u/180326?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jim Tittsler/>
|
<img src=https://avatars.githubusercontent.com/u/180326?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jim Tittsler/>
|
||||||
@@ -554,15 +489,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Jim Tittsler</b></sub>
|
<sub style="font-size:14px"><b>Jim Tittsler</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
|
||||||
<a href=https://github.com/ShadowJonathan>
|
|
||||||
<img src=https://avatars.githubusercontent.com/u/22740616?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jonathan de Jong/>
|
|
||||||
<br />
|
|
||||||
<sub style="font-size:14px"><b>Jonathan de Jong</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/piec>
|
<a href=https://github.com/piec>
|
||||||
<img src=https://avatars.githubusercontent.com/u/781471?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Pierre Carru/>
|
<img src=https://avatars.githubusercontent.com/u/781471?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Pierre Carru/>
|
||||||
@@ -570,20 +496,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Pierre Carru</b></sub>
|
<sub style="font-size:14px"><b>Pierre Carru</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
|
||||||
<a href=https://github.com/Donran>
|
|
||||||
<img src=https://avatars.githubusercontent.com/u/4838348?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Pontus N/>
|
|
||||||
<br />
|
|
||||||
<sub style="font-size:14px"><b>Pontus N</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
|
||||||
<a href=https://github.com/nnsee>
|
|
||||||
<img src=https://avatars.githubusercontent.com/u/36747857?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Rasmus Moorats/>
|
|
||||||
<br />
|
|
||||||
<sub style="font-size:14px"><b>Rasmus Moorats</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/rcursaru>
|
<a href=https://github.com/rcursaru>
|
||||||
<img src=https://avatars.githubusercontent.com/u/16259641?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=rcursaru/>
|
<img src=https://avatars.githubusercontent.com/u/16259641?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=rcursaru/>
|
||||||
@@ -593,9 +505,9 @@ make build
|
|||||||
</td>
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/renovate-bot>
|
<a href=https://github.com/renovate-bot>
|
||||||
<img src=https://avatars.githubusercontent.com/u/25180681?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Mend Renovate/>
|
<img src=https://avatars.githubusercontent.com/u/25180681?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=WhiteSource Renovate/>
|
||||||
<br />
|
<br />
|
||||||
<sub style="font-size:14px"><b>Mend Renovate</b></sub>
|
<sub style="font-size:14px"><b>WhiteSource Renovate</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -614,20 +526,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Shaanan Cohney</b></sub>
|
<sub style="font-size:14px"><b>Shaanan Cohney</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
|
||||||
<a href=https://github.com/stefanvanburen>
|
|
||||||
<img src=https://avatars.githubusercontent.com/u/622527?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Stefan VanBuren/>
|
|
||||||
<br />
|
|
||||||
<sub style="font-size:14px"><b>Stefan VanBuren</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
|
||||||
<a href=https://github.com/sophware>
|
|
||||||
<img src=https://avatars.githubusercontent.com/u/41669?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=sophware/>
|
|
||||||
<br />
|
|
||||||
<sub style="font-size:14px"><b>sophware</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/m-tanner-dev0>
|
<a href=https://github.com/m-tanner-dev0>
|
||||||
<img src=https://avatars.githubusercontent.com/u/97977342?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tanner/>
|
<img src=https://avatars.githubusercontent.com/u/97977342?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tanner/>
|
||||||
@@ -642,8 +540,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Teteros</b></sub>
|
<sub style="font-size:14px"><b>Teteros</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/gitter-badger>
|
<a href=https://github.com/gitter-badger>
|
||||||
<img src=https://avatars.githubusercontent.com/u/8518239?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=The Gitter Badger/>
|
<img src=https://avatars.githubusercontent.com/u/8518239?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=The Gitter Badger/>
|
||||||
@@ -658,13 +554,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Tianon Gravi</b></sub>
|
<sub style="font-size:14px"><b>Tianon Gravi</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
</tr>
|
||||||
<a href=https://github.com/thetillhoff>
|
<tr>
|
||||||
<img src=https://avatars.githubusercontent.com/u/25052289?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Till Hoffmann/>
|
|
||||||
<br />
|
|
||||||
<sub style="font-size:14px"><b>Till Hoffmann</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/woudsma>
|
<a href=https://github.com/woudsma>
|
||||||
<img src=https://avatars.githubusercontent.com/u/6162978?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tjerk Woudsma/>
|
<img src=https://avatars.githubusercontent.com/u/6162978?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tjerk Woudsma/>
|
||||||
@@ -679,15 +570,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Yang Bin</b></sub>
|
<sub style="font-size:14px"><b>Yang Bin</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
|
||||||
<a href=https://github.com/gozssky>
|
|
||||||
<img src=https://avatars.githubusercontent.com/u/17199941?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Yujie Xia/>
|
|
||||||
<br />
|
|
||||||
<sub style="font-size:14px"><b>Yujie Xia</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/zekker6>
|
<a href=https://github.com/zekker6>
|
||||||
<img src=https://avatars.githubusercontent.com/u/1367798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Zakhar Bessarab/>
|
<img src=https://avatars.githubusercontent.com/u/1367798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Zakhar Bessarab/>
|
||||||
@@ -695,13 +577,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Zakhar Bessarab</b></sub>
|
<sub style="font-size:14px"><b>Zakhar Bessarab</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
|
||||||
<a href=https://github.com/zhzy0077>
|
|
||||||
<img src=https://avatars.githubusercontent.com/u/8717471?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Zhiyuan Zheng/>
|
|
||||||
<br />
|
|
||||||
<sub style="font-size:14px"><b>Zhiyuan Zheng</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/Bpazy>
|
<a href=https://github.com/Bpazy>
|
||||||
<img src=https://avatars.githubusercontent.com/u/9838749?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Ziyuan Han/>
|
<img src=https://avatars.githubusercontent.com/u/9838749?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Ziyuan Han/>
|
||||||
@@ -723,6 +598,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>henning mueller</b></sub>
|
<sub style="font-size:14px"><b>henning mueller</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/ignoramous>
|
<a href=https://github.com/ignoramous>
|
||||||
<img src=https://avatars.githubusercontent.com/u/852289?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ignoramous/>
|
<img src=https://avatars.githubusercontent.com/u/852289?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ignoramous/>
|
||||||
@@ -730,13 +607,11 @@ make build
|
|||||||
<sub style="font-size:14px"><b>ignoramous</b></sub>
|
<sub style="font-size:14px"><b>ignoramous</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/lion24>
|
<a href=https://github.com/lion24>
|
||||||
<img src=https://avatars.githubusercontent.com/u/1382102?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=sharkonet/>
|
<img src=https://avatars.githubusercontent.com/u/1382102?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=lion24/>
|
||||||
<br />
|
<br />
|
||||||
<sub style="font-size:14px"><b>sharkonet</b></sub>
|
<sub style="font-size:14px"><b>lion24</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
@@ -746,13 +621,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>pernila</b></sub>
|
<sub style="font-size:14px"><b>pernila</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
|
||||||
<a href=https://github.com/phpmalik>
|
|
||||||
<img src=https://avatars.githubusercontent.com/u/26834645?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=phpmalik/>
|
|
||||||
<br />
|
|
||||||
<sub style="font-size:14px"><b>phpmalik</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/Wakeful-Cloud>
|
<a href=https://github.com/Wakeful-Cloud>
|
||||||
<img src=https://avatars.githubusercontent.com/u/38930607?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Wakeful-Cloud/>
|
<img src=https://avatars.githubusercontent.com/u/38930607?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Wakeful-Cloud/>
|
||||||
|
|||||||
31
acls.go
31
acls.go
@@ -5,7 +5,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/netip"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -14,6 +13,7 @@ import (
|
|||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/tailscale/hujson"
|
"github.com/tailscale/hujson"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
"inet.af/netaddr"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -162,12 +162,7 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
|
|||||||
|
|
||||||
destPorts := []tailcfg.NetPortRange{}
|
destPorts := []tailcfg.NetPortRange{}
|
||||||
for innerIndex, dest := range acl.Destinations {
|
for innerIndex, dest := range acl.Destinations {
|
||||||
dests, err := h.generateACLPolicyDest(
|
dests, err := h.generateACLPolicyDest(machines, *h.aclPolicy, dest, needsWildcard)
|
||||||
machines,
|
|
||||||
*h.aclPolicy,
|
|
||||||
dest,
|
|
||||||
needsWildcard,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
Msgf("Error parsing ACL %d, Destination %d", index, innerIndex)
|
Msgf("Error parsing ACL %d, Destination %d", index, innerIndex)
|
||||||
@@ -260,12 +255,7 @@ func (h *Headscale) generateACLPolicyDest(
|
|||||||
func parseProtocol(protocol string) ([]int, bool, error) {
|
func parseProtocol(protocol string) ([]int, bool, error) {
|
||||||
switch protocol {
|
switch protocol {
|
||||||
case "":
|
case "":
|
||||||
return []int{
|
return []int{protocolICMP, protocolIPv6ICMP, protocolTCP, protocolUDP}, false, nil
|
||||||
protocolICMP,
|
|
||||||
protocolIPv6ICMP,
|
|
||||||
protocolTCP,
|
|
||||||
protocolUDP,
|
|
||||||
}, false, nil
|
|
||||||
case "igmp":
|
case "igmp":
|
||||||
return []int{protocolIGMP}, true, nil
|
return []int{protocolIGMP}, true, nil
|
||||||
case "ipv4", "ip-in-ip":
|
case "ipv4", "ip-in-ip":
|
||||||
@@ -294,9 +284,7 @@ func parseProtocol(protocol string) ([]int, bool, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
needsWildcard := protocolNumber != protocolTCP &&
|
needsWildcard := protocolNumber != protocolTCP && protocolNumber != protocolUDP && protocolNumber != protocolSCTP
|
||||||
protocolNumber != protocolUDP &&
|
|
||||||
protocolNumber != protocolSCTP
|
|
||||||
|
|
||||||
return []int{protocolNumber}, needsWildcard, nil
|
return []int{protocolNumber}, needsWildcard, nil
|
||||||
}
|
}
|
||||||
@@ -379,7 +367,7 @@ func expandAlias(
|
|||||||
|
|
||||||
// if alias is a namespace
|
// if alias is a namespace
|
||||||
nodes := filterMachinesByNamespace(machines, alias)
|
nodes := filterMachinesByNamespace(machines, alias)
|
||||||
nodes = excludeCorrectlyTaggedNodes(aclPolicy, nodes, alias, stripEmailDomain)
|
nodes = excludeCorrectlyTaggedNodes(aclPolicy, nodes, alias)
|
||||||
|
|
||||||
for _, n := range nodes {
|
for _, n := range nodes {
|
||||||
ips = append(ips, n.IPAddresses.ToStringSlice()...)
|
ips = append(ips, n.IPAddresses.ToStringSlice()...)
|
||||||
@@ -394,13 +382,13 @@ func expandAlias(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if alias is an IP
|
// if alias is an IP
|
||||||
ip, err := netip.ParseAddr(alias)
|
ip, err := netaddr.ParseIP(alias)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return []string{ip.String()}, nil
|
return []string{ip.String()}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// if alias is an CIDR
|
// if alias is an CIDR
|
||||||
cidr, err := netip.ParsePrefix(alias)
|
cidr, err := netaddr.ParseIPPrefix(alias)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return []string{cidr.String()}, nil
|
return []string{cidr.String()}, nil
|
||||||
}
|
}
|
||||||
@@ -417,13 +405,10 @@ func excludeCorrectlyTaggedNodes(
|
|||||||
aclPolicy ACLPolicy,
|
aclPolicy ACLPolicy,
|
||||||
nodes []Machine,
|
nodes []Machine,
|
||||||
namespace string,
|
namespace string,
|
||||||
stripEmailDomain bool,
|
|
||||||
) []Machine {
|
) []Machine {
|
||||||
out := []Machine{}
|
out := []Machine{}
|
||||||
tags := []string{}
|
tags := []string{}
|
||||||
for tag := range aclPolicy.TagOwners {
|
for tag, ns := range aclPolicy.TagOwners {
|
||||||
owners, _ := expandTagOwners(aclPolicy, namespace, stripEmailDomain)
|
|
||||||
ns := append(owners, namespace)
|
|
||||||
if contains(ns, namespace) {
|
if contains(ns, namespace) {
|
||||||
tags = append(tags, tag)
|
tags = append(tags, tag)
|
||||||
}
|
}
|
||||||
|
|||||||
219
acls_test.go
219
acls_test.go
@@ -2,11 +2,11 @@ package headscale
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"net/netip"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gopkg.in/check.v1"
|
"gopkg.in/check.v1"
|
||||||
|
"inet.af/netaddr"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -62,11 +62,7 @@ func (s *Suite) TestBasicRule(c *check.C) {
|
|||||||
func (s *Suite) TestInvalidAction(c *check.C) {
|
func (s *Suite) TestInvalidAction(c *check.C) {
|
||||||
app.aclPolicy = &ACLPolicy{
|
app.aclPolicy = &ACLPolicy{
|
||||||
ACLs: []ACL{
|
ACLs: []ACL{
|
||||||
{
|
{Action: "invalidAction", Sources: []string{"*"}, Destinations: []string{"*:*"}},
|
||||||
Action: "invalidAction",
|
|
||||||
Sources: []string{"*"},
|
|
||||||
Destinations: []string{"*:*"},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := app.UpdateACLRules()
|
err := app.UpdateACLRules()
|
||||||
@@ -81,11 +77,7 @@ func (s *Suite) TestInvalidGroupInGroup(c *check.C) {
|
|||||||
"group:error": []string{"foo", "group:test"},
|
"group:error": []string{"foo", "group:test"},
|
||||||
},
|
},
|
||||||
ACLs: []ACL{
|
ACLs: []ACL{
|
||||||
{
|
{Action: "accept", Sources: []string{"group:error"}, Destinations: []string{"*:*"}},
|
||||||
Action: "accept",
|
|
||||||
Sources: []string{"group:error"},
|
|
||||||
Destinations: []string{"*:*"},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := app.UpdateACLRules()
|
err := app.UpdateACLRules()
|
||||||
@@ -96,11 +88,7 @@ func (s *Suite) TestInvalidTagOwners(c *check.C) {
|
|||||||
// this ACL is wrong because no tagOwners own the requested tag for the server
|
// this ACL is wrong because no tagOwners own the requested tag for the server
|
||||||
app.aclPolicy = &ACLPolicy{
|
app.aclPolicy = &ACLPolicy{
|
||||||
ACLs: []ACL{
|
ACLs: []ACL{
|
||||||
{
|
{Action: "accept", Sources: []string{"tag:foo"}, Destinations: []string{"*:*"}},
|
||||||
Action: "accept",
|
|
||||||
Sources: []string{"tag:foo"},
|
|
||||||
Destinations: []string{"*:*"},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := app.UpdateACLRules()
|
err := app.UpdateACLRules()
|
||||||
@@ -114,7 +102,7 @@ func (s *Suite) TestValidExpandTagOwnersInSources(c *check.C) {
|
|||||||
namespace, err := app.CreateNamespace("user1")
|
namespace, err := app.CreateNamespace("user1")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
|
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
_, err = app.GetMachine("user1", "testmachine")
|
_, err = app.GetMachine("user1", "testmachine")
|
||||||
@@ -131,7 +119,7 @@ func (s *Suite) TestValidExpandTagOwnersInSources(c *check.C) {
|
|||||||
NodeKey: "bar",
|
NodeKey: "bar",
|
||||||
DiscoKey: "faa",
|
DiscoKey: "faa",
|
||||||
Hostname: "testmachine",
|
Hostname: "testmachine",
|
||||||
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.1")},
|
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
|
||||||
NamespaceID: namespace.ID,
|
NamespaceID: namespace.ID,
|
||||||
RegisterMethod: RegisterMethodAuthKey,
|
RegisterMethod: RegisterMethodAuthKey,
|
||||||
AuthKeyID: uint(pak.ID),
|
AuthKeyID: uint(pak.ID),
|
||||||
@@ -143,11 +131,7 @@ func (s *Suite) TestValidExpandTagOwnersInSources(c *check.C) {
|
|||||||
Groups: Groups{"group:test": []string{"user1", "user2"}},
|
Groups: Groups{"group:test": []string{"user1", "user2"}},
|
||||||
TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}},
|
TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}},
|
||||||
ACLs: []ACL{
|
ACLs: []ACL{
|
||||||
{
|
{Action: "accept", Sources: []string{"tag:test"}, Destinations: []string{"*:*"}},
|
||||||
Action: "accept",
|
|
||||||
Sources: []string{"tag:test"},
|
|
||||||
Destinations: []string{"*:*"},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err = app.UpdateACLRules()
|
err = app.UpdateACLRules()
|
||||||
@@ -164,7 +148,7 @@ func (s *Suite) TestValidExpandTagOwnersInDestinations(c *check.C) {
|
|||||||
namespace, err := app.CreateNamespace("user1")
|
namespace, err := app.CreateNamespace("user1")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
|
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
_, err = app.GetMachine("user1", "testmachine")
|
_, err = app.GetMachine("user1", "testmachine")
|
||||||
@@ -181,7 +165,7 @@ func (s *Suite) TestValidExpandTagOwnersInDestinations(c *check.C) {
|
|||||||
NodeKey: "bar",
|
NodeKey: "bar",
|
||||||
DiscoKey: "faa",
|
DiscoKey: "faa",
|
||||||
Hostname: "testmachine",
|
Hostname: "testmachine",
|
||||||
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.1")},
|
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
|
||||||
NamespaceID: namespace.ID,
|
NamespaceID: namespace.ID,
|
||||||
RegisterMethod: RegisterMethodAuthKey,
|
RegisterMethod: RegisterMethodAuthKey,
|
||||||
AuthKeyID: uint(pak.ID),
|
AuthKeyID: uint(pak.ID),
|
||||||
@@ -193,11 +177,7 @@ func (s *Suite) TestValidExpandTagOwnersInDestinations(c *check.C) {
|
|||||||
Groups: Groups{"group:test": []string{"user1", "user2"}},
|
Groups: Groups{"group:test": []string{"user1", "user2"}},
|
||||||
TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}},
|
TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}},
|
||||||
ACLs: []ACL{
|
ACLs: []ACL{
|
||||||
{
|
{Action: "accept", Sources: []string{"*"}, Destinations: []string{"tag:test:*"}},
|
||||||
Action: "accept",
|
|
||||||
Sources: []string{"*"},
|
|
||||||
Destinations: []string{"tag:test:*"},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err = app.UpdateACLRules()
|
err = app.UpdateACLRules()
|
||||||
@@ -214,7 +194,7 @@ func (s *Suite) TestInvalidTagValidNamespace(c *check.C) {
|
|||||||
namespace, err := app.CreateNamespace("user1")
|
namespace, err := app.CreateNamespace("user1")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
|
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
_, err = app.GetMachine("user1", "testmachine")
|
_, err = app.GetMachine("user1", "testmachine")
|
||||||
@@ -231,7 +211,7 @@ func (s *Suite) TestInvalidTagValidNamespace(c *check.C) {
|
|||||||
NodeKey: "bar",
|
NodeKey: "bar",
|
||||||
DiscoKey: "faa",
|
DiscoKey: "faa",
|
||||||
Hostname: "testmachine",
|
Hostname: "testmachine",
|
||||||
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.1")},
|
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
|
||||||
NamespaceID: namespace.ID,
|
NamespaceID: namespace.ID,
|
||||||
RegisterMethod: RegisterMethodAuthKey,
|
RegisterMethod: RegisterMethodAuthKey,
|
||||||
AuthKeyID: uint(pak.ID),
|
AuthKeyID: uint(pak.ID),
|
||||||
@@ -242,11 +222,7 @@ func (s *Suite) TestInvalidTagValidNamespace(c *check.C) {
|
|||||||
app.aclPolicy = &ACLPolicy{
|
app.aclPolicy = &ACLPolicy{
|
||||||
TagOwners: TagOwners{"tag:test": []string{"user1"}},
|
TagOwners: TagOwners{"tag:test": []string{"user1"}},
|
||||||
ACLs: []ACL{
|
ACLs: []ACL{
|
||||||
{
|
{Action: "accept", Sources: []string{"user1"}, Destinations: []string{"*:*"}},
|
||||||
Action: "accept",
|
|
||||||
Sources: []string{"user1"},
|
|
||||||
Destinations: []string{"*:*"},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err = app.UpdateACLRules()
|
err = app.UpdateACLRules()
|
||||||
@@ -263,7 +239,7 @@ func (s *Suite) TestValidTagInvalidNamespace(c *check.C) {
|
|||||||
namespace, err := app.CreateNamespace("user1")
|
namespace, err := app.CreateNamespace("user1")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
|
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
_, err = app.GetMachine("user1", "webserver")
|
_, err = app.GetMachine("user1", "webserver")
|
||||||
@@ -280,7 +256,7 @@ func (s *Suite) TestValidTagInvalidNamespace(c *check.C) {
|
|||||||
NodeKey: "bar",
|
NodeKey: "bar",
|
||||||
DiscoKey: "faa",
|
DiscoKey: "faa",
|
||||||
Hostname: "webserver",
|
Hostname: "webserver",
|
||||||
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.1")},
|
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
|
||||||
NamespaceID: namespace.ID,
|
NamespaceID: namespace.ID,
|
||||||
RegisterMethod: RegisterMethodAuthKey,
|
RegisterMethod: RegisterMethodAuthKey,
|
||||||
AuthKeyID: uint(pak.ID),
|
AuthKeyID: uint(pak.ID),
|
||||||
@@ -299,7 +275,7 @@ func (s *Suite) TestValidTagInvalidNamespace(c *check.C) {
|
|||||||
NodeKey: "bar2",
|
NodeKey: "bar2",
|
||||||
DiscoKey: "faab",
|
DiscoKey: "faab",
|
||||||
Hostname: "user",
|
Hostname: "user",
|
||||||
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.2")},
|
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
|
||||||
NamespaceID: namespace.ID,
|
NamespaceID: namespace.ID,
|
||||||
RegisterMethod: RegisterMethodAuthKey,
|
RegisterMethod: RegisterMethodAuthKey,
|
||||||
AuthKeyID: uint(pak.ID),
|
AuthKeyID: uint(pak.ID),
|
||||||
@@ -395,7 +371,7 @@ func (s *Suite) TestPortNamespace(c *check.C) {
|
|||||||
namespace, err := app.CreateNamespace("testnamespace")
|
namespace, err := app.CreateNamespace("testnamespace")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
|
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
_, err = app.GetMachine("testnamespace", "testmachine")
|
_, err = app.GetMachine("testnamespace", "testmachine")
|
||||||
@@ -437,7 +413,7 @@ func (s *Suite) TestPortGroup(c *check.C) {
|
|||||||
namespace, err := app.CreateNamespace("testnamespace")
|
namespace, err := app.CreateNamespace("testnamespace")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
|
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
_, err = app.GetMachine("testnamespace", "testmachine")
|
_, err = app.GetMachine("testnamespace", "testmachine")
|
||||||
@@ -825,6 +801,7 @@ func Test_listMachinesInNamespace(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nolint
|
||||||
func Test_expandAlias(t *testing.T) {
|
func Test_expandAlias(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
machines []Machine
|
machines []Machine
|
||||||
@@ -843,10 +820,10 @@ func Test_expandAlias(t *testing.T) {
|
|||||||
args: args{
|
args: args{
|
||||||
alias: "*",
|
alias: "*",
|
||||||
machines: []Machine{
|
machines: []Machine{
|
||||||
{IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.1")}},
|
{IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")}},
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.78.84.227"),
|
netaddr.MustParseIP("100.78.84.227"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -863,25 +840,25 @@ func Test_expandAlias(t *testing.T) {
|
|||||||
machines: []Machine{
|
machines: []Machine{
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.1"),
|
netaddr.MustParseIP("100.64.0.1"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "joe"},
|
Namespace: Namespace{Name: "joe"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.2"),
|
netaddr.MustParseIP("100.64.0.2"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "joe"},
|
Namespace: Namespace{Name: "joe"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.3"),
|
netaddr.MustParseIP("100.64.0.3"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "marc"},
|
Namespace: Namespace{Name: "marc"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.4"),
|
netaddr.MustParseIP("100.64.0.4"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "mickael"},
|
Namespace: Namespace{Name: "mickael"},
|
||||||
},
|
},
|
||||||
@@ -901,25 +878,25 @@ func Test_expandAlias(t *testing.T) {
|
|||||||
machines: []Machine{
|
machines: []Machine{
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.1"),
|
netaddr.MustParseIP("100.64.0.1"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "joe"},
|
Namespace: Namespace{Name: "joe"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.2"),
|
netaddr.MustParseIP("100.64.0.2"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "joe"},
|
Namespace: Namespace{Name: "joe"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.3"),
|
netaddr.MustParseIP("100.64.0.3"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "marc"},
|
Namespace: Namespace{Name: "marc"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.4"),
|
netaddr.MustParseIP("100.64.0.4"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "mickael"},
|
Namespace: Namespace{Name: "mickael"},
|
||||||
},
|
},
|
||||||
@@ -950,7 +927,7 @@ func Test_expandAlias(t *testing.T) {
|
|||||||
machines: []Machine{},
|
machines: []Machine{},
|
||||||
aclPolicy: ACLPolicy{
|
aclPolicy: ACLPolicy{
|
||||||
Hosts: Hosts{
|
Hosts: Hosts{
|
||||||
"homeNetwork": netip.MustParsePrefix("192.168.1.0/24"),
|
"homeNetwork": netaddr.MustParseIPPrefix("192.168.1.0/24"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
stripEmailDomain: true,
|
stripEmailDomain: true,
|
||||||
@@ -987,7 +964,7 @@ func Test_expandAlias(t *testing.T) {
|
|||||||
machines: []Machine{
|
machines: []Machine{
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.1"),
|
netaddr.MustParseIP("100.64.0.1"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "joe"},
|
Namespace: Namespace{Name: "joe"},
|
||||||
HostInfo: HostInfo{
|
HostInfo: HostInfo{
|
||||||
@@ -998,7 +975,7 @@ func Test_expandAlias(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.2"),
|
netaddr.MustParseIP("100.64.0.2"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "joe"},
|
Namespace: Namespace{Name: "joe"},
|
||||||
HostInfo: HostInfo{
|
HostInfo: HostInfo{
|
||||||
@@ -1009,13 +986,13 @@ func Test_expandAlias(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.3"),
|
netaddr.MustParseIP("100.64.0.3"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "marc"},
|
Namespace: Namespace{Name: "marc"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.4"),
|
netaddr.MustParseIP("100.64.0.4"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "joe"},
|
Namespace: Namespace{Name: "joe"},
|
||||||
},
|
},
|
||||||
@@ -1035,25 +1012,25 @@ func Test_expandAlias(t *testing.T) {
|
|||||||
machines: []Machine{
|
machines: []Machine{
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.1"),
|
netaddr.MustParseIP("100.64.0.1"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "joe"},
|
Namespace: Namespace{Name: "joe"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.2"),
|
netaddr.MustParseIP("100.64.0.2"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "joe"},
|
Namespace: Namespace{Name: "joe"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.3"),
|
netaddr.MustParseIP("100.64.0.3"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "marc"},
|
Namespace: Namespace{Name: "marc"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.4"),
|
netaddr.MustParseIP("100.64.0.4"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "mickael"},
|
Namespace: Namespace{Name: "mickael"},
|
||||||
},
|
},
|
||||||
@@ -1076,27 +1053,27 @@ func Test_expandAlias(t *testing.T) {
|
|||||||
machines: []Machine{
|
machines: []Machine{
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.1"),
|
netaddr.MustParseIP("100.64.0.1"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "joe"},
|
Namespace: Namespace{Name: "joe"},
|
||||||
ForcedTags: []string{"tag:hr-webserver"},
|
ForcedTags: []string{"tag:hr-webserver"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.2"),
|
netaddr.MustParseIP("100.64.0.2"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "joe"},
|
Namespace: Namespace{Name: "joe"},
|
||||||
ForcedTags: []string{"tag:hr-webserver"},
|
ForcedTags: []string{"tag:hr-webserver"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.3"),
|
netaddr.MustParseIP("100.64.0.3"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "marc"},
|
Namespace: Namespace{Name: "marc"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.4"),
|
netaddr.MustParseIP("100.64.0.4"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "mickael"},
|
Namespace: Namespace{Name: "mickael"},
|
||||||
},
|
},
|
||||||
@@ -1114,14 +1091,14 @@ func Test_expandAlias(t *testing.T) {
|
|||||||
machines: []Machine{
|
machines: []Machine{
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.1"),
|
netaddr.MustParseIP("100.64.0.1"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "joe"},
|
Namespace: Namespace{Name: "joe"},
|
||||||
ForcedTags: []string{"tag:hr-webserver"},
|
ForcedTags: []string{"tag:hr-webserver"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.2"),
|
netaddr.MustParseIP("100.64.0.2"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "joe"},
|
Namespace: Namespace{Name: "joe"},
|
||||||
HostInfo: HostInfo{
|
HostInfo: HostInfo{
|
||||||
@@ -1132,13 +1109,13 @@ func Test_expandAlias(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.3"),
|
netaddr.MustParseIP("100.64.0.3"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "marc"},
|
Namespace: Namespace{Name: "marc"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.4"),
|
netaddr.MustParseIP("100.64.0.4"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "mickael"},
|
Namespace: Namespace{Name: "mickael"},
|
||||||
},
|
},
|
||||||
@@ -1160,7 +1137,7 @@ func Test_expandAlias(t *testing.T) {
|
|||||||
machines: []Machine{
|
machines: []Machine{
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.1"),
|
netaddr.MustParseIP("100.64.0.1"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "joe"},
|
Namespace: Namespace{Name: "joe"},
|
||||||
HostInfo: HostInfo{
|
HostInfo: HostInfo{
|
||||||
@@ -1171,7 +1148,7 @@ func Test_expandAlias(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.2"),
|
netaddr.MustParseIP("100.64.0.2"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "joe"},
|
Namespace: Namespace{Name: "joe"},
|
||||||
HostInfo: HostInfo{
|
HostInfo: HostInfo{
|
||||||
@@ -1182,13 +1159,13 @@ func Test_expandAlias(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.3"),
|
netaddr.MustParseIP("100.64.0.3"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "marc"},
|
Namespace: Namespace{Name: "marc"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.4"),
|
netaddr.MustParseIP("100.64.0.4"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "joe"},
|
Namespace: Namespace{Name: "joe"},
|
||||||
},
|
},
|
||||||
@@ -1224,10 +1201,9 @@ func Test_expandAlias(t *testing.T) {
|
|||||||
|
|
||||||
func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
|
func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
aclPolicy ACLPolicy
|
aclPolicy ACLPolicy
|
||||||
nodes []Machine
|
nodes []Machine
|
||||||
namespace string
|
namespace string
|
||||||
stripEmailDomain bool
|
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -1244,7 +1220,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
|
|||||||
nodes: []Machine{
|
nodes: []Machine{
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.1"),
|
netaddr.MustParseIP("100.64.0.1"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "joe"},
|
Namespace: Namespace{Name: "joe"},
|
||||||
HostInfo: HostInfo{
|
HostInfo: HostInfo{
|
||||||
@@ -1255,7 +1231,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.2"),
|
netaddr.MustParseIP("100.64.0.2"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "joe"},
|
Namespace: Namespace{Name: "joe"},
|
||||||
HostInfo: HostInfo{
|
HostInfo: HostInfo{
|
||||||
@@ -1266,68 +1242,16 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.4"),
|
netaddr.MustParseIP("100.64.0.4"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "joe"},
|
Namespace: Namespace{Name: "joe"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
namespace: "joe",
|
namespace: "joe",
|
||||||
stripEmailDomain: true,
|
|
||||||
},
|
},
|
||||||
want: []Machine{
|
want: []Machine{
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.4")},
|
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.4")},
|
||||||
Namespace: Namespace{Name: "joe"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "exclude nodes with valid tags, and owner is in a group",
|
|
||||||
args: args{
|
|
||||||
aclPolicy: ACLPolicy{
|
|
||||||
Groups: Groups{
|
|
||||||
"group:accountant": []string{"joe", "bar"},
|
|
||||||
},
|
|
||||||
TagOwners: TagOwners{
|
|
||||||
"tag:accountant-webserver": []string{"group:accountant"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
nodes: []Machine{
|
|
||||||
{
|
|
||||||
IPAddresses: MachineAddresses{
|
|
||||||
netip.MustParseAddr("100.64.0.1"),
|
|
||||||
},
|
|
||||||
Namespace: Namespace{Name: "joe"},
|
|
||||||
HostInfo: HostInfo{
|
|
||||||
OS: "centos",
|
|
||||||
Hostname: "foo",
|
|
||||||
RequestTags: []string{"tag:accountant-webserver"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
IPAddresses: MachineAddresses{
|
|
||||||
netip.MustParseAddr("100.64.0.2"),
|
|
||||||
},
|
|
||||||
Namespace: Namespace{Name: "joe"},
|
|
||||||
HostInfo: HostInfo{
|
|
||||||
OS: "centos",
|
|
||||||
Hostname: "foo",
|
|
||||||
RequestTags: []string{"tag:accountant-webserver"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
IPAddresses: MachineAddresses{
|
|
||||||
netip.MustParseAddr("100.64.0.4"),
|
|
||||||
},
|
|
||||||
Namespace: Namespace{Name: "joe"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
namespace: "joe",
|
|
||||||
stripEmailDomain: true,
|
|
||||||
},
|
|
||||||
want: []Machine{
|
|
||||||
{
|
|
||||||
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.4")},
|
|
||||||
Namespace: Namespace{Name: "joe"},
|
Namespace: Namespace{Name: "joe"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1341,7 +1265,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
|
|||||||
nodes: []Machine{
|
nodes: []Machine{
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.1"),
|
netaddr.MustParseIP("100.64.0.1"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "joe"},
|
Namespace: Namespace{Name: "joe"},
|
||||||
HostInfo: HostInfo{
|
HostInfo: HostInfo{
|
||||||
@@ -1352,24 +1276,23 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.2"),
|
netaddr.MustParseIP("100.64.0.2"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "joe"},
|
Namespace: Namespace{Name: "joe"},
|
||||||
ForcedTags: []string{"tag:accountant-webserver"},
|
ForcedTags: []string{"tag:accountant-webserver"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.4"),
|
netaddr.MustParseIP("100.64.0.4"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "joe"},
|
Namespace: Namespace{Name: "joe"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
namespace: "joe",
|
namespace: "joe",
|
||||||
stripEmailDomain: true,
|
|
||||||
},
|
},
|
||||||
want: []Machine{
|
want: []Machine{
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.4")},
|
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.4")},
|
||||||
Namespace: Namespace{Name: "joe"},
|
Namespace: Namespace{Name: "joe"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1383,7 +1306,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
|
|||||||
nodes: []Machine{
|
nodes: []Machine{
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.1"),
|
netaddr.MustParseIP("100.64.0.1"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "joe"},
|
Namespace: Namespace{Name: "joe"},
|
||||||
HostInfo: HostInfo{
|
HostInfo: HostInfo{
|
||||||
@@ -1394,7 +1317,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.2"),
|
netaddr.MustParseIP("100.64.0.2"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "joe"},
|
Namespace: Namespace{Name: "joe"},
|
||||||
HostInfo: HostInfo{
|
HostInfo: HostInfo{
|
||||||
@@ -1405,18 +1328,17 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.4"),
|
netaddr.MustParseIP("100.64.0.4"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "joe"},
|
Namespace: Namespace{Name: "joe"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
namespace: "joe",
|
namespace: "joe",
|
||||||
stripEmailDomain: true,
|
|
||||||
},
|
},
|
||||||
want: []Machine{
|
want: []Machine{
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.1"),
|
netaddr.MustParseIP("100.64.0.1"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "joe"},
|
Namespace: Namespace{Name: "joe"},
|
||||||
HostInfo: HostInfo{
|
HostInfo: HostInfo{
|
||||||
@@ -1427,7 +1349,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.2"),
|
netaddr.MustParseIP("100.64.0.2"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "joe"},
|
Namespace: Namespace{Name: "joe"},
|
||||||
HostInfo: HostInfo{
|
HostInfo: HostInfo{
|
||||||
@@ -1438,7 +1360,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
IPAddresses: MachineAddresses{
|
IPAddresses: MachineAddresses{
|
||||||
netip.MustParseAddr("100.64.0.4"),
|
netaddr.MustParseIP("100.64.0.4"),
|
||||||
},
|
},
|
||||||
Namespace: Namespace{Name: "joe"},
|
Namespace: Namespace{Name: "joe"},
|
||||||
},
|
},
|
||||||
@@ -1451,7 +1373,6 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
|
|||||||
test.args.aclPolicy,
|
test.args.aclPolicy,
|
||||||
test.args.nodes,
|
test.args.nodes,
|
||||||
test.args.namespace,
|
test.args.namespace,
|
||||||
test.args.stripEmailDomain,
|
|
||||||
)
|
)
|
||||||
if !reflect.DeepEqual(got, test.want) {
|
if !reflect.DeepEqual(got, test.want) {
|
||||||
t.Errorf("excludeCorrectlyTaggedNodes() = %v, want %v", got, test.want)
|
t.Errorf("excludeCorrectlyTaggedNodes() = %v, want %v", got, test.want)
|
||||||
|
|||||||
@@ -2,55 +2,47 @@ package headscale
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/netip"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tailscale/hujson"
|
"github.com/tailscale/hujson"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
"inet.af/netaddr"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ACLPolicy represents a Tailscale ACL Policy.
|
// ACLPolicy represents a Tailscale ACL Policy.
|
||||||
type ACLPolicy struct {
|
type ACLPolicy struct {
|
||||||
Groups Groups `json:"groups" yaml:"groups"`
|
Groups Groups `json:"groups" yaml:"groups"`
|
||||||
Hosts Hosts `json:"hosts" yaml:"hosts"`
|
Hosts Hosts `json:"hosts" yaml:"hosts"`
|
||||||
TagOwners TagOwners `json:"tagOwners" yaml:"tagOwners"`
|
TagOwners TagOwners `json:"tagOwners" yaml:"tagOwners"`
|
||||||
ACLs []ACL `json:"acls" yaml:"acls"`
|
ACLs []ACL `json:"acls" yaml:"acls"`
|
||||||
Tests []ACLTest `json:"tests" yaml:"tests"`
|
Tests []ACLTest `json:"tests" yaml:"tests"`
|
||||||
AutoApprovers AutoApprovers `json:"autoApprovers" yaml:"autoApprovers"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ACL is a basic rule for the ACL Policy.
|
// ACL is a basic rule for the ACL Policy.
|
||||||
type ACL struct {
|
type ACL struct {
|
||||||
Action string `json:"action" yaml:"action"`
|
Action string `json:"action" yaml:"action"`
|
||||||
Protocol string `json:"proto" yaml:"proto"`
|
Protocol string `json:"proto" yaml:"proto"`
|
||||||
Sources []string `json:"src" yaml:"src"`
|
Sources []string `json:"src" yaml:"src"`
|
||||||
Destinations []string `json:"dst" yaml:"dst"`
|
Destinations []string `json:"dst" yaml:"dst"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Groups references a series of alias in the ACL rules.
|
// Groups references a series of alias in the ACL rules.
|
||||||
type Groups map[string][]string
|
type Groups map[string][]string
|
||||||
|
|
||||||
// Hosts are alias for IP addresses or subnets.
|
// Hosts are alias for IP addresses or subnets.
|
||||||
type Hosts map[string]netip.Prefix
|
type Hosts map[string]netaddr.IPPrefix
|
||||||
|
|
||||||
// TagOwners specify what users (namespaces?) are allow to use certain tags.
|
// TagOwners specify what users (namespaces?) are allow to use certain tags.
|
||||||
type TagOwners map[string][]string
|
type TagOwners map[string][]string
|
||||||
|
|
||||||
// ACLTest is not implemented, but should be use to check if a certain rule is allowed.
|
// ACLTest is not implemented, but should be use to check if a certain rule is allowed.
|
||||||
type ACLTest struct {
|
type ACLTest struct {
|
||||||
Source string `json:"src" yaml:"src"`
|
Source string `json:"src" yaml:"src"`
|
||||||
Accept []string `json:"accept" yaml:"accept"`
|
Accept []string `json:"accept" yaml:"accept"`
|
||||||
Deny []string `json:"deny,omitempty" yaml:"deny,omitempty"`
|
Deny []string `json:"deny,omitempty" yaml:"deny,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AutoApprovers specify which users (namespaces?), groups or tags have their advertised routes
|
// UnmarshalJSON allows to parse the Hosts directly into netaddr objects.
|
||||||
// or exit node status automatically enabled.
|
|
||||||
type AutoApprovers struct {
|
|
||||||
Routes map[string][]string `json:"routes" yaml:"routes"`
|
|
||||||
ExitNode []string `json:"exitNode" yaml:"exitNode"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalJSON allows to parse the Hosts directly into netip objects.
|
|
||||||
func (hosts *Hosts) UnmarshalJSON(data []byte) error {
|
func (hosts *Hosts) UnmarshalJSON(data []byte) error {
|
||||||
newHosts := Hosts{}
|
newHosts := Hosts{}
|
||||||
hostIPPrefixMap := make(map[string]string)
|
hostIPPrefixMap := make(map[string]string)
|
||||||
@@ -68,7 +60,7 @@ func (hosts *Hosts) UnmarshalJSON(data []byte) error {
|
|||||||
if !strings.Contains(prefixStr, "/") {
|
if !strings.Contains(prefixStr, "/") {
|
||||||
prefixStr += "/32"
|
prefixStr += "/32"
|
||||||
}
|
}
|
||||||
prefix, err := netip.ParsePrefix(prefixStr)
|
prefix, err := netaddr.ParseIPPrefix(prefixStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -79,7 +71,7 @@ func (hosts *Hosts) UnmarshalJSON(data []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalYAML allows to parse the Hosts directly into netip objects.
|
// UnmarshalYAML allows to parse the Hosts directly into netaddr objects.
|
||||||
func (hosts *Hosts) UnmarshalYAML(data []byte) error {
|
func (hosts *Hosts) UnmarshalYAML(data []byte) error {
|
||||||
newHosts := Hosts{}
|
newHosts := Hosts{}
|
||||||
hostIPPrefixMap := make(map[string]string)
|
hostIPPrefixMap := make(map[string]string)
|
||||||
@@ -89,7 +81,7 @@ func (hosts *Hosts) UnmarshalYAML(data []byte) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for host, prefixStr := range hostIPPrefixMap {
|
for host, prefixStr := range hostIPPrefixMap {
|
||||||
prefix, err := netip.ParsePrefix(prefixStr)
|
prefix, err := netaddr.ParseIPPrefix(prefixStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -108,28 +100,3 @@ func (policy ACLPolicy) IsZero() bool {
|
|||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the list of autoApproving namespaces, groups or tags for a given IPPrefix.
|
|
||||||
func (autoApprovers *AutoApprovers) GetRouteApprovers(
|
|
||||||
prefix netip.Prefix,
|
|
||||||
) ([]string, error) {
|
|
||||||
if prefix.Bits() == 0 {
|
|
||||||
return autoApprovers.ExitNode, nil // 0.0.0.0/0, ::/0 or equivalent
|
|
||||||
}
|
|
||||||
|
|
||||||
approverAliases := []string{}
|
|
||||||
|
|
||||||
for autoApprovedPrefix, autoApproverAliases := range autoApprovers.Routes {
|
|
||||||
autoApprovedPrefix, err := netip.ParsePrefix(autoApprovedPrefix)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if prefix.Bits() >= autoApprovedPrefix.Bits() &&
|
|
||||||
autoApprovedPrefix.Contains(prefix.Masked().Addr()) {
|
|
||||||
approverAliases = append(approverAliases, autoApproverAliases...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return approverAliases, nil
|
|
||||||
}
|
|
||||||
|
|||||||
775
api.go
775
api.go
@@ -2,19 +2,25 @@ package headscale
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/klauspost/compress/zstd"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/key"
|
"tailscale.com/types/key"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// TODO(juan): remove this once https://github.com/juanfont/headscale/issues/727 is fixed.
|
|
||||||
registrationHoldoff = time.Second * 5
|
|
||||||
reservedResponseHeaderSize = 4
|
reservedResponseHeaderSize = 4
|
||||||
RegisterMethodAuthKey = "authkey"
|
RegisterMethodAuthKey = "authkey"
|
||||||
RegisterMethodOIDC = "oidc"
|
RegisterMethodOIDC = "oidc"
|
||||||
@@ -53,7 +59,7 @@ func (h *Headscale) HealthHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.pingDB(req.Context()); err != nil {
|
if err := h.pingDB(); err != nil {
|
||||||
respond(err)
|
respond(err)
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -62,6 +68,23 @@ func (h *Headscale) HealthHandler(
|
|||||||
respond(nil)
|
respond(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KeyHandler provides the Headscale pub key
|
||||||
|
// Listens in /key.
|
||||||
|
func (h *Headscale) KeyHandler(
|
||||||
|
writer http.ResponseWriter,
|
||||||
|
req *http.Request,
|
||||||
|
) {
|
||||||
|
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
writer.WriteHeader(http.StatusOK)
|
||||||
|
_, err := writer.Write([]byte(MachinePublicKeyStripPrefix(h.privateKey.Public())))
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Err(err).
|
||||||
|
Msg("Failed to write response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type registerWebAPITemplateConfig struct {
|
type registerWebAPITemplateConfig struct {
|
||||||
Key string
|
Key string
|
||||||
}
|
}
|
||||||
@@ -84,44 +107,13 @@ var registerWebAPITemplate = template.Must(
|
|||||||
`))
|
`))
|
||||||
|
|
||||||
// RegisterWebAPI shows a simple message in the browser to point to the CLI
|
// RegisterWebAPI shows a simple message in the browser to point to the CLI
|
||||||
// Listens in /register/:nkey.
|
// Listens in /register.
|
||||||
//
|
|
||||||
// This is not part of the Tailscale control API, as we could send whatever URL
|
|
||||||
// in the RegisterResponse.AuthURL field.
|
|
||||||
func (h *Headscale) RegisterWebAPI(
|
func (h *Headscale) RegisterWebAPI(
|
||||||
writer http.ResponseWriter,
|
writer http.ResponseWriter,
|
||||||
req *http.Request,
|
req *http.Request,
|
||||||
) {
|
) {
|
||||||
vars := mux.Vars(req)
|
machineKeyStr := req.URL.Query().Get("key")
|
||||||
nodeKeyStr, ok := vars["nkey"]
|
if machineKeyStr == "" {
|
||||||
|
|
||||||
if !NodePublicKeyRegex.Match([]byte(nodeKeyStr)) {
|
|
||||||
log.Warn().Str("node_key", nodeKeyStr).Msg("Invalid node key passed to registration url")
|
|
||||||
|
|
||||||
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
||||||
writer.WriteHeader(http.StatusUnauthorized)
|
|
||||||
_, err := writer.Write([]byte("Unauthorized"))
|
|
||||||
if err != nil {
|
|
||||||
log.Error().
|
|
||||||
Caller().
|
|
||||||
Err(err).
|
|
||||||
Msg("Failed to write response")
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// We need to make sure we dont open for XSS style injections, if the parameter that
|
|
||||||
// is passed as a key is not parsable/validated as a NodePublic key, then fail to render
|
|
||||||
// the template and log an error.
|
|
||||||
var nodeKey key.NodePublic
|
|
||||||
err := nodeKey.UnmarshalText(
|
|
||||||
[]byte(NodePublicKeyEnsurePrefix(nodeKeyStr)),
|
|
||||||
)
|
|
||||||
|
|
||||||
if !ok || nodeKeyStr == "" || err != nil {
|
|
||||||
log.Warn().Err(err).Msg("Failed to parse incoming nodekey")
|
|
||||||
|
|
||||||
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
writer.WriteHeader(http.StatusBadRequest)
|
writer.WriteHeader(http.StatusBadRequest)
|
||||||
_, err := writer.Write([]byte("Wrong params"))
|
_, err := writer.Write([]byte("Wrong params"))
|
||||||
@@ -137,7 +129,7 @@ func (h *Headscale) RegisterWebAPI(
|
|||||||
|
|
||||||
var content bytes.Buffer
|
var content bytes.Buffer
|
||||||
if err := registerWebAPITemplate.Execute(&content, registerWebAPITemplateConfig{
|
if err := registerWebAPITemplate.Execute(&content, registerWebAPITemplateConfig{
|
||||||
Key: nodeKeyStr,
|
Key: machineKeyStr,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
Str("func", "RegisterWebAPI").
|
Str("func", "RegisterWebAPI").
|
||||||
@@ -158,7 +150,7 @@ func (h *Headscale) RegisterWebAPI(
|
|||||||
|
|
||||||
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
writer.WriteHeader(http.StatusOK)
|
writer.WriteHeader(http.StatusOK)
|
||||||
_, err = writer.Write(content.Bytes())
|
_, err := writer.Write(content.Bytes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
Caller().
|
Caller().
|
||||||
@@ -166,3 +158,708 @@ func (h *Headscale) RegisterWebAPI(
|
|||||||
Msg("Failed to write response")
|
Msg("Failed to write response")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegistrationHandler handles the actual registration process of a machine
|
||||||
|
// Endpoint /machine/:mkey.
|
||||||
|
func (h *Headscale) RegistrationHandler(
|
||||||
|
writer http.ResponseWriter,
|
||||||
|
req *http.Request,
|
||||||
|
) {
|
||||||
|
vars := mux.Vars(req)
|
||||||
|
machineKeyStr, ok := vars["mkey"]
|
||||||
|
if !ok || machineKeyStr == "" {
|
||||||
|
log.Error().
|
||||||
|
Str("handler", "RegistrationHandler").
|
||||||
|
Msg("No machine ID in request")
|
||||||
|
http.Error(writer, "No machine ID in request", http.StatusBadRequest)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body, _ := io.ReadAll(req.Body)
|
||||||
|
|
||||||
|
var machineKey key.MachinePublic
|
||||||
|
err := machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machineKeyStr)))
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Err(err).
|
||||||
|
Msg("Cannot parse machine key")
|
||||||
|
machineRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc()
|
||||||
|
http.Error(writer, "Cannot parse machine key", http.StatusBadRequest)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
registerRequest := tailcfg.RegisterRequest{}
|
||||||
|
err = decode(body, ®isterRequest, &machineKey, h.privateKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Err(err).
|
||||||
|
Msg("Cannot decode message")
|
||||||
|
machineRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc()
|
||||||
|
http.Error(writer, "Cannot decode message", http.StatusBadRequest)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().UTC()
|
||||||
|
machine, err := h.GetMachineByMachineKey(machineKey)
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
log.Info().Str("machine", registerRequest.Hostinfo.Hostname).Msg("New machine")
|
||||||
|
|
||||||
|
machineKeyStr := MachinePublicKeyStripPrefix(machineKey)
|
||||||
|
|
||||||
|
// If the machine has AuthKey set, handle registration via PreAuthKeys
|
||||||
|
if registerRequest.Auth.AuthKey != "" {
|
||||||
|
h.handleAuthKey(writer, req, machineKey, registerRequest)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
givenName, err := h.GenerateGivenName(registerRequest.Hostinfo.Hostname)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Str("func", "RegistrationHandler").
|
||||||
|
Str("hostinfo.name", registerRequest.Hostinfo.Hostname).
|
||||||
|
Err(err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// The machine did not have a key to authenticate, which means
|
||||||
|
// that we rely on a method that calls back some how (OpenID or CLI)
|
||||||
|
// We create the machine and then keep it around until a callback
|
||||||
|
// happens
|
||||||
|
newMachine := Machine{
|
||||||
|
MachineKey: machineKeyStr,
|
||||||
|
Hostname: registerRequest.Hostinfo.Hostname,
|
||||||
|
GivenName: givenName,
|
||||||
|
NodeKey: NodePublicKeyStripPrefix(registerRequest.NodeKey),
|
||||||
|
LastSeen: &now,
|
||||||
|
Expiry: &time.Time{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if !registerRequest.Expiry.IsZero() {
|
||||||
|
log.Trace().
|
||||||
|
Caller().
|
||||||
|
Str("machine", registerRequest.Hostinfo.Hostname).
|
||||||
|
Time("expiry", registerRequest.Expiry).
|
||||||
|
Msg("Non-zero expiry time requested")
|
||||||
|
newMachine.Expiry = ®isterRequest.Expiry
|
||||||
|
}
|
||||||
|
|
||||||
|
h.registrationCache.Set(
|
||||||
|
machineKeyStr,
|
||||||
|
newMachine,
|
||||||
|
registerCacheExpiration,
|
||||||
|
)
|
||||||
|
|
||||||
|
h.handleMachineRegistrationNew(writer, req, machineKey, registerRequest)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// The machine is already registered, so we need to pass through reauth or key update.
|
||||||
|
if machine != nil {
|
||||||
|
// If the NodeKey stored in headscale is the same as the key presented in a registration
|
||||||
|
// request, then we have a node that is either:
|
||||||
|
// - Trying to log out (sending a expiry in the past)
|
||||||
|
// - A valid, registered machine, looking for the node map
|
||||||
|
// - Expired machine wanting to reauthenticate
|
||||||
|
if machine.NodeKey == NodePublicKeyStripPrefix(registerRequest.NodeKey) {
|
||||||
|
// The client sends an Expiry in the past if the client is requesting to expire the key (aka logout)
|
||||||
|
// https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L648
|
||||||
|
if !registerRequest.Expiry.IsZero() && registerRequest.Expiry.UTC().Before(now) {
|
||||||
|
h.handleMachineLogOut(writer, req, machineKey, *machine)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If machine is not expired, and is register, we have a already accepted this machine,
|
||||||
|
// let it proceed with a valid registration
|
||||||
|
if !machine.isExpired() {
|
||||||
|
h.handleMachineValidRegistration(writer, req, machineKey, *machine)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The NodeKey we have matches OldNodeKey, which means this is a refresh after a key expiration
|
||||||
|
if machine.NodeKey == NodePublicKeyStripPrefix(registerRequest.OldNodeKey) &&
|
||||||
|
!machine.isExpired() {
|
||||||
|
h.handleMachineRefreshKey(writer, req, machineKey, registerRequest, *machine)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// The machine has expired
|
||||||
|
h.handleMachineExpired(writer, req, machineKey, registerRequest, *machine)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Headscale) getMapResponse(
|
||||||
|
machineKey key.MachinePublic,
|
||||||
|
mapRequest tailcfg.MapRequest,
|
||||||
|
machine *Machine,
|
||||||
|
) ([]byte, error) {
|
||||||
|
log.Trace().
|
||||||
|
Str("func", "getMapResponse").
|
||||||
|
Str("machine", mapRequest.Hostinfo.Hostname).
|
||||||
|
Msg("Creating Map response")
|
||||||
|
node, err := machine.toNode(h.cfg.BaseDomain, h.cfg.DNSConfig, true)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Str("func", "getMapResponse").
|
||||||
|
Err(err).
|
||||||
|
Msg("Cannot convert to node")
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
peers, err := h.getValidPeers(machine)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Str("func", "getMapResponse").
|
||||||
|
Err(err).
|
||||||
|
Msg("Cannot fetch peers")
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
profiles := getMapResponseUserProfiles(*machine, peers)
|
||||||
|
|
||||||
|
nodePeers, err := peers.toNodes(h.cfg.BaseDomain, h.cfg.DNSConfig, true)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Str("func", "getMapResponse").
|
||||||
|
Err(err).
|
||||||
|
Msg("Failed to convert peers to Tailscale nodes")
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dnsConfig := getMapResponseDNSConfig(
|
||||||
|
h.cfg.DNSConfig,
|
||||||
|
h.cfg.BaseDomain,
|
||||||
|
*machine,
|
||||||
|
peers,
|
||||||
|
)
|
||||||
|
|
||||||
|
resp := tailcfg.MapResponse{
|
||||||
|
KeepAlive: false,
|
||||||
|
Node: node,
|
||||||
|
Peers: nodePeers,
|
||||||
|
DNSConfig: dnsConfig,
|
||||||
|
Domain: h.cfg.BaseDomain,
|
||||||
|
PacketFilter: h.aclRules,
|
||||||
|
DERPMap: h.DERPMap,
|
||||||
|
UserProfiles: profiles,
|
||||||
|
Debug: &tailcfg.Debug{
|
||||||
|
DisableLogTail: !h.cfg.LogTail.Enabled,
|
||||||
|
RandomizeClientPort: h.cfg.RandomizeClientPort,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Trace().
|
||||||
|
Str("func", "getMapResponse").
|
||||||
|
Str("machine", mapRequest.Hostinfo.Hostname).
|
||||||
|
// Interface("payload", resp).
|
||||||
|
Msgf("Generated map response: %s", tailMapResponseToString(resp))
|
||||||
|
|
||||||
|
var respBody []byte
|
||||||
|
if mapRequest.Compress == "zstd" {
|
||||||
|
src, err := json.Marshal(resp)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Str("func", "getMapResponse").
|
||||||
|
Err(err).
|
||||||
|
Msg("Failed to marshal response for the client")
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder, _ := zstd.NewWriter(nil)
|
||||||
|
srcCompressed := encoder.EncodeAll(src, nil)
|
||||||
|
respBody = h.privateKey.SealTo(machineKey, srcCompressed)
|
||||||
|
} else {
|
||||||
|
respBody, err = encode(resp, &machineKey, h.privateKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// declare the incoming size on the first 4 bytes
|
||||||
|
data := make([]byte, reservedResponseHeaderSize)
|
||||||
|
binary.LittleEndian.PutUint32(data, uint32(len(respBody)))
|
||||||
|
data = append(data, respBody...)
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Headscale) getMapKeepAliveResponse(
|
||||||
|
machineKey key.MachinePublic,
|
||||||
|
mapRequest tailcfg.MapRequest,
|
||||||
|
) ([]byte, error) {
|
||||||
|
mapResponse := tailcfg.MapResponse{
|
||||||
|
KeepAlive: true,
|
||||||
|
}
|
||||||
|
var respBody []byte
|
||||||
|
var err error
|
||||||
|
if mapRequest.Compress == "zstd" {
|
||||||
|
src, err := json.Marshal(mapResponse)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Str("func", "getMapKeepAliveResponse").
|
||||||
|
Err(err).
|
||||||
|
Msg("Failed to marshal keepalive response for the client")
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
encoder, _ := zstd.NewWriter(nil)
|
||||||
|
srcCompressed := encoder.EncodeAll(src, nil)
|
||||||
|
respBody = h.privateKey.SealTo(machineKey, srcCompressed)
|
||||||
|
} else {
|
||||||
|
respBody, err = encode(mapResponse, &machineKey, h.privateKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data := make([]byte, reservedResponseHeaderSize)
|
||||||
|
binary.LittleEndian.PutUint32(data, uint32(len(respBody)))
|
||||||
|
data = append(data, respBody...)
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Headscale) handleMachineLogOut(
|
||||||
|
writer http.ResponseWriter,
|
||||||
|
req *http.Request,
|
||||||
|
machineKey key.MachinePublic,
|
||||||
|
machine Machine,
|
||||||
|
) {
|
||||||
|
resp := tailcfg.RegisterResponse{}
|
||||||
|
|
||||||
|
log.Info().
|
||||||
|
Str("machine", machine.Hostname).
|
||||||
|
Msg("Client requested logout")
|
||||||
|
|
||||||
|
err := h.ExpireMachine(&machine)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Str("func", "handleMachineLogOut").
|
||||||
|
Err(err).
|
||||||
|
Msg("Failed to expire machine")
|
||||||
|
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.AuthURL = ""
|
||||||
|
resp.MachineAuthorized = false
|
||||||
|
resp.User = *machine.Namespace.toUser()
|
||||||
|
respBody, err := encode(resp, &machineKey, h.privateKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Err(err).
|
||||||
|
Msg("Cannot encode message")
|
||||||
|
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
writer.WriteHeader(http.StatusOK)
|
||||||
|
_, err = writer.Write(respBody)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Err(err).
|
||||||
|
Msg("Failed to write response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Headscale) handleMachineValidRegistration(
|
||||||
|
writer http.ResponseWriter,
|
||||||
|
req *http.Request,
|
||||||
|
machineKey key.MachinePublic,
|
||||||
|
machine Machine,
|
||||||
|
) {
|
||||||
|
resp := tailcfg.RegisterResponse{}
|
||||||
|
|
||||||
|
// The machine registration is valid, respond with redirect to /map
|
||||||
|
log.Debug().
|
||||||
|
Str("machine", machine.Hostname).
|
||||||
|
Msg("Client is registered and we have the current NodeKey. All clear to /map")
|
||||||
|
|
||||||
|
resp.AuthURL = ""
|
||||||
|
resp.MachineAuthorized = true
|
||||||
|
resp.User = *machine.Namespace.toUser()
|
||||||
|
resp.Login = *machine.Namespace.toLogin()
|
||||||
|
|
||||||
|
respBody, err := encode(resp, &machineKey, h.privateKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Err(err).
|
||||||
|
Msg("Cannot encode message")
|
||||||
|
machineRegistrations.WithLabelValues("update", "web", "error", machine.Namespace.Name).
|
||||||
|
Inc()
|
||||||
|
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
machineRegistrations.WithLabelValues("update", "web", "success", machine.Namespace.Name).
|
||||||
|
Inc()
|
||||||
|
|
||||||
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
writer.WriteHeader(http.StatusOK)
|
||||||
|
_, err = writer.Write(respBody)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Err(err).
|
||||||
|
Msg("Failed to write response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Headscale) handleMachineExpired(
|
||||||
|
writer http.ResponseWriter,
|
||||||
|
req *http.Request,
|
||||||
|
machineKey key.MachinePublic,
|
||||||
|
registerRequest tailcfg.RegisterRequest,
|
||||||
|
machine Machine,
|
||||||
|
) {
|
||||||
|
resp := tailcfg.RegisterResponse{}
|
||||||
|
|
||||||
|
// The client has registered before, but has expired
|
||||||
|
log.Debug().
|
||||||
|
Str("machine", machine.Hostname).
|
||||||
|
Msg("Machine registration has expired. Sending a authurl to register")
|
||||||
|
|
||||||
|
if registerRequest.Auth.AuthKey != "" {
|
||||||
|
h.handleAuthKey(writer, req, machineKey, registerRequest)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.cfg.OIDC.Issuer != "" {
|
||||||
|
resp.AuthURL = fmt.Sprintf("%s/oidc/register/%s",
|
||||||
|
strings.TrimSuffix(h.cfg.ServerURL, "/"), machineKey.String())
|
||||||
|
} else {
|
||||||
|
resp.AuthURL = fmt.Sprintf("%s/register?key=%s",
|
||||||
|
strings.TrimSuffix(h.cfg.ServerURL, "/"), machineKey.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
respBody, err := encode(resp, &machineKey, h.privateKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Err(err).
|
||||||
|
Msg("Cannot encode message")
|
||||||
|
machineRegistrations.WithLabelValues("reauth", "web", "error", machine.Namespace.Name).
|
||||||
|
Inc()
|
||||||
|
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
machineRegistrations.WithLabelValues("reauth", "web", "success", machine.Namespace.Name).
|
||||||
|
Inc()
|
||||||
|
|
||||||
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
writer.WriteHeader(http.StatusOK)
|
||||||
|
_, err = writer.Write(respBody)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Err(err).
|
||||||
|
Msg("Failed to write response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Headscale) handleMachineRefreshKey(
|
||||||
|
writer http.ResponseWriter,
|
||||||
|
req *http.Request,
|
||||||
|
machineKey key.MachinePublic,
|
||||||
|
registerRequest tailcfg.RegisterRequest,
|
||||||
|
machine Machine,
|
||||||
|
) {
|
||||||
|
resp := tailcfg.RegisterResponse{}
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("machine", machine.Hostname).
|
||||||
|
Msg("We have the OldNodeKey in the database. This is a key refresh")
|
||||||
|
machine.NodeKey = NodePublicKeyStripPrefix(registerRequest.NodeKey)
|
||||||
|
|
||||||
|
if err := h.db.Save(&machine).Error; err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Err(err).
|
||||||
|
Msg("Failed to update machine key in the database")
|
||||||
|
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.AuthURL = ""
|
||||||
|
resp.User = *machine.Namespace.toUser()
|
||||||
|
respBody, err := encode(resp, &machineKey, h.privateKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Err(err).
|
||||||
|
Msg("Cannot encode message")
|
||||||
|
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
writer.WriteHeader(http.StatusOK)
|
||||||
|
_, err = writer.Write(respBody)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Err(err).
|
||||||
|
Msg("Failed to write response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Headscale) handleMachineRegistrationNew(
|
||||||
|
writer http.ResponseWriter,
|
||||||
|
req *http.Request,
|
||||||
|
machineKey key.MachinePublic,
|
||||||
|
registerRequest tailcfg.RegisterRequest,
|
||||||
|
) {
|
||||||
|
resp := tailcfg.RegisterResponse{}
|
||||||
|
|
||||||
|
// The machine registration is new, redirect the client to the registration URL
|
||||||
|
log.Debug().
|
||||||
|
Str("machine", registerRequest.Hostinfo.Hostname).
|
||||||
|
Msg("The node is sending us a new NodeKey, sending auth url")
|
||||||
|
if h.cfg.OIDC.Issuer != "" {
|
||||||
|
resp.AuthURL = fmt.Sprintf(
|
||||||
|
"%s/oidc/register/%s",
|
||||||
|
strings.TrimSuffix(h.cfg.ServerURL, "/"),
|
||||||
|
machineKey.String(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
resp.AuthURL = fmt.Sprintf("%s/register?key=%s",
|
||||||
|
strings.TrimSuffix(h.cfg.ServerURL, "/"), MachinePublicKeyStripPrefix(machineKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
respBody, err := encode(resp, &machineKey, h.privateKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Err(err).
|
||||||
|
Msg("Cannot encode message")
|
||||||
|
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
writer.WriteHeader(http.StatusOK)
|
||||||
|
_, err = writer.Write(respBody)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Err(err).
|
||||||
|
Msg("Failed to write response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: check if any locks are needed around IP allocation.
|
||||||
|
func (h *Headscale) handleAuthKey(
|
||||||
|
writer http.ResponseWriter,
|
||||||
|
req *http.Request,
|
||||||
|
machineKey key.MachinePublic,
|
||||||
|
registerRequest tailcfg.RegisterRequest,
|
||||||
|
) {
|
||||||
|
machineKeyStr := MachinePublicKeyStripPrefix(machineKey)
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("func", "handleAuthKey").
|
||||||
|
Str("machine", registerRequest.Hostinfo.Hostname).
|
||||||
|
Msgf("Processing auth key for %s", registerRequest.Hostinfo.Hostname)
|
||||||
|
resp := tailcfg.RegisterResponse{}
|
||||||
|
|
||||||
|
pak, err := h.checkKeyValidity(registerRequest.Auth.AuthKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Str("func", "handleAuthKey").
|
||||||
|
Str("machine", registerRequest.Hostinfo.Hostname).
|
||||||
|
Err(err).
|
||||||
|
Msg("Failed authentication via AuthKey")
|
||||||
|
resp.MachineAuthorized = false
|
||||||
|
respBody, err := encode(resp, &machineKey, h.privateKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Str("func", "handleAuthKey").
|
||||||
|
Str("machine", registerRequest.Hostinfo.Hostname).
|
||||||
|
Err(err).
|
||||||
|
Msg("Cannot encode message")
|
||||||
|
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||||||
|
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
|
||||||
|
Inc()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
writer.WriteHeader(http.StatusUnauthorized)
|
||||||
|
_, err = writer.Write(respBody)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Err(err).
|
||||||
|
Msg("Failed to write response")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Str("func", "handleAuthKey").
|
||||||
|
Str("machine", registerRequest.Hostinfo.Hostname).
|
||||||
|
Msg("Failed authentication via AuthKey")
|
||||||
|
|
||||||
|
if pak != nil {
|
||||||
|
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
|
||||||
|
Inc()
|
||||||
|
} else {
|
||||||
|
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", "unknown").Inc()
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("func", "handleAuthKey").
|
||||||
|
Str("machine", registerRequest.Hostinfo.Hostname).
|
||||||
|
Msg("Authentication key was valid, proceeding to acquire IP addresses")
|
||||||
|
|
||||||
|
nodeKey := NodePublicKeyStripPrefix(registerRequest.NodeKey)
|
||||||
|
|
||||||
|
// retrieve machine information if it exist
|
||||||
|
// The error is not important, because if it does not
|
||||||
|
// exist, then this is a new machine and we will move
|
||||||
|
// on to registration.
|
||||||
|
machine, _ := h.GetMachineByMachineKey(machineKey)
|
||||||
|
if machine != nil {
|
||||||
|
log.Trace().
|
||||||
|
Caller().
|
||||||
|
Str("machine", machine.Hostname).
|
||||||
|
Msg("machine already registered, refreshing with new auth key")
|
||||||
|
|
||||||
|
machine.NodeKey = nodeKey
|
||||||
|
machine.AuthKeyID = uint(pak.ID)
|
||||||
|
err := h.RefreshMachine(machine, registerRequest.Expiry)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Str("machine", machine.Hostname).
|
||||||
|
Err(err).
|
||||||
|
Msg("Failed to refresh machine")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
|
||||||
|
givenName, err := h.GenerateGivenName(registerRequest.Hostinfo.Hostname)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Str("func", "RegistrationHandler").
|
||||||
|
Str("hostinfo.name", registerRequest.Hostinfo.Hostname).
|
||||||
|
Err(err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
machineToRegister := Machine{
|
||||||
|
Hostname: registerRequest.Hostinfo.Hostname,
|
||||||
|
GivenName: givenName,
|
||||||
|
NamespaceID: pak.Namespace.ID,
|
||||||
|
MachineKey: machineKeyStr,
|
||||||
|
RegisterMethod: RegisterMethodAuthKey,
|
||||||
|
Expiry: ®isterRequest.Expiry,
|
||||||
|
NodeKey: nodeKey,
|
||||||
|
LastSeen: &now,
|
||||||
|
AuthKeyID: uint(pak.ID),
|
||||||
|
}
|
||||||
|
|
||||||
|
machine, err = h.RegisterMachine(
|
||||||
|
machineToRegister,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Err(err).
|
||||||
|
Msg("could not register machine")
|
||||||
|
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
|
||||||
|
Inc()
|
||||||
|
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.UsePreAuthKey(pak)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Err(err).
|
||||||
|
Msg("Failed to use pre-auth key")
|
||||||
|
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
|
||||||
|
Inc()
|
||||||
|
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.MachineAuthorized = true
|
||||||
|
resp.User = *pak.Namespace.toUser()
|
||||||
|
respBody, err := encode(resp, &machineKey, h.privateKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Str("func", "handleAuthKey").
|
||||||
|
Str("machine", registerRequest.Hostinfo.Hostname).
|
||||||
|
Err(err).
|
||||||
|
Msg("Cannot encode message")
|
||||||
|
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
|
||||||
|
Inc()
|
||||||
|
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "success", pak.Namespace.Name).
|
||||||
|
Inc()
|
||||||
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
writer.WriteHeader(http.StatusOK)
|
||||||
|
_, err = writer.Write(respBody)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Err(err).
|
||||||
|
Msg("Failed to write response")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().
|
||||||
|
Str("func", "handleAuthKey").
|
||||||
|
Str("machine", registerRequest.Hostinfo.Hostname).
|
||||||
|
Str("ips", strings.Join(machine.IPAddresses.ToStringSlice(), ", ")).
|
||||||
|
Msg("Successfully authenticated via AuthKey")
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
package headscale
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"tailscale.com/tailcfg"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (h *Headscale) generateMapResponse(
|
|
||||||
mapRequest tailcfg.MapRequest,
|
|
||||||
machine *Machine,
|
|
||||||
) (*tailcfg.MapResponse, error) {
|
|
||||||
log.Trace().
|
|
||||||
Str("func", "generateMapResponse").
|
|
||||||
Str("machine", mapRequest.Hostinfo.Hostname).
|
|
||||||
Msg("Creating Map response")
|
|
||||||
node, err := machine.toNode(h.cfg.BaseDomain, h.cfg.DNSConfig)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().
|
|
||||||
Caller().
|
|
||||||
Str("func", "generateMapResponse").
|
|
||||||
Err(err).
|
|
||||||
Msg("Cannot convert to node")
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
peers, err := h.getValidPeers(machine)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().
|
|
||||||
Caller().
|
|
||||||
Str("func", "generateMapResponse").
|
|
||||||
Err(err).
|
|
||||||
Msg("Cannot fetch peers")
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
profiles := getMapResponseUserProfiles(*machine, peers)
|
|
||||||
|
|
||||||
nodePeers, err := peers.toNodes(h.cfg.BaseDomain, h.cfg.DNSConfig)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().
|
|
||||||
Caller().
|
|
||||||
Str("func", "generateMapResponse").
|
|
||||||
Err(err).
|
|
||||||
Msg("Failed to convert peers to Tailscale nodes")
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
dnsConfig := getMapResponseDNSConfig(
|
|
||||||
h.cfg.DNSConfig,
|
|
||||||
h.cfg.BaseDomain,
|
|
||||||
*machine,
|
|
||||||
peers,
|
|
||||||
)
|
|
||||||
|
|
||||||
resp := tailcfg.MapResponse{
|
|
||||||
KeepAlive: false,
|
|
||||||
Node: node,
|
|
||||||
Peers: nodePeers,
|
|
||||||
DNSConfig: dnsConfig,
|
|
||||||
Domain: h.cfg.BaseDomain,
|
|
||||||
PacketFilter: h.aclRules,
|
|
||||||
DERPMap: h.DERPMap,
|
|
||||||
UserProfiles: profiles,
|
|
||||||
Debug: &tailcfg.Debug{
|
|
||||||
DisableLogTail: !h.cfg.LogTail.Enabled,
|
|
||||||
RandomizeClientPort: h.cfg.RandomizeClientPort,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Trace().
|
|
||||||
Str("func", "generateMapResponse").
|
|
||||||
Str("machine", mapRequest.Hostinfo.Hostname).
|
|
||||||
// Interface("payload", resp).
|
|
||||||
Msgf("Generated map response: %s", tailMapResponseToString(resp))
|
|
||||||
|
|
||||||
return &resp, nil
|
|
||||||
}
|
|
||||||
@@ -14,7 +14,7 @@ const (
|
|||||||
apiPrefixLength = 7
|
apiPrefixLength = 7
|
||||||
apiKeyLength = 32
|
apiKeyLength = 32
|
||||||
|
|
||||||
ErrAPIKeyFailedToParse = Error("Failed to parse ApiKey")
|
errAPIKeyFailedToParse = Error("Failed to parse ApiKey")
|
||||||
)
|
)
|
||||||
|
|
||||||
// APIKey describes the datamodel for API keys used to remotely authenticate with
|
// APIKey describes the datamodel for API keys used to remotely authenticate with
|
||||||
@@ -116,7 +116,7 @@ func (h *Headscale) ExpireAPIKey(key *APIKey) error {
|
|||||||
func (h *Headscale) ValidateAPIKey(keyStr string) (bool, error) {
|
func (h *Headscale) ValidateAPIKey(keyStr string) (bool, error) {
|
||||||
prefix, hash, found := strings.Cut(keyStr, ".")
|
prefix, hash, found := strings.Cut(keyStr, ".")
|
||||||
if !found {
|
if !found {
|
||||||
return false, ErrAPIKeyFailedToParse
|
return false, errAPIKeyFailedToParse
|
||||||
}
|
}
|
||||||
|
|
||||||
key, err := h.GetAPIKey(prefix)
|
key, err := h.GetAPIKey(prefix)
|
||||||
|
|||||||
136
app.go
136
app.go
@@ -18,13 +18,13 @@ import (
|
|||||||
|
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
grpcMiddleware "github.com/grpc-ecosystem/go-grpc-middleware"
|
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
|
||||||
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||||
"github.com/patrickmn/go-cache"
|
"github.com/patrickmn/go-cache"
|
||||||
zerolog "github.com/philip-bui/grpc-zerolog"
|
zerolog "github.com/philip-bui/grpc-zerolog"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
"github.com/puzpuzpuz/xsync/v2"
|
"github.com/puzpuzpuz/xsync"
|
||||||
zl "github.com/rs/zerolog"
|
zl "github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"golang.org/x/crypto/acme"
|
"golang.org/x/crypto/acme"
|
||||||
@@ -72,15 +72,12 @@ const (
|
|||||||
|
|
||||||
// Headscale represents the base app of the service.
|
// Headscale represents the base app of the service.
|
||||||
type Headscale struct {
|
type Headscale struct {
|
||||||
cfg *Config
|
cfg *Config
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
dbString string
|
dbString string
|
||||||
dbType string
|
dbType string
|
||||||
dbDebug bool
|
dbDebug bool
|
||||||
privateKey *key.MachinePrivate
|
privateKey *key.MachinePrivate
|
||||||
noisePrivateKey *key.MachinePrivate
|
|
||||||
|
|
||||||
noiseMux *mux.Router
|
|
||||||
|
|
||||||
DERPMap *tailcfg.DERPMap
|
DERPMap *tailcfg.DERPMap
|
||||||
DERPServer *DERPServer
|
DERPServer *DERPServer
|
||||||
@@ -88,7 +85,7 @@ type Headscale struct {
|
|||||||
aclPolicy *ACLPolicy
|
aclPolicy *ACLPolicy
|
||||||
aclRules []tailcfg.FilterRule
|
aclRules []tailcfg.FilterRule
|
||||||
|
|
||||||
lastStateChange *xsync.MapOf[string, time.Time]
|
lastStateChange *xsync.MapOf[time.Time]
|
||||||
|
|
||||||
oidcProvider *oidc.Provider
|
oidcProvider *oidc.Provider
|
||||||
oauth2Config *oauth2.Config
|
oauth2Config *oauth2.Config
|
||||||
@@ -123,42 +120,22 @@ func LookupTLSClientAuthMode(mode string) (tls.ClientAuthType, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewHeadscale(cfg *Config) (*Headscale, error) {
|
func NewHeadscale(cfg *Config) (*Headscale, error) {
|
||||||
privateKey, err := readOrCreatePrivateKey(cfg.PrivateKeyPath)
|
privKey, err := readOrCreatePrivateKey(cfg.PrivateKeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to read or create private key: %w", err)
|
return nil, fmt.Errorf("failed to read or create private key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TS2021 requires to have a different key from the legacy protocol.
|
|
||||||
noisePrivateKey, err := readOrCreatePrivateKey(cfg.NoisePrivateKeyPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to read or create Noise protocol private key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if privateKey.Equal(*noisePrivateKey) {
|
|
||||||
return nil, fmt.Errorf("private key and noise private key are the same: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var dbString string
|
var dbString string
|
||||||
switch cfg.DBtype {
|
switch cfg.DBtype {
|
||||||
case Postgres:
|
case Postgres:
|
||||||
dbString = fmt.Sprintf(
|
dbString = fmt.Sprintf(
|
||||||
"host=%s dbname=%s user=%s",
|
"host=%s port=%d dbname=%s user=%s password=%s sslmode=disable",
|
||||||
cfg.DBhost,
|
cfg.DBhost,
|
||||||
|
cfg.DBport,
|
||||||
cfg.DBname,
|
cfg.DBname,
|
||||||
cfg.DBuser,
|
cfg.DBuser,
|
||||||
|
cfg.DBpass,
|
||||||
)
|
)
|
||||||
|
|
||||||
if !cfg.DBssl {
|
|
||||||
dbString += " sslmode=disable"
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.DBport != 0 {
|
|
||||||
dbString += fmt.Sprintf(" port=%d", cfg.DBport)
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.DBpass != "" {
|
|
||||||
dbString += fmt.Sprintf(" password=%s", cfg.DBpass)
|
|
||||||
}
|
|
||||||
case Sqlite:
|
case Sqlite:
|
||||||
dbString = cfg.DBpath
|
dbString = cfg.DBpath
|
||||||
default:
|
default:
|
||||||
@@ -174,8 +151,7 @@ func NewHeadscale(cfg *Config) (*Headscale, error) {
|
|||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
dbType: cfg.DBtype,
|
dbType: cfg.DBtype,
|
||||||
dbString: dbString,
|
dbString: dbString,
|
||||||
privateKey: privateKey,
|
privateKey: privKey,
|
||||||
noisePrivateKey: noisePrivateKey,
|
|
||||||
aclRules: tailcfg.FilterAllowAll, // default allowall
|
aclRules: tailcfg.FilterAllowAll, // default allowall
|
||||||
registrationCache: registrationCache,
|
registrationCache: registrationCache,
|
||||||
pollNetMapStreamWG: sync.WaitGroup{},
|
pollNetMapStreamWG: sync.WaitGroup{},
|
||||||
@@ -189,11 +165,7 @@ func NewHeadscale(cfg *Config) (*Headscale, error) {
|
|||||||
if cfg.OIDC.Issuer != "" {
|
if cfg.OIDC.Issuer != "" {
|
||||||
err = app.initOIDC()
|
err = app.initOIDC()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if cfg.OIDC.OnlyStartIfOIDCIsAvailable {
|
return nil, err
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
log.Warn().Err(err).Msg("failed to set up OIDC provider, falling back to CLI based authentication")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,7 +247,7 @@ func (h *Headscale) expireEphemeralNodesWorker() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if expiredFound {
|
if expiredFound {
|
||||||
h.setLastStateChangeToNow()
|
h.setLastStateChangeToNow(namespace.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -443,24 +415,19 @@ func (h *Headscale) ensureUnixSocketIsAbsent() error {
|
|||||||
func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *mux.Router {
|
func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *mux.Router {
|
||||||
router := mux.NewRouter()
|
router := mux.NewRouter()
|
||||||
|
|
||||||
router.HandleFunc(ts2021UpgradePath, h.NoiseUpgradeHandler).Methods(http.MethodPost)
|
|
||||||
|
|
||||||
router.HandleFunc("/health", h.HealthHandler).Methods(http.MethodGet)
|
router.HandleFunc("/health", h.HealthHandler).Methods(http.MethodGet)
|
||||||
router.HandleFunc("/key", h.KeyHandler).Methods(http.MethodGet)
|
router.HandleFunc("/key", h.KeyHandler).Methods(http.MethodGet)
|
||||||
router.HandleFunc("/register/{nkey}", h.RegisterWebAPI).Methods(http.MethodGet)
|
router.HandleFunc("/register", h.RegisterWebAPI).Methods(http.MethodGet)
|
||||||
h.addLegacyHandlers(router)
|
router.HandleFunc("/machine/{mkey}/map", h.PollNetMapHandler).Methods(http.MethodPost)
|
||||||
|
router.HandleFunc("/machine/{mkey}", h.RegistrationHandler).Methods(http.MethodPost)
|
||||||
router.HandleFunc("/oidc/register/{nkey}", h.RegisterOIDC).Methods(http.MethodGet)
|
router.HandleFunc("/oidc/register/{mkey}", h.RegisterOIDC).Methods(http.MethodGet)
|
||||||
router.HandleFunc("/oidc/callback", h.OIDCCallback).Methods(http.MethodGet)
|
router.HandleFunc("/oidc/callback", h.OIDCCallback).Methods(http.MethodGet)
|
||||||
router.HandleFunc("/apple", h.AppleConfigMessage).Methods(http.MethodGet)
|
router.HandleFunc("/apple", h.AppleConfigMessage).Methods(http.MethodGet)
|
||||||
router.HandleFunc("/apple/{platform}", h.ApplePlatformConfig).
|
router.HandleFunc("/apple/{platform}", h.ApplePlatformConfig).Methods(http.MethodGet)
|
||||||
Methods(http.MethodGet)
|
|
||||||
router.HandleFunc("/windows", h.WindowsConfigMessage).Methods(http.MethodGet)
|
router.HandleFunc("/windows", h.WindowsConfigMessage).Methods(http.MethodGet)
|
||||||
router.HandleFunc("/windows/tailscale.reg", h.WindowsRegConfig).
|
router.HandleFunc("/windows/tailscale.reg", h.WindowsRegConfig).Methods(http.MethodGet)
|
||||||
Methods(http.MethodGet)
|
|
||||||
router.HandleFunc("/swagger", SwaggerUI).Methods(http.MethodGet)
|
router.HandleFunc("/swagger", SwaggerUI).Methods(http.MethodGet)
|
||||||
router.HandleFunc("/swagger/v1/openapiv2.json", SwaggerAPIv1).
|
router.HandleFunc("/swagger/v1/openapiv2.json", SwaggerAPIv1).Methods(http.MethodGet)
|
||||||
Methods(http.MethodGet)
|
|
||||||
|
|
||||||
if h.cfg.DERP.ServerEnabled {
|
if h.cfg.DERP.ServerEnabled {
|
||||||
router.HandleFunc("/derp", h.DERPHandler)
|
router.HandleFunc("/derp", h.DERPHandler)
|
||||||
@@ -477,16 +444,6 @@ func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *mux.Router {
|
|||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) createNoiseMux() *mux.Router {
|
|
||||||
router := mux.NewRouter()
|
|
||||||
|
|
||||||
router.HandleFunc("/machine/register", h.NoiseRegistrationHandler).
|
|
||||||
Methods(http.MethodPost)
|
|
||||||
router.HandleFunc("/machine/map", h.NoisePollNetMapHandler)
|
|
||||||
|
|
||||||
return router
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serve launches a GIN server with the Headscale API.
|
// Serve launches a GIN server with the Headscale API.
|
||||||
func (h *Headscale) Serve() error {
|
func (h *Headscale) Serve() error {
|
||||||
var err error
|
var err error
|
||||||
@@ -605,7 +562,7 @@ func (h *Headscale) Serve() error {
|
|||||||
|
|
||||||
grpcOptions := []grpc.ServerOption{
|
grpcOptions := []grpc.ServerOption{
|
||||||
grpc.UnaryInterceptor(
|
grpc.UnaryInterceptor(
|
||||||
grpcMiddleware.ChainUnaryServer(
|
grpc_middleware.ChainUnaryServer(
|
||||||
h.grpcAuthenticationInterceptor,
|
h.grpcAuthenticationInterceptor,
|
||||||
zerolog.NewUnaryServerInterceptor(),
|
zerolog.NewUnaryServerInterceptor(),
|
||||||
),
|
),
|
||||||
@@ -640,15 +597,8 @@ func (h *Headscale) Serve() error {
|
|||||||
//
|
//
|
||||||
// HTTP setup
|
// HTTP setup
|
||||||
//
|
//
|
||||||
// This is the regular router that we expose
|
|
||||||
// over our main Addr. It also serves the legacy Tailcale API
|
|
||||||
router := h.createRouter(grpcGatewayMux)
|
|
||||||
|
|
||||||
// This router is served only over the Noise connection, and exposes only the new API.
|
router := h.createRouter(grpcGatewayMux)
|
||||||
//
|
|
||||||
// The HTTP2 server that exposes this router is created for
|
|
||||||
// a single hijacked connection from /ts2021, using netutil.NewOneConnListener
|
|
||||||
h.noiseMux = h.createNoiseMux()
|
|
||||||
|
|
||||||
httpServer := &http.Server{
|
httpServer := &http.Server{
|
||||||
Addr: h.cfg.Addr,
|
Addr: h.cfg.Addr,
|
||||||
@@ -742,10 +692,7 @@ func (h *Headscale) Serve() error {
|
|||||||
h.pollNetMapStreamWG.Wait()
|
h.pollNetMapStreamWG.Wait()
|
||||||
|
|
||||||
// Gracefully shut down servers
|
// Gracefully shut down servers
|
||||||
ctx, cancel := context.WithTimeout(
|
ctx, cancel := context.WithTimeout(context.Background(), HTTPShutdownTimeout)
|
||||||
context.Background(),
|
|
||||||
HTTPShutdownTimeout,
|
|
||||||
)
|
|
||||||
if err := promHTTPServer.Shutdown(ctx); err != nil {
|
if err := promHTTPServer.Shutdown(ctx); err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to shutdown prometheus http")
|
log.Error().Err(err).Msg("Failed to shutdown prometheus http")
|
||||||
}
|
}
|
||||||
@@ -824,18 +771,10 @@ func (h *Headscale) getTLSSettings() (*tls.Config, error) {
|
|||||||
// Configuration via autocert with HTTP-01. This requires listening on
|
// Configuration via autocert with HTTP-01. This requires listening on
|
||||||
// port 80 for the certificate validation in addition to the headscale
|
// port 80 for the certificate validation in addition to the headscale
|
||||||
// service, which can be configured to run on any other port.
|
// service, which can be configured to run on any other port.
|
||||||
|
|
||||||
server := &http.Server{
|
|
||||||
Addr: h.cfg.TLS.LetsEncrypt.Listen,
|
|
||||||
Handler: certManager.HTTPHandler(http.HandlerFunc(h.redirect)),
|
|
||||||
ReadTimeout: HTTPReadTimeout,
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
err := server.ListenAndServe()
|
|
||||||
log.Fatal().
|
log.Fatal().
|
||||||
Caller().
|
Caller().
|
||||||
Err(err).
|
Err(http.ListenAndServe(h.cfg.TLS.LetsEncrypt.Listen, certManager.HTTPHandler(http.HandlerFunc(h.redirect)))).
|
||||||
Msg("failed to set up a HTTP server")
|
Msg("failed to set up a HTTP server")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -872,36 +811,35 @@ func (h *Headscale) getTLSSettings() (*tls.Config, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) setLastStateChangeToNow() {
|
func (h *Headscale) setLastStateChangeToNow(namespaces ...string) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
|
|
||||||
namespaces, err := h.ListNamespaces()
|
if len(namespaces) == 0 {
|
||||||
if err != nil {
|
namespaces, err = h.ListNamespacesStr()
|
||||||
log.Error().
|
if err != nil {
|
||||||
Caller().
|
log.Error().Caller().Err(err).Msg("failed to fetch all namespaces, failing to update last changed state.")
|
||||||
Err(err).
|
}
|
||||||
Msg("failed to fetch all namespaces, failing to update last changed state.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, namespace := range namespaces {
|
for _, namespace := range namespaces {
|
||||||
lastStateUpdate.WithLabelValues(namespace.Name, "headscale").Set(float64(now.Unix()))
|
lastStateUpdate.WithLabelValues(namespace, "headscale").Set(float64(now.Unix()))
|
||||||
if h.lastStateChange == nil {
|
if h.lastStateChange == nil {
|
||||||
h.lastStateChange = xsync.NewMapOf[time.Time]()
|
h.lastStateChange = xsync.NewMapOf[time.Time]()
|
||||||
}
|
}
|
||||||
h.lastStateChange.Store(namespace.Name, now)
|
h.lastStateChange.Store(namespace, now)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) getLastStateChange(namespaces ...Namespace) time.Time {
|
func (h *Headscale) getLastStateChange(namespaces ...string) time.Time {
|
||||||
times := []time.Time{}
|
times := []time.Time{}
|
||||||
|
|
||||||
// getLastStateChange takes a list of namespaces as a "filter", if no namespaces
|
// getLastStateChange takes a list of namespaces as a "filter", if no namespaces
|
||||||
// are past, then use the entier list of namespaces and look for the last update
|
// are past, then use the entier list of namespaces and look for the last update
|
||||||
if len(namespaces) > 0 {
|
if len(namespaces) > 0 {
|
||||||
for _, namespace := range namespaces {
|
for _, namespace := range namespaces {
|
||||||
if lastChange, ok := h.lastStateChange.Load(namespace.Name); ok {
|
if lastChange, ok := h.lastStateChange.Load(namespace); ok {
|
||||||
times = append(times, lastChange)
|
times = append(times, lastChange)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package headscale
|
package headscale
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/netip"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gopkg.in/check.v1"
|
"gopkg.in/check.v1"
|
||||||
|
"inet.af/netaddr"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test(t *testing.T) {
|
func Test(t *testing.T) {
|
||||||
@@ -34,13 +35,13 @@ func (s *Suite) ResetDB(c *check.C) {
|
|||||||
os.RemoveAll(tmpDir)
|
os.RemoveAll(tmpDir)
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
tmpDir, err = os.MkdirTemp("", "autoygg-client-test")
|
tmpDir, err = ioutil.TempDir("", "autoygg-client-test")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Fatal(err)
|
c.Fatal(err)
|
||||||
}
|
}
|
||||||
cfg := Config{
|
cfg := Config{
|
||||||
IPPrefixes: []netip.Prefix{
|
IPPrefixes: []netaddr.IPPrefix{
|
||||||
netip.MustParsePrefix("10.27.0.0/23"),
|
netaddr.MustParseIPPrefix("10.27.0.0/23"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -134,9 +134,7 @@ If you loose a key, create a new one and revoke (expire) the old one.`,
|
|||||||
|
|
||||||
expiration := time.Now().UTC().Add(time.Duration(duration))
|
expiration := time.Now().UTC().Add(time.Duration(duration))
|
||||||
|
|
||||||
log.Trace().
|
log.Trace().Dur("expiration", time.Duration(duration)).Msg("expiration has been set")
|
||||||
Dur("expiration", time.Duration(duration)).
|
|
||||||
Msg("expiration has been set")
|
|
||||||
|
|
||||||
request.Expiration = timestamppb.New(expiration)
|
request.Expiration = timestamppb.New(expiration)
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package cli
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/juanfont/headscale"
|
|
||||||
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/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -11,7 +10,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
errPreAuthKeyMalformed = Error("key is malformed. expected 64 hex characters with `nodekey` prefix")
|
keyLength = 64
|
||||||
|
errPreAuthKeyTooShort = Error("key too short, must be 64 hexadecimal characters")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Error is used to compare errors as per https://dave.cheney.net/2016/04/07/constant-errors
|
// Error is used to compare errors as per https://dave.cheney.net/2016/04/07/constant-errors
|
||||||
@@ -87,8 +87,8 @@ var createNodeCmd = &cobra.Command{
|
|||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !headscale.NodePublicKeyRegex.Match([]byte(machineKey)) {
|
if len(machineKey) != keyLength {
|
||||||
err = errPreAuthKeyMalformed
|
err = errPreAuthKeyTooShort
|
||||||
ErrorOutput(
|
ErrorOutput(
|
||||||
err,
|
err,
|
||||||
fmt.Sprintf("Error: %s", err),
|
fmt.Sprintf("Error: %s", err),
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
package cli
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/oauth2-proxy/mockoidc"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
errMockOidcClientIDNotDefined = Error("MOCKOIDC_CLIENT_ID not defined")
|
|
||||||
errMockOidcClientSecretNotDefined = Error("MOCKOIDC_CLIENT_SECRET not defined")
|
|
||||||
errMockOidcPortNotDefined = Error("MOCKOIDC_PORT not defined")
|
|
||||||
accessTTL = 10 * time.Minute
|
|
||||||
refreshTTL = 60 * time.Minute
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
rootCmd.AddCommand(mockOidcCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
var mockOidcCmd = &cobra.Command{
|
|
||||||
Use: "mockoidc",
|
|
||||||
Short: "Runs a mock OIDC server for testing",
|
|
||||||
Long: "This internal command runs a OpenID Connect for testing purposes",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
err := mockOIDC()
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msgf("Error running mock OIDC server")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func mockOIDC() error {
|
|
||||||
clientID := os.Getenv("MOCKOIDC_CLIENT_ID")
|
|
||||||
if clientID == "" {
|
|
||||||
return errMockOidcClientIDNotDefined
|
|
||||||
}
|
|
||||||
clientSecret := os.Getenv("MOCKOIDC_CLIENT_SECRET")
|
|
||||||
if clientSecret == "" {
|
|
||||||
return errMockOidcClientSecretNotDefined
|
|
||||||
}
|
|
||||||
addrStr := os.Getenv("MOCKOIDC_ADDR")
|
|
||||||
if addrStr == "" {
|
|
||||||
return errMockOidcPortNotDefined
|
|
||||||
}
|
|
||||||
portStr := os.Getenv("MOCKOIDC_PORT")
|
|
||||||
if portStr == "" {
|
|
||||||
return errMockOidcPortNotDefined
|
|
||||||
}
|
|
||||||
|
|
||||||
port, err := strconv.Atoi(portStr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
mock, err := getMockOIDC(clientID, clientSecret)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", addrStr, port))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = mock.Start(listener, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Info().Msgf("Mock OIDC server listening on %s", listener.Addr().String())
|
|
||||||
log.Info().Msgf("Issuer: %s", mock.Issuer())
|
|
||||||
c := make(chan struct{})
|
|
||||||
<-c
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getMockOIDC(clientID string, clientSecret string) (*mockoidc.MockOIDC, error) {
|
|
||||||
keypair, err := mockoidc.NewKeypair(nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
mock := mockoidc.MockOIDC{
|
|
||||||
ClientID: clientID,
|
|
||||||
ClientSecret: clientSecret,
|
|
||||||
AccessTTL: accessTTL,
|
|
||||||
RefreshTTL: refreshTTL,
|
|
||||||
CodeChallengeMethodsSupported: []string{"plain", "S256"},
|
|
||||||
Keypair: keypair,
|
|
||||||
SessionStore: mockoidc.NewSessionStore(),
|
|
||||||
UserQueue: &mockoidc.UserQueue{},
|
|
||||||
ErrorQueue: &mockoidc.ErrorQueue{},
|
|
||||||
}
|
|
||||||
|
|
||||||
return &mock, nil
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,6 @@ package cli
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/netip"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -14,6 +13,7 @@ import (
|
|||||||
"github.com/pterm/pterm"
|
"github.com/pterm/pterm"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
|
"inet.af/netaddr"
|
||||||
"tailscale.com/types/key"
|
"tailscale.com/types/key"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ var registerNodeCmd = &cobra.Command{
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
ErrorOutput(
|
||||||
err,
|
err,
|
||||||
fmt.Sprintf("Error getting node key from flag: %s", err),
|
fmt.Sprintf("Error getting machine key from flag: %s", err),
|
||||||
output,
|
output,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ var registerNodeCmd = &cobra.Command{
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
SuccessOutput(response.Machine, fmt.Sprintf("Machine %s registered", response.Machine.GivenName), output)
|
SuccessOutput(response.Machine, "Machine register", output)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -557,7 +557,7 @@ func nodesToPtables(
|
|||||||
var IPV4Address string
|
var IPV4Address string
|
||||||
var IPV6Address string
|
var IPV6Address string
|
||||||
for _, addr := range machine.IpAddresses {
|
for _, addr := range machine.IpAddresses {
|
||||||
if netip.MustParseAddr(addr).Is4() {
|
if netaddr.MustParseIP(addr).Is4() {
|
||||||
IPV4Address = addr
|
IPV4Address = addr
|
||||||
} else {
|
} else {
|
||||||
IPV6Address = addr
|
IPV6Address = addr
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package cli
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||||
@@ -34,8 +33,6 @@ func init() {
|
|||||||
Bool("ephemeral", false, "Preauthkey for ephemeral nodes")
|
Bool("ephemeral", false, "Preauthkey for ephemeral nodes")
|
||||||
createPreAuthKeyCmd.Flags().
|
createPreAuthKeyCmd.Flags().
|
||||||
StringP("expiration", "e", DefaultPreAuthKeyExpiry, "Human-readable expiration of the key (e.g. 30m, 24h)")
|
StringP("expiration", "e", DefaultPreAuthKeyExpiry, "Human-readable expiration of the key (e.g. 30m, 24h)")
|
||||||
createPreAuthKeyCmd.Flags().
|
|
||||||
StringSlice("tags", []string{}, "Tags to automatically assign to node")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var preauthkeysCmd = &cobra.Command{
|
var preauthkeysCmd = &cobra.Command{
|
||||||
@@ -84,16 +81,7 @@ var listPreAuthKeys = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
tableData := pterm.TableData{
|
tableData := pterm.TableData{
|
||||||
{
|
{"ID", "Key", "Reusable", "Ephemeral", "Used", "Expiration", "Created"},
|
||||||
"ID",
|
|
||||||
"Key",
|
|
||||||
"Reusable",
|
|
||||||
"Ephemeral",
|
|
||||||
"Used",
|
|
||||||
"Expiration",
|
|
||||||
"Created",
|
|
||||||
"Tags",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
for _, key := range response.PreAuthKeys {
|
for _, key := range response.PreAuthKeys {
|
||||||
expiration := "-"
|
expiration := "-"
|
||||||
@@ -108,14 +96,6 @@ var listPreAuthKeys = &cobra.Command{
|
|||||||
reusable = fmt.Sprintf("%v", key.GetReusable())
|
reusable = fmt.Sprintf("%v", key.GetReusable())
|
||||||
}
|
}
|
||||||
|
|
||||||
aclTags := ""
|
|
||||||
|
|
||||||
for _, tag := range key.AclTags {
|
|
||||||
aclTags += "," + tag
|
|
||||||
}
|
|
||||||
|
|
||||||
aclTags = strings.TrimLeft(aclTags, ",")
|
|
||||||
|
|
||||||
tableData = append(tableData, []string{
|
tableData = append(tableData, []string{
|
||||||
key.GetId(),
|
key.GetId(),
|
||||||
key.GetKey(),
|
key.GetKey(),
|
||||||
@@ -124,7 +104,6 @@ var listPreAuthKeys = &cobra.Command{
|
|||||||
strconv.FormatBool(key.GetUsed()),
|
strconv.FormatBool(key.GetUsed()),
|
||||||
expiration,
|
expiration,
|
||||||
key.GetCreatedAt().AsTime().Format("2006-01-02 15:04:05"),
|
key.GetCreatedAt().AsTime().Format("2006-01-02 15:04:05"),
|
||||||
aclTags,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -157,7 +136,6 @@ var createPreAuthKeyCmd = &cobra.Command{
|
|||||||
|
|
||||||
reusable, _ := cmd.Flags().GetBool("reusable")
|
reusable, _ := cmd.Flags().GetBool("reusable")
|
||||||
ephemeral, _ := cmd.Flags().GetBool("ephemeral")
|
ephemeral, _ := cmd.Flags().GetBool("ephemeral")
|
||||||
tags, _ := cmd.Flags().GetStringSlice("tags")
|
|
||||||
|
|
||||||
log.Trace().
|
log.Trace().
|
||||||
Bool("reusable", reusable).
|
Bool("reusable", reusable).
|
||||||
@@ -169,7 +147,6 @@ var createPreAuthKeyCmd = &cobra.Command{
|
|||||||
Namespace: namespace,
|
Namespace: namespace,
|
||||||
Reusable: reusable,
|
Reusable: reusable,
|
||||||
Ephemeral: ephemeral,
|
Ephemeral: ephemeral,
|
||||||
AclTags: tags,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
durationStr, _ := cmd.Flags().GetString("expiration")
|
durationStr, _ := cmd.Flags().GetString("expiration")
|
||||||
@@ -187,9 +164,7 @@ var createPreAuthKeyCmd = &cobra.Command{
|
|||||||
|
|
||||||
expiration := time.Now().UTC().Add(time.Duration(duration))
|
expiration := time.Now().UTC().Add(time.Duration(duration))
|
||||||
|
|
||||||
log.Trace().
|
log.Trace().Dur("expiration", time.Duration(duration)).Msg("expiration has been set")
|
||||||
Dur("expiration", time.Duration(duration)).
|
|
||||||
Msg("expiration has been set")
|
|
||||||
|
|
||||||
request.Expiration = timestamppb.New(expiration)
|
request.Expiration = timestamppb.New(expiration)
|
||||||
|
|
||||||
|
|||||||
@@ -15,10 +15,6 @@ import (
|
|||||||
var cfgFile string = ""
|
var cfgFile string = ""
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
if len(os.Args) > 1 && (os.Args[1] == "version" || os.Args[1] == "mockoidc") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cobra.OnInitialize(initConfig)
|
cobra.OnInitialize(initConfig)
|
||||||
rootCmd.PersistentFlags().
|
rootCmd.PersistentFlags().
|
||||||
StringVarP(&cfgFile, "config", "c", "", "config file (default is /etc/headscale/config.yaml)")
|
StringVarP(&cfgFile, "config", "c", "", "config file (default is /etc/headscale/config.yaml)")
|
||||||
@@ -29,18 +25,15 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func initConfig() {
|
func initConfig() {
|
||||||
if cfgFile == "" {
|
|
||||||
cfgFile = os.Getenv("HEADSCALE_CONFIG")
|
|
||||||
}
|
|
||||||
if cfgFile != "" {
|
if cfgFile != "" {
|
||||||
err := headscale.LoadConfig(cfgFile, true)
|
err := headscale.LoadConfig(cfgFile, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Caller().Err(err).Msgf("Error loading config file %s", cfgFile)
|
log.Fatal().Caller().Err(err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
err := headscale.LoadConfig("", false)
|
err := headscale.LoadConfig("", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Caller().Err(err).Msgf("Error loading config")
|
log.Fatal().Caller().Err(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +44,7 @@ func initConfig() {
|
|||||||
|
|
||||||
machineOutput := HasMachineOutputFlag()
|
machineOutput := HasMachineOutputFlag()
|
||||||
|
|
||||||
zerolog.SetGlobalLevel(cfg.Log.Level)
|
zerolog.SetGlobalLevel(cfg.LogLevel)
|
||||||
|
|
||||||
// If the user has requested a "machine" readable format,
|
// If the user has requested a "machine" readable format,
|
||||||
// then disable login so the output remains valid.
|
// then disable login so the output remains valid.
|
||||||
@@ -59,10 +52,6 @@ func initConfig() {
|
|||||||
zerolog.SetGlobalLevel(zerolog.Disabled)
|
zerolog.SetGlobalLevel(zerolog.Disabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Log.Format == headscale.JSONLogFormat {
|
|
||||||
log.Logger = log.Output(os.Stdout)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !cfg.DisableUpdateCheck && !machineOutput {
|
if !cfg.DisableUpdateCheck && !machineOutput {
|
||||||
if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") &&
|
if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") &&
|
||||||
Version != "dev" {
|
Version != "dev" {
|
||||||
|
|||||||
@@ -19,16 +19,12 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
HeadscaleDateTimeFormat = "2006-01-02 15:04:05"
|
HeadscaleDateTimeFormat = "2006-01-02 15:04:05"
|
||||||
SocketWritePermissions = 0o666
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func getHeadscaleApp() (*headscale.Headscale, error) {
|
func getHeadscaleApp() (*headscale.Headscale, error) {
|
||||||
cfg, err := headscale.GetHeadscaleConfig()
|
cfg, err := headscale.GetHeadscaleConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf("failed to load configuration while creating headscale instance: %w", err)
|
||||||
"failed to load configuration while creating headscale instance: %w",
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
app, err := headscale.NewHeadscale(cfg)
|
app, err := headscale.NewHeadscale(cfg)
|
||||||
@@ -82,19 +78,6 @@ func getHeadscaleCLIClient() (context.Context, v1.HeadscaleServiceClient, *grpc.
|
|||||||
|
|
||||||
address = cfg.UnixSocket
|
address = cfg.UnixSocket
|
||||||
|
|
||||||
// Try to give the user better feedback if we cannot write to the headscale
|
|
||||||
// socket.
|
|
||||||
socket, err := os.OpenFile(cfg.UnixSocket, os.O_WRONLY, SocketWritePermissions) //nolint
|
|
||||||
if err != nil {
|
|
||||||
if os.IsPermission(err) {
|
|
||||||
log.Fatal().
|
|
||||||
Err(err).
|
|
||||||
Str("socket", cfg.UnixSocket).
|
|
||||||
Msgf("Unable to read/write to headscale socket, do you have the correct permissions?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
socket.Close()
|
|
||||||
|
|
||||||
grpcOptions = append(
|
grpcOptions = append(
|
||||||
grpcOptions,
|
grpcOptions,
|
||||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -27,7 +28,7 @@ func (s *Suite) TearDownSuite(c *check.C) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (*Suite) TestConfigFileLoading(c *check.C) {
|
func (*Suite) TestConfigFileLoading(c *check.C) {
|
||||||
tmpDir, err := os.MkdirTemp("", "headscale")
|
tmpDir, err := ioutil.TempDir("", "headscale")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Fatal(err)
|
c.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -72,7 +73,7 @@ func (*Suite) TestConfigFileLoading(c *check.C) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (*Suite) TestConfigLoading(c *check.C) {
|
func (*Suite) TestConfigLoading(c *check.C) {
|
||||||
tmpDir, err := os.MkdirTemp("", "headscale")
|
tmpDir, err := ioutil.TempDir("", "headscale")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Fatal(err)
|
c.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -116,7 +117,7 @@ func (*Suite) TestConfigLoading(c *check.C) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (*Suite) TestDNSConfigLoading(c *check.C) {
|
func (*Suite) TestDNSConfigLoading(c *check.C) {
|
||||||
tmpDir, err := os.MkdirTemp("", "headscale")
|
tmpDir, err := ioutil.TempDir("", "headscale")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Fatal(err)
|
c.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -151,24 +152,22 @@ func (*Suite) TestDNSConfigLoading(c *check.C) {
|
|||||||
func writeConfig(c *check.C, tmpDir string, configYaml []byte) {
|
func writeConfig(c *check.C, tmpDir string, configYaml []byte) {
|
||||||
// Populate a custom config file
|
// Populate a custom config file
|
||||||
configFile := filepath.Join(tmpDir, "config.yaml")
|
configFile := filepath.Join(tmpDir, "config.yaml")
|
||||||
err := os.WriteFile(configFile, configYaml, 0o600)
|
err := ioutil.WriteFile(configFile, configYaml, 0o600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Fatalf("Couldn't write file %s", configFile)
|
c.Fatalf("Couldn't write file %s", configFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*Suite) TestTLSConfigValidation(c *check.C) {
|
func (*Suite) TestTLSConfigValidation(c *check.C) {
|
||||||
tmpDir, err := os.MkdirTemp("", "headscale")
|
tmpDir, err := ioutil.TempDir("", "headscale")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Fatal(err)
|
c.Fatal(err)
|
||||||
}
|
}
|
||||||
// defer os.RemoveAll(tmpDir)
|
// defer os.RemoveAll(tmpDir)
|
||||||
configYaml := []byte(`---
|
|
||||||
tls_letsencrypt_hostname: example.com
|
configYaml := []byte(
|
||||||
tls_letsencrypt_challenge_type: ""
|
"---\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"\"\ntls_cert_path: \"abc.pem\"",
|
||||||
tls_cert_path: abc.pem
|
)
|
||||||
noise:
|
|
||||||
private_key_path: noise_private.key`)
|
|
||||||
writeConfig(c, tmpDir, configYaml)
|
writeConfig(c, tmpDir, configYaml)
|
||||||
|
|
||||||
// Check configuration validation errors (1)
|
// Check configuration validation errors (1)
|
||||||
@@ -193,13 +192,9 @@ noise:
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Check configuration validation errors (2)
|
// Check configuration validation errors (2)
|
||||||
configYaml = []byte(`---
|
configYaml = []byte(
|
||||||
noise:
|
"---\nserver_url: \"http://127.0.0.1:8080\"\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"TLS-ALPN-01\"",
|
||||||
private_key_path: noise_private.key
|
)
|
||||||
server_url: http://127.0.0.1:8080
|
|
||||||
tls_letsencrypt_hostname: example.com
|
|
||||||
tls_letsencrypt_challenge_type: TLS-ALPN-01
|
|
||||||
`)
|
|
||||||
writeConfig(c, tmpDir, configYaml)
|
writeConfig(c, tmpDir, configYaml)
|
||||||
err = headscale.LoadConfig(tmpDir, false)
|
err = headscale.LoadConfig(tmpDir, false)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|||||||
@@ -35,20 +35,12 @@ grpc_listen_addr: 0.0.0.0:50443
|
|||||||
# are doing.
|
# are doing.
|
||||||
grpc_allow_insecure: false
|
grpc_allow_insecure: false
|
||||||
|
|
||||||
# Private key used to encrypt the traffic between headscale
|
# Private key used encrypt the traffic between headscale
|
||||||
# and Tailscale clients.
|
# and Tailscale clients.
|
||||||
# The private key file will be autogenerated if it's missing.
|
# The private key file which will be
|
||||||
|
# autogenerated if it's missing
|
||||||
private_key_path: /var/lib/headscale/private.key
|
private_key_path: /var/lib/headscale/private.key
|
||||||
|
|
||||||
# The Noise section includes specific configuration for the
|
|
||||||
# TS2021 Noise protocol
|
|
||||||
noise:
|
|
||||||
# The Noise private key is used to encrypt the
|
|
||||||
# traffic between headscale and Tailscale clients when
|
|
||||||
# using the new Noise-based protocol. It must be different
|
|
||||||
# from the legacy private key.
|
|
||||||
private_key_path: /var/lib/headscale/noise_private.key
|
|
||||||
|
|
||||||
# List of IP prefixes to allocate tailaddresses from.
|
# List of IP prefixes to allocate tailaddresses from.
|
||||||
# Each prefix consists of either an IPv4 or IPv6 address,
|
# Each prefix consists of either an IPv4 or IPv6 address,
|
||||||
# and the associated prefix length, delimited by a slash.
|
# and the associated prefix length, delimited by a slash.
|
||||||
@@ -77,7 +69,7 @@ derp:
|
|||||||
region_code: "headscale"
|
region_code: "headscale"
|
||||||
region_name: "Headscale Embedded DERP"
|
region_name: "Headscale Embedded DERP"
|
||||||
|
|
||||||
# Listens over UDP at the configured address for STUN connections - to help with NAT traversal.
|
# Listens in UDP at the configured address for STUN connections to help on NAT traversal.
|
||||||
# When the embedded DERP server is enabled stun_listen_addr MUST be defined.
|
# When the embedded DERP server is enabled stun_listen_addr MUST be defined.
|
||||||
#
|
#
|
||||||
# For more details on how this works, check this great article: https://tailscale.com/blog/how-tailscale-works/
|
# For more details on how this works, check this great article: https://tailscale.com/blog/how-tailscale-works/
|
||||||
@@ -111,9 +103,9 @@ disable_check_updates: false
|
|||||||
# Time before an inactive ephemeral node is deleted?
|
# Time before an inactive ephemeral node is deleted?
|
||||||
ephemeral_node_inactivity_timeout: 30m
|
ephemeral_node_inactivity_timeout: 30m
|
||||||
|
|
||||||
# Period to check for node updates within the tailnet. A value too low will severely affect
|
# Period to check for node updates in the tailnet. A value too low will severily affect
|
||||||
# CPU consumption of Headscale. A value too high (over 60s) will cause problems
|
# CPU consumption of Headscale. A value too high (over 60s) will cause problems
|
||||||
# for the nodes, as they won't get updates or keep alive messages frequently enough.
|
# to the nodes, as they won't get updates or keep alive messages in time.
|
||||||
# In case of doubts, do not touch the default 10s.
|
# In case of doubts, do not touch the default 10s.
|
||||||
node_update_check_interval: 10s
|
node_update_check_interval: 10s
|
||||||
|
|
||||||
@@ -122,14 +114,12 @@ db_type: sqlite3
|
|||||||
db_path: /var/lib/headscale/db.sqlite
|
db_path: /var/lib/headscale/db.sqlite
|
||||||
|
|
||||||
# # Postgres config
|
# # Postgres config
|
||||||
# If using a Unix socket to connect to Postgres, set the socket path in the 'host' field and leave 'port' blank.
|
|
||||||
# db_type: postgres
|
# db_type: postgres
|
||||||
# db_host: localhost
|
# db_host: localhost
|
||||||
# db_port: 5432
|
# db_port: 5432
|
||||||
# db_name: headscale
|
# db_name: headscale
|
||||||
# db_user: foo
|
# db_user: foo
|
||||||
# db_pass: bar
|
# db_pass: bar
|
||||||
# db_ssl: false
|
|
||||||
|
|
||||||
### TLS configuration
|
### TLS configuration
|
||||||
#
|
#
|
||||||
@@ -163,7 +153,7 @@ tls_letsencrypt_cache_dir: /var/lib/headscale/cache
|
|||||||
# See [docs/tls.md](docs/tls.md) for more information
|
# See [docs/tls.md](docs/tls.md) for more information
|
||||||
tls_letsencrypt_challenge_type: HTTP-01
|
tls_letsencrypt_challenge_type: HTTP-01
|
||||||
# When HTTP-01 challenge is chosen, letsencrypt must set up a
|
# When HTTP-01 challenge is chosen, letsencrypt must set up a
|
||||||
# verification endpoint, and it will be listening on:
|
# verification endpoint, and it will be listning on:
|
||||||
# :http = port 80
|
# :http = port 80
|
||||||
tls_letsencrypt_listen: ":http"
|
tls_letsencrypt_listen: ":http"
|
||||||
|
|
||||||
@@ -171,10 +161,7 @@ tls_letsencrypt_listen: ":http"
|
|||||||
tls_cert_path: ""
|
tls_cert_path: ""
|
||||||
tls_key_path: ""
|
tls_key_path: ""
|
||||||
|
|
||||||
log:
|
log_level: info
|
||||||
# Output formatting for logs: text or json
|
|
||||||
format: text
|
|
||||||
level: info
|
|
||||||
|
|
||||||
# Path to a file containg ACL policies.
|
# Path to a file containg ACL policies.
|
||||||
# ACLs can be defined as YAML or HUJSON.
|
# ACLs can be defined as YAML or HUJSON.
|
||||||
@@ -191,9 +178,6 @@ acl_policy_path: ""
|
|||||||
# - https://tailscale.com/blog/2021-09-private-dns-with-magicdns/
|
# - https://tailscale.com/blog/2021-09-private-dns-with-magicdns/
|
||||||
#
|
#
|
||||||
dns_config:
|
dns_config:
|
||||||
# Whether to prefer using Headscale provided DNS or use local.
|
|
||||||
override_local_dns: true
|
|
||||||
|
|
||||||
# List of DNS servers to expose to clients.
|
# List of DNS servers to expose to clients.
|
||||||
nameservers:
|
nameservers:
|
||||||
- 1.1.1.1
|
- 1.1.1.1
|
||||||
@@ -232,7 +216,6 @@ unix_socket_permission: "0770"
|
|||||||
# help us test it.
|
# help us test it.
|
||||||
# OpenID Connect
|
# OpenID Connect
|
||||||
# oidc:
|
# oidc:
|
||||||
# only_start_if_oidc_is_available: true
|
|
||||||
# issuer: "https://your-oidc.issuer.com/path"
|
# issuer: "https://your-oidc.issuer.com/path"
|
||||||
# client_id: "your-oidc-client-id"
|
# client_id: "your-oidc-client-id"
|
||||||
# client_secret: "your-oidc-client-secret"
|
# client_secret: "your-oidc-client-secret"
|
||||||
|
|||||||
144
config.go
144
config.go
@@ -5,7 +5,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/netip"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -14,7 +13,7 @@ import (
|
|||||||
"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"
|
||||||
"go4.org/netipx"
|
"inet.af/netaddr"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/dnstype"
|
"tailscale.com/types/dnstype"
|
||||||
)
|
)
|
||||||
@@ -22,9 +21,6 @@ import (
|
|||||||
const (
|
const (
|
||||||
tlsALPN01ChallengeType = "TLS-ALPN-01"
|
tlsALPN01ChallengeType = "TLS-ALPN-01"
|
||||||
http01ChallengeType = "HTTP-01"
|
http01ChallengeType = "HTTP-01"
|
||||||
|
|
||||||
JSONLogFormat = "json"
|
|
||||||
TextLogFormat = "text"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config contains the initial Headscale configuration.
|
// Config contains the initial Headscale configuration.
|
||||||
@@ -36,11 +32,10 @@ type Config struct {
|
|||||||
GRPCAllowInsecure bool
|
GRPCAllowInsecure bool
|
||||||
EphemeralNodeInactivityTimeout time.Duration
|
EphemeralNodeInactivityTimeout time.Duration
|
||||||
NodeUpdateCheckInterval time.Duration
|
NodeUpdateCheckInterval time.Duration
|
||||||
IPPrefixes []netip.Prefix
|
IPPrefixes []netaddr.IPPrefix
|
||||||
PrivateKeyPath string
|
PrivateKeyPath string
|
||||||
NoisePrivateKeyPath string
|
|
||||||
BaseDomain string
|
BaseDomain string
|
||||||
Log LogConfig
|
LogLevel zerolog.Level
|
||||||
DisableUpdateCheck bool
|
DisableUpdateCheck bool
|
||||||
|
|
||||||
DERP DERPConfig
|
DERP DERPConfig
|
||||||
@@ -52,7 +47,6 @@ type Config struct {
|
|||||||
DBname string
|
DBname string
|
||||||
DBuser string
|
DBuser string
|
||||||
DBpass string
|
DBpass string
|
||||||
DBssl bool
|
|
||||||
|
|
||||||
TLS TLSConfig
|
TLS TLSConfig
|
||||||
|
|
||||||
@@ -90,15 +84,14 @@ type LetsEncryptConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OIDCConfig struct {
|
type OIDCConfig struct {
|
||||||
OnlyStartIfOIDCIsAvailable bool
|
Issuer string
|
||||||
Issuer string
|
ClientID string
|
||||||
ClientID string
|
ClientSecret string
|
||||||
ClientSecret string
|
Scope []string
|
||||||
Scope []string
|
ExtraParams map[string]string
|
||||||
ExtraParams map[string]string
|
AllowedDomains []string
|
||||||
AllowedDomains []string
|
AllowedUsers []string
|
||||||
AllowedUsers []string
|
StripEmaildomain bool
|
||||||
StripEmaildomain bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type DERPConfig struct {
|
type DERPConfig struct {
|
||||||
@@ -128,11 +121,6 @@ type ACLConfig struct {
|
|||||||
PolicyPath string
|
PolicyPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogConfig struct {
|
|
||||||
Format string
|
|
||||||
Level zerolog.Level
|
|
||||||
}
|
|
||||||
|
|
||||||
func LoadConfig(path string, isFile bool) error {
|
func LoadConfig(path string, isFile bool) error {
|
||||||
if isFile {
|
if isFile {
|
||||||
viper.SetConfigFile(path)
|
viper.SetConfigFile(path)
|
||||||
@@ -156,11 +144,9 @@ func LoadConfig(path string, isFile bool) error {
|
|||||||
viper.SetDefault("tls_letsencrypt_challenge_type", http01ChallengeType)
|
viper.SetDefault("tls_letsencrypt_challenge_type", http01ChallengeType)
|
||||||
viper.SetDefault("tls_client_auth_mode", "relaxed")
|
viper.SetDefault("tls_client_auth_mode", "relaxed")
|
||||||
|
|
||||||
viper.SetDefault("log.level", "info")
|
viper.SetDefault("log_level", "info")
|
||||||
viper.SetDefault("log.format", TextLogFormat)
|
|
||||||
|
|
||||||
viper.SetDefault("dns_config", nil)
|
viper.SetDefault("dns_config", nil)
|
||||||
viper.SetDefault("dns_config.override_local_dns", true)
|
|
||||||
|
|
||||||
viper.SetDefault("derp.server.enabled", false)
|
viper.SetDefault("derp.server.enabled", false)
|
||||||
viper.SetDefault("derp.server.stun.enabled", true)
|
viper.SetDefault("derp.server.stun.enabled", true)
|
||||||
@@ -176,7 +162,6 @@ 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("logtail.enabled", false)
|
viper.SetDefault("logtail.enabled", false)
|
||||||
viper.SetDefault("randomize_client_port", false)
|
viper.SetDefault("randomize_client_port", false)
|
||||||
@@ -198,10 +183,6 @@ func LoadConfig(path string, isFile bool) error {
|
|||||||
errorText += "Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both\n"
|
errorText += "Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
if !viper.IsSet("noise") || viper.GetString("noise.private_key_path") == "" {
|
|
||||||
errorText += "Fatal config error: headscale now requires a new `noise.private_key_path` field in the config file for the Tailscale v2 protocol\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (viper.GetString("tls_letsencrypt_hostname") != "") &&
|
if (viper.GetString("tls_letsencrypt_hostname") != "") &&
|
||||||
(viper.GetString("tls_letsencrypt_challenge_type") == tlsALPN01ChallengeType) &&
|
(viper.GetString("tls_letsencrypt_challenge_type") == tlsALPN01ChallengeType) &&
|
||||||
(!strings.HasSuffix(viper.GetString("listen_addr"), ":443")) {
|
(!strings.HasSuffix(viper.GetString("listen_addr"), ":443")) {
|
||||||
@@ -346,48 +327,18 @@ func GetACLConfig() ACLConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetLogConfig() LogConfig {
|
|
||||||
logLevelStr := viper.GetString("log.level")
|
|
||||||
logLevel, err := zerolog.ParseLevel(logLevelStr)
|
|
||||||
if err != nil {
|
|
||||||
logLevel = zerolog.DebugLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
logFormatOpt := viper.GetString("log.format")
|
|
||||||
var logFormat string
|
|
||||||
switch logFormatOpt {
|
|
||||||
case "json":
|
|
||||||
logFormat = JSONLogFormat
|
|
||||||
case "text":
|
|
||||||
logFormat = TextLogFormat
|
|
||||||
case "":
|
|
||||||
logFormat = TextLogFormat
|
|
||||||
default:
|
|
||||||
log.Error().
|
|
||||||
Str("func", "GetLogConfig").
|
|
||||||
Msgf("Could not parse log format: %s. Valid choices are 'json' or 'text'", logFormatOpt)
|
|
||||||
}
|
|
||||||
|
|
||||||
return LogConfig{
|
|
||||||
Format: logFormat,
|
|
||||||
Level: logLevel,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetDNSConfig() (*tailcfg.DNSConfig, string) {
|
func GetDNSConfig() (*tailcfg.DNSConfig, string) {
|
||||||
if viper.IsSet("dns_config") {
|
if viper.IsSet("dns_config") {
|
||||||
dnsConfig := &tailcfg.DNSConfig{}
|
dnsConfig := &tailcfg.DNSConfig{}
|
||||||
|
|
||||||
overrideLocalDNS := viper.GetBool("dns_config.override_local_dns")
|
|
||||||
|
|
||||||
if viper.IsSet("dns_config.nameservers") {
|
if viper.IsSet("dns_config.nameservers") {
|
||||||
nameserversStr := viper.GetStringSlice("dns_config.nameservers")
|
nameserversStr := viper.GetStringSlice("dns_config.nameservers")
|
||||||
|
|
||||||
nameservers := make([]netip.Addr, len(nameserversStr))
|
nameservers := make([]netaddr.IP, len(nameserversStr))
|
||||||
resolvers := make([]*dnstype.Resolver, len(nameserversStr))
|
resolvers := make([]*dnstype.Resolver, len(nameserversStr))
|
||||||
|
|
||||||
for index, nameserverStr := range nameserversStr {
|
for index, nameserverStr := range nameserversStr {
|
||||||
nameserver, err := netip.ParseAddr(nameserverStr)
|
nameserver, err := netaddr.ParseIP(nameserverStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
Str("func", "getDNSConfig").
|
Str("func", "getDNSConfig").
|
||||||
@@ -402,12 +353,7 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dnsConfig.Nameservers = nameservers
|
dnsConfig.Nameservers = nameservers
|
||||||
|
dnsConfig.Resolvers = resolvers
|
||||||
if overrideLocalDNS {
|
|
||||||
dnsConfig.Resolvers = resolvers
|
|
||||||
} else {
|
|
||||||
dnsConfig.FallbackResolvers = resolvers
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if viper.IsSet("dns_config.restricted_nameservers") {
|
if viper.IsSet("dns_config.restricted_nameservers") {
|
||||||
@@ -422,7 +368,7 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) {
|
|||||||
len(restrictedNameservers),
|
len(restrictedNameservers),
|
||||||
)
|
)
|
||||||
for index, nameserverStr := range restrictedNameservers {
|
for index, nameserverStr := range restrictedNameservers {
|
||||||
nameserver, err := netip.ParseAddr(nameserverStr)
|
nameserver, err := netaddr.ParseIP(nameserverStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
Str("func", "getDNSConfig").
|
Str("func", "getDNSConfig").
|
||||||
@@ -442,17 +388,17 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if viper.IsSet("dns_config.domains") {
|
if viper.IsSet("dns_config.domains") {
|
||||||
domains := viper.GetStringSlice("dns_config.domains")
|
dnsConfig.Domains = viper.GetStringSlice("dns_config.domains")
|
||||||
if len(dnsConfig.Nameservers) > 0 {
|
|
||||||
dnsConfig.Domains = domains
|
|
||||||
} else if domains != nil {
|
|
||||||
log.Warn().
|
|
||||||
Msg("Warning: dns_config.domains is set, but no nameservers are configured. Ignoring domains.")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if viper.IsSet("dns_config.magic_dns") {
|
if viper.IsSet("dns_config.magic_dns") {
|
||||||
dnsConfig.Proxied = viper.GetBool("dns_config.magic_dns")
|
magicDNS := viper.GetBool("dns_config.magic_dns")
|
||||||
|
if len(dnsConfig.Nameservers) > 0 {
|
||||||
|
dnsConfig.Proxied = magicDNS
|
||||||
|
} else if magicDNS {
|
||||||
|
log.Warn().
|
||||||
|
Msg("Warning: dns_config.magic_dns is set, but no nameservers are configured. Ignoring magic_dns.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var baseDomain string
|
var baseDomain string
|
||||||
@@ -475,22 +421,44 @@ func GetHeadscaleConfig() (*Config, error) {
|
|||||||
randomizeClientPort := viper.GetBool("randomize_client_port")
|
randomizeClientPort := viper.GetBool("randomize_client_port")
|
||||||
|
|
||||||
configuredPrefixes := viper.GetStringSlice("ip_prefixes")
|
configuredPrefixes := viper.GetStringSlice("ip_prefixes")
|
||||||
parsedPrefixes := make([]netip.Prefix, 0, len(configuredPrefixes)+1)
|
parsedPrefixes := make([]netaddr.IPPrefix, 0, len(configuredPrefixes)+1)
|
||||||
|
|
||||||
|
logLevelStr := viper.GetString("log_level")
|
||||||
|
logLevel, err := zerolog.ParseLevel(logLevelStr)
|
||||||
|
if err != nil {
|
||||||
|
logLevel = zerolog.DebugLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
legacyPrefixField := viper.GetString("ip_prefix")
|
||||||
|
if len(legacyPrefixField) > 0 {
|
||||||
|
log.
|
||||||
|
Warn().
|
||||||
|
Msgf(
|
||||||
|
"%s, %s",
|
||||||
|
"use of 'ip_prefix' for configuration is deprecated",
|
||||||
|
"please see 'ip_prefixes' in the shipped example.",
|
||||||
|
)
|
||||||
|
legacyPrefix, err := netaddr.ParseIPPrefix(legacyPrefixField)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("failed to parse ip_prefix: %w", err))
|
||||||
|
}
|
||||||
|
parsedPrefixes = append(parsedPrefixes, legacyPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
for i, prefixInConfig := range configuredPrefixes {
|
for i, prefixInConfig := range configuredPrefixes {
|
||||||
prefix, err := netip.ParsePrefix(prefixInConfig)
|
prefix, err := netaddr.ParseIPPrefix(prefixInConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("failed to parse ip_prefixes[%d]: %w", i, err))
|
panic(fmt.Errorf("failed to parse ip_prefixes[%d]: %w", i, err))
|
||||||
}
|
}
|
||||||
parsedPrefixes = append(parsedPrefixes, prefix)
|
parsedPrefixes = append(parsedPrefixes, prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
prefixes := make([]netip.Prefix, 0, len(parsedPrefixes))
|
prefixes := make([]netaddr.IPPrefix, 0, len(parsedPrefixes))
|
||||||
{
|
{
|
||||||
// dedup
|
// dedup
|
||||||
normalizedPrefixes := make(map[string]int, len(parsedPrefixes))
|
normalizedPrefixes := make(map[string]int, len(parsedPrefixes))
|
||||||
for i, p := range parsedPrefixes {
|
for i, p := range parsedPrefixes {
|
||||||
normalized, _ := netipx.RangeOfPrefix(p).Prefix()
|
normalized, _ := p.Range().Prefix()
|
||||||
normalizedPrefixes[normalized.String()] = i
|
normalizedPrefixes[normalized.String()] = i
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,7 +469,7 @@ func GetHeadscaleConfig() (*Config, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(prefixes) < 1 {
|
if len(prefixes) < 1 {
|
||||||
prefixes = append(prefixes, netip.MustParsePrefix("100.64.0.0/10"))
|
prefixes = append(prefixes, netaddr.MustParseIPPrefix("100.64.0.0/10"))
|
||||||
log.Warn().
|
log.Warn().
|
||||||
Msgf("'ip_prefixes' not configured, falling back to default: %v", prefixes)
|
Msgf("'ip_prefixes' not configured, falling back to default: %v", prefixes)
|
||||||
}
|
}
|
||||||
@@ -513,14 +481,12 @@ func GetHeadscaleConfig() (*Config, error) {
|
|||||||
GRPCAddr: viper.GetString("grpc_listen_addr"),
|
GRPCAddr: viper.GetString("grpc_listen_addr"),
|
||||||
GRPCAllowInsecure: viper.GetBool("grpc_allow_insecure"),
|
GRPCAllowInsecure: viper.GetBool("grpc_allow_insecure"),
|
||||||
DisableUpdateCheck: viper.GetBool("disable_check_updates"),
|
DisableUpdateCheck: viper.GetBool("disable_check_updates"),
|
||||||
|
LogLevel: logLevel,
|
||||||
|
|
||||||
IPPrefixes: prefixes,
|
IPPrefixes: prefixes,
|
||||||
PrivateKeyPath: AbsolutePathFromConfigPath(
|
PrivateKeyPath: AbsolutePathFromConfigPath(
|
||||||
viper.GetString("private_key_path"),
|
viper.GetString("private_key_path"),
|
||||||
),
|
),
|
||||||
NoisePrivateKeyPath: AbsolutePathFromConfigPath(
|
|
||||||
viper.GetString("noise.private_key_path"),
|
|
||||||
),
|
|
||||||
BaseDomain: baseDomain,
|
BaseDomain: baseDomain,
|
||||||
|
|
||||||
DERP: derpConfig,
|
DERP: derpConfig,
|
||||||
@@ -540,7 +506,6 @@ func GetHeadscaleConfig() (*Config, error) {
|
|||||||
DBname: viper.GetString("db_name"),
|
DBname: viper.GetString("db_name"),
|
||||||
DBuser: viper.GetString("db_user"),
|
DBuser: viper.GetString("db_user"),
|
||||||
DBpass: viper.GetString("db_pass"),
|
DBpass: viper.GetString("db_pass"),
|
||||||
DBssl: viper.GetBool("db_ssl"),
|
|
||||||
|
|
||||||
TLS: GetTLSConfig(),
|
TLS: GetTLSConfig(),
|
||||||
|
|
||||||
@@ -553,9 +518,6 @@ func GetHeadscaleConfig() (*Config, error) {
|
|||||||
UnixSocketPermission: GetFileMode("unix_socket_permission"),
|
UnixSocketPermission: GetFileMode("unix_socket_permission"),
|
||||||
|
|
||||||
OIDC: OIDCConfig{
|
OIDC: OIDCConfig{
|
||||||
OnlyStartIfOIDCIsAvailable: viper.GetBool(
|
|
||||||
"oidc.only_start_if_oidc_is_available",
|
|
||||||
),
|
|
||||||
Issuer: viper.GetString("oidc.issuer"),
|
Issuer: viper.GetString("oidc.issuer"),
|
||||||
ClientID: viper.GetString("oidc.client_id"),
|
ClientID: viper.GetString("oidc.client_id"),
|
||||||
ClientSecret: viper.GetString("oidc.client_secret"),
|
ClientSecret: viper.GetString("oidc.client_secret"),
|
||||||
@@ -577,7 +539,5 @@ func GetHeadscaleConfig() (*Config, error) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
ACL: GetACLConfig(),
|
ACL: GetACLConfig(),
|
||||||
|
|
||||||
Log: GetLogConfig(),
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
19
db.go
19
db.go
@@ -6,7 +6,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/glebarez/sqlite"
|
"github.com/glebarez/sqlite"
|
||||||
@@ -14,6 +13,7 @@ import (
|
|||||||
"gorm.io/driver/postgres"
|
"gorm.io/driver/postgres"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/logger"
|
"gorm.io/gorm/logger"
|
||||||
|
"inet.af/netaddr"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -131,11 +131,6 @@ func (h *Headscale) initDB() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = db.AutoMigrate(&PreAuthKeyACLTag{})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = db.Migrator().DropTable("shared_machines")
|
_ = db.Migrator().DropTable("shared_machines")
|
||||||
|
|
||||||
err = db.AutoMigrate(&APIKey{})
|
err = db.AutoMigrate(&APIKey{})
|
||||||
@@ -226,8 +221,8 @@ func (h *Headscale) setValue(key string, value string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) pingDB(ctx context.Context) error {
|
func (h *Headscale) pingDB() error {
|
||||||
ctx, cancel := context.WithTimeout(ctx, time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
db, err := h.db.DB()
|
db, err := h.db.DB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -253,7 +248,7 @@ func (hi *HostInfo) Scan(destination interface{}) error {
|
|||||||
return json.Unmarshal([]byte(value), hi)
|
return json.Unmarshal([]byte(value), hi)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("%w: unexpected data type %T", ErrMachineAddressesInvalid, destination)
|
return fmt.Errorf("%w: unexpected data type %T", errMachineAddressesInvalid, destination)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,7 +259,7 @@ func (hi HostInfo) Value() (driver.Value, error) {
|
|||||||
return string(bytes), err
|
return string(bytes), err
|
||||||
}
|
}
|
||||||
|
|
||||||
type IPPrefixes []netip.Prefix
|
type IPPrefixes []netaddr.IPPrefix
|
||||||
|
|
||||||
func (i *IPPrefixes) Scan(destination interface{}) error {
|
func (i *IPPrefixes) Scan(destination interface{}) error {
|
||||||
switch value := destination.(type) {
|
switch value := destination.(type) {
|
||||||
@@ -275,7 +270,7 @@ func (i *IPPrefixes) Scan(destination interface{}) error {
|
|||||||
return json.Unmarshal([]byte(value), i)
|
return json.Unmarshal([]byte(value), i)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("%w: unexpected data type %T", ErrMachineAddressesInvalid, destination)
|
return fmt.Errorf("%w: unexpected data type %T", errMachineAddressesInvalid, destination)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,7 +292,7 @@ func (i *StringList) Scan(destination interface{}) error {
|
|||||||
return json.Unmarshal([]byte(value), i)
|
return json.Unmarshal([]byte(value), i)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("%w: unexpected data type %T", ErrMachineAddressesInvalid, destination)
|
return fmt.Errorf("%w: unexpected data type %T", errMachineAddressesInvalid, destination)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
5
derp.go
5
derp.go
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@@ -34,7 +35,7 @@ func loadDERPMapFromURL(addr url.URL) (*tailcfg.DERPMap, error) {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), HTTPReadTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), HTTPReadTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, addr.String(), nil)
|
req, err := http.NewRequestWithContext(ctx, "GET", addr.String(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -49,7 +50,7 @@ func loadDERPMapFromURL(addr url.URL) (*tailcfg.DERPMap, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -99,13 +98,10 @@ func (h *Headscale) DERPHandler(
|
|||||||
req *http.Request,
|
req *http.Request,
|
||||||
) {
|
) {
|
||||||
log.Trace().Caller().Msgf("/derp request from %v", req.RemoteAddr)
|
log.Trace().Caller().Msgf("/derp request from %v", req.RemoteAddr)
|
||||||
upgrade := strings.ToLower(req.Header.Get("Upgrade"))
|
up := strings.ToLower(req.Header.Get("Upgrade"))
|
||||||
|
if up != "websocket" && up != "derp" {
|
||||||
if upgrade != "websocket" && upgrade != "derp" {
|
if up != "" {
|
||||||
if upgrade != "" {
|
log.Warn().Caller().Msgf("Weird websockets connection upgrade: %q", up)
|
||||||
log.Warn().
|
|
||||||
Caller().
|
|
||||||
Msg("No Upgrade header in DERP server request. If headscale is behind a reverse proxy, make sure it is configured to pass WebSockets through.")
|
|
||||||
}
|
}
|
||||||
writer.Header().Set("Content-Type", "text/plain")
|
writer.Header().Set("Content-Type", "text/plain")
|
||||||
writer.WriteHeader(http.StatusUpgradeRequired)
|
writer.WriteHeader(http.StatusUpgradeRequired)
|
||||||
@@ -157,7 +153,7 @@ func (h *Headscale) DERPHandler(
|
|||||||
|
|
||||||
if !fastStart {
|
if !fastStart {
|
||||||
pubKey := h.privateKey.Public()
|
pubKey := h.privateKey.Public()
|
||||||
pubKeyStr := pubKey.UntypedHexString() //nolint
|
pubKeyStr := pubKey.UntypedHexString() // 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"+
|
||||||
@@ -167,7 +163,7 @@ func (h *Headscale) DERPHandler(
|
|||||||
pubKeyStr)
|
pubKeyStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
h.DERPServer.tailscaleDERP.Accept(req.Context(), netConn, conn, netConn.RemoteAddr().String())
|
h.DERPServer.tailscaleDERP.Accept(netConn, conn, netConn.RemoteAddr().String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// DERPProbeHandler is the endpoint that js/wasm clients hit to measure
|
// DERPProbeHandler is the endpoint that js/wasm clients hit to measure
|
||||||
@@ -177,7 +173,7 @@ func (h *Headscale) DERPProbeHandler(
|
|||||||
req *http.Request,
|
req *http.Request,
|
||||||
) {
|
) {
|
||||||
switch req.Method {
|
switch req.Method {
|
||||||
case http.MethodHead, http.MethodGet:
|
case "HEAD", "GET":
|
||||||
writer.Header().Set("Access-Control-Allow-Origin", "*")
|
writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
writer.WriteHeader(http.StatusOK)
|
writer.WriteHeader(http.StatusOK)
|
||||||
default:
|
default:
|
||||||
@@ -205,7 +201,7 @@ func (h *Headscale) DERPBootstrapDNSHandler(
|
|||||||
) {
|
) {
|
||||||
dnsEntries := make(map[string][]net.IP)
|
dnsEntries := make(map[string][]net.IP)
|
||||||
|
|
||||||
resolvCtx, cancel := context.WithTimeout(req.Context(), time.Minute)
|
resolvCtx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
var resolver net.Resolver
|
var resolver net.Resolver
|
||||||
for _, region := range h.DERPMap.Regions {
|
for _, region := range h.DERPMap.Regions {
|
||||||
@@ -280,8 +276,7 @@ func serverSTUNListener(ctx context.Context, packetConn *net.UDPConn) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
addr, _ := netip.AddrFromSlice(udpAddr.IP)
|
res := stun.Response(txid, udpAddr.IP, uint16(udpAddr.Port))
|
||||||
res := stun.Response(txid, netip.AddrPortFrom(addr, uint16(udpAddr.Port)))
|
|
||||||
_, err = packetConn.WriteTo(res, udpAddr)
|
_, err = packetConn.WriteTo(res, udpAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Trace().Caller().Err(err).Msgf("Issue writing to UDP")
|
log.Trace().Caller().Err(err).Msgf("Issue writing to UDP")
|
||||||
|
|||||||
21
dns.go
21
dns.go
@@ -2,11 +2,10 @@ package headscale
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
mapset "github.com/deckarep/golang-set/v2"
|
mapset "github.com/deckarep/golang-set/v2"
|
||||||
"go4.org/netipx"
|
"inet.af/netaddr"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/util/dnsname"
|
"tailscale.com/util/dnsname"
|
||||||
)
|
)
|
||||||
@@ -40,11 +39,11 @@ const (
|
|||||||
|
|
||||||
// From the netmask we can find out the wildcard bits (the bits that are not set in the netmask).
|
// From the netmask we can find out the wildcard bits (the bits that are not set in the netmask).
|
||||||
// This allows us to then calculate the subnets included in the subsequent class block and generate the entries.
|
// This allows us to then calculate the subnets included in the subsequent class block and generate the entries.
|
||||||
func generateMagicDNSRootDomains(ipPrefixes []netip.Prefix) []dnsname.FQDN {
|
func generateMagicDNSRootDomains(ipPrefixes []netaddr.IPPrefix) []dnsname.FQDN {
|
||||||
fqdns := make([]dnsname.FQDN, 0, len(ipPrefixes))
|
fqdns := make([]dnsname.FQDN, 0, len(ipPrefixes))
|
||||||
for _, ipPrefix := range ipPrefixes {
|
for _, ipPrefix := range ipPrefixes {
|
||||||
var generateDNSRoot func(netip.Prefix) []dnsname.FQDN
|
var generateDNSRoot func(netaddr.IPPrefix) []dnsname.FQDN
|
||||||
switch ipPrefix.Addr().BitLen() {
|
switch ipPrefix.IP().BitLen() {
|
||||||
case ipv4AddressLength:
|
case ipv4AddressLength:
|
||||||
generateDNSRoot = generateIPv4DNSRootDomain
|
generateDNSRoot = generateIPv4DNSRootDomain
|
||||||
|
|
||||||
@@ -55,7 +54,7 @@ func generateMagicDNSRootDomains(ipPrefixes []netip.Prefix) []dnsname.FQDN {
|
|||||||
panic(
|
panic(
|
||||||
fmt.Sprintf(
|
fmt.Sprintf(
|
||||||
"unsupported IP version with address length %d",
|
"unsupported IP version with address length %d",
|
||||||
ipPrefix.Addr().BitLen(),
|
ipPrefix.IP().BitLen(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -66,9 +65,9 @@ func generateMagicDNSRootDomains(ipPrefixes []netip.Prefix) []dnsname.FQDN {
|
|||||||
return fqdns
|
return fqdns
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateIPv4DNSRootDomain(ipPrefix netip.Prefix) []dnsname.FQDN {
|
func generateIPv4DNSRootDomain(ipPrefix netaddr.IPPrefix) []dnsname.FQDN {
|
||||||
// Conversion to the std lib net.IPnet, a bit easier to operate
|
// Conversion to the std lib net.IPnet, a bit easier to operate
|
||||||
netRange := netipx.PrefixIPNet(ipPrefix)
|
netRange := ipPrefix.IPNet()
|
||||||
maskBits, _ := netRange.Mask.Size()
|
maskBits, _ := netRange.Mask.Size()
|
||||||
|
|
||||||
// lastOctet is the last IP byte covered by the mask
|
// lastOctet is the last IP byte covered by the mask
|
||||||
@@ -102,11 +101,11 @@ func generateIPv4DNSRootDomain(ipPrefix netip.Prefix) []dnsname.FQDN {
|
|||||||
return fqdns
|
return fqdns
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateIPv6DNSRootDomain(ipPrefix netip.Prefix) []dnsname.FQDN {
|
func generateIPv6DNSRootDomain(ipPrefix netaddr.IPPrefix) []dnsname.FQDN {
|
||||||
const nibbleLen = 4
|
const nibbleLen = 4
|
||||||
|
|
||||||
maskBits, _ := netipx.PrefixIPNet(ipPrefix).Mask.Size()
|
maskBits, _ := ipPrefix.IPNet().Mask.Size()
|
||||||
expanded := ipPrefix.Addr().StringExpanded()
|
expanded := ipPrefix.IP().StringExpanded()
|
||||||
nibbleStr := strings.Map(func(r rune) rune {
|
nibbleStr := strings.Map(func(r rune) rune {
|
||||||
if r == ':' {
|
if r == ':' {
|
||||||
return -1
|
return -1
|
||||||
|
|||||||
42
dns_test.go
42
dns_test.go
@@ -2,16 +2,16 @@ package headscale
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
|
||||||
|
|
||||||
"gopkg.in/check.v1"
|
"gopkg.in/check.v1"
|
||||||
|
"inet.af/netaddr"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/dnstype"
|
"tailscale.com/types/dnstype"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Suite) TestMagicDNSRootDomains100(c *check.C) {
|
func (s *Suite) TestMagicDNSRootDomains100(c *check.C) {
|
||||||
prefixes := []netip.Prefix{
|
prefixes := []netaddr.IPPrefix{
|
||||||
netip.MustParsePrefix("100.64.0.0/10"),
|
netaddr.MustParseIPPrefix("100.64.0.0/10"),
|
||||||
}
|
}
|
||||||
domains := generateMagicDNSRootDomains(prefixes)
|
domains := generateMagicDNSRootDomains(prefixes)
|
||||||
|
|
||||||
@@ -47,8 +47,8 @@ func (s *Suite) TestMagicDNSRootDomains100(c *check.C) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Suite) TestMagicDNSRootDomains172(c *check.C) {
|
func (s *Suite) TestMagicDNSRootDomains172(c *check.C) {
|
||||||
prefixes := []netip.Prefix{
|
prefixes := []netaddr.IPPrefix{
|
||||||
netip.MustParsePrefix("172.16.0.0/16"),
|
netaddr.MustParseIPPrefix("172.16.0.0/16"),
|
||||||
}
|
}
|
||||||
domains := generateMagicDNSRootDomains(prefixes)
|
domains := generateMagicDNSRootDomains(prefixes)
|
||||||
|
|
||||||
@@ -75,8 +75,8 @@ func (s *Suite) TestMagicDNSRootDomains172(c *check.C) {
|
|||||||
|
|
||||||
// Happens when netmask is a multiple of 4 bits (sounds likely).
|
// Happens when netmask is a multiple of 4 bits (sounds likely).
|
||||||
func (s *Suite) TestMagicDNSRootDomainsIPv6Single(c *check.C) {
|
func (s *Suite) TestMagicDNSRootDomainsIPv6Single(c *check.C) {
|
||||||
prefixes := []netip.Prefix{
|
prefixes := []netaddr.IPPrefix{
|
||||||
netip.MustParsePrefix("fd7a:115c:a1e0::/48"),
|
netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/48"),
|
||||||
}
|
}
|
||||||
domains := generateMagicDNSRootDomains(prefixes)
|
domains := generateMagicDNSRootDomains(prefixes)
|
||||||
|
|
||||||
@@ -89,8 +89,8 @@ func (s *Suite) TestMagicDNSRootDomainsIPv6Single(c *check.C) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Suite) TestMagicDNSRootDomainsIPv6SingleMultiple(c *check.C) {
|
func (s *Suite) TestMagicDNSRootDomainsIPv6SingleMultiple(c *check.C) {
|
||||||
prefixes := []netip.Prefix{
|
prefixes := []netaddr.IPPrefix{
|
||||||
netip.MustParsePrefix("fd7a:115c:a1e0::/50"),
|
netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/50"),
|
||||||
}
|
}
|
||||||
domains := generateMagicDNSRootDomains(prefixes)
|
domains := generateMagicDNSRootDomains(prefixes)
|
||||||
|
|
||||||
@@ -126,7 +126,6 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
nil,
|
nil,
|
||||||
nil,
|
|
||||||
)
|
)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
@@ -135,7 +134,6 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
nil,
|
nil,
|
||||||
nil,
|
|
||||||
)
|
)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
@@ -144,7 +142,6 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
nil,
|
nil,
|
||||||
nil,
|
|
||||||
)
|
)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
@@ -153,7 +150,6 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
nil,
|
nil,
|
||||||
nil,
|
|
||||||
)
|
)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
@@ -169,7 +165,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
|
|||||||
NamespaceID: namespaceShared1.ID,
|
NamespaceID: namespaceShared1.ID,
|
||||||
Namespace: *namespaceShared1,
|
Namespace: *namespaceShared1,
|
||||||
RegisterMethod: RegisterMethodAuthKey,
|
RegisterMethod: RegisterMethodAuthKey,
|
||||||
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")},
|
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")},
|
||||||
AuthKeyID: uint(preAuthKeyInShared1.ID),
|
AuthKeyID: uint(preAuthKeyInShared1.ID),
|
||||||
}
|
}
|
||||||
app.db.Save(machineInShared1)
|
app.db.Save(machineInShared1)
|
||||||
@@ -186,7 +182,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
|
|||||||
NamespaceID: namespaceShared2.ID,
|
NamespaceID: namespaceShared2.ID,
|
||||||
Namespace: *namespaceShared2,
|
Namespace: *namespaceShared2,
|
||||||
RegisterMethod: RegisterMethodAuthKey,
|
RegisterMethod: RegisterMethodAuthKey,
|
||||||
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.2")},
|
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.2")},
|
||||||
AuthKeyID: uint(preAuthKeyInShared2.ID),
|
AuthKeyID: uint(preAuthKeyInShared2.ID),
|
||||||
}
|
}
|
||||||
app.db.Save(machineInShared2)
|
app.db.Save(machineInShared2)
|
||||||
@@ -203,7 +199,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
|
|||||||
NamespaceID: namespaceShared3.ID,
|
NamespaceID: namespaceShared3.ID,
|
||||||
Namespace: *namespaceShared3,
|
Namespace: *namespaceShared3,
|
||||||
RegisterMethod: RegisterMethodAuthKey,
|
RegisterMethod: RegisterMethodAuthKey,
|
||||||
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.3")},
|
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.3")},
|
||||||
AuthKeyID: uint(preAuthKeyInShared3.ID),
|
AuthKeyID: uint(preAuthKeyInShared3.ID),
|
||||||
}
|
}
|
||||||
app.db.Save(machineInShared3)
|
app.db.Save(machineInShared3)
|
||||||
@@ -220,7 +216,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
|
|||||||
NamespaceID: namespaceShared1.ID,
|
NamespaceID: namespaceShared1.ID,
|
||||||
Namespace: *namespaceShared1,
|
Namespace: *namespaceShared1,
|
||||||
RegisterMethod: RegisterMethodAuthKey,
|
RegisterMethod: RegisterMethodAuthKey,
|
||||||
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.4")},
|
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")},
|
||||||
AuthKeyID: uint(PreAuthKey2InShared1.ID),
|
AuthKeyID: uint(PreAuthKey2InShared1.ID),
|
||||||
}
|
}
|
||||||
app.db.Save(machine2InShared1)
|
app.db.Save(machine2InShared1)
|
||||||
@@ -273,7 +269,6 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
nil,
|
nil,
|
||||||
nil,
|
|
||||||
)
|
)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
@@ -282,7 +277,6 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
nil,
|
nil,
|
||||||
nil,
|
|
||||||
)
|
)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
@@ -291,7 +285,6 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
nil,
|
nil,
|
||||||
nil,
|
|
||||||
)
|
)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
@@ -300,7 +293,6 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
nil,
|
nil,
|
||||||
nil,
|
|
||||||
)
|
)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
@@ -316,7 +308,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
|
|||||||
NamespaceID: namespaceShared1.ID,
|
NamespaceID: namespaceShared1.ID,
|
||||||
Namespace: *namespaceShared1,
|
Namespace: *namespaceShared1,
|
||||||
RegisterMethod: RegisterMethodAuthKey,
|
RegisterMethod: RegisterMethodAuthKey,
|
||||||
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")},
|
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")},
|
||||||
AuthKeyID: uint(preAuthKeyInShared1.ID),
|
AuthKeyID: uint(preAuthKeyInShared1.ID),
|
||||||
}
|
}
|
||||||
app.db.Save(machineInShared1)
|
app.db.Save(machineInShared1)
|
||||||
@@ -333,7 +325,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
|
|||||||
NamespaceID: namespaceShared2.ID,
|
NamespaceID: namespaceShared2.ID,
|
||||||
Namespace: *namespaceShared2,
|
Namespace: *namespaceShared2,
|
||||||
RegisterMethod: RegisterMethodAuthKey,
|
RegisterMethod: RegisterMethodAuthKey,
|
||||||
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.2")},
|
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.2")},
|
||||||
AuthKeyID: uint(preAuthKeyInShared2.ID),
|
AuthKeyID: uint(preAuthKeyInShared2.ID),
|
||||||
}
|
}
|
||||||
app.db.Save(machineInShared2)
|
app.db.Save(machineInShared2)
|
||||||
@@ -350,7 +342,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
|
|||||||
NamespaceID: namespaceShared3.ID,
|
NamespaceID: namespaceShared3.ID,
|
||||||
Namespace: *namespaceShared3,
|
Namespace: *namespaceShared3,
|
||||||
RegisterMethod: RegisterMethodAuthKey,
|
RegisterMethod: RegisterMethodAuthKey,
|
||||||
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.3")},
|
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.3")},
|
||||||
AuthKeyID: uint(preAuthKeyInShared3.ID),
|
AuthKeyID: uint(preAuthKeyInShared3.ID),
|
||||||
}
|
}
|
||||||
app.db.Save(machineInShared3)
|
app.db.Save(machineInShared3)
|
||||||
@@ -367,7 +359,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
|
|||||||
NamespaceID: namespaceShared1.ID,
|
NamespaceID: namespaceShared1.ID,
|
||||||
Namespace: *namespaceShared1,
|
Namespace: *namespaceShared1,
|
||||||
RegisterMethod: RegisterMethodAuthKey,
|
RegisterMethod: RegisterMethodAuthKey,
|
||||||
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.4")},
|
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")},
|
||||||
AuthKeyID: uint(preAuthKey2InShared1.ID),
|
AuthKeyID: uint(preAuthKey2InShared1.ID),
|
||||||
}
|
}
|
||||||
app.db.Save(machine2InShared1)
|
app.db.Save(machine2InShared1)
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ written by community members. It is _not_ verified by `headscale` developers.
|
|||||||
|
|
||||||
- [Running headscale in a container](running-headscale-container.md)
|
- [Running headscale in a container](running-headscale-container.md)
|
||||||
- [Running headscale on OpenBSD](running-headscale-openbsd.md)
|
- [Running headscale on OpenBSD](running-headscale-openbsd.md)
|
||||||
- [Running headscale behind a reverse proxy](reverse-proxy.md)
|
|
||||||
|
|
||||||
## Misc
|
## Misc
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ ACLs could be written either on [huJSON](https://github.com/tailscale/hujson)
|
|||||||
or YAML. Check the [test ACLs](../tests/acls) for further information.
|
or YAML. Check the [test ACLs](../tests/acls) for further information.
|
||||||
|
|
||||||
When registering the servers we will need to add the flag
|
When registering the servers we will need to add the flag
|
||||||
`--advertise-tags=tag:<tag1>,tag:<tag2>`, and the user (namespace) that is
|
`--advertised-tags=tag:<tag1>,tag:<tag2>`, and the user (namespace) that is
|
||||||
registering the server should be allowed to do it. Since anyone can add tags to
|
registering the server should be allowed to do it. Since anyone can add tags to
|
||||||
a server they can register, the check of the tags is done on headscale server
|
a server they can register, the check of the tags is done on headscale server
|
||||||
and only valid tags are applied. A tag is valid if the namespace that is
|
and only valid tags are applied. A tag is valid if the namespace that is
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
# Connecting an Android client
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
This documentation has the goal of showing how a user can use the official Android [Tailscale](https://tailscale.com) client with `headscale`.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
Install the official Tailscale Android client from the [Google Play Store](https://play.google.com/store/apps/details?id=com.tailscale.ipn) or [F-Droid](https://f-droid.org/packages/com.tailscale.ipn/).
|
|
||||||
|
|
||||||
Ensure that the installed version is at least 1.30.0, as that is the first release to support custom URLs.
|
|
||||||
|
|
||||||
## Configuring the headscale URL
|
|
||||||
|
|
||||||
After opening the app, the kebab menu icon (three dots) on the top bar on the right must be repeatedly opened and closed until the _Change server_ option appears in the menu. This is where you can enter your headscale URL.
|
|
||||||
|
|
||||||
A screen recording of this process can be seen in the `tailscale-android` PR which implemented this functionality: <https://github.com/tailscale/tailscale-android/pull/55>
|
|
||||||
|
|
||||||
After saving and restarting the app, selecting the regular _Sign in_ option (non-SSO) should open up the headscale authentication page.
|
|
||||||
32
docs/build-headscale-container.md
Normal file
32
docs/build-headscale-container.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Build docker from scratch
|
||||||
|
|
||||||
|
The Dockerfiles included in the repository are using the [buildx plugin](https://docs.docker.com/buildx/working-with-buildx/). This plugin is includes in docker newer than Docker-ce CLI 19.03.2. The plugin is used to be able to build different container arches. Building the Dockerfiles without buildx is not possible.
|
||||||
|
|
||||||
|
# Build native
|
||||||
|
|
||||||
|
To build the container on the native arch you can just use:
|
||||||
|
```
|
||||||
|
$ sudo docker buildx build -t headscale:custom-arch .
|
||||||
|
```
|
||||||
|
|
||||||
|
For example: This will build a amd64(x86_64) container if your hostsystem is amd64(x86_64). Or a arm64 container on a arm64 hostsystem (raspberry pi4).
|
||||||
|
|
||||||
|
# Build cross platform
|
||||||
|
|
||||||
|
To build a arm64 container on a amd64 hostsystem you could use:
|
||||||
|
```
|
||||||
|
$ sudo docker buildx build --platform linux/arm64 -t headscale:custom-arm64 .
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
**Import: Currently arm32 build are not supported as there is a problem with a library used by headscale. Hopefully this will be fixed soon.**
|
||||||
|
|
||||||
|
# Build multiple arches
|
||||||
|
|
||||||
|
To build multiple archres you could use:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ sudo docker buildx create --use
|
||||||
|
$ sudo docker buildx build --platform linux/amd64,linux/arm64 .
|
||||||
|
|
||||||
|
```
|
||||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 34 KiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 49 KiB |
@@ -1,100 +0,0 @@
|
|||||||
# Running headscale behind a reverse proxy
|
|
||||||
|
|
||||||
Running headscale behind a reverse proxy is useful when running multiple applications on the same server, and you want to reuse the same external IP and port - usually tcp/443 for HTTPS.
|
|
||||||
|
|
||||||
### WebSockets
|
|
||||||
|
|
||||||
The reverse proxy MUST be configured to support WebSockets, as it is needed for clients running Tailscale v1.30+.
|
|
||||||
|
|
||||||
WebSockets support is required when using the headscale embedded DERP server. In this case, you will also need to expose the UDP port used for STUN (by default, udp/3478). Please check our [config-example.yaml](https://github.com/juanfont/headscale/blob/main/config-example.yaml).
|
|
||||||
|
|
||||||
### TLS
|
|
||||||
|
|
||||||
Headscale can be configured not to use TLS, leaving it to the reverse proxy to handle. Add the following configuration values to your headscale config file.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
server_url: https://<YOUR_SERVER_NAME> # This should be the FQDN at which headscale will be served
|
|
||||||
listen_addr: 0.0.0.0:8080
|
|
||||||
metrics_listen_addr: 0.0.0.0:9090
|
|
||||||
tls_cert_path: ""
|
|
||||||
tls_key_path: ""
|
|
||||||
```
|
|
||||||
|
|
||||||
## nginx
|
|
||||||
|
|
||||||
The following example configuration can be used in your nginx setup, substituting values as necessary. `<IP:PORT>` should be the IP address and port where headscale is running. In most cases, this will be `http://localhost:8080`.
|
|
||||||
|
|
||||||
```Nginx
|
|
||||||
map $http_upgrade $connection_upgrade {
|
|
||||||
default keep-alive;
|
|
||||||
'websocket' upgrade;
|
|
||||||
'' close;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
listen [::]:80;
|
|
||||||
|
|
||||||
listen 443 ssl http2;
|
|
||||||
listen [::]:443 ssl http2;
|
|
||||||
|
|
||||||
server_name <YOUR_SERVER_NAME>;
|
|
||||||
|
|
||||||
ssl_certificate <PATH_TO_CERT>;
|
|
||||||
ssl_certificate_key <PATH_CERT_KEY>;
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://<IP:PORT>;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection $connection_upgrade;
|
|
||||||
proxy_set_header Host $server_name;
|
|
||||||
proxy_redirect http:// https://;
|
|
||||||
proxy_buffering off;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
|
|
||||||
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## istio/envoy
|
|
||||||
|
|
||||||
If you using [Istio](https://istio.io/) ingressgateway or [Envoy](https://www.envoyproxy.io/) as reverse proxy, there are some tips for you. If not set, you may see some debug log in proxy as below:
|
|
||||||
|
|
||||||
```log
|
|
||||||
Sending local reply with details upgrade_failed
|
|
||||||
```
|
|
||||||
|
|
||||||
### Envoy
|
|
||||||
|
|
||||||
You need add a new upgrade_type named `tailscale-control-protocol`. [see detail](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto#extensions-filters-network-http-connection-manager-v3-httpconnectionmanager-upgradeconfig)
|
|
||||||
|
|
||||||
### Istio
|
|
||||||
|
|
||||||
Same as envoy, we can use `EnvoyFilter` to add upgrade_type.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
apiVersion: networking.istio.io/v1alpha3
|
|
||||||
kind: EnvoyFilter
|
|
||||||
metadata:
|
|
||||||
name: headscale-behind-istio-ingress
|
|
||||||
namespace: istio-system
|
|
||||||
spec:
|
|
||||||
configPatches:
|
|
||||||
- applyTo: NETWORK_FILTER
|
|
||||||
match:
|
|
||||||
listener:
|
|
||||||
filterChain:
|
|
||||||
filter:
|
|
||||||
name: envoy.filters.network.http_connection_manager
|
|
||||||
patch:
|
|
||||||
operation: MERGE
|
|
||||||
value:
|
|
||||||
typed_config:
|
|
||||||
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
|
|
||||||
upgrade_configs:
|
|
||||||
- upgrade_type: tailscale-control-protocol
|
|
||||||
```
|
|
||||||
@@ -48,15 +48,11 @@ Modify the config file to your preferences before launching Docker container.
|
|||||||
Here are some settings that you likely want:
|
Here are some settings that you likely want:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# Change to your hostname or host IP
|
server_url: http://your-host-name:8080 # Change to your hostname or host IP
|
||||||
server_url: http://your-host-name:8080
|
|
||||||
# Listen to 0.0.0.0 so it's accessible outside the container
|
# Listen to 0.0.0.0 so it's accessible outside the container
|
||||||
metrics_listen_addr: 0.0.0.0:9090
|
metrics_listen_addr: 0.0.0.0:9090
|
||||||
# The default /var/lib/headscale path is not writable in the container
|
# The default /var/lib/headscale path is not writable in the container
|
||||||
private_key_path: /etc/headscale/private.key
|
private_key_path: /etc/headscale/private.key
|
||||||
# The default /var/lib/headscale path is not writable in the container
|
|
||||||
noise:
|
|
||||||
private_key_path: /etc/headscale/noise_private.key
|
|
||||||
# The default /var/lib/headscale path is not writable in the container
|
# The default /var/lib/headscale path is not writable in the container
|
||||||
db_path: /etc/headscale/db.sqlite
|
db_path: /etc/headscale/db.sqlite
|
||||||
```
|
```
|
||||||
@@ -67,6 +63,7 @@ db_path: /etc/headscale/db.sqlite
|
|||||||
docker run \
|
docker run \
|
||||||
--name headscale \
|
--name headscale \
|
||||||
--detach \
|
--detach \
|
||||||
|
--rm \
|
||||||
--volume $(pwd)/config:/etc/headscale/ \
|
--volume $(pwd)/config:/etc/headscale/ \
|
||||||
--publish 127.0.0.1:8080:8080 \
|
--publish 127.0.0.1:8080:8080 \
|
||||||
--publish 127.0.0.1:9090:9090 \
|
--publish 127.0.0.1:9090:9090 \
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ describing how to make `headscale` run properly in a server environment.
|
|||||||
|
|
||||||
```shell
|
```shell
|
||||||
# Install prerequistes
|
# Install prerequistes
|
||||||
# 1. go v1.19+: headscale newer than 0.17 needs go 1.19+ to compile
|
# 1. go v1.18+: headscale newer than 0.15 needs go 1.18+ to compile
|
||||||
# 2. gmake: Makefile in the headscale repo is written in GNU make syntax
|
# 2. gmake: Makefile in the headscale repo is written in GNU make syntax
|
||||||
pkg_add -D snap go
|
pkg_add -D snap go
|
||||||
pkg_add gmake
|
pkg_add gmake
|
||||||
@@ -46,7 +46,7 @@ cp headscale /usr/local/sbin
|
|||||||
|
|
||||||
```shell
|
```shell
|
||||||
# Install prerequistes
|
# Install prerequistes
|
||||||
# 1. go v1.19+: headscale newer than 0.17 needs go 1.19+ to compile
|
# 1. go v1.18+: headscale newer than 0.15 needs go 1.18+ to compile
|
||||||
# 2. gmake: Makefile in the headscale repo is written in GNU make syntax
|
# 2. gmake: Makefile in the headscale repo is written in GNU make syntax
|
||||||
|
|
||||||
git clone https://github.com/juanfont/headscale.git
|
git clone https://github.com/juanfont/headscale.git
|
||||||
|
|||||||
14
flake.lock
generated
14
flake.lock
generated
@@ -2,11 +2,11 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"flake-utils": {
|
"flake-utils": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1659877975,
|
"lastModified": 1653893745,
|
||||||
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
|
"narHash": "sha256-0jntwV3Z8//YwuOjzhV2sgJJPt+HY6KhU7VZUL0fKZQ=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "flake-utils",
|
"repo": "flake-utils",
|
||||||
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
|
"rev": "1ed9fb1935d260de5fe1c2f7ee0ebaae17ed2fa1",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -17,16 +17,16 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1666869603,
|
"lastModified": 1654847188,
|
||||||
"narHash": "sha256-3V53or4Vpu4+LrGfGSh3T2V8+qf5RP6nRuex9GywkwE=",
|
"narHash": "sha256-MC+eP7XOGE1LAswOPqdcGoUqY9mEQ3ZaaxamVTbc0hM=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "2001e2b31c565bcdf7bc13062b8d7cfccaca05b8",
|
"rev": "8b66e3f2ebcc644b78cec9d6f152192f4e7d322f",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"ref": "nixos-unstable",
|
"ref": "nixos-22.05",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
|||||||
301
flake.nix
301
flake.nix
@@ -2,178 +2,167 @@
|
|||||||
description = "headscale - Open Source Tailscale Control server";
|
description = "headscale - Open Source Tailscale Control server";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-22.05";
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = {
|
outputs = { self, nixpkgs, flake-utils, ... }:
|
||||||
self,
|
let
|
||||||
nixpkgs,
|
headscaleVersion = if (self ? shortRev) then self.shortRev else "dev";
|
||||||
flake-utils,
|
in
|
||||||
...
|
|
||||||
}: let
|
|
||||||
headscaleVersion =
|
|
||||||
if (self ? shortRev)
|
|
||||||
then self.shortRev
|
|
||||||
else "dev";
|
|
||||||
in
|
|
||||||
{
|
{
|
||||||
overlay = _: prev: let
|
overlay = final: prev:
|
||||||
pkgs = nixpkgs.legacyPackages.${prev.system};
|
let
|
||||||
in rec {
|
pkgs = nixpkgs.legacyPackages.${prev.system};
|
||||||
headscale = pkgs.buildGo119Module rec {
|
in
|
||||||
pname = "headscale";
|
rec {
|
||||||
version = headscaleVersion;
|
headscale =
|
||||||
src = pkgs.lib.cleanSource self;
|
pkgs.buildGo118Module rec {
|
||||||
|
pname = "headscale";
|
||||||
|
version = headscaleVersion;
|
||||||
|
src = pkgs.lib.cleanSource self;
|
||||||
|
|
||||||
tags = ["ts2019"];
|
# 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.
|
||||||
|
vendorSha256 = "sha256-s1hzT07uqKrwOSrx9cKUbrP1PnTxQaRSoc8p9PgxLYg=";
|
||||||
|
|
||||||
# Only run unit tests when testing a build
|
ldflags = [ "-s" "-w" "-X github.com/juanfont/headscale/cmd/headscale/cli.Version=v${version}" ];
|
||||||
checkFlags = ["-short"];
|
};
|
||||||
|
|
||||||
# When updating go.mod or go.sum, a new sha will need to be calculated,
|
golines =
|
||||||
# update this if you have a mismatch after doing a change to thos files.
|
pkgs.buildGoModule rec {
|
||||||
vendorSha256 = "sha256-Cq0WipTQ+kGcvnfP0kjyvjyonl2OC9W7Tj0MCuB1lDU=";
|
pname = "golines";
|
||||||
|
version = "0.9.0";
|
||||||
|
|
||||||
ldflags = ["-s" "-w" "-X github.com/juanfont/headscale/cmd/headscale/cli.Version=v${version}"];
|
src = pkgs.fetchFromGitHub {
|
||||||
};
|
owner = "segmentio";
|
||||||
|
repo = "golines";
|
||||||
|
rev = "v${version}";
|
||||||
|
sha256 = "sha256-BUXEg+4r9L/gqe4DhTlhN55P3jWt7ZyWFQycO6QePrw=";
|
||||||
|
};
|
||||||
|
|
||||||
golines = pkgs.buildGoModule rec {
|
vendorSha256 = "sha256-sEzWUeVk5GB0H41wrp12P8sBWRjg0FHUX6ABDEEBqK8=";
|
||||||
pname = "golines";
|
|
||||||
version = "0.11.0";
|
|
||||||
|
|
||||||
src = pkgs.fetchFromGitHub {
|
nativeBuildInputs = [ pkgs.installShellFiles ];
|
||||||
owner = "segmentio";
|
};
|
||||||
repo = "golines";
|
|
||||||
rev = "v${version}";
|
golangci-lint = prev.golangci-lint.override {
|
||||||
sha256 = "sha256-2K9KAg8iSubiTbujyFGN3yggrL+EDyeUCs9OOta/19A=";
|
# Override https://github.com/NixOS/nixpkgs/pull/166801 which changed this
|
||||||
|
# to buildGo118Module because it does not build on Darwin.
|
||||||
|
inherit (prev) buildGoModule;
|
||||||
};
|
};
|
||||||
|
|
||||||
vendorSha256 = "sha256-rxYuzn4ezAxaeDhxd8qdOzt+CKYIh03A9zKNdzILq18=";
|
# golangci-lint =
|
||||||
|
# pkgs.buildGo117Module rec {
|
||||||
|
# pname = "golangci-lint";
|
||||||
|
# version = "1.46.2";
|
||||||
|
#
|
||||||
|
# src = pkgs.fetchFromGitHub {
|
||||||
|
# owner = "golangci";
|
||||||
|
# repo = "golangci-lint";
|
||||||
|
# rev = "v${version}";
|
||||||
|
# sha256 = "sha256-7sDAwWz+qoB/ngeH35tsJ5FZUfAQvQsU6kU9rUHIHMk=";
|
||||||
|
# };
|
||||||
|
#
|
||||||
|
# vendorSha256 = "sha256-w38OKN6HPoz37utG/2QSPMai55IRDXCIIymeMe6ogIU=";
|
||||||
|
#
|
||||||
|
# nativeBuildInputs = [ pkgs.installShellFiles ];
|
||||||
|
# };
|
||||||
|
|
||||||
nativeBuildInputs = [pkgs.installShellFiles];
|
protoc-gen-grpc-gateway =
|
||||||
|
pkgs.buildGoModule rec {
|
||||||
|
pname = "grpc-gateway";
|
||||||
|
version = "2.8.0";
|
||||||
|
|
||||||
|
src = pkgs.fetchFromGitHub {
|
||||||
|
owner = "grpc-ecosystem";
|
||||||
|
repo = "grpc-gateway";
|
||||||
|
rev = "v${version}";
|
||||||
|
sha256 = "sha256-8eBBBYJ+tBjB2fgPMX/ZlbN3eeS75e8TAZYOKXs6hcg=";
|
||||||
|
};
|
||||||
|
|
||||||
|
vendorSha256 = "sha256-AW2Gn/mlZyLMwF+NpK59eiOmQrYWW/9HPjbunYc9Ij4=";
|
||||||
|
|
||||||
|
nativeBuildInputs = [ pkgs.installShellFiles ];
|
||||||
|
|
||||||
|
subPackages = [ "protoc-gen-grpc-gateway" "protoc-gen-openapiv2" ];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
} // flake-utils.lib.eachDefaultSystem
|
||||||
golangci-lint = prev.golangci-lint.override {
|
(system:
|
||||||
# Override https://github.com/NixOS/nixpkgs/pull/166801 which changed this
|
let
|
||||||
# to buildGo118Module because it does not build on Darwin.
|
pkgs = import nixpkgs {
|
||||||
inherit (prev) buildGoModule;
|
overlays = [ self.overlay ];
|
||||||
};
|
inherit system;
|
||||||
|
|
||||||
# golangci-lint =
|
|
||||||
# pkgs.buildGo117Module rec {
|
|
||||||
# pname = "golangci-lint";
|
|
||||||
# version = "1.46.2";
|
|
||||||
#
|
|
||||||
# src = pkgs.fetchFromGitHub {
|
|
||||||
# owner = "golangci";
|
|
||||||
# repo = "golangci-lint";
|
|
||||||
# rev = "v${version}";
|
|
||||||
# sha256 = "sha256-7sDAwWz+qoB/ngeH35tsJ5FZUfAQvQsU6kU9rUHIHMk=";
|
|
||||||
# };
|
|
||||||
#
|
|
||||||
# vendorSha256 = "sha256-w38OKN6HPoz37utG/2QSPMai55IRDXCIIymeMe6ogIU=";
|
|
||||||
#
|
|
||||||
# nativeBuildInputs = [ pkgs.installShellFiles ];
|
|
||||||
# };
|
|
||||||
|
|
||||||
protoc-gen-grpc-gateway = pkgs.buildGoModule rec {
|
|
||||||
pname = "grpc-gateway";
|
|
||||||
version = "2.8.0";
|
|
||||||
|
|
||||||
src = pkgs.fetchFromGitHub {
|
|
||||||
owner = "grpc-ecosystem";
|
|
||||||
repo = "grpc-gateway";
|
|
||||||
rev = "v${version}";
|
|
||||||
sha256 = "sha256-8eBBBYJ+tBjB2fgPMX/ZlbN3eeS75e8TAZYOKXs6hcg=";
|
|
||||||
};
|
};
|
||||||
|
buildDeps = with pkgs; [ git go_1_18 gnumake ];
|
||||||
vendorSha256 = "sha256-AW2Gn/mlZyLMwF+NpK59eiOmQrYWW/9HPjbunYc9Ij4=";
|
devDeps = with pkgs;
|
||||||
|
buildDeps ++ [
|
||||||
nativeBuildInputs = [pkgs.installShellFiles];
|
|
||||||
|
|
||||||
subPackages = ["protoc-gen-grpc-gateway" "protoc-gen-openapiv2"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// flake-utils.lib.eachDefaultSystem
|
|
||||||
(system: let
|
|
||||||
pkgs = import nixpkgs {
|
|
||||||
overlays = [self.overlay];
|
|
||||||
inherit system;
|
|
||||||
};
|
|
||||||
buildDeps = with pkgs; [git go_1_19 gnumake];
|
|
||||||
devDeps = with pkgs;
|
|
||||||
buildDeps
|
|
||||||
++ [
|
|
||||||
golangci-lint
|
|
||||||
golines
|
|
||||||
nodePackages.prettier
|
|
||||||
|
|
||||||
# Protobuf dependencies
|
|
||||||
protobuf
|
|
||||||
protoc-gen-go
|
|
||||||
protoc-gen-go-grpc
|
|
||||||
protoc-gen-grpc-gateway
|
|
||||||
buf
|
|
||||||
clang-tools # clang-format
|
|
||||||
];
|
|
||||||
|
|
||||||
# Add entry to build a docker image with headscale
|
|
||||||
# caveat: only works on Linux
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# nix build .#headscale-docker
|
|
||||||
# docker load < result
|
|
||||||
headscale-docker = pkgs.dockerTools.buildLayeredImage {
|
|
||||||
name = "headscale";
|
|
||||||
tag = headscaleVersion;
|
|
||||||
contents = [pkgs.headscale];
|
|
||||||
config.Entrypoint = [(pkgs.headscale + "/bin/headscale")];
|
|
||||||
};
|
|
||||||
in rec {
|
|
||||||
# `nix develop`
|
|
||||||
devShell = pkgs.mkShell {
|
|
||||||
buildInputs = devDeps;
|
|
||||||
|
|
||||||
shellHook = ''
|
|
||||||
export GOFLAGS=-tags="ts2019"
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
# `nix build`
|
|
||||||
packages = with pkgs; {
|
|
||||||
inherit headscale;
|
|
||||||
inherit headscale-docker;
|
|
||||||
};
|
|
||||||
|
|
||||||
defaultPackage = pkgs.headscale;
|
|
||||||
|
|
||||||
# `nix run`
|
|
||||||
apps.headscale = flake-utils.lib.mkApp {
|
|
||||||
drv = packages.headscale;
|
|
||||||
};
|
|
||||||
defaultApp = apps.headscale;
|
|
||||||
|
|
||||||
checks = {
|
|
||||||
format =
|
|
||||||
pkgs.runCommand "check-format"
|
|
||||||
{
|
|
||||||
buildInputs = with pkgs; [
|
|
||||||
gnumake
|
|
||||||
nixpkgs-fmt
|
|
||||||
golangci-lint
|
golangci-lint
|
||||||
nodePackages.prettier
|
|
||||||
golines
|
golines
|
||||||
clang-tools
|
nodePackages.prettier
|
||||||
|
|
||||||
|
# Protobuf dependencies
|
||||||
|
protobuf
|
||||||
|
protoc-gen-go
|
||||||
|
protoc-gen-go-grpc
|
||||||
|
protoc-gen-grpc-gateway
|
||||||
|
buf
|
||||||
|
clang-tools # clang-format
|
||||||
];
|
];
|
||||||
} ''
|
|
||||||
${pkgs.nixpkgs-fmt}/bin/nixpkgs-fmt ${./.}
|
|
||||||
${pkgs.golangci-lint}/bin/golangci-lint run --fix --timeout 10m
|
# Add entry to build a docker image with headscale
|
||||||
${pkgs.nodePackages.prettier}/bin/prettier --write '**/**.{ts,js,md,yaml,yml,sass,css,scss,html}'
|
# caveat: only works on Linux
|
||||||
${pkgs.golines}/bin/golines --max-len=88 --base-formatter=gofumpt -w ${./.}
|
#
|
||||||
${pkgs.clang-tools}/bin/clang-format -style="{BasedOnStyle: Google, IndentWidth: 4, AlignConsecutiveDeclarations: true, AlignConsecutiveAssignments: true, ColumnLimit: 0}" -i ${./.}
|
# Usage:
|
||||||
'';
|
# nix build .#headscale-docker
|
||||||
};
|
# docker load < result
|
||||||
});
|
headscale-docker = pkgs.dockerTools.buildLayeredImage {
|
||||||
|
name = "headscale";
|
||||||
|
tag = headscaleVersion;
|
||||||
|
contents = [ pkgs.headscale ];
|
||||||
|
config.Entrypoint = [ (pkgs.headscale + "/bin/headscale") ];
|
||||||
|
};
|
||||||
|
in
|
||||||
|
rec {
|
||||||
|
# `nix develop`
|
||||||
|
devShell = pkgs.mkShell { buildInputs = devDeps; };
|
||||||
|
|
||||||
|
# `nix build`
|
||||||
|
packages = with pkgs; {
|
||||||
|
inherit headscale;
|
||||||
|
inherit headscale-docker;
|
||||||
|
};
|
||||||
|
|
||||||
|
defaultPackage = pkgs.headscale;
|
||||||
|
|
||||||
|
# `nix run`
|
||||||
|
apps.headscale = flake-utils.lib.mkApp {
|
||||||
|
drv = packages.headscale;
|
||||||
|
};
|
||||||
|
defaultApp = apps.headscale;
|
||||||
|
|
||||||
|
checks = {
|
||||||
|
format = pkgs.runCommand "check-format"
|
||||||
|
{
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
gnumake
|
||||||
|
nixpkgs-fmt
|
||||||
|
golangci-lint
|
||||||
|
nodePackages.prettier
|
||||||
|
golines
|
||||||
|
clang-tools
|
||||||
|
];
|
||||||
|
} ''
|
||||||
|
${pkgs.nixpkgs-fmt}/bin/nixpkgs-fmt ${./.}
|
||||||
|
${pkgs.golangci-lint}/bin/golangci-lint run --fix --timeout 10m
|
||||||
|
${pkgs.nodePackages.prettier}/bin/prettier --write '**/**.{ts,js,md,yaml,yml,sass,css,scss,html}'
|
||||||
|
${pkgs.golines}/bin/golines --max-len=88 --base-formatter=gofumpt -w ${./.}
|
||||||
|
${pkgs.clang-tools}/bin/clang-format -style="{BasedOnStyle: Google, IndentWidth: 4, AlignConsecutiveDeclarations: true, AlignConsecutiveAssignments: true, ColumnLimit: 0}" -i ${./.}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.28.1
|
// protoc-gen-go v1.27.1
|
||||||
// protoc (unknown)
|
// protoc (unknown)
|
||||||
// source: headscale/v1/apikey.proto
|
// source: headscale/v1/apikey.proto
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.28.1
|
// protoc-gen-go v1.27.1
|
||||||
// protoc (unknown)
|
// protoc (unknown)
|
||||||
// source: headscale/v1/device.proto
|
// source: headscale/v1/device.proto
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.28.1
|
// protoc-gen-go v1.27.1
|
||||||
// protoc (unknown)
|
// protoc (unknown)
|
||||||
// source: headscale/v1/headscale.proto
|
// source: headscale/v1/headscale.proto
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,4 @@
|
|||||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
// versions:
|
|
||||||
// - protoc-gen-go-grpc v1.2.0
|
|
||||||
// - protoc (unknown)
|
|
||||||
// source: headscale/v1/headscale.proto
|
|
||||||
|
|
||||||
package v1
|
package v1
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.28.1
|
// protoc-gen-go v1.27.1
|
||||||
// protoc (unknown)
|
// protoc (unknown)
|
||||||
// source: headscale/v1/machine.proto
|
// source: headscale/v1/machine.proto
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.28.1
|
// protoc-gen-go v1.27.1
|
||||||
// protoc (unknown)
|
// protoc (unknown)
|
||||||
// source: headscale/v1/namespace.proto
|
// source: headscale/v1/namespace.proto
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.28.1
|
// protoc-gen-go v1.27.1
|
||||||
// protoc (unknown)
|
// protoc (unknown)
|
||||||
// source: headscale/v1/preauthkey.proto
|
// source: headscale/v1/preauthkey.proto
|
||||||
|
|
||||||
@@ -34,7 +34,6 @@ type PreAuthKey struct {
|
|||||||
Used bool `protobuf:"varint,6,opt,name=used,proto3" json:"used,omitempty"`
|
Used bool `protobuf:"varint,6,opt,name=used,proto3" json:"used,omitempty"`
|
||||||
Expiration *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=expiration,proto3" json:"expiration,omitempty"`
|
Expiration *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=expiration,proto3" json:"expiration,omitempty"`
|
||||||
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
|
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
|
||||||
AclTags []string `protobuf:"bytes,9,rep,name=acl_tags,json=aclTags,proto3" json:"acl_tags,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *PreAuthKey) Reset() {
|
func (x *PreAuthKey) Reset() {
|
||||||
@@ -125,13 +124,6 @@ func (x *PreAuthKey) GetCreatedAt() *timestamppb.Timestamp {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *PreAuthKey) GetAclTags() []string {
|
|
||||||
if x != nil {
|
|
||||||
return x.AclTags
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreatePreAuthKeyRequest struct {
|
type CreatePreAuthKeyRequest struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -141,7 +133,6 @@ type CreatePreAuthKeyRequest struct {
|
|||||||
Reusable bool `protobuf:"varint,2,opt,name=reusable,proto3" json:"reusable,omitempty"`
|
Reusable bool `protobuf:"varint,2,opt,name=reusable,proto3" json:"reusable,omitempty"`
|
||||||
Ephemeral bool `protobuf:"varint,3,opt,name=ephemeral,proto3" json:"ephemeral,omitempty"`
|
Ephemeral bool `protobuf:"varint,3,opt,name=ephemeral,proto3" json:"ephemeral,omitempty"`
|
||||||
Expiration *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expiration,proto3" json:"expiration,omitempty"`
|
Expiration *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expiration,proto3" json:"expiration,omitempty"`
|
||||||
AclTags []string `protobuf:"bytes,5,rep,name=acl_tags,json=aclTags,proto3" json:"acl_tags,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *CreatePreAuthKeyRequest) Reset() {
|
func (x *CreatePreAuthKeyRequest) Reset() {
|
||||||
@@ -204,13 +195,6 @@ func (x *CreatePreAuthKeyRequest) GetExpiration() *timestamppb.Timestamp {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *CreatePreAuthKeyRequest) GetAclTags() []string {
|
|
||||||
if x != nil {
|
|
||||||
return x.AclTags
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreatePreAuthKeyResponse struct {
|
type CreatePreAuthKeyResponse struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -452,7 +436,7 @@ var file_headscale_v1_preauthkey_proto_rawDesc = []byte{
|
|||||||
0x72, 0x65, 0x61, 0x75, 0x74, 0x68, 0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12,
|
0x72, 0x65, 0x61, 0x75, 0x74, 0x68, 0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12,
|
||||||
0x0c, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67,
|
0x0c, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67,
|
||||||
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74,
|
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74,
|
||||||
0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xac,
|
0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x91,
|
||||||
0x02, 0x0a, 0x0a, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x12, 0x1c, 0x0a,
|
0x02, 0x0a, 0x0a, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x12, 0x1c, 0x0a,
|
||||||
0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||||
0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69,
|
0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69,
|
||||||
@@ -470,45 +454,42 @@ var file_headscale_v1_preauthkey_proto_rawDesc = []byte{
|
|||||||
0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f,
|
0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f,
|
||||||
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69,
|
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69,
|
||||||
0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64,
|
0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64,
|
||||||
0x41, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x63, 0x6c, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x18, 0x09,
|
0x41, 0x74, 0x22, 0xad, 0x01, 0x0a, 0x17, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, 0x65,
|
||||||
0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x61, 0x63, 0x6c, 0x54, 0x61, 0x67, 0x73, 0x22, 0xc8, 0x01,
|
0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c,
|
||||||
0x0a, 0x17, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b,
|
0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||||
0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d,
|
0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x1a, 0x0a, 0x08,
|
||||||
0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61,
|
0x72, 0x65, 0x75, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08,
|
||||||
0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x75, 0x73, 0x61,
|
0x72, 0x65, 0x75, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x70, 0x68, 0x65,
|
||||||
0x62, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x72, 0x65, 0x75, 0x73, 0x61,
|
0x6d, 0x65, 0x72, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x65, 0x70, 0x68,
|
||||||
0x62, 0x6c, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c,
|
0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x12, 0x3a, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61,
|
||||||
0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61,
|
0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f,
|
||||||
0x6c, 0x12, 0x3a, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18,
|
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d,
|
||||||
0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
|
0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69,
|
||||||
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
|
0x6f, 0x6e, 0x22, 0x56, 0x0a, 0x18, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, 0x65, 0x41,
|
||||||
0x70, 0x52, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x19, 0x0a,
|
0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a,
|
||||||
0x08, 0x61, 0x63, 0x6c, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52,
|
0x0a, 0x0c, 0x70, 0x72, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01,
|
||||||
0x07, 0x61, 0x63, 0x6c, 0x54, 0x61, 0x67, 0x73, 0x22, 0x56, 0x0a, 0x18, 0x43, 0x72, 0x65, 0x61,
|
0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65,
|
||||||
0x74, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70,
|
0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x0a,
|
||||||
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68,
|
0x70, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x22, 0x49, 0x0a, 0x17, 0x45, 0x78,
|
||||||
0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x68, 0x65, 0x61,
|
0x70, 0x69, 0x72, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65,
|
||||||
0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74,
|
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61,
|
||||||
0x68, 0x4b, 0x65, 0x79, 0x52, 0x0a, 0x70, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79,
|
0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70,
|
||||||
0x22, 0x49, 0x0a, 0x17, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74,
|
0x61, 0x63, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
|
||||||
0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e,
|
0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x1a, 0x0a, 0x18, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x50,
|
||||||
|
0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||||
|
0x65, 0x22, 0x36, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68,
|
||||||
|
0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e,
|
||||||
0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09,
|
0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09,
|
||||||
0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79,
|
0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0x57, 0x0a, 0x17, 0x4c, 0x69, 0x73,
|
||||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x1a, 0x0a, 0x18, 0x45,
|
0x74, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70,
|
||||||
0x78, 0x70, 0x69, 0x72, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52,
|
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3c, 0x0a, 0x0d, 0x70, 0x72, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68,
|
||||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x36, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x50,
|
0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x68, 0x65,
|
||||||
0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x65, 0x41, 0x75,
|
||||||
0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01,
|
0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x0b, 0x70, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65,
|
||||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22,
|
0x79, 0x73, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
|
||||||
0x57, 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65,
|
0x2f, 0x6a, 0x75, 0x61, 0x6e, 0x66, 0x6f, 0x6e, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63,
|
||||||
0x79, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3c, 0x0a, 0x0d, 0x70, 0x72,
|
0x61, 0x6c, 0x65, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70,
|
||||||
0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28,
|
0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
0x0b, 0x32, 0x18, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31,
|
|
||||||
0x2e, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x0b, 0x70, 0x72, 0x65,
|
|
||||||
0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x73, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68,
|
|
||||||
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x75, 0x61, 0x6e, 0x66, 0x6f, 0x6e, 0x74, 0x2f,
|
|
||||||
0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f,
|
|
||||||
0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.28.1
|
// protoc-gen-go v1.27.1
|
||||||
// protoc (unknown)
|
// protoc (unknown)
|
||||||
// source: headscale/v1/routes.proto
|
// source: headscale/v1/routes.proto
|
||||||
|
|
||||||
|
|||||||
@@ -824,12 +824,6 @@
|
|||||||
"expiration": {
|
"expiration": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "date-time"
|
"format": "date-time"
|
||||||
},
|
|
||||||
"aclTags": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1108,12 +1102,6 @@
|
|||||||
"createdAt": {
|
"createdAt": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "date-time"
|
"format": "date-time"
|
||||||
},
|
|
||||||
"aclTags": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
177
go.mod
177
go.mod
@@ -1,150 +1,141 @@
|
|||||||
module github.com/juanfont/headscale
|
module github.com/juanfont/headscale
|
||||||
|
|
||||||
go 1.19
|
go 1.18
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/AlecAivazis/survey/v2 v2.3.6
|
github.com/AlecAivazis/survey/v2 v2.3.4
|
||||||
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.1.3
|
github.com/coreos/go-oidc/v3 v3.1.0
|
||||||
github.com/coreos/go-oidc/v3 v3.4.0
|
|
||||||
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.4.3
|
||||||
github.com/gofrs/uuid v4.3.0+incompatible
|
github.com/gofrs/uuid v4.2.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.12.0
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.10.0
|
||||||
github.com/klauspost/compress v1.15.12
|
github.com/klauspost/compress v1.15.4
|
||||||
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.13.0
|
github.com/prometheus/client_golang v1.12.1
|
||||||
github.com/prometheus/common v0.37.0
|
github.com/prometheus/common v0.32.1
|
||||||
github.com/pterm/pterm v0.12.49
|
github.com/pterm/pterm v0.12.41
|
||||||
github.com/puzpuzpuz/xsync/v2 v2.0.2
|
github.com/puzpuzpuz/xsync v1.2.1
|
||||||
github.com/rs/zerolog v1.28.0
|
github.com/rs/zerolog v1.26.1
|
||||||
github.com/spf13/cobra v1.6.1
|
github.com/spf13/cobra v1.4.0
|
||||||
github.com/spf13/viper v1.13.0
|
github.com/spf13/viper v1.11.0
|
||||||
github.com/stretchr/testify v1.8.0
|
github.com/stretchr/testify v1.8.0
|
||||||
github.com/tailscale/hujson v0.0.0-20220630195928-54599719472f
|
github.com/tailscale/hujson v0.0.0-20220506202205-92b4b88a9e17
|
||||||
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
|
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f
|
||||||
golang.org/x/crypto v0.1.0
|
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
|
||||||
golang.org/x/net v0.1.0
|
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29
|
||||||
golang.org/x/oauth2 v0.1.0
|
google.golang.org/genproto v0.0.0-20220422154200-b37d22cd5731
|
||||||
golang.org/x/sync v0.1.0
|
google.golang.org/grpc v1.46.0
|
||||||
google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c
|
google.golang.org/protobuf v1.28.0
|
||||||
google.golang.org/grpc v1.50.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.3.5
|
||||||
gorm.io/gorm v1.24.1-0.20221019064659-5dd2bb482755
|
gorm.io/gorm v1.23.4
|
||||||
tailscale.com v1.32.2
|
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6
|
||||||
|
tailscale.com v1.26.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
atomicgo.dev/cursor v0.1.1 // indirect
|
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
|
||||||
atomicgo.dev/keyboard v0.2.8 // indirect
|
github.com/Microsoft/go-winio v0.5.2 // indirect
|
||||||
filippo.io/edwards25519 v1.0.0 // indirect
|
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // 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
|
||||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect
|
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect
|
||||||
|
github.com/atomicgo/cursor v0.0.1 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
github.com/cespare/xxhash/v2 v2.1.2 // 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/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/docker/cli v20.10.21+incompatible // indirect
|
github.com/docker/cli v20.10.17+incompatible // indirect
|
||||||
github.com/docker/docker v20.10.21+incompatible // indirect
|
github.com/docker/docker v20.10.16+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.4.0 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
|
github.com/glebarez/go-sqlite v1.16.0 // indirect
|
||||||
github.com/glebarez/go-sqlite v1.19.2 // 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/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.8 // indirect
|
||||||
github.com/google/go-github v17.0.0+incompatible // indirect
|
github.com/google/go-github v17.0.0+incompatible // indirect
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
github.com/gookit/color v1.5.2 // indirect
|
github.com/gookit/color v1.5.0 // indirect
|
||||||
github.com/hashicorp/go-version v1.6.0 // indirect
|
github.com/hashicorp/go-version v1.4.0 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/hdevalence/ed25519consensus v0.1.0 // indirect
|
github.com/imdario/mergo v0.3.12 // indirect
|
||||||
github.com/imdario/mergo v0.3.13 // indirect
|
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
|
||||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||||
github.com/jackc/pgconn v1.13.0 // indirect
|
github.com/jackc/pgconn v1.12.0 // indirect
|
||||||
github.com/jackc/pgio v1.0.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/pgproto3/v2 v2.3.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
||||||
github.com/jackc/pgtype v1.12.0 // indirect
|
github.com/jackc/pgtype v1.11.0 // indirect
|
||||||
github.com/jackc/pgx/v4 v4.17.2 // indirect
|
github.com/jackc/pgx/v4 v4.16.0 // 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.4 // indirect
|
||||||
github.com/josharian/native v1.0.0 // indirect
|
github.com/josharian/native v1.0.0 // indirect
|
||||||
github.com/jsimonetti/rtnetlink v1.2.3 // indirect
|
github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b // 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.0 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/lithammer/fuzzysearch v1.1.5 // indirect
|
|
||||||
github.com/magiconair/properties v1.8.6 // indirect
|
github.com/magiconair/properties v1.8.6 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||||
github.com/mdlayher/netlink v1.6.2 // indirect
|
github.com/mdlayher/netlink v1.6.0 // indirect
|
||||||
github.com/mdlayher/socket v0.2.3 // indirect
|
github.com/mdlayher/socket v0.2.3 // indirect
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // 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.4.3 // indirect
|
||||||
github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // indirect
|
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // 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.0.3-0.20220114050600-8b9d41f48198 // indirect
|
||||||
github.com/opencontainers/runc v1.1.4 // indirect
|
github.com/opencontainers/runc v1.1.3 // indirect
|
||||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
github.com/pelletier/go-toml v1.9.4 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
|
github.com/pelletier/go-toml/v2 v2.0.0-beta.8 // 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.2.0 // indirect
|
||||||
github.com/prometheus/procfs v0.8.0 // indirect
|
github.com/prometheus/procfs v0.7.3 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||||
github.com/rivo/uniseg v0.4.2 // indirect
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 // indirect
|
github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||||
github.com/spf13/afero v1.9.2 // indirect
|
github.com/spf13/afero v1.8.2 // indirect
|
||||||
github.com/spf13/cast v1.5.0 // indirect
|
github.com/spf13/cast v1.4.1 // 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.2.0 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // 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-20210125001918-ca9a967f8778 // indirect
|
||||||
go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect
|
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
|
||||||
golang.org/x/mod v0.6.0 // indirect
|
go4.org/mem v0.0.0-20210711025021-927187094b94 // indirect
|
||||||
golang.org/x/sys v0.1.0 // indirect
|
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 // indirect
|
||||||
golang.org/x/term v0.1.0 // indirect
|
golang.org/x/net v0.0.0-20220516155154-20f960328961 // indirect
|
||||||
golang.org/x/text v0.4.0 // indirect
|
golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a // indirect
|
||||||
golang.org/x/time v0.1.0 // indirect
|
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect
|
||||||
golang.org/x/tools v0.2.0 // indirect
|
golang.org/x/text v0.3.7 // indirect
|
||||||
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
|
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
|
||||||
|
golang.zx2c4.com/wireguard/windows v0.4.10 // 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.66.4 // indirect
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
|
||||||
modernc.org/libc v1.21.4 // indirect
|
modernc.org/libc v1.14.12 // indirect
|
||||||
modernc.org/mathutil v1.5.0 // indirect
|
modernc.org/mathutil v1.4.1 // indirect
|
||||||
modernc.org/memory v1.4.0 // indirect
|
modernc.org/memory v1.0.7 // indirect
|
||||||
modernc.org/sqlite v1.19.3 // indirect
|
modernc.org/sqlite v1.16.0 // indirect
|
||||||
nhooyr.io/websocket v1.8.7 // indirect
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
replace github.com/ory/dockertest/v3 => github.com/juanfont/dockertest/v3 v3.0.0-20220730223459-cbf255cdb9ac
|
||||||
|
|||||||
26
grpcv1.go
26
grpcv1.go
@@ -1,4 +1,4 @@
|
|||||||
// nolint
|
//nolint
|
||||||
package headscale
|
package headscale
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/key"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type headscaleV1APIServer struct { // v1.HeadscaleServiceServer
|
type headscaleV1APIServer struct { // v1.HeadscaleServiceServer
|
||||||
@@ -107,21 +106,11 @@ func (api headscaleV1APIServer) CreatePreAuthKey(
|
|||||||
expiration = request.GetExpiration().AsTime()
|
expiration = request.GetExpiration().AsTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tag := range request.AclTags {
|
|
||||||
err := validateTag(tag)
|
|
||||||
if err != nil {
|
|
||||||
return &v1.CreatePreAuthKeyResponse{
|
|
||||||
PreAuthKey: nil,
|
|
||||||
}, status.Error(codes.InvalidArgument, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
preAuthKey, err := api.h.CreatePreAuthKey(
|
preAuthKey, err := api.h.CreatePreAuthKey(
|
||||||
request.GetNamespace(),
|
request.GetNamespace(),
|
||||||
request.GetReusable(),
|
request.GetReusable(),
|
||||||
request.GetEphemeral(),
|
request.GetEphemeral(),
|
||||||
&expiration,
|
&expiration,
|
||||||
request.AclTags,
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -170,7 +159,7 @@ func (api headscaleV1APIServer) RegisterMachine(
|
|||||||
) (*v1.RegisterMachineResponse, error) {
|
) (*v1.RegisterMachineResponse, error) {
|
||||||
log.Trace().
|
log.Trace().
|
||||||
Str("namespace", request.GetNamespace()).
|
Str("namespace", request.GetNamespace()).
|
||||||
Str("node_key", request.GetKey()).
|
Str("machine_key", request.GetKey()).
|
||||||
Msg("Registering machine")
|
Msg("Registering machine")
|
||||||
|
|
||||||
machine, err := api.h.RegisterMachineFromAuthCallback(
|
machine, err := api.h.RegisterMachineFromAuthCallback(
|
||||||
@@ -210,7 +199,7 @@ func (api headscaleV1APIServer) SetTags(
|
|||||||
err := validateTag(tag)
|
err := validateTag(tag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &v1.SetTagsResponse{
|
return &v1.SetTagsResponse{
|
||||||
Machine: nil,
|
Machine: nil,
|
||||||
}, status.Error(codes.InvalidArgument, err.Error())
|
}, status.Error(codes.InvalidArgument, err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -480,7 +469,7 @@ func (api headscaleV1APIServer) DebugCreateMachine(
|
|||||||
Hostname: "DebugTestMachine",
|
Hostname: "DebugTestMachine",
|
||||||
}
|
}
|
||||||
|
|
||||||
givenName, err := api.h.GenerateGivenName(request.GetKey(), request.GetName())
|
givenName, err := api.h.GenerateGivenName(request.GetName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -497,14 +486,9 @@ func (api headscaleV1APIServer) DebugCreateMachine(
|
|||||||
|
|
||||||
HostInfo: HostInfo(hostinfo),
|
HostInfo: HostInfo(hostinfo),
|
||||||
}
|
}
|
||||||
nodeKey := key.NodePublic{}
|
|
||||||
err = nodeKey.UnmarshalText([]byte(request.GetKey()))
|
|
||||||
if err != nil {
|
|
||||||
log.Panic().Msg("can not add machine for debug. invalid node key")
|
|
||||||
}
|
|
||||||
|
|
||||||
api.h.registrationCache.Set(
|
api.h.registrationCache.Set(
|
||||||
NodePublicKeyStripPrefix(nodeKey),
|
request.GetKey(),
|
||||||
newMachine,
|
newMachine,
|
||||||
registerCacheExpiration,
|
registerCacheExpiration,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
//go:build ts2019
|
|
||||||
|
|
||||||
package headscale
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (h *Headscale) addLegacyHandlers(router *mux.Router) {
|
|
||||||
router.HandleFunc("/machine/{mkey}/map", h.PollNetMapHandler).
|
|
||||||
Methods(http.MethodPost)
|
|
||||||
router.HandleFunc("/machine/{mkey}", h.RegistrationHandler).Methods(http.MethodPost)
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
//go:build !ts2019
|
|
||||||
|
|
||||||
package headscale
|
|
||||||
|
|
||||||
import "github.com/gorilla/mux"
|
|
||||||
|
|
||||||
func (h *Headscale) addLegacyHandlers(router *mux.Router) {
|
|
||||||
}
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
package integration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
var errParseAuthPage = errors.New("failed to parse auth page")
|
|
||||||
|
|
||||||
type AuthWebFlowScenario struct {
|
|
||||||
*Scenario
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAuthWebFlowAuthenticationPingAll(t *testing.T) {
|
|
||||||
IntegrationSkip(t)
|
|
||||||
|
|
||||||
baseScenario, err := NewScenario()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to create scenario: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
scenario := AuthWebFlowScenario{
|
|
||||||
Scenario: baseScenario,
|
|
||||||
}
|
|
||||||
|
|
||||||
spec := map[string]int{
|
|
||||||
"namespace1": len(TailscaleVersions),
|
|
||||||
"namespace2": len(TailscaleVersions),
|
|
||||||
}
|
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to create headscale environment: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
allClients, err := scenario.ListTailscaleClients()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to get clients: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
allIps, err := scenario.ListTailscaleClientsIPs()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to get clients: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = scenario.WaitForTailscaleSync()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed wait for tailscale clients to be in sync: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
success := 0
|
|
||||||
|
|
||||||
for _, client := range allClients {
|
|
||||||
for _, ip := range allIps {
|
|
||||||
err := client.Ping(ip.String())
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to ping %s from %s: %s", ip, client.Hostname(), err)
|
|
||||||
} else {
|
|
||||||
success++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
|
|
||||||
|
|
||||||
err = scenario.Shutdown()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to tear down scenario: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AuthWebFlowScenario) CreateHeadscaleEnv(namespaces map[string]int) error {
|
|
||||||
err := s.StartHeadscale()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.Headscale().WaitForReady()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for namespaceName, clientCount := range namespaces {
|
|
||||||
log.Printf("creating namespace %s with %d clients", namespaceName, clientCount)
|
|
||||||
err = s.CreateNamespace(namespaceName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.CreateTailscaleNodesInNamespace(namespaceName, "all", clientCount)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.runTailscaleUp(namespaceName, s.Headscale().GetEndpoint())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AuthWebFlowScenario) runTailscaleUp(
|
|
||||||
namespaceStr, loginServer string,
|
|
||||||
) error {
|
|
||||||
log.Printf("running tailscale up for namespace %s", namespaceStr)
|
|
||||||
if namespace, ok := s.namespaces[namespaceStr]; ok {
|
|
||||||
for _, client := range namespace.Clients {
|
|
||||||
namespace.joinWaitGroup.Add(1)
|
|
||||||
|
|
||||||
go func(c TailscaleClient) {
|
|
||||||
defer namespace.joinWaitGroup.Done()
|
|
||||||
|
|
||||||
// TODO(juanfont): error handle this
|
|
||||||
loginURL, err := c.UpWithLoginURL(loginServer)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("failed to run tailscale up: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.runHeadscaleRegister(namespaceStr, loginURL)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("failed to register client: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.WaitForReady()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error waiting for client %s to be ready: %s", c.Hostname(), err)
|
|
||||||
}
|
|
||||||
}(client)
|
|
||||||
}
|
|
||||||
namespace.joinWaitGroup.Wait()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("failed to up tailscale node: %w", errNoNamespaceAvailable)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AuthWebFlowScenario) runHeadscaleRegister(namespaceStr string, loginURL *url.URL) error {
|
|
||||||
log.Printf("loginURL: %s", loginURL)
|
|
||||||
loginURL.Host = fmt.Sprintf("%s:8080", s.Headscale().GetIP())
|
|
||||||
loginURL.Scheme = "http"
|
|
||||||
|
|
||||||
httpClient := &http.Client{}
|
|
||||||
ctx := context.Background()
|
|
||||||
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, loginURL.String(), nil)
|
|
||||||
resp, err := httpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
// see api.go HTML template
|
|
||||||
codeSep := strings.Split(string(body), "</code>")
|
|
||||||
if len(codeSep) != 2 {
|
|
||||||
return errParseAuthPage
|
|
||||||
}
|
|
||||||
|
|
||||||
keySep := strings.Split(codeSep[0], "key ")
|
|
||||||
if len(keySep) != 2 {
|
|
||||||
return errParseAuthPage
|
|
||||||
}
|
|
||||||
key := keySep[1]
|
|
||||||
log.Printf("registering node %s", key)
|
|
||||||
|
|
||||||
if headscale, ok := s.controlServers["headscale"]; ok {
|
|
||||||
_, err = headscale.Execute([]string{"headscale", "-n", namespaceStr, "nodes", "register", "--key", key})
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("failed to register node: %s", err)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("failed to find headscale: %w", errNoHeadscaleAvailable)
|
|
||||||
}
|
|
||||||
@@ -1,377 +0,0 @@
|
|||||||
package integration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"sort"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func executeAndUnmarshal[T any](headscale ControlServer, command []string, result T) error {
|
|
||||||
str, err := headscale.Execute(command)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal([]byte(str), result)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNamespaceCommand(t *testing.T) {
|
|
||||||
IntegrationSkip(t)
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
scenario, err := NewScenario()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
spec := map[string]int{
|
|
||||||
"namespace1": 0,
|
|
||||||
"namespace2": 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
var listNamespaces []v1.Namespace
|
|
||||||
err = executeAndUnmarshal(scenario.Headscale(),
|
|
||||||
[]string{
|
|
||||||
"headscale",
|
|
||||||
"namespaces",
|
|
||||||
"list",
|
|
||||||
"--output",
|
|
||||||
"json",
|
|
||||||
},
|
|
||||||
&listNamespaces,
|
|
||||||
)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
result := []string{listNamespaces[0].Name, listNamespaces[1].Name}
|
|
||||||
sort.Strings(result)
|
|
||||||
|
|
||||||
assert.Equal(
|
|
||||||
t,
|
|
||||||
[]string{"namespace1", "namespace2"},
|
|
||||||
result,
|
|
||||||
)
|
|
||||||
|
|
||||||
_, err = scenario.Headscale().Execute(
|
|
||||||
[]string{
|
|
||||||
"headscale",
|
|
||||||
"namespaces",
|
|
||||||
"rename",
|
|
||||||
"--output",
|
|
||||||
"json",
|
|
||||||
"namespace2",
|
|
||||||
"newname",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
var listAfterRenameNamespaces []v1.Namespace
|
|
||||||
err = executeAndUnmarshal(scenario.Headscale(),
|
|
||||||
[]string{
|
|
||||||
"headscale",
|
|
||||||
"namespaces",
|
|
||||||
"list",
|
|
||||||
"--output",
|
|
||||||
"json",
|
|
||||||
},
|
|
||||||
&listAfterRenameNamespaces,
|
|
||||||
)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
result = []string{listAfterRenameNamespaces[0].Name, listAfterRenameNamespaces[1].Name}
|
|
||||||
sort.Strings(result)
|
|
||||||
|
|
||||||
assert.Equal(
|
|
||||||
t,
|
|
||||||
[]string{"namespace1", "newname"},
|
|
||||||
result,
|
|
||||||
)
|
|
||||||
|
|
||||||
err = scenario.Shutdown()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPreAuthKeyCommand(t *testing.T) {
|
|
||||||
IntegrationSkip(t)
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
namespace := "preauthkeyspace"
|
|
||||||
count := 3
|
|
||||||
|
|
||||||
scenario, err := NewScenario()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
spec := map[string]int{
|
|
||||||
namespace: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
keys := make([]*v1.PreAuthKey, count)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
for index := 0; index < count; index++ {
|
|
||||||
var preAuthKey v1.PreAuthKey
|
|
||||||
err := executeAndUnmarshal(
|
|
||||||
scenario.Headscale(),
|
|
||||||
[]string{
|
|
||||||
"headscale",
|
|
||||||
"preauthkeys",
|
|
||||||
"--namespace",
|
|
||||||
namespace,
|
|
||||||
"create",
|
|
||||||
"--reusable",
|
|
||||||
"--expiration",
|
|
||||||
"24h",
|
|
||||||
"--output",
|
|
||||||
"json",
|
|
||||||
"--tags",
|
|
||||||
"tag:test1,tag:test2",
|
|
||||||
},
|
|
||||||
&preAuthKey,
|
|
||||||
)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
keys[index] = &preAuthKey
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Len(t, keys, 3)
|
|
||||||
|
|
||||||
var listedPreAuthKeys []v1.PreAuthKey
|
|
||||||
err = executeAndUnmarshal(
|
|
||||||
scenario.Headscale(),
|
|
||||||
[]string{
|
|
||||||
"headscale",
|
|
||||||
"preauthkeys",
|
|
||||||
"--namespace",
|
|
||||||
namespace,
|
|
||||||
"list",
|
|
||||||
"--output",
|
|
||||||
"json",
|
|
||||||
},
|
|
||||||
&listedPreAuthKeys,
|
|
||||||
)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// There is one key created by "scenario.CreateHeadscaleEnv"
|
|
||||||
assert.Len(t, listedPreAuthKeys, 4)
|
|
||||||
|
|
||||||
assert.Equal(
|
|
||||||
t,
|
|
||||||
[]string{keys[0].Id, keys[1].Id, keys[2].Id},
|
|
||||||
[]string{listedPreAuthKeys[1].Id, listedPreAuthKeys[2].Id, listedPreAuthKeys[3].Id},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert.NotEmpty(t, listedPreAuthKeys[1].Key)
|
|
||||||
assert.NotEmpty(t, listedPreAuthKeys[2].Key)
|
|
||||||
assert.NotEmpty(t, listedPreAuthKeys[3].Key)
|
|
||||||
|
|
||||||
assert.True(t, listedPreAuthKeys[1].Expiration.AsTime().After(time.Now()))
|
|
||||||
assert.True(t, listedPreAuthKeys[2].Expiration.AsTime().After(time.Now()))
|
|
||||||
assert.True(t, listedPreAuthKeys[3].Expiration.AsTime().After(time.Now()))
|
|
||||||
|
|
||||||
assert.True(
|
|
||||||
t,
|
|
||||||
listedPreAuthKeys[1].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)),
|
|
||||||
)
|
|
||||||
assert.True(
|
|
||||||
t,
|
|
||||||
listedPreAuthKeys[2].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)),
|
|
||||||
)
|
|
||||||
assert.True(
|
|
||||||
t,
|
|
||||||
listedPreAuthKeys[3].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)),
|
|
||||||
)
|
|
||||||
|
|
||||||
for index := range listedPreAuthKeys {
|
|
||||||
if index == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, listedPreAuthKeys[index].AclTags, []string{"tag:test1", "tag:test2"})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test key expiry
|
|
||||||
_, err = scenario.Headscale().Execute(
|
|
||||||
[]string{
|
|
||||||
"headscale",
|
|
||||||
"preauthkeys",
|
|
||||||
"--namespace",
|
|
||||||
namespace,
|
|
||||||
"expire",
|
|
||||||
listedPreAuthKeys[1].Key,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
var listedPreAuthKeysAfterExpire []v1.PreAuthKey
|
|
||||||
err = executeAndUnmarshal(
|
|
||||||
scenario.Headscale(),
|
|
||||||
[]string{
|
|
||||||
"headscale",
|
|
||||||
"preauthkeys",
|
|
||||||
"--namespace",
|
|
||||||
namespace,
|
|
||||||
"list",
|
|
||||||
"--output",
|
|
||||||
"json",
|
|
||||||
},
|
|
||||||
&listedPreAuthKeysAfterExpire,
|
|
||||||
)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.True(t, listedPreAuthKeysAfterExpire[1].Expiration.AsTime().Before(time.Now()))
|
|
||||||
assert.True(t, listedPreAuthKeysAfterExpire[2].Expiration.AsTime().After(time.Now()))
|
|
||||||
assert.True(t, listedPreAuthKeysAfterExpire[3].Expiration.AsTime().After(time.Now()))
|
|
||||||
|
|
||||||
err = scenario.Shutdown()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) {
|
|
||||||
IntegrationSkip(t)
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
namespace := "pre-auth-key-without-exp-namespace"
|
|
||||||
|
|
||||||
scenario, err := NewScenario()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
spec := map[string]int{
|
|
||||||
namespace: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
var preAuthKey v1.PreAuthKey
|
|
||||||
err = executeAndUnmarshal(
|
|
||||||
scenario.Headscale(),
|
|
||||||
[]string{
|
|
||||||
"headscale",
|
|
||||||
"preauthkeys",
|
|
||||||
"--namespace",
|
|
||||||
namespace,
|
|
||||||
"create",
|
|
||||||
"--reusable",
|
|
||||||
"--output",
|
|
||||||
"json",
|
|
||||||
},
|
|
||||||
&preAuthKey,
|
|
||||||
)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
var listedPreAuthKeys []v1.PreAuthKey
|
|
||||||
err = executeAndUnmarshal(
|
|
||||||
scenario.Headscale(),
|
|
||||||
[]string{
|
|
||||||
"headscale",
|
|
||||||
"preauthkeys",
|
|
||||||
"--namespace",
|
|
||||||
namespace,
|
|
||||||
"list",
|
|
||||||
"--output",
|
|
||||||
"json",
|
|
||||||
},
|
|
||||||
&listedPreAuthKeys,
|
|
||||||
)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// There is one key created by "scenario.CreateHeadscaleEnv"
|
|
||||||
assert.Len(t, listedPreAuthKeys, 2)
|
|
||||||
|
|
||||||
assert.True(t, listedPreAuthKeys[1].Expiration.AsTime().After(time.Now()))
|
|
||||||
assert.True(
|
|
||||||
t,
|
|
||||||
listedPreAuthKeys[1].Expiration.AsTime().Before(time.Now().Add(time.Minute*70)),
|
|
||||||
)
|
|
||||||
|
|
||||||
err = scenario.Shutdown()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
|
|
||||||
IntegrationSkip(t)
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
namespace := "pre-auth-key-reus-ephm-namespace"
|
|
||||||
|
|
||||||
scenario, err := NewScenario()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
spec := map[string]int{
|
|
||||||
namespace: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
var preAuthReusableKey v1.PreAuthKey
|
|
||||||
err = executeAndUnmarshal(
|
|
||||||
scenario.Headscale(),
|
|
||||||
[]string{
|
|
||||||
"headscale",
|
|
||||||
"preauthkeys",
|
|
||||||
"--namespace",
|
|
||||||
namespace,
|
|
||||||
"create",
|
|
||||||
"--reusable=true",
|
|
||||||
"--output",
|
|
||||||
"json",
|
|
||||||
},
|
|
||||||
&preAuthReusableKey,
|
|
||||||
)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
var preAuthEphemeralKey v1.PreAuthKey
|
|
||||||
err = executeAndUnmarshal(
|
|
||||||
scenario.Headscale(),
|
|
||||||
[]string{
|
|
||||||
"headscale",
|
|
||||||
"preauthkeys",
|
|
||||||
"--namespace",
|
|
||||||
namespace,
|
|
||||||
"create",
|
|
||||||
"--ephemeral=true",
|
|
||||||
"--output",
|
|
||||||
"json",
|
|
||||||
},
|
|
||||||
&preAuthEphemeralKey,
|
|
||||||
)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.True(t, preAuthEphemeralKey.GetEphemeral())
|
|
||||||
assert.False(t, preAuthEphemeralKey.GetReusable())
|
|
||||||
|
|
||||||
var listedPreAuthKeys []v1.PreAuthKey
|
|
||||||
err = executeAndUnmarshal(
|
|
||||||
scenario.Headscale(),
|
|
||||||
[]string{
|
|
||||||
"headscale",
|
|
||||||
"preauthkeys",
|
|
||||||
"--namespace",
|
|
||||||
namespace,
|
|
||||||
"list",
|
|
||||||
"--output",
|
|
||||||
"json",
|
|
||||||
},
|
|
||||||
&listedPreAuthKeys,
|
|
||||||
)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// There is one key created by "scenario.CreateHeadscaleEnv"
|
|
||||||
assert.Len(t, listedPreAuthKeys, 3)
|
|
||||||
|
|
||||||
err = scenario.Shutdown()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package integration
|
|
||||||
|
|
||||||
import (
|
|
||||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ControlServer interface {
|
|
||||||
Shutdown() error
|
|
||||||
Execute(command []string) (string, error)
|
|
||||||
GetHealthEndpoint() string
|
|
||||||
GetEndpoint() string
|
|
||||||
WaitForReady() error
|
|
||||||
CreateNamespace(namespace string) error
|
|
||||||
CreateAuthKey(namespace string) (*v1.PreAuthKey, error)
|
|
||||||
ListMachinesInNamespace(namespace string) ([]*v1.Machine, error)
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package dockertestutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/ory/dockertest/v3/docker"
|
|
||||||
)
|
|
||||||
|
|
||||||
func IsRunningInContainer() bool {
|
|
||||||
if _, err := os.Stat("/.dockerenv"); err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func DockerRestartPolicy(config *docker.HostConfig) {
|
|
||||||
// set AutoRemove to true so that stopped container goes away by itself on error *immediately*.
|
|
||||||
// when set to false, containers remain until the end of the integration test.
|
|
||||||
config.AutoRemove = false
|
|
||||||
config.RestartPolicy = docker.RestartPolicy{
|
|
||||||
Name: "no",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func DockerAllowLocalIPv6(config *docker.HostConfig) {
|
|
||||||
if config.Sysctls == nil {
|
|
||||||
config.Sysctls = make(map[string]string, 1)
|
|
||||||
}
|
|
||||||
config.Sysctls["net.ipv6.conf.all.disable_ipv6"] = "0"
|
|
||||||
}
|
|
||||||
|
|
||||||
func DockerAllowNetworkAdministration(config *docker.HostConfig) {
|
|
||||||
config.CapAdd = append(config.CapAdd, "NET_ADMIN")
|
|
||||||
config.Mounts = append(config.Mounts, docker.HostMount{
|
|
||||||
Type: "bind",
|
|
||||||
Source: "/dev/net/tun",
|
|
||||||
Target: "/dev/net/tun",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
package dockertestutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ory/dockertest/v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
const dockerExecuteTimeout = time.Second * 10
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrDockertestCommandFailed = errors.New("dockertest command failed")
|
|
||||||
ErrDockertestCommandTimeout = errors.New("dockertest command timed out")
|
|
||||||
)
|
|
||||||
|
|
||||||
type ExecuteCommandConfig struct {
|
|
||||||
timeout time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExecuteCommandOption func(*ExecuteCommandConfig) error
|
|
||||||
|
|
||||||
func ExecuteCommandTimeout(timeout time.Duration) ExecuteCommandOption {
|
|
||||||
return ExecuteCommandOption(func(conf *ExecuteCommandConfig) error {
|
|
||||||
conf.timeout = timeout
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExecuteCommand(
|
|
||||||
resource *dockertest.Resource,
|
|
||||||
cmd []string,
|
|
||||||
env []string,
|
|
||||||
options ...ExecuteCommandOption,
|
|
||||||
) (string, string, error) {
|
|
||||||
var stdout bytes.Buffer
|
|
||||||
var stderr bytes.Buffer
|
|
||||||
|
|
||||||
execConfig := ExecuteCommandConfig{
|
|
||||||
timeout: dockerExecuteTimeout,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, opt := range options {
|
|
||||||
if err := opt(&execConfig); err != nil {
|
|
||||||
return "", "", fmt.Errorf("execute-command/options: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type result struct {
|
|
||||||
exitCode int
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
resultChan := make(chan result, 1)
|
|
||||||
|
|
||||||
// Run your long running function in it's own goroutine and pass back it's
|
|
||||||
// response into our channel.
|
|
||||||
go func() {
|
|
||||||
exitCode, err := resource.Exec(
|
|
||||||
cmd,
|
|
||||||
dockertest.ExecOptions{
|
|
||||||
Env: append(env, "HEADSCALE_LOG_LEVEL=disabled"),
|
|
||||||
StdOut: &stdout,
|
|
||||||
StdErr: &stderr,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
resultChan <- result{exitCode, err}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Listen on our channel AND a timeout channel - which ever happens first.
|
|
||||||
select {
|
|
||||||
case res := <-resultChan:
|
|
||||||
if res.err != nil {
|
|
||||||
return stdout.String(), stderr.String(), res.err
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.exitCode != 0 {
|
|
||||||
// Uncomment for debugging
|
|
||||||
// log.Println("Command: ", cmd)
|
|
||||||
// log.Println("stdout: ", stdout.String())
|
|
||||||
// log.Println("stderr: ", stderr.String())
|
|
||||||
|
|
||||||
return stdout.String(), stderr.String(), ErrDockertestCommandFailed
|
|
||||||
}
|
|
||||||
|
|
||||||
return stdout.String(), stderr.String(), nil
|
|
||||||
case <-time.After(execConfig.timeout):
|
|
||||||
|
|
||||||
return stdout.String(), stderr.String(), ErrDockertestCommandTimeout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
package dockertestutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/ory/dockertest/v3"
|
|
||||||
"github.com/ory/dockertest/v3/docker"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ErrContainerNotFound = errors.New("container not found")
|
|
||||||
|
|
||||||
func GetFirstOrCreateNetwork(pool *dockertest.Pool, name string) (*dockertest.Network, error) {
|
|
||||||
networks, err := pool.NetworksByName(name)
|
|
||||||
if err != nil || len(networks) == 0 {
|
|
||||||
if _, err := pool.CreateNetwork(name); err == nil {
|
|
||||||
// Create does not give us an updated version of the resource, so we need to
|
|
||||||
// get it again.
|
|
||||||
networks, err := pool.NetworksByName(name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &networks[0], nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &networks[0], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func AddContainerToNetwork(
|
|
||||||
pool *dockertest.Pool,
|
|
||||||
network *dockertest.Network,
|
|
||||||
testContainer string,
|
|
||||||
) error {
|
|
||||||
containers, err := pool.Client.ListContainers(docker.ListContainersOptions{
|
|
||||||
All: true,
|
|
||||||
Filters: map[string][]string{
|
|
||||||
"name": {testContainer},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = pool.Client.ConnectNetwork(network.Network.ID, docker.NetworkConnectionOptions{
|
|
||||||
Container: containers[0].ID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(kradalby): This doesnt work reliably, but calling the exact same functions
|
|
||||||
// seem to work fine...
|
|
||||||
// if container, ok := pool.ContainerByName("/" + testContainer); ok {
|
|
||||||
// err := container.ConnectToNetwork(network)
|
|
||||||
// if err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,340 +0,0 @@
|
|||||||
package integration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPingAllByIP(t *testing.T) {
|
|
||||||
IntegrationSkip(t)
|
|
||||||
|
|
||||||
scenario, err := NewScenario()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to create scenario: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
spec := map[string]int{
|
|
||||||
"namespace1": len(TailscaleVersions),
|
|
||||||
"namespace2": len(TailscaleVersions),
|
|
||||||
}
|
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to create headscale environment: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
allClients, err := scenario.ListTailscaleClients()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to get clients: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
allIps, err := scenario.ListTailscaleClientsIPs()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to get clients: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = scenario.WaitForTailscaleSync()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed wait for tailscale clients to be in sync: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
success := 0
|
|
||||||
|
|
||||||
for _, client := range allClients {
|
|
||||||
for _, ip := range allIps {
|
|
||||||
err := client.Ping(ip.String())
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to ping %s from %s: %s", ip, client.Hostname(), err)
|
|
||||||
} else {
|
|
||||||
success++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
|
|
||||||
|
|
||||||
err = scenario.Shutdown()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to tear down scenario: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPingAllByHostname(t *testing.T) {
|
|
||||||
IntegrationSkip(t)
|
|
||||||
|
|
||||||
scenario, err := NewScenario()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to create scenario: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
spec := map[string]int{
|
|
||||||
// Omit 1.16.2 (-1) because it does not have the FQDN field
|
|
||||||
"namespace3": len(TailscaleVersions) - 1,
|
|
||||||
"namespace4": len(TailscaleVersions) - 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to create headscale environment: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
allClients, err := scenario.ListTailscaleClients()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to get clients: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = scenario.WaitForTailscaleSync()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed wait for tailscale clients to be in sync: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
allHostnames, err := scenario.ListTailscaleClientsFQDNs()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to get FQDNs: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
success := 0
|
|
||||||
|
|
||||||
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))
|
|
||||||
|
|
||||||
err = scenario.Shutdown()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to tear down scenario: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTaildrop(t *testing.T) {
|
|
||||||
IntegrationSkip(t)
|
|
||||||
|
|
||||||
retry := func(times int, sleepInverval time.Duration, doWork func() error) error {
|
|
||||||
var err error
|
|
||||||
for attempts := 0; attempts < times; attempts++ {
|
|
||||||
err = doWork()
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
time.Sleep(sleepInverval)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
scenario, err := NewScenario()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to create scenario: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
spec := map[string]int{
|
|
||||||
// Omit 1.16.2 (-1) because it does not have the FQDN field
|
|
||||||
"taildrop": len(TailscaleVersions) - 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to create headscale environment: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
allClients, err := scenario.ListTailscaleClients()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to get clients: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = scenario.WaitForTailscaleSync()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed wait for tailscale clients to be in sync: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This will essentially fetch and cache all the FQDNs
|
|
||||||
_, err = scenario.ListTailscaleClientsFQDNs()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to get FQDNs: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, client := range allClients {
|
|
||||||
command := []string{"touch", fmt.Sprintf("/tmp/file_from_%s", client.Hostname())}
|
|
||||||
|
|
||||||
if _, _, err := client.Execute(command); err != nil {
|
|
||||||
t.Errorf("failed to create taildrop file on %s, err: %s", client.Hostname(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, peer := range allClients {
|
|
||||||
if client.Hostname() == peer.Hostname() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// It is safe to ignore this error as we handled it when caching it
|
|
||||||
peerFQDN, _ := peer.FQDN()
|
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("%s-%s", client.Hostname(), peer.Hostname()), func(t *testing.T) {
|
|
||||||
command := []string{
|
|
||||||
"tailscale", "file", "cp",
|
|
||||||
fmt.Sprintf("/tmp/file_from_%s", client.Hostname()),
|
|
||||||
fmt.Sprintf("%s:", peerFQDN),
|
|
||||||
}
|
|
||||||
|
|
||||||
err := retry(10, 1*time.Second, func() error {
|
|
||||||
t.Logf(
|
|
||||||
"Sending file from %s to %s\n",
|
|
||||||
client.Hostname(),
|
|
||||||
peer.Hostname(),
|
|
||||||
)
|
|
||||||
_, _, err := client.Execute(command)
|
|
||||||
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf(
|
|
||||||
"failed to send taildrop file on %s, err: %s",
|
|
||||||
client.Hostname(),
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, client := range allClients {
|
|
||||||
command := []string{
|
|
||||||
"tailscale", "file",
|
|
||||||
"get",
|
|
||||||
"/tmp/",
|
|
||||||
}
|
|
||||||
if _, _, err := client.Execute(command); err != nil {
|
|
||||||
t.Errorf("failed to get taildrop file on %s, err: %s", client.Hostname(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, peer := range allClients {
|
|
||||||
if client.Hostname() == peer.Hostname() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("%s-%s", client.Hostname(), peer.Hostname()), func(t *testing.T) {
|
|
||||||
command := []string{
|
|
||||||
"ls",
|
|
||||||
fmt.Sprintf("/tmp/file_from_%s", peer.Hostname()),
|
|
||||||
}
|
|
||||||
log.Printf(
|
|
||||||
"Checking file in %s from %s\n",
|
|
||||||
client.Hostname(),
|
|
||||||
peer.Hostname(),
|
|
||||||
)
|
|
||||||
|
|
||||||
result, _, err := client.Execute(command)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to execute command to ls taildrop: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("Result for %s: %s\n", peer.Hostname(), result)
|
|
||||||
if fmt.Sprintf("/tmp/file_from_%s\n", peer.Hostname()) != result {
|
|
||||||
t.Errorf(
|
|
||||||
"taildrop result is not correct %s, wanted %s",
|
|
||||||
result,
|
|
||||||
fmt.Sprintf("/tmp/file_from_%s\n", peer.Hostname()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = scenario.Shutdown()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to tear down scenario: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResolveMagicDNS(t *testing.T) {
|
|
||||||
IntegrationSkip(t)
|
|
||||||
|
|
||||||
scenario, err := NewScenario()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to create scenario: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
spec := map[string]int{
|
|
||||||
// Omit 1.16.2 (-1) because it does not have the FQDN field
|
|
||||||
"magicdns1": len(TailscaleVersions) - 1,
|
|
||||||
"magicdns2": len(TailscaleVersions) - 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to create headscale environment: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
allClients, err := scenario.ListTailscaleClients()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to get clients: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = scenario.WaitForTailscaleSync()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed wait for tailscale clients to be in sync: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Poor mans cache
|
|
||||||
_, err = scenario.ListTailscaleClientsFQDNs()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to get FQDNs: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = scenario.ListTailscaleClientsIPs()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to get IPs: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, client := range allClients {
|
|
||||||
for _, peer := range allClients {
|
|
||||||
// It is safe to ignore this error as we handled it when caching it
|
|
||||||
peerFQDN, _ := peer.FQDN()
|
|
||||||
|
|
||||||
command := []string{
|
|
||||||
"tailscale",
|
|
||||||
"ip", peerFQDN,
|
|
||||||
}
|
|
||||||
result, _, err := client.Execute(command)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf(
|
|
||||||
"failed to execute resolve/ip command %s from %s: %s",
|
|
||||||
peerFQDN,
|
|
||||||
client.Hostname(),
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
ips, err := peer.IPs()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf(
|
|
||||||
"failed to get ips for %s: %s",
|
|
||||||
peer.Hostname(),
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, ip := range ips {
|
|
||||||
if !strings.Contains(result, ip.String()) {
|
|
||||||
t.Errorf("ip %s is not found in \n%s\n", ip.String(), result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = scenario.Shutdown()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to tear down scenario: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
package hsic
|
|
||||||
|
|
||||||
// const (
|
|
||||||
// defaultEphemeralNodeInactivityTimeout = time.Second * 30
|
|
||||||
// defaultNodeUpdateCheckInterval = time.Second * 10
|
|
||||||
// )
|
|
||||||
|
|
||||||
// TODO(kradalby): This approach doesnt work because we cannot
|
|
||||||
// serialise our config object to YAML or JSON.
|
|
||||||
// func DefaultConfig() headscale.Config {
|
|
||||||
// derpMap, _ := url.Parse("https://controlplane.tailscale.com/derpmap/default")
|
|
||||||
//
|
|
||||||
// config := headscale.Config{
|
|
||||||
// Log: headscale.LogConfig{
|
|
||||||
// Level: zerolog.TraceLevel,
|
|
||||||
// },
|
|
||||||
// ACL: headscale.GetACLConfig(),
|
|
||||||
// DBtype: "sqlite3",
|
|
||||||
// EphemeralNodeInactivityTimeout: defaultEphemeralNodeInactivityTimeout,
|
|
||||||
// NodeUpdateCheckInterval: defaultNodeUpdateCheckInterval,
|
|
||||||
// IPPrefixes: []netip.Prefix{
|
|
||||||
// netip.MustParsePrefix("fd7a:115c:a1e0::/48"),
|
|
||||||
// netip.MustParsePrefix("100.64.0.0/10"),
|
|
||||||
// },
|
|
||||||
// DNSConfig: &tailcfg.DNSConfig{
|
|
||||||
// Proxied: true,
|
|
||||||
// Nameservers: []netip.Addr{
|
|
||||||
// netip.MustParseAddr("127.0.0.11"),
|
|
||||||
// netip.MustParseAddr("1.1.1.1"),
|
|
||||||
// },
|
|
||||||
// Resolvers: []*dnstype.Resolver{
|
|
||||||
// {
|
|
||||||
// Addr: "127.0.0.11",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// Addr: "1.1.1.1",
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// BaseDomain: "headscale.net",
|
|
||||||
//
|
|
||||||
// DBpath: "/tmp/integration_test_db.sqlite3",
|
|
||||||
//
|
|
||||||
// PrivateKeyPath: "/tmp/integration_private.key",
|
|
||||||
// NoisePrivateKeyPath: "/tmp/noise_integration_private.key",
|
|
||||||
// Addr: "0.0.0.0:8080",
|
|
||||||
// MetricsAddr: "127.0.0.1:9090",
|
|
||||||
// ServerURL: "http://headscale:8080",
|
|
||||||
//
|
|
||||||
// DERP: headscale.DERPConfig{
|
|
||||||
// URLs: []url.URL{
|
|
||||||
// *derpMap,
|
|
||||||
// },
|
|
||||||
// AutoUpdate: false,
|
|
||||||
// UpdateFrequency: 1 * time.Minute,
|
|
||||||
// },
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return config
|
|
||||||
// }
|
|
||||||
|
|
||||||
// TODO: Reuse the actual configuration object above.
|
|
||||||
func DefaultConfigYAML() string {
|
|
||||||
yaml := `
|
|
||||||
log:
|
|
||||||
level: trace
|
|
||||||
acl_policy_path: ""
|
|
||||||
db_type: sqlite3
|
|
||||||
db_path: /tmp/integration_test_db.sqlite3
|
|
||||||
ephemeral_node_inactivity_timeout: 30m
|
|
||||||
node_update_check_interval: 10s
|
|
||||||
ip_prefixes:
|
|
||||||
- fd7a:115c:a1e0::/48
|
|
||||||
- 100.64.0.0/10
|
|
||||||
dns_config:
|
|
||||||
base_domain: headscale.net
|
|
||||||
magic_dns: true
|
|
||||||
domains: []
|
|
||||||
nameservers:
|
|
||||||
- 127.0.0.11
|
|
||||||
- 1.1.1.1
|
|
||||||
private_key_path: /tmp/private.key
|
|
||||||
noise:
|
|
||||||
private_key_path: /tmp/noise_private.key
|
|
||||||
listen_addr: 0.0.0.0:8080
|
|
||||||
metrics_listen_addr: 127.0.0.1:9090
|
|
||||||
server_url: http://headscale:8080
|
|
||||||
|
|
||||||
derp:
|
|
||||||
urls:
|
|
||||||
- https://controlplane.tailscale.com/derpmap/default
|
|
||||||
auto_update_enabled: false
|
|
||||||
update_frequency: 1m
|
|
||||||
`
|
|
||||||
|
|
||||||
return yaml
|
|
||||||
}
|
|
||||||
@@ -1,346 +0,0 @@
|
|||||||
package hsic
|
|
||||||
|
|
||||||
import (
|
|
||||||
"archive/tar"
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/juanfont/headscale"
|
|
||||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
|
||||||
"github.com/juanfont/headscale/integration/dockertestutil"
|
|
||||||
"github.com/ory/dockertest/v3"
|
|
||||||
"github.com/ory/dockertest/v3/docker"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
hsicHashLength = 6
|
|
||||||
dockerContextPath = "../."
|
|
||||||
aclPolicyPath = "/etc/headscale/acl.hujson"
|
|
||||||
)
|
|
||||||
|
|
||||||
var errHeadscaleStatusCodeNotOk = errors.New("headscale status code not ok")
|
|
||||||
|
|
||||||
type HeadscaleInContainer struct {
|
|
||||||
hostname string
|
|
||||||
port int
|
|
||||||
|
|
||||||
pool *dockertest.Pool
|
|
||||||
container *dockertest.Resource
|
|
||||||
network *dockertest.Network
|
|
||||||
|
|
||||||
// optional config
|
|
||||||
aclPolicy *headscale.ACLPolicy
|
|
||||||
env []string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Option = func(c *HeadscaleInContainer)
|
|
||||||
|
|
||||||
func WithACLPolicy(acl *headscale.ACLPolicy) Option {
|
|
||||||
return func(hsic *HeadscaleInContainer) {
|
|
||||||
hsic.aclPolicy = acl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithConfigEnv(configEnv map[string]string) Option {
|
|
||||||
return func(hsic *HeadscaleInContainer) {
|
|
||||||
env := []string{}
|
|
||||||
|
|
||||||
for key, value := range configEnv {
|
|
||||||
env = append(env, fmt.Sprintf("%s=%s", key, value))
|
|
||||||
}
|
|
||||||
|
|
||||||
hsic.env = env
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(
|
|
||||||
pool *dockertest.Pool,
|
|
||||||
port int,
|
|
||||||
network *dockertest.Network,
|
|
||||||
opts ...Option,
|
|
||||||
) (*HeadscaleInContainer, error) {
|
|
||||||
hash, err := headscale.GenerateRandomStringDNSSafe(hsicHashLength)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
hostname := fmt.Sprintf("hs-%s", hash)
|
|
||||||
portProto := fmt.Sprintf("%d/tcp", port)
|
|
||||||
|
|
||||||
hsic := &HeadscaleInContainer{
|
|
||||||
hostname: hostname,
|
|
||||||
port: port,
|
|
||||||
|
|
||||||
pool: pool,
|
|
||||||
network: network,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, opt := range opts {
|
|
||||||
opt(hsic)
|
|
||||||
}
|
|
||||||
|
|
||||||
if hsic.aclPolicy != nil {
|
|
||||||
hsic.env = append(hsic.env, fmt.Sprintf("HEADSCALE_ACL_POLICY_PATH=%s", aclPolicyPath))
|
|
||||||
}
|
|
||||||
|
|
||||||
headscaleBuildOptions := &dockertest.BuildOptions{
|
|
||||||
Dockerfile: "Dockerfile.debug",
|
|
||||||
ContextDir: dockerContextPath,
|
|
||||||
}
|
|
||||||
|
|
||||||
runOptions := &dockertest.RunOptions{
|
|
||||||
Name: hostname,
|
|
||||||
ExposedPorts: []string{portProto},
|
|
||||||
Networks: []*dockertest.Network{network},
|
|
||||||
// Cmd: []string{"headscale", "serve"},
|
|
||||||
// TODO(kradalby): Get rid of this hack, we currently need to give us some
|
|
||||||
// to inject the headscale configuration further down.
|
|
||||||
Entrypoint: []string{"/bin/bash", "-c", "/bin/sleep 3 ; headscale serve"},
|
|
||||||
Env: hsic.env,
|
|
||||||
}
|
|
||||||
|
|
||||||
// dockertest isnt very good at handling containers that has already
|
|
||||||
// been created, this is an attempt to make sure this container isnt
|
|
||||||
// present.
|
|
||||||
err = pool.RemoveContainerByName(hostname)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
container, err := pool.BuildAndRunWithBuildOptions(
|
|
||||||
headscaleBuildOptions,
|
|
||||||
runOptions,
|
|
||||||
dockertestutil.DockerRestartPolicy,
|
|
||||||
dockertestutil.DockerAllowLocalIPv6,
|
|
||||||
dockertestutil.DockerAllowNetworkAdministration,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("could not start headscale container: %w", err)
|
|
||||||
}
|
|
||||||
log.Printf("Created %s container\n", hostname)
|
|
||||||
|
|
||||||
hsic.container = container
|
|
||||||
|
|
||||||
err = hsic.WriteFile("/etc/headscale/config.yaml", []byte(DefaultConfigYAML()))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to write headscale config to container: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if hsic.aclPolicy != nil {
|
|
||||||
data, err := json.Marshal(hsic.aclPolicy)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to marshal ACL Policy to JSON: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = hsic.WriteFile(aclPolicyPath, data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to write ACL policy to container: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return hsic, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *HeadscaleInContainer) Shutdown() error {
|
|
||||||
return t.pool.Purge(t.container)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *HeadscaleInContainer) Execute(
|
|
||||||
command []string,
|
|
||||||
) (string, error) {
|
|
||||||
log.Println("command", command)
|
|
||||||
log.Printf("running command for %s\n", t.hostname)
|
|
||||||
stdout, stderr, err := dockertestutil.ExecuteCommand(
|
|
||||||
t.container,
|
|
||||||
command,
|
|
||||||
[]string{},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("command stderr: %s\n", stderr)
|
|
||||||
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if stdout != "" {
|
|
||||||
log.Printf("command stdout: %s\n", stdout)
|
|
||||||
}
|
|
||||||
|
|
||||||
return stdout, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *HeadscaleInContainer) GetIP() string {
|
|
||||||
return t.container.GetIPInNetwork(t.network)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *HeadscaleInContainer) GetPort() string {
|
|
||||||
return fmt.Sprintf("%d", t.port)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *HeadscaleInContainer) GetHealthEndpoint() string {
|
|
||||||
hostEndpoint := fmt.Sprintf("%s:%d",
|
|
||||||
t.GetIP(),
|
|
||||||
t.port)
|
|
||||||
|
|
||||||
return fmt.Sprintf("http://%s/health", hostEndpoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *HeadscaleInContainer) GetEndpoint() string {
|
|
||||||
hostEndpoint := fmt.Sprintf("%s:%d",
|
|
||||||
t.GetIP(),
|
|
||||||
t.port)
|
|
||||||
|
|
||||||
return fmt.Sprintf("http://%s", hostEndpoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *HeadscaleInContainer) WaitForReady() error {
|
|
||||||
url := t.GetHealthEndpoint()
|
|
||||||
|
|
||||||
log.Printf("waiting for headscale to be ready at %s", url)
|
|
||||||
|
|
||||||
return t.pool.Retry(func() error {
|
|
||||||
resp, err := http.Get(url) //nolint
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("headscale is not ready: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return errHeadscaleStatusCodeNotOk
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *HeadscaleInContainer) CreateNamespace(
|
|
||||||
namespace string,
|
|
||||||
) error {
|
|
||||||
command := []string{"headscale", "namespaces", "create", namespace}
|
|
||||||
|
|
||||||
_, _, err := dockertestutil.ExecuteCommand(
|
|
||||||
t.container,
|
|
||||||
command,
|
|
||||||
[]string{},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *HeadscaleInContainer) CreateAuthKey(
|
|
||||||
namespace string,
|
|
||||||
) (*v1.PreAuthKey, error) {
|
|
||||||
command := []string{
|
|
||||||
"headscale",
|
|
||||||
"--namespace",
|
|
||||||
namespace,
|
|
||||||
"preauthkeys",
|
|
||||||
"create",
|
|
||||||
"--reusable",
|
|
||||||
"--expiration",
|
|
||||||
"24h",
|
|
||||||
"--output",
|
|
||||||
"json",
|
|
||||||
}
|
|
||||||
|
|
||||||
result, _, err := dockertestutil.ExecuteCommand(
|
|
||||||
t.container,
|
|
||||||
command,
|
|
||||||
[]string{},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to execute create auth key command: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var preAuthKey v1.PreAuthKey
|
|
||||||
err = json.Unmarshal([]byte(result), &preAuthKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to unmarshal auth key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &preAuthKey, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *HeadscaleInContainer) ListMachinesInNamespace(
|
|
||||||
namespace string,
|
|
||||||
) ([]*v1.Machine, error) {
|
|
||||||
command := []string{"headscale", "--namespace", namespace, "nodes", "list", "--output", "json"}
|
|
||||||
|
|
||||||
result, _, err := dockertestutil.ExecuteCommand(
|
|
||||||
t.container,
|
|
||||||
command,
|
|
||||||
[]string{},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to execute list node command: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var nodes []*v1.Machine
|
|
||||||
err = json.Unmarshal([]byte(result), &nodes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to unmarshal nodes: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nodes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *HeadscaleInContainer) WriteFile(path string, data []byte) error {
|
|
||||||
dirPath, fileName := filepath.Split(path)
|
|
||||||
|
|
||||||
file := bytes.NewReader(data)
|
|
||||||
|
|
||||||
buf := bytes.NewBuffer([]byte{})
|
|
||||||
|
|
||||||
tarWriter := tar.NewWriter(buf)
|
|
||||||
|
|
||||||
header := &tar.Header{
|
|
||||||
Name: fileName,
|
|
||||||
Size: file.Size(),
|
|
||||||
// Mode: int64(stat.Mode()),
|
|
||||||
// ModTime: stat.ModTime(),
|
|
||||||
}
|
|
||||||
|
|
||||||
err := tarWriter.WriteHeader(header)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed write file header to tar: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = io.Copy(tarWriter, file)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to copy file to tar: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = tarWriter.Close()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to close tar: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("tar: %s", buf.String())
|
|
||||||
|
|
||||||
// Ensure the directory is present inside the container
|
|
||||||
_, err = t.Execute([]string{"mkdir", "-p", dirPath})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to ensure directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = t.pool.Client.UploadToContainer(
|
|
||||||
t.container.Container.ID,
|
|
||||||
docker.UploadToContainerOptions{
|
|
||||||
NoOverwriteDirNonDir: false,
|
|
||||||
Path: dirPath,
|
|
||||||
InputStream: bytes.NewReader(buf.Bytes()),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,425 +0,0 @@
|
|||||||
package integration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/netip"
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/juanfont/headscale"
|
|
||||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
|
||||||
"github.com/juanfont/headscale/integration/dockertestutil"
|
|
||||||
"github.com/juanfont/headscale/integration/hsic"
|
|
||||||
"github.com/juanfont/headscale/integration/tsic"
|
|
||||||
"github.com/ory/dockertest/v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
scenarioHashLength = 6
|
|
||||||
maxWait = 60 * time.Second
|
|
||||||
headscalePort = 8080
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
errNoHeadscaleAvailable = errors.New("no headscale available")
|
|
||||||
errNoNamespaceAvailable = errors.New("no namespace available")
|
|
||||||
TailscaleVersions = []string{
|
|
||||||
"head",
|
|
||||||
"unstable",
|
|
||||||
"1.32.1",
|
|
||||||
"1.30.2",
|
|
||||||
"1.28.0",
|
|
||||||
"1.26.2",
|
|
||||||
"1.24.2",
|
|
||||||
"1.22.2",
|
|
||||||
"1.20.4",
|
|
||||||
"1.18.2",
|
|
||||||
"1.16.2",
|
|
||||||
|
|
||||||
// These versions seem to fail when fetching from apt.
|
|
||||||
// "1.14.6",
|
|
||||||
// "1.12.4",
|
|
||||||
// "1.10.2",
|
|
||||||
// "1.8.7",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
type Namespace struct {
|
|
||||||
Clients map[string]TailscaleClient
|
|
||||||
|
|
||||||
createWaitGroup sync.WaitGroup
|
|
||||||
joinWaitGroup sync.WaitGroup
|
|
||||||
syncWaitGroup sync.WaitGroup
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(kradalby): make control server configurable, test correctness with Tailscale SaaS.
|
|
||||||
type Scenario struct {
|
|
||||||
// TODO(kradalby): support multiple headcales for later, currently only
|
|
||||||
// use one.
|
|
||||||
controlServers map[string]ControlServer
|
|
||||||
|
|
||||||
namespaces map[string]*Namespace
|
|
||||||
|
|
||||||
pool *dockertest.Pool
|
|
||||||
network *dockertest.Network
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewScenario() (*Scenario, error) {
|
|
||||||
hash, err := headscale.GenerateRandomStringDNSSafe(scenarioHashLength)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
pool, err := dockertest.NewPool("")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("could not connect to docker: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
pool.MaxWait = maxWait
|
|
||||||
|
|
||||||
networkName := fmt.Sprintf("hs-%s", hash)
|
|
||||||
if overrideNetworkName := os.Getenv("HEADSCALE_TEST_NETWORK_NAME"); overrideNetworkName != "" {
|
|
||||||
networkName = overrideNetworkName
|
|
||||||
}
|
|
||||||
|
|
||||||
network, err := dockertestutil.GetFirstOrCreateNetwork(pool, networkName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create or get network: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We run the test suite in a docker container that calls a couple of endpoints for
|
|
||||||
// readiness checks, this ensures that we can run the tests with individual networks
|
|
||||||
// and have the client reach the different containers
|
|
||||||
err = dockertestutil.AddContainerToNetwork(pool, network, "headscale-test-suite")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to add test suite container to network: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Scenario{
|
|
||||||
controlServers: make(map[string]ControlServer),
|
|
||||||
namespaces: make(map[string]*Namespace),
|
|
||||||
|
|
||||||
pool: pool,
|
|
||||||
network: network,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Scenario) Shutdown() error {
|
|
||||||
for _, control := range s.controlServers {
|
|
||||||
err := control.Shutdown()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to tear down control: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for namespaceName, namespace := range s.namespaces {
|
|
||||||
for _, client := range namespace.Clients {
|
|
||||||
log.Printf("removing client %s in namespace %s", client.Hostname(), namespaceName)
|
|
||||||
err := client.Shutdown()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to tear down client: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.pool.RemoveNetwork(s.network); err != nil {
|
|
||||||
return fmt.Errorf("failed to remove network: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(kradalby): This seem redundant to the previous call
|
|
||||||
// if err := s.network.Close(); err != nil {
|
|
||||||
// return fmt.Errorf("failed to tear down network: %w", err)
|
|
||||||
// }
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Scenario) Namespaces() []string {
|
|
||||||
namespaces := make([]string, 0)
|
|
||||||
for namespace := range s.namespaces {
|
|
||||||
namespaces = append(namespaces, namespace)
|
|
||||||
}
|
|
||||||
|
|
||||||
return namespaces
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Headscale related stuff
|
|
||||||
// Note: These functions assume that there is a _single_ headscale instance for now
|
|
||||||
|
|
||||||
// TODO(kradalby): make port and headscale configurable, multiple instances support?
|
|
||||||
func (s *Scenario) StartHeadscale() error {
|
|
||||||
headscale, err := hsic.New(s.pool, headscalePort, s.network,
|
|
||||||
hsic.WithACLPolicy(
|
|
||||||
&headscale.ACLPolicy{
|
|
||||||
ACLs: []headscale.ACL{
|
|
||||||
{
|
|
||||||
Action: "accept",
|
|
||||||
Sources: []string{"*"},
|
|
||||||
Destinations: []string{"*:*"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create headscale container: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.controlServers["headscale"] = headscale
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Scenario) Headscale() *hsic.HeadscaleInContainer {
|
|
||||||
//nolint
|
|
||||||
return s.controlServers["headscale"].(*hsic.HeadscaleInContainer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Scenario) CreatePreAuthKey(namespace string) (*v1.PreAuthKey, error) {
|
|
||||||
if headscale, ok := s.controlServers["headscale"]; ok {
|
|
||||||
key, err := headscale.CreateAuthKey(namespace)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create namespace: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return key, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("failed to create namespace: %w", errNoHeadscaleAvailable)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Scenario) CreateNamespace(namespace string) error {
|
|
||||||
if headscale, ok := s.controlServers["headscale"]; ok {
|
|
||||||
err := headscale.CreateNamespace(namespace)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create namespace: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.namespaces[namespace] = &Namespace{
|
|
||||||
Clients: make(map[string]TailscaleClient),
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("failed to create namespace: %w", errNoHeadscaleAvailable)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Client related stuff
|
|
||||||
|
|
||||||
func (s *Scenario) CreateTailscaleNodesInNamespace(
|
|
||||||
namespaceStr string,
|
|
||||||
requestedVersion string,
|
|
||||||
count int,
|
|
||||||
) error {
|
|
||||||
if namespace, ok := s.namespaces[namespaceStr]; ok {
|
|
||||||
for i := 0; i < count; i++ {
|
|
||||||
version := requestedVersion
|
|
||||||
if requestedVersion == "all" {
|
|
||||||
version = TailscaleVersions[i%len(TailscaleVersions)]
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace.createWaitGroup.Add(1)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer namespace.createWaitGroup.Done()
|
|
||||||
|
|
||||||
// TODO(kradalby): error handle this
|
|
||||||
tsClient, err := tsic.New(s.pool, version, s.network)
|
|
||||||
if err != nil {
|
|
||||||
// return fmt.Errorf("failed to add tailscale node: %w", err)
|
|
||||||
log.Printf("failed to add tailscale node: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace.Clients[tsClient.Hostname()] = tsClient
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
namespace.createWaitGroup.Wait()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("failed to add tailscale node: %w", errNoNamespaceAvailable)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Scenario) RunTailscaleUp(
|
|
||||||
namespaceStr, loginServer, authKey string,
|
|
||||||
) error {
|
|
||||||
if namespace, ok := s.namespaces[namespaceStr]; ok {
|
|
||||||
for _, client := range namespace.Clients {
|
|
||||||
namespace.joinWaitGroup.Add(1)
|
|
||||||
|
|
||||||
go func(c TailscaleClient) {
|
|
||||||
defer namespace.joinWaitGroup.Done()
|
|
||||||
|
|
||||||
// TODO(kradalby): error handle this
|
|
||||||
_ = c.Up(loginServer, authKey)
|
|
||||||
}(client)
|
|
||||||
}
|
|
||||||
namespace.joinWaitGroup.Wait()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("failed to up tailscale node: %w", errNoNamespaceAvailable)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Scenario) CountTailscale() int {
|
|
||||||
count := 0
|
|
||||||
|
|
||||||
for _, namespace := range s.namespaces {
|
|
||||||
count += len(namespace.Clients)
|
|
||||||
}
|
|
||||||
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Scenario) WaitForTailscaleSync() error {
|
|
||||||
tsCount := s.CountTailscale()
|
|
||||||
|
|
||||||
for _, namespace := range s.namespaces {
|
|
||||||
for _, client := range namespace.Clients {
|
|
||||||
namespace.syncWaitGroup.Add(1)
|
|
||||||
|
|
||||||
go func(c TailscaleClient) {
|
|
||||||
defer namespace.syncWaitGroup.Done()
|
|
||||||
|
|
||||||
// TODO(kradalby): error handle this
|
|
||||||
_ = c.WaitForPeers(tsCount)
|
|
||||||
}(client)
|
|
||||||
}
|
|
||||||
namespace.syncWaitGroup.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateHeadscaleEnv is a conventient method returning a set up Headcale
|
|
||||||
// test environment with nodes of all versions, joined to the server with X
|
|
||||||
// namespaces.
|
|
||||||
func (s *Scenario) CreateHeadscaleEnv(namespaces map[string]int) error {
|
|
||||||
err := s.StartHeadscale()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.Headscale().WaitForReady()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for namespaceName, clientCount := range namespaces {
|
|
||||||
err = s.CreateNamespace(namespaceName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.CreateTailscaleNodesInNamespace(namespaceName, "all", clientCount)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
key, err := s.CreatePreAuthKey(namespaceName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.RunTailscaleUp(namespaceName, s.Headscale().GetEndpoint(), key.GetKey())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Scenario) GetIPs(namespace string) ([]netip.Addr, error) {
|
|
||||||
var ips []netip.Addr
|
|
||||||
if ns, ok := s.namespaces[namespace]; ok {
|
|
||||||
for _, client := range ns.Clients {
|
|
||||||
clientIps, err := client.IPs()
|
|
||||||
if err != nil {
|
|
||||||
return ips, fmt.Errorf("failed to get ips: %w", err)
|
|
||||||
}
|
|
||||||
ips = append(ips, clientIps...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ips, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return ips, fmt.Errorf("failed to get ips: %w", errNoNamespaceAvailable)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Scenario) GetClients(namespace string) ([]TailscaleClient, error) {
|
|
||||||
var clients []TailscaleClient
|
|
||||||
if ns, ok := s.namespaces[namespace]; ok {
|
|
||||||
for _, client := range ns.Clients {
|
|
||||||
clients = append(clients, client)
|
|
||||||
}
|
|
||||||
|
|
||||||
return clients, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return clients, fmt.Errorf("failed to get clients: %w", errNoNamespaceAvailable)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Scenario) ListTailscaleClients(namespaces ...string) ([]TailscaleClient, error) {
|
|
||||||
var allClients []TailscaleClient
|
|
||||||
|
|
||||||
if len(namespaces) == 0 {
|
|
||||||
namespaces = s.Namespaces()
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, namespace := range namespaces {
|
|
||||||
clients, err := s.GetClients(namespace)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
allClients = append(allClients, clients...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return allClients, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Scenario) ListTailscaleClientsIPs(namespaces ...string) ([]netip.Addr, error) {
|
|
||||||
var allIps []netip.Addr
|
|
||||||
|
|
||||||
if len(namespaces) == 0 {
|
|
||||||
namespaces = s.Namespaces()
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, namespace := range namespaces {
|
|
||||||
ips, err := s.GetIPs(namespace)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
allIps = append(allIps, ips...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return allIps, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Scenario) ListTailscaleClientsFQDNs(namespaces ...string) ([]string, error) {
|
|
||||||
allFQDNs := make([]string, 0)
|
|
||||||
|
|
||||||
clients, err := s.ListTailscaleClients(namespaces...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, client := range clients {
|
|
||||||
fqdn, err := client.FQDN()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
allFQDNs = append(allFQDNs, fqdn)
|
|
||||||
}
|
|
||||||
|
|
||||||
return allFQDNs, nil
|
|
||||||
}
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
package integration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/juanfont/headscale/integration/dockertestutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
// This file is intendet to "test the test framework", by proxy it will also test
|
|
||||||
// some Headcsale/Tailscale stuff, but mostly in very simple ways.
|
|
||||||
|
|
||||||
func IntegrationSkip(t *testing.T) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
if !dockertestutil.IsRunningInContainer() {
|
|
||||||
t.Skip("not running in docker, skipping")
|
|
||||||
}
|
|
||||||
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("skipping integration tests due to short flag")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHeadscale(t *testing.T) {
|
|
||||||
IntegrationSkip(t)
|
|
||||||
|
|
||||||
var err error
|
|
||||||
|
|
||||||
namespace := "test-space"
|
|
||||||
|
|
||||||
scenario, err := NewScenario()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to create scenario: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("start-headscale", func(t *testing.T) {
|
|
||||||
err = scenario.StartHeadscale()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to create start headcale: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = scenario.Headscale().WaitForReady()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("headscale failed to become ready: %s", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("create-namespace", func(t *testing.T) {
|
|
||||||
err := scenario.CreateNamespace(namespace)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to create namespace: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := scenario.namespaces[namespace]; !ok {
|
|
||||||
t.Errorf("namespace is not in scenario")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("create-auth-key", func(t *testing.T) {
|
|
||||||
_, err := scenario.CreatePreAuthKey(namespace)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to create preauthkey: %s", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
err = scenario.Shutdown()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to tear down scenario: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCreateTailscale(t *testing.T) {
|
|
||||||
IntegrationSkip(t)
|
|
||||||
|
|
||||||
namespace := "only-create-containers"
|
|
||||||
|
|
||||||
scenario, err := NewScenario()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to create scenario: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
scenario.namespaces[namespace] = &Namespace{
|
|
||||||
Clients: make(map[string]TailscaleClient),
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("create-tailscale", func(t *testing.T) {
|
|
||||||
err := scenario.CreateTailscaleNodesInNamespace(namespace, "all", 3)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to add tailscale nodes: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if clients := len(scenario.namespaces[namespace].Clients); clients != 3 {
|
|
||||||
t.Errorf("wrong number of tailscale clients: %d != %d", clients, 3)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(kradalby): Test "all" version logic
|
|
||||||
})
|
|
||||||
|
|
||||||
err = scenario.Shutdown()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to tear down scenario: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTailscaleNodesJoiningHeadcale(t *testing.T) {
|
|
||||||
IntegrationSkip(t)
|
|
||||||
|
|
||||||
var err error
|
|
||||||
|
|
||||||
namespace := "join-node-test"
|
|
||||||
|
|
||||||
count := 1
|
|
||||||
|
|
||||||
scenario, err := NewScenario()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to create scenario: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("start-headscale", func(t *testing.T) {
|
|
||||||
err = scenario.StartHeadscale()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to create start headcale: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
headscale := scenario.Headscale()
|
|
||||||
err = headscale.WaitForReady()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("headscale failed to become ready: %s", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("create-namespace", func(t *testing.T) {
|
|
||||||
err := scenario.CreateNamespace(namespace)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to create namespace: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := scenario.namespaces[namespace]; !ok {
|
|
||||||
t.Errorf("namespace is not in scenario")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("create-tailscale", func(t *testing.T) {
|
|
||||||
err := scenario.CreateTailscaleNodesInNamespace(namespace, "1.30.2", count)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to add tailscale nodes: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if clients := len(scenario.namespaces[namespace].Clients); clients != count {
|
|
||||||
t.Errorf("wrong number of tailscale clients: %d != %d", clients, count)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("join-headscale", func(t *testing.T) {
|
|
||||||
key, err := scenario.CreatePreAuthKey(namespace)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to create preauthkey: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = scenario.RunTailscaleUp(namespace, scenario.Headscale().GetEndpoint(), key.GetKey())
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to login: %s", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("get-ips", func(t *testing.T) {
|
|
||||||
ips, err := scenario.GetIPs(namespace)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to get tailscale ips: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ips) != count*2 {
|
|
||||||
t.Errorf("got the wrong amount of tailscale ips, %d != %d", len(ips), count*2)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
err = scenario.Shutdown()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to tear down scenario: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package integration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/netip"
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"tailscale.com/ipn/ipnstate"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TailscaleClient interface {
|
|
||||||
Hostname() string
|
|
||||||
Shutdown() error
|
|
||||||
Version() string
|
|
||||||
Execute(command []string) (string, string, error)
|
|
||||||
Up(loginServer, authKey string) error
|
|
||||||
UpWithLoginURL(loginServer string) (*url.URL, error)
|
|
||||||
IPs() ([]netip.Addr, error)
|
|
||||||
FQDN() (string, error)
|
|
||||||
Status() (*ipnstate.Status, error)
|
|
||||||
WaitForReady() error
|
|
||||||
WaitForPeers(expected int) error
|
|
||||||
Ping(hostnameOrIP string) error
|
|
||||||
}
|
|
||||||
@@ -1,359 +0,0 @@
|
|||||||
package tsic
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/netip"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/cenkalti/backoff/v4"
|
|
||||||
"github.com/juanfont/headscale"
|
|
||||||
"github.com/juanfont/headscale/integration/dockertestutil"
|
|
||||||
"github.com/ory/dockertest/v3"
|
|
||||||
"github.com/ory/dockertest/v3/docker"
|
|
||||||
"tailscale.com/ipn/ipnstate"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
tsicHashLength = 6
|
|
||||||
dockerContextPath = "../."
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
errTailscalePingFailed = errors.New("ping failed")
|
|
||||||
errTailscaleNotLoggedIn = errors.New("tailscale not logged in")
|
|
||||||
errTailscaleWrongPeerCount = errors.New("wrong peer count")
|
|
||||||
errTailscaleNotConnected = errors.New("tailscale not connected")
|
|
||||||
)
|
|
||||||
|
|
||||||
type TailscaleInContainer struct {
|
|
||||||
version string
|
|
||||||
hostname string
|
|
||||||
|
|
||||||
pool *dockertest.Pool
|
|
||||||
container *dockertest.Resource
|
|
||||||
network *dockertest.Network
|
|
||||||
|
|
||||||
// "cache"
|
|
||||||
ips []netip.Addr
|
|
||||||
fqdn string
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(
|
|
||||||
pool *dockertest.Pool,
|
|
||||||
version string,
|
|
||||||
network *dockertest.Network,
|
|
||||||
) (*TailscaleInContainer, error) {
|
|
||||||
hash, err := headscale.GenerateRandomStringDNSSafe(tsicHashLength)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
hostname := fmt.Sprintf("ts-%s-%s", strings.ReplaceAll(version, ".", "-"), hash)
|
|
||||||
|
|
||||||
// TODO(kradalby): figure out why we need to "refresh" the network here.
|
|
||||||
// network, err = dockertestutil.GetFirstOrCreateNetwork(pool, network.Network.Name)
|
|
||||||
// if err != nil {
|
|
||||||
// return nil, err
|
|
||||||
// }
|
|
||||||
|
|
||||||
tailscaleOptions := &dockertest.RunOptions{
|
|
||||||
Name: hostname,
|
|
||||||
Networks: []*dockertest.Network{network},
|
|
||||||
Cmd: []string{
|
|
||||||
"tailscaled", "--tun=tsdev",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// dockertest isnt very good at handling containers that has already
|
|
||||||
// been created, this is an attempt to make sure this container isnt
|
|
||||||
// present.
|
|
||||||
err = pool.RemoveContainerByName(hostname)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
container, err := pool.BuildAndRunWithBuildOptions(
|
|
||||||
createTailscaleBuildOptions(version),
|
|
||||||
tailscaleOptions,
|
|
||||||
dockertestutil.DockerRestartPolicy,
|
|
||||||
dockertestutil.DockerAllowLocalIPv6,
|
|
||||||
dockertestutil.DockerAllowNetworkAdministration,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("could not start tailscale container: %w", err)
|
|
||||||
}
|
|
||||||
log.Printf("Created %s container\n", hostname)
|
|
||||||
|
|
||||||
return &TailscaleInContainer{
|
|
||||||
version: version,
|
|
||||||
hostname: hostname,
|
|
||||||
|
|
||||||
pool: pool,
|
|
||||||
container: container,
|
|
||||||
network: network,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TailscaleInContainer) Shutdown() error {
|
|
||||||
return t.pool.Purge(t.container)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TailscaleInContainer) Hostname() string {
|
|
||||||
return t.hostname
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TailscaleInContainer) Version() string {
|
|
||||||
return t.version
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TailscaleInContainer) Execute(
|
|
||||||
command []string,
|
|
||||||
) (string, string, error) {
|
|
||||||
log.Println("command", command)
|
|
||||||
log.Printf("running command for %s\n", t.hostname)
|
|
||||||
stdout, stderr, err := dockertestutil.ExecuteCommand(
|
|
||||||
t.container,
|
|
||||||
command,
|
|
||||||
[]string{},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("command stderr: %s\n", stderr)
|
|
||||||
|
|
||||||
if stdout != "" {
|
|
||||||
log.Printf("command stdout: %s\n", stdout)
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Contains(stderr, "NeedsLogin") {
|
|
||||||
return stdout, stderr, errTailscaleNotLoggedIn
|
|
||||||
}
|
|
||||||
|
|
||||||
return stdout, stderr, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return stdout, stderr, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TailscaleInContainer) Up(
|
|
||||||
loginServer, authKey string,
|
|
||||||
) error {
|
|
||||||
command := []string{
|
|
||||||
"tailscale",
|
|
||||||
"up",
|
|
||||||
"-login-server",
|
|
||||||
loginServer,
|
|
||||||
"--authkey",
|
|
||||||
authKey,
|
|
||||||
"--hostname",
|
|
||||||
t.hostname,
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, _, err := t.Execute(command); err != nil {
|
|
||||||
return fmt.Errorf("failed to join tailscale client: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TailscaleInContainer) UpWithLoginURL(
|
|
||||||
loginServer string,
|
|
||||||
) (*url.URL, error) {
|
|
||||||
command := []string{
|
|
||||||
"tailscale",
|
|
||||||
"up",
|
|
||||||
"-login-server",
|
|
||||||
loginServer,
|
|
||||||
"--hostname",
|
|
||||||
t.hostname,
|
|
||||||
}
|
|
||||||
|
|
||||||
_, stderr, _ := t.Execute(command)
|
|
||||||
|
|
||||||
urlStr := strings.ReplaceAll(stderr, "\nTo authenticate, visit:\n\n\t", "")
|
|
||||||
urlStr = strings.TrimSpace(urlStr)
|
|
||||||
|
|
||||||
// parse URL
|
|
||||||
loginURL, err := url.Parse(urlStr)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Could not parse login URL: %s", err)
|
|
||||||
log.Printf("Original join command result: %s", stderr)
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return loginURL, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TailscaleInContainer) IPs() ([]netip.Addr, error) {
|
|
||||||
if t.ips != nil && len(t.ips) != 0 {
|
|
||||||
return t.ips, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ips := make([]netip.Addr, 0)
|
|
||||||
|
|
||||||
command := []string{
|
|
||||||
"tailscale",
|
|
||||||
"ip",
|
|
||||||
}
|
|
||||||
|
|
||||||
result, _, err := t.Execute(command)
|
|
||||||
if err != nil {
|
|
||||||
return []netip.Addr{}, fmt.Errorf("failed to join tailscale client: %w", 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 = append(ips, ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ips, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TailscaleInContainer) Status() (*ipnstate.Status, error) {
|
|
||||||
command := []string{
|
|
||||||
"tailscale",
|
|
||||||
"status",
|
|
||||||
"--json",
|
|
||||||
}
|
|
||||||
|
|
||||||
result, _, err := t.Execute(command)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to execute tailscale status command: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var status ipnstate.Status
|
|
||||||
err = json.Unmarshal([]byte(result), &status)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to unmarshal tailscale status: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &status, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TailscaleInContainer) FQDN() (string, error) {
|
|
||||||
if t.fqdn != "" {
|
|
||||||
return t.fqdn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
status, err := t.Status()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to get FQDN: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return status.Self.DNSName, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TailscaleInContainer) WaitForReady() error {
|
|
||||||
return t.pool.Retry(func() error {
|
|
||||||
status, err := t.Status()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to fetch tailscale status: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if status.CurrentTailnet != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return errTailscaleNotConnected
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TailscaleInContainer) WaitForPeers(expected int) error {
|
|
||||||
return t.pool.Retry(func() error {
|
|
||||||
status, err := t.Status()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to fetch tailscale status: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if peers := status.Peers(); len(peers) != expected {
|
|
||||||
return errTailscaleWrongPeerCount
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(kradalby): Make multiping, go routine magic.
|
|
||||||
func (t *TailscaleInContainer) Ping(hostnameOrIP string) error {
|
|
||||||
return t.pool.Retry(func() error {
|
|
||||||
command := []string{
|
|
||||||
"tailscale", "ping",
|
|
||||||
"--timeout=1s",
|
|
||||||
"--c=10",
|
|
||||||
"--until-direct=true",
|
|
||||||
hostnameOrIP,
|
|
||||||
}
|
|
||||||
|
|
||||||
result, _, err := t.Execute(command)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf(
|
|
||||||
"failed to run ping command from %s to %s, err: %s",
|
|
||||||
t.Hostname(),
|
|
||||||
hostnameOrIP,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.Contains(result, "pong") && !strings.Contains(result, "is local") {
|
|
||||||
return backoff.Permanent(errTailscalePingFailed)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func createTailscaleBuildOptions(version string) *dockertest.BuildOptions {
|
|
||||||
var tailscaleBuildOptions *dockertest.BuildOptions
|
|
||||||
switch version {
|
|
||||||
case "head":
|
|
||||||
tailscaleBuildOptions = &dockertest.BuildOptions{
|
|
||||||
Dockerfile: "Dockerfile.tailscale-HEAD",
|
|
||||||
ContextDir: dockerContextPath,
|
|
||||||
BuildArgs: []docker.BuildArg{},
|
|
||||||
}
|
|
||||||
case "unstable":
|
|
||||||
tailscaleBuildOptions = &dockertest.BuildOptions{
|
|
||||||
Dockerfile: "Dockerfile.tailscale",
|
|
||||||
ContextDir: dockerContextPath,
|
|
||||||
BuildArgs: []docker.BuildArg{
|
|
||||||
{
|
|
||||||
Name: "TAILSCALE_VERSION",
|
|
||||||
Value: "*", // Installs the latest version https://askubuntu.com/a/824926
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "TAILSCALE_CHANNEL",
|
|
||||||
Value: "unstable",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
tailscaleBuildOptions = &dockertest.BuildOptions{
|
|
||||||
Dockerfile: "Dockerfile.tailscale",
|
|
||||||
ContextDir: dockerContextPath,
|
|
||||||
BuildArgs: []docker.BuildArg{
|
|
||||||
{
|
|
||||||
Name: "TAILSCALE_VERSION",
|
|
||||||
Value: version,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "TAILSCALE_CHANNEL",
|
|
||||||
Value: "stable",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tailscaleBuildOptions
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
// nolint
|
//go:build integration
|
||||||
|
// +build integration
|
||||||
|
|
||||||
package headscale
|
package headscale
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -7,16 +9,20 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||||
"github.com/ory/dockertest/v3"
|
"github.com/ory/dockertest/v3"
|
||||||
"github.com/ory/dockertest/v3/docker"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
headscaleCLIContainerName = "headscale-cli"
|
||||||
|
)
|
||||||
|
|
||||||
type IntegrationCLITestSuite struct {
|
type IntegrationCLITestSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
stats *suite.SuiteInformation
|
stats *suite.SuiteInformation
|
||||||
@@ -27,11 +33,7 @@ type IntegrationCLITestSuite struct {
|
|||||||
env []string
|
env []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIntegrationCLITestSuite(t *testing.T) {
|
func TestCLIIntegrationTestSuite(t *testing.T) {
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("skipping integration tests due to short flag")
|
|
||||||
}
|
|
||||||
|
|
||||||
s := new(IntegrationCLITestSuite)
|
s := new(IntegrationCLITestSuite)
|
||||||
|
|
||||||
suite.Run(t, s)
|
suite.Run(t, s)
|
||||||
@@ -46,15 +48,19 @@ func (s *IntegrationCLITestSuite) SetupTest() {
|
|||||||
s.FailNow(fmt.Sprintf("Could not connect to docker: %s", err), "")
|
s.FailNow(fmt.Sprintf("Could not connect to docker: %s", err), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
network, err := GetFirstOrCreateNetwork(&s.pool, headscaleNetwork)
|
if pnetwork, err := s.pool.CreateNetwork("headscale-test"); err == nil {
|
||||||
if err != nil {
|
s.network = *pnetwork
|
||||||
s.FailNow(fmt.Sprintf("Failed to create or get network: %s", err), "")
|
} else {
|
||||||
|
s.FailNow(fmt.Sprintf("Could not create network: %s", err), "")
|
||||||
}
|
}
|
||||||
s.network = network
|
|
||||||
|
platform := fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
|
|
||||||
headscaleBuildOptions := &dockertest.BuildOptions{
|
headscaleBuildOptions := &dockertest.BuildOptions{
|
||||||
Dockerfile: "Dockerfile",
|
Dockerfile: "Dockerfile",
|
||||||
ContextDir: ".",
|
ContextDir: ".",
|
||||||
|
Platform: platform,
|
||||||
|
Version: "2",
|
||||||
}
|
}
|
||||||
|
|
||||||
currentPath, err := os.Getwd()
|
currentPath, err := os.Getwd()
|
||||||
@@ -63,19 +69,16 @@ func (s *IntegrationCLITestSuite) SetupTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
headscaleOptions := &dockertest.RunOptions{
|
headscaleOptions := &dockertest.RunOptions{
|
||||||
Name: "headscale-cli",
|
Name: headscaleCLIContainerName,
|
||||||
Mounts: []string{
|
Mounts: []string{
|
||||||
fmt.Sprintf("%s/integration_test/etc:/etc/headscale", currentPath),
|
fmt.Sprintf("%s/integration_test/etc:/etc/headscale", currentPath),
|
||||||
},
|
},
|
||||||
Cmd: []string{"headscale", "serve"},
|
Networks: []*dockertest.Network{&s.network},
|
||||||
Networks: []*dockertest.Network{&s.network},
|
Cmd: []string{"headscale", "serve"},
|
||||||
ExposedPorts: []string{"8080/tcp"},
|
Platform: platform,
|
||||||
PortBindings: map[docker.Port][]docker.PortBinding{
|
|
||||||
"8080/tcp": {{HostPort: "8080"}},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.pool.RemoveContainerByName(headscaleHostname)
|
err = s.pool.RemoveContainerByName(headscaleCLIContainerName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.FailNow(
|
s.FailNow(
|
||||||
fmt.Sprintf(
|
fmt.Sprintf(
|
||||||
@@ -86,24 +89,21 @@ func (s *IntegrationCLITestSuite) SetupTest() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Creating headscale container for CLI tests")
|
fmt.Println("Creating headscale container")
|
||||||
if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
|
if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
|
||||||
s.headscale = *pheadscale
|
s.headscale = *pheadscale
|
||||||
} else {
|
} else {
|
||||||
s.FailNow(fmt.Sprintf("Could not start headscale container: %s", err), "")
|
s.FailNow(fmt.Sprintf("Could not start headscale container: %s", err), "")
|
||||||
}
|
}
|
||||||
fmt.Println("Created headscale container for CLI tests")
|
fmt.Println("Created headscale container")
|
||||||
|
|
||||||
fmt.Println("Waiting for headscale to be ready for CLI tests")
|
fmt.Println("Waiting for headscale to be ready")
|
||||||
hostEndpoint := fmt.Sprintf("%s:%s",
|
hostEndpoint := fmt.Sprintf("localhost:%s", s.headscale.GetPort("8080/tcp"))
|
||||||
s.headscale.GetIPInNetwork(&s.network),
|
|
||||||
s.headscale.GetPort("8080/tcp"))
|
|
||||||
|
|
||||||
if err := s.pool.Retry(func() error {
|
if err := s.pool.Retry(func() error {
|
||||||
url := fmt.Sprintf("http://%s/health", hostEndpoint)
|
url := fmt.Sprintf("http://%s/health", hostEndpoint)
|
||||||
resp, err := http.Get(url)
|
resp, err := http.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("headscale for CLI test is not ready: %s\n", err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
@@ -118,7 +118,7 @@ func (s *IntegrationCLITestSuite) SetupTest() {
|
|||||||
// https://github.com/stretchr/testify/issues/849
|
// https://github.com/stretchr/testify/issues/849
|
||||||
return // fmt.Errorf("Could not connect to headscale: %s", err)
|
return // fmt.Errorf("Could not connect to headscale: %s", err)
|
||||||
}
|
}
|
||||||
fmt.Println("headscale container is ready for CLI tests")
|
fmt.Println("headscale container is ready")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *IntegrationCLITestSuite) TearDownTest() {
|
func (s *IntegrationCLITestSuite) TearDownTest() {
|
||||||
@@ -139,7 +139,7 @@ func (s *IntegrationCLITestSuite) HandleStats(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *IntegrationCLITestSuite) createNamespace(name string) (*v1.Namespace, error) {
|
func (s *IntegrationCLITestSuite) createNamespace(name string) (*v1.Namespace, error) {
|
||||||
result, _, err := ExecuteCommand(
|
result, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -182,7 +182,7 @@ func (s *IntegrationCLITestSuite) TestNamespaceCommand() {
|
|||||||
assert.Equal(s.T(), names[2], namespaces[2].Name)
|
assert.Equal(s.T(), names[2], namespaces[2].Name)
|
||||||
|
|
||||||
// Test list namespaces
|
// Test list namespaces
|
||||||
listResult, _, err := ExecuteCommand(
|
listResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -204,7 +204,7 @@ func (s *IntegrationCLITestSuite) TestNamespaceCommand() {
|
|||||||
assert.Equal(s.T(), names[2], listedNamespaces[2].Name)
|
assert.Equal(s.T(), names[2], listedNamespaces[2].Name)
|
||||||
|
|
||||||
// Test rename namespace
|
// Test rename namespace
|
||||||
renameResult, _, err := ExecuteCommand(
|
renameResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -226,7 +226,7 @@ func (s *IntegrationCLITestSuite) TestNamespaceCommand() {
|
|||||||
assert.Equal(s.T(), renamedNamespace.Name, "newname")
|
assert.Equal(s.T(), renamedNamespace.Name, "newname")
|
||||||
|
|
||||||
// Test list after rename namespaces
|
// Test list after rename namespaces
|
||||||
listAfterRenameResult, _, err := ExecuteCommand(
|
listAfterRenameResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -257,7 +257,7 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommand() {
|
|||||||
assert.Nil(s.T(), err)
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
preAuthResult, _, err := ExecuteCommand(
|
preAuthResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -270,8 +270,6 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommand() {
|
|||||||
"24h",
|
"24h",
|
||||||
"--output",
|
"--output",
|
||||||
"json",
|
"json",
|
||||||
"--tags",
|
|
||||||
"tag:test1,tag:test2",
|
|
||||||
},
|
},
|
||||||
[]string{},
|
[]string{},
|
||||||
)
|
)
|
||||||
@@ -287,7 +285,7 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommand() {
|
|||||||
assert.Len(s.T(), keys, 5)
|
assert.Len(s.T(), keys, 5)
|
||||||
|
|
||||||
// Test list of keys
|
// Test list of keys
|
||||||
listResult, _, err := ExecuteCommand(
|
listResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -345,14 +343,9 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommand() {
|
|||||||
listedPreAuthKeys[4].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)),
|
listedPreAuthKeys[4].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Test that tags are present
|
|
||||||
for i := 0; i < count; i++ {
|
|
||||||
assert.Equal(s.T(), listedPreAuthKeys[i].AclTags, []string{"tag:test1", "tag:test2"})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expire three keys
|
// Expire three keys
|
||||||
for i := 0; i < 3; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
_, _, err := ExecuteCommand(
|
_, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -368,7 +361,7 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommand() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Test list pre auth keys after expire
|
// Test list pre auth keys after expire
|
||||||
listAfterExpireResult, _, err := ExecuteCommand(
|
listAfterExpireResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -413,7 +406,7 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommandWithoutExpiry() {
|
|||||||
namespace, err := s.createNamespace("pre-auth-key-without-exp-namespace")
|
namespace, err := s.createNamespace("pre-auth-key-without-exp-namespace")
|
||||||
assert.Nil(s.T(), err)
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
preAuthResult, _, err := ExecuteCommand(
|
preAuthResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -434,7 +427,7 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommandWithoutExpiry() {
|
|||||||
assert.Nil(s.T(), err)
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
// Test list of keys
|
// Test list of keys
|
||||||
listResult, _, err := ExecuteCommand(
|
listResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -466,7 +459,7 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommandReusableEphemeral() {
|
|||||||
namespace, err := s.createNamespace("pre-auth-key-reus-ephm-namespace")
|
namespace, err := s.createNamespace("pre-auth-key-reus-ephm-namespace")
|
||||||
assert.Nil(s.T(), err)
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
preAuthReusableResult, _, err := ExecuteCommand(
|
preAuthReusableResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -489,7 +482,7 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommandReusableEphemeral() {
|
|||||||
assert.True(s.T(), preAuthReusableKey.GetReusable())
|
assert.True(s.T(), preAuthReusableKey.GetReusable())
|
||||||
assert.False(s.T(), preAuthReusableKey.GetEphemeral())
|
assert.False(s.T(), preAuthReusableKey.GetEphemeral())
|
||||||
|
|
||||||
preAuthEphemeralResult, _, err := ExecuteCommand(
|
preAuthEphemeralResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -531,7 +524,7 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommandReusableEphemeral() {
|
|||||||
// assert.NotNil(s.T(), err)
|
// assert.NotNil(s.T(), err)
|
||||||
|
|
||||||
// Test list of keys
|
// Test list of keys
|
||||||
listResult, _, err := ExecuteCommand(
|
listResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -558,14 +551,14 @@ func (s *IntegrationCLITestSuite) TestNodeTagCommand() {
|
|||||||
assert.Nil(s.T(), err)
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
machineKeys := []string{
|
machineKeys := []string{
|
||||||
"nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
|
"9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
|
||||||
"nodekey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
|
"6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
|
||||||
}
|
}
|
||||||
machines := make([]*v1.Machine, len(machineKeys))
|
machines := make([]*v1.Machine, len(machineKeys))
|
||||||
assert.Nil(s.T(), err)
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
for index, machineKey := range machineKeys {
|
for index, machineKey := range machineKeys {
|
||||||
_, _, err := ExecuteCommand(
|
_, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -584,7 +577,7 @@ func (s *IntegrationCLITestSuite) TestNodeTagCommand() {
|
|||||||
)
|
)
|
||||||
assert.Nil(s.T(), err)
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
machineResult, _, err := ExecuteCommand(
|
machineResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -609,7 +602,7 @@ func (s *IntegrationCLITestSuite) TestNodeTagCommand() {
|
|||||||
}
|
}
|
||||||
assert.Len(s.T(), machines, len(machineKeys))
|
assert.Len(s.T(), machines, len(machineKeys))
|
||||||
|
|
||||||
addTagResult, _, err := ExecuteCommand(
|
addTagResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -629,7 +622,7 @@ func (s *IntegrationCLITestSuite) TestNodeTagCommand() {
|
|||||||
assert.Equal(s.T(), []string{"tag:test"}, machine.ForcedTags)
|
assert.Equal(s.T(), []string{"tag:test"}, machine.ForcedTags)
|
||||||
|
|
||||||
// try to set a wrong tag and retrieve the error
|
// try to set a wrong tag and retrieve the error
|
||||||
wrongTagResult, _, err := ExecuteCommand(
|
wrongTagResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -651,7 +644,7 @@ func (s *IntegrationCLITestSuite) TestNodeTagCommand() {
|
|||||||
assert.Contains(s.T(), errorOutput.Error, "tag must start with the string 'tag:'")
|
assert.Contains(s.T(), errorOutput.Error, "tag must start with the string 'tag:'")
|
||||||
|
|
||||||
// Test list all nodes after added seconds
|
// Test list all nodes after added seconds
|
||||||
listAllResult, _, err := ExecuteCommand(
|
listAllResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -691,17 +684,17 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
|
|||||||
|
|
||||||
// Randomly generated machine keys
|
// Randomly generated machine keys
|
||||||
machineKeys := []string{
|
machineKeys := []string{
|
||||||
"nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
|
"9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
|
||||||
"nodekey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
|
"6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
|
||||||
"nodekey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
|
"f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
|
||||||
"nodekey:8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1",
|
"8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1",
|
||||||
"nodekey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
|
"cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
|
||||||
}
|
}
|
||||||
machines := make([]*v1.Machine, len(machineKeys))
|
machines := make([]*v1.Machine, len(machineKeys))
|
||||||
assert.Nil(s.T(), err)
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
for index, machineKey := range machineKeys {
|
for index, machineKey := range machineKeys {
|
||||||
_, _, err := ExecuteCommand(
|
_, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -720,7 +713,7 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
|
|||||||
)
|
)
|
||||||
assert.Nil(s.T(), err)
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
machineResult, _, err := ExecuteCommand(
|
machineResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -747,7 +740,7 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
|
|||||||
assert.Len(s.T(), machines, len(machineKeys))
|
assert.Len(s.T(), machines, len(machineKeys))
|
||||||
|
|
||||||
// Test list all nodes after added seconds
|
// Test list all nodes after added seconds
|
||||||
listAllResult, _, err := ExecuteCommand(
|
listAllResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -779,14 +772,14 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
|
|||||||
assert.Equal(s.T(), "machine-5", listAll[4].Name)
|
assert.Equal(s.T(), "machine-5", listAll[4].Name)
|
||||||
|
|
||||||
otherNamespaceMachineKeys := []string{
|
otherNamespaceMachineKeys := []string{
|
||||||
"nodekey:b5b444774186d4217adcec407563a1223929465ee2c68a4da13af0d0185b4f8e",
|
"b5b444774186d4217adcec407563a1223929465ee2c68a4da13af0d0185b4f8e",
|
||||||
"nodekey:dc721977ac7415aafa87f7d4574cbe07c6b171834a6d37375782bdc1fb6b3584",
|
"dc721977ac7415aafa87f7d4574cbe07c6b171834a6d37375782bdc1fb6b3584",
|
||||||
}
|
}
|
||||||
otherNamespaceMachines := make([]*v1.Machine, len(otherNamespaceMachineKeys))
|
otherNamespaceMachines := make([]*v1.Machine, len(otherNamespaceMachineKeys))
|
||||||
assert.Nil(s.T(), err)
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
for index, machineKey := range otherNamespaceMachineKeys {
|
for index, machineKey := range otherNamespaceMachineKeys {
|
||||||
_, _, err := ExecuteCommand(
|
_, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -805,7 +798,7 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
|
|||||||
)
|
)
|
||||||
assert.Nil(s.T(), err)
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
machineResult, _, err := ExecuteCommand(
|
machineResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -832,7 +825,7 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
|
|||||||
assert.Len(s.T(), otherNamespaceMachines, len(otherNamespaceMachineKeys))
|
assert.Len(s.T(), otherNamespaceMachines, len(otherNamespaceMachineKeys))
|
||||||
|
|
||||||
// Test list all nodes after added otherNamespace
|
// Test list all nodes after added otherNamespace
|
||||||
listAllWithotherNamespaceResult, _, err := ExecuteCommand(
|
listAllWithotherNamespaceResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -862,7 +855,7 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
|
|||||||
assert.Equal(s.T(), "otherNamespace-machine-2", listAllWithotherNamespace[6].Name)
|
assert.Equal(s.T(), "otherNamespace-machine-2", listAllWithotherNamespace[6].Name)
|
||||||
|
|
||||||
// Test list all nodes after added otherNamespace
|
// Test list all nodes after added otherNamespace
|
||||||
listOnlyotherNamespaceMachineNamespaceResult, _, err := ExecuteCommand(
|
listOnlyotherNamespaceMachineNamespaceResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -901,7 +894,7 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Delete a machines
|
// Delete a machines
|
||||||
_, _, err = ExecuteCommand(
|
_, err = ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -919,7 +912,7 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
|
|||||||
assert.Nil(s.T(), err)
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
// Test: list main namespace after machine is deleted
|
// Test: list main namespace after machine is deleted
|
||||||
listOnlyMachineNamespaceAfterDeleteResult, _, err := ExecuteCommand(
|
listOnlyMachineNamespaceAfterDeleteResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -950,17 +943,17 @@ func (s *IntegrationCLITestSuite) TestNodeExpireCommand() {
|
|||||||
|
|
||||||
// Randomly generated machine keys
|
// Randomly generated machine keys
|
||||||
machineKeys := []string{
|
machineKeys := []string{
|
||||||
"nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
|
"9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
|
||||||
"nodekey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
|
"6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
|
||||||
"nodekey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
|
"f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
|
||||||
"nodekey:8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1",
|
"8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1",
|
||||||
"nodekey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
|
"cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
|
||||||
}
|
}
|
||||||
machines := make([]*v1.Machine, len(machineKeys))
|
machines := make([]*v1.Machine, len(machineKeys))
|
||||||
assert.Nil(s.T(), err)
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
for index, machineKey := range machineKeys {
|
for index, machineKey := range machineKeys {
|
||||||
_, _, err := ExecuteCommand(
|
_, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -979,7 +972,7 @@ func (s *IntegrationCLITestSuite) TestNodeExpireCommand() {
|
|||||||
)
|
)
|
||||||
assert.Nil(s.T(), err)
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
machineResult, _, err := ExecuteCommand(
|
machineResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -1005,7 +998,7 @@ func (s *IntegrationCLITestSuite) TestNodeExpireCommand() {
|
|||||||
|
|
||||||
assert.Len(s.T(), machines, len(machineKeys))
|
assert.Len(s.T(), machines, len(machineKeys))
|
||||||
|
|
||||||
listAllResult, _, err := ExecuteCommand(
|
listAllResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -1031,7 +1024,7 @@ func (s *IntegrationCLITestSuite) TestNodeExpireCommand() {
|
|||||||
assert.True(s.T(), listAll[4].Expiry.AsTime().IsZero())
|
assert.True(s.T(), listAll[4].Expiry.AsTime().IsZero())
|
||||||
|
|
||||||
for i := 0; i < 3; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
_, _, err := ExecuteCommand(
|
_, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -1045,7 +1038,7 @@ func (s *IntegrationCLITestSuite) TestNodeExpireCommand() {
|
|||||||
assert.Nil(s.T(), err)
|
assert.Nil(s.T(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
listAllAfterExpiryResult, _, err := ExecuteCommand(
|
listAllAfterExpiryResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -1077,17 +1070,17 @@ func (s *IntegrationCLITestSuite) TestNodeRenameCommand() {
|
|||||||
|
|
||||||
// Randomly generated machine keys
|
// Randomly generated machine keys
|
||||||
machineKeys := []string{
|
machineKeys := []string{
|
||||||
"nodekey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
|
"cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
|
||||||
"nodekey:8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1",
|
"8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1",
|
||||||
"nodekey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
|
"f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
|
||||||
"nodekey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
|
"6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
|
||||||
"nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
|
"9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
|
||||||
}
|
}
|
||||||
machines := make([]*v1.Machine, len(machineKeys))
|
machines := make([]*v1.Machine, len(machineKeys))
|
||||||
assert.Nil(s.T(), err)
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
for index, machineKey := range machineKeys {
|
for index, machineKey := range machineKeys {
|
||||||
_, _, err := ExecuteCommand(
|
_, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -1106,7 +1099,7 @@ func (s *IntegrationCLITestSuite) TestNodeRenameCommand() {
|
|||||||
)
|
)
|
||||||
assert.Nil(s.T(), err)
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
machineResult, _, err := ExecuteCommand(
|
machineResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -1132,7 +1125,7 @@ func (s *IntegrationCLITestSuite) TestNodeRenameCommand() {
|
|||||||
|
|
||||||
assert.Len(s.T(), machines, len(machineKeys))
|
assert.Len(s.T(), machines, len(machineKeys))
|
||||||
|
|
||||||
listAllResult, _, err := ExecuteCommand(
|
listAllResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -1158,7 +1151,7 @@ func (s *IntegrationCLITestSuite) TestNodeRenameCommand() {
|
|||||||
assert.Contains(s.T(), listAll[4].GetGivenName(), "machine-5")
|
assert.Contains(s.T(), listAll[4].GetGivenName(), "machine-5")
|
||||||
|
|
||||||
for i := 0; i < 3; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
_, _, err := ExecuteCommand(
|
_, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -1173,7 +1166,7 @@ func (s *IntegrationCLITestSuite) TestNodeRenameCommand() {
|
|||||||
assert.Nil(s.T(), err)
|
assert.Nil(s.T(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
listAllAfterRenameResult, _, err := ExecuteCommand(
|
listAllAfterRenameResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -1199,7 +1192,7 @@ func (s *IntegrationCLITestSuite) TestNodeRenameCommand() {
|
|||||||
assert.Contains(s.T(), listAllAfterRename[4].GetGivenName(), "machine-5")
|
assert.Contains(s.T(), listAllAfterRename[4].GetGivenName(), "machine-5")
|
||||||
|
|
||||||
// Test failure for too long names
|
// Test failure for too long names
|
||||||
result, _, err := ExecuteCommand(
|
result, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -1214,7 +1207,7 @@ func (s *IntegrationCLITestSuite) TestNodeRenameCommand() {
|
|||||||
assert.Nil(s.T(), err)
|
assert.Nil(s.T(), err)
|
||||||
assert.Contains(s.T(), result, "not be over 63 chars")
|
assert.Contains(s.T(), result, "not be over 63 chars")
|
||||||
|
|
||||||
listAllAfterRenameAttemptResult, _, err := ExecuteCommand(
|
listAllAfterRenameAttemptResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -1248,9 +1241,9 @@ func (s *IntegrationCLITestSuite) TestRouteCommand() {
|
|||||||
assert.Nil(s.T(), err)
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
// Randomly generated machine keys
|
// Randomly generated machine keys
|
||||||
machineKey := "nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe"
|
machineKey := "9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe"
|
||||||
|
|
||||||
_, _, err = ExecuteCommand(
|
_, err = ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -1273,7 +1266,7 @@ func (s *IntegrationCLITestSuite) TestRouteCommand() {
|
|||||||
)
|
)
|
||||||
assert.Nil(s.T(), err)
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
machineResult, _, err := ExecuteCommand(
|
machineResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -1297,7 +1290,7 @@ func (s *IntegrationCLITestSuite) TestRouteCommand() {
|
|||||||
assert.Equal(s.T(), uint64(1), machine.Id)
|
assert.Equal(s.T(), uint64(1), machine.Id)
|
||||||
assert.Equal(s.T(), "route-machine", machine.Name)
|
assert.Equal(s.T(), "route-machine", machine.Name)
|
||||||
|
|
||||||
listAllResult, _, err := ExecuteCommand(
|
listAllResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -1322,7 +1315,7 @@ func (s *IntegrationCLITestSuite) TestRouteCommand() {
|
|||||||
|
|
||||||
assert.Empty(s.T(), listAll.EnabledRoutes)
|
assert.Empty(s.T(), listAll.EnabledRoutes)
|
||||||
|
|
||||||
enableTwoRoutesResult, _, err := ExecuteCommand(
|
enableTwoRoutesResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -1354,7 +1347,7 @@ func (s *IntegrationCLITestSuite) TestRouteCommand() {
|
|||||||
assert.Contains(s.T(), enableTwoRoutes.EnabledRoutes, "192.168.1.0/24")
|
assert.Contains(s.T(), enableTwoRoutes.EnabledRoutes, "192.168.1.0/24")
|
||||||
|
|
||||||
// Enable only one route, effectively disabling one of the routes
|
// Enable only one route, effectively disabling one of the routes
|
||||||
enableOneRouteResult, _, err := ExecuteCommand(
|
enableOneRouteResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -1383,7 +1376,7 @@ func (s *IntegrationCLITestSuite) TestRouteCommand() {
|
|||||||
assert.Contains(s.T(), enableOneRoute.EnabledRoutes, "10.0.0.0/8")
|
assert.Contains(s.T(), enableOneRoute.EnabledRoutes, "10.0.0.0/8")
|
||||||
|
|
||||||
// Enable only one route, effectively disabling one of the routes
|
// Enable only one route, effectively disabling one of the routes
|
||||||
failEnableNonAdvertisedRoute, _, err := ExecuteCommand(
|
failEnableNonAdvertisedRoute, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -1407,7 +1400,7 @@ func (s *IntegrationCLITestSuite) TestRouteCommand() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Enable all routes on host
|
// Enable all routes on host
|
||||||
enableAllRouteResult, _, err := ExecuteCommand(
|
enableAllRouteResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -1442,7 +1435,7 @@ func (s *IntegrationCLITestSuite) TestApiKeyCommand() {
|
|||||||
keys := make([]string, count)
|
keys := make([]string, count)
|
||||||
|
|
||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
apiResult, _, err := ExecuteCommand(
|
apiResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -1468,7 +1461,7 @@ func (s *IntegrationCLITestSuite) TestApiKeyCommand() {
|
|||||||
assert.Len(s.T(), keys, 5)
|
assert.Len(s.T(), keys, 5)
|
||||||
|
|
||||||
// Test list of keys
|
// Test list of keys
|
||||||
listResult, _, err := ExecuteCommand(
|
listResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -1530,7 +1523,7 @@ func (s *IntegrationCLITestSuite) TestApiKeyCommand() {
|
|||||||
|
|
||||||
// Expire three keys
|
// Expire three keys
|
||||||
for i := 0; i < 3; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
_, _, err := ExecuteCommand(
|
_, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -1547,7 +1540,7 @@ func (s *IntegrationCLITestSuite) TestApiKeyCommand() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Test list pre auth keys after expire
|
// Test list pre auth keys after expire
|
||||||
listAfterExpireResult, _, err := ExecuteCommand(
|
listAfterExpireResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -1588,9 +1581,9 @@ func (s *IntegrationCLITestSuite) TestNodeMoveCommand() {
|
|||||||
assert.Nil(s.T(), err)
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
// Randomly generated machine key
|
// Randomly generated machine key
|
||||||
machineKey := "nodekey:688411b767663479632d44140f08a9fde87383adc7cdeb518f62ce28a17ef0aa"
|
machineKey := "688411b767663479632d44140f08a9fde87383adc7cdeb518f62ce28a17ef0aa"
|
||||||
|
|
||||||
_, _, err = ExecuteCommand(
|
_, err = ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -1609,7 +1602,7 @@ func (s *IntegrationCLITestSuite) TestNodeMoveCommand() {
|
|||||||
)
|
)
|
||||||
assert.Nil(s.T(), err)
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
machineResult, _, err := ExecuteCommand(
|
machineResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -1636,7 +1629,7 @@ func (s *IntegrationCLITestSuite) TestNodeMoveCommand() {
|
|||||||
|
|
||||||
machineId := fmt.Sprintf("%d", machine.Id)
|
machineId := fmt.Sprintf("%d", machine.Id)
|
||||||
|
|
||||||
moveToNewNSResult, _, err := ExecuteCommand(
|
moveToNewNSResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -1658,7 +1651,7 @@ func (s *IntegrationCLITestSuite) TestNodeMoveCommand() {
|
|||||||
|
|
||||||
assert.Equal(s.T(), machine.Namespace, newNamespace)
|
assert.Equal(s.T(), machine.Namespace, newNamespace)
|
||||||
|
|
||||||
listAllNodesResult, _, err := ExecuteCommand(
|
listAllNodesResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -1681,7 +1674,7 @@ func (s *IntegrationCLITestSuite) TestNodeMoveCommand() {
|
|||||||
assert.Equal(s.T(), allNodes[0].Namespace, machine.Namespace)
|
assert.Equal(s.T(), allNodes[0].Namespace, machine.Namespace)
|
||||||
assert.Equal(s.T(), allNodes[0].Namespace, newNamespace)
|
assert.Equal(s.T(), allNodes[0].Namespace, newNamespace)
|
||||||
|
|
||||||
moveToNonExistingNSResult, _, err := ExecuteCommand(
|
moveToNonExistingNSResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -1705,7 +1698,7 @@ func (s *IntegrationCLITestSuite) TestNodeMoveCommand() {
|
|||||||
)
|
)
|
||||||
assert.Equal(s.T(), machine.Namespace, newNamespace)
|
assert.Equal(s.T(), machine.Namespace, newNamespace)
|
||||||
|
|
||||||
moveToOldNSResult, _, err := ExecuteCommand(
|
moveToOldNSResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -1727,7 +1720,7 @@ func (s *IntegrationCLITestSuite) TestNodeMoveCommand() {
|
|||||||
|
|
||||||
assert.Equal(s.T(), machine.Namespace, oldNamespace)
|
assert.Equal(s.T(), machine.Namespace, oldNamespace)
|
||||||
|
|
||||||
moveToSameNSResult, _, err := ExecuteCommand(
|
moveToSameNSResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -1756,10 +1749,8 @@ func (s *IntegrationCLITestSuite) TestLoadConfigFromCommand() {
|
|||||||
assert.Nil(s.T(), err)
|
assert.Nil(s.T(), err)
|
||||||
altConfig, err := os.ReadFile("integration_test/etc/alt-config.dump.gold.yaml")
|
altConfig, err := os.ReadFile("integration_test/etc/alt-config.dump.gold.yaml")
|
||||||
assert.Nil(s.T(), err)
|
assert.Nil(s.T(), err)
|
||||||
altEnvConfig, err := os.ReadFile("integration_test/etc/alt-env-config.dump.gold.yaml")
|
|
||||||
assert.Nil(s.T(), err)
|
|
||||||
|
|
||||||
_, _, err = ExecuteCommand(
|
_, err = ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -1774,7 +1765,7 @@ func (s *IntegrationCLITestSuite) TestLoadConfigFromCommand() {
|
|||||||
|
|
||||||
assert.YAMLEq(s.T(), string(defaultConfig), string(defaultDumpConfig))
|
assert.YAMLEq(s.T(), string(defaultConfig), string(defaultDumpConfig))
|
||||||
|
|
||||||
_, _, err = ExecuteCommand(
|
_, err = ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -1790,40 +1781,4 @@ func (s *IntegrationCLITestSuite) TestLoadConfigFromCommand() {
|
|||||||
assert.Nil(s.T(), err)
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
assert.YAMLEq(s.T(), string(altConfig), string(altDumpConfig))
|
assert.YAMLEq(s.T(), string(altConfig), string(altDumpConfig))
|
||||||
|
|
||||||
_, _, err = ExecuteCommand(
|
|
||||||
&s.headscale,
|
|
||||||
[]string{
|
|
||||||
"headscale",
|
|
||||||
"dumpConfig",
|
|
||||||
},
|
|
||||||
[]string{
|
|
||||||
"HEADSCALE_CONFIG=/etc/headscale/alt-env-config.yaml",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
assert.Nil(s.T(), err)
|
|
||||||
|
|
||||||
altEnvDumpConfig, err := os.ReadFile("integration_test/etc/config.dump.yaml")
|
|
||||||
assert.Nil(s.T(), err)
|
|
||||||
|
|
||||||
assert.YAMLEq(s.T(), string(altEnvConfig), string(altEnvDumpConfig))
|
|
||||||
|
|
||||||
_, _, err = ExecuteCommand(
|
|
||||||
&s.headscale,
|
|
||||||
[]string{
|
|
||||||
"headscale",
|
|
||||||
"-c",
|
|
||||||
"/etc/headscale/alt-config.yaml",
|
|
||||||
"dumpConfig",
|
|
||||||
},
|
|
||||||
[]string{
|
|
||||||
"HEADSCALE_CONFIG=/etc/headscale/alt-env-config.yaml",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
assert.Nil(s.T(), err)
|
|
||||||
|
|
||||||
altDumpConfig, err = os.ReadFile("integration_test/etc/config.dump.yaml")
|
|
||||||
assert.Nil(s.T(), err)
|
|
||||||
|
|
||||||
assert.YAMLEq(s.T(), string(altConfig), string(altDumpConfig))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
//nolint
|
//go:build integration
|
||||||
|
// +build integration
|
||||||
|
|
||||||
package headscale
|
package headscale
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -6,7 +8,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -15,27 +16,23 @@ import (
|
|||||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||||
"github.com/ory/dockertest/v3"
|
"github.com/ory/dockertest/v3"
|
||||||
"github.com/ory/dockertest/v3/docker"
|
"github.com/ory/dockertest/v3/docker"
|
||||||
|
"inet.af/netaddr"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
headscaleNetwork = "headscale-test"
|
|
||||||
headscaleHostname = "headscale"
|
|
||||||
DOCKER_EXECUTE_TIMEOUT = 10 * time.Second
|
DOCKER_EXECUTE_TIMEOUT = 10 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errEnvVarEmpty = errors.New("getenv: environment variable empty")
|
errEnvVarEmpty = errors.New("getenv: environment variable empty")
|
||||||
|
|
||||||
IpPrefix4 = netip.MustParsePrefix("100.64.0.0/10")
|
IpPrefix4 = netaddr.MustParseIPPrefix("100.64.0.0/10")
|
||||||
IpPrefix6 = netip.MustParsePrefix("fd7a:115c:a1e0::/48")
|
IpPrefix6 = netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/48")
|
||||||
|
|
||||||
tailscaleVersions = []string{
|
tailscaleVersions = []string{
|
||||||
"head",
|
"head",
|
||||||
"unstable",
|
"unstable",
|
||||||
"1.32.0",
|
"1.26.0",
|
||||||
"1.30.2",
|
|
||||||
"1.28.0",
|
|
||||||
"1.26.2",
|
|
||||||
"1.24.2",
|
"1.24.2",
|
||||||
"1.22.2",
|
"1.22.2",
|
||||||
"1.20.4",
|
"1.20.4",
|
||||||
@@ -69,7 +66,7 @@ func ExecuteCommand(
|
|||||||
cmd []string,
|
cmd []string,
|
||||||
env []string,
|
env []string,
|
||||||
options ...ExecuteCommandOption,
|
options ...ExecuteCommandOption,
|
||||||
) (string, string, error) {
|
) (string, error) {
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
|
|
||||||
@@ -79,7 +76,7 @@ func ExecuteCommand(
|
|||||||
|
|
||||||
for _, opt := range options {
|
for _, opt := range options {
|
||||||
if err := opt(&execConfig); err != nil {
|
if err := opt(&execConfig); err != nil {
|
||||||
return "", "", fmt.Errorf("execute-command/options: %w", err)
|
return "", fmt.Errorf("execute-command/options: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +105,7 @@ func ExecuteCommand(
|
|||||||
select {
|
select {
|
||||||
case res := <-resultChan:
|
case res := <-resultChan:
|
||||||
if res.err != nil {
|
if res.err != nil {
|
||||||
return stdout.String(), stderr.String(), res.err
|
return "", res.err
|
||||||
}
|
}
|
||||||
|
|
||||||
if res.exitCode != 0 {
|
if res.exitCode != 0 {
|
||||||
@@ -116,19 +113,13 @@ func ExecuteCommand(
|
|||||||
fmt.Println("stdout: ", stdout.String())
|
fmt.Println("stdout: ", stdout.String())
|
||||||
fmt.Println("stderr: ", stderr.String())
|
fmt.Println("stderr: ", stderr.String())
|
||||||
|
|
||||||
return stdout.String(), stderr.String(), fmt.Errorf(
|
return "", fmt.Errorf("command failed with: %s", stderr.String())
|
||||||
"command failed with: %s",
|
|
||||||
stderr.String(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return stdout.String(), stderr.String(), nil
|
return stdout.String(), nil
|
||||||
case <-time.After(execConfig.timeout):
|
case <-time.After(execConfig.timeout):
|
||||||
|
|
||||||
return stdout.String(), stderr.String(), fmt.Errorf(
|
return "", fmt.Errorf("command timed out after %s", execConfig.timeout)
|
||||||
"command timed out after %s",
|
|
||||||
execConfig.timeout,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,12 +193,12 @@ func getDockerBuildOptions(version string) *dockertest.BuildOptions {
|
|||||||
|
|
||||||
func getIPs(
|
func getIPs(
|
||||||
tailscales map[string]dockertest.Resource,
|
tailscales map[string]dockertest.Resource,
|
||||||
) (map[string][]netip.Addr, error) {
|
) (map[string][]netaddr.IP, error) {
|
||||||
ips := make(map[string][]netip.Addr)
|
ips := make(map[string][]netaddr.IP)
|
||||||
for hostname, tailscale := range tailscales {
|
for hostname, tailscale := range tailscales {
|
||||||
command := []string{"tailscale", "ip"}
|
command := []string{"tailscale", "ip"}
|
||||||
|
|
||||||
result, _, err := ExecuteCommand(
|
result, err := ExecuteCommand(
|
||||||
&tailscale,
|
&tailscale,
|
||||||
command,
|
command,
|
||||||
[]string{},
|
[]string{},
|
||||||
@@ -221,7 +212,7 @@ func getIPs(
|
|||||||
if len(address) < 1 {
|
if len(address) < 1 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ip, err := netip.ParseAddr(address)
|
ip, err := netaddr.ParseIP(address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -235,7 +226,8 @@ func getIPs(
|
|||||||
func getDNSNames(
|
func getDNSNames(
|
||||||
headscale *dockertest.Resource,
|
headscale *dockertest.Resource,
|
||||||
) ([]string, error) {
|
) ([]string, error) {
|
||||||
listAllResult, _, err := ExecuteCommand(
|
|
||||||
|
listAllResult, err := ExecuteCommand(
|
||||||
headscale,
|
headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -268,7 +260,8 @@ func getDNSNames(
|
|||||||
func getMagicFQDN(
|
func getMagicFQDN(
|
||||||
headscale *dockertest.Resource,
|
headscale *dockertest.Resource,
|
||||||
) ([]string, error) {
|
) ([]string, error) {
|
||||||
listAllResult, _, err := ExecuteCommand(
|
|
||||||
|
listAllResult, err := ExecuteCommand(
|
||||||
headscale,
|
headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -292,11 +285,7 @@ func getMagicFQDN(
|
|||||||
hostnames := make([]string, len(listAll))
|
hostnames := make([]string, len(listAll))
|
||||||
|
|
||||||
for index := range listAll {
|
for index := range listAll {
|
||||||
hostnames[index] = fmt.Sprintf(
|
hostnames[index] = fmt.Sprintf("%s.%s.headscale.net", listAll[index].GetGivenName(), listAll[index].GetNamespace().GetName())
|
||||||
"%s.%s.headscale.net",
|
|
||||||
listAll[index].GetGivenName(),
|
|
||||||
listAll[index].GetNamespace().GetName(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return hostnames, nil
|
return hostnames, nil
|
||||||
@@ -323,21 +312,3 @@ func GetEnvBool(key string) (bool, error) {
|
|||||||
|
|
||||||
return v, nil
|
return v, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetFirstOrCreateNetwork(pool *dockertest.Pool, name string) (dockertest.Network, error) {
|
|
||||||
networks, err := pool.NetworksByName(name)
|
|
||||||
if err != nil || len(networks) == 0 {
|
|
||||||
if _, err := pool.CreateNetwork(name); err == nil {
|
|
||||||
// Create does not give us an updated version of the resource, so we need to
|
|
||||||
// get it again.
|
|
||||||
networks, err := pool.NetworksByName(name)
|
|
||||||
if err != nil {
|
|
||||||
return dockertest.Network{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return networks[0], nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return networks[0], nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
//nolint
|
//go:build integration
|
||||||
|
|
||||||
package headscale
|
package headscale
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -7,48 +8,46 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ccding/go-stun/stun"
|
|
||||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||||
"github.com/ory/dockertest/v3"
|
"github.com/ory/dockertest/v3"
|
||||||
"github.com/ory/dockertest/v3/docker"
|
"github.com/ory/dockertest/v3/docker"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/ccding/go-stun/stun"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
headscaleDerpHostname = "headscale-derp"
|
headscaleDerpContainerName = "headscale-derp"
|
||||||
namespaceName = "derpnamespace"
|
namespaceName = "derpnamespace"
|
||||||
totalContainers = 3
|
totalContainers = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
type IntegrationDERPTestSuite struct {
|
type IntegrationDERPTestSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
stats *suite.SuiteInformation
|
stats *suite.SuiteInformation
|
||||||
|
|
||||||
pool dockertest.Pool
|
pool dockertest.Pool
|
||||||
network dockertest.Network
|
networks map[int]dockertest.Network // so we keep the containers isolated
|
||||||
containerNetworks map[int]dockertest.Network // so we keep the containers isolated
|
headscale dockertest.Resource
|
||||||
headscale dockertest.Resource
|
saveLogs bool
|
||||||
saveLogs bool
|
|
||||||
|
|
||||||
tailscales map[string]dockertest.Resource
|
tailscales map[string]dockertest.Resource
|
||||||
joinWaitGroup sync.WaitGroup
|
joinWaitGroup sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIntegrationDERPTestSuite(t *testing.T) {
|
func TestDERPIntegrationTestSuite(t *testing.T) {
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("skipping integration tests due to short flag")
|
|
||||||
}
|
|
||||||
|
|
||||||
saveLogs, err := GetEnvBool("HEADSCALE_INTEGRATION_SAVE_LOG")
|
saveLogs, err := GetEnvBool("HEADSCALE_INTEGRATION_SAVE_LOG")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
saveLogs = false
|
saveLogs = false
|
||||||
@@ -57,7 +56,7 @@ func TestIntegrationDERPTestSuite(t *testing.T) {
|
|||||||
s := new(IntegrationDERPTestSuite)
|
s := new(IntegrationDERPTestSuite)
|
||||||
|
|
||||||
s.tailscales = make(map[string]dockertest.Resource)
|
s.tailscales = make(map[string]dockertest.Resource)
|
||||||
s.containerNetworks = make(map[int]dockertest.Network)
|
s.networks = make(map[int]dockertest.Network)
|
||||||
s.saveLogs = saveLogs
|
s.saveLogs = saveLogs
|
||||||
|
|
||||||
suite.Run(t, s)
|
suite.Run(t, s)
|
||||||
@@ -82,7 +81,7 @@ func TestIntegrationDERPTestSuite(t *testing.T) {
|
|||||||
log.Printf("Could not purge resource: %s\n", err)
|
log.Printf("Could not purge resource: %s\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, network := range s.containerNetworks {
|
for _, network := range s.networks {
|
||||||
if err := network.Close(); err != nil {
|
if err := network.Close(); err != nil {
|
||||||
log.Printf("Could not close network: %s\n", err)
|
log.Printf("Could not close network: %s\n", err)
|
||||||
}
|
}
|
||||||
@@ -97,23 +96,21 @@ func (s *IntegrationDERPTestSuite) SetupSuite() {
|
|||||||
s.FailNow(fmt.Sprintf("Could not connect to docker: %s", err), "")
|
s.FailNow(fmt.Sprintf("Could not connect to docker: %s", err), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
network, err := GetFirstOrCreateNetwork(&s.pool, headscaleNetwork)
|
|
||||||
if err != nil {
|
|
||||||
s.FailNow(fmt.Sprintf("Failed to create or get network: %s", err), "")
|
|
||||||
}
|
|
||||||
s.network = network
|
|
||||||
|
|
||||||
for i := 0; i < totalContainers; i++ {
|
for i := 0; i < totalContainers; i++ {
|
||||||
if pnetwork, err := s.pool.CreateNetwork(fmt.Sprintf("headscale-derp-%d", i)); err == nil {
|
if pnetwork, err := s.pool.CreateNetwork(fmt.Sprintf("headscale-derp-%d", i)); err == nil {
|
||||||
s.containerNetworks[i] = *pnetwork
|
s.networks[i] = *pnetwork
|
||||||
} else {
|
} else {
|
||||||
s.FailNow(fmt.Sprintf("Could not create network: %s", err), "")
|
s.FailNow(fmt.Sprintf("Could not create network: %s", err), "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
platform := fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
|
|
||||||
headscaleBuildOptions := &dockertest.BuildOptions{
|
headscaleBuildOptions := &dockertest.BuildOptions{
|
||||||
Dockerfile: "Dockerfile",
|
Dockerfile: "Dockerfile",
|
||||||
ContextDir: ".",
|
ContextDir: ".",
|
||||||
|
Platform: platform,
|
||||||
|
Version: "2",
|
||||||
}
|
}
|
||||||
|
|
||||||
currentPath, err := os.Getwd()
|
currentPath, err := os.Getwd()
|
||||||
@@ -122,7 +119,7 @@ func (s *IntegrationDERPTestSuite) SetupSuite() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
headscaleOptions := &dockertest.RunOptions{
|
headscaleOptions := &dockertest.RunOptions{
|
||||||
Name: headscaleDerpHostname,
|
Name: headscaleDerpContainerName,
|
||||||
Mounts: []string{
|
Mounts: []string{
|
||||||
fmt.Sprintf(
|
fmt.Sprintf(
|
||||||
"%s/integration_test/etc_embedded_derp:/etc/headscale",
|
"%s/integration_test/etc_embedded_derp:/etc/headscale",
|
||||||
@@ -130,15 +127,15 @@ func (s *IntegrationDERPTestSuite) SetupSuite() {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
Cmd: []string{"headscale", "serve"},
|
Cmd: []string{"headscale", "serve"},
|
||||||
Networks: []*dockertest.Network{&s.network},
|
|
||||||
ExposedPorts: []string{"8443/tcp", "3478/udp"},
|
ExposedPorts: []string{"8443/tcp", "3478/udp"},
|
||||||
PortBindings: map[docker.Port][]docker.PortBinding{
|
PortBindings: map[docker.Port][]docker.PortBinding{
|
||||||
"8443/tcp": {{HostPort: "8443"}},
|
"8443/tcp": {{HostPort: "8443"}},
|
||||||
"3478/udp": {{HostPort: "3478"}},
|
"3478/udp": {{HostPort: "3478"}},
|
||||||
},
|
},
|
||||||
|
Platform: platform,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.pool.RemoveContainerByName(headscaleDerpHostname)
|
err = s.pool.RemoveContainerByName(headscaleDerpContainerName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.FailNow(
|
s.FailNow(
|
||||||
fmt.Sprintf(
|
fmt.Sprintf(
|
||||||
@@ -149,30 +146,28 @@ func (s *IntegrationDERPTestSuite) SetupSuite() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("Creating headscale container for DERP integration tests")
|
log.Println("Creating headscale container")
|
||||||
if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
|
if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
|
||||||
s.headscale = *pheadscale
|
s.headscale = *pheadscale
|
||||||
} else {
|
} else {
|
||||||
s.FailNow(fmt.Sprintf("Could not start headscale container: %s", err), "")
|
s.FailNow(fmt.Sprintf("Could not start headscale container: %s", err), "")
|
||||||
}
|
}
|
||||||
log.Println("Created headscale container for embedded DERP tests")
|
log.Println("Created headscale container to test DERP")
|
||||||
|
|
||||||
log.Println("Creating tailscale containers for embedded DERP tests")
|
log.Println("Creating tailscale containers")
|
||||||
|
|
||||||
for i := 0; i < totalContainers; i++ {
|
for i := 0; i < totalContainers; i++ {
|
||||||
version := tailscaleVersions[i%len(tailscaleVersions)]
|
version := tailscaleVersions[i%len(tailscaleVersions)]
|
||||||
hostname, container := s.tailscaleContainer(
|
hostname, container := s.tailscaleContainer(
|
||||||
fmt.Sprint(i),
|
fmt.Sprint(i),
|
||||||
version,
|
version,
|
||||||
s.containerNetworks[i],
|
s.networks[i],
|
||||||
)
|
)
|
||||||
s.tailscales[hostname] = *container
|
s.tailscales[hostname] = *container
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("Waiting for headscale to be ready for embedded DERP tests")
|
log.Println("Waiting for headscale to be ready")
|
||||||
hostEndpoint := fmt.Sprintf("%s:%s",
|
hostEndpoint := fmt.Sprintf("localhost:%s", s.headscale.GetPort("8443/tcp"))
|
||||||
s.headscale.GetIPInNetwork(&s.network),
|
|
||||||
s.headscale.GetPort("8443/tcp"))
|
|
||||||
|
|
||||||
if err := s.pool.Retry(func() error {
|
if err := s.pool.Retry(func() error {
|
||||||
url := fmt.Sprintf("https://%s/health", hostEndpoint)
|
url := fmt.Sprintf("https://%s/health", hostEndpoint)
|
||||||
@@ -181,7 +176,6 @@ func (s *IntegrationDERPTestSuite) SetupSuite() {
|
|||||||
client := &http.Client{Transport: insecureTransport}
|
client := &http.Client{Transport: insecureTransport}
|
||||||
resp, err := client.Get(url)
|
resp, err := client.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("headscale for embedded DERP tests is not ready: %s\n", err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,10 +191,10 @@ func (s *IntegrationDERPTestSuite) SetupSuite() {
|
|||||||
// https://github.com/stretchr/testify/issues/849
|
// https://github.com/stretchr/testify/issues/849
|
||||||
return // fmt.Errorf("Could not connect to headscale: %s", err)
|
return // fmt.Errorf("Could not connect to headscale: %s", err)
|
||||||
}
|
}
|
||||||
log.Println("headscale container is ready for embedded DERP tests")
|
log.Println("headscale container is ready")
|
||||||
|
|
||||||
log.Printf("Creating headscale namespace: %s\n", namespaceName)
|
log.Printf("Creating headscale namespace: %s\n", namespaceName)
|
||||||
result, _, err := ExecuteCommand(
|
result, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{"headscale", "namespaces", "create", namespaceName},
|
[]string{"headscale", "namespaces", "create", namespaceName},
|
||||||
[]string{},
|
[]string{},
|
||||||
@@ -209,7 +203,7 @@ func (s *IntegrationDERPTestSuite) SetupSuite() {
|
|||||||
assert.Nil(s.T(), err)
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
log.Printf("Creating pre auth key for %s\n", namespaceName)
|
log.Printf("Creating pre auth key for %s\n", namespaceName)
|
||||||
preAuthResult, _, err := ExecuteCommand(
|
preAuthResult, err := ExecuteCommand(
|
||||||
&s.headscale,
|
&s.headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
@@ -272,7 +266,7 @@ func (s *IntegrationDERPTestSuite) Join(
|
|||||||
|
|
||||||
log.Println("Join command:", command)
|
log.Println("Join command:", command)
|
||||||
log.Printf("Running join command for %s\n", hostname)
|
log.Printf("Running join command for %s\n", hostname)
|
||||||
_, _, err := ExecuteCommand(
|
_, err := ExecuteCommand(
|
||||||
&tailscale,
|
&tailscale,
|
||||||
command,
|
command,
|
||||||
[]string{},
|
[]string{},
|
||||||
@@ -333,7 +327,7 @@ func (s *IntegrationDERPTestSuite) TearDownSuite() {
|
|||||||
log.Printf("Could not purge resource: %s\n", err)
|
log.Printf("Could not purge resource: %s\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, network := range s.containerNetworks {
|
for _, network := range s.networks {
|
||||||
if err := network.Close(); err != nil {
|
if err := network.Close(); err != nil {
|
||||||
log.Printf("Could not close network: %s\n", err)
|
log.Printf("Could not close network: %s\n", err)
|
||||||
}
|
}
|
||||||
@@ -380,7 +374,7 @@ func (s *IntegrationDERPTestSuite) saveLog(
|
|||||||
|
|
||||||
log.Printf("Saving logs for %s to %s\n", resource.Container.Name, basePath)
|
log.Printf("Saving logs for %s to %s\n", resource.Container.Name, basePath)
|
||||||
|
|
||||||
err = os.WriteFile(
|
err = ioutil.WriteFile(
|
||||||
path.Join(basePath, resource.Container.Name+".stdout.log"),
|
path.Join(basePath, resource.Container.Name+".stdout.log"),
|
||||||
[]byte(stdout.String()),
|
[]byte(stdout.String()),
|
||||||
0o644,
|
0o644,
|
||||||
@@ -389,7 +383,7 @@ func (s *IntegrationDERPTestSuite) saveLog(
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.WriteFile(
|
err = ioutil.WriteFile(
|
||||||
path.Join(basePath, resource.Container.Name+".stderr.log"),
|
path.Join(basePath, resource.Container.Name+".stderr.log"),
|
||||||
[]byte(stdout.String()),
|
[]byte(stdout.String()),
|
||||||
0o644,
|
0o644,
|
||||||
@@ -427,7 +421,7 @@ func (s *IntegrationDERPTestSuite) TestPingAllPeersByHostname() {
|
|||||||
peername,
|
peername,
|
||||||
)
|
)
|
||||||
log.Println(command)
|
log.Println(command)
|
||||||
result, _, err := ExecuteCommand(
|
result, err := ExecuteCommand(
|
||||||
&tailscale,
|
&tailscale,
|
||||||
command,
|
command,
|
||||||
[]string{},
|
[]string{},
|
||||||
@@ -441,9 +435,7 @@ func (s *IntegrationDERPTestSuite) TestPingAllPeersByHostname() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *IntegrationDERPTestSuite) TestDERPSTUN() {
|
func (s *IntegrationDERPTestSuite) TestDERPSTUN() {
|
||||||
headscaleSTUNAddr := fmt.Sprintf("%s:%s",
|
headscaleSTUNAddr := fmt.Sprintf("localhost:%s", s.headscale.GetPort("3478/udp"))
|
||||||
s.headscale.GetIPInNetwork(&s.network),
|
|
||||||
s.headscale.GetPort("3478/udp"))
|
|
||||||
client := stun.NewClient()
|
client := stun.NewClient()
|
||||||
client.SetVerbose(true)
|
client.SetVerbose(true)
|
||||||
client.SetVVerbose(true)
|
client.SetVVerbose(true)
|
||||||
|
|||||||
@@ -1,558 +0,0 @@
|
|||||||
// nolint
|
|
||||||
package headscale
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ory/dockertest/v3"
|
|
||||||
"github.com/ory/dockertest/v3/docker"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/suite"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
oidcHeadscaleHostname = "headscale-oidc"
|
|
||||||
oidcMockHostname = "headscale-mock-oidc"
|
|
||||||
oidcNamespaceName = "oidcnamespace"
|
|
||||||
totalOidcContainers = 3
|
|
||||||
)
|
|
||||||
|
|
||||||
type IntegrationOIDCTestSuite struct {
|
|
||||||
suite.Suite
|
|
||||||
stats *suite.SuiteInformation
|
|
||||||
|
|
||||||
pool dockertest.Pool
|
|
||||||
network dockertest.Network
|
|
||||||
headscale dockertest.Resource
|
|
||||||
mockOidc dockertest.Resource
|
|
||||||
saveLogs bool
|
|
||||||
|
|
||||||
tailscales map[string]dockertest.Resource
|
|
||||||
joinWaitGroup sync.WaitGroup
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIntegrationOIDCTestSuite(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("skipping integration tests due to short flag")
|
|
||||||
}
|
|
||||||
|
|
||||||
saveLogs, err := GetEnvBool("HEADSCALE_INTEGRATION_SAVE_LOG")
|
|
||||||
if err != nil {
|
|
||||||
saveLogs = false
|
|
||||||
}
|
|
||||||
|
|
||||||
s := new(IntegrationOIDCTestSuite)
|
|
||||||
|
|
||||||
s.tailscales = make(map[string]dockertest.Resource)
|
|
||||||
s.saveLogs = saveLogs
|
|
||||||
|
|
||||||
suite.Run(t, s)
|
|
||||||
|
|
||||||
// HandleStats, which allows us to check if we passed and save logs
|
|
||||||
// is called after TearDown, so we cannot tear down containers before
|
|
||||||
// we have potentially saved the logs.
|
|
||||||
if s.saveLogs {
|
|
||||||
for _, tailscale := range s.tailscales {
|
|
||||||
if err := s.pool.Purge(&tailscale); err != nil {
|
|
||||||
log.Printf("Could not purge resource: %s\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !s.stats.Passed() {
|
|
||||||
err := s.saveLog(&s.headscale, "test_output")
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Could not save log: %s\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.pool.Purge(&s.mockOidc); err != nil {
|
|
||||||
log.Printf("Could not purge resource: %s\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.pool.Purge(&s.headscale); err != nil {
|
|
||||||
t.Logf("Could not purge resource: %s\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.network.Close(); err != nil {
|
|
||||||
log.Printf("Could not close network: %s\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *IntegrationOIDCTestSuite) SetupSuite() {
|
|
||||||
if ppool, err := dockertest.NewPool(""); err == nil {
|
|
||||||
s.pool = *ppool
|
|
||||||
} else {
|
|
||||||
s.FailNow(fmt.Sprintf("Could not connect to docker: %s", err), "")
|
|
||||||
}
|
|
||||||
|
|
||||||
network, err := GetFirstOrCreateNetwork(&s.pool, headscaleNetwork)
|
|
||||||
if err != nil {
|
|
||||||
s.FailNow(fmt.Sprintf("Failed to create or get network: %s", err), "")
|
|
||||||
}
|
|
||||||
s.network = network
|
|
||||||
|
|
||||||
log.Printf("Network config: %v", s.network.Network.IPAM.Config[0])
|
|
||||||
|
|
||||||
s.Suite.T().Log("Setting up mock OIDC")
|
|
||||||
mockOidcOptions := &dockertest.RunOptions{
|
|
||||||
Name: oidcMockHostname,
|
|
||||||
Cmd: []string{"headscale", "mockoidc"},
|
|
||||||
ExposedPorts: []string{"10000/tcp"},
|
|
||||||
PortBindings: map[docker.Port][]docker.PortBinding{
|
|
||||||
"10000/tcp": {{HostPort: "10000"}},
|
|
||||||
},
|
|
||||||
Networks: []*dockertest.Network{&s.network},
|
|
||||||
Env: []string{
|
|
||||||
fmt.Sprintf("MOCKOIDC_ADDR=%s", oidcMockHostname),
|
|
||||||
"MOCKOIDC_PORT=10000",
|
|
||||||
"MOCKOIDC_CLIENT_ID=superclient",
|
|
||||||
"MOCKOIDC_CLIENT_SECRET=supersecret",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
headscaleBuildOptions := &dockertest.BuildOptions{
|
|
||||||
Dockerfile: "Dockerfile.debug",
|
|
||||||
ContextDir: ".",
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.pool.RemoveContainerByName(oidcMockHostname)
|
|
||||||
if err != nil {
|
|
||||||
s.FailNow(
|
|
||||||
fmt.Sprintf(
|
|
||||||
"Could not remove existing container before building test: %s",
|
|
||||||
err,
|
|
||||||
),
|
|
||||||
"",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if pmockoidc, err := s.pool.BuildAndRunWithBuildOptions(
|
|
||||||
headscaleBuildOptions,
|
|
||||||
mockOidcOptions,
|
|
||||||
DockerRestartPolicy); err == nil {
|
|
||||||
s.mockOidc = *pmockoidc
|
|
||||||
} else {
|
|
||||||
s.FailNow(fmt.Sprintf("Could not start mockOIDC container: %s", err), "")
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Suite.T().Logf("Waiting for headscale mock oidc to be ready for tests")
|
|
||||||
hostEndpoint := fmt.Sprintf(
|
|
||||||
"%s:%s",
|
|
||||||
s.mockOidc.GetIPInNetwork(&s.network),
|
|
||||||
s.mockOidc.GetPort("10000/tcp"),
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := s.pool.Retry(func() error {
|
|
||||||
url := fmt.Sprintf("http://%s/oidc/.well-known/openid-configuration", hostEndpoint)
|
|
||||||
resp, err := http.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("headscale mock OIDC tests is not ready: %s\n", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return fmt.Errorf("status code not OK")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}); err != nil {
|
|
||||||
// TODO(kradalby): If we cannot access headscale, or any other fatal error during
|
|
||||||
// test setup, we need to abort and tear down. However, testify does not seem to
|
|
||||||
// support that at the moment:
|
|
||||||
// https://github.com/stretchr/testify/issues/849
|
|
||||||
return // fmt.Errorf("Could not connect to headscale: %s", err)
|
|
||||||
}
|
|
||||||
s.Suite.T().Log("headscale-mock-oidc container is ready for embedded OIDC tests")
|
|
||||||
|
|
||||||
oidcCfg := fmt.Sprintf(`
|
|
||||||
oidc:
|
|
||||||
issuer: http://%s:10000/oidc
|
|
||||||
client_id: superclient
|
|
||||||
client_secret: supersecret
|
|
||||||
strip_email_domain: true`, s.mockOidc.GetIPInNetwork(&s.network))
|
|
||||||
|
|
||||||
currentPath, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
s.FailNow(fmt.Sprintf("Could not determine current path: %s", err), "")
|
|
||||||
}
|
|
||||||
|
|
||||||
baseConfig, err := os.ReadFile(
|
|
||||||
path.Join(currentPath, "integration_test/etc_oidc/base_config.yaml"))
|
|
||||||
if err != nil {
|
|
||||||
s.FailNow(fmt.Sprintf("Could not read base config: %s", err), "")
|
|
||||||
}
|
|
||||||
config := string(baseConfig) + oidcCfg
|
|
||||||
|
|
||||||
log.Println(config)
|
|
||||||
|
|
||||||
configPath := path.Join(currentPath, "integration_test/etc_oidc/config.yaml")
|
|
||||||
err = os.WriteFile(configPath, []byte(config), 0o644)
|
|
||||||
if err != nil {
|
|
||||||
s.FailNow(fmt.Sprintf("Could not write config: %s", err), "")
|
|
||||||
}
|
|
||||||
|
|
||||||
headscaleOptions := &dockertest.RunOptions{
|
|
||||||
Name: oidcHeadscaleHostname,
|
|
||||||
Networks: []*dockertest.Network{&s.network},
|
|
||||||
Mounts: []string{
|
|
||||||
path.Join(currentPath,
|
|
||||||
"integration_test/etc_oidc:/etc/headscale",
|
|
||||||
),
|
|
||||||
},
|
|
||||||
Cmd: []string{"headscale", "serve"},
|
|
||||||
ExposedPorts: []string{"8443/tcp", "3478/udp"},
|
|
||||||
PortBindings: map[docker.Port][]docker.PortBinding{
|
|
||||||
"8443/tcp": {{HostPort: "8443"}},
|
|
||||||
"3478/udp": {{HostPort: "3478"}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.pool.RemoveContainerByName(oidcHeadscaleHostname)
|
|
||||||
if err != nil {
|
|
||||||
s.FailNow(
|
|
||||||
fmt.Sprintf(
|
|
||||||
"Could not remove existing container before building test: %s",
|
|
||||||
err,
|
|
||||||
),
|
|
||||||
"",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Suite.T().Logf("Creating headscale container for OIDC integration tests")
|
|
||||||
if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
|
|
||||||
s.headscale = *pheadscale
|
|
||||||
} else {
|
|
||||||
s.FailNow(fmt.Sprintf("Could not start headscale container: %s", err), "")
|
|
||||||
}
|
|
||||||
s.Suite.T().Logf("Created headscale container for embedded OIDC tests")
|
|
||||||
|
|
||||||
s.Suite.T().Logf("Creating tailscale containers for embedded OIDC tests")
|
|
||||||
|
|
||||||
for i := 0; i < totalOidcContainers; i++ {
|
|
||||||
version := tailscaleVersions[i%len(tailscaleVersions)]
|
|
||||||
hostname, container := s.tailscaleContainer(
|
|
||||||
fmt.Sprint(i),
|
|
||||||
version,
|
|
||||||
)
|
|
||||||
s.tailscales[hostname] = *container
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Suite.T().Logf("Waiting for headscale to be ready for embedded OIDC tests")
|
|
||||||
hostMockEndpoint := fmt.Sprintf(
|
|
||||||
"%s:%s",
|
|
||||||
s.headscale.GetIPInNetwork(&s.network),
|
|
||||||
s.headscale.GetPort("8443/tcp"),
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := s.pool.Retry(func() error {
|
|
||||||
url := fmt.Sprintf("https://%s/health", hostMockEndpoint)
|
|
||||||
insecureTransport := http.DefaultTransport.(*http.Transport).Clone()
|
|
||||||
insecureTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
|
||||||
client := &http.Client{Transport: insecureTransport}
|
|
||||||
resp, err := client.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("headscale for embedded OIDC tests is not ready: %s\n", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return fmt.Errorf("status code not OK")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}); err != nil {
|
|
||||||
// TODO(kradalby): If we cannot access headscale, or any other fatal error during
|
|
||||||
// test setup, we need to abort and tear down. However, testify does not seem to
|
|
||||||
// support that at the moment:
|
|
||||||
// https://github.com/stretchr/testify/issues/849
|
|
||||||
return // fmt.Errorf("Could not connect to headscale: %s", err)
|
|
||||||
}
|
|
||||||
s.Suite.T().Log("headscale container is ready for embedded OIDC tests")
|
|
||||||
|
|
||||||
s.Suite.T().Logf("Creating headscale namespace: %s\n", oidcNamespaceName)
|
|
||||||
result, _, err := ExecuteCommand(
|
|
||||||
&s.headscale,
|
|
||||||
[]string{"headscale", "namespaces", "create", oidcNamespaceName},
|
|
||||||
[]string{},
|
|
||||||
)
|
|
||||||
log.Println("headscale create namespace result: ", result)
|
|
||||||
assert.Nil(s.T(), err)
|
|
||||||
|
|
||||||
headscaleEndpoint := fmt.Sprintf(
|
|
||||||
"https://headscale:%s",
|
|
||||||
s.headscale.GetPort("8443/tcp"),
|
|
||||||
)
|
|
||||||
|
|
||||||
log.Printf(
|
|
||||||
"Joining tailscale containers to headscale at %s\n",
|
|
||||||
headscaleEndpoint,
|
|
||||||
)
|
|
||||||
for hostname, tailscale := range s.tailscales {
|
|
||||||
s.joinWaitGroup.Add(1)
|
|
||||||
go s.AuthenticateOIDC(headscaleEndpoint, hostname, tailscale)
|
|
||||||
|
|
||||||
// TODO(juan): Workaround for https://github.com/juanfont/headscale/issues/814
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.joinWaitGroup.Wait()
|
|
||||||
|
|
||||||
// The nodes need a bit of time to get their updated maps from headscale
|
|
||||||
// TODO: See if we can have a more deterministic wait here.
|
|
||||||
time.Sleep(60 * time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *IntegrationOIDCTestSuite) AuthenticateOIDC(
|
|
||||||
endpoint, hostname string,
|
|
||||||
tailscale dockertest.Resource,
|
|
||||||
) {
|
|
||||||
defer s.joinWaitGroup.Done()
|
|
||||||
|
|
||||||
loginURL, err := s.joinOIDC(endpoint, hostname, tailscale)
|
|
||||||
if err != nil {
|
|
||||||
s.FailNow(fmt.Sprintf("Could not join OIDC node: %s", err), "")
|
|
||||||
}
|
|
||||||
|
|
||||||
insecureTransport := &http.Transport{
|
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
||||||
}
|
|
||||||
client := &http.Client{Transport: insecureTransport}
|
|
||||||
resp, err := client.Get(loginURL.String())
|
|
||||||
assert.Nil(s.T(), err)
|
|
||||||
|
|
||||||
log.Printf("auth body, err: %#v, %s", resp, err)
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
assert.Nil(s.T(), err)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
s.FailNow(fmt.Sprintf("Could not read login page: %s", err), "")
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("Login page for %s: %s", hostname, string(body))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *IntegrationOIDCTestSuite) joinOIDC(
|
|
||||||
endpoint, hostname string,
|
|
||||||
tailscale dockertest.Resource,
|
|
||||||
) (*url.URL, error) {
|
|
||||||
command := []string{
|
|
||||||
"tailscale",
|
|
||||||
"up",
|
|
||||||
"-login-server",
|
|
||||||
endpoint,
|
|
||||||
"--hostname",
|
|
||||||
hostname,
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("Join command:", command)
|
|
||||||
log.Printf("Running join command for %s\n", hostname)
|
|
||||||
_, stderr, _ := ExecuteCommand(
|
|
||||||
&tailscale,
|
|
||||||
command,
|
|
||||||
[]string{},
|
|
||||||
)
|
|
||||||
|
|
||||||
// This piece of code just gets the login URL out of the stderr of the tailscale client.
|
|
||||||
// See https://github.com/tailscale/tailscale/blob/main/cmd/tailscale/cli/up.go#L584.
|
|
||||||
urlStr := strings.ReplaceAll(stderr, "\nTo authenticate, visit:\n\n\t", "")
|
|
||||||
urlStr = strings.TrimSpace(urlStr)
|
|
||||||
|
|
||||||
// parse URL
|
|
||||||
loginUrl, err := url.Parse(urlStr)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Could not parse login URL: %s", err)
|
|
||||||
log.Printf("Original join command result: %s", stderr)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return loginUrl, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *IntegrationOIDCTestSuite) tailscaleContainer(
|
|
||||||
identifier, version string,
|
|
||||||
) (string, *dockertest.Resource) {
|
|
||||||
tailscaleBuildOptions := getDockerBuildOptions(version)
|
|
||||||
|
|
||||||
hostname := fmt.Sprintf(
|
|
||||||
"tailscale-%s-%s",
|
|
||||||
strings.Replace(version, ".", "-", -1),
|
|
||||||
identifier,
|
|
||||||
)
|
|
||||||
tailscaleOptions := &dockertest.RunOptions{
|
|
||||||
Name: hostname,
|
|
||||||
Networks: []*dockertest.Network{&s.network},
|
|
||||||
Cmd: []string{
|
|
||||||
"tailscaled", "--tun=tsdev",
|
|
||||||
},
|
|
||||||
|
|
||||||
// expose the host IP address, so we can access it from inside the container
|
|
||||||
ExtraHosts: []string{
|
|
||||||
"host.docker.internal:host-gateway",
|
|
||||||
"headscale:host-gateway",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
pts, err := s.pool.BuildAndRunWithBuildOptions(
|
|
||||||
tailscaleBuildOptions,
|
|
||||||
tailscaleOptions,
|
|
||||||
DockerRestartPolicy,
|
|
||||||
DockerAllowLocalIPv6,
|
|
||||||
DockerAllowNetworkAdministration,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
s.FailNow(
|
|
||||||
fmt.Sprintf(
|
|
||||||
"Could not start tailscale container version %s: %s",
|
|
||||||
version,
|
|
||||||
err,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
log.Printf("Created %s container\n", hostname)
|
|
||||||
|
|
||||||
return hostname, pts
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *IntegrationOIDCTestSuite) TearDownSuite() {
|
|
||||||
if !s.saveLogs {
|
|
||||||
for _, tailscale := range s.tailscales {
|
|
||||||
if err := s.pool.Purge(&tailscale); err != nil {
|
|
||||||
log.Printf("Could not purge resource: %s\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.pool.Purge(&s.headscale); err != nil {
|
|
||||||
log.Printf("Could not purge resource: %s\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.pool.Purge(&s.mockOidc); err != nil {
|
|
||||||
log.Printf("Could not purge resource: %s\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.network.Close(); err != nil {
|
|
||||||
log.Printf("Could not close network: %s\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *IntegrationOIDCTestSuite) HandleStats(
|
|
||||||
suiteName string,
|
|
||||||
stats *suite.SuiteInformation,
|
|
||||||
) {
|
|
||||||
s.stats = stats
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *IntegrationOIDCTestSuite) saveLog(
|
|
||||||
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 = s.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"),
|
|
||||||
[]byte(stdout.String()),
|
|
||||||
0o644,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.WriteFile(
|
|
||||||
path.Join(basePath, resource.Container.Name+".stderr.log"),
|
|
||||||
[]byte(stdout.String()),
|
|
||||||
0o644,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *IntegrationOIDCTestSuite) TestPingAllPeersByAddress() {
|
|
||||||
for hostname, tailscale := range s.tailscales {
|
|
||||||
ips, err := getIPs(s.tailscales)
|
|
||||||
assert.Nil(s.T(), err)
|
|
||||||
for peername, peerIPs := range ips {
|
|
||||||
for i, ip := range peerIPs {
|
|
||||||
// We currently cant ping ourselves, so skip that.
|
|
||||||
if peername == hostname {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
s.T().
|
|
||||||
Run(fmt.Sprintf("%s-%s-%d", hostname, peername, i), func(t *testing.T) {
|
|
||||||
// We are only interested in "direct ping" which means what we
|
|
||||||
// might need a couple of more attempts before reaching the node.
|
|
||||||
command := []string{
|
|
||||||
"tailscale", "ping",
|
|
||||||
"--timeout=1s",
|
|
||||||
"--c=10",
|
|
||||||
"--until-direct=true",
|
|
||||||
ip.String(),
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf(
|
|
||||||
"Pinging from %s to %s (%s)\n",
|
|
||||||
hostname,
|
|
||||||
peername,
|
|
||||||
ip,
|
|
||||||
)
|
|
||||||
stdout, stderr, err := ExecuteCommand(
|
|
||||||
&tailscale,
|
|
||||||
command,
|
|
||||||
[]string{},
|
|
||||||
)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
log.Printf(
|
|
||||||
"result for %s: stdout: %s, stderr: %s\n",
|
|
||||||
hostname,
|
|
||||||
stdout,
|
|
||||||
stderr,
|
|
||||||
)
|
|
||||||
assert.Contains(t, stdout, "pong")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
764
integration_test.go
Normal file
764
integration_test.go
Normal file
@@ -0,0 +1,764 @@
|
|||||||
|
s/go:build integration
|
||||||
|
// +build integration
|
||||||
|
|
||||||
|
package headscale
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||||
|
"github.com/ory/dockertest/v3"
|
||||||
|
"github.com/ory/dockertest/v3/docker"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"inet.af/netaddr"
|
||||||
|
"tailscale.com/client/tailscale/apitype"
|
||||||
|
"tailscale.com/ipn/ipnstate"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
headscaleContainerName = "headscale"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IntegrationTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
stats *suite.SuiteInformation
|
||||||
|
|
||||||
|
pool dockertest.Pool
|
||||||
|
network dockertest.Network
|
||||||
|
headscale dockertest.Resource
|
||||||
|
saveLogs bool
|
||||||
|
|
||||||
|
namespaces map[string]TestNamespace
|
||||||
|
|
||||||
|
joinWaitGroup sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntegrationTestSuite(t *testing.T) {
|
||||||
|
saveLogs, err := GetEnvBool("HEADSCALE_INTEGRATION_SAVE_LOG")
|
||||||
|
if err != nil {
|
||||||
|
saveLogs = false
|
||||||
|
}
|
||||||
|
|
||||||
|
s := new(IntegrationTestSuite)
|
||||||
|
|
||||||
|
s.namespaces = map[string]TestNamespace{
|
||||||
|
"thisspace": {
|
||||||
|
count: 10,
|
||||||
|
tailscales: make(map[string]dockertest.Resource),
|
||||||
|
},
|
||||||
|
"otherspace": {
|
||||||
|
count: 2,
|
||||||
|
tailscales: make(map[string]dockertest.Resource),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
s.saveLogs = saveLogs
|
||||||
|
|
||||||
|
suite.Run(t, s)
|
||||||
|
|
||||||
|
// HandleStats, which allows us to check if we passed and save logs
|
||||||
|
// is called after TearDown, so we cannot tear down containers before
|
||||||
|
// we have potentially saved the logs.
|
||||||
|
if s.saveLogs {
|
||||||
|
for _, scales := range s.namespaces {
|
||||||
|
for _, tailscale := range scales.tailscales {
|
||||||
|
if err := s.pool.Purge(&tailscale); err != nil {
|
||||||
|
log.Printf("Could not purge resource: %s\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.stats.Passed() {
|
||||||
|
err := s.saveLog(&s.headscale, "test_output")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Could not save log: %s\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := s.pool.Purge(&s.headscale); err != nil {
|
||||||
|
log.Printf("Could not purge resource: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.network.Close(); err != nil {
|
||||||
|
log.Printf("Could not close network: %s\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IntegrationTestSuite) saveLog(
|
||||||
|
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 = s.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 = ioutil.WriteFile(
|
||||||
|
path.Join(basePath, resource.Container.Name+".stdout.log"),
|
||||||
|
[]byte(stdout.String()),
|
||||||
|
0o644,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ioutil.WriteFile(
|
||||||
|
path.Join(basePath, resource.Container.Name+".stderr.log"),
|
||||||
|
[]byte(stdout.String()),
|
||||||
|
0o644,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IntegrationTestSuite) Join(
|
||||||
|
endpoint, key, hostname string,
|
||||||
|
tailscale dockertest.Resource,
|
||||||
|
) {
|
||||||
|
defer s.joinWaitGroup.Done()
|
||||||
|
|
||||||
|
command := []string{
|
||||||
|
"tailscale",
|
||||||
|
"up",
|
||||||
|
"-login-server",
|
||||||
|
endpoint,
|
||||||
|
"--authkey",
|
||||||
|
key,
|
||||||
|
"--hostname",
|
||||||
|
hostname,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Join command:", command)
|
||||||
|
log.Printf("Running join command for %s\n", hostname)
|
||||||
|
_, err := ExecuteCommand(
|
||||||
|
&tailscale,
|
||||||
|
command,
|
||||||
|
[]string{},
|
||||||
|
)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
log.Printf("%s joined\n", hostname)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IntegrationTestSuite) tailscaleContainer(
|
||||||
|
namespace, identifier, version string,
|
||||||
|
) (string, *dockertest.Resource) {
|
||||||
|
tailscaleBuildOptions := getDockerBuildOptions(version)
|
||||||
|
|
||||||
|
hostname := fmt.Sprintf(
|
||||||
|
"%s-tailscale-%s-%s",
|
||||||
|
namespace,
|
||||||
|
strings.Replace(version, ".", "-", -1),
|
||||||
|
identifier,
|
||||||
|
)
|
||||||
|
tailscaleOptions := &dockertest.RunOptions{
|
||||||
|
Name: hostname,
|
||||||
|
Networks: []*dockertest.Network{&s.network},
|
||||||
|
Cmd: []string{
|
||||||
|
"tailscaled", "--tun=tsdev",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pts, err := s.pool.BuildAndRunWithBuildOptions(
|
||||||
|
tailscaleBuildOptions,
|
||||||
|
tailscaleOptions,
|
||||||
|
DockerRestartPolicy,
|
||||||
|
DockerAllowLocalIPv6,
|
||||||
|
DockerAllowNetworkAdministration,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Could not start tailscale container version %s: %s", version, err)
|
||||||
|
}
|
||||||
|
log.Printf("Created %s container\n", hostname)
|
||||||
|
|
||||||
|
return hostname, pts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IntegrationTestSuite) SetupSuite() {
|
||||||
|
var err error
|
||||||
|
app = Headscale{
|
||||||
|
dbType: "sqlite3",
|
||||||
|
dbString: "integration_test_db.sqlite3",
|
||||||
|
}
|
||||||
|
|
||||||
|
if ppool, err := dockertest.NewPool(""); err == nil {
|
||||||
|
s.pool = *ppool
|
||||||
|
} else {
|
||||||
|
s.FailNow(fmt.Sprintf("Could not connect to docker: %s", err), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
if pnetwork, err := s.pool.CreateNetwork("headscale-test"); err == nil {
|
||||||
|
s.network = *pnetwork
|
||||||
|
} else {
|
||||||
|
s.FailNow(fmt.Sprintf("Could not create network: %s", err), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
platform := fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
|
|
||||||
|
headscaleBuildOptions := &dockertest.BuildOptions{
|
||||||
|
Dockerfile: "Dockerfile",
|
||||||
|
ContextDir: ".",
|
||||||
|
Platform: platform,
|
||||||
|
Version: "2",
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPath, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
s.FailNow(fmt.Sprintf("Could not determine current path: %s", err), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
headscaleOptions := &dockertest.RunOptions{
|
||||||
|
Name: "headscale",
|
||||||
|
Mounts: []string{
|
||||||
|
fmt.Sprintf("%s/integration_test/etc:/etc/headscale", currentPath),
|
||||||
|
},
|
||||||
|
Networks: []*dockertest.Network{&s.network},
|
||||||
|
Cmd: []string{"headscale", "serve"},
|
||||||
|
Platform: platform,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.pool.RemoveContainerByName(headscaleContainerName)
|
||||||
|
if err != nil {
|
||||||
|
s.FailNow(fmt.Sprintf("Could not remove existing container before building test: %s", err), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Creating headscale container")
|
||||||
|
if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
|
||||||
|
s.headscale = *pheadscale
|
||||||
|
} else {
|
||||||
|
s.FailNow(fmt.Sprintf("Could not start headscale container: %s", err), "")
|
||||||
|
}
|
||||||
|
log.Println("Created headscale container")
|
||||||
|
|
||||||
|
log.Println("Creating tailscale containers")
|
||||||
|
for namespace, scales := range s.namespaces {
|
||||||
|
for i := 0; i < scales.count; i++ {
|
||||||
|
version := tailscaleVersions[i%len(tailscaleVersions)]
|
||||||
|
|
||||||
|
hostname, container := s.tailscaleContainer(
|
||||||
|
namespace,
|
||||||
|
fmt.Sprint(i),
|
||||||
|
version,
|
||||||
|
)
|
||||||
|
scales.tailscales[hostname] = *container
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Waiting for headscale to be ready")
|
||||||
|
hostEndpoint := fmt.Sprintf("localhost:%s", s.headscale.GetPort("8080/tcp"))
|
||||||
|
|
||||||
|
if err := s.pool.Retry(func() error {
|
||||||
|
url := fmt.Sprintf("http://%s/health", hostEndpoint)
|
||||||
|
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("status code not OK")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
// TODO(kradalby): If we cannot access headscale, or any other fatal error during
|
||||||
|
// test setup, we need to abort and tear down. However, testify does not seem to
|
||||||
|
// support that at the moment:
|
||||||
|
// https://github.com/stretchr/testify/issues/849
|
||||||
|
return // fmt.Errorf("Could not connect to headscale: %s", err)
|
||||||
|
}
|
||||||
|
log.Println("headscale container is ready")
|
||||||
|
|
||||||
|
for namespace, scales := range s.namespaces {
|
||||||
|
log.Printf("Creating headscale namespace: %s\n", namespace)
|
||||||
|
result, err := ExecuteCommand(
|
||||||
|
&s.headscale,
|
||||||
|
[]string{"headscale", "namespaces", "create", namespace},
|
||||||
|
[]string{},
|
||||||
|
)
|
||||||
|
log.Println("headscale create namespace result: ", result)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
log.Printf("Creating pre auth key for %s\n", namespace)
|
||||||
|
preAuthResult, err := ExecuteCommand(
|
||||||
|
&s.headscale,
|
||||||
|
[]string{
|
||||||
|
"headscale",
|
||||||
|
"--namespace",
|
||||||
|
namespace,
|
||||||
|
"preauthkeys",
|
||||||
|
"create",
|
||||||
|
"--reusable",
|
||||||
|
"--expiration",
|
||||||
|
"24h",
|
||||||
|
"--output",
|
||||||
|
"json",
|
||||||
|
},
|
||||||
|
[]string{"LOG_LEVEL=error"},
|
||||||
|
)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
var preAuthKey v1.PreAuthKey
|
||||||
|
err = json.Unmarshal([]byte(preAuthResult), &preAuthKey)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
assert.True(s.T(), preAuthKey.Reusable)
|
||||||
|
|
||||||
|
headscaleEndpoint := "http://headscale:8080"
|
||||||
|
|
||||||
|
log.Printf(
|
||||||
|
"Joining tailscale containers to headscale at %s\n",
|
||||||
|
headscaleEndpoint,
|
||||||
|
)
|
||||||
|
for hostname, tailscale := range scales.tailscales {
|
||||||
|
s.joinWaitGroup.Add(1)
|
||||||
|
go s.Join(headscaleEndpoint, preAuthKey.Key, hostname, tailscale)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.joinWaitGroup.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// The nodes need a bit of time to get their updated maps from headscale
|
||||||
|
// TODO: See if we can have a more deterministic wait here.
|
||||||
|
time.Sleep(60 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IntegrationTestSuite) TearDownSuite() {
|
||||||
|
if !s.saveLogs {
|
||||||
|
for _, scales := range s.namespaces {
|
||||||
|
for _, tailscale := range scales.tailscales {
|
||||||
|
if err := s.pool.Purge(&tailscale); err != nil {
|
||||||
|
log.Printf("Could not purge resource: %s\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.pool.Purge(&s.headscale); err != nil {
|
||||||
|
log.Printf("Could not purge resource: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.network.Close(); err != nil {
|
||||||
|
log.Printf("Could not close network: %s\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IntegrationTestSuite) HandleStats(
|
||||||
|
suiteName string,
|
||||||
|
stats *suite.SuiteInformation,
|
||||||
|
) {
|
||||||
|
s.stats = stats
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IntegrationTestSuite) TestListNodes() {
|
||||||
|
for namespace, scales := range s.namespaces {
|
||||||
|
log.Println("Listing nodes")
|
||||||
|
result, err := ExecuteCommand(
|
||||||
|
&s.headscale,
|
||||||
|
[]string{"headscale", "--namespace", namespace, "nodes", "list"},
|
||||||
|
[]string{},
|
||||||
|
)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
log.Printf("List nodes: \n%s\n", result)
|
||||||
|
|
||||||
|
// Chck that the correct count of host is present in node list
|
||||||
|
lines := strings.Split(result, "\n")
|
||||||
|
assert.Equal(s.T(), len(scales.tailscales), len(lines)-2)
|
||||||
|
|
||||||
|
for hostname := range scales.tailscales {
|
||||||
|
assert.Contains(s.T(), result, hostname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IntegrationTestSuite) TestGetIpAddresses() {
|
||||||
|
for _, scales := range s.namespaces {
|
||||||
|
ips, err := getIPs(scales.tailscales)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
for hostname := range scales.tailscales {
|
||||||
|
ips := ips[hostname]
|
||||||
|
for _, ip := range ips {
|
||||||
|
s.T().Run(hostname, func(t *testing.T) {
|
||||||
|
assert.NotNil(t, ip)
|
||||||
|
|
||||||
|
log.Printf("IP for %s: %s\n", hostname, ip)
|
||||||
|
|
||||||
|
// c.Assert(ip.Valid(), check.IsTrue)
|
||||||
|
assert.True(t, ip.Is4() || ip.Is6())
|
||||||
|
switch {
|
||||||
|
case ip.Is4():
|
||||||
|
assert.True(t, IpPrefix4.Contains(ip))
|
||||||
|
case ip.Is6():
|
||||||
|
assert.True(t, IpPrefix6.Contains(ip))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(kradalby): fix this test
|
||||||
|
// We need some way to import ipnstate.Status from multiple go packages.
|
||||||
|
// Currently it will only work with 1.18.x since that is the last
|
||||||
|
// version we have in go.mod
|
||||||
|
// func (s *IntegrationTestSuite) TestStatus() {
|
||||||
|
// for _, scales := range s.namespaces {
|
||||||
|
// ips, err := getIPs(scales.tailscales)
|
||||||
|
// assert.Nil(s.T(), err)
|
||||||
|
//
|
||||||
|
// for hostname, tailscale := range scales.tailscales {
|
||||||
|
// s.T().Run(hostname, func(t *testing.T) {
|
||||||
|
// command := []string{"tailscale", "status", "--json"}
|
||||||
|
//
|
||||||
|
// log.Printf("Getting status for %s\n", hostname)
|
||||||
|
// result, err := ExecuteCommand(
|
||||||
|
// &tailscale,
|
||||||
|
// command,
|
||||||
|
// []string{},
|
||||||
|
// )
|
||||||
|
// assert.Nil(t, err)
|
||||||
|
//
|
||||||
|
// var status ipnstate.Status
|
||||||
|
// err = json.Unmarshal([]byte(result), &status)
|
||||||
|
// assert.Nil(s.T(), err)
|
||||||
|
//
|
||||||
|
// // TODO(kradalby): Replace this check with peer length of SAME namespace
|
||||||
|
// // Check if we have as many nodes in status
|
||||||
|
// // as we have IPs/tailscales
|
||||||
|
// // lines := strings.Split(result, "\n")
|
||||||
|
// // assert.Equal(t, len(ips), len(lines)-1)
|
||||||
|
// // assert.Equal(t, len(scales.tailscales), len(lines)-1)
|
||||||
|
//
|
||||||
|
// peerIps := getIPsfromIPNstate(status)
|
||||||
|
//
|
||||||
|
// // Check that all hosts is present in all hosts status
|
||||||
|
// for ipHostname, ip := range ips {
|
||||||
|
// if hostname != ipHostname {
|
||||||
|
// assert.Contains(t, peerIps, ip)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
func getIPsfromIPNstate(status ipnstate.Status) []netaddr.IP {
|
||||||
|
ips := make([]netaddr.IP, 0)
|
||||||
|
|
||||||
|
for _, peer := range status.Peer {
|
||||||
|
ips = append(ips, peer.TailscaleIPs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ips
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Adopt test for cross communication between namespaces
|
||||||
|
func (s *IntegrationTestSuite) TestPingAllPeersByAddress() {
|
||||||
|
for _, scales := range s.namespaces {
|
||||||
|
ips, err := getIPs(scales.tailscales)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
for hostname, tailscale := range scales.tailscales {
|
||||||
|
for peername, peerIPs := range ips {
|
||||||
|
for i, ip := range peerIPs {
|
||||||
|
// We currently cant ping ourselves, so skip that.
|
||||||
|
if peername == hostname {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.T().
|
||||||
|
Run(fmt.Sprintf("%s-%s-%d", hostname, peername, i), func(t *testing.T) {
|
||||||
|
// We are only interested in "direct ping" which means what we
|
||||||
|
// might need a couple of more attempts before reaching the node.
|
||||||
|
command := []string{
|
||||||
|
"tailscale", "ping",
|
||||||
|
"--timeout=1s",
|
||||||
|
"--c=10",
|
||||||
|
"--until-direct=true",
|
||||||
|
ip.String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf(
|
||||||
|
"Pinging from %s to %s (%s)\n",
|
||||||
|
hostname,
|
||||||
|
peername,
|
||||||
|
ip,
|
||||||
|
)
|
||||||
|
result, err := ExecuteCommand(
|
||||||
|
&tailscale,
|
||||||
|
command,
|
||||||
|
[]string{},
|
||||||
|
)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
log.Printf("Result for %s: %s\n", hostname, result)
|
||||||
|
assert.Contains(t, result, "pong")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IntegrationTestSuite) TestTailDrop() {
|
||||||
|
for _, scales := range s.namespaces {
|
||||||
|
ips, err := getIPs(scales.tailscales)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
retry := func(times int, sleepInverval time.Duration, doWork func() error) (err error) {
|
||||||
|
for attempts := 0; attempts < times; attempts++ {
|
||||||
|
err = doWork()
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(sleepInverval)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for hostname, tailscale := range scales.tailscales {
|
||||||
|
command := []string{"touch", fmt.Sprintf("/tmp/file_from_%s", hostname)}
|
||||||
|
_, err := ExecuteCommand(
|
||||||
|
&tailscale,
|
||||||
|
command,
|
||||||
|
[]string{},
|
||||||
|
)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
for peername := range ips {
|
||||||
|
if peername == hostname {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) {
|
||||||
|
command := []string{
|
||||||
|
"tailscale", "file", "cp",
|
||||||
|
fmt.Sprintf("/tmp/file_from_%s", hostname),
|
||||||
|
fmt.Sprintf("%s:", ips[peername][1]),
|
||||||
|
}
|
||||||
|
retry(10, 1*time.Second, func() error {
|
||||||
|
log.Printf(
|
||||||
|
"Sending file from %s to %s\n",
|
||||||
|
hostname,
|
||||||
|
peername,
|
||||||
|
)
|
||||||
|
_, err := ExecuteCommand(
|
||||||
|
&tailscale,
|
||||||
|
command,
|
||||||
|
[]string{},
|
||||||
|
ExecuteCommandTimeout(60*time.Second),
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for hostname, tailscale := range scales.tailscales {
|
||||||
|
command := []string{
|
||||||
|
"tailscale", "file",
|
||||||
|
"get",
|
||||||
|
"/tmp/",
|
||||||
|
}
|
||||||
|
_, err := ExecuteCommand(
|
||||||
|
&tailscale,
|
||||||
|
command,
|
||||||
|
[]string{},
|
||||||
|
)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
for peername, ip := range ips {
|
||||||
|
if peername == hostname {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) {
|
||||||
|
command := []string{
|
||||||
|
"ls",
|
||||||
|
fmt.Sprintf("/tmp/file_from_%s", peername),
|
||||||
|
}
|
||||||
|
log.Printf(
|
||||||
|
"Checking file in %s (%s) from %s (%s)\n",
|
||||||
|
hostname,
|
||||||
|
ips[hostname][1],
|
||||||
|
peername,
|
||||||
|
ip,
|
||||||
|
)
|
||||||
|
result, err := ExecuteCommand(
|
||||||
|
&tailscale,
|
||||||
|
command,
|
||||||
|
[]string{},
|
||||||
|
)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
log.Printf("Result for %s: %s\n", peername, result)
|
||||||
|
assert.Equal(
|
||||||
|
t,
|
||||||
|
fmt.Sprintf("/tmp/file_from_%s\n", peername),
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IntegrationTestSuite) TestPingAllPeersByHostname() {
|
||||||
|
hostnames, err := getMagicFQDN(&s.headscale)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
log.Printf("Resolved hostnames: %#v", hostnames)
|
||||||
|
for _, scales := range s.namespaces {
|
||||||
|
for hostname, tailscale := range scales.tailscales {
|
||||||
|
for _, peername := range hostnames {
|
||||||
|
if strings.Contains(peername, hostname) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) {
|
||||||
|
command := []string{
|
||||||
|
"tailscale", "ping",
|
||||||
|
"--timeout=10s",
|
||||||
|
"--c=20",
|
||||||
|
"--until-direct=true",
|
||||||
|
peername,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf(
|
||||||
|
"Pinging using hostname from %s to %s\n",
|
||||||
|
hostname,
|
||||||
|
peername,
|
||||||
|
)
|
||||||
|
result, err := ExecuteCommand(
|
||||||
|
&tailscale,
|
||||||
|
command,
|
||||||
|
[]string{},
|
||||||
|
)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
log.Printf("Result for %s: %s\n", hostname, result)
|
||||||
|
assert.Contains(t, result, "pong")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IntegrationTestSuite) TestMagicDNS() {
|
||||||
|
hostnames, err := getMagicFQDN(&s.headscale)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
log.Printf("Resolved hostnames: %#v", hostnames)
|
||||||
|
|
||||||
|
for _, scales := range s.namespaces {
|
||||||
|
ips, err := getIPs(scales.tailscales)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
for hostname, tailscale := range scales.tailscales {
|
||||||
|
for _, peername := range hostnames {
|
||||||
|
if strings.Contains(peername, hostname) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) {
|
||||||
|
command := []string{
|
||||||
|
"tailscale", "ip", peername,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf(
|
||||||
|
"Resolving name %s from %s\n",
|
||||||
|
peername,
|
||||||
|
hostname,
|
||||||
|
)
|
||||||
|
result, err := ExecuteCommand(
|
||||||
|
&tailscale,
|
||||||
|
command,
|
||||||
|
[]string{},
|
||||||
|
)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
log.Printf("Result for %s: %s\n", hostname, result)
|
||||||
|
|
||||||
|
peerBaseName := peername[:len(peername)-MachineGivenNameHashLength-1]
|
||||||
|
expectedAddresses := ips[peerBaseName]
|
||||||
|
for _, ip := range expectedAddresses {
|
||||||
|
assert.Contains(t, result, ip.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAPIURLs(
|
||||||
|
tailscales map[string]dockertest.Resource,
|
||||||
|
) (map[netaddr.IP]string, error) {
|
||||||
|
fts := make(map[netaddr.IP]string)
|
||||||
|
for _, tailscale := range tailscales {
|
||||||
|
command := []string{
|
||||||
|
"curl",
|
||||||
|
"--unix-socket",
|
||||||
|
"/run/tailscale/tailscaled.sock",
|
||||||
|
"http://localhost/localapi/v0/file-targets",
|
||||||
|
}
|
||||||
|
result, err := ExecuteCommand(
|
||||||
|
&tailscale,
|
||||||
|
command,
|
||||||
|
[]string{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var pft []apitype.FileTarget
|
||||||
|
if err := json.Unmarshal([]byte(result), &pft); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid JSON: %w", err)
|
||||||
|
}
|
||||||
|
for _, ft := range pft {
|
||||||
|
n := ft.Node
|
||||||
|
for _, a := range n.Addresses { // just add all the addresses
|
||||||
|
if _, ok := fts[a.IP()]; !ok {
|
||||||
|
if ft.PeerAPIURL == "" {
|
||||||
|
return nil, errors.New("api url is empty")
|
||||||
|
}
|
||||||
|
fts[a.IP()] = ft.PeerAPIURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fts, nil
|
||||||
|
}
|
||||||
@@ -14,12 +14,10 @@ derp:
|
|||||||
urls:
|
urls:
|
||||||
- https://controlplane.tailscale.com/derpmap/default
|
- https://controlplane.tailscale.com/derpmap/default
|
||||||
dns_config:
|
dns_config:
|
||||||
override_local_dns: true
|
|
||||||
base_domain: headscale.net
|
base_domain: headscale.net
|
||||||
domains: []
|
domains: []
|
||||||
magic_dns: true
|
magic_dns: true
|
||||||
nameservers:
|
nameservers:
|
||||||
- 127.0.0.11
|
|
||||||
- 1.1.1.1
|
- 1.1.1.1
|
||||||
ephemeral_node_inactivity_timeout: 30m
|
ephemeral_node_inactivity_timeout: 30m
|
||||||
node_update_check_interval: 10s
|
node_update_check_interval: 10s
|
||||||
@@ -29,22 +27,17 @@ ip_prefixes:
|
|||||||
- fd7a:115c:a1e0::/48
|
- fd7a:115c:a1e0::/48
|
||||||
- 100.64.0.0/10
|
- 100.64.0.0/10
|
||||||
listen_addr: 0.0.0.0:18080
|
listen_addr: 0.0.0.0:18080
|
||||||
log:
|
log_level: disabled
|
||||||
level: disabled
|
|
||||||
format: text
|
|
||||||
logtail:
|
logtail:
|
||||||
enabled: false
|
enabled: false
|
||||||
metrics_listen_addr: 127.0.0.1:19090
|
metrics_listen_addr: 127.0.0.1:19090
|
||||||
oidc:
|
oidc:
|
||||||
only_start_if_oidc_is_available: true
|
|
||||||
scope:
|
scope:
|
||||||
- openid
|
- openid
|
||||||
- profile
|
- profile
|
||||||
- email
|
- email
|
||||||
strip_email_domain: true
|
strip_email_domain: true
|
||||||
private_key_path: private.key
|
private_key_path: private.key
|
||||||
noise:
|
|
||||||
private_key_path: noise_private.key
|
|
||||||
server_url: http://headscale:18080
|
server_url: http://headscale:18080
|
||||||
tls_client_auth_mode: relaxed
|
tls_client_auth_mode: relaxed
|
||||||
tls_letsencrypt_cache_dir: /var/www/.cache
|
tls_letsencrypt_cache_dir: /var/www/.cache
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
log:
|
log_level: trace
|
||||||
level: trace
|
|
||||||
acl_policy_path: ""
|
acl_policy_path: ""
|
||||||
db_type: sqlite3
|
db_type: sqlite3
|
||||||
ephemeral_node_inactivity_timeout: 30m
|
ephemeral_node_inactivity_timeout: 30m
|
||||||
@@ -8,17 +7,13 @@ ip_prefixes:
|
|||||||
- fd7a:115c:a1e0::/48
|
- fd7a:115c:a1e0::/48
|
||||||
- 100.64.0.0/10
|
- 100.64.0.0/10
|
||||||
dns_config:
|
dns_config:
|
||||||
override_local_dns: true
|
|
||||||
base_domain: headscale.net
|
base_domain: headscale.net
|
||||||
magic_dns: true
|
magic_dns: true
|
||||||
domains: []
|
domains: []
|
||||||
nameservers:
|
nameservers:
|
||||||
- 127.0.0.11
|
|
||||||
- 1.1.1.1
|
- 1.1.1.1
|
||||||
db_path: /tmp/integration_test_db.sqlite3
|
db_path: /tmp/integration_test_db.sqlite3
|
||||||
private_key_path: private.key
|
private_key_path: private.key
|
||||||
noise:
|
|
||||||
private_key_path: noise_private.key
|
|
||||||
listen_addr: 0.0.0.0:18080
|
listen_addr: 0.0.0.0:18080
|
||||||
metrics_listen_addr: 127.0.0.1:19090
|
metrics_listen_addr: 127.0.0.1:19090
|
||||||
server_url: http://headscale:18080
|
server_url: http://headscale:18080
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
acl_policy_path: ""
|
|
||||||
cli:
|
|
||||||
insecure: false
|
|
||||||
timeout: 5s
|
|
||||||
db_path: /tmp/integration_test_db.sqlite3
|
|
||||||
db_type: sqlite3
|
|
||||||
derp:
|
|
||||||
auto_update_enabled: false
|
|
||||||
server:
|
|
||||||
enabled: false
|
|
||||||
stun:
|
|
||||||
enabled: true
|
|
||||||
update_frequency: 1m
|
|
||||||
urls:
|
|
||||||
- https://controlplane.tailscale.com/derpmap/default
|
|
||||||
dns_config:
|
|
||||||
override_local_dns: true
|
|
||||||
base_domain: headscale.net
|
|
||||||
domains: []
|
|
||||||
magic_dns: true
|
|
||||||
nameservers:
|
|
||||||
- 1.1.1.1
|
|
||||||
ephemeral_node_inactivity_timeout: 30m
|
|
||||||
node_update_check_interval: 30s
|
|
||||||
grpc_allow_insecure: false
|
|
||||||
grpc_listen_addr: :50443
|
|
||||||
ip_prefixes:
|
|
||||||
- fd7a:115c:a1e0::/48
|
|
||||||
- 100.64.0.0/10
|
|
||||||
listen_addr: 0.0.0.0:18080
|
|
||||||
log:
|
|
||||||
level: disabled
|
|
||||||
format: text
|
|
||||||
logtail:
|
|
||||||
enabled: false
|
|
||||||
metrics_listen_addr: 127.0.0.1:19090
|
|
||||||
oidc:
|
|
||||||
only_start_if_oidc_is_available: true
|
|
||||||
scope:
|
|
||||||
- openid
|
|
||||||
- profile
|
|
||||||
- email
|
|
||||||
strip_email_domain: true
|
|
||||||
private_key_path: private.key
|
|
||||||
noise:
|
|
||||||
private_key_path: noise_private.key
|
|
||||||
server_url: http://headscale:18080
|
|
||||||
tls_client_auth_mode: relaxed
|
|
||||||
tls_letsencrypt_cache_dir: /var/www/.cache
|
|
||||||
tls_letsencrypt_challenge_type: HTTP-01
|
|
||||||
unix_socket: /var/run/headscale.sock
|
|
||||||
unix_socket_permission: "0o770"
|
|
||||||
randomize_client_port: false
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
log:
|
|
||||||
level: trace
|
|
||||||
acl_policy_path: ""
|
|
||||||
db_type: sqlite3
|
|
||||||
ephemeral_node_inactivity_timeout: 30m
|
|
||||||
node_update_check_interval: 30s
|
|
||||||
ip_prefixes:
|
|
||||||
- fd7a:115c:a1e0::/48
|
|
||||||
- 100.64.0.0/10
|
|
||||||
dns_config:
|
|
||||||
override_local_dns: true
|
|
||||||
base_domain: headscale.net
|
|
||||||
magic_dns: true
|
|
||||||
domains: []
|
|
||||||
nameservers:
|
|
||||||
- 1.1.1.1
|
|
||||||
db_path: /tmp/integration_test_db.sqlite3
|
|
||||||
private_key_path: private.key
|
|
||||||
noise:
|
|
||||||
private_key_path: noise_private.key
|
|
||||||
listen_addr: 0.0.0.0:18080
|
|
||||||
metrics_listen_addr: 127.0.0.1:19090
|
|
||||||
server_url: http://headscale:18080
|
|
||||||
|
|
||||||
derp:
|
|
||||||
urls:
|
|
||||||
- https://controlplane.tailscale.com/derpmap/default
|
|
||||||
auto_update_enabled: false
|
|
||||||
update_frequency: 1m
|
|
||||||
@@ -14,12 +14,10 @@ derp:
|
|||||||
urls:
|
urls:
|
||||||
- https://controlplane.tailscale.com/derpmap/default
|
- https://controlplane.tailscale.com/derpmap/default
|
||||||
dns_config:
|
dns_config:
|
||||||
override_local_dns: true
|
|
||||||
base_domain: headscale.net
|
base_domain: headscale.net
|
||||||
domains: []
|
domains: []
|
||||||
magic_dns: true
|
magic_dns: true
|
||||||
nameservers:
|
nameservers:
|
||||||
- 127.0.0.11
|
|
||||||
- 1.1.1.1
|
- 1.1.1.1
|
||||||
ephemeral_node_inactivity_timeout: 30m
|
ephemeral_node_inactivity_timeout: 30m
|
||||||
node_update_check_interval: 10s
|
node_update_check_interval: 10s
|
||||||
@@ -29,22 +27,17 @@ ip_prefixes:
|
|||||||
- fd7a:115c:a1e0::/48
|
- fd7a:115c:a1e0::/48
|
||||||
- 100.64.0.0/10
|
- 100.64.0.0/10
|
||||||
listen_addr: 0.0.0.0:8080
|
listen_addr: 0.0.0.0:8080
|
||||||
log:
|
log_level: disabled
|
||||||
format: text
|
|
||||||
level: disabled
|
|
||||||
logtail:
|
logtail:
|
||||||
enabled: false
|
enabled: false
|
||||||
metrics_listen_addr: 127.0.0.1:9090
|
metrics_listen_addr: 127.0.0.1:9090
|
||||||
oidc:
|
oidc:
|
||||||
only_start_if_oidc_is_available: true
|
|
||||||
scope:
|
scope:
|
||||||
- openid
|
- openid
|
||||||
- profile
|
- profile
|
||||||
- email
|
- email
|
||||||
strip_email_domain: true
|
strip_email_domain: true
|
||||||
private_key_path: private.key
|
private_key_path: private.key
|
||||||
noise:
|
|
||||||
private_key_path: noise_private.key
|
|
||||||
server_url: http://headscale:8080
|
server_url: http://headscale:8080
|
||||||
tls_client_auth_mode: relaxed
|
tls_client_auth_mode: relaxed
|
||||||
tls_letsencrypt_cache_dir: /var/www/.cache
|
tls_letsencrypt_cache_dir: /var/www/.cache
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
log:
|
log_level: trace
|
||||||
level: trace
|
|
||||||
acl_policy_path: ""
|
acl_policy_path: ""
|
||||||
db_type: sqlite3
|
db_type: sqlite3
|
||||||
ephemeral_node_inactivity_timeout: 30m
|
ephemeral_node_inactivity_timeout: 30m
|
||||||
@@ -8,17 +7,13 @@ ip_prefixes:
|
|||||||
- fd7a:115c:a1e0::/48
|
- fd7a:115c:a1e0::/48
|
||||||
- 100.64.0.0/10
|
- 100.64.0.0/10
|
||||||
dns_config:
|
dns_config:
|
||||||
override_local_dns: true
|
|
||||||
base_domain: headscale.net
|
base_domain: headscale.net
|
||||||
magic_dns: true
|
magic_dns: true
|
||||||
domains: []
|
domains: []
|
||||||
nameservers:
|
nameservers:
|
||||||
- 127.0.0.11
|
|
||||||
- 1.1.1.1
|
- 1.1.1.1
|
||||||
db_path: /tmp/integration_test_db.sqlite3
|
db_path: /tmp/integration_test_db.sqlite3
|
||||||
private_key_path: private.key
|
private_key_path: private.key
|
||||||
noise:
|
|
||||||
private_key_path: noise_private.key
|
|
||||||
listen_addr: 0.0.0.0:8080
|
listen_addr: 0.0.0.0:8080
|
||||||
metrics_listen_addr: 127.0.0.1:9090
|
metrics_listen_addr: 127.0.0.1:9090
|
||||||
server_url: http://headscale:8080
|
server_url: http://headscale:8080
|
||||||
|
|||||||
@@ -14,10 +14,8 @@ dns_config:
|
|||||||
- 1.1.1.1
|
- 1.1.1.1
|
||||||
db_path: /tmp/integration_test_db.sqlite3
|
db_path: /tmp/integration_test_db.sqlite3
|
||||||
private_key_path: private.key
|
private_key_path: private.key
|
||||||
noise:
|
listen_addr: 0.0.0.0:8443
|
||||||
private_key_path: noise_private.key
|
server_url: https://headscale:8443
|
||||||
listen_addr: 0.0.0.0:443
|
|
||||||
server_url: https://headscale:443
|
|
||||||
tls_cert_path: "/etc/headscale/tls/server.crt"
|
tls_cert_path: "/etc/headscale/tls/server.crt"
|
||||||
tls_key_path: "/etc/headscale/tls/server.key"
|
tls_key_path: "/etc/headscale/tls/server.key"
|
||||||
tls_client_auth_mode: disabled
|
tls_client_auth_mode: disabled
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
log_level: trace
|
|
||||||
acl_policy_path: ""
|
|
||||||
db_type: sqlite3
|
|
||||||
ephemeral_node_inactivity_timeout: 30m
|
|
||||||
node_update_check_interval: 10s
|
|
||||||
ip_prefixes:
|
|
||||||
- fd7a:115c:a1e0::/48
|
|
||||||
- 100.64.0.0/10
|
|
||||||
db_path: /tmp/integration_test_db.sqlite3
|
|
||||||
private_key_path: private.key
|
|
||||||
noise:
|
|
||||||
private_key_path: noise_private.key
|
|
||||||
listen_addr: 0.0.0.0:8443
|
|
||||||
server_url: https://headscale-oidc:8443
|
|
||||||
tls_cert_path: "/etc/headscale/tls/server.crt"
|
|
||||||
tls_key_path: "/etc/headscale/tls/server.key"
|
|
||||||
tls_client_auth_mode: disabled
|
|
||||||
derp:
|
|
||||||
urls:
|
|
||||||
- https://controlplane.tailscale.com/derpmap/default
|
|
||||||
auto_update_enabled: true
|
|
||||||
update_frequency: 1m
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user