mirror of
https://github.com/juanfont/headscale.git
synced 2026-04-18 14:59:54 +02:00
Compare commits
23 Commits
v0.22.0-al
...
port-embed
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
510b5af4ed | ||
|
|
b40cf732d6 | ||
|
|
55aea185c7 | ||
|
|
adecb7b0ea | ||
|
|
b8bf0a3b9f | ||
|
|
17b597ed77 | ||
|
|
a5afe4bd06 | ||
|
|
a71cc81fe7 | ||
|
|
679305c3e4 | ||
|
|
c0680f34f1 | ||
|
|
64ebe6b0c8 | ||
|
|
e6b26499f7 | ||
|
|
977eb1dee3 | ||
|
|
b2e2b02210 | ||
|
|
2abff4bb08 | ||
|
|
54c00645d1 | ||
|
|
cad5ce0ebd | ||
|
|
b12a167fa2 | ||
|
|
667295e15e | ||
|
|
bea52678e3 | ||
|
|
307cfc3304 | ||
|
|
5e74ca9414 | ||
|
|
9836b097a4 |
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -19,6 +19,6 @@ jobs:
|
|||||||
- uses: cachix/install-nix-action@v16
|
- uses: cachix/install-nix-action@v16
|
||||||
|
|
||||||
- name: Run goreleaser
|
- name: Run goreleaser
|
||||||
run: nix develop --command -- goreleaser release --rm-dist
|
run: nix develop --command -- goreleaser release --clean
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
57
.github/workflows/test-integration-v2-TestACLDevice1CanAccessDevice2.yaml
vendored
Normal file
57
.github/workflows/test-integration-v2-TestACLDevice1CanAccessDevice2.yaml
vendored
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
||||||
|
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
||||||
|
|
||||||
|
name: Integration Test v2 - TestACLDevice1CanAccessDevice2
|
||||||
|
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- name: Get changed files
|
||||||
|
id: changed-files
|
||||||
|
uses: tj-actions/changed-files@v34
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
*.nix
|
||||||
|
go.*
|
||||||
|
**/*.go
|
||||||
|
integration_test/
|
||||||
|
config-example.yaml
|
||||||
|
|
||||||
|
- uses: cachix/install-nix-action@v18
|
||||||
|
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
|
- name: Run general integration tests
|
||||||
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
run: |
|
||||||
|
nix develop --command -- docker run \
|
||||||
|
--tty --rm \
|
||||||
|
--volume ~/.cache/hs-integration-go:/go \
|
||||||
|
--name headscale-test-suite \
|
||||||
|
--volume $PWD:$PWD -w $PWD/integration \
|
||||||
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
|
golang:1 \
|
||||||
|
go test ./... \
|
||||||
|
-tags ts2019 \
|
||||||
|
-failfast \
|
||||||
|
-timeout 120m \
|
||||||
|
-parallel 1 \
|
||||||
|
-run "^TestACLDevice1CanAccessDevice2$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
||||||
57
.github/workflows/test-integration-v2-TestACLNamedHostsCanReach.yaml
vendored
Normal file
57
.github/workflows/test-integration-v2-TestACLNamedHostsCanReach.yaml
vendored
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
||||||
|
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
||||||
|
|
||||||
|
name: Integration Test v2 - TestACLNamedHostsCanReach
|
||||||
|
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- name: Get changed files
|
||||||
|
id: changed-files
|
||||||
|
uses: tj-actions/changed-files@v34
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
*.nix
|
||||||
|
go.*
|
||||||
|
**/*.go
|
||||||
|
integration_test/
|
||||||
|
config-example.yaml
|
||||||
|
|
||||||
|
- uses: cachix/install-nix-action@v18
|
||||||
|
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
|
- name: Run general integration tests
|
||||||
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
run: |
|
||||||
|
nix develop --command -- docker run \
|
||||||
|
--tty --rm \
|
||||||
|
--volume ~/.cache/hs-integration-go:/go \
|
||||||
|
--name headscale-test-suite \
|
||||||
|
--volume $PWD:$PWD -w $PWD/integration \
|
||||||
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
|
golang:1 \
|
||||||
|
go test ./... \
|
||||||
|
-tags ts2019 \
|
||||||
|
-failfast \
|
||||||
|
-timeout 120m \
|
||||||
|
-parallel 1 \
|
||||||
|
-run "^TestACLNamedHostsCanReach$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
||||||
57
.github/workflows/test-integration-v2-TestACLNamedHostsCanReachBySubnet.yaml
vendored
Normal file
57
.github/workflows/test-integration-v2-TestACLNamedHostsCanReachBySubnet.yaml
vendored
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
||||||
|
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
||||||
|
|
||||||
|
name: Integration Test v2 - TestACLNamedHostsCanReachBySubnet
|
||||||
|
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- name: Get changed files
|
||||||
|
id: changed-files
|
||||||
|
uses: tj-actions/changed-files@v34
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
*.nix
|
||||||
|
go.*
|
||||||
|
**/*.go
|
||||||
|
integration_test/
|
||||||
|
config-example.yaml
|
||||||
|
|
||||||
|
- uses: cachix/install-nix-action@v18
|
||||||
|
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
|
- name: Run general integration tests
|
||||||
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
run: |
|
||||||
|
nix develop --command -- docker run \
|
||||||
|
--tty --rm \
|
||||||
|
--volume ~/.cache/hs-integration-go:/go \
|
||||||
|
--name headscale-test-suite \
|
||||||
|
--volume $PWD:$PWD -w $PWD/integration \
|
||||||
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
|
golang:1 \
|
||||||
|
go test ./... \
|
||||||
|
-tags ts2019 \
|
||||||
|
-failfast \
|
||||||
|
-timeout 120m \
|
||||||
|
-parallel 1 \
|
||||||
|
-run "^TestACLNamedHostsCanReachBySubnet$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
||||||
57
.github/workflows/test-integration-v2-TestDERPServerScenario.yaml
vendored
Normal file
57
.github/workflows/test-integration-v2-TestDERPServerScenario.yaml
vendored
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
||||||
|
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
||||||
|
|
||||||
|
name: Integration Test v2 - TestDERPServerScenario
|
||||||
|
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- name: Get changed files
|
||||||
|
id: changed-files
|
||||||
|
uses: tj-actions/changed-files@v34
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
*.nix
|
||||||
|
go.*
|
||||||
|
**/*.go
|
||||||
|
integration_test/
|
||||||
|
config-example.yaml
|
||||||
|
|
||||||
|
- uses: cachix/install-nix-action@v18
|
||||||
|
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
|
- name: Run general integration tests
|
||||||
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
run: |
|
||||||
|
nix develop --command -- docker run \
|
||||||
|
--tty --rm \
|
||||||
|
--volume ~/.cache/hs-integration-go:/go \
|
||||||
|
--name headscale-test-suite \
|
||||||
|
--volume $PWD:$PWD -w $PWD/integration \
|
||||||
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
|
golang:1 \
|
||||||
|
go test ./... \
|
||||||
|
-tags ts2019 \
|
||||||
|
-failfast \
|
||||||
|
-timeout 120m \
|
||||||
|
-parallel 1 \
|
||||||
|
-run "^TestDERPServerScenario$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,3 +1,5 @@
|
|||||||
|
ignored/
|
||||||
|
|
||||||
# Binaries for programs and plugins
|
# Binaries for programs and plugins
|
||||||
*.exe
|
*.exe
|
||||||
*.exe~
|
*.exe~
|
||||||
@@ -12,7 +14,7 @@
|
|||||||
*.out
|
*.out
|
||||||
|
|
||||||
# Dependency directories (remove the comment below to include it)
|
# Dependency directories (remove the comment below to include it)
|
||||||
# vendor/
|
vendor/
|
||||||
|
|
||||||
dist/
|
dist/
|
||||||
/headscale
|
/headscale
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
before:
|
before:
|
||||||
hooks:
|
hooks:
|
||||||
- go mod tidy -compat=1.20
|
- go mod tidy -compat=1.20
|
||||||
|
- go mod vendor
|
||||||
|
|
||||||
release:
|
release:
|
||||||
prerelease: auto
|
prerelease: auto
|
||||||
@@ -31,19 +32,16 @@ builds:
|
|||||||
|
|
||||||
archives:
|
archives:
|
||||||
- id: golang-cross
|
- id: golang-cross
|
||||||
builds:
|
name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
||||||
- darwin-amd64
|
|
||||||
- darwin-arm64
|
|
||||||
- freebsd-amd64
|
|
||||||
- linux-386
|
|
||||||
- linux-amd64
|
|
||||||
- linux-arm64
|
|
||||||
- linux-arm-5
|
|
||||||
- linux-arm-6
|
|
||||||
- linux-arm-7
|
|
||||||
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
|
||||||
format: binary
|
format: binary
|
||||||
|
|
||||||
|
source:
|
||||||
|
enabled: true
|
||||||
|
name_template: "{{ .ProjectName }}_{{ .Version }}"
|
||||||
|
format: tar.gz
|
||||||
|
files:
|
||||||
|
- "vendor/"
|
||||||
|
|
||||||
nfpms:
|
nfpms:
|
||||||
# Configure nFPM for .deb and .rpm releases
|
# Configure nFPM for .deb and .rpm releases
|
||||||
#
|
#
|
||||||
@@ -65,7 +63,7 @@ nfpms:
|
|||||||
bindir: /usr/bin
|
bindir: /usr/bin
|
||||||
formats:
|
formats:
|
||||||
- deb
|
- deb
|
||||||
- rpm
|
# - rpm
|
||||||
contents:
|
contents:
|
||||||
- src: ./config-example.yaml
|
- src: ./config-example.yaml
|
||||||
dst: /etc/headscale/config.yaml
|
dst: /etc/headscale/config.yaml
|
||||||
|
|||||||
16
CHANGELOG.md
16
CHANGELOG.md
@@ -1,12 +1,24 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
## 0.22.0 (2023-XX-XX)
|
## 0.23.0 (2023-XX-XX)
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
- Add `.deb` and `.rpm` packages to release process [#1297](https://github.com/juanfont/headscale/pull/1297)
|
## 0.22.1 (2023-04-20)
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
- Fix issue where SystemD could not bind to port 80 [#1365](https://github.com/juanfont/headscale/pull/1365)
|
||||||
|
|
||||||
|
## 0.22.0 (2023-04-20)
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
- Add `.deb` packages to release process [#1297](https://github.com/juanfont/headscale/pull/1297)
|
||||||
|
- Update and simplify the documentation to use new `.deb` packages [#1349](https://github.com/juanfont/headscale/pull/1349)
|
||||||
- Add 32-bit Arm platforms to release process [#1297](https://github.com/juanfont/headscale/pull/1297)
|
- Add 32-bit Arm platforms to release process [#1297](https://github.com/juanfont/headscale/pull/1297)
|
||||||
- Fix longstanding bug that would prevent "\*" from working properly in ACLs (issue [#699](https://github.com/juanfont/headscale/issues/699)) [#1279](https://github.com/juanfont/headscale/pull/1279)
|
- Fix longstanding bug that would prevent "\*" from working properly in ACLs (issue [#699](https://github.com/juanfont/headscale/issues/699)) [#1279](https://github.com/juanfont/headscale/pull/1279)
|
||||||
|
- Fix issue where IPv6 could not be used in, or while using ACLs (part of [#809](https://github.com/juanfont/headscale/issues/809)) [#1339](https://github.com/juanfont/headscale/pull/1339)
|
||||||
- Target Go 1.20 and Tailscale 1.38 for Headscale [#1323](https://github.com/juanfont/headscale/pull/1323)
|
- Target Go 1.20 and Tailscale 1.38 for Headscale [#1323](https://github.com/juanfont/headscale/pull/1323)
|
||||||
|
|
||||||
## 0.21.0 (2023-03-20)
|
## 0.21.0 (2023-03-20)
|
||||||
|
|||||||
21
Makefile
21
Makefile
@@ -36,7 +36,7 @@ test_integration_cli:
|
|||||||
-v ~/.cache/hs-integration-go:/go \
|
-v ~/.cache/hs-integration-go:/go \
|
||||||
-v $$PWD:$$PWD -w $$PWD \
|
-v $$PWD:$$PWD -w $$PWD \
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock golang:1 \
|
-v /var/run/docker.sock:/var/run/docker.sock golang:1 \
|
||||||
go test $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationCLI ./...
|
go run gotest.tools/gotestsum@latest -- $(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
|
docker network rm $$(docker network ls --filter name=headscale --quiet) || true
|
||||||
@@ -46,7 +46,7 @@ test_integration_derp:
|
|||||||
-v ~/.cache/hs-integration-go:/go \
|
-v ~/.cache/hs-integration-go:/go \
|
||||||
-v $$PWD:$$PWD -w $$PWD \
|
-v $$PWD:$$PWD -w $$PWD \
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock golang:1 \
|
-v /var/run/docker.sock:/var/run/docker.sock golang:1 \
|
||||||
go test $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationDERP ./...
|
go run gotest.tools/gotestsum@latest -- $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationDERP ./...
|
||||||
|
|
||||||
test_integration_v2_general:
|
test_integration_v2_general:
|
||||||
docker run \
|
docker run \
|
||||||
@@ -56,13 +56,7 @@ test_integration_v2_general:
|
|||||||
-v $$PWD:$$PWD -w $$PWD/integration \
|
-v $$PWD:$$PWD -w $$PWD/integration \
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test $(TAGS) -failfast ./... -timeout 120m -parallel 8
|
go run gotest.tools/gotestsum@latest -- $(TAGS) -failfast ./... -timeout 120m -parallel 8
|
||||||
|
|
||||||
coverprofile_func:
|
|
||||||
go tool cover -func=coverage.out
|
|
||||||
|
|
||||||
coverprofile_html:
|
|
||||||
go tool cover -html=coverage.out
|
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
golangci-lint run --fix --timeout 10m
|
golangci-lint run --fix --timeout 10m
|
||||||
@@ -80,11 +74,4 @@ compress: build
|
|||||||
|
|
||||||
generate:
|
generate:
|
||||||
rm -rf gen
|
rm -rf gen
|
||||||
go run github.com/bufbuild/buf/cmd/buf generate proto
|
buf generate proto
|
||||||
|
|
||||||
install-protobuf-plugins:
|
|
||||||
go install \
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway \
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2 \
|
|
||||||
google.golang.org/protobuf/cmd/protoc-gen-go \
|
|
||||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc
|
|
||||||
|
|||||||
72
acls.go
72
acls.go
@@ -13,6 +13,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/samber/lo"
|
||||||
"github.com/tailscale/hujson"
|
"github.com/tailscale/hujson"
|
||||||
"go4.org/netipx"
|
"go4.org/netipx"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
@@ -407,15 +408,40 @@ func generateACLPolicyDest(
|
|||||||
needsWildcard bool,
|
needsWildcard bool,
|
||||||
stripEmaildomain bool,
|
stripEmaildomain bool,
|
||||||
) ([]tailcfg.NetPortRange, error) {
|
) ([]tailcfg.NetPortRange, error) {
|
||||||
tokens := strings.Split(dest, ":")
|
var tokens []string
|
||||||
|
|
||||||
|
log.Trace().Str("destination", dest).Msg("generating policy destination")
|
||||||
|
|
||||||
|
// Check if there is a IPv4/6:Port combination, IPv6 has more than
|
||||||
|
// three ":".
|
||||||
|
tokens = strings.Split(dest, ":")
|
||||||
if len(tokens) < expectedTokenItems || len(tokens) > 3 {
|
if len(tokens) < expectedTokenItems || len(tokens) > 3 {
|
||||||
return nil, errInvalidPortFormat
|
port := tokens[len(tokens)-1]
|
||||||
|
|
||||||
|
maybeIPv6Str := strings.TrimSuffix(dest, ":"+port)
|
||||||
|
log.Trace().Str("maybeIPv6Str", maybeIPv6Str).Msg("")
|
||||||
|
|
||||||
|
if maybeIPv6, err := netip.ParseAddr(maybeIPv6Str); err != nil && !maybeIPv6.Is6() {
|
||||||
|
log.Trace().Err(err).Msg("trying to parse as IPv6")
|
||||||
|
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"failed to parse destination, tokens %v: %w",
|
||||||
|
tokens,
|
||||||
|
errInvalidPortFormat,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
tokens = []string{maybeIPv6Str, port}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Trace().Strs("tokens", tokens).Msg("generating policy destination")
|
||||||
|
|
||||||
var alias string
|
var alias string
|
||||||
// We can have here stuff like:
|
// We can have here stuff like:
|
||||||
// git-server:*
|
// git-server:*
|
||||||
// 192.168.1.0/24:22
|
// 192.168.1.0/24:22
|
||||||
|
// fd7a:115c:a1e0::2:22
|
||||||
|
// fd7a:115c:a1e0::2/128:22
|
||||||
// tag:montreal-webserver:80,443
|
// tag:montreal-webserver:80,443
|
||||||
// tag:api-server:443
|
// tag:api-server:443
|
||||||
// example-host-1:*
|
// example-host-1:*
|
||||||
@@ -508,9 +534,11 @@ func parseProtocol(protocol string) ([]int, bool, error) {
|
|||||||
// - a group
|
// - a group
|
||||||
// - a tag
|
// - a tag
|
||||||
// - a host
|
// - a host
|
||||||
|
// - an ip
|
||||||
|
// - a cidr
|
||||||
// and transform these in IPAddresses.
|
// and transform these in IPAddresses.
|
||||||
func expandAlias(
|
func expandAlias(
|
||||||
machines []Machine,
|
machines Machines,
|
||||||
aclPolicy ACLPolicy,
|
aclPolicy ACLPolicy,
|
||||||
alias string,
|
alias string,
|
||||||
stripEmailDomain bool,
|
stripEmailDomain bool,
|
||||||
@@ -592,19 +620,40 @@ func expandAlias(
|
|||||||
|
|
||||||
// if alias is an host
|
// if alias is an host
|
||||||
if h, ok := aclPolicy.Hosts[alias]; ok {
|
if h, ok := aclPolicy.Hosts[alias]; ok {
|
||||||
return []string{h.String()}, nil
|
log.Trace().Str("host", h.String()).Msg("expandAlias got hosts entry")
|
||||||
|
|
||||||
|
return expandAlias(machines, aclPolicy, h.String(), stripEmailDomain)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if alias is an IP
|
// if alias is an IP
|
||||||
ip, err := netip.ParseAddr(alias)
|
if ip, err := netip.ParseAddr(alias); err == nil {
|
||||||
if err == nil {
|
log.Trace().Str("ip", ip.String()).Msg("expandAlias got ip")
|
||||||
return []string{ip.String()}, nil
|
ips := []string{ip.String()}
|
||||||
|
matches := machines.FilterByIP(ip)
|
||||||
|
|
||||||
|
for _, machine := range matches {
|
||||||
|
ips = append(ips, machine.IPAddresses.ToStringSlice()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lo.Uniq(ips), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// if alias is an CIDR
|
if cidr, err := netip.ParsePrefix(alias); err == nil {
|
||||||
cidr, err := netip.ParsePrefix(alias)
|
log.Trace().Str("cidr", cidr.String()).Msg("expandAlias got cidr")
|
||||||
if err == nil {
|
val := []string{cidr.String()}
|
||||||
return []string{cidr.String()}, nil
|
// This is suboptimal and quite expensive, but if we only add the cidr, we will miss all the relevant IPv6
|
||||||
|
// addresses for the hosts that belong to tailscale. This doesnt really affect stuff like subnet routers.
|
||||||
|
for _, machine := range machines {
|
||||||
|
for _, ip := range machine.IPAddresses {
|
||||||
|
// log.Trace().
|
||||||
|
// Msgf("checking if machine ip (%s) is part of cidr (%s): %v, is single ip cidr (%v), addr: %s", ip.String(), cidr.String(), cidr.Contains(ip), cidr.IsSingleIP(), cidr.Addr().String())
|
||||||
|
if cidr.Contains(ip) {
|
||||||
|
val = append(val, machine.IPAddresses.ToStringSlice()...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lo.Uniq(val), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Warn().Msgf("No IPs found with the alias %v", alias)
|
log.Warn().Msgf("No IPs found with the alias %v", alias)
|
||||||
@@ -666,6 +715,7 @@ func expandPorts(portsStr string, needsWildcard bool) (*[]tailcfg.PortRange, err
|
|||||||
|
|
||||||
ports := []tailcfg.PortRange{}
|
ports := []tailcfg.PortRange{}
|
||||||
for _, portStr := range strings.Split(portsStr, ",") {
|
for _, portStr := range strings.Split(portsStr, ",") {
|
||||||
|
log.Trace().Msgf("parsing portstring: %s", portStr)
|
||||||
rang := strings.Split(portStr, "-")
|
rang := strings.Split(portStr, "-")
|
||||||
switch len(rang) {
|
switch len(rang) {
|
||||||
case 1:
|
case 1:
|
||||||
|
|||||||
88
acls_test.go
88
acls_test.go
@@ -1026,22 +1026,7 @@ func Test_expandAlias(t *testing.T) {
|
|||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "private network",
|
name: "simple host by ip passed through",
|
||||||
args: args{
|
|
||||||
alias: "homeNetwork",
|
|
||||||
machines: []Machine{},
|
|
||||||
aclPolicy: ACLPolicy{
|
|
||||||
Hosts: Hosts{
|
|
||||||
"homeNetwork": netip.MustParsePrefix("192.168.1.0/24"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
stripEmailDomain: true,
|
|
||||||
},
|
|
||||||
want: []string{"192.168.1.0/24"},
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simple host by ip",
|
|
||||||
args: args{
|
args: args{
|
||||||
alias: "10.0.0.1",
|
alias: "10.0.0.1",
|
||||||
machines: []Machine{},
|
machines: []Machine{},
|
||||||
@@ -1051,6 +1036,62 @@ func Test_expandAlias(t *testing.T) {
|
|||||||
want: []string{"10.0.0.1"},
|
want: []string{"10.0.0.1"},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "simple host by ipv4 single ipv4",
|
||||||
|
args: args{
|
||||||
|
alias: "10.0.0.1",
|
||||||
|
machines: []Machine{
|
||||||
|
{
|
||||||
|
IPAddresses: MachineAddresses{
|
||||||
|
netip.MustParseAddr("10.0.0.1"),
|
||||||
|
},
|
||||||
|
User: User{Name: "mickael"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
aclPolicy: ACLPolicy{},
|
||||||
|
stripEmailDomain: true,
|
||||||
|
},
|
||||||
|
want: []string{"10.0.0.1"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "simple host by ipv4 single dual stack",
|
||||||
|
args: args{
|
||||||
|
alias: "10.0.0.1",
|
||||||
|
machines: []Machine{
|
||||||
|
{
|
||||||
|
IPAddresses: MachineAddresses{
|
||||||
|
netip.MustParseAddr("10.0.0.1"),
|
||||||
|
netip.MustParseAddr("fd7a:115c:a1e0:ab12:4843:2222:6273:2222"),
|
||||||
|
},
|
||||||
|
User: User{Name: "mickael"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
aclPolicy: ACLPolicy{},
|
||||||
|
stripEmailDomain: true,
|
||||||
|
},
|
||||||
|
want: []string{"10.0.0.1", "fd7a:115c:a1e0:ab12:4843:2222:6273:2222"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "simple host by ipv6 single dual stack",
|
||||||
|
args: args{
|
||||||
|
alias: "fd7a:115c:a1e0:ab12:4843:2222:6273:2222",
|
||||||
|
machines: []Machine{
|
||||||
|
{
|
||||||
|
IPAddresses: MachineAddresses{
|
||||||
|
netip.MustParseAddr("10.0.0.1"),
|
||||||
|
netip.MustParseAddr("fd7a:115c:a1e0:ab12:4843:2222:6273:2222"),
|
||||||
|
},
|
||||||
|
User: User{Name: "mickael"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
aclPolicy: ACLPolicy{},
|
||||||
|
stripEmailDomain: true,
|
||||||
|
},
|
||||||
|
want: []string{"fd7a:115c:a1e0:ab12:4843:2222:6273:2222", "10.0.0.1"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "simple host by hostname alias",
|
name: "simple host by hostname alias",
|
||||||
args: args{
|
args: args{
|
||||||
@@ -1066,6 +1107,21 @@ func Test_expandAlias(t *testing.T) {
|
|||||||
want: []string{"10.0.0.132/32"},
|
want: []string{"10.0.0.132/32"},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "private network",
|
||||||
|
args: args{
|
||||||
|
alias: "homeNetwork",
|
||||||
|
machines: []Machine{},
|
||||||
|
aclPolicy: ACLPolicy{
|
||||||
|
Hosts: Hosts{
|
||||||
|
"homeNetwork": netip.MustParsePrefix("192.168.1.0/24"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stripEmailDomain: true,
|
||||||
|
},
|
||||||
|
want: []string{"192.168.1.0/24"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "simple CIDR",
|
name: "simple CIDR",
|
||||||
args: args{
|
args: args{
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ WorkingDirectory=/var/lib/headscale
|
|||||||
ReadWritePaths=/var/lib/headscale /var/run
|
ReadWritePaths=/var/lib/headscale /var/run
|
||||||
|
|
||||||
AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_CHOWN
|
AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_CHOWN
|
||||||
CapabilityBoundingSet=CAP_CHOWN
|
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_CHOWN
|
||||||
LockPersonality=true
|
LockPersonality=true
|
||||||
NoNewPrivileges=true
|
NoNewPrivileges=true
|
||||||
PrivateDevices=true
|
PrivateDevices=true
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ summary() {
|
|||||||
echo ""
|
echo ""
|
||||||
echo " Please follow the next steps to start the software:"
|
echo " Please follow the next steps to start the software:"
|
||||||
echo ""
|
echo ""
|
||||||
|
echo " sudo systemctl enable headscale"
|
||||||
echo " sudo systemctl start headscale"
|
echo " sudo systemctl start headscale"
|
||||||
echo ""
|
echo ""
|
||||||
echo " Configuration settings can be adjusted here:"
|
echo " Configuration settings can be adjusted here:"
|
||||||
|
|||||||
198
docs/running-headscale-linux-manual.md
Normal file
198
docs/running-headscale-linux-manual.md
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
# Running headscale on Linux
|
||||||
|
|
||||||
|
## Note: Outdated and "advanced"
|
||||||
|
|
||||||
|
This documentation is considered the "legacy"/advanced/manual version of the documentation, you most likely do not
|
||||||
|
want to use this documentation and rather look at the distro specific documentation (TODO LINK)[].
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
This documentation has the goal of showing a user how-to set up and run `headscale` on Linux.
|
||||||
|
In additional to the "get up and running section", there is an optional [SystemD section](#running-headscale-in-the-background-with-systemd)
|
||||||
|
describing how to make `headscale` run properly in a server environment.
|
||||||
|
|
||||||
|
## Configure and run `headscale`
|
||||||
|
|
||||||
|
1. Download the latest [`headscale` binary from GitHub's release page](https://github.com/juanfont/headscale/releases):
|
||||||
|
|
||||||
|
```shell
|
||||||
|
wget --output-document=/usr/local/bin/headscale \
|
||||||
|
https://github.com/juanfont/headscale/releases/download/v<HEADSCALE VERSION>/headscale_<HEADSCALE VERSION>_linux_<ARCH>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Make `headscale` executable:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
chmod +x /usr/local/bin/headscale
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Prepare a directory to hold `headscale` configuration and the [SQLite](https://www.sqlite.org/) database:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Directory for configuration
|
||||||
|
|
||||||
|
mkdir -p /etc/headscale
|
||||||
|
|
||||||
|
# Directory for Database, and other variable data (like certificates)
|
||||||
|
mkdir -p /var/lib/headscale
|
||||||
|
# or if you create a headscale user:
|
||||||
|
useradd \
|
||||||
|
--create-home \
|
||||||
|
--home-dir /var/lib/headscale/ \
|
||||||
|
--system \
|
||||||
|
--user-group \
|
||||||
|
--shell /usr/bin/nologin \
|
||||||
|
headscale
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Create an empty SQLite database:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
touch /var/lib/headscale/db.sqlite
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Create a `headscale` configuration:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
touch /etc/headscale/config.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
**(Strongly Recommended)** Download a copy of the [example configuration][config-example.yaml](https://github.com/juanfont/headscale/blob/main/config-example.yaml) from the headscale repository.
|
||||||
|
|
||||||
|
6. Start the headscale server:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
headscale serve
|
||||||
|
```
|
||||||
|
|
||||||
|
This command will start `headscale` in the current terminal session.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
To continue the tutorial, open a new terminal and let it run in the background.
|
||||||
|
Alternatively use terminal emulators like [tmux](https://github.com/tmux/tmux) or [screen](https://www.gnu.org/software/screen/).
|
||||||
|
|
||||||
|
To run `headscale` in the background, please follow the steps in the [SystemD section](#running-headscale-in-the-background-with-systemd) before continuing.
|
||||||
|
|
||||||
|
7. Verify `headscale` is running:
|
||||||
|
|
||||||
|
Verify `headscale` is available:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl http://127.0.0.1:9090/metrics
|
||||||
|
```
|
||||||
|
|
||||||
|
8. Create a user ([tailnet](https://tailscale.com/kb/1136/tailnet/)):
|
||||||
|
|
||||||
|
```shell
|
||||||
|
headscale users create myfirstuser
|
||||||
|
```
|
||||||
|
|
||||||
|
### Register a machine (normal login)
|
||||||
|
|
||||||
|
On a client machine, execute the `tailscale` login command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
tailscale up --login-server YOUR_HEADSCALE_URL
|
||||||
|
```
|
||||||
|
|
||||||
|
Register the machine:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
headscale --user myfirstuser nodes register --key <YOUR_MACHINE_KEY>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Register machine using a pre authenticated key
|
||||||
|
|
||||||
|
Generate a key using the command line:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
headscale --user myfirstuser preauthkeys create --reusable --expiration 24h
|
||||||
|
```
|
||||||
|
|
||||||
|
This will return a pre-authenticated key that can be used to connect a node to `headscale` during the `tailscale` command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
tailscale up --login-server <YOUR_HEADSCALE_URL> --authkey <YOUR_AUTH_KEY>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running `headscale` in the background with SystemD
|
||||||
|
|
||||||
|
:warning: **Deprecated**: This part is very outdated and you should use the [pre-packaged Headscale for this](./running-headscale-linux.md
|
||||||
|
|
||||||
|
This section demonstrates how to run `headscale` as a service in the background with [SystemD](https://www.freedesktop.org/wiki/Software/systemd/).
|
||||||
|
This should work on most modern Linux distributions.
|
||||||
|
|
||||||
|
1. Create a SystemD service configuration at `/etc/systemd/system/headscale.service` containing:
|
||||||
|
|
||||||
|
```systemd
|
||||||
|
[Unit]
|
||||||
|
Description=headscale controller
|
||||||
|
After=syslog.target
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=headscale
|
||||||
|
Group=headscale
|
||||||
|
ExecStart=/usr/local/bin/headscale serve
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
# Optional security enhancements
|
||||||
|
NoNewPrivileges=yes
|
||||||
|
PrivateTmp=yes
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=yes
|
||||||
|
WorkingDirectory=/var/lib/headscale
|
||||||
|
ReadWritePaths=/var/lib/headscale /var/run/headscale
|
||||||
|
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||||
|
RuntimeDirectory=headscale
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that when running as the headscale user ensure that, either you add your current user to the headscale group:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
usermod -a -G headscale current_user
|
||||||
|
```
|
||||||
|
|
||||||
|
or run all headscale commands as the headscale user:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
su - headscale
|
||||||
|
```
|
||||||
|
|
||||||
|
2. In `/etc/headscale/config.yaml`, override the default `headscale` unix socket with path that is writable by the `headscale` user or group:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
unix_socket: /var/run/headscale/headscale.sock
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Reload SystemD to load the new configuration file:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
systemctl daemon-reload
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Enable and start the new `headscale` service:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
systemctl enable --now headscale
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Verify the headscale service:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
systemctl status headscale
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify `headscale` is available:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl http://127.0.0.1:9090/metrics
|
||||||
|
```
|
||||||
|
|
||||||
|
`headscale` will now run in the background and start at boot.
|
||||||
@@ -1,83 +1,65 @@
|
|||||||
# Running headscale on Linux
|
# Running headscale on Linux
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Ubuntu 20.04 or newer, Debian 11 or newer.
|
||||||
|
|
||||||
## Goal
|
## Goal
|
||||||
|
|
||||||
This documentation has the goal of showing a user how-to set up and run `headscale` on Linux.
|
Get Headscale up and running.
|
||||||
In additional to the "get up and running section", there is an optional [SystemD section](#running-headscale-in-the-background-with-systemd)
|
|
||||||
describing how to make `headscale` run properly in a server environment.
|
|
||||||
|
|
||||||
## Configure and run `headscale`
|
This includes running Headscale with SystemD.
|
||||||
|
|
||||||
1. Download the latest [`headscale` binary from GitHub's release page](https://github.com/juanfont/headscale/releases):
|
## Migrating from manual install
|
||||||
|
|
||||||
|
If you are migrating from the old manual install, the best thing would be to remove
|
||||||
|
the files installed by following [the guide in reverse](./running-headscale-linux-manual.md).
|
||||||
|
|
||||||
|
You should _not_ delete the database (`/var/headscale/db.sqlite`) and the
|
||||||
|
configuration (`/etc/headscale/config.yaml`).
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Download the lastest Headscale package for your platform (`.deb` for Ubuntu and Debian) from [Headscale's releases page]():
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
wget --output-document=/usr/local/bin/headscale \
|
wget --output-document=headscale.deb \
|
||||||
https://github.com/juanfont/headscale/releases/download/v<HEADSCALE VERSION>/headscale_<HEADSCALE VERSION>_linux_<ARCH>
|
https://github.com/juanfont/headscale/releases/download/v<HEADSCALE VERSION>/headscale_<HEADSCALE VERSION>_linux_<ARCH>.deb
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Make `headscale` executable:
|
2. Install Headscale:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
chmod +x /usr/local/bin/headscale
|
sudo dpkg --install headscale.deb
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Prepare a directory to hold `headscale` configuration and the [SQLite](https://www.sqlite.org/) database:
|
3. Enable Headscale service, this will start Headscale at boot:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# Directory for configuration
|
sudo systemctl enable headscale
|
||||||
|
|
||||||
mkdir -p /etc/headscale
|
|
||||||
|
|
||||||
# Directory for Database, and other variable data (like certificates)
|
|
||||||
mkdir -p /var/lib/headscale
|
|
||||||
# or if you create a headscale user:
|
|
||||||
useradd \
|
|
||||||
--create-home \
|
|
||||||
--home-dir /var/lib/headscale/ \
|
|
||||||
--system \
|
|
||||||
--user-group \
|
|
||||||
--shell /usr/bin/nologin \
|
|
||||||
headscale
|
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Create an empty SQLite database:
|
4. Configure Headscale by editing the configuration file:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
touch /var/lib/headscale/db.sqlite
|
nano /etc/headscale/config.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Create a `headscale` configuration:
|
5. Start Headscale:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
touch /etc/headscale/config.yaml
|
sudo systemctl start headscale
|
||||||
```
|
```
|
||||||
|
|
||||||
**(Strongly Recommended)** Download a copy of the [example configuration][config-example.yaml](https://github.com/juanfont/headscale/blob/main/config-example.yaml) from the headscale repository.
|
6. Check that Headscale is running as intended:
|
||||||
|
|
||||||
6. Start the headscale server:
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
headscale serve
|
systemctl status headscale
|
||||||
```
|
```
|
||||||
|
|
||||||
This command will start `headscale` in the current terminal session.
|
## Using Headscale
|
||||||
|
|
||||||
---
|
### Create a user
|
||||||
|
|
||||||
To continue the tutorial, open a new terminal and let it run in the background.
|
|
||||||
Alternatively use terminal emulators like [tmux](https://github.com/tmux/tmux) or [screen](https://www.gnu.org/software/screen/).
|
|
||||||
|
|
||||||
To run `headscale` in the background, please follow the steps in the [SystemD section](#running-headscale-in-the-background-with-systemd) before continuing.
|
|
||||||
|
|
||||||
7. Verify `headscale` is running:
|
|
||||||
|
|
||||||
Verify `headscale` is available:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
curl http://127.0.0.1:9090/metrics
|
|
||||||
```
|
|
||||||
|
|
||||||
8. Create a user ([tailnet](https://tailscale.com/kb/1136/tailnet/)):
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
headscale users create myfirstuser
|
headscale users create myfirstuser
|
||||||
@@ -85,16 +67,16 @@ headscale users create myfirstuser
|
|||||||
|
|
||||||
### Register a machine (normal login)
|
### Register a machine (normal login)
|
||||||
|
|
||||||
On a client machine, execute the `tailscale` login command:
|
On a client machine, run the `tailscale` login command:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
tailscale up --login-server YOUR_HEADSCALE_URL
|
tailscale up --login-server <YOUR_HEADSCALE_URL>
|
||||||
```
|
```
|
||||||
|
|
||||||
Register the machine:
|
Register the machine:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
headscale --user myfirstuser nodes register --key <YOU_+MACHINE_KEY>
|
headscale --user myfirstuser nodes register --key <YOUR_MACHINE_KEY>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Register machine using a pre authenticated key
|
### Register machine using a pre authenticated key
|
||||||
@@ -105,87 +87,9 @@ Generate a key using the command line:
|
|||||||
headscale --user myfirstuser preauthkeys create --reusable --expiration 24h
|
headscale --user myfirstuser preauthkeys create --reusable --expiration 24h
|
||||||
```
|
```
|
||||||
|
|
||||||
This will return a pre-authenticated key that can be used to connect a node to `headscale` during the `tailscale` command:
|
This will return a pre-authenticated key that is used to
|
||||||
|
connect a node to `headscale` during the `tailscale` command:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
tailscale up --login-server <YOUR_HEADSCALE_URL> --authkey <YOUR_AUTH_KEY>
|
tailscale up --login-server <YOUR_HEADSCALE_URL> --authkey <YOUR_AUTH_KEY>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Running `headscale` in the background with SystemD
|
|
||||||
|
|
||||||
This section demonstrates how to run `headscale` as a service in the background with [SystemD](https://www.freedesktop.org/wiki/Software/systemd/).
|
|
||||||
This should work on most modern Linux distributions.
|
|
||||||
|
|
||||||
1. Create a SystemD service configuration at `/etc/systemd/system/headscale.service` containing:
|
|
||||||
|
|
||||||
```systemd
|
|
||||||
[Unit]
|
|
||||||
Description=headscale controller
|
|
||||||
After=syslog.target
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=headscale
|
|
||||||
Group=headscale
|
|
||||||
ExecStart=/usr/local/bin/headscale serve
|
|
||||||
Restart=always
|
|
||||||
RestartSec=5
|
|
||||||
|
|
||||||
# Optional security enhancements
|
|
||||||
NoNewPrivileges=yes
|
|
||||||
PrivateTmp=yes
|
|
||||||
ProtectSystem=strict
|
|
||||||
ProtectHome=yes
|
|
||||||
WorkingDirectory=/var/lib/headscale
|
|
||||||
ReadWritePaths=/var/lib/headscale /var/run/headscale
|
|
||||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
|
||||||
RuntimeDirectory=headscale
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that when running as the headscale user ensure that, either you add your current user to the headscale group:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
usermod -a -G headscale current_user
|
|
||||||
```
|
|
||||||
|
|
||||||
or run all headscale commands as the headscale user:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
su - headscale
|
|
||||||
```
|
|
||||||
|
|
||||||
2. In `/etc/headscale/config.yaml`, override the default `headscale` unix socket with path that is writable by the `headscale` user or group:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
unix_socket: /var/run/headscale/headscale.sock
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Reload SystemD to load the new configuration file:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
systemctl daemon-reload
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Enable and start the new `headscale` service:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
systemctl enable --now headscale
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Verify the headscale service:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
systemctl status headscale
|
|
||||||
```
|
|
||||||
|
|
||||||
Verify `headscale` is available:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
curl http://127.0.0.1:9090/metrics
|
|
||||||
```
|
|
||||||
|
|
||||||
`headscale` will now run in the background and start at boot.
|
|
||||||
|
|||||||
30
flake.lock
generated
30
flake.lock
generated
@@ -1,12 +1,15 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"flake-utils": {
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1680776469,
|
"lastModified": 1681202837,
|
||||||
"narHash": "sha256-3CXUDK/3q/kieWtdsYpDOBJw3Gw4Af6x+2EiSnIkNQw=",
|
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "flake-utils",
|
"repo": "flake-utils",
|
||||||
"rev": "411e8764155aa9354dbcd6d5faaeb97e9e3dce24",
|
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -17,11 +20,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1680789907,
|
"lastModified": 1681753173,
|
||||||
"narHash": "sha256-0AOMkabjbOauxspnqfzqgLKhB2gSh3sLkz1p/jIckcs=",
|
"narHash": "sha256-MrGmzZWLUqh2VstoikKLFFIELXm/lsf/G9U9zR96VD4=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "9de84cd029054adc54fdc6442e121fbc5ac33baf",
|
"rev": "0a4206a51b386e5cda731e8ac78d76ad924c7125",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -36,6 +39,21 @@
|
|||||||
"flake-utils": "flake-utils",
|
"flake-utils": "flake-utils",
|
||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "root",
|
"root": "root",
|
||||||
|
|||||||
@@ -129,6 +129,14 @@
|
|||||||
|
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
export GOFLAGS=-tags="ts2019"
|
export GOFLAGS=-tags="ts2019"
|
||||||
|
export PATH="$PWD/result/bin:$PATH"
|
||||||
|
|
||||||
|
mkdir -p ./ignored
|
||||||
|
export HEADSCALE_PRIVATE_KEY_PATH="./ignored/private.key"
|
||||||
|
export HEADSCALE_NOISE_PRIVATE_KEY_PATH="./ignored/noise_private.key"
|
||||||
|
export HEADSCALE_DB_PATH="./ignored/db.sqlite"
|
||||||
|
export HEADSCALE_TLS_LETSENCRYPT_CACHE_DIR="./ignored/cache"
|
||||||
|
export HEADSCALE_UNIX_SOCKET="./ignored/headscale.sock"
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,16 +12,14 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
const numberOfTestClients = 2
|
func aclScenario(t *testing.T, policy *headscale.ACLPolicy, clientsPerUser int) *Scenario {
|
||||||
|
|
||||||
func aclScenario(t *testing.T, policy headscale.ACLPolicy) *Scenario {
|
|
||||||
t.Helper()
|
t.Helper()
|
||||||
scenario, err := NewScenario()
|
scenario, err := NewScenario()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
"user1": numberOfTestClients,
|
"user1": clientsPerUser,
|
||||||
"user2": numberOfTestClients,
|
"user2": clientsPerUser,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec,
|
err = scenario.CreateHeadscaleEnv(spec,
|
||||||
@@ -29,18 +27,15 @@ func aclScenario(t *testing.T, policy headscale.ACLPolicy) *Scenario {
|
|||||||
tsic.WithDockerEntrypoint([]string{
|
tsic.WithDockerEntrypoint([]string{
|
||||||
"/bin/bash",
|
"/bin/bash",
|
||||||
"-c",
|
"-c",
|
||||||
"/bin/sleep 3 ; update-ca-certificates ; python3 -m http.server 80 & tailscaled --tun=tsdev",
|
"/bin/sleep 3 ; update-ca-certificates ; python3 -m http.server --bind :: 80 & tailscaled --tun=tsdev",
|
||||||
}),
|
}),
|
||||||
tsic.WithDockerWorkdir("/"),
|
tsic.WithDockerWorkdir("/"),
|
||||||
},
|
},
|
||||||
hsic.WithACLPolicy(&policy),
|
hsic.WithACLPolicy(policy),
|
||||||
hsic.WithTestName("acl"),
|
hsic.WithTestName("acl"),
|
||||||
)
|
)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// allClients, err := scenario.ListTailscaleClients()
|
|
||||||
// assert.NoError(t, err)
|
|
||||||
|
|
||||||
err = scenario.WaitForTailscaleSync()
|
err = scenario.WaitForTailscaleSync()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
@@ -230,7 +225,7 @@ func TestACLAllowUser80Dst(t *testing.T) {
|
|||||||
IntegrationSkip(t)
|
IntegrationSkip(t)
|
||||||
|
|
||||||
scenario := aclScenario(t,
|
scenario := aclScenario(t,
|
||||||
headscale.ACLPolicy{
|
&headscale.ACLPolicy{
|
||||||
ACLs: []headscale.ACL{
|
ACLs: []headscale.ACL{
|
||||||
{
|
{
|
||||||
Action: "accept",
|
Action: "accept",
|
||||||
@@ -239,6 +234,7 @@ func TestACLAllowUser80Dst(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
1,
|
||||||
)
|
)
|
||||||
|
|
||||||
user1Clients, err := scenario.ListTailscaleClients("user1")
|
user1Clients, err := scenario.ListTailscaleClients("user1")
|
||||||
@@ -285,7 +281,7 @@ func TestACLDenyAllPort80(t *testing.T) {
|
|||||||
IntegrationSkip(t)
|
IntegrationSkip(t)
|
||||||
|
|
||||||
scenario := aclScenario(t,
|
scenario := aclScenario(t,
|
||||||
headscale.ACLPolicy{
|
&headscale.ACLPolicy{
|
||||||
Groups: map[string][]string{
|
Groups: map[string][]string{
|
||||||
"group:integration-acl-test": {"user1", "user2"},
|
"group:integration-acl-test": {"user1", "user2"},
|
||||||
},
|
},
|
||||||
@@ -297,6 +293,7 @@ func TestACLDenyAllPort80(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
4,
|
||||||
)
|
)
|
||||||
|
|
||||||
allClients, err := scenario.ListTailscaleClients()
|
allClients, err := scenario.ListTailscaleClients()
|
||||||
@@ -333,7 +330,7 @@ func TestACLAllowUserDst(t *testing.T) {
|
|||||||
IntegrationSkip(t)
|
IntegrationSkip(t)
|
||||||
|
|
||||||
scenario := aclScenario(t,
|
scenario := aclScenario(t,
|
||||||
headscale.ACLPolicy{
|
&headscale.ACLPolicy{
|
||||||
ACLs: []headscale.ACL{
|
ACLs: []headscale.ACL{
|
||||||
{
|
{
|
||||||
Action: "accept",
|
Action: "accept",
|
||||||
@@ -342,6 +339,7 @@ func TestACLAllowUserDst(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
2,
|
||||||
)
|
)
|
||||||
|
|
||||||
user1Clients, err := scenario.ListTailscaleClients("user1")
|
user1Clients, err := scenario.ListTailscaleClients("user1")
|
||||||
@@ -390,7 +388,7 @@ func TestACLAllowStarDst(t *testing.T) {
|
|||||||
IntegrationSkip(t)
|
IntegrationSkip(t)
|
||||||
|
|
||||||
scenario := aclScenario(t,
|
scenario := aclScenario(t,
|
||||||
headscale.ACLPolicy{
|
&headscale.ACLPolicy{
|
||||||
ACLs: []headscale.ACL{
|
ACLs: []headscale.ACL{
|
||||||
{
|
{
|
||||||
Action: "accept",
|
Action: "accept",
|
||||||
@@ -399,6 +397,7 @@ func TestACLAllowStarDst(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
2,
|
||||||
)
|
)
|
||||||
|
|
||||||
user1Clients, err := scenario.ListTailscaleClients("user1")
|
user1Clients, err := scenario.ListTailscaleClients("user1")
|
||||||
@@ -441,155 +440,6 @@ func TestACLAllowStarDst(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// This test aims to cover cases where individual hosts are allowed and denied
|
|
||||||
// access based on their assigned hostname
|
|
||||||
// https://github.com/juanfont/headscale/issues/941
|
|
||||||
|
|
||||||
// ACL = [{
|
|
||||||
// "DstPorts": [{
|
|
||||||
// "Bits": null,
|
|
||||||
// "IP": "100.64.0.3/32",
|
|
||||||
// "Ports": {
|
|
||||||
// "First": 0,
|
|
||||||
// "Last": 65535
|
|
||||||
// }
|
|
||||||
// }],
|
|
||||||
// "SrcIPs": ["*"]
|
|
||||||
// }, {
|
|
||||||
//
|
|
||||||
// "DstPorts": [{
|
|
||||||
// "Bits": null,
|
|
||||||
// "IP": "100.64.0.2/32",
|
|
||||||
// "Ports": {
|
|
||||||
// "First": 0,
|
|
||||||
// "Last": 65535
|
|
||||||
// }
|
|
||||||
// }],
|
|
||||||
// "SrcIPs": ["100.64.0.1/32"]
|
|
||||||
// }]
|
|
||||||
//
|
|
||||||
// ACL Cache Map= {
|
|
||||||
// "*": {
|
|
||||||
// "100.64.0.3/32": {}
|
|
||||||
// },
|
|
||||||
// "100.64.0.1/32": {
|
|
||||||
// "100.64.0.2/32": {}
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
func TestACLNamedHostsCanReach(t *testing.T) {
|
|
||||||
IntegrationSkip(t)
|
|
||||||
|
|
||||||
scenario := aclScenario(t,
|
|
||||||
headscale.ACLPolicy{
|
|
||||||
Hosts: headscale.Hosts{
|
|
||||||
"test1": netip.MustParsePrefix("100.64.0.1/32"),
|
|
||||||
"test2": netip.MustParsePrefix("100.64.0.2/32"),
|
|
||||||
"test3": netip.MustParsePrefix("100.64.0.3/32"),
|
|
||||||
},
|
|
||||||
ACLs: []headscale.ACL{
|
|
||||||
// Everyone can curl test3
|
|
||||||
{
|
|
||||||
Action: "accept",
|
|
||||||
Sources: []string{"*"},
|
|
||||||
Destinations: []string{"test3:*"},
|
|
||||||
},
|
|
||||||
// test1 can curl test2
|
|
||||||
{
|
|
||||||
Action: "accept",
|
|
||||||
Sources: []string{"test1"},
|
|
||||||
Destinations: []string{"test2:*"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Since user/users dont matter here, we basically expect that some clients
|
|
||||||
// will be assigned these ips and that we can pick them up for our own use.
|
|
||||||
test1ip := netip.MustParseAddr("100.64.0.1")
|
|
||||||
test1, err := scenario.FindTailscaleClientByIP(test1ip)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
test1fqdn, err := test1.FQDN()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
test1ipURL := fmt.Sprintf("http://%s/etc/hostname", test1ip.String())
|
|
||||||
test1fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test1fqdn)
|
|
||||||
|
|
||||||
test2ip := netip.MustParseAddr("100.64.0.2")
|
|
||||||
test2, err := scenario.FindTailscaleClientByIP(test2ip)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
test2fqdn, err := test2.FQDN()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
test2ipURL := fmt.Sprintf("http://%s/etc/hostname", test2ip.String())
|
|
||||||
test2fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test2fqdn)
|
|
||||||
|
|
||||||
test3ip := netip.MustParseAddr("100.64.0.3")
|
|
||||||
test3, err := scenario.FindTailscaleClientByIP(test3ip)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
test3fqdn, err := test3.FQDN()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
test3ipURL := fmt.Sprintf("http://%s/etc/hostname", test3ip.String())
|
|
||||||
test3fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test3fqdn)
|
|
||||||
|
|
||||||
// test1 can query test3
|
|
||||||
result, err := test1.Curl(test3ipURL)
|
|
||||||
assert.Len(t, result, 13)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
result, err = test1.Curl(test3fqdnURL)
|
|
||||||
assert.Len(t, result, 13)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// test2 can query test3
|
|
||||||
result, err = test2.Curl(test3ipURL)
|
|
||||||
assert.Len(t, result, 13)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
result, err = test2.Curl(test3fqdnURL)
|
|
||||||
assert.Len(t, result, 13)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// test3 cannot query test1
|
|
||||||
result, err = test3.Curl(test1ipURL)
|
|
||||||
assert.Empty(t, result)
|
|
||||||
assert.Error(t, err)
|
|
||||||
|
|
||||||
result, err = test3.Curl(test1fqdnURL)
|
|
||||||
assert.Empty(t, result)
|
|
||||||
assert.Error(t, err)
|
|
||||||
|
|
||||||
// test3 cannot query test2
|
|
||||||
result, err = test3.Curl(test2ipURL)
|
|
||||||
assert.Empty(t, result)
|
|
||||||
assert.Error(t, err)
|
|
||||||
|
|
||||||
result, err = test3.Curl(test2fqdnURL)
|
|
||||||
assert.Empty(t, result)
|
|
||||||
assert.Error(t, err)
|
|
||||||
|
|
||||||
// test1 can query test2
|
|
||||||
result, err = test1.Curl(test2ipURL)
|
|
||||||
assert.Len(t, result, 13)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
result, err = test1.Curl(test2fqdnURL)
|
|
||||||
assert.Len(t, result, 13)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// test2 cannot query test1
|
|
||||||
result, err = test2.Curl(test1ipURL)
|
|
||||||
assert.Empty(t, result)
|
|
||||||
assert.Error(t, err)
|
|
||||||
|
|
||||||
result, err = test2.Curl(test1fqdnURL)
|
|
||||||
assert.Empty(t, result)
|
|
||||||
assert.Error(t, err)
|
|
||||||
|
|
||||||
err = scenario.Shutdown()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestACLNamedHostsCanReachBySubnet is the same as
|
// TestACLNamedHostsCanReachBySubnet is the same as
|
||||||
// TestACLNamedHostsCanReach, but it tests if we expand a
|
// TestACLNamedHostsCanReach, but it tests if we expand a
|
||||||
// full CIDR correctly. All routes should work.
|
// full CIDR correctly. All routes should work.
|
||||||
@@ -597,7 +447,7 @@ func TestACLNamedHostsCanReachBySubnet(t *testing.T) {
|
|||||||
IntegrationSkip(t)
|
IntegrationSkip(t)
|
||||||
|
|
||||||
scenario := aclScenario(t,
|
scenario := aclScenario(t,
|
||||||
headscale.ACLPolicy{
|
&headscale.ACLPolicy{
|
||||||
Hosts: headscale.Hosts{
|
Hosts: headscale.Hosts{
|
||||||
"all": netip.MustParsePrefix("100.64.0.0/24"),
|
"all": netip.MustParsePrefix("100.64.0.0/24"),
|
||||||
},
|
},
|
||||||
@@ -610,6 +460,7 @@ func TestACLNamedHostsCanReachBySubnet(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
3,
|
||||||
)
|
)
|
||||||
|
|
||||||
user1Clients, err := scenario.ListTailscaleClients("user1")
|
user1Clients, err := scenario.ListTailscaleClients("user1")
|
||||||
@@ -651,3 +502,450 @@ func TestACLNamedHostsCanReachBySubnet(t *testing.T) {
|
|||||||
err = scenario.Shutdown()
|
err = scenario.Shutdown()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This test aims to cover cases where individual hosts are allowed and denied
|
||||||
|
// access based on their assigned hostname
|
||||||
|
// https://github.com/juanfont/headscale/issues/941
|
||||||
|
//
|
||||||
|
// ACL = [{
|
||||||
|
// "DstPorts": [{
|
||||||
|
// "Bits": null,
|
||||||
|
// "IP": "100.64.0.3/32",
|
||||||
|
// "Ports": {
|
||||||
|
// "First": 0,
|
||||||
|
// "Last": 65535
|
||||||
|
// }
|
||||||
|
// }],
|
||||||
|
// "SrcIPs": ["*"]
|
||||||
|
// }, {
|
||||||
|
//
|
||||||
|
// "DstPorts": [{
|
||||||
|
// "Bits": null,
|
||||||
|
// "IP": "100.64.0.2/32",
|
||||||
|
// "Ports": {
|
||||||
|
// "First": 0,
|
||||||
|
// "Last": 65535
|
||||||
|
// }
|
||||||
|
// }],
|
||||||
|
// "SrcIPs": ["100.64.0.1/32"]
|
||||||
|
// }]
|
||||||
|
//
|
||||||
|
// ACL Cache Map= {
|
||||||
|
// "*": {
|
||||||
|
// "100.64.0.3/32": {}
|
||||||
|
// },
|
||||||
|
// "100.64.0.1/32": {
|
||||||
|
// "100.64.0.2/32": {}
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// https://github.com/juanfont/headscale/issues/941
|
||||||
|
// Additionally verify ipv6 behaviour, part of
|
||||||
|
// https://github.com/juanfont/headscale/issues/809
|
||||||
|
func TestACLNamedHostsCanReach(t *testing.T) {
|
||||||
|
IntegrationSkip(t)
|
||||||
|
|
||||||
|
tests := map[string]struct {
|
||||||
|
policy headscale.ACLPolicy
|
||||||
|
}{
|
||||||
|
"ipv4": {
|
||||||
|
policy: headscale.ACLPolicy{
|
||||||
|
Hosts: headscale.Hosts{
|
||||||
|
"test1": netip.MustParsePrefix("100.64.0.1/32"),
|
||||||
|
"test2": netip.MustParsePrefix("100.64.0.2/32"),
|
||||||
|
"test3": netip.MustParsePrefix("100.64.0.3/32"),
|
||||||
|
},
|
||||||
|
ACLs: []headscale.ACL{
|
||||||
|
// Everyone can curl test3
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Sources: []string{"*"},
|
||||||
|
Destinations: []string{"test3:*"},
|
||||||
|
},
|
||||||
|
// test1 can curl test2
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Sources: []string{"test1"},
|
||||||
|
Destinations: []string{"test2:*"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ipv6": {
|
||||||
|
policy: headscale.ACLPolicy{
|
||||||
|
Hosts: headscale.Hosts{
|
||||||
|
"test1": netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
|
||||||
|
"test2": netip.MustParsePrefix("fd7a:115c:a1e0::2/128"),
|
||||||
|
"test3": netip.MustParsePrefix("fd7a:115c:a1e0::3/128"),
|
||||||
|
},
|
||||||
|
ACLs: []headscale.ACL{
|
||||||
|
// Everyone can curl test3
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Sources: []string{"*"},
|
||||||
|
Destinations: []string{"test3:*"},
|
||||||
|
},
|
||||||
|
// test1 can curl test2
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Sources: []string{"test1"},
|
||||||
|
Destinations: []string{"test2:*"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range tests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
scenario := aclScenario(t,
|
||||||
|
&testCase.policy,
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Since user/users dont matter here, we basically expect that some clients
|
||||||
|
// will be assigned these ips and that we can pick them up for our own use.
|
||||||
|
test1ip4 := netip.MustParseAddr("100.64.0.1")
|
||||||
|
test1ip6 := netip.MustParseAddr("fd7a:115c:a1e0::1")
|
||||||
|
test1, err := scenario.FindTailscaleClientByIP(test1ip6)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
test1fqdn, err := test1.FQDN()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
test1ip4URL := fmt.Sprintf("http://%s/etc/hostname", test1ip4.String())
|
||||||
|
test1ip6URL := fmt.Sprintf("http://[%s]/etc/hostname", test1ip6.String())
|
||||||
|
test1fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test1fqdn)
|
||||||
|
|
||||||
|
test2ip4 := netip.MustParseAddr("100.64.0.2")
|
||||||
|
test2ip6 := netip.MustParseAddr("fd7a:115c:a1e0::2")
|
||||||
|
test2, err := scenario.FindTailscaleClientByIP(test2ip6)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
test2fqdn, err := test2.FQDN()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
test2ip4URL := fmt.Sprintf("http://%s/etc/hostname", test2ip4.String())
|
||||||
|
test2ip6URL := fmt.Sprintf("http://[%s]/etc/hostname", test2ip6.String())
|
||||||
|
test2fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test2fqdn)
|
||||||
|
|
||||||
|
test3ip4 := netip.MustParseAddr("100.64.0.3")
|
||||||
|
test3ip6 := netip.MustParseAddr("fd7a:115c:a1e0::3")
|
||||||
|
test3, err := scenario.FindTailscaleClientByIP(test3ip6)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
test3fqdn, err := test3.FQDN()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
test3ip4URL := fmt.Sprintf("http://%s/etc/hostname", test3ip4.String())
|
||||||
|
test3ip6URL := fmt.Sprintf("http://[%s]/etc/hostname", test3ip6.String())
|
||||||
|
test3fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test3fqdn)
|
||||||
|
|
||||||
|
// test1 can query test3
|
||||||
|
result, err := test1.Curl(test3ip4URL)
|
||||||
|
assert.Lenf(
|
||||||
|
t,
|
||||||
|
result,
|
||||||
|
13,
|
||||||
|
"failed to connect from test1 to test3 with URL %s, expected hostname of 13 chars, got %s",
|
||||||
|
test3ip4URL,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
result, err = test1.Curl(test3ip6URL)
|
||||||
|
assert.Lenf(
|
||||||
|
t,
|
||||||
|
result,
|
||||||
|
13,
|
||||||
|
"failed to connect from test1 to test3 with URL %s, expected hostname of 13 chars, got %s",
|
||||||
|
test3ip6URL,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
result, err = test1.Curl(test3fqdnURL)
|
||||||
|
assert.Lenf(
|
||||||
|
t,
|
||||||
|
result,
|
||||||
|
13,
|
||||||
|
"failed to connect from test1 to test3 with URL %s, expected hostname of 13 chars, got %s",
|
||||||
|
test3fqdnURL,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// test2 can query test3
|
||||||
|
result, err = test2.Curl(test3ip4URL)
|
||||||
|
assert.Lenf(
|
||||||
|
t,
|
||||||
|
result,
|
||||||
|
13,
|
||||||
|
"failed to connect from test1 to test3 with URL %s, expected hostname of 13 chars, got %s",
|
||||||
|
test3ip4URL,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
result, err = test2.Curl(test3ip6URL)
|
||||||
|
assert.Lenf(
|
||||||
|
t,
|
||||||
|
result,
|
||||||
|
13,
|
||||||
|
"failed to connect from test1 to test3 with URL %s, expected hostname of 13 chars, got %s",
|
||||||
|
test3ip6URL,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
result, err = test2.Curl(test3fqdnURL)
|
||||||
|
assert.Lenf(
|
||||||
|
t,
|
||||||
|
result,
|
||||||
|
13,
|
||||||
|
"failed to connect from test1 to test3 with URL %s, expected hostname of 13 chars, got %s",
|
||||||
|
test3fqdnURL,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// test3 cannot query test1
|
||||||
|
result, err = test3.Curl(test1ip4URL)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
result, err = test3.Curl(test1ip6URL)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
result, err = test3.Curl(test1fqdnURL)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// test3 cannot query test2
|
||||||
|
result, err = test3.Curl(test2ip4URL)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
result, err = test3.Curl(test2ip6URL)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
result, err = test3.Curl(test2fqdnURL)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// test1 can query test2
|
||||||
|
result, err = test1.Curl(test2ip4URL)
|
||||||
|
assert.Lenf(
|
||||||
|
t,
|
||||||
|
result,
|
||||||
|
13,
|
||||||
|
"failed to connect from test1 to test2 with URL %s, expected hostname of 13 chars, got %s",
|
||||||
|
test2ip4URL,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
result, err = test1.Curl(test2ip6URL)
|
||||||
|
assert.Lenf(
|
||||||
|
t,
|
||||||
|
result,
|
||||||
|
13,
|
||||||
|
"failed to connect from test1 to test2 with URL %s, expected hostname of 13 chars, got %s",
|
||||||
|
test2ip6URL,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
result, err = test1.Curl(test2fqdnURL)
|
||||||
|
assert.Lenf(
|
||||||
|
t,
|
||||||
|
result,
|
||||||
|
13,
|
||||||
|
"failed to connect from test1 to test2 with URL %s, expected hostname of 13 chars, got %s",
|
||||||
|
test2fqdnURL,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// test2 cannot query test1
|
||||||
|
result, err = test2.Curl(test1ip4URL)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
result, err = test2.Curl(test1ip6URL)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
result, err = test2.Curl(test1fqdnURL)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
err = scenario.Shutdown()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestACLDevice1CanAccessDevice2 is a table driven test that aims to test
|
||||||
|
// the various ways to achieve a connection between device1 and device2 where
|
||||||
|
// device1 can access device2, but not the other way around. This can be
|
||||||
|
// viewed as one of the most important tests here as it covers most of the
|
||||||
|
// syntax that can be used.
|
||||||
|
//
|
||||||
|
// Before adding new taste cases, consider if it can be reduced to a case
|
||||||
|
// in this function.
|
||||||
|
func TestACLDevice1CanAccessDevice2(t *testing.T) {
|
||||||
|
IntegrationSkip(t)
|
||||||
|
|
||||||
|
tests := map[string]struct {
|
||||||
|
policy headscale.ACLPolicy
|
||||||
|
}{
|
||||||
|
"ipv4": {
|
||||||
|
policy: headscale.ACLPolicy{
|
||||||
|
ACLs: []headscale.ACL{
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Sources: []string{"100.64.0.1"},
|
||||||
|
Destinations: []string{"100.64.0.2:*"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ipv6": {
|
||||||
|
policy: headscale.ACLPolicy{
|
||||||
|
ACLs: []headscale.ACL{
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Sources: []string{"fd7a:115c:a1e0::1"},
|
||||||
|
Destinations: []string{"fd7a:115c:a1e0::2:*"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"hostv4cidr": {
|
||||||
|
policy: headscale.ACLPolicy{
|
||||||
|
Hosts: headscale.Hosts{
|
||||||
|
"test1": netip.MustParsePrefix("100.64.0.1/32"),
|
||||||
|
"test2": netip.MustParsePrefix("100.64.0.2/32"),
|
||||||
|
},
|
||||||
|
ACLs: []headscale.ACL{
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Sources: []string{"test1"},
|
||||||
|
Destinations: []string{"test2:*"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"hostv6cidr": {
|
||||||
|
policy: headscale.ACLPolicy{
|
||||||
|
Hosts: headscale.Hosts{
|
||||||
|
"test1": netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
|
||||||
|
"test2": netip.MustParsePrefix("fd7a:115c:a1e0::2/128"),
|
||||||
|
},
|
||||||
|
ACLs: []headscale.ACL{
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Sources: []string{"test1"},
|
||||||
|
Destinations: []string{"test2:*"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"group": {
|
||||||
|
policy: headscale.ACLPolicy{
|
||||||
|
Groups: map[string][]string{
|
||||||
|
"group:one": {"user1"},
|
||||||
|
"group:two": {"user2"},
|
||||||
|
},
|
||||||
|
ACLs: []headscale.ACL{
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Sources: []string{"group:one"},
|
||||||
|
Destinations: []string{"group:two:*"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// TODO(kradalby): Add similar tests for Tags, might need support
|
||||||
|
// in the scenario function when we create or join the clients.
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range tests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
scenario := aclScenario(t, &testCase.policy, 1)
|
||||||
|
|
||||||
|
test1ip := netip.MustParseAddr("100.64.0.1")
|
||||||
|
test1ip6 := netip.MustParseAddr("fd7a:115c:a1e0::1")
|
||||||
|
test1, err := scenario.FindTailscaleClientByIP(test1ip)
|
||||||
|
assert.NotNil(t, test1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
test1fqdn, err := test1.FQDN()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
test1ipURL := fmt.Sprintf("http://%s/etc/hostname", test1ip.String())
|
||||||
|
test1ip6URL := fmt.Sprintf("http://[%s]/etc/hostname", test1ip6.String())
|
||||||
|
test1fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test1fqdn)
|
||||||
|
|
||||||
|
test2ip := netip.MustParseAddr("100.64.0.2")
|
||||||
|
test2ip6 := netip.MustParseAddr("fd7a:115c:a1e0::2")
|
||||||
|
test2, err := scenario.FindTailscaleClientByIP(test2ip)
|
||||||
|
assert.NotNil(t, test2)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
test2fqdn, err := test2.FQDN()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
test2ipURL := fmt.Sprintf("http://%s/etc/hostname", test2ip.String())
|
||||||
|
test2ip6URL := fmt.Sprintf("http://[%s]/etc/hostname", test2ip6.String())
|
||||||
|
test2fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test2fqdn)
|
||||||
|
|
||||||
|
// test1 can query test2
|
||||||
|
result, err := test1.Curl(test2ipURL)
|
||||||
|
assert.Lenf(
|
||||||
|
t,
|
||||||
|
result,
|
||||||
|
13,
|
||||||
|
"failed to connect from test1 to test with URL %s, expected hostname of 13 chars, got %s",
|
||||||
|
test2ipURL,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
result, err = test1.Curl(test2ip6URL)
|
||||||
|
assert.Lenf(
|
||||||
|
t,
|
||||||
|
result,
|
||||||
|
13,
|
||||||
|
"failed to connect from test1 to test with URL %s, expected hostname of 13 chars, got %s",
|
||||||
|
test2ip6URL,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
result, err = test1.Curl(test2fqdnURL)
|
||||||
|
assert.Lenf(
|
||||||
|
t,
|
||||||
|
result,
|
||||||
|
13,
|
||||||
|
"failed to connect from test1 to test with URL %s, expected hostname of 13 chars, got %s",
|
||||||
|
test2fqdnURL,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
result, err = test2.Curl(test1ipURL)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
result, err = test2.Curl(test1ip6URL)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
result, err = test2.Curl(test1fqdnURL)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
err = scenario.Shutdown()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -362,6 +362,15 @@ func (s *AuthOIDCScenario) runTailscaleUp(
|
|||||||
|
|
||||||
user.joinWaitGroup.Wait()
|
user.joinWaitGroup.Wait()
|
||||||
|
|
||||||
|
for _, client := range user.Clients {
|
||||||
|
err := client.WaitForReady()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("client %s was not ready: %s", client.Hostname(), err)
|
||||||
|
|
||||||
|
return fmt.Errorf("failed to up tailscale node: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -274,6 +274,15 @@ func (s *AuthWebFlowScenario) runTailscaleUp(
|
|||||||
}
|
}
|
||||||
user.joinWaitGroup.Wait()
|
user.joinWaitGroup.Wait()
|
||||||
|
|
||||||
|
for _, client := range user.Clients {
|
||||||
|
err := client.WaitForReady()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("client %s was not ready: %s", client.Hostname(), err)
|
||||||
|
|
||||||
|
return fmt.Errorf("failed to up tailscale node: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ package integration
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||||
|
"github.com/ory/dockertest/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ControlServer interface {
|
type ControlServer interface {
|
||||||
Shutdown() error
|
Shutdown() error
|
||||||
SaveLog(string) error
|
SaveLog(string) error
|
||||||
Execute(command []string) (string, error)
|
Execute(command []string) (string, error)
|
||||||
|
ConnectToNetwork(network *dockertest.Network) error
|
||||||
GetHealthEndpoint() string
|
GetHealthEndpoint() string
|
||||||
GetEndpoint() string
|
GetEndpoint() string
|
||||||
WaitForReady() error
|
WaitForReady() error
|
||||||
|
|||||||
236
integration/embedded_derp_test.go
Normal file
236
integration/embedded_derp_test.go
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/juanfont/headscale"
|
||||||
|
"github.com/juanfont/headscale/integration/dockertestutil"
|
||||||
|
"github.com/juanfont/headscale/integration/hsic"
|
||||||
|
"github.com/juanfont/headscale/integration/tsic"
|
||||||
|
"github.com/ory/dockertest/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EmbeddedDERPServerScenario struct {
|
||||||
|
*Scenario
|
||||||
|
|
||||||
|
tsicNetworks map[string]*dockertest.Network
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDERPServerScenario(t *testing.T) {
|
||||||
|
IntegrationSkip(t)
|
||||||
|
// t.Parallel()
|
||||||
|
|
||||||
|
baseScenario, err := NewScenario()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to create scenario: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scenario := EmbeddedDERPServerScenario{
|
||||||
|
Scenario: baseScenario,
|
||||||
|
tsicNetworks: map[string]*dockertest.Network{},
|
||||||
|
}
|
||||||
|
|
||||||
|
spec := map[string]int{
|
||||||
|
"user1": len(TailscaleVersions),
|
||||||
|
}
|
||||||
|
|
||||||
|
headscaleConfig := map[string]string{}
|
||||||
|
headscaleConfig["HEADSCALE_DERP_URLS"] = ""
|
||||||
|
headscaleConfig["HEADSCALE_DERP_SERVER_ENABLED"] = "true"
|
||||||
|
headscaleConfig["HEADSCALE_DERP_SERVER_REGION_ID"] = "999"
|
||||||
|
headscaleConfig["HEADSCALE_DERP_SERVER_REGION_CODE"] = "headscale"
|
||||||
|
headscaleConfig["HEADSCALE_DERP_SERVER_REGION_NAME"] = "Headscale Embedded DERP"
|
||||||
|
headscaleConfig["HEADSCALE_DERP_SERVER_STUN_LISTEN_ADDR"] = "0.0.0.0:3478"
|
||||||
|
|
||||||
|
err = scenario.CreateHeadscaleEnv(
|
||||||
|
spec,
|
||||||
|
hsic.WithConfigEnv(headscaleConfig),
|
||||||
|
hsic.WithTestName("derpserver"),
|
||||||
|
hsic.WithExtraPorts([]string{"3478/udp"}),
|
||||||
|
hsic.WithTLS(),
|
||||||
|
hsic.WithHostnameAsServerURL(),
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
allHostnames, err := scenario.ListTailscaleClientsFQDNs()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to get FQDNs: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
success := pingDerpAllHelper(t, allClients, allHostnames)
|
||||||
|
|
||||||
|
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 *EmbeddedDERPServerScenario) CreateHeadscaleEnv(
|
||||||
|
users map[string]int,
|
||||||
|
opts ...hsic.Option,
|
||||||
|
) error {
|
||||||
|
hsServer, err := s.Headscale(opts...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
headscaleEndpoint := hsServer.GetEndpoint()
|
||||||
|
headscaleURL, err := url.Parse(headscaleEndpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
headscaleURL.Host = fmt.Sprintf("%s:%s", hsServer.GetHostname(), headscaleURL.Port())
|
||||||
|
|
||||||
|
err = hsServer.WaitForReady()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, err := headscale.GenerateRandomStringDNSSafe(scenarioHashLength)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for userName, clientCount := range users {
|
||||||
|
err = s.CreateUser(userName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.CreateTailscaleIsolatedNodesInUser(
|
||||||
|
hash,
|
||||||
|
userName,
|
||||||
|
"all",
|
||||||
|
clientCount,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := s.CreatePreAuthKey(userName, true, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.RunTailscaleUp(userName, headscaleURL.String(), key.GetKey())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EmbeddedDERPServerScenario) CreateTailscaleIsolatedNodesInUser(
|
||||||
|
hash string,
|
||||||
|
userStr string,
|
||||||
|
requestedVersion string,
|
||||||
|
count int,
|
||||||
|
opts ...tsic.Option,
|
||||||
|
) error {
|
||||||
|
hsServer, err := s.Headscale()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if user, ok := s.users[userStr]; ok {
|
||||||
|
for clientN := 0; clientN < count; clientN++ {
|
||||||
|
networkName := fmt.Sprintf("tsnet-%s-%s-%d",
|
||||||
|
hash,
|
||||||
|
userStr,
|
||||||
|
clientN,
|
||||||
|
)
|
||||||
|
network, err := dockertestutil.GetFirstOrCreateNetwork(
|
||||||
|
s.pool,
|
||||||
|
networkName,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create or get %s network: %w", networkName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.tsicNetworks[networkName] = network
|
||||||
|
|
||||||
|
err = hsServer.ConnectToNetwork(network)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to connect headscale to %s network: %w", networkName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
version := requestedVersion
|
||||||
|
if requestedVersion == "all" {
|
||||||
|
version = TailscaleVersions[clientN%len(TailscaleVersions)]
|
||||||
|
}
|
||||||
|
|
||||||
|
cert := hsServer.GetCert()
|
||||||
|
|
||||||
|
user.createWaitGroup.Add(1)
|
||||||
|
|
||||||
|
opts = append(opts,
|
||||||
|
tsic.WithHeadscaleTLS(cert),
|
||||||
|
)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer user.createWaitGroup.Done()
|
||||||
|
|
||||||
|
// TODO(kradalby): error handle this
|
||||||
|
tsClient, err := tsic.New(
|
||||||
|
s.pool,
|
||||||
|
version,
|
||||||
|
network,
|
||||||
|
opts...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
// return fmt.Errorf("failed to add tailscale node: %w", err)
|
||||||
|
log.Printf("failed to create tailscale node: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tsClient.WaitForReady()
|
||||||
|
if err != nil {
|
||||||
|
// return fmt.Errorf("failed to add tailscale node: %w", err)
|
||||||
|
log.Printf("failed to wait for tailscaled: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Clients[tsClient.Hostname()] = tsClient
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
user.createWaitGroup.Wait()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("failed to add tailscale node: %w", errNoUserAvailable)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EmbeddedDERPServerScenario) Shutdown() error {
|
||||||
|
for _, network := range s.tsicNetworks {
|
||||||
|
err := s.pool.RemoveNetwork(network)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Scenario.Shutdown()
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"math/big"
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
@@ -23,6 +24,7 @@ import (
|
|||||||
"github.com/juanfont/headscale/integration/dockertestutil"
|
"github.com/juanfont/headscale/integration/dockertestutil"
|
||||||
"github.com/juanfont/headscale/integration/integrationutil"
|
"github.com/juanfont/headscale/integration/integrationutil"
|
||||||
"github.com/ory/dockertest/v3"
|
"github.com/ory/dockertest/v3"
|
||||||
|
"github.com/ory/dockertest/v3/docker"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -52,6 +54,8 @@ type HeadscaleInContainer struct {
|
|||||||
|
|
||||||
// optional config
|
// optional config
|
||||||
port int
|
port int
|
||||||
|
extraPorts []string
|
||||||
|
hostPortBindings map[string][]string
|
||||||
aclPolicy *headscale.ACLPolicy
|
aclPolicy *headscale.ACLPolicy
|
||||||
env map[string]string
|
env map[string]string
|
||||||
tlsCert []byte
|
tlsCert []byte
|
||||||
@@ -77,7 +81,7 @@ func WithACLPolicy(acl *headscale.ACLPolicy) Option {
|
|||||||
// WithTLS creates certificates and enables HTTPS.
|
// WithTLS creates certificates and enables HTTPS.
|
||||||
func WithTLS() Option {
|
func WithTLS() Option {
|
||||||
return func(hsic *HeadscaleInContainer) {
|
return func(hsic *HeadscaleInContainer) {
|
||||||
cert, key, err := createCertificate()
|
cert, key, err := createCertificate(hsic.hostname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to create certificates for headscale test: %s", err)
|
log.Fatalf("failed to create certificates for headscale test: %s", err)
|
||||||
}
|
}
|
||||||
@@ -108,6 +112,19 @@ func WithPort(port int) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithExtraPorts exposes additional ports on the container (e.g. 3478/udp for STUN).
|
||||||
|
func WithExtraPorts(ports []string) Option {
|
||||||
|
return func(hsic *HeadscaleInContainer) {
|
||||||
|
hsic.extraPorts = ports
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithHostPortBindings(bindings map[string][]string) Option {
|
||||||
|
return func(hsic *HeadscaleInContainer) {
|
||||||
|
hsic.hostPortBindings = bindings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WithTestName sets a name for the test, this will be reflected
|
// WithTestName sets a name for the test, this will be reflected
|
||||||
// in the Docker container name.
|
// in the Docker container name.
|
||||||
func WithTestName(testName string) Option {
|
func WithTestName(testName string) Option {
|
||||||
@@ -173,6 +190,16 @@ func New(
|
|||||||
|
|
||||||
portProto := fmt.Sprintf("%d/tcp", hsic.port)
|
portProto := fmt.Sprintf("%d/tcp", hsic.port)
|
||||||
|
|
||||||
|
serverURL, err := url.Parse(hsic.env["HEADSCALE_SERVER_URL"])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(hsic.tlsCert) != 0 && len(hsic.tlsKey) != 0 {
|
||||||
|
serverURL.Scheme = "https"
|
||||||
|
hsic.env["HEADSCALE_SERVER_URL"] = serverURL.String()
|
||||||
|
}
|
||||||
|
|
||||||
headscaleBuildOptions := &dockertest.BuildOptions{
|
headscaleBuildOptions := &dockertest.BuildOptions{
|
||||||
Dockerfile: "Dockerfile.debug",
|
Dockerfile: "Dockerfile.debug",
|
||||||
ContextDir: dockerContextPath,
|
ContextDir: dockerContextPath,
|
||||||
@@ -187,7 +214,7 @@ func New(
|
|||||||
|
|
||||||
runOptions := &dockertest.RunOptions{
|
runOptions := &dockertest.RunOptions{
|
||||||
Name: hsic.hostname,
|
Name: hsic.hostname,
|
||||||
ExposedPorts: []string{portProto},
|
ExposedPorts: append([]string{portProto}, hsic.extraPorts...),
|
||||||
Networks: []*dockertest.Network{network},
|
Networks: []*dockertest.Network{network},
|
||||||
// Cmd: []string{"headscale", "serve"},
|
// Cmd: []string{"headscale", "serve"},
|
||||||
// TODO(kradalby): Get rid of this hack, we currently need to give us some
|
// TODO(kradalby): Get rid of this hack, we currently need to give us some
|
||||||
@@ -196,6 +223,18 @@ func New(
|
|||||||
Env: env,
|
Env: env,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(hsic.hostPortBindings) > 0 {
|
||||||
|
runOptions.PortBindings = map[docker.Port][]docker.PortBinding{}
|
||||||
|
for port, hostPorts := range hsic.hostPortBindings {
|
||||||
|
runOptions.PortBindings[docker.Port(port)] = []docker.PortBinding{}
|
||||||
|
for _, hostPort := range hostPorts {
|
||||||
|
runOptions.PortBindings[docker.Port(port)] = append(
|
||||||
|
runOptions.PortBindings[docker.Port(port)],
|
||||||
|
docker.PortBinding{HostPort: hostPort})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// dockertest isnt very good at handling containers that has already
|
// dockertest isnt very good at handling containers that has already
|
||||||
// been created, this is an attempt to make sure this container isnt
|
// been created, this is an attempt to make sure this container isnt
|
||||||
// present.
|
// present.
|
||||||
@@ -256,6 +295,10 @@ func New(
|
|||||||
return hsic, nil
|
return hsic, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *HeadscaleInContainer) ConnectToNetwork(network *dockertest.Network) error {
|
||||||
|
return t.container.ConnectToNetwork(network)
|
||||||
|
}
|
||||||
|
|
||||||
func (t *HeadscaleInContainer) hasTLS() bool {
|
func (t *HeadscaleInContainer) hasTLS() bool {
|
||||||
return len(t.tlsCert) != 0 && len(t.tlsKey) != 0
|
return len(t.tlsCert) != 0 && len(t.tlsKey) != 0
|
||||||
}
|
}
|
||||||
@@ -456,7 +499,7 @@ func (t *HeadscaleInContainer) WriteFile(path string, data []byte) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// nolint
|
// nolint
|
||||||
func createCertificate() ([]byte, []byte, error) {
|
func createCertificate(hostname string) ([]byte, []byte, error) {
|
||||||
// From:
|
// From:
|
||||||
// https://shaneutt.com/blog/golang-ca-and-signed-cert-go/
|
// https://shaneutt.com/blog/golang-ca-and-signed-cert-go/
|
||||||
|
|
||||||
@@ -468,7 +511,7 @@ func createCertificate() ([]byte, []byte, error) {
|
|||||||
Locality: []string{"Leiden"},
|
Locality: []string{"Leiden"},
|
||||||
},
|
},
|
||||||
NotBefore: time.Now(),
|
NotBefore: time.Now(),
|
||||||
NotAfter: time.Now().Add(30 * time.Minute),
|
NotAfter: time.Now().Add(60 * time.Minute),
|
||||||
IsCA: true,
|
IsCA: true,
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{
|
ExtKeyUsage: []x509.ExtKeyUsage{
|
||||||
x509.ExtKeyUsageClientAuth,
|
x509.ExtKeyUsageClientAuth,
|
||||||
@@ -486,16 +529,17 @@ func createCertificate() ([]byte, []byte, error) {
|
|||||||
cert := &x509.Certificate{
|
cert := &x509.Certificate{
|
||||||
SerialNumber: big.NewInt(1658),
|
SerialNumber: big.NewInt(1658),
|
||||||
Subject: pkix.Name{
|
Subject: pkix.Name{
|
||||||
|
CommonName: hostname,
|
||||||
Organization: []string{"Headscale testing INC"},
|
Organization: []string{"Headscale testing INC"},
|
||||||
Country: []string{"NL"},
|
Country: []string{"NL"},
|
||||||
Locality: []string{"Leiden"},
|
Locality: []string{"Leiden"},
|
||||||
},
|
},
|
||||||
IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback},
|
|
||||||
NotBefore: time.Now(),
|
NotBefore: time.Now(),
|
||||||
NotAfter: time.Now().Add(30 * time.Minute),
|
NotAfter: time.Now().Add(60 * time.Minute),
|
||||||
SubjectKeyId: []byte{1, 2, 3, 4, 6},
|
SubjectKeyId: []byte{1, 2, 3, 4, 6},
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
||||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||||
|
DNSNames: []string{hostname},
|
||||||
}
|
}
|
||||||
|
|
||||||
certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||||
|
|||||||
@@ -356,6 +356,15 @@ func (s *Scenario) RunTailscaleUp(
|
|||||||
|
|
||||||
user.joinWaitGroup.Wait()
|
user.joinWaitGroup.Wait()
|
||||||
|
|
||||||
|
for _, client := range user.Clients {
|
||||||
|
err := client.WaitForReady()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("client %s was not ready: %s", client.Hostname(), err)
|
||||||
|
|
||||||
|
return fmt.Errorf("failed to up tailscale node: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/juanfont/headscale/integration/dockertestutil"
|
||||||
"github.com/juanfont/headscale/integration/tsic"
|
"github.com/juanfont/headscale/integration/tsic"
|
||||||
"tailscale.com/ipn/ipnstate"
|
"tailscale.com/ipn/ipnstate"
|
||||||
)
|
)
|
||||||
@@ -13,7 +14,7 @@ type TailscaleClient interface {
|
|||||||
Hostname() string
|
Hostname() string
|
||||||
Shutdown() error
|
Shutdown() error
|
||||||
Version() string
|
Version() string
|
||||||
Execute(command []string) (string, string, error)
|
Execute(command []string, options ...dockertestutil.ExecuteCommandOption) (string, string, error)
|
||||||
Up(loginServer, authKey string) error
|
Up(loginServer, authKey string) error
|
||||||
UpWithLoginURL(loginServer string) (*url.URL, error)
|
UpWithLoginURL(loginServer string) (*url.URL, error)
|
||||||
Logout() error
|
Logout() error
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
errTailscalePingFailed = errors.New("ping failed")
|
errTailscalePingFailed = errors.New("ping failed")
|
||||||
|
errTailscalePingNotDERP = errors.New("ping not via DERP")
|
||||||
errTailscaleNotLoggedIn = errors.New("tailscale not logged in")
|
errTailscaleNotLoggedIn = errors.New("tailscale not logged in")
|
||||||
errTailscaleWrongPeerCount = errors.New("wrong peer count")
|
errTailscaleWrongPeerCount = errors.New("wrong peer count")
|
||||||
errTailscaleCannotUpWithoutAuthkey = errors.New("cannot up without authkey")
|
errTailscaleCannotUpWithoutAuthkey = errors.New("cannot up without authkey")
|
||||||
@@ -56,6 +57,7 @@ type TailscaleInContainer struct {
|
|||||||
withSSH bool
|
withSSH bool
|
||||||
withTags []string
|
withTags []string
|
||||||
withEntrypoint []string
|
withEntrypoint []string
|
||||||
|
withExtraHosts []string
|
||||||
workdir string
|
workdir string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +126,12 @@ func WithDockerWorkdir(dir string) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithExtraHosts(hosts []string) Option {
|
||||||
|
return func(tsic *TailscaleInContainer) {
|
||||||
|
tsic.withExtraHosts = hosts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WithDockerEntrypoint allows the docker entrypoint of the container
|
// WithDockerEntrypoint allows the docker entrypoint of the container
|
||||||
// to be overridden. This is a dangerous option which can make
|
// to be overridden. This is a dangerous option which can make
|
||||||
// the container not work as intended as a typo might prevent
|
// the container not work as intended as a typo might prevent
|
||||||
@@ -169,11 +177,12 @@ func New(
|
|||||||
|
|
||||||
tailscaleOptions := &dockertest.RunOptions{
|
tailscaleOptions := &dockertest.RunOptions{
|
||||||
Name: hostname,
|
Name: hostname,
|
||||||
Networks: []*dockertest.Network{network},
|
Networks: []*dockertest.Network{tsic.network},
|
||||||
// Cmd: []string{
|
// Cmd: []string{
|
||||||
// "tailscaled", "--tun=tsdev",
|
// "tailscaled", "--tun=tsdev",
|
||||||
// },
|
// },
|
||||||
Entrypoint: tsic.withEntrypoint,
|
Entrypoint: tsic.withEntrypoint,
|
||||||
|
ExtraHosts: tsic.withExtraHosts,
|
||||||
}
|
}
|
||||||
|
|
||||||
if tsic.headscaleHostname != "" {
|
if tsic.headscaleHostname != "" {
|
||||||
@@ -248,11 +257,13 @@ func (t *TailscaleInContainer) ID() string {
|
|||||||
// result of stdout as a string.
|
// result of stdout as a string.
|
||||||
func (t *TailscaleInContainer) Execute(
|
func (t *TailscaleInContainer) Execute(
|
||||||
command []string,
|
command []string,
|
||||||
|
options ...dockertestutil.ExecuteCommandOption,
|
||||||
) (string, string, error) {
|
) (string, string, error) {
|
||||||
stdout, stderr, err := dockertestutil.ExecuteCommand(
|
stdout, stderr, err := dockertestutil.ExecuteCommand(
|
||||||
t.container,
|
t.container,
|
||||||
command,
|
command,
|
||||||
[]string{},
|
[]string{},
|
||||||
|
options...,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("command stderr: %s\n", stderr)
|
log.Printf("command stderr: %s\n", stderr)
|
||||||
@@ -430,6 +441,15 @@ func (t *TailscaleInContainer) WaitForReady() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ipnstate.Status.CurrentTailnet was added in Tailscale 1.22.0
|
||||||
|
// https://github.com/tailscale/tailscale/pull/3865
|
||||||
|
//
|
||||||
|
// Before that, we can check the BackendState to see if the
|
||||||
|
// tailscaled daemon is connected to the control system.
|
||||||
|
if status.BackendState == "Running" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
return errTailscaleNotConnected
|
return errTailscaleNotConnected
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -468,7 +488,7 @@ func (t *TailscaleInContainer) WaitForPeers(expected int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// PingOption repreent optional settings that can be given
|
// PingOption represent optional settings that can be given
|
||||||
// to ping another host.
|
// to ping another host.
|
||||||
PingOption = func(args *pingArgs)
|
PingOption = func(args *pingArgs)
|
||||||
|
|
||||||
@@ -526,7 +546,12 @@ func (t *TailscaleInContainer) Ping(hostnameOrIP string, opts ...PingOption) err
|
|||||||
command = append(command, hostnameOrIP)
|
command = append(command, hostnameOrIP)
|
||||||
|
|
||||||
return t.pool.Retry(func() error {
|
return t.pool.Retry(func() error {
|
||||||
result, _, err := t.Execute(command)
|
result, _, err := t.Execute(
|
||||||
|
command,
|
||||||
|
dockertestutil.ExecuteCommandTimeout(
|
||||||
|
time.Duration(int64(args.timeout)*int64(args.count)),
|
||||||
|
),
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf(
|
log.Printf(
|
||||||
"failed to run ping command from %s to %s, err: %s",
|
"failed to run ping command from %s to %s, err: %s",
|
||||||
@@ -538,10 +563,22 @@ func (t *TailscaleInContainer) Ping(hostnameOrIP string, opts ...PingOption) err
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.Contains(result, "pong") && !strings.Contains(result, "is local") {
|
if strings.Contains(result, "is local") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(result, "pong") {
|
||||||
return backoff.Permanent(errTailscalePingFailed)
|
return backoff.Permanent(errTailscalePingFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !args.direct {
|
||||||
|
if strings.Contains(result, "via DERP") {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return backoff.Permanent(errTailscalePingNotDERP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,14 @@ package integration
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/juanfont/headscale/integration/tsic"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
derpPingTimeout = 2 * time.Second
|
||||||
|
derpPingCount = 10
|
||||||
)
|
)
|
||||||
|
|
||||||
func pingAllHelper(t *testing.T, clients []TailscaleClient, addrs []string) int {
|
func pingAllHelper(t *testing.T, clients []TailscaleClient, addrs []string) int {
|
||||||
@@ -22,6 +30,52 @@ func pingAllHelper(t *testing.T, clients []TailscaleClient, addrs []string) int
|
|||||||
return success
|
return success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func pingDerpAllHelper(t *testing.T, clients []TailscaleClient, addrs []string) int {
|
||||||
|
t.Helper()
|
||||||
|
success := 0
|
||||||
|
|
||||||
|
for _, client := range clients {
|
||||||
|
for _, addr := range addrs {
|
||||||
|
if isSelfClient(client, addr) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err := client.Ping(
|
||||||
|
addr,
|
||||||
|
tsic.WithPingTimeout(derpPingTimeout),
|
||||||
|
tsic.WithPingCount(derpPingCount),
|
||||||
|
tsic.WithPingUntilDirect(false),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to ping %s from %s: %s", addr, client.Hostname(), err)
|
||||||
|
} else {
|
||||||
|
success++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return success
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSelfClient(client TailscaleClient, addr string) bool {
|
||||||
|
if addr == client.Hostname() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
ips, err := client.IPs()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ip := range ips {
|
||||||
|
if ip.String() == addr {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// pingAllNegativeHelper is intended to have 1 or more nodes timeing out from the ping,
|
// pingAllNegativeHelper is intended to have 1 or more nodes timeing out from the ping,
|
||||||
// it counts failures instead of successes.
|
// it counts failures instead of successes.
|
||||||
// func pingAllNegativeHelper(t *testing.T, clients []TailscaleClient, addrs []string) int {
|
// func pingAllNegativeHelper(t *testing.T, clients []TailscaleClient, addrs []string) int {
|
||||||
@@ -46,3 +100,35 @@ func pingAllHelper(t *testing.T, clients []TailscaleClient, addrs []string) int
|
|||||||
//
|
//
|
||||||
// return failures
|
// return failures
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
// // findPeerByIP takes an IP and a map of peers from status.Peer, and returns a *ipnstate.PeerStatus
|
||||||
|
// // if there is a peer with the given IP. If no peer is found, nil is returned.
|
||||||
|
// func findPeerByIP(
|
||||||
|
// ip netip.Addr,
|
||||||
|
// peers map[key.NodePublic]*ipnstate.PeerStatus,
|
||||||
|
// ) *ipnstate.PeerStatus {
|
||||||
|
// for _, peer := range peers {
|
||||||
|
// for _, peerIP := range peer.TailscaleIPs {
|
||||||
|
// if ip == peerIP {
|
||||||
|
// return peer
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // findPeerByHostname takes a hostname and a map of peers from status.Peer, and returns a *ipnstate.PeerStatus
|
||||||
|
// // if there is a peer with the given hostname. If no peer is found, nil is returned.
|
||||||
|
// func findPeerByHostname(
|
||||||
|
// hostname string,
|
||||||
|
// peers map[key.NodePublic]*ipnstate.PeerStatus,
|
||||||
|
// ) *ipnstate.PeerStatus {
|
||||||
|
// for _, peer := range peers {
|
||||||
|
// if hostname == peer.HostName {
|
||||||
|
// return peer
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
|||||||
14
machine.go
14
machine.go
@@ -1267,3 +1267,17 @@ func (h *Headscale) GenerateGivenName(machineKey string, suppliedName string) (s
|
|||||||
|
|
||||||
return givenName, nil
|
return givenName, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (machines Machines) FilterByIP(ip netip.Addr) Machines {
|
||||||
|
found := make(Machines, 0)
|
||||||
|
|
||||||
|
for _, machine := range machines {
|
||||||
|
for _, mIP := range machine.IPAddresses {
|
||||||
|
if ip == mIP {
|
||||||
|
found = append(found, machine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user