mirror of
https://github.com/juanfont/headscale.git
synced 2025-08-17 18:07:43 +00:00
Compare commits
37 Commits
port-derp-
...
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 | ||
![]() |
d0b3b1bfc4 | ||
![]() |
6eea96eabc | ||
![]() |
d08fee78c3 | ||
![]() |
bb5f0d456c | ||
![]() |
c186c49e25 | ||
![]() |
4ec6894773 | ||
![]() |
dd9b4b1cb7 | ||
![]() |
a43bb9c958 | ||
![]() |
ba905ff6fc | ||
![]() |
99bd09f688 | ||
![]() |
a6bc792a61 | ||
![]() |
6381d3660a | ||
![]() |
66c5f74d78 | ||
![]() |
1723a6bf40 |
45
.github/workflows/docs.yml
vendored
Normal file
45
.github/workflows/docs.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
name: Build documentation
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Install python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: 3.x
|
||||||
|
- name: Setup cache
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
key: ${{ github.ref }}
|
||||||
|
path: .cache
|
||||||
|
- name: Setup dependencies
|
||||||
|
run: pip install mkdocs-material pillow cairosvg mkdocs-minify-plugin
|
||||||
|
- name: Build docs
|
||||||
|
run: mkdocs build --strict
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-pages-artifact@v1
|
||||||
|
with:
|
||||||
|
path: ./site
|
||||||
|
deploy:
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
steps:
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v1
|
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"
|
8
.gitignore
vendored
8
.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
|
||||||
@@ -35,3 +37,7 @@ result
|
|||||||
.direnv/
|
.direnv/
|
||||||
|
|
||||||
integration_test/etc/config.dump.yaml
|
integration_test/etc/config.dump.yaml
|
||||||
|
|
||||||
|
# MkDocs
|
||||||
|
.cache
|
||||||
|
/site
|
||||||
|
@@ -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
|
|
||||||
|
179
README.md
179
README.md
@@ -188,13 +188,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Juan Font</b></sub>
|
<sub style="font-size:14px"><b>Juan Font</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
|
||||||
<a href=https://github.com/restanrm>
|
|
||||||
<img src=https://avatars.githubusercontent.com/u/4344371?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Adrien Raffin-Caboisse/>
|
|
||||||
<br />
|
|
||||||
<sub style="font-size:14px"><b>Adrien Raffin-Caboisse</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/cure>
|
<a href=https://github.com/cure>
|
||||||
<img src=https://avatars.githubusercontent.com/u/149135?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Ward Vandewege/>
|
<img src=https://avatars.githubusercontent.com/u/149135?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Ward Vandewege/>
|
||||||
@@ -216,8 +209,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Benjamin Roberts</b></sub>
|
<sub style="font-size:14px"><b>Benjamin Roberts</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/reynico>
|
<a href=https://github.com/reynico>
|
||||||
<img src=https://avatars.githubusercontent.com/u/715768?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Nico/>
|
<img src=https://avatars.githubusercontent.com/u/715768?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Nico/>
|
||||||
@@ -225,6 +216,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Nico</b></sub>
|
<sub style="font-size:14px"><b>Nico</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/evenh>
|
<a href=https://github.com/evenh>
|
||||||
<img src=https://avatars.githubusercontent.com/u/2701536?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Even Holthe/>
|
<img src=https://avatars.githubusercontent.com/u/2701536?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Even Holthe/>
|
||||||
@@ -260,6 +253,13 @@ make build
|
|||||||
<sub style="font-size:14px"><b>unreality</b></sub>
|
<sub style="font-size:14px"><b>unreality</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
|
<a href=https://github.com/mpldr>
|
||||||
|
<img src=https://avatars.githubusercontent.com/u/33086936?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Moritz Poldrack/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Moritz Poldrack</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
@@ -270,10 +270,10 @@ make build
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/mpldr>
|
<a href=https://github.com/restanrm>
|
||||||
<img src=https://avatars.githubusercontent.com/u/33086936?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Moritz Poldrack/>
|
<img src=https://avatars.githubusercontent.com/u/4344371?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Adrien Raffin-Caboisse/>
|
||||||
<br />
|
<br />
|
||||||
<sub style="font-size:14px"><b>Moritz Poldrack</b></sub>
|
<sub style="font-size:14px"><b>Adrien Raffin-Caboisse</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
@@ -350,6 +350,13 @@ make build
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
|
<a href=https://github.com/majst01>
|
||||||
|
<img src=https://avatars.githubusercontent.com/u/410110?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Stefan Majer/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Stefan Majer</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/fdelucchijr>
|
<a href=https://github.com/fdelucchijr>
|
||||||
<img src=https://avatars.githubusercontent.com/u/69133647?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Fernando De Lucchi/>
|
<img src=https://avatars.githubusercontent.com/u/69133647?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Fernando De Lucchi/>
|
||||||
@@ -364,13 +371,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Orville Q. Song</b></sub>
|
<sub style="font-size:14px"><b>Orville Q. Song</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
|
||||||
<a href=https://github.com/majst01>
|
|
||||||
<img src=https://avatars.githubusercontent.com/u/410110?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Stefan Majer/>
|
|
||||||
<br />
|
|
||||||
<sub style="font-size:14px"><b>Stefan Majer</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/hdhoang>
|
<a href=https://github.com/hdhoang>
|
||||||
<img src=https://avatars.githubusercontent.com/u/12537?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=hdhoang/>
|
<img src=https://avatars.githubusercontent.com/u/12537?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=hdhoang/>
|
||||||
@@ -385,6 +385,15 @@ make build
|
|||||||
<sub style="font-size:14px"><b>bravechamp</b></sub>
|
<sub style="font-size:14px"><b>bravechamp</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
|
<a href=https://github.com/bravechamp>
|
||||||
|
<img src=https://avatars.githubusercontent.com/u/48980452?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=bravechamp/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>bravechamp</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/deonthomasgy>
|
<a href=https://github.com/deonthomasgy>
|
||||||
<img src=https://avatars.githubusercontent.com/u/150036?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Deon Thomas/>
|
<img src=https://avatars.githubusercontent.com/u/150036?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Deon Thomas/>
|
||||||
@@ -392,8 +401,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Deon Thomas</b></sub>
|
<sub style="font-size:14px"><b>Deon Thomas</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/madjam002>
|
<a href=https://github.com/madjam002>
|
||||||
<img src=https://avatars.githubusercontent.com/u/679137?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jamie Greeff/>
|
<img src=https://avatars.githubusercontent.com/u/679137?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jamie Greeff/>
|
||||||
@@ -429,6 +436,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Paul Tötterman</b></sub>
|
<sub style="font-size:14px"><b>Paul Tötterman</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/samson4649>
|
<a href=https://github.com/samson4649>
|
||||||
<img src=https://avatars.githubusercontent.com/u/12725953?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Samuel Lock/>
|
<img src=https://avatars.githubusercontent.com/u/12725953?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Samuel Lock/>
|
||||||
@@ -436,8 +445,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Samuel Lock</b></sub>
|
<sub style="font-size:14px"><b>Samuel Lock</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/kevin1sMe>
|
<a href=https://github.com/kevin1sMe>
|
||||||
<img src=https://avatars.githubusercontent.com/u/6886076?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=kevinlin/>
|
<img src=https://avatars.githubusercontent.com/u/6886076?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=kevinlin/>
|
||||||
@@ -473,6 +480,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>dbevacqua</b></sub>
|
<sub style="font-size:14px"><b>dbevacqua</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/joshuataylor>
|
<a href=https://github.com/joshuataylor>
|
||||||
<img src=https://avatars.githubusercontent.com/u/225131?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Josh Taylor/>
|
<img src=https://avatars.githubusercontent.com/u/225131?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Josh Taylor/>
|
||||||
@@ -480,8 +489,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Josh Taylor</b></sub>
|
<sub style="font-size:14px"><b>Josh Taylor</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/CNLHC>
|
<a href=https://github.com/CNLHC>
|
||||||
<img src=https://avatars.githubusercontent.com/u/21005146?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=LiuHanCheng/>
|
<img src=https://avatars.githubusercontent.com/u/21005146?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=LiuHanCheng/>
|
||||||
@@ -517,6 +524,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Steven Honson</b></sub>
|
<sub style="font-size:14px"><b>Steven Honson</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/ratsclub>
|
<a href=https://github.com/ratsclub>
|
||||||
<img src=https://avatars.githubusercontent.com/u/25647735?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Victor Freire/>
|
<img src=https://avatars.githubusercontent.com/u/25647735?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Victor Freire/>
|
||||||
@@ -524,15 +533,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Victor Freire</b></sub>
|
<sub style="font-size:14px"><b>Victor Freire</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
|
||||||
<a href=https://github.com/lachy2849>
|
|
||||||
<img src=https://avatars.githubusercontent.com/u/98844035?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=lachy2849/>
|
|
||||||
<br />
|
|
||||||
<sub style="font-size:14px"><b>lachy2849</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/t56k>
|
<a href=https://github.com/t56k>
|
||||||
<img src=https://avatars.githubusercontent.com/u/12165422?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=thomas/>
|
<img src=https://avatars.githubusercontent.com/u/12165422?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=thomas/>
|
||||||
@@ -540,6 +540,13 @@ make build
|
|||||||
<sub style="font-size:14px"><b>thomas</b></sub>
|
<sub style="font-size:14px"><b>thomas</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
|
<a href=https://github.com/linsomniac>
|
||||||
|
<img src=https://avatars.githubusercontent.com/u/466380?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Sean Reifschneider/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Sean Reifschneider</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/aberoham>
|
<a href=https://github.com/aberoham>
|
||||||
<img src=https://avatars.githubusercontent.com/u/586805?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Abraham Ingersoll/>
|
<img src=https://avatars.githubusercontent.com/u/586805?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Abraham Ingersoll/>
|
||||||
@@ -561,6 +568,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Andrei Pechkurov</b></sub>
|
<sub style="font-size:14px"><b>Andrei Pechkurov</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/theryecatcher>
|
<a href=https://github.com/theryecatcher>
|
||||||
<img src=https://avatars.githubusercontent.com/u/16442416?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Anoop Sundaresh/>
|
<img src=https://avatars.githubusercontent.com/u/16442416?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Anoop Sundaresh/>
|
||||||
@@ -568,8 +577,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Anoop Sundaresh</b></sub>
|
<sub style="font-size:14px"><b>Anoop Sundaresh</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/apognu>
|
<a href=https://github.com/apognu>
|
||||||
<img src=https://avatars.githubusercontent.com/u/3017182?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Antoine POPINEAU/>
|
<img src=https://avatars.githubusercontent.com/u/3017182?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Antoine POPINEAU/>
|
||||||
@@ -577,6 +584,13 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Antoine POPINEAU</b></sub>
|
<sub style="font-size:14px"><b>Antoine POPINEAU</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
|
<a href=https://github.com/tony1661>
|
||||||
|
<img src=https://avatars.githubusercontent.com/u/5287266?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Antonio Fernandez/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Antonio Fernandez</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/aofei>
|
<a href=https://github.com/aofei>
|
||||||
<img src=https://avatars.githubusercontent.com/u/5037285?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Aofei Sheng/>
|
<img src=https://avatars.githubusercontent.com/u/5037285?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Aofei Sheng/>
|
||||||
@@ -591,13 +605,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Arnar</b></sub>
|
<sub style="font-size:14px"><b>Arnar</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
|
||||||
<a href=https://github.com/awoimbee>
|
|
||||||
<img src=https://avatars.githubusercontent.com/u/22431493?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Arthur Woimbée/>
|
|
||||||
<br />
|
|
||||||
<sub style="font-size:14px"><b>Arthur Woimbée</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/avirut>
|
<a href=https://github.com/avirut>
|
||||||
<img src=https://avatars.githubusercontent.com/u/27095602?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Avirut Mehta/>
|
<img src=https://avatars.githubusercontent.com/u/27095602?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Avirut Mehta/>
|
||||||
@@ -605,6 +612,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Avirut Mehta</b></sub>
|
<sub style="font-size:14px"><b>Avirut Mehta</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/stensonb>
|
<a href=https://github.com/stensonb>
|
||||||
<img src=https://avatars.githubusercontent.com/u/933389?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Bryan Stenson/>
|
<img src=https://avatars.githubusercontent.com/u/933389?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Bryan Stenson/>
|
||||||
@@ -612,8 +621,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Bryan Stenson</b></sub>
|
<sub style="font-size:14px"><b>Bryan Stenson</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/yangchuansheng>
|
<a href=https://github.com/yangchuansheng>
|
||||||
<img src=https://avatars.githubusercontent.com/u/15308462?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt= Carson Yang/>
|
<img src=https://avatars.githubusercontent.com/u/15308462?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt= Carson Yang/>
|
||||||
@@ -649,6 +656,15 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Felix Yan</b></sub>
|
<sub style="font-size:14px"><b>Felix Yan</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
|
<a href=https://github.com/gabe565>
|
||||||
|
<img src=https://avatars.githubusercontent.com/u/7717888?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Gabe Cook/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Gabe Cook</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/JJGadgets>
|
<a href=https://github.com/JJGadgets>
|
||||||
<img src=https://avatars.githubusercontent.com/u/5709019?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=JJGadgets/>
|
<img src=https://avatars.githubusercontent.com/u/5709019?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=JJGadgets/>
|
||||||
@@ -656,8 +672,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>JJGadgets</b></sub>
|
<sub style="font-size:14px"><b>JJGadgets</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/hrtkpf>
|
<a href=https://github.com/hrtkpf>
|
||||||
<img src=https://avatars.githubusercontent.com/u/42646788?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=hrtkpf/>
|
<img src=https://avatars.githubusercontent.com/u/42646788?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=hrtkpf/>
|
||||||
@@ -686,6 +700,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>John Axel Eriksson</b></sub>
|
<sub style="font-size:14px"><b>John Axel Eriksson</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/ShadowJonathan>
|
<a href=https://github.com/ShadowJonathan>
|
||||||
<img src=https://avatars.githubusercontent.com/u/22740616?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jonathan de Jong/>
|
<img src=https://avatars.githubusercontent.com/u/22740616?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jonathan de Jong/>
|
||||||
@@ -693,6 +709,20 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Jonathan de Jong</b></sub>
|
<sub style="font-size:14px"><b>Jonathan de Jong</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
|
<a href=https://github.com/JulienFloris>
|
||||||
|
<img src=https://avatars.githubusercontent.com/u/20380255?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Julien Zweverink/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Julien Zweverink</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
|
<a href=https://github.com/win-t>
|
||||||
|
<img src=https://avatars.githubusercontent.com/u/1589120?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Kurnia D Win/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Kurnia D Win</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/foxtrot>
|
<a href=https://github.com/foxtrot>
|
||||||
<img src=https://avatars.githubusercontent.com/u/4153572?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Marc/>
|
<img src=https://avatars.githubusercontent.com/u/4153572?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Marc/>
|
||||||
@@ -700,8 +730,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Marc</b></sub>
|
<sub style="font-size:14px"><b>Marc</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/magf>
|
<a href=https://github.com/magf>
|
||||||
<img src=https://avatars.githubusercontent.com/u/11992737?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Maxim Gajdaj/>
|
<img src=https://avatars.githubusercontent.com/u/11992737?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Maxim Gajdaj/>
|
||||||
@@ -716,6 +744,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Michael Savage</b></sub>
|
<sub style="font-size:14px"><b>Michael Savage</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/piec>
|
<a href=https://github.com/piec>
|
||||||
<img src=https://avatars.githubusercontent.com/u/781471?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Pierre Carru/>
|
<img src=https://avatars.githubusercontent.com/u/781471?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Pierre Carru/>
|
||||||
@@ -744,8 +774,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>rcursaru</b></sub>
|
<sub style="font-size:14px"><b>rcursaru</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/renovate-bot>
|
<a href=https://github.com/renovate-bot>
|
||||||
<img src=https://avatars.githubusercontent.com/u/25180681?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Mend Renovate/>
|
<img src=https://avatars.githubusercontent.com/u/25180681?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Mend Renovate/>
|
||||||
@@ -760,13 +788,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Ryan Fowler</b></sub>
|
<sub style="font-size:14px"><b>Ryan Fowler</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
</tr>
|
||||||
<a href=https://github.com/linsomniac>
|
<tr>
|
||||||
<img src=https://avatars.githubusercontent.com/u/466380?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Sean Reifschneider/>
|
|
||||||
<br />
|
|
||||||
<sub style="font-size:14px"><b>Sean Reifschneider</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/shaananc>
|
<a href=https://github.com/shaananc>
|
||||||
<img src=https://avatars.githubusercontent.com/u/2287839?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Shaanan Cohney/>
|
<img src=https://avatars.githubusercontent.com/u/2287839?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Shaanan Cohney/>
|
||||||
@@ -788,8 +811,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>sophware</b></sub>
|
<sub style="font-size:14px"><b>sophware</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/m-tanner-dev0>
|
<a href=https://github.com/m-tanner-dev0>
|
||||||
<img src=https://avatars.githubusercontent.com/u/97977342?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tanner/>
|
<img src=https://avatars.githubusercontent.com/u/97977342?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tanner/>
|
||||||
@@ -804,6 +825,15 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Teteros</b></sub>
|
<sub style="font-size:14px"><b>Teteros</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
|
<a href=https://github.com/Teteros>
|
||||||
|
<img src=https://avatars.githubusercontent.com/u/5067989?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Teteros/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Teteros</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/gitter-badger>
|
<a href=https://github.com/gitter-badger>
|
||||||
<img src=https://avatars.githubusercontent.com/u/8518239?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=The Gitter Badger/>
|
<img src=https://avatars.githubusercontent.com/u/8518239?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=The Gitter Badger/>
|
||||||
@@ -832,8 +862,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Tjerk Woudsma</b></sub>
|
<sub style="font-size:14px"><b>Tjerk Woudsma</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/y0ngb1n>
|
<a href=https://github.com/y0ngb1n>
|
||||||
<img src=https://avatars.githubusercontent.com/u/25719408?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Yang Bin/>
|
<img src=https://avatars.githubusercontent.com/u/25719408?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Yang Bin/>
|
||||||
@@ -848,6 +876,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Yujie Xia</b></sub>
|
<sub style="font-size:14px"><b>Yujie Xia</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/newellz2>
|
<a href=https://github.com/newellz2>
|
||||||
<img src=https://avatars.githubusercontent.com/u/52436542?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Zachary Newell/>
|
<img src=https://avatars.githubusercontent.com/u/52436542?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Zachary Newell/>
|
||||||
@@ -876,8 +906,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Ziyuan Han</b></sub>
|
<sub style="font-size:14px"><b>Ziyuan Han</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/caelansar>
|
<a href=https://github.com/caelansar>
|
||||||
<img src=https://avatars.githubusercontent.com/u/31852257?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=caelansar/>
|
<img src=https://avatars.githubusercontent.com/u/31852257?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=caelansar/>
|
||||||
@@ -892,6 +920,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>derelm</b></sub>
|
<sub style="font-size:14px"><b>derelm</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/dnaq>
|
<a href=https://github.com/dnaq>
|
||||||
<img src=https://avatars.githubusercontent.com/u/1299717?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=dnaq/>
|
<img src=https://avatars.githubusercontent.com/u/1299717?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=dnaq/>
|
||||||
@@ -920,8 +950,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>jimyag</b></sub>
|
<sub style="font-size:14px"><b>jimyag</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/magichuihui>
|
<a href=https://github.com/magichuihui>
|
||||||
<img src=https://avatars.githubusercontent.com/u/10866198?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=suhelen/>
|
<img src=https://avatars.githubusercontent.com/u/10866198?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=suhelen/>
|
||||||
@@ -936,6 +964,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>sharkonet</b></sub>
|
<sub style="font-size:14px"><b>sharkonet</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/ma6174>
|
<a href=https://github.com/ma6174>
|
||||||
<img src=https://avatars.githubusercontent.com/u/1449133?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ma6174/>
|
<img src=https://avatars.githubusercontent.com/u/1449133?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ma6174/>
|
||||||
@@ -951,10 +981,17 @@ make build
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/pernila>
|
<a href=https://github.com/nicholas-yap>
|
||||||
<img src=https://avatars.githubusercontent.com/u/12460060?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=pernila/>
|
<img src=https://avatars.githubusercontent.com/u/38109533?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=nicholas-yap/>
|
||||||
<br />
|
<br />
|
||||||
<sub style="font-size:14px"><b>pernila</b></sub>
|
<sub style="font-size:14px"><b>nicholas-yap</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
|
<a href=https://github.com/pernila>
|
||||||
|
<img src=https://avatars.githubusercontent.com/u/12460060?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tommi Pernila/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Tommi Pernila</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
@@ -964,8 +1001,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>phpmalik</b></sub>
|
<sub style="font-size:14px"><b>phpmalik</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/Wakeful-Cloud>
|
<a href=https://github.com/Wakeful-Cloud>
|
||||||
<img src=https://avatars.githubusercontent.com/u/38930607?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Wakeful-Cloud/>
|
<img src=https://avatars.githubusercontent.com/u/38930607?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Wakeful-Cloud/>
|
||||||
@@ -973,6 +1008,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Wakeful-Cloud</b></sub>
|
<sub style="font-size:14px"><b>Wakeful-Cloud</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/xpzouying>
|
<a href=https://github.com/xpzouying>
|
||||||
<img src=https://avatars.githubusercontent.com/u/3946563?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=zy/>
|
<img src=https://avatars.githubusercontent.com/u/3946563?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=zy/>
|
||||||
|
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{
|
||||||
|
@@ -1,56 +0,0 @@
|
|||||||
# headscale documentation
|
|
||||||
|
|
||||||
This page contains the official and community contributed documentation for `headscale`.
|
|
||||||
|
|
||||||
If you are having trouble with following the documentation or get unexpected results,
|
|
||||||
please ask on [Discord](https://discord.gg/c84AZQhmpx) instead of opening an Issue.
|
|
||||||
|
|
||||||
## Official documentation
|
|
||||||
|
|
||||||
### How-to
|
|
||||||
|
|
||||||
- [Running headscale on Linux](running-headscale-linux.md)
|
|
||||||
- [Control headscale remotely](remote-cli.md)
|
|
||||||
- [Using a Windows client with headscale](windows-client.md)
|
|
||||||
- [Configuring OIDC](oidc.md)
|
|
||||||
|
|
||||||
### References
|
|
||||||
|
|
||||||
- [Configuration](../config-example.yaml)
|
|
||||||
- [Glossary](glossary.md)
|
|
||||||
- [TLS](tls.md)
|
|
||||||
|
|
||||||
## Community documentation
|
|
||||||
|
|
||||||
Community documentation is not actively maintained by the headscale authors and is
|
|
||||||
written by community members. It is _not_ verified by `headscale` developers.
|
|
||||||
|
|
||||||
**It might be outdated and it might miss necessary steps**.
|
|
||||||
|
|
||||||
- [Running headscale in a container](running-headscale-container.md)
|
|
||||||
- [Running headscale on OpenBSD](running-headscale-openbsd.md)
|
|
||||||
- [Running headscale behind a reverse proxy](reverse-proxy.md)
|
|
||||||
- [Set Custom DNS records](dns-records.md)
|
|
||||||
|
|
||||||
## Misc
|
|
||||||
|
|
||||||
### Policy ACLs
|
|
||||||
|
|
||||||
Headscale implements the same policy ACLs as Tailscale.com, adapted to the self-hosted environment.
|
|
||||||
|
|
||||||
For instance, instead of referring to users when defining groups you must
|
|
||||||
use users (which are the equivalent to user/logins in Tailscale.com).
|
|
||||||
|
|
||||||
Please check https://tailscale.com/kb/1018/acls/, and `./tests/acls/` in this repo for working examples.
|
|
||||||
|
|
||||||
When using ACL's the User borders are no longer applied. All machines
|
|
||||||
whichever the User have the ability to communicate with other hosts as
|
|
||||||
long as the ACL's permits this exchange.
|
|
||||||
|
|
||||||
The [ACLs](acls.md) document should help understand a fictional case of setting
|
|
||||||
up ACLs in a small company. All concepts presented in this document could be
|
|
||||||
applied outside of business oriented usage.
|
|
||||||
|
|
||||||
### Apple devices
|
|
||||||
|
|
||||||
An endpoint with information on how to connect your Apple devices (currently macOS only) is available at `/apple` on your running instance.
|
|
13
docs/acls.md
13
docs/acls.md
@@ -1,4 +1,15 @@
|
|||||||
# ACLs use case example
|
Headscale implements the same policy ACLs as Tailscale.com, adapted to the self-hosted environment.
|
||||||
|
|
||||||
|
For instance, instead of referring to users when defining groups you must
|
||||||
|
use users (which are the equivalent to user/logins in Tailscale.com).
|
||||||
|
|
||||||
|
Please check https://tailscale.com/kb/1018/acls/, and `./tests/acls/` in this repo for working examples.
|
||||||
|
|
||||||
|
When using ACL's the User borders are no longer applied. All machines
|
||||||
|
whichever the User have the ability to communicate with other hosts as
|
||||||
|
long as the ACL's permits this exchange.
|
||||||
|
|
||||||
|
## ACLs use case example
|
||||||
|
|
||||||
Let's build an example use case for a small business (It may be the place where
|
Let's build an example use case for a small business (It may be the place where
|
||||||
ACL's are the most useful).
|
ACL's are the most useful).
|
||||||
|
@@ -1,5 +1,12 @@
|
|||||||
# Setting custom DNS records
|
# Setting custom DNS records
|
||||||
|
|
||||||
|
!!! warning "Community documentation"
|
||||||
|
|
||||||
|
This page is not actively maintained by the headscale authors and is
|
||||||
|
written by community members. It is _not_ verified by `headscale` developers.
|
||||||
|
|
||||||
|
**It might be outdated and it might miss necessary steps**.
|
||||||
|
|
||||||
## Goal
|
## Goal
|
||||||
|
|
||||||
This documentation has the goal of showing how a user can set custom DNS records with `headscale`s magic dns.
|
This documentation has the goal of showing how a user can set custom DNS records with `headscale`s magic dns.
|
||||||
|
@@ -12,6 +12,11 @@ Ensure that the installed version is at least 1.38.1, as that is the first relea
|
|||||||
|
|
||||||
## Configuring the headscale URL
|
## Configuring the headscale URL
|
||||||
|
|
||||||
|
!!! info "Apple devices"
|
||||||
|
|
||||||
|
An endpoint with information on how to connect your Apple devices
|
||||||
|
(currently macOS only) is available at `/apple` on your running instance.
|
||||||
|
|
||||||
Ensure that the tailscale app is logged out before proceeding.
|
Ensure that the tailscale app is logged out before proceeding.
|
||||||
|
|
||||||
Go to iOS settings, scroll down past game center and tv provider to the tailscale app and select it. The headscale URL can be entered into the _"ALTERNATE COORDINATION SERVER URL"_ box.
|
Go to iOS settings, scroll down past game center and tv provider to the tailscale app and select it. The headscale URL can be entered into the _"ALTERNATE COORDINATION SERVER URL"_ box.
|
||||||
|
12
docs/index.md
Normal file
12
docs/index.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
hide:
|
||||||
|
- navigation
|
||||||
|
- toc
|
||||||
|
---
|
||||||
|
|
||||||
|
# headscale documentation
|
||||||
|
|
||||||
|
This site contains the official and community contributed documentation for `headscale`.
|
||||||
|
|
||||||
|
If you are having trouble with following the documentation or get unexpected results,
|
||||||
|
please ask on [Discord](https://discord.gg/c84AZQhmpx) instead of opening an Issue.
|
@@ -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:"
|
||||||
|
@@ -1,5 +1,12 @@
|
|||||||
# Running headscale behind a reverse proxy
|
# Running headscale behind a reverse proxy
|
||||||
|
|
||||||
|
!!! warning "Community documentation"
|
||||||
|
|
||||||
|
This page is not actively maintained by the headscale authors and is
|
||||||
|
written by community members. It is _not_ verified by `headscale` developers.
|
||||||
|
|
||||||
|
**It might be outdated and it might miss necessary steps**.
|
||||||
|
|
||||||
Running headscale behind a reverse proxy is useful when running multiple applications on the same server, and you want to reuse the same external IP and port - usually tcp/443 for HTTPS.
|
Running headscale behind a reverse proxy is useful when running multiple applications on the same server, and you want to reuse the same external IP and port - usually tcp/443 for HTTPS.
|
||||||
|
|
||||||
### WebSockets
|
### WebSockets
|
||||||
|
@@ -1,7 +1,11 @@
|
|||||||
# Running headscale in a container
|
# Running headscale in a container
|
||||||
|
|
||||||
**Note:** the container documentation is maintained by the _community_ and there is no guarentee
|
!!! warning "Community documentation"
|
||||||
it is up to date, or working.
|
|
||||||
|
This page is not actively maintained by the headscale authors and is
|
||||||
|
written by community members. It is _not_ verified by `headscale` developers.
|
||||||
|
|
||||||
|
**It might be outdated and it might miss necessary steps**.
|
||||||
|
|
||||||
## Goal
|
## Goal
|
||||||
|
|
||||||
@@ -24,7 +28,7 @@ cd ./headscale
|
|||||||
touch ./config/db.sqlite
|
touch ./config/db.sqlite
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **(Strongly Recommended)** Download a copy of the [example configuration](../config-example.yaml) from the [headscale repository](https://github.com/juanfont/headscale/).
|
3. **(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.
|
||||||
|
|
||||||
Using wget:
|
Using wget:
|
||||||
|
|
||||||
|
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,84 +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
|
||||||
```
|
```
|
||||||
|
|
||||||
It is **strongly recommended** to copy and modify the [example configuration](../config-example.yaml)
|
6. Check that Headscale is running as intended:
|
||||||
from the [headscale repository](../)
|
|
||||||
|
|
||||||
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
|
||||||
@@ -86,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
|
||||||
@@ -106,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.
|
|
||||||
|
@@ -1,5 +1,12 @@
|
|||||||
# Running headscale on OpenBSD
|
# Running headscale on OpenBSD
|
||||||
|
|
||||||
|
!!! warning "Community documentation"
|
||||||
|
|
||||||
|
This page is not actively maintained by the headscale authors and is
|
||||||
|
written by community members. It is _not_ verified by `headscale` developers.
|
||||||
|
|
||||||
|
**It might be outdated and it might miss necessary steps**.
|
||||||
|
|
||||||
## Goal
|
## Goal
|
||||||
|
|
||||||
This documentation has the goal of showing a user how-to install and run `headscale` on OpenBSD 7.1.
|
This documentation has the goal of showing a user how-to install and run `headscale` on OpenBSD 7.1.
|
||||||
@@ -87,8 +94,7 @@ touch /var/lib/headscale/db.sqlite
|
|||||||
touch /etc/headscale/config.yaml
|
touch /etc/headscale/config.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
It is **strongly recommended** to copy and modify the [example configuration](../config-example.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.
|
||||||
from the [headscale repository](../)
|
|
||||||
|
|
||||||
4. Start the headscale server:
|
4. Start the headscale server:
|
||||||
|
|
||||||
|
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)
|
||||||
|
@@ -33,7 +33,8 @@ var (
|
|||||||
tailscaleVersions2021 = []string{
|
tailscaleVersions2021 = []string{
|
||||||
"head",
|
"head",
|
||||||
"unstable",
|
"unstable",
|
||||||
"1.36.0",
|
"1.38.4",
|
||||||
|
"1.36.2",
|
||||||
"1.34.2",
|
"1.34.2",
|
||||||
"1.32.3",
|
"1.32.3",
|
||||||
"1.30.2",
|
"1.30.2",
|
||||||
@@ -355,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
|
||||||
|
}
|
||||||
|
144
mkdocs.yml
Normal file
144
mkdocs.yml
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
site_name: Headscale
|
||||||
|
site_url: https://juanfont.github.io/headscale
|
||||||
|
site_author: Headscale authors
|
||||||
|
site_description: >-
|
||||||
|
An open source, self-hosted implementation of the Tailscale control server.
|
||||||
|
|
||||||
|
# Repository
|
||||||
|
repo_name: juanfont/headscale
|
||||||
|
repo_url: https://github.com/juanfont/headscale
|
||||||
|
|
||||||
|
# Copyright
|
||||||
|
copyright: Copyright © 2023 Headscale authors
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
theme:
|
||||||
|
name: material
|
||||||
|
features:
|
||||||
|
- announce.dismiss
|
||||||
|
- content.action.edit
|
||||||
|
- content.action.view
|
||||||
|
- content.code.annotate
|
||||||
|
- content.code.copy
|
||||||
|
# - content.tabs.link
|
||||||
|
- content.tooltips
|
||||||
|
# - header.autohide
|
||||||
|
# - navigation.expand
|
||||||
|
- navigation.footer
|
||||||
|
- navigation.indexes
|
||||||
|
# - navigation.instant
|
||||||
|
# - navigation.prune
|
||||||
|
- navigation.sections
|
||||||
|
- navigation.tabs
|
||||||
|
# - navigation.tabs.sticky
|
||||||
|
- navigation.top
|
||||||
|
- navigation.tracking
|
||||||
|
- search.highlight
|
||||||
|
- search.share
|
||||||
|
- search.suggest
|
||||||
|
- toc.follow
|
||||||
|
# - toc.integrate
|
||||||
|
palette:
|
||||||
|
- scheme: default
|
||||||
|
primary: white
|
||||||
|
toggle:
|
||||||
|
icon: material/brightness-7
|
||||||
|
name: Switch to dark mode
|
||||||
|
- scheme: slate
|
||||||
|
toggle:
|
||||||
|
icon: material/brightness-4
|
||||||
|
name: Switch to light mode
|
||||||
|
font:
|
||||||
|
text: Roboto
|
||||||
|
code: Roboto Mono
|
||||||
|
favicon: assets/favicon.png
|
||||||
|
logo: ./logo/headscale3-dots.svg
|
||||||
|
|
||||||
|
# Plugins
|
||||||
|
plugins:
|
||||||
|
- search:
|
||||||
|
separator: '[\s\-,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])'
|
||||||
|
- minify:
|
||||||
|
minify_html: true
|
||||||
|
- social: {}
|
||||||
|
|
||||||
|
# Customization
|
||||||
|
extra:
|
||||||
|
annotate:
|
||||||
|
json: [.s2]
|
||||||
|
social:
|
||||||
|
- icon: fontawesome/brands/github
|
||||||
|
link: https://github.com/juanfont/headscale
|
||||||
|
- icon: material/coffee
|
||||||
|
link: https://ko-fi.com/headscale
|
||||||
|
- icon: fontawesome/brands/docker
|
||||||
|
link: https://github.com/juanfont/headscale/pkgs/container/headscale
|
||||||
|
- icon: fontawesome/brands/discord
|
||||||
|
link: https://discord.gg/c84AZQhmpx
|
||||||
|
|
||||||
|
# Extensions
|
||||||
|
markdown_extensions:
|
||||||
|
- abbr
|
||||||
|
- admonition
|
||||||
|
- attr_list
|
||||||
|
- def_list
|
||||||
|
- footnotes
|
||||||
|
- md_in_html
|
||||||
|
- toc:
|
||||||
|
permalink: true
|
||||||
|
- pymdownx.arithmatex:
|
||||||
|
generic: true
|
||||||
|
- pymdownx.betterem:
|
||||||
|
smart_enable: all
|
||||||
|
- pymdownx.caret
|
||||||
|
- pymdownx.details
|
||||||
|
- pymdownx.emoji:
|
||||||
|
emoji_generator: !!python/name:materialx.emoji.to_svg
|
||||||
|
emoji_index: !!python/name:materialx.emoji.twemoji
|
||||||
|
- pymdownx.highlight:
|
||||||
|
anchor_linenums: true
|
||||||
|
line_spans: __span
|
||||||
|
pygments_lang_class: true
|
||||||
|
- pymdownx.inlinehilite
|
||||||
|
- pymdownx.keys
|
||||||
|
- pymdownx.magiclink:
|
||||||
|
repo_url_shorthand: true
|
||||||
|
user: squidfunk
|
||||||
|
repo: mkdocs-material
|
||||||
|
- pymdownx.mark
|
||||||
|
- pymdownx.smartsymbols
|
||||||
|
- pymdownx.superfences:
|
||||||
|
custom_fences:
|
||||||
|
- name: mermaid
|
||||||
|
class: mermaid
|
||||||
|
format: !!python/name:pymdownx.superfences.fence_code_format
|
||||||
|
- pymdownx.tabbed:
|
||||||
|
alternate_style: true
|
||||||
|
- pymdownx.tasklist:
|
||||||
|
custom_checkbox: true
|
||||||
|
- pymdownx.tilde
|
||||||
|
|
||||||
|
# Page tree
|
||||||
|
nav:
|
||||||
|
- Home: index.md
|
||||||
|
- Getting started:
|
||||||
|
- Installation:
|
||||||
|
- Linux: running-headscale-linux.md
|
||||||
|
- OpenBSD: running-headscale-openbsd.md
|
||||||
|
- Container: running-headscale-container.md
|
||||||
|
- Configuration:
|
||||||
|
- OIDC authentication: oidc.md
|
||||||
|
- Exit node: exit-node.md
|
||||||
|
- Reverse proxy: reverse-proxy.md
|
||||||
|
- TLS: tls.md
|
||||||
|
- ACLs: acls.md
|
||||||
|
- Custom DNS records: dns-records.md
|
||||||
|
- Remote CLI: remote-cli.md
|
||||||
|
- Usage:
|
||||||
|
- Android: android-client.md
|
||||||
|
- Windows: windows-client.md
|
||||||
|
- iOS: iOS-client.md
|
||||||
|
- Proposals:
|
||||||
|
- ACLs: proposals/001-acls.md
|
||||||
|
- Better routing: proposals/002-better-routing.md
|
||||||
|
- Glossary: glossary.md
|
Reference in New Issue
Block a user