mirror of
https://github.com/juanfont/headscale.git
synced 2025-08-20 12:37:27 +00:00
Compare commits
241 Commits
v0.15.0-be
...
fix-spurio
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0d98493360 | ||
![]() |
03659c4175 | ||
![]() |
843e2bd9b6 | ||
![]() |
28efd92fca | ||
![]() |
7bb87a7300 | ||
![]() |
db8db0299e | ||
![]() |
e80954b6c8 | ||
![]() |
a23035aee7 | ||
![]() |
e51e6f487f | ||
![]() |
f78deaebb6 | ||
![]() |
4d2949bda9 | ||
![]() |
cb0899b534 | ||
![]() |
ecf5259693 | ||
![]() |
970dea5d68 | ||
![]() |
cd9807a1d3 | ||
![]() |
613dc61339 | ||
![]() |
3023323528 | ||
![]() |
2dfd8a9098 | ||
![]() |
c8ed1f0f43 | ||
![]() |
f9e2ce2c8c | ||
![]() |
886e95c00d | ||
![]() |
6dd9e93346 | ||
![]() |
2dacf839dc | ||
![]() |
8f6952acee | ||
![]() |
235a90276f | ||
![]() |
5c285afda5 | ||
![]() |
db930af50e | ||
![]() |
ffa570e877 | ||
![]() |
96ae78f422 | ||
![]() |
580c72bf16 | ||
![]() |
9254afff2d | ||
![]() |
7ce0bd053c | ||
![]() |
41a8c14acb | ||
![]() |
be2487f4c0 | ||
![]() |
dd3f24b83f | ||
![]() |
bc63c577a9 | ||
![]() |
57c81e4153 | ||
![]() |
556ca5fec7 | ||
![]() |
93682ab708 | ||
![]() |
6eeee8e5c7 | ||
![]() |
d195847d8f | ||
![]() |
3d8dc9d2bf | ||
![]() |
8601dd1f42 | ||
![]() |
3abdc870d8 | ||
![]() |
367f8489db | ||
![]() |
c312f8bf4a | ||
![]() |
1f43c39f93 | ||
![]() |
9f03a012fb | ||
![]() |
22dd61d849 | ||
![]() |
a92f6abc6e | ||
![]() |
9cdaa9730b | ||
![]() |
5d67ed0ce1 | ||
![]() |
62d774b6ee | ||
![]() |
a14f50eeca | ||
![]() |
98e98a8adb | ||
![]() |
fa7ef3df2f | ||
![]() |
c3324371d6 | ||
![]() |
6e08241712 | ||
![]() |
c07dd3f14f | ||
![]() |
b2ae9b6cac | ||
![]() |
57536b020e | ||
![]() |
0003e30084 | ||
![]() |
23be13b113 | ||
![]() |
5e44266292 | ||
![]() |
32522cb482 | ||
![]() |
6d296a195d | ||
![]() |
3272febfb3 | ||
![]() |
7dae780be1 | ||
![]() |
73f1c06f65 | ||
![]() |
b60727b205 | ||
![]() |
8cee31d8d7 | ||
![]() |
b5aace6d3a | ||
![]() |
7e286c570e | ||
![]() |
52fd13bfc4 | ||
![]() |
b8e4aeede8 | ||
![]() |
9a632c17d1 | ||
![]() |
8758ee1c4d | ||
![]() |
150ae1846a | ||
![]() |
452286552c | ||
![]() |
631cf58ff0 | ||
![]() |
8a2c0e88f4 | ||
![]() |
af6a47fdd3 | ||
![]() |
94d910557f | ||
![]() |
a8a683d3cc | ||
![]() |
a1caa5b45c | ||
![]() |
f42868f67f | ||
![]() |
a6455653c0 | ||
![]() |
91e5cbd793 | ||
![]() |
c8503075e0 | ||
![]() |
4068a7b00b | ||
![]() |
daae2fe549 | ||
![]() |
739653fa71 | ||
![]() |
304109a6c5 | ||
![]() |
c29af96a19 | ||
![]() |
d21e9d29d1 | ||
![]() |
b65bd5baa8 | ||
![]() |
0165b89941 | ||
![]() |
53b62f3f39 | ||
![]() |
cd2914ab3b | ||
![]() |
e85b97143c | ||
![]() |
1eafe960b8 | ||
![]() |
749c92954c | ||
![]() |
db9ba17920 | ||
![]() |
d5ce7d7523 | ||
![]() |
2e6687209b | ||
![]() |
2e04abf4bb | ||
![]() |
882c0c34c1 | ||
![]() |
61ebb713f2 | ||
![]() |
ac5ad42474 | ||
![]() |
9d4822b8c7 | ||
![]() |
466d03d574 | ||
![]() |
d43fec7f96 | ||
![]() |
62f4c205f5 | ||
![]() |
003c19004d | ||
![]() |
70274d528c | ||
![]() |
6d41279781 | ||
![]() |
b781446e86 | ||
![]() |
1c9b1c0579 | ||
![]() |
ade9552736 | ||
![]() |
68403cb76e | ||
![]() |
537ecb8db0 | ||
![]() |
8f5875efe4 | ||
![]() |
98ac88d5ef | ||
![]() |
d13338a9fb | ||
![]() |
1579ffb66a | ||
![]() |
0bfa5302a7 | ||
![]() |
b8aad5451d | ||
![]() |
61440c42d3 | ||
![]() |
18ee6274e1 | ||
![]() |
0abfbdc18a | ||
![]() |
082a852c5e | ||
![]() |
af081e9fd3 | ||
![]() |
8b5e8b7dfc | ||
![]() |
1e7d7e510e | ||
![]() |
a806694d23 | ||
![]() |
62d7fae056 | ||
![]() |
06d85688fd | ||
![]() |
dd219d0ff6 | ||
![]() |
6087e1cf6f | ||
![]() |
c47fb1ae54 | ||
![]() |
48cec3cd90 | ||
![]() |
e54c508c10 | ||
![]() |
941e9d9b0f | ||
![]() |
11ccae8e52 | ||
![]() |
b803240dc1 | ||
![]() |
bdbf620ece | ||
![]() |
e5d22b8a70 | ||
![]() |
05c5e2280b | ||
![]() |
b41d89946a | ||
![]() |
cc0c88a63a | ||
![]() |
c06689dec1 | ||
![]() |
b85dd7abbd | ||
![]() |
6aeaff43aa | ||
![]() |
dd26cbd193 | ||
![]() |
9a60eeaf86 | ||
![]() |
b0ae3240fd | ||
![]() |
41efe98953 | ||
![]() |
2b68c90778 | ||
![]() |
f19c048569 | ||
![]() |
6cc8bbc24f | ||
![]() |
c24de595f6 | ||
![]() |
63641a7b17 | ||
![]() |
a6570d33a6 | ||
![]() |
124d8a3424 | ||
![]() |
5de9de14a9 | ||
![]() |
15f8cb5034 | ||
![]() |
03452a8dca | ||
![]() |
15ed71315c | ||
![]() |
05df8e947a | ||
![]() |
b3fa66dbd2 | ||
![]() |
a27b386123 | ||
![]() |
580db9b58f | ||
![]() |
1114449601 | ||
![]() |
b47de07eea | ||
![]() |
e1fcf0da26 | ||
![]() |
dcf3ea567c | ||
![]() |
de2ea83b3b | ||
![]() |
eb06054a7b | ||
![]() |
eb500155e8 | ||
![]() |
dc909ba6d7 | ||
![]() |
70910c4595 | ||
![]() |
54c3e00a1f | ||
![]() |
e78c002f5a | ||
![]() |
237f7f1027 | ||
![]() |
992efbd84a | ||
![]() |
e9eb90fa76 | ||
![]() |
88378c22fb | ||
![]() |
b742379627 | ||
![]() |
df37d1a639 | ||
![]() |
758b1ba1cb | ||
![]() |
435ee36d78 | ||
![]() |
35efd8f95a | ||
![]() |
09d78c7a05 | ||
![]() |
60655c5242 | ||
![]() |
22d2443281 | ||
![]() |
a70669fca7 | ||
![]() |
0720473033 | ||
![]() |
e799307e74 | ||
![]() |
575f33d183 | ||
![]() |
607c1eb316 | ||
![]() |
d69dada8ff | ||
![]() |
f9e0c13890 | ||
![]() |
12a50ac8ac | ||
![]() |
b342cf0240 | ||
![]() |
e3ff87b7ef | ||
![]() |
745696b310 | ||
![]() |
23cde8445f | ||
![]() |
9d43f589ae | ||
![]() |
897d480f4d | ||
![]() |
6f172a6e4c | ||
![]() |
44a5372c53 | ||
![]() |
f2ea6fb30f | ||
![]() |
4a4952899b | ||
![]() |
b72a8aa7d1 | ||
![]() |
e301d0d1df | ||
![]() |
75ca91b0f7 | ||
![]() |
e208ccc982 | ||
![]() |
71a62697aa | ||
![]() |
f9c0597875 | ||
![]() |
aa3eb5171a | ||
![]() |
dcc46af8de | ||
![]() |
b61500670c | ||
![]() |
9a61725e9f | ||
![]() |
6126d6d9b5 | ||
![]() |
06e6c29a5b | ||
![]() |
a9122c3de3 | ||
![]() |
d55c79e75b | ||
![]() |
d27f2bc538 | ||
![]() |
e3bcc88880 | ||
![]() |
14e49885fb | ||
![]() |
fbc1843889 | ||
![]() |
45d5ab30ff | ||
![]() |
d5fd7a5c00 | ||
![]() |
397b6fc4bf | ||
![]() |
55d746d3f5 | ||
![]() |
c364c2a382 | ||
![]() |
e540679dbd | ||
![]() |
86b329d8bf | ||
![]() |
7bdd7748e4 | ||
![]() |
0426212348 | ||
![]() |
85cf443ac6 |
@@ -3,6 +3,7 @@
|
||||
// development
|
||||
integration_test.go
|
||||
integration_test/
|
||||
!integration_test/etc_embedded_derp/tls/server.crt
|
||||
|
||||
Dockerfile*
|
||||
docker-compose*
|
||||
|
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -6,6 +6,8 @@ labels: ["bug"]
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
<!-- Headscale is a multinational community across the globe. Our common language is English. Please consider raising the bug report in this language. -->
|
||||
|
||||
**Bug description**
|
||||
|
||||
<!-- A clear and concise description of what the bug is. Describe the expected bahavior
|
||||
|
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -7,5 +7,5 @@ contact_links:
|
||||
url: "https://github.com/juanfont/headscale/blob/main/docs"
|
||||
about: "Find documentation about how to configure and run headscale."
|
||||
- name: "headscale Discord community"
|
||||
url: "https://discord.com/invite/XcQxk2VHjx"
|
||||
url: "https://discord.gg/xGj2TuqyxY"
|
||||
about: "Please ask and answer questions about usage of headscale here."
|
||||
|
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -6,6 +6,8 @@ labels: ["enhancement"]
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
<!-- Headscale is a multinational community across the globe. Our common language is English. Please consider raising the feature request in this language. -->
|
||||
|
||||
**Feature request**
|
||||
|
||||
<!-- A clear and precise description of what new or changed feature you want. -->
|
||||
|
2
.github/ISSUE_TEMPLATE/other_issue.md
vendored
2
.github/ISSUE_TEMPLATE/other_issue.md
vendored
@@ -6,6 +6,8 @@ labels: ["bug"]
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
<!-- Headscale is a multinational community across the globe. Our common language is English. Please consider raising the issue in this language. -->
|
||||
|
||||
<!-- If you have a question, please consider using our Discord for asking questions -->
|
||||
|
||||
**Issue description**
|
||||
|
12
.github/pull_request_template.md
vendored
12
.github/pull_request_template.md
vendored
@@ -1,10 +1,10 @@
|
||||
<!-- Please tick if the following things apply. You… -->
|
||||
|
||||
- [] read the [CONTRIBUTING guidelines](README.md#user-content-contributing)
|
||||
- [] raised a GitHub issue or discussed it on the projects chat beforehand
|
||||
- [] added unit tests
|
||||
- [] added integration tests
|
||||
- [] updated documentation if needed
|
||||
- [] updated CHANGELOG.md
|
||||
- [ ] read the [CONTRIBUTING guidelines](README.md#user-content-contributing)
|
||||
- [ ] raised a GitHub issue or discussed it on the projects chat beforehand
|
||||
- [ ] added unit tests
|
||||
- [ ] added integration tests
|
||||
- [ ] updated documentation if needed
|
||||
- [ ] updated CHANGELOG.md
|
||||
|
||||
<!-- If applicable, please reference the issue using `Fixes #XXX` and add tests to cover your new code. -->
|
||||
|
17
.github/workflows/build.yml
vendored
17
.github/workflows/build.yml
vendored
@@ -22,30 +22,21 @@ jobs:
|
||||
uses: tj-actions/changed-files@v14.1
|
||||
with:
|
||||
files: |
|
||||
*.nix
|
||||
go.*
|
||||
**/*.go
|
||||
integration_test/
|
||||
config-example.yaml
|
||||
|
||||
- name: Setup Go
|
||||
- uses: cachix/install-nix-action@v16
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: "1.17.7"
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: |
|
||||
go version
|
||||
sudo apt update
|
||||
sudo apt install -y make
|
||||
|
||||
- name: Run build
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: make build
|
||||
run: nix build
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
with:
|
||||
name: headscale-linux
|
||||
path: headscale
|
||||
path: result/bin/headscale
|
||||
|
4
.github/workflows/contributors.yml
vendored
4
.github/workflows/contributors.yml
vendored
@@ -17,6 +17,10 @@ jobs:
|
||||
run: git push origin --delete update-contributors
|
||||
- name: Create up-to-date contributors branch
|
||||
run: git checkout -B update-contributors
|
||||
- name: Push empty contributors branch
|
||||
run: git push origin update-contributors
|
||||
- name: Switch back to main
|
||||
run: git checkout main
|
||||
- uses: BobAnkh/add-contributors@v0.2.2
|
||||
with:
|
||||
CONTRIBUTOR: "## Contributors"
|
||||
|
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -16,6 +16,7 @@ jobs:
|
||||
uses: tj-actions/changed-files@v14.1
|
||||
with:
|
||||
files: |
|
||||
*.nix
|
||||
go.*
|
||||
**/*.go
|
||||
integration_test/
|
||||
@@ -45,6 +46,7 @@ jobs:
|
||||
uses: tj-actions/changed-files@v14.1
|
||||
with:
|
||||
files: |
|
||||
*.nix
|
||||
**/*.md
|
||||
**/*.yml
|
||||
**/*.yaml
|
||||
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.17.7
|
||||
go-version: 1.18.0
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
|
8
.github/workflows/test-integration.yml
vendored
8
.github/workflows/test-integration.yml
vendored
@@ -16,17 +16,15 @@ jobs:
|
||||
uses: tj-actions/changed-files@v14.1
|
||||
with:
|
||||
files: |
|
||||
*.nix
|
||||
go.*
|
||||
**/*.go
|
||||
integration_test/
|
||||
config-example.yaml
|
||||
|
||||
- name: Setup Go
|
||||
- uses: cachix/install-nix-action@v16
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: "1.17.7"
|
||||
|
||||
- name: Run Integration tests
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: make test_integration
|
||||
run: nix develop --command -- make test_integration
|
||||
|
19
.github/workflows/test.yml
vendored
19
.github/workflows/test.yml
vendored
@@ -16,28 +16,15 @@ jobs:
|
||||
uses: tj-actions/changed-files@v14.1
|
||||
with:
|
||||
files: |
|
||||
*.nix
|
||||
go.*
|
||||
**/*.go
|
||||
integration_test/
|
||||
config-example.yaml
|
||||
|
||||
- name: Setup Go
|
||||
- uses: cachix/install-nix-action@v16
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: "1.17.7"
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: |
|
||||
go version
|
||||
sudo apt update
|
||||
sudo apt install -y make
|
||||
|
||||
- name: Run tests
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: make test
|
||||
|
||||
- name: Run build
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: make
|
||||
run: nix develop --check
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@@ -27,3 +27,7 @@ derp.yaml
|
||||
.idea
|
||||
|
||||
test_output/
|
||||
|
||||
# Nix build output
|
||||
result
|
||||
.direnv/
|
||||
|
20
CHANGELOG.md
20
CHANGELOG.md
@@ -1,6 +1,15 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 0.15.0 (2022-xx-xx)
|
||||
## 0.16.0 (2022-xx-xx)
|
||||
|
||||
### Changes
|
||||
|
||||
- Headscale fails to serve if the ACL policy file cannot be parsed [#537](https://github.com/juanfont/headscale/pull/537)
|
||||
- Fix labels cardinality error when registering unknown pre-auth key [#519](https://github.com/juanfont/headscale/pull/519)
|
||||
- Fix send on closed channel crash in polling [#542](https://github.com/juanfont/headscale/pull/542)
|
||||
- Fixed spurious calls to setLastStateChangeToNow from ephemeral nodes [#566](https://github.com/juanfont/headscale/pull/566)
|
||||
|
||||
## 0.15.0 (2022-03-20)
|
||||
|
||||
**Note:** Take a backup of your database before upgrading.
|
||||
|
||||
@@ -8,12 +17,18 @@
|
||||
|
||||
- Boundaries between Namespaces has been removed and all nodes can communicate by default [#357](https://github.com/juanfont/headscale/pull/357)
|
||||
- To limit access between nodes, use [ACLs](./docs/acls.md).
|
||||
- `/metrics` is now a configurable host:port endpoint: [#344](https://github.com/juanfont/headscale/pull/344). You must update your `config.yaml` file to include:
|
||||
```yaml
|
||||
metrics_listen_addr: 127.0.0.1:9090
|
||||
```
|
||||
|
||||
### Features
|
||||
|
||||
- Add support for writing ACL files with YAML [#359](https://github.com/juanfont/headscale/pull/359)
|
||||
- Users can now use emails in ACL's groups [#372](https://github.com/juanfont/headscale/issues/372)
|
||||
- Add shorthand aliases for commands and subcommands [#376](https://github.com/juanfont/headscale/pull/376)
|
||||
- Add `/windows` endpoint for Windows configuration instructions + registry file download [#392](https://github.com/juanfont/headscale/pull/392)
|
||||
- Added embedded DERP (and STUN) server into Headscale [#388](https://github.com/juanfont/headscale/pull/388)
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -22,6 +37,9 @@
|
||||
- Nodes are now only written to database if they are registrated successfully
|
||||
- Fix a limitation in the ACLs that prevented users to write rules with `*` as source [#374](https://github.com/juanfont/headscale/issues/374)
|
||||
- Reduce the overhead of marshal/unmarshal for Hostinfo, routes and endpoints by using specific types in Machine [#371](https://github.com/juanfont/headscale/pull/371)
|
||||
- Apply normalization function to FQDN on hostnames when hosts registers and retrieve informations [#363](https://github.com/juanfont/headscale/issues/363)
|
||||
- Fix a bug that prevented the use of `tailscale logout` with OIDC [#508](https://github.com/juanfont/headscale/issues/508)
|
||||
- Added Tailscale repo HEAD and unstable releases channel to the integration tests targets [#513](https://github.com/juanfont/headscale/pull/513)
|
||||
|
||||
## 0.14.0 (2022-02-24)
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
# Builder image
|
||||
FROM docker.io/golang:1.17.7-bullseye AS build
|
||||
FROM docker.io/golang:1.18.0-bullseye AS build
|
||||
ENV GOPATH /go
|
||||
WORKDIR /go/src/headscale
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
# Builder image
|
||||
FROM docker.io/golang:1.17.7-alpine AS build
|
||||
FROM docker.io/golang:1.18.0-alpine AS build
|
||||
ENV GOPATH /go
|
||||
WORKDIR /go/src/headscale
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
# Builder image
|
||||
FROM docker.io/golang:1.17.7-bullseye AS build
|
||||
FROM docker.io/golang:1.18.0-bullseye AS build
|
||||
ENV GOPATH /go
|
||||
WORKDIR /go/src/headscale
|
||||
|
||||
|
@@ -1,11 +1,17 @@
|
||||
FROM ubuntu:latest
|
||||
|
||||
ARG TAILSCALE_VERSION
|
||||
ARG TAILSCALE_VERSION=*
|
||||
ARG TAILSCALE_CHANNEL=stable
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y gnupg curl \
|
||||
&& curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/focal.gpg | apt-key add - \
|
||||
&& curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/focal.list | tee /etc/apt/sources.list.d/tailscale.list \
|
||||
&& curl -fsSL https://pkgs.tailscale.com/${TAILSCALE_CHANNEL}/ubuntu/focal.gpg | apt-key add - \
|
||||
&& curl -fsSL https://pkgs.tailscale.com/${TAILSCALE_CHANNEL}/ubuntu/focal.list | tee /etc/apt/sources.list.d/tailscale.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y tailscale=${TAILSCALE_VERSION} dnsutils \
|
||||
&& apt-get install -y ca-certificates tailscale=${TAILSCALE_VERSION} dnsutils \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ADD integration_test/etc_embedded_derp/tls/server.crt /usr/local/share/ca-certificates/
|
||||
RUN chmod 644 /usr/local/share/ca-certificates/server.crt
|
||||
|
||||
RUN update-ca-certificates
|
||||
|
21
Dockerfile.tailscale-HEAD
Normal file
21
Dockerfile.tailscale-HEAD
Normal file
@@ -0,0 +1,21 @@
|
||||
FROM golang:latest
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y ca-certificates dnsutils git iptables \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
RUN git clone https://github.com/tailscale/tailscale.git
|
||||
|
||||
WORKDIR tailscale
|
||||
|
||||
RUN sh build_dist.sh tailscale.com/cmd/tailscale
|
||||
RUN sh build_dist.sh tailscale.com/cmd/tailscaled
|
||||
|
||||
RUN cp tailscale /usr/local/bin/
|
||||
RUN cp tailscaled /usr/local/bin/
|
||||
|
||||
ADD integration_test/etc_embedded_derp/tls/server.crt /usr/local/share/ca-certificates/
|
||||
RUN chmod 644 /usr/local/share/ca-certificates/server.crt
|
||||
|
||||
RUN update-ca-certificates
|
11
Makefile
11
Makefile
@@ -1,5 +1,5 @@
|
||||
# Calculate version
|
||||
version = $(shell ./scripts/version-at-commit.sh)
|
||||
version = $(git describe --always --tags --dirty)
|
||||
|
||||
rwildcard=$(foreach d,$(wildcard $1*),$(call rwildcard,$d/,$2) $(filter $(subst *,%,$2),$d))
|
||||
|
||||
@@ -10,7 +10,7 @@ PROTO_SOURCES = $(call rwildcard,,*.proto)
|
||||
|
||||
|
||||
build:
|
||||
GGO_ENABLED=0 go build -ldflags "-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$(version)" cmd/headscale/headscale.go
|
||||
CGO_ENABLED=0 go build -trimpath -buildmode=pie -mod=readonly -ldflags "-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$(version)" cmd/headscale/headscale.go
|
||||
|
||||
dev: lint test build
|
||||
|
||||
@@ -23,6 +23,9 @@ test_integration:
|
||||
test_integration_cli:
|
||||
go test -tags integration -v integration_cli_test.go integration_common_test.go
|
||||
|
||||
test_integration_derp:
|
||||
go test -tags integration -v integration_embedded_derp_test.go integration_common_test.go
|
||||
|
||||
coverprofile_func:
|
||||
go tool cover -func=coverage.out
|
||||
|
||||
@@ -38,14 +41,14 @@ fmt:
|
||||
clang-format -style="{BasedOnStyle: Google, IndentWidth: 4, AlignConsecutiveDeclarations: true, AlignConsecutiveAssignments: true, ColumnLimit: 0}" -i $(PROTO_SOURCES)
|
||||
|
||||
proto-lint:
|
||||
cd proto/ && buf lint
|
||||
cd proto/ && go run github.com/bufbuild/buf/cmd/buf lint
|
||||
|
||||
compress: build
|
||||
upx --brute headscale
|
||||
|
||||
generate:
|
||||
rm -rf gen
|
||||
buf generate proto
|
||||
go run github.com/bufbuild/buf/cmd/buf generate proto
|
||||
|
||||
install-protobuf-plugins:
|
||||
go install \
|
||||
|
148
README.md
148
README.md
@@ -4,7 +4,7 @@
|
||||
|
||||
An open source, self-hosted implementation of the Tailscale control server.
|
||||
|
||||
Join our [Discord](https://discord.gg/XcQxk2VHjx) server for a chat.
|
||||
Join our [Discord](https://discord.gg/c84AZQhmpx) server for a chat.
|
||||
|
||||
**Note:** Always select the same GitHub tag as the released version you use
|
||||
to ensure you have the correct example configuration and documentation.
|
||||
@@ -63,6 +63,7 @@ one of the maintainers.
|
||||
- Dual stack (IPv4 and IPv6)
|
||||
- Routing advertising (including exit nodes)
|
||||
- Ephemeral nodes
|
||||
- Embedded [DERP server](https://tailscale.com/blog/how-tailscale-works/#encrypted-tcp-relays-derp)
|
||||
|
||||
## Client OS support
|
||||
|
||||
@@ -90,6 +91,10 @@ Please have a look at the documentation under [`docs/`](docs/).
|
||||
To contribute to headscale you would need the lastest version of [Go](https://golang.org)
|
||||
and [Buf](https://buf.build)(Protobuf generator).
|
||||
|
||||
We recommend using [Nix](https://nixos.org/) to setup a development environment. This can
|
||||
be done with `nix develop`, which will install the tools and give you a shell.
|
||||
This guarantees that you will have the same dev env as `headscale` maintainers.
|
||||
|
||||
PRs and suggestions are welcome.
|
||||
|
||||
### Code style
|
||||
@@ -114,10 +119,12 @@ Check out the `.golangci.yaml` and `Makefile` to see the specific configuration.
|
||||
|
||||
- Go
|
||||
- Buf
|
||||
- Protobuf tools:
|
||||
- Protobuf tools
|
||||
|
||||
Install and activate:
|
||||
|
||||
```shell
|
||||
make install-protobuf-plugins
|
||||
nix develop
|
||||
```
|
||||
|
||||
### Testing and building
|
||||
@@ -139,6 +146,12 @@ make test
|
||||
|
||||
To build the program:
|
||||
|
||||
```shell
|
||||
nix build
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```shell
|
||||
make build
|
||||
```
|
||||
@@ -161,6 +174,13 @@ make build
|
||||
<sub style="font-size:14px"><b>Juan Font</b></sub>
|
||||
</a>
|
||||
</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">
|
||||
<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/>
|
||||
@@ -176,10 +196,19 @@ make build
|
||||
</a>
|
||||
</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/>
|
||||
<a href=https://github.com/e-zk>
|
||||
<img src=https://avatars.githubusercontent.com/u/58356365?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=e-zk/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Adrien Raffin-Caboisse</b></sub>
|
||||
<sub style="font-size:14px"><b>e-zk</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/arch4ngel>
|
||||
<img src=https://avatars.githubusercontent.com/u/11574161?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Justin Angel/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Justin Angel</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
@@ -189,8 +218,13 @@ make build
|
||||
<sub style="font-size:14px"><b>Alessandro (Ale) Segala</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/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/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Nico</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/unreality>
|
||||
<img src=https://avatars.githubusercontent.com/u/352522?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=unreality/>
|
||||
@@ -198,6 +232,22 @@ make build
|
||||
<sub style="font-size:14px"><b>unreality</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/mpldr>
|
||||
<img src=https://avatars.githubusercontent.com/u/33086936?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Moritz Poldrack/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Moritz Poldrack</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/Niek>
|
||||
<img src=https://avatars.githubusercontent.com/u/213140?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Niek van der Maas/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Niek van der Maas</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/negbie>
|
||||
<img src=https://avatars.githubusercontent.com/u/20154956?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Eugen Biegler/>
|
||||
@@ -226,6 +276,13 @@ make build
|
||||
<sub style="font-size:14px"><b>Hoàng Đức Hiếu</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/mevansam>
|
||||
<img src=https://avatars.githubusercontent.com/u/403630?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Mevan Samaratunga/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Mevan Samaratunga</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/dragetd>
|
||||
<img src=https://avatars.githubusercontent.com/u/3639577?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Michael G./>
|
||||
@@ -242,6 +299,13 @@ make build
|
||||
<sub style="font-size:14px"><b>Paul Tötterman</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/artemklevtsov>
|
||||
<img src=https://avatars.githubusercontent.com/u/603798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Artem Klevtsov/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Artem Klevtsov</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/cmars>
|
||||
<img src=https://avatars.githubusercontent.com/u/23741?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Casey Marshall/>
|
||||
@@ -264,12 +328,14 @@ make build
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/lachy-2849>
|
||||
<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=lachy-2849/>
|
||||
<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>lachy-2849</b></sub>
|
||||
<sub style="font-size:14px"><b>lachy2849</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/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/>
|
||||
@@ -277,8 +343,6 @@ make build
|
||||
<sub style="font-size:14px"><b>thomas</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/aberoham>
|
||||
<img src=https://avatars.githubusercontent.com/u/586805?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Abraham Ingersoll/>
|
||||
@@ -287,10 +351,10 @@ make build
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/artemklevtsov>
|
||||
<img src=https://avatars.githubusercontent.com/u/603798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Artem Klevtsov/>
|
||||
<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/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Artem Klevtsov</b></sub>
|
||||
<sub style="font-size:14px"><b>Aofei Sheng</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
@@ -307,6 +371,15 @@ make build
|
||||
<sub style="font-size:14px"><b>Bryan Stenson</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/yangchuansheng>
|
||||
<img src=https://avatars.githubusercontent.com/u/15308462?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt= Carson Yang/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b> Carson Yang</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/fkr>
|
||||
<img src=https://avatars.githubusercontent.com/u/51063?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Felix Kronlage-Dammers/>
|
||||
@@ -321,8 +394,6 @@ make build
|
||||
<sub style="font-size:14px"><b>Felix Yan</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/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/>
|
||||
@@ -351,6 +422,8 @@ make build
|
||||
<sub style="font-size:14px"><b>Pierre Carru</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/rcursaru>
|
||||
<img src=https://avatars.githubusercontent.com/u/16259641?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=rcursaru/>
|
||||
@@ -358,6 +431,13 @@ make build
|
||||
<sub style="font-size:14px"><b>rcursaru</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/renovate-bot>
|
||||
<img src=https://avatars.githubusercontent.com/u/25180681?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=WhiteSource Renovate/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>WhiteSource Renovate</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/ryanfowler>
|
||||
<img src=https://avatars.githubusercontent.com/u/2668821?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Ryan Fowler/>
|
||||
@@ -365,8 +445,6 @@ make build
|
||||
<sub style="font-size:14px"><b>Ryan Fowler</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/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/>
|
||||
@@ -388,6 +466,8 @@ make build
|
||||
<sub style="font-size:14px"><b>Teteros</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/gitter-badger>
|
||||
<img src=https://avatars.githubusercontent.com/u/8518239?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=The Gitter Badger/>
|
||||
@@ -409,8 +489,13 @@ make build
|
||||
<sub style="font-size:14px"><b>Tjerk Woudsma</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/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/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Yang Bin</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/zekker6>
|
||||
<img src=https://avatars.githubusercontent.com/u/1367798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Zakhar Bessarab/>
|
||||
@@ -425,6 +510,15 @@ make build
|
||||
<sub style="font-size:14px"><b>ZiYuan</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/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>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/derelm>
|
||||
<img src=https://avatars.githubusercontent.com/u/465155?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=derelm/>
|
||||
@@ -433,10 +527,10 @@ make build
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/e-zk>
|
||||
<img src=https://avatars.githubusercontent.com/u/58356365?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=e-zk/>
|
||||
<a href=https://github.com/nning>
|
||||
<img src=https://avatars.githubusercontent.com/u/557430?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=henning mueller/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>e-zk</b></sub>
|
||||
<sub style="font-size:14px"><b>henning mueller</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
@@ -453,8 +547,6 @@ make build
|
||||
<sub style="font-size:14px"><b>lion24</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/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=pernila/>
|
||||
@@ -462,6 +554,8 @@ make build
|
||||
<sub style="font-size:14px"><b>pernila</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/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/>
|
||||
|
21
acls.go
21
acls.go
@@ -17,12 +17,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
errEmptyPolicy = Error("empty policy")
|
||||
errInvalidAction = Error("invalid action")
|
||||
errInvalidUserSection = Error("invalid user section")
|
||||
errInvalidGroup = Error("invalid group")
|
||||
errInvalidTag = Error("invalid tag")
|
||||
errInvalidPortFormat = Error("invalid port format")
|
||||
errEmptyPolicy = Error("empty policy")
|
||||
errInvalidAction = Error("invalid action")
|
||||
errInvalidGroup = Error("invalid group")
|
||||
errInvalidTag = Error("invalid tag")
|
||||
errInvalidPortFormat = Error("invalid port format")
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -230,6 +229,10 @@ func expandAlias(
|
||||
return []string{"*"}, nil
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Str("alias", alias).
|
||||
Msg("Expanding")
|
||||
|
||||
if strings.HasPrefix(alias, "group:") {
|
||||
namespaces, err := expandGroup(aclPolicy, alias, stripEmailDomain)
|
||||
if err != nil {
|
||||
@@ -293,7 +296,9 @@ func expandAlias(
|
||||
return []string{cidr.String()}, nil
|
||||
}
|
||||
|
||||
return ips, errInvalidUserSection
|
||||
log.Warn().Msgf("No IPs found with the alias %v", alias)
|
||||
|
||||
return ips, nil
|
||||
}
|
||||
|
||||
// excludeCorrectlyTaggedNodes will remove from the list of input nodes the ones
|
||||
@@ -439,7 +444,7 @@ func expandGroup(
|
||||
errInvalidGroup,
|
||||
)
|
||||
}
|
||||
grp, err := NormalizeNamespaceName(group, stripEmailDomain)
|
||||
grp, err := NormalizeToFQDNRules(group, stripEmailDomain)
|
||||
if err != nil {
|
||||
return []string{}, fmt.Errorf(
|
||||
"failed to normalize group %q, err: %w",
|
||||
|
119
api.go
119
api.go
@@ -45,22 +45,21 @@ type registerWebAPITemplateConfig struct {
|
||||
}
|
||||
|
||||
var registerWebAPITemplate = template.Must(
|
||||
template.New("registerweb").Parse(`<html>
|
||||
template.New("registerweb").Parse(`
|
||||
<html>
|
||||
<head>
|
||||
<title>Registration - Headscale</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>headscale</h1>
|
||||
<p>
|
||||
Run the command below in the headscale server to add this machine to your network:
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<code>
|
||||
<b>headscale -n NAMESPACE nodes register --key {{.Key}}</b>
|
||||
</code>
|
||||
</p>
|
||||
|
||||
<h1>headscale</h1>
|
||||
<h2>Machine registration</h2>
|
||||
<p>
|
||||
Run the command below in the headscale server to add this machine to your network:
|
||||
</p>
|
||||
<pre><code>headscale -n NAMESPACE nodes register --key {{.Key}}</code></pre>
|
||||
</body>
|
||||
</html>`),
|
||||
)
|
||||
</html>
|
||||
`))
|
||||
|
||||
// RegisterWebAPI shows a simple message in the browser to point to the CLI
|
||||
// Listens in /register.
|
||||
@@ -134,6 +133,19 @@ func (h *Headscale) RegistrationHandler(ctx *gin.Context) {
|
||||
|
||||
return
|
||||
}
|
||||
hname, err := NormalizeToFQDNRules(
|
||||
req.Hostinfo.Hostname,
|
||||
h.cfg.OIDC.StripEmaildomain,
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Str("func", "RegistrationHandler").
|
||||
Str("hostinfo.name", req.Hostinfo.Hostname).
|
||||
Err(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// The machine did not have a key to authenticate, which means
|
||||
// that we rely on a method that calls back some how (OpenID or CLI)
|
||||
@@ -141,7 +153,7 @@ func (h *Headscale) RegistrationHandler(ctx *gin.Context) {
|
||||
// happens
|
||||
newMachine := Machine{
|
||||
MachineKey: machineKeyStr,
|
||||
Name: req.Hostinfo.Hostname,
|
||||
Name: hname,
|
||||
NodeKey: NodePublicKeyStripPrefix(req.NodeKey),
|
||||
LastSeen: &now,
|
||||
Expiry: &time.Time{},
|
||||
@@ -556,8 +568,13 @@ func (h *Headscale) handleAuthKey(
|
||||
Str("func", "handleAuthKey").
|
||||
Str("machine", registerRequest.Hostinfo.Hostname).
|
||||
Msg("Failed authentication via AuthKey")
|
||||
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
|
||||
Inc()
|
||||
|
||||
if pak != nil {
|
||||
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
|
||||
Inc()
|
||||
} else {
|
||||
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", "unknown").Inc()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -568,35 +585,51 @@ func (h *Headscale) handleAuthKey(
|
||||
Msg("Authentication key was valid, proceeding to acquire IP addresses")
|
||||
|
||||
nodeKey := NodePublicKeyStripPrefix(registerRequest.NodeKey)
|
||||
now := time.Now().UTC()
|
||||
|
||||
machineToRegister := Machine{
|
||||
Name: registerRequest.Hostinfo.Hostname,
|
||||
NamespaceID: pak.Namespace.ID,
|
||||
MachineKey: machineKeyStr,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
Expiry: ®isterRequest.Expiry,
|
||||
NodeKey: nodeKey,
|
||||
LastSeen: &now,
|
||||
AuthKeyID: uint(pak.ID),
|
||||
}
|
||||
|
||||
machine, err := h.RegisterMachine(
|
||||
machineToRegister,
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
// retrieve machine information if it exist
|
||||
// The error is not important, because if it does not
|
||||
// exist, then this is a new machine and we will move
|
||||
// on to registration.
|
||||
machine, _ := h.GetMachineByMachineKey(machineKey)
|
||||
if machine != nil {
|
||||
log.Trace().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("could not register machine")
|
||||
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
|
||||
Inc()
|
||||
ctx.String(
|
||||
http.StatusInternalServerError,
|
||||
"could not register machine",
|
||||
)
|
||||
Str("machine", machine.Name).
|
||||
Msg("machine already registered, refreshing with new auth key")
|
||||
|
||||
return
|
||||
machine.NodeKey = nodeKey
|
||||
machine.AuthKeyID = uint(pak.ID)
|
||||
h.RefreshMachine(machine, registerRequest.Expiry)
|
||||
} else {
|
||||
now := time.Now().UTC()
|
||||
machineToRegister := Machine{
|
||||
Name: registerRequest.Hostinfo.Hostname,
|
||||
NamespaceID: pak.Namespace.ID,
|
||||
MachineKey: machineKeyStr,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
Expiry: ®isterRequest.Expiry,
|
||||
NodeKey: nodeKey,
|
||||
LastSeen: &now,
|
||||
AuthKeyID: uint(pak.ID),
|
||||
}
|
||||
|
||||
machine, err = h.RegisterMachine(
|
||||
machineToRegister,
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("could not register machine")
|
||||
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
|
||||
Inc()
|
||||
ctx.String(
|
||||
http.StatusInternalServerError,
|
||||
"could not register machine",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
h.UsePreAuthKey(pak)
|
||||
|
100
app.go
100
app.go
@@ -47,6 +47,14 @@ import (
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
const (
|
||||
errSTUNAddressNotSet = Error("STUN address not set")
|
||||
errUnsupportedDatabase = Error("unsupported DB")
|
||||
errUnsupportedLetsEncryptChallengeType = Error(
|
||||
"unknown value for Lets Encrypt challenge type",
|
||||
)
|
||||
)
|
||||
|
||||
const (
|
||||
AuthPrefix = "Bearer "
|
||||
Postgres = "postgres"
|
||||
@@ -58,11 +66,6 @@ const (
|
||||
registerCacheExpiration = time.Minute * 15
|
||||
registerCacheCleanup = time.Minute * 20
|
||||
|
||||
errUnsupportedDatabase = Error("unsupported DB")
|
||||
errUnsupportedLetsEncryptChallengeType = Error(
|
||||
"unknown value for Lets Encrypt challenge type",
|
||||
)
|
||||
|
||||
DisabledClientAuth = "disabled"
|
||||
RelaxedClientAuth = "relaxed"
|
||||
EnforcedClientAuth = "enforced"
|
||||
@@ -72,6 +75,7 @@ const (
|
||||
type Config struct {
|
||||
ServerURL string
|
||||
Addr string
|
||||
MetricsAddr string
|
||||
GRPCAddr string
|
||||
GRPCAllowInsecure bool
|
||||
EphemeralNodeInactivityTimeout time.Duration
|
||||
@@ -119,10 +123,15 @@ type OIDCConfig struct {
|
||||
}
|
||||
|
||||
type DERPConfig struct {
|
||||
URLs []url.URL
|
||||
Paths []string
|
||||
AutoUpdate bool
|
||||
UpdateFrequency time.Duration
|
||||
ServerEnabled bool
|
||||
ServerRegionID int
|
||||
ServerRegionCode string
|
||||
ServerRegionName string
|
||||
STUNAddr string
|
||||
URLs []url.URL
|
||||
Paths []string
|
||||
AutoUpdate bool
|
||||
UpdateFrequency time.Duration
|
||||
}
|
||||
|
||||
type CLIConfig struct {
|
||||
@@ -141,7 +150,8 @@ type Headscale struct {
|
||||
dbDebug bool
|
||||
privateKey *key.MachinePrivate
|
||||
|
||||
DERPMap *tailcfg.DERPMap
|
||||
DERPMap *tailcfg.DERPMap
|
||||
DERPServer *DERPServer
|
||||
|
||||
aclPolicy *ACLPolicy
|
||||
aclRules []tailcfg.FilterRule
|
||||
@@ -177,7 +187,6 @@ func LookupTLSClientAuthMode(mode string) (tls.ClientAuthType, bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// NewHeadscale returns the Headscale app.
|
||||
func NewHeadscale(cfg Config) (*Headscale, error) {
|
||||
privKey, err := readOrCreatePrivateKey(cfg.PrivateKeyPath)
|
||||
if err != nil {
|
||||
@@ -238,6 +247,14 @@ func NewHeadscale(cfg Config) (*Headscale, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.DERP.ServerEnabled {
|
||||
embeddedDERPServer, err := app.NewDERPServer()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
app.DERPServer = embeddedDERPServer
|
||||
}
|
||||
|
||||
return &app, nil
|
||||
}
|
||||
|
||||
@@ -275,11 +292,13 @@ func (h *Headscale) expireEphemeralNodesWorker() {
|
||||
return
|
||||
}
|
||||
|
||||
expiredFound := false
|
||||
for _, machine := range machines {
|
||||
if machine.AuthKey != nil && machine.LastSeen != nil &&
|
||||
machine.AuthKey.Ephemeral &&
|
||||
time.Now().
|
||||
After(machine.LastSeen.Add(h.cfg.EphemeralNodeInactivityTimeout)) {
|
||||
expiredFound = true
|
||||
log.Info().
|
||||
Str("machine", machine.Name).
|
||||
Msg("Ephemeral client removed from database")
|
||||
@@ -294,7 +313,9 @@ func (h *Headscale) expireEphemeralNodesWorker() {
|
||||
}
|
||||
}
|
||||
|
||||
h.setLastStateChangeToNow(namespace.Name)
|
||||
if expiredFound {
|
||||
h.setLastStateChangeToNow(namespace.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,8 +415,6 @@ func (h *Headscale) httpAuthenticationMiddleware(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.AbortWithStatus(http.StatusUnauthorized)
|
||||
|
||||
valid, err := h.ValidateAPIKey(strings.TrimPrefix(authHeader, AuthPrefix))
|
||||
if err != nil {
|
||||
log.Error().
|
||||
@@ -433,11 +452,17 @@ func (h *Headscale) ensureUnixSocketIsAbsent() error {
|
||||
return os.Remove(h.cfg.UnixSocket)
|
||||
}
|
||||
|
||||
func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *gin.Engine {
|
||||
router := gin.Default()
|
||||
func (h *Headscale) createPrometheusRouter() *gin.Engine {
|
||||
promRouter := gin.Default()
|
||||
|
||||
prometheus := ginprometheus.NewPrometheus("gin")
|
||||
prometheus.Use(router)
|
||||
prometheus.Use(promRouter)
|
||||
|
||||
return promRouter
|
||||
}
|
||||
|
||||
func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *gin.Engine {
|
||||
router := gin.Default()
|
||||
|
||||
router.GET(
|
||||
"/health",
|
||||
@@ -449,11 +474,19 @@ func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *gin.Engine {
|
||||
router.POST("/machine/:id", h.RegistrationHandler)
|
||||
router.GET("/oidc/register/:mkey", h.RegisterOIDC)
|
||||
router.GET("/oidc/callback", h.OIDCCallback)
|
||||
router.GET("/apple", h.AppleMobileConfig)
|
||||
router.GET("/apple", h.AppleConfigMessage)
|
||||
router.GET("/apple/:platform", h.ApplePlatformConfig)
|
||||
router.GET("/windows", h.WindowsConfigMessage)
|
||||
router.GET("/windows/tailscale.reg", h.WindowsRegConfig)
|
||||
router.GET("/swagger", SwaggerUI)
|
||||
router.GET("/swagger/v1/openapiv2.json", SwaggerAPIv1)
|
||||
|
||||
if h.cfg.DERP.ServerEnabled {
|
||||
router.Any("/derp", h.DERPHandler)
|
||||
router.Any("/derp/probe", h.DERPProbeHandler)
|
||||
router.Any("/bootstrap-dns", h.DERPBootstrapDNSHandler)
|
||||
}
|
||||
|
||||
api := router.Group("/api")
|
||||
api.Use(h.httpAuthenticationMiddleware)
|
||||
{
|
||||
@@ -472,6 +505,16 @@ func (h *Headscale) Serve() error {
|
||||
// Fetch an initial DERP Map before we start serving
|
||||
h.DERPMap = GetDERPMap(h.cfg.DERP)
|
||||
|
||||
if h.cfg.DERP.ServerEnabled {
|
||||
// When embedded DERP is enabled we always need a STUN server
|
||||
if h.cfg.DERP.STUNAddr == "" {
|
||||
return errSTUNAddressNotSet
|
||||
}
|
||||
|
||||
h.DERPMap.Regions[h.DERPServer.region.RegionID] = &h.DERPServer.region
|
||||
go h.ServeSTUN()
|
||||
}
|
||||
|
||||
if h.cfg.DERP.AutoUpdate {
|
||||
derpMapCancelChannel := make(chan struct{})
|
||||
defer func() { derpMapCancelChannel <- struct{}{} }()
|
||||
@@ -649,6 +692,27 @@ func (h *Headscale) Serve() error {
|
||||
log.Info().
|
||||
Msgf("listening and serving HTTP on: %s", h.cfg.Addr)
|
||||
|
||||
promRouter := h.createPrometheusRouter()
|
||||
|
||||
promHTTPServer := &http.Server{
|
||||
Addr: h.cfg.MetricsAddr,
|
||||
Handler: promRouter,
|
||||
ReadTimeout: HTTPReadTimeout,
|
||||
WriteTimeout: 0,
|
||||
}
|
||||
|
||||
var promHTTPListener net.Listener
|
||||
promHTTPListener, err = net.Listen("tcp", h.cfg.MetricsAddr)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to bind to TCP address: %w", err)
|
||||
}
|
||||
|
||||
errorGroup.Go(func() error { return promHTTPServer.Serve(promHTTPListener) })
|
||||
|
||||
log.Info().
|
||||
Msgf("listening and serving metrics on: %s", h.cfg.MetricsAddr)
|
||||
|
||||
return errorGroup.Wait()
|
||||
}
|
||||
|
||||
|
@@ -23,7 +23,7 @@ func init() {
|
||||
apiKeysCmd.AddCommand(listAPIKeys)
|
||||
|
||||
createAPIKeyCmd.Flags().
|
||||
DurationP("expiration", "e", DefaultAPIKeyExpiry, "Human-readable expiration of the key (30m, 24h, 365d...)")
|
||||
DurationP("expiration", "e", DefaultAPIKeyExpiry, "Human-readable expiration of the key (e.g. 30m, 24h)")
|
||||
|
||||
apiKeysCmd.AddCommand(createAPIKeyCmd)
|
||||
|
||||
|
@@ -31,7 +31,7 @@ func init() {
|
||||
createPreAuthKeyCmd.PersistentFlags().
|
||||
Bool("ephemeral", false, "Preauthkey for ephemeral nodes")
|
||||
createPreAuthKeyCmd.Flags().
|
||||
DurationP("expiration", "e", DefaultPreAuthKeyExpiry, "Human-readable expiration of the key (30m, 24h, 365d...)")
|
||||
DurationP("expiration", "e", DefaultPreAuthKeyExpiry, "Human-readable expiration of the key (e.g. 30m, 24h)")
|
||||
}
|
||||
|
||||
var preauthkeysCmd = &cobra.Command{
|
||||
|
@@ -1,8 +1,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -19,12 +18,12 @@ var serveCmd = &cobra.Command{
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
h, err := getHeadscaleApp()
|
||||
if err != nil {
|
||||
log.Fatalf("Error initializing: %s", err)
|
||||
log.Fatal().Caller().Err(err).Msg("Error initializing")
|
||||
}
|
||||
|
||||
err = h.Serve()
|
||||
if err != nil {
|
||||
log.Fatalf("Error initializing: %s", err)
|
||||
log.Fatal().Caller().Err(err).Msg("Error starting server")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@@ -55,6 +55,9 @@ func LoadConfig(path string) error {
|
||||
|
||||
viper.SetDefault("dns_config", nil)
|
||||
|
||||
viper.SetDefault("derp.server.enabled", false)
|
||||
viper.SetDefault("derp.server.stun.enabled", true)
|
||||
|
||||
viper.SetDefault("unix_socket", "/var/run/headscale.sock")
|
||||
viper.SetDefault("unix_socket_permission", "0o770")
|
||||
|
||||
@@ -117,6 +120,16 @@ func LoadConfig(path string) error {
|
||||
}
|
||||
|
||||
func GetDERPConfig() headscale.DERPConfig {
|
||||
serverEnabled := viper.GetBool("derp.server.enabled")
|
||||
serverRegionID := viper.GetInt("derp.server.region_id")
|
||||
serverRegionCode := viper.GetString("derp.server.region_code")
|
||||
serverRegionName := viper.GetString("derp.server.region_name")
|
||||
stunAddr := viper.GetString("derp.server.stun_listen_addr")
|
||||
|
||||
if serverEnabled && stunAddr == "" {
|
||||
log.Fatal().Msg("derp.server.stun_listen_addr must be set if derp.server.enabled is true")
|
||||
}
|
||||
|
||||
urlStrs := viper.GetStringSlice("derp.urls")
|
||||
|
||||
urls := make([]url.URL, len(urlStrs))
|
||||
@@ -138,10 +151,15 @@ func GetDERPConfig() headscale.DERPConfig {
|
||||
updateFrequency := viper.GetDuration("derp.update_frequency")
|
||||
|
||||
return headscale.DERPConfig{
|
||||
URLs: urls,
|
||||
Paths: paths,
|
||||
AutoUpdate: autoUpdate,
|
||||
UpdateFrequency: updateFrequency,
|
||||
ServerEnabled: serverEnabled,
|
||||
ServerRegionID: serverRegionID,
|
||||
ServerRegionCode: serverRegionCode,
|
||||
ServerRegionName: serverRegionName,
|
||||
STUNAddr: stunAddr,
|
||||
URLs: urls,
|
||||
Paths: paths,
|
||||
AutoUpdate: autoUpdate,
|
||||
UpdateFrequency: updateFrequency,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,6 +322,7 @@ func getHeadscaleConfig() headscale.Config {
|
||||
return headscale.Config{
|
||||
ServerURL: viper.GetString("server_url"),
|
||||
Addr: viper.GetString("listen_addr"),
|
||||
MetricsAddr: viper.GetString("metrics_listen_addr"),
|
||||
GRPCAddr: viper.GetString("grpc_listen_addr"),
|
||||
GRPCAllowInsecure: viper.GetBool("grpc_allow_insecure"),
|
||||
|
||||
@@ -389,7 +408,7 @@ func getHeadscaleApp() (*headscale.Headscale, error) {
|
||||
aclPath := absPath(viper.GetString("acl_policy_path"))
|
||||
err = app.LoadACLPolicy(aclPath)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
log.Fatal().
|
||||
Str("path", aclPath).
|
||||
Err(err).
|
||||
Msg("Could not load the ACL policy")
|
||||
|
@@ -55,6 +55,7 @@ func (*Suite) TestConfigLoading(c *check.C) {
|
||||
// Test that config file was interpreted correctly
|
||||
c.Assert(viper.GetString("server_url"), check.Equals, "http://127.0.0.1:8080")
|
||||
c.Assert(viper.GetString("listen_addr"), check.Equals, "0.0.0.0:8080")
|
||||
c.Assert(viper.GetString("metrics_listen_addr"), check.Equals, "127.0.0.1:9090")
|
||||
c.Assert(viper.GetString("db_type"), check.Equals, "sqlite3")
|
||||
c.Assert(viper.GetString("db_path"), check.Equals, "/var/lib/headscale/db.sqlite")
|
||||
c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "")
|
||||
|
@@ -16,6 +16,12 @@ server_url: http://127.0.0.1:8080
|
||||
#
|
||||
listen_addr: 0.0.0.0:8080
|
||||
|
||||
# Address to listen to /metrics, you may want
|
||||
# to keep this endpoint private to your internal
|
||||
# network
|
||||
#
|
||||
metrics_listen_addr: 127.0.0.1:9090
|
||||
|
||||
# Address to listen for gRPC.
|
||||
# gRPC is used for controlling a headscale server
|
||||
# remotely with the CLI
|
||||
@@ -49,6 +55,26 @@ ip_prefixes:
|
||||
# headscale needs a list of DERP servers that can be presented
|
||||
# to the clients.
|
||||
derp:
|
||||
server:
|
||||
# If enabled, runs the embedded DERP server and merges it into the rest of the DERP config
|
||||
# The Headscale server_url defined above MUST be using https, DERP requires TLS to be in place
|
||||
enabled: false
|
||||
|
||||
# Region ID to use for the embedded DERP server.
|
||||
# The local DERP prevails if the region ID collides with other region ID coming from
|
||||
# the regular DERP config.
|
||||
region_id: 999
|
||||
|
||||
# Region code and name are displayed in the Tailscale UI to identify a DERP region
|
||||
region_code: "headscale"
|
||||
region_name: "Headscale Embedded DERP"
|
||||
|
||||
# Listens in UDP at the configured address for STUN connections to help on NAT traversal.
|
||||
# When the embedded DERP server is enabled stun_listen_addr MUST be defined.
|
||||
#
|
||||
# For more details on how this works, check this great article: https://tailscale.com/blog/how-tailscale-works/
|
||||
stun_listen_addr: "0.0.0.0:3478"
|
||||
|
||||
# List of externally available DERP maps encoded in JSON
|
||||
urls:
|
||||
- https://controlplane.tailscale.com/derpmap/default
|
||||
@@ -117,7 +143,7 @@ tls_client_auth_mode: relaxed
|
||||
tls_letsencrypt_cache_dir: /var/lib/headscale/cache
|
||||
|
||||
# Type of ACME challenge to use, currently supported types:
|
||||
# HTTP-01 or TLS_ALPN-01
|
||||
# HTTP-01 or TLS-ALPN-01
|
||||
# See [docs/tls.md](docs/tls.md) for more information
|
||||
tls_letsencrypt_challenge_type: HTTP-01
|
||||
# When HTTP-01 challenge is chosen, letsencrypt must set up a
|
||||
|
@@ -12,4 +12,4 @@ regions:
|
||||
ipv6: "2604:a880:400:d1::828:b001"
|
||||
stunport: 0
|
||||
stunonly: false
|
||||
derptestport: 0
|
||||
derpport: 0
|
||||
|
3
derp.go
3
derp.go
@@ -148,6 +148,9 @@ func (h *Headscale) scheduledDERPMapUpdateWorker(cancelChan <-chan struct{}) {
|
||||
case <-ticker.C:
|
||||
log.Info().Msg("Fetching DERPMap updates")
|
||||
h.DERPMap = GetDERPMap(h.cfg.DERP)
|
||||
if h.cfg.DERP.ServerEnabled {
|
||||
h.DERPMap.Regions[h.DERPServer.region.RegionID] = &h.DERPServer.region
|
||||
}
|
||||
|
||||
namespaces, err := h.ListNamespaces()
|
||||
if err != nil {
|
||||
|
231
derp_server.go
Normal file
231
derp_server.go
Normal file
@@ -0,0 +1,231 @@
|
||||
package headscale
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/net/stun"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
// fastStartHeader is the header (with value "1") that signals to the HTTP
|
||||
// server that the DERP HTTP client does not want the HTTP 101 response
|
||||
// headers and it will begin writing & reading the DERP protocol immediately
|
||||
// following its HTTP request.
|
||||
const fastStartHeader = "Derp-Fast-Start"
|
||||
|
||||
type DERPServer struct {
|
||||
tailscaleDERP *derp.Server
|
||||
region tailcfg.DERPRegion
|
||||
}
|
||||
|
||||
func (h *Headscale) NewDERPServer() (*DERPServer, error) {
|
||||
server := derp.NewServer(key.NodePrivate(*h.privateKey), log.Info().Msgf)
|
||||
region, err := h.generateRegionLocalDERP()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &DERPServer{server, region}, nil
|
||||
}
|
||||
|
||||
func (h *Headscale) generateRegionLocalDERP() (tailcfg.DERPRegion, error) {
|
||||
serverURL, err := url.Parse(h.cfg.ServerURL)
|
||||
if err != nil {
|
||||
return tailcfg.DERPRegion{}, err
|
||||
}
|
||||
var host string
|
||||
var port int
|
||||
host, portStr, err := net.SplitHostPort(serverURL.Host)
|
||||
if err != nil {
|
||||
if serverURL.Scheme == "https" {
|
||||
host = serverURL.Host
|
||||
port = 443
|
||||
} else {
|
||||
host = serverURL.Host
|
||||
port = 80
|
||||
}
|
||||
} else {
|
||||
port, err = strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
return tailcfg.DERPRegion{}, err
|
||||
}
|
||||
}
|
||||
|
||||
localDERPregion := tailcfg.DERPRegion{
|
||||
RegionID: h.cfg.DERP.ServerRegionID,
|
||||
RegionCode: h.cfg.DERP.ServerRegionCode,
|
||||
RegionName: h.cfg.DERP.ServerRegionName,
|
||||
Avoid: false,
|
||||
Nodes: []*tailcfg.DERPNode{
|
||||
{
|
||||
Name: fmt.Sprintf("%d", h.cfg.DERP.ServerRegionID),
|
||||
RegionID: h.cfg.DERP.ServerRegionID,
|
||||
HostName: host,
|
||||
DERPPort: port,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, portSTUNStr, err := net.SplitHostPort(h.cfg.DERP.STUNAddr)
|
||||
if err != nil {
|
||||
return tailcfg.DERPRegion{}, err
|
||||
}
|
||||
portSTUN, err := strconv.Atoi(portSTUNStr)
|
||||
if err != nil {
|
||||
return tailcfg.DERPRegion{}, err
|
||||
}
|
||||
localDERPregion.Nodes[0].STUNPort = portSTUN
|
||||
|
||||
return localDERPregion, nil
|
||||
}
|
||||
|
||||
func (h *Headscale) DERPHandler(ctx *gin.Context) {
|
||||
log.Trace().Caller().Msgf("/derp request from %v", ctx.ClientIP())
|
||||
up := strings.ToLower(ctx.Request.Header.Get("Upgrade"))
|
||||
if up != "websocket" && up != "derp" {
|
||||
if up != "" {
|
||||
log.Warn().Caller().Msgf("Weird websockets connection upgrade: %q", up)
|
||||
}
|
||||
ctx.String(http.StatusUpgradeRequired, "DERP requires connection upgrade")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fastStart := ctx.Request.Header.Get(fastStartHeader) == "1"
|
||||
|
||||
hijacker, ok := ctx.Writer.(http.Hijacker)
|
||||
if !ok {
|
||||
log.Error().Caller().Msg("DERP requires Hijacker interface from Gin")
|
||||
ctx.String(http.StatusInternalServerError, "HTTP does not support general TCP support")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
netConn, conn, err := hijacker.Hijack()
|
||||
if err != nil {
|
||||
log.Error().Caller().Err(err).Msgf("Hijack failed")
|
||||
ctx.String(http.StatusInternalServerError, "HTTP does not support general TCP support")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !fastStart {
|
||||
pubKey := h.privateKey.Public()
|
||||
pubKeyStr := pubKey.UntypedHexString() // nolint
|
||||
fmt.Fprintf(conn, "HTTP/1.1 101 Switching Protocols\r\n"+
|
||||
"Upgrade: DERP\r\n"+
|
||||
"Connection: Upgrade\r\n"+
|
||||
"Derp-Version: %v\r\n"+
|
||||
"Derp-Public-Key: %s\r\n\r\n",
|
||||
derp.ProtocolVersion,
|
||||
pubKeyStr)
|
||||
}
|
||||
|
||||
h.DERPServer.tailscaleDERP.Accept(netConn, conn, netConn.RemoteAddr().String())
|
||||
}
|
||||
|
||||
// DERPProbeHandler is the endpoint that js/wasm clients hit to measure
|
||||
// DERP latency, since they can't do UDP STUN queries.
|
||||
func (h *Headscale) DERPProbeHandler(ctx *gin.Context) {
|
||||
switch ctx.Request.Method {
|
||||
case "HEAD", "GET":
|
||||
ctx.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
default:
|
||||
ctx.String(http.StatusMethodNotAllowed, "bogus probe method")
|
||||
}
|
||||
}
|
||||
|
||||
// DERPBootstrapDNSHandler implements the /bootsrap-dns endpoint
|
||||
// Described in https://github.com/tailscale/tailscale/issues/1405,
|
||||
// this endpoint provides a way to help a client when it fails to start up
|
||||
// because its DNS are broken.
|
||||
// The initial implementation is here https://github.com/tailscale/tailscale/pull/1406
|
||||
// They have a cache, but not clear if that is really necessary at Headscale, uh, scale.
|
||||
// An example implementation is found here https://derp.tailscale.com/bootstrap-dns
|
||||
func (h *Headscale) DERPBootstrapDNSHandler(ctx *gin.Context) {
|
||||
dnsEntries := make(map[string][]net.IP)
|
||||
|
||||
resolvCtx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
var r net.Resolver
|
||||
for _, region := range h.DERPMap.Regions {
|
||||
for _, node := range region.Nodes { // we don't care if we override some nodes
|
||||
addrs, err := r.LookupIP(resolvCtx, "ip", node.HostName)
|
||||
if err != nil {
|
||||
log.Trace().Caller().Err(err).Msgf("bootstrap DNS lookup failed %q", node.HostName)
|
||||
|
||||
continue
|
||||
}
|
||||
dnsEntries[node.HostName] = addrs
|
||||
}
|
||||
}
|
||||
ctx.JSON(http.StatusOK, dnsEntries)
|
||||
}
|
||||
|
||||
// ServeSTUN starts a STUN server on the configured addr.
|
||||
func (h *Headscale) ServeSTUN() {
|
||||
packetConn, err := net.ListenPacket("udp", h.cfg.DERP.STUNAddr)
|
||||
if err != nil {
|
||||
log.Fatal().Msgf("failed to open STUN listener: %v", err)
|
||||
}
|
||||
log.Info().Msgf("STUN server started at %s", packetConn.LocalAddr())
|
||||
|
||||
udpConn, ok := packetConn.(*net.UDPConn)
|
||||
if !ok {
|
||||
log.Fatal().Msg("STUN listener is not a UDP listener")
|
||||
}
|
||||
serverSTUNListener(context.Background(), udpConn)
|
||||
}
|
||||
|
||||
func serverSTUNListener(ctx context.Context, packetConn *net.UDPConn) {
|
||||
var buf [64 << 10]byte
|
||||
var (
|
||||
bytesRead int
|
||||
udpAddr *net.UDPAddr
|
||||
err error
|
||||
)
|
||||
for {
|
||||
bytesRead, udpAddr, err = packetConn.ReadFromUDP(buf[:])
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
log.Error().Caller().Err(err).Msgf("STUN ReadFrom")
|
||||
time.Sleep(time.Second)
|
||||
|
||||
continue
|
||||
}
|
||||
log.Trace().Caller().Msgf("STUN request from %v", udpAddr)
|
||||
pkt := buf[:bytesRead]
|
||||
if !stun.Is(pkt) {
|
||||
log.Trace().Caller().Msgf("UDP packet is not STUN")
|
||||
|
||||
continue
|
||||
}
|
||||
txid, err := stun.ParseBindingRequest(pkt)
|
||||
if err != nil {
|
||||
log.Trace().Caller().Err(err).Msgf("STUN parse error")
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
res := stun.Response(txid, udpAddr.IP, uint16(udpAddr.Port))
|
||||
_, err = packetConn.WriteTo(res, udpAddr)
|
||||
if err != nil {
|
||||
log.Trace().Caller().Err(err).Msgf("Issue writing to UDP")
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
@@ -3,7 +3,7 @@
|
||||
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/XcQxk2VHjx) instead of opening an Issue.
|
||||
please ask on [Discord](https://discord.gg/c84AZQhmpx) instead of opening an Issue.
|
||||
|
||||
## Official documentation
|
||||
|
||||
|
@@ -5,4 +5,5 @@ metadata:
|
||||
data:
|
||||
server_url: $(PUBLIC_PROTO)://$(PUBLIC_HOSTNAME)
|
||||
listen_addr: "0.0.0.0:8080"
|
||||
metrics_listen_addr: "127.0.0.1:9090"
|
||||
ephemeral_node_inactivity_timeout: "30m"
|
||||
|
@@ -25,6 +25,11 @@ spec:
|
||||
configMapKeyRef:
|
||||
name: headscale-config
|
||||
key: listen_addr
|
||||
- name: METRICS_LISTEN_ADDR
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: headscale-config
|
||||
key: metrics_listen_addr
|
||||
- name: DERP_MAP_PATH
|
||||
value: /vol/config/derp.yaml
|
||||
- name: EPHEMERAL_NODE_INACTIVITY_TIMEOUT
|
||||
|
@@ -26,6 +26,11 @@ spec:
|
||||
configMapKeyRef:
|
||||
name: headscale-config
|
||||
key: listen_addr
|
||||
- name: METRICS_LISTEN_ADDR
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: headscale-config
|
||||
key: metrics_listen_addr
|
||||
- name: DERP_MAP_PATH
|
||||
value: /vol/config/derp.yaml
|
||||
- name: EPHEMERAL_NODE_INACTIVITY_TIMEOUT
|
||||
|
362
docs/proposals/001-acls.md
Normal file
362
docs/proposals/001-acls.md
Normal file
@@ -0,0 +1,362 @@
|
||||
# ACLs
|
||||
|
||||
A key component of tailscale is the notion of Tailnet. This notion is hidden
|
||||
but the implications that it have on how to use tailscale are not.
|
||||
|
||||
For tailscale an [tailnet](https://tailscale.com/kb/1136/tailnet/) is the
|
||||
following:
|
||||
|
||||
> For personal users, you are a tailnet of many devices and one person. Each
|
||||
> device gets a private Tailscale IP address in the CGNAT range and every
|
||||
> device can talk directly to every other device, wherever they are on the
|
||||
> internet.
|
||||
>
|
||||
> For businesses and organizations, a tailnet is many devices and many users.
|
||||
> It can be based on your Microsoft Active Directory, your Google Workspace, a
|
||||
> GitHub organization, Okta tenancy, or other identity provider namespace. All
|
||||
> of the devices and users in your tailnet can be seen by the tailnet
|
||||
> administrators in the Tailscale admin console. There you can apply
|
||||
> tailnet-wide configuration, such as ACLs that affect visibility of devices
|
||||
> inside your tailnet, DNS settings, and more.
|
||||
|
||||
## Current implementation and issues
|
||||
|
||||
Currently in headscale, the namespaces are used both as tailnet and users. The
|
||||
issue is that if we want to use the ACL's we can't use both at the same time.
|
||||
|
||||
Tailnet's cannot communicate with each others. So we can't have an ACL that
|
||||
authorize tailnet (namespace) A to talk to tailnet (namespace) B.
|
||||
|
||||
We also can't write ACLs based on the users (namespaces in headscale) since all
|
||||
devices belong to the same user.
|
||||
|
||||
With the current implementation the only ACL that we can user is to associate
|
||||
each headscale IP to a host manually then write the ACLs according to this
|
||||
manual mapping.
|
||||
|
||||
```json
|
||||
{
|
||||
"hosts": {
|
||||
"host1": "100.64.0.1",
|
||||
"server": "100.64.0.2"
|
||||
},
|
||||
"acls": [
|
||||
{ "action": "accept", "users": ["host1"], "ports": ["host2:80,443"] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
While this works, it requires a lot of manual editing on the configuration and
|
||||
to keep track of all devices IP address.
|
||||
|
||||
## Proposition for a next implementation
|
||||
|
||||
In order to ease the use of ACL's we need to split the tailnet and users
|
||||
notion.
|
||||
|
||||
A solution could be to consider a headscale server (in it's entirety) as a
|
||||
tailnet.
|
||||
|
||||
For personal users the default behavior could either allow all communications
|
||||
between all namespaces (like tailscale) or dissallow all communications between
|
||||
namespaces (current behavior).
|
||||
|
||||
For businesses and organisations, viewing a headscale instance a single tailnet
|
||||
would allow users (namespace) to talk to each other with the ACLs. As described
|
||||
in tailscale's documentation [[1]], a server should be tagged and personnal
|
||||
devices should be tied to a user. Translated in headscale's terms each user can
|
||||
have multiple devices and all those devices should be in the same namespace.
|
||||
The servers should be tagged and used as such.
|
||||
|
||||
This implementation would render useless the sharing feature that is currently
|
||||
implemented since an ACL could do the same. Simplifying to only one user
|
||||
interface to do one thing is easier and less confusing for the users.
|
||||
|
||||
To better suit the ACLs in this proposition, it's advised to consider that each
|
||||
namespaces belong to one person. This person can have multiple devices, they
|
||||
will all be considered as the same user in the ACLs. OIDC feature wouldn't need
|
||||
to map people to namespace, just create a namespace if the person isn't
|
||||
registered yet.
|
||||
|
||||
As a sidenote, users would like to write ACLs as YAML. We should offer users
|
||||
the ability to rules in either format (HuJSON or YAML).
|
||||
|
||||
[1]: https://tailscale.com/kb/1068/acl-tags/
|
||||
|
||||
## Example
|
||||
|
||||
Let's build an example use case for a small business (It may be the place where
|
||||
ACL's are the most useful).
|
||||
|
||||
We have a small company with a boss, an admin, two developper and an intern.
|
||||
|
||||
The boss should have access to all servers but not to the users hosts. Admin
|
||||
should also have access to all hosts except that their permissions should be
|
||||
limited to maintaining the hosts (for example purposes). The developers can do
|
||||
anything they want on dev hosts, but only watch on productions hosts. Intern
|
||||
can only interact with the development servers.
|
||||
|
||||
Each user have at least a device connected to the network and we have some
|
||||
servers.
|
||||
|
||||
- database.prod
|
||||
- database.dev
|
||||
- app-server1.prod
|
||||
- app-server1.dev
|
||||
- billing.internal
|
||||
|
||||
### Current headscale implementation
|
||||
|
||||
Let's create some namespaces
|
||||
|
||||
```bash
|
||||
headscale namespaces create prod
|
||||
headscale namespaces create dev
|
||||
headscale namespaces create internal
|
||||
headscale namespaces create users
|
||||
|
||||
headscale nodes register -n users boss-computer
|
||||
headscale nodes register -n users admin1-computer
|
||||
headscale nodes register -n users dev1-computer
|
||||
headscale nodes register -n users dev1-phone
|
||||
headscale nodes register -n users dev2-computer
|
||||
headscale nodes register -n users intern1-computer
|
||||
|
||||
headscale nodes register -n prod database
|
||||
headscale nodes register -n prod app-server1
|
||||
|
||||
headscale nodes register -n dev database
|
||||
headscale nodes register -n dev app-server1
|
||||
|
||||
headscale nodes register -n internal billing
|
||||
|
||||
headscale nodes list
|
||||
ID | Name | Namespace | IP address
|
||||
1 | boss-computer | users | 100.64.0.1
|
||||
2 | admin1-computer | users | 100.64.0.2
|
||||
3 | dev1-computer | users | 100.64.0.3
|
||||
4 | dev1-phone | users | 100.64.0.4
|
||||
5 | dev2-computer | users | 100.64.0.5
|
||||
6 | intern1-computer | users | 100.64.0.6
|
||||
7 | database | prod | 100.64.0.7
|
||||
8 | app-server1 | prod | 100.64.0.8
|
||||
9 | database | dev | 100.64.0.9
|
||||
10 | app-server1 | dev | 100.64.0.10
|
||||
11 | internal | internal | 100.64.0.11
|
||||
```
|
||||
|
||||
In order to only allow the communications related to our description above we
|
||||
need to add the following ACLs
|
||||
|
||||
```json
|
||||
{
|
||||
"hosts": {
|
||||
"boss-computer": "100.64.0.1",
|
||||
"admin1-computer": "100.64.0.2",
|
||||
"dev1-computer": "100.64.0.3",
|
||||
"dev1-phone": "100.64.0.4",
|
||||
"dev2-computer": "100.64.0.5",
|
||||
"intern1-computer": "100.64.0.6",
|
||||
"prod-app-server1": "100.64.0.8"
|
||||
},
|
||||
"groups": {
|
||||
"group:dev": ["dev1-computer", "dev1-phone", "dev2-computer"],
|
||||
"group:admin": ["admin1-computer"],
|
||||
"group:boss": ["boss-computer"],
|
||||
"group:intern": ["intern1-computer"]
|
||||
},
|
||||
"acls": [
|
||||
// boss have access to all servers but no users hosts
|
||||
{
|
||||
"action": "accept",
|
||||
"users": ["group:boss"],
|
||||
"ports": ["prod:*", "dev:*", "internal:*"]
|
||||
},
|
||||
|
||||
// admin have access to adminstration port (lets only consider port 22 here)
|
||||
{
|
||||
"action": "accept",
|
||||
"users": ["group:admin"],
|
||||
"ports": ["prod:22", "dev:22", "internal:22"]
|
||||
},
|
||||
|
||||
// dev can do anything on dev servers and check access on prod servers
|
||||
{
|
||||
"action": "accept",
|
||||
"users": ["group:dev"],
|
||||
"ports": ["dev:*", "prod-app-server1:80,443"]
|
||||
},
|
||||
|
||||
// interns only have access to port 80 and 443 on dev servers (lame internship)
|
||||
{ "action": "accept", "users": ["group:intern"], "ports": ["dev:80,443"] },
|
||||
|
||||
// users can access their own devices
|
||||
{
|
||||
"action": "accept",
|
||||
"users": ["dev1-computer"],
|
||||
"ports": ["dev1-phone:*"]
|
||||
},
|
||||
{
|
||||
"action": "accept",
|
||||
"users": ["dev1-phone"],
|
||||
"ports": ["dev1-computer:*"]
|
||||
},
|
||||
|
||||
// internal namespace communications should still be allowed within the namespace
|
||||
{ "action": "accept", "users": ["dev"], "ports": ["dev:*"] },
|
||||
{ "action": "accept", "users": ["prod"], "ports": ["prod:*"] },
|
||||
{ "action": "accept", "users": ["internal"], "ports": ["internal:*"] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Since communications between namespace isn't possible we also have to share the
|
||||
devices between the namespaces.
|
||||
|
||||
```bash
|
||||
|
||||
// add boss host to prod, dev and internal network
|
||||
headscale nodes share -i 1 -n prod
|
||||
headscale nodes share -i 1 -n dev
|
||||
headscale nodes share -i 1 -n internal
|
||||
|
||||
// add admin computer to prod, dev and internal network
|
||||
headscale nodes share -i 2 -n prod
|
||||
headscale nodes share -i 2 -n dev
|
||||
headscale nodes share -i 2 -n internal
|
||||
|
||||
// add all dev to prod and dev network
|
||||
headscale nodes share -i 3 -n dev
|
||||
headscale nodes share -i 4 -n dev
|
||||
headscale nodes share -i 3 -n prod
|
||||
headscale nodes share -i 4 -n prod
|
||||
headscale nodes share -i 5 -n dev
|
||||
headscale nodes share -i 5 -n prod
|
||||
|
||||
headscale nodes share -i 6 -n dev
|
||||
```
|
||||
|
||||
This fake network have not been tested but it should work. Operating it could
|
||||
be quite tedious if the company grows. Each time a new user join we have to add
|
||||
it to a group, and share it to the correct namespaces. If the user want
|
||||
multiple devices we have to allow communication to each of them one by one. If
|
||||
business conduct a change in the organisations we may have to rewrite all acls
|
||||
and reorganise all namespaces.
|
||||
|
||||
If we add servers in production we should also update the ACLs to allow dev
|
||||
access to certain category of them (only app servers for example).
|
||||
|
||||
### example based on the proposition in this document
|
||||
|
||||
Let's create the namespaces
|
||||
|
||||
```bash
|
||||
headscale namespaces create boss
|
||||
headscale namespaces create admin1
|
||||
headscale namespaces create dev1
|
||||
headscale namespaces create dev2
|
||||
headscale namespaces create intern1
|
||||
```
|
||||
|
||||
We don't need to create namespaces for the servers because the servers will be
|
||||
tagged. When registering the servers we will need to add the flag
|
||||
`--advertised-tags=tag:<tag1>,tag:<tag2>`, and the user (namespace) that is
|
||||
registering the server should be allowed to do it. Since anyone can add tags to
|
||||
a server they can register, the check of the tags is done on headscale server
|
||||
and only valid tags are applied. A tag is valid if the namespace that is
|
||||
registering it is allowed to do it.
|
||||
|
||||
Here are the ACL's to implement the same permissions as above:
|
||||
|
||||
```json
|
||||
{
|
||||
// groups are simpler and only list the namespaces name
|
||||
"groups": {
|
||||
"group:boss": ["boss"],
|
||||
"group:dev": ["dev1", "dev2"],
|
||||
"group:admin": ["admin1"],
|
||||
"group:intern": ["intern1"]
|
||||
},
|
||||
"tagOwners": {
|
||||
// the administrators can add servers in production
|
||||
"tag:prod-databases": ["group:admin"],
|
||||
"tag:prod-app-servers": ["group:admin"],
|
||||
|
||||
// the boss can tag any server as internal
|
||||
"tag:internal": ["group:boss"],
|
||||
|
||||
// dev can add servers for dev purposes as well as admins
|
||||
"tag:dev-databases": ["group:admin", "group:dev"],
|
||||
"tag:dev-app-servers": ["group:admin", "group:dev"]
|
||||
|
||||
// interns cannot add servers
|
||||
},
|
||||
"acls": [
|
||||
// boss have access to all servers
|
||||
{
|
||||
"action": "accept",
|
||||
"users": ["group:boss"],
|
||||
"ports": [
|
||||
"tag:prod-databases:*",
|
||||
"tag:prod-app-servers:*",
|
||||
"tag:internal:*",
|
||||
"tag:dev-databases:*",
|
||||
"tag:dev-app-servers:*"
|
||||
]
|
||||
},
|
||||
|
||||
// admin have only access to administrative ports of the servers
|
||||
{
|
||||
"action": "accept",
|
||||
"users": ["group:admin"],
|
||||
"ports": [
|
||||
"tag:prod-databases:22",
|
||||
"tag:prod-app-servers:22",
|
||||
"tag:internal:22",
|
||||
"tag:dev-databases:22",
|
||||
"tag:dev-app-servers:22"
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"action": "accept",
|
||||
"users": ["group:dev"],
|
||||
"ports": [
|
||||
"tag:dev-databases:*",
|
||||
"tag:dev-app-servers:*",
|
||||
"tag:prod-app-servers:80,443"
|
||||
]
|
||||
},
|
||||
|
||||
// servers should be able to talk to database. Database should not be able to initiate connections to server
|
||||
{
|
||||
"action": "accept",
|
||||
"users": ["tag:dev-app-servers"],
|
||||
"ports": ["tag:dev-databases:5432"]
|
||||
},
|
||||
{
|
||||
"action": "accept",
|
||||
"users": ["tag:prod-app-servers"],
|
||||
"ports": ["tag:prod-databases:5432"]
|
||||
},
|
||||
|
||||
// interns have access to dev-app-servers only in reading mode
|
||||
{
|
||||
"action": "accept",
|
||||
"users": ["group:intern"],
|
||||
"ports": ["tag:dev-app-servers:80,443"]
|
||||
},
|
||||
|
||||
// we still have to allow internal namespaces communications since nothing guarantees that each user have their own namespaces. This could be talked over.
|
||||
{ "action": "accept", "users": ["boss"], "ports": ["boss:*"] },
|
||||
{ "action": "accept", "users": ["dev1"], "ports": ["dev1:*"] },
|
||||
{ "action": "accept", "users": ["dev2"], "ports": ["dev2:*"] },
|
||||
{ "action": "accept", "users": ["admin1"], "ports": ["admin1:*"] },
|
||||
{ "action": "accept", "users": ["intern1"], "ports": ["intern1:*"] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
With this implementation, the sharing step is not necessary. Maintenance cost
|
||||
of the ACL file is lower and less tedious (no need to map hostname and IP's
|
||||
into it).
|
@@ -14,8 +14,8 @@ not work with alternatives like [Podman](https://podman.io). The Docker image ca
|
||||
1. Prepare a directory on the host Docker node in your directory of choice, used to hold `headscale` configuration and the [SQLite](https://www.sqlite.org/) database:
|
||||
|
||||
```shell
|
||||
mkdir ./headscale && cd ./headscale
|
||||
mkdir ./config
|
||||
mkdir -p ./headscale/config
|
||||
cd ./headscale
|
||||
```
|
||||
|
||||
2. Create an empty SQlite datebase in the headscale directory:
|
||||
@@ -45,6 +45,17 @@ touch ./config/config.yaml
|
||||
```
|
||||
|
||||
Modify the config file to your preferences before launching Docker container.
|
||||
Here are some settings that you likely want:
|
||||
|
||||
```yaml
|
||||
server_url: http://your-host-name:8080 # Change to your hostname or host IP
|
||||
# Listen to 0.0.0.0 so it's accessible outside the container
|
||||
metrics_listen_addr: 0.0.0.0:9090
|
||||
# The default /var/lib/headscale path is not writable in the container
|
||||
private_key_path: /etc/headscale/private.key
|
||||
# The default /var/lib/headscale path is not writable in the container
|
||||
db_path: /etc/headscale/db.sqlite
|
||||
```
|
||||
|
||||
4. Start the headscale server while working in the host headscale directory:
|
||||
|
||||
@@ -55,11 +66,14 @@ docker run \
|
||||
--rm \
|
||||
--volume $(pwd)/config:/etc/headscale/ \
|
||||
--publish 127.0.0.1:8080:8080 \
|
||||
--publish 127.0.0.1:9090:9090 \
|
||||
headscale/headscale:<VERSION> \
|
||||
headscale serve
|
||||
|
||||
```
|
||||
|
||||
Note: use `0.0.0.0:8080:8080` instead of `127.0.0.1:8080:8080` if you want to expose the container externally.
|
||||
|
||||
This command will mount `config/` under `/etc/headscale`, forward port 8080 out of the container so the
|
||||
`headscale` instance becomes available and then detach so headscale runs in the background.
|
||||
|
||||
@@ -80,13 +94,14 @@ docker ps
|
||||
Verify `headscale` is available:
|
||||
|
||||
```shell
|
||||
curl http://127.0.0.1:8080/metrics
|
||||
curl http://127.0.0.1:9090/metrics
|
||||
```
|
||||
|
||||
6. Create a namespace ([tailnet](https://tailscale.com/kb/1136/tailnet/)):
|
||||
|
||||
```shell
|
||||
docker exec headscale -- headscale namespaces create myfirstnamespace
|
||||
docker exec headscale \
|
||||
headscale namespaces create myfirstnamespace
|
||||
```
|
||||
|
||||
### Register a machine (normal login)
|
||||
@@ -100,7 +115,7 @@ tailscale up --login-server YOUR_HEADSCALE_URL
|
||||
To register a machine when running `headscale` in a container, take the headscale command and pass it to the container:
|
||||
|
||||
```shell
|
||||
docker exec headscale -- \
|
||||
docker exec headscale \
|
||||
headscale --namespace myfirstnamespace nodes register --key <YOU_+MACHINE_KEY>
|
||||
```
|
||||
|
||||
@@ -109,7 +124,7 @@ docker exec headscale -- \
|
||||
Generate a key using the command line:
|
||||
|
||||
```shell
|
||||
docker exec headscale -- \
|
||||
docker exec headscale \
|
||||
headscale --namespace myfirstnamespace preauthkeys create --reusable --expiration 24h
|
||||
```
|
||||
|
||||
|
@@ -30,6 +30,14 @@ 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:
|
||||
@@ -50,7 +58,7 @@ from the [headscale repository](../)
|
||||
6. Start the headscale server:
|
||||
|
||||
```shell
|
||||
headscale serve
|
||||
headscale serve
|
||||
```
|
||||
|
||||
This command will start `headscale` in the current terminal session.
|
||||
@@ -67,7 +75,7 @@ To run `headscale` in the background, please follow the steps in the [SystemD se
|
||||
Verify `headscale` is available:
|
||||
|
||||
```shell
|
||||
curl http://127.0.0.1:8080/metrics
|
||||
curl http://127.0.0.1:9090/metrics
|
||||
```
|
||||
|
||||
8. Create a namespace ([tailnet](https://tailscale.com/kb/1136/tailnet/)):
|
||||
@@ -150,7 +158,7 @@ or run all headscale commands as the headscale user:
|
||||
su - headscale
|
||||
```
|
||||
|
||||
2. In `/etc/headscale/config.yaml`, override the default `headscale` unix socket with a SystemD friendly path:
|
||||
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
|
||||
@@ -165,8 +173,7 @@ systemctl daemon-reload
|
||||
4. Enable and start the new `headscale` service:
|
||||
|
||||
```shell
|
||||
systemctl enable headscale
|
||||
systemctl start headscale
|
||||
systemctl enable --now headscale
|
||||
```
|
||||
|
||||
5. Verify the headscale service:
|
||||
@@ -178,7 +185,7 @@ systemctl status headscale
|
||||
Verify `headscale` is available:
|
||||
|
||||
```shell
|
||||
curl http://127.0.0.1:8080/metrics
|
||||
curl http://127.0.0.1:9090/metrics
|
||||
```
|
||||
|
||||
`headscale` will now run in the background and start at boot.
|
||||
|
42
flake.lock
generated
Normal file
42
flake.lock
generated
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"locked": {
|
||||
"lastModified": 1644229661,
|
||||
"narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "3cecb5b042f7f209c56ffd8371b2711a290ec797",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1647536224,
|
||||
"narHash": "sha256-SUIiz4DhMXgM7i+hvFWmLnhywr1WeRGIz+EIbwQQguM=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "dd8cebebbf0f9352501f251ac37b851d947f92dc",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"ref": "master",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
148
flake.nix
Normal file
148
flake.nix
Normal file
@@ -0,0 +1,148 @@
|
||||
{
|
||||
description = "headscale - Open Source Tailscale Control server";
|
||||
|
||||
inputs = {
|
||||
# TODO: Use unstable when Go 1.18 has made it in
|
||||
# https://nixpk.gs/pr-tracker.html?pr=164292
|
||||
# nixpkgs.url = "nixpkgs/nixpkgs-unstable";
|
||||
nixpkgs.url = "nixpkgs/master";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils, ... }:
|
||||
let
|
||||
headscaleVersion = if (self ? shortRev) then self.shortRev else "dev";
|
||||
in
|
||||
{
|
||||
overlay = final: prev:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${prev.system};
|
||||
in
|
||||
rec {
|
||||
golines =
|
||||
pkgs.buildGoModule rec {
|
||||
pname = "golines";
|
||||
version = "0.9.0";
|
||||
|
||||
src = pkgs.fetchFromGitHub {
|
||||
owner = "segmentio";
|
||||
repo = "golines";
|
||||
rev = "v${version}";
|
||||
sha256 = "sha256-BUXEg+4r9L/gqe4DhTlhN55P3jWt7ZyWFQycO6QePrw=";
|
||||
};
|
||||
|
||||
vendorSha256 = "sha256-sEzWUeVk5GB0H41wrp12P8sBWRjg0FHUX6ABDEEBqK8=";
|
||||
|
||||
nativeBuildInputs = [ pkgs.installShellFiles ];
|
||||
};
|
||||
|
||||
protoc-gen-grpc-gateway =
|
||||
pkgs.buildGoModule rec {
|
||||
pname = "grpc-gateway";
|
||||
version = "2.8.0";
|
||||
|
||||
src = pkgs.fetchFromGitHub {
|
||||
owner = "grpc-ecosystem";
|
||||
repo = "grpc-gateway";
|
||||
rev = "v${version}";
|
||||
sha256 = "sha256-8eBBBYJ+tBjB2fgPMX/ZlbN3eeS75e8TAZYOKXs6hcg=";
|
||||
};
|
||||
|
||||
vendorSha256 = "sha256-AW2Gn/mlZyLMwF+NpK59eiOmQrYWW/9HPjbunYc9Ij4=";
|
||||
|
||||
nativeBuildInputs = [ pkgs.installShellFiles ];
|
||||
|
||||
subPackages = [ "protoc-gen-grpc-gateway" "protoc-gen-openapiv2" ];
|
||||
};
|
||||
|
||||
headscale =
|
||||
pkgs.buildGo118Module rec {
|
||||
pname = "headscale";
|
||||
version = headscaleVersion;
|
||||
src = pkgs.lib.cleanSource self;
|
||||
|
||||
# When updating go.mod or go.sum, a new sha will need to be calculated,
|
||||
# update this if you have a mismatch after doing a change to thos files.
|
||||
vendorSha256 = "sha256-VsMhgAP0YY6oo/iW7UXg6jc/rv5oZLSkluQ12TKsXXs=";
|
||||
|
||||
ldflags = [ "-s" "-w" "-X github.com/juanfont/headscale/cmd/headscale/cli.Version=v${version}" ];
|
||||
};
|
||||
};
|
||||
} // flake-utils.lib.eachDefaultSystem
|
||||
(system:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
overlays = [ self.overlay ];
|
||||
inherit system;
|
||||
};
|
||||
buildDeps = with pkgs; [ git go_1_18 gnumake ];
|
||||
devDeps = with pkgs;
|
||||
buildDeps ++ [
|
||||
golangci-lint
|
||||
golines
|
||||
nodePackages.prettier
|
||||
|
||||
# Protobuf dependencies
|
||||
protobuf
|
||||
protoc-gen-go
|
||||
protoc-gen-go-grpc
|
||||
protoc-gen-grpc-gateway
|
||||
buf
|
||||
clang-tools # clang-format
|
||||
];
|
||||
|
||||
|
||||
# Add entry to build a docker image with headscale
|
||||
# caveat: only works on Linux
|
||||
#
|
||||
# Usage:
|
||||
# nix build .#headscale-docker
|
||||
# docker load < result
|
||||
headscale-docker = pkgs.dockerTools.buildLayeredImage {
|
||||
name = "headscale";
|
||||
tag = headscaleVersion;
|
||||
contents = [ pkgs.headscale ];
|
||||
config.Entrypoint = [ (pkgs.headscale + "/bin/headscale") ];
|
||||
};
|
||||
in
|
||||
rec {
|
||||
# `nix develop`
|
||||
devShell = pkgs.mkShell { buildInputs = devDeps; };
|
||||
|
||||
# `nix build`
|
||||
packages = with pkgs; {
|
||||
inherit headscale;
|
||||
inherit headscale-docker;
|
||||
};
|
||||
|
||||
defaultPackage = pkgs.headscale;
|
||||
|
||||
# `nix run`
|
||||
apps.headscale = flake-utils.lib.mkApp {
|
||||
drv = packages.headscale;
|
||||
};
|
||||
defaultApp = apps.headscale;
|
||||
|
||||
checks = {
|
||||
format = pkgs.runCommand "check-format"
|
||||
{
|
||||
buildInputs = with pkgs; [
|
||||
gnumake
|
||||
nixpkgs-fmt
|
||||
golangci-lint
|
||||
nodePackages.prettier
|
||||
golines
|
||||
clang-tools
|
||||
];
|
||||
} ''
|
||||
${pkgs.nixpkgs-fmt}/bin/nixpkgs-fmt ${./.}
|
||||
${pkgs.golangci-lint}/bin/golangci-lint run --fix --timeout 10m
|
||||
${pkgs.nodePackages.prettier}/bin/prettier --write '**/**.{ts,js,md,yaml,yml,sass,css,scss,html}'
|
||||
${pkgs.golines}/bin/golines --max-len=88 --base-formatter=gofumpt -w ${./.}
|
||||
${pkgs.clang-tools}/bin/clang-format -style="{BasedOnStyle: Google, IndentWidth: 4, AlignConsecutiveDeclarations: true, AlignConsecutiveAssignments: true, ColumnLimit: 0}" -i ${./.}
|
||||
'';
|
||||
};
|
||||
|
||||
|
||||
});
|
||||
}
|
113
go.mod
113
go.mod
@@ -1,72 +1,71 @@
|
||||
module github.com/juanfont/headscale
|
||||
|
||||
go 1.17
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.2
|
||||
github.com/AlecAivazis/survey/v2 v2.3.4
|
||||
github.com/ccding/go-stun/stun v0.0.0-20200514191101-4dc67bcdb029
|
||||
github.com/coreos/go-oidc/v3 v3.1.0
|
||||
github.com/efekarakus/termcolor v1.0.1
|
||||
github.com/fatih/set v0.2.1
|
||||
github.com/gin-gonic/gin v1.7.7
|
||||
github.com/glebarez/sqlite v1.3.5
|
||||
github.com/glebarez/sqlite v1.4.3
|
||||
github.com/gofrs/uuid v4.2.0+incompatible
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.3
|
||||
github.com/infobloxopen/protoc-gen-gorm v1.1.0
|
||||
github.com/klauspost/compress v1.14.4
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.10.0
|
||||
github.com/klauspost/compress v1.15.1
|
||||
github.com/ory/dockertest/v3 v3.8.1
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/philip-bui/grpc-zerolog v1.0.1
|
||||
github.com/prometheus/client_golang v1.12.1
|
||||
github.com/pterm/pterm v0.12.37
|
||||
github.com/pterm/pterm v0.12.41
|
||||
github.com/rs/zerolog v1.26.1
|
||||
github.com/spf13/cobra v1.3.0
|
||||
github.com/spf13/viper v1.10.1
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/tailscale/hujson v0.0.0-20211215203138-ffd971c5f362
|
||||
github.com/spf13/cobra v1.4.0
|
||||
github.com/spf13/viper v1.11.0
|
||||
github.com/stretchr/testify v1.7.1
|
||||
github.com/tailscale/hujson v0.0.0-20220421170326-6583d0610064
|
||||
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
|
||||
github.com/zsais/go-gin-prometheus v0.1.0
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292
|
||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b
|
||||
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4
|
||||
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
google.golang.org/genproto v0.0.0-20220228195345-15d65a4533f7
|
||||
google.golang.org/grpc v1.44.0
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0
|
||||
google.golang.org/protobuf v1.27.1
|
||||
google.golang.org/genproto v0.0.0-20220422154200-b37d22cd5731
|
||||
google.golang.org/grpc v1.46.0
|
||||
google.golang.org/protobuf v1.28.0
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
gorm.io/driver/postgres v1.3.1
|
||||
gorm.io/gorm v1.23.1
|
||||
gorm.io/driver/postgres v1.3.5
|
||||
gorm.io/gorm v1.23.4
|
||||
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6
|
||||
tailscale.com v1.22.0
|
||||
tailscale.com v1.24.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/Microsoft/go-winio v0.5.2 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
|
||||
github.com/Microsoft/go-winio v0.5.1 // indirect
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
||||
github.com/akutz/memconn v0.1.0 // indirect
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect
|
||||
github.com/atomicgo/cursor v0.0.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.2 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/containerd/continuity v0.2.2 // indirect
|
||||
github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/denisenkom/go-mssqldb v0.12.0 // indirect
|
||||
github.com/docker/cli v20.10.12+incompatible // indirect
|
||||
github.com/docker/docker v20.10.12+incompatible // indirect
|
||||
github.com/docker/cli v20.10.11+incompatible // indirect
|
||||
github.com/docker/docker v20.10.7+incompatible // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.14.8 // indirect
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.10.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.6.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.16.0 // indirect
|
||||
github.com/go-playground/locales v0.13.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.17.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.4.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/glog v1.0.0 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/go-cmp v0.5.7 // indirect
|
||||
github.com/google/go-github v17.0.0+incompatible // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
@@ -77,37 +76,40 @@ require (
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||
github.com/jackc/pgconn v1.11.0 // indirect
|
||||
github.com/jackc/pgconn v1.12.0 // indirect
|
||||
github.com/jackc/pgio v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.2.0 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.3.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
||||
github.com/jackc/pgtype v1.10.0 // indirect
|
||||
github.com/jackc/pgx/v4 v4.15.0 // indirect
|
||||
github.com/jinzhu/gorm v1.9.16 // indirect
|
||||
github.com/jackc/pgtype v1.11.0 // indirect
|
||||
github.com/jackc/pgx/v4 v4.16.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.4 // indirect
|
||||
github.com/josharian/native v1.0.0 // indirect
|
||||
github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/kr/pretty v0.3.0 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/lib/pq v1.10.3 // indirect
|
||||
github.com/leodido/go-urn v1.2.0 // indirect
|
||||
github.com/magiconair/properties v1.8.6 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.11 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/mdlayher/netlink v1.6.0 // indirect
|
||||
github.com/mdlayher/socket v0.2.3 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
|
||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.3 // indirect
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
|
||||
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.2 // indirect
|
||||
github.com/opencontainers/runc v1.1.0 // indirect
|
||||
github.com/opencontainers/runc v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml v1.9.4 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.0-beta.8 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
@@ -115,31 +117,32 @@ require (
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.8.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
github.com/spf13/afero v1.8.1 // indirect
|
||||
github.com/spf13/afero v1.8.2 // indirect
|
||||
github.com/spf13/cast v1.4.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.2.0 // indirect
|
||||
github.com/ugorji/go/codec v1.2.7 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/ugorji/go/codec v1.1.7 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
||||
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
|
||||
go4.org/mem v0.0.0-20210711025021-927187094b94 // indirect
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 // indirect
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect
|
||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect
|
||||
golang.org/x/net v0.0.0-20220412020605-290c469a71a5 // indirect
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
|
||||
golang.zx2c4.com/wireguard/windows v0.4.10 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
gopkg.in/ini.v1 v1.66.4 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
modernc.org/libc v1.14.5 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
|
||||
modernc.org/libc v1.14.12 // indirect
|
||||
modernc.org/mathutil v1.4.1 // indirect
|
||||
modernc.org/memory v1.0.5 // indirect
|
||||
modernc.org/sqlite v1.14.7 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
modernc.org/memory v1.0.7 // indirect
|
||||
modernc.org/sqlite v1.16.0 // indirect
|
||||
)
|
||||
|
@@ -72,7 +72,7 @@ func (s *IntegrationCLITestSuite) SetupTest() {
|
||||
if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
|
||||
s.headscale = *pheadscale
|
||||
} else {
|
||||
log.Fatalf("Could not start resource: %s", err)
|
||||
log.Fatalf("Could not start headscale container: %s", err)
|
||||
}
|
||||
fmt.Println("Created headscale container")
|
||||
|
||||
|
@@ -6,6 +6,7 @@ package headscale
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ory/dockertest/v3"
|
||||
@@ -18,8 +19,25 @@ const DOCKER_EXECUTE_TIMEOUT = 10 * time.Second
|
||||
var (
|
||||
IpPrefix4 = netaddr.MustParseIPPrefix("100.64.0.0/10")
|
||||
IpPrefix6 = netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/48")
|
||||
|
||||
tailscaleVersions = []string{
|
||||
"head",
|
||||
"unstable",
|
||||
"1.24.0",
|
||||
"1.22.2",
|
||||
"1.20.4",
|
||||
"1.18.2",
|
||||
"1.16.2",
|
||||
"1.14.3",
|
||||
"1.12.3",
|
||||
}
|
||||
)
|
||||
|
||||
type TestNamespace struct {
|
||||
count int
|
||||
tailscales map[string]dockertest.Resource
|
||||
}
|
||||
|
||||
type ExecuteCommandConfig struct {
|
||||
timeout time.Duration
|
||||
}
|
||||
@@ -119,3 +137,78 @@ func DockerAllowNetworkAdministration(config *docker.HostConfig) {
|
||||
Target: "/dev/net/tun",
|
||||
})
|
||||
}
|
||||
|
||||
func getDockerBuildOptions(version string) *dockertest.BuildOptions {
|
||||
var tailscaleBuildOptions *dockertest.BuildOptions
|
||||
switch version {
|
||||
case "head":
|
||||
tailscaleBuildOptions = &dockertest.BuildOptions{
|
||||
Dockerfile: "Dockerfile.tailscale-HEAD",
|
||||
ContextDir: ".",
|
||||
BuildArgs: []docker.BuildArg{},
|
||||
}
|
||||
case "unstable":
|
||||
tailscaleBuildOptions = &dockertest.BuildOptions{
|
||||
Dockerfile: "Dockerfile.tailscale",
|
||||
ContextDir: ".",
|
||||
BuildArgs: []docker.BuildArg{
|
||||
{
|
||||
Name: "TAILSCALE_VERSION",
|
||||
Value: "*", // Installs the latest version https://askubuntu.com/a/824926
|
||||
},
|
||||
{
|
||||
Name: "TAILSCALE_CHANNEL",
|
||||
Value: "unstable",
|
||||
},
|
||||
},
|
||||
}
|
||||
default:
|
||||
tailscaleBuildOptions = &dockertest.BuildOptions{
|
||||
Dockerfile: "Dockerfile.tailscale",
|
||||
ContextDir: ".",
|
||||
BuildArgs: []docker.BuildArg{
|
||||
{
|
||||
Name: "TAILSCALE_VERSION",
|
||||
Value: version,
|
||||
},
|
||||
{
|
||||
Name: "TAILSCALE_CHANNEL",
|
||||
Value: "stable",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
return tailscaleBuildOptions
|
||||
}
|
||||
|
||||
func getIPs(
|
||||
tailscales map[string]dockertest.Resource,
|
||||
) (map[string][]netaddr.IP, error) {
|
||||
ips := make(map[string][]netaddr.IP)
|
||||
for hostname, tailscale := range tailscales {
|
||||
command := []string{"tailscale", "ip"}
|
||||
|
||||
result, err := ExecuteCommand(
|
||||
&tailscale,
|
||||
command,
|
||||
[]string{},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, address := range strings.Split(result, "\n") {
|
||||
address = strings.TrimSuffix(address, "\n")
|
||||
if len(address) < 1 {
|
||||
continue
|
||||
}
|
||||
ip, err := netaddr.ParseIP(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ips[hostname] = append(ips[hostname], ip)
|
||||
}
|
||||
}
|
||||
|
||||
return ips, nil
|
||||
}
|
||||
|
388
integration_embedded_derp_test.go
Normal file
388
integration_embedded_derp_test.go
Normal file
@@ -0,0 +1,388 @@
|
||||
//go:build integration
|
||||
|
||||
package headscale
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
"github.com/ory/dockertest/v3"
|
||||
"github.com/ory/dockertest/v3/docker"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/ccding/go-stun/stun"
|
||||
)
|
||||
|
||||
const (
|
||||
headscaleHostname = "headscale-derp"
|
||||
namespaceName = "derpnamespace"
|
||||
totalContainers = 3
|
||||
)
|
||||
|
||||
type IntegrationDERPTestSuite struct {
|
||||
suite.Suite
|
||||
stats *suite.SuiteInformation
|
||||
|
||||
pool dockertest.Pool
|
||||
networks map[int]dockertest.Network // so we keep the containers isolated
|
||||
headscale dockertest.Resource
|
||||
|
||||
tailscales map[string]dockertest.Resource
|
||||
joinWaitGroup sync.WaitGroup
|
||||
}
|
||||
|
||||
func TestDERPIntegrationTestSuite(t *testing.T) {
|
||||
s := new(IntegrationDERPTestSuite)
|
||||
|
||||
s.tailscales = make(map[string]dockertest.Resource)
|
||||
s.networks = make(map[int]dockertest.Network)
|
||||
|
||||
suite.Run(t, s)
|
||||
|
||||
// HandleStats, which allows us to check if we passed and save logs
|
||||
// is called after TearDown, so we cannot tear down containers before
|
||||
// we have potentially saved the logs.
|
||||
for _, tailscale := range s.tailscales {
|
||||
if err := s.pool.Purge(&tailscale); err != nil {
|
||||
log.Printf("Could not purge resource: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
if !s.stats.Passed() {
|
||||
err := s.saveLog(&s.headscale, "test_output")
|
||||
if err != nil {
|
||||
log.Printf("Could not save log: %s\n", err)
|
||||
}
|
||||
}
|
||||
if err := s.pool.Purge(&s.headscale); err != nil {
|
||||
log.Printf("Could not purge resource: %s\n", err)
|
||||
}
|
||||
|
||||
for _, network := range s.networks {
|
||||
if err := network.Close(); err != nil {
|
||||
log.Printf("Could not close network: %s\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *IntegrationDERPTestSuite) SetupSuite() {
|
||||
if ppool, err := dockertest.NewPool(""); err == nil {
|
||||
s.pool = *ppool
|
||||
} else {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
}
|
||||
|
||||
for i := 0; i < totalContainers; i++ {
|
||||
if pnetwork, err := s.pool.CreateNetwork(fmt.Sprintf("headscale-derp-%d", i)); err == nil {
|
||||
s.networks[i] = *pnetwork
|
||||
} else {
|
||||
log.Fatalf("Could not create network: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
headscaleBuildOptions := &dockertest.BuildOptions{
|
||||
Dockerfile: "Dockerfile",
|
||||
ContextDir: ".",
|
||||
}
|
||||
|
||||
currentPath, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatalf("Could not determine current path: %s", err)
|
||||
}
|
||||
|
||||
headscaleOptions := &dockertest.RunOptions{
|
||||
Name: headscaleHostname,
|
||||
Mounts: []string{
|
||||
fmt.Sprintf("%s/integration_test/etc_embedded_derp:/etc/headscale", currentPath),
|
||||
},
|
||||
Cmd: []string{"headscale", "serve"},
|
||||
ExposedPorts: []string{"8443/tcp", "3478/udp"},
|
||||
PortBindings: map[docker.Port][]docker.PortBinding{
|
||||
"8443/tcp": {{HostPort: "8443"}},
|
||||
"3478/udp": {{HostPort: "3478"}},
|
||||
},
|
||||
}
|
||||
|
||||
log.Println("Creating headscale container")
|
||||
if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
|
||||
s.headscale = *pheadscale
|
||||
} else {
|
||||
log.Fatalf("Could not start headscale container: %s", err)
|
||||
}
|
||||
log.Println("Created headscale container to test DERP")
|
||||
|
||||
log.Println("Creating tailscale containers")
|
||||
|
||||
for i := 0; i < totalContainers; i++ {
|
||||
version := tailscaleVersions[i%len(tailscaleVersions)]
|
||||
hostname, container := s.tailscaleContainer(
|
||||
fmt.Sprint(i),
|
||||
version,
|
||||
s.networks[i],
|
||||
)
|
||||
s.tailscales[hostname] = *container
|
||||
}
|
||||
|
||||
log.Println("Waiting for headscale to be ready")
|
||||
hostEndpoint := fmt.Sprintf("localhost:%s", s.headscale.GetPort("8443/tcp"))
|
||||
|
||||
if err := s.pool.Retry(func() error {
|
||||
url := fmt.Sprintf("https://%s/health", hostEndpoint)
|
||||
insecureTransport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
insecureTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
client := &http.Client{Transport: insecureTransport}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("status code not OK")
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
// TODO(kradalby): If we cannot access headscale, or any other fatal error during
|
||||
// test setup, we need to abort and tear down. However, testify does not seem to
|
||||
// support that at the moment:
|
||||
// https://github.com/stretchr/testify/issues/849
|
||||
return // fmt.Errorf("Could not connect to headscale: %s", err)
|
||||
}
|
||||
log.Println("headscale container is ready")
|
||||
|
||||
log.Printf("Creating headscale namespace: %s\n", namespaceName)
|
||||
result, err := ExecuteCommand(
|
||||
&s.headscale,
|
||||
[]string{"headscale", "namespaces", "create", namespaceName},
|
||||
[]string{},
|
||||
)
|
||||
log.Println("headscale create namespace result: ", result)
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
log.Printf("Creating pre auth key for %s\n", namespaceName)
|
||||
preAuthResult, err := ExecuteCommand(
|
||||
&s.headscale,
|
||||
[]string{
|
||||
"headscale",
|
||||
"--namespace",
|
||||
namespaceName,
|
||||
"preauthkeys",
|
||||
"create",
|
||||
"--reusable",
|
||||
"--expiration",
|
||||
"24h",
|
||||
"--output",
|
||||
"json",
|
||||
},
|
||||
[]string{"LOG_LEVEL=error"},
|
||||
)
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
var preAuthKey v1.PreAuthKey
|
||||
err = json.Unmarshal([]byte(preAuthResult), &preAuthKey)
|
||||
assert.Nil(s.T(), err)
|
||||
assert.True(s.T(), preAuthKey.Reusable)
|
||||
|
||||
headscaleEndpoint := fmt.Sprintf("https://headscale:%s", s.headscale.GetPort("8443/tcp"))
|
||||
|
||||
log.Printf(
|
||||
"Joining tailscale containers to headscale at %s\n",
|
||||
headscaleEndpoint,
|
||||
)
|
||||
for hostname, tailscale := range s.tailscales {
|
||||
s.joinWaitGroup.Add(1)
|
||||
go s.Join(headscaleEndpoint, preAuthKey.Key, hostname, tailscale)
|
||||
}
|
||||
|
||||
s.joinWaitGroup.Wait()
|
||||
|
||||
// The nodes need a bit of time to get their updated maps from headscale
|
||||
// TODO: See if we can have a more deterministic wait here.
|
||||
time.Sleep(60 * time.Second)
|
||||
}
|
||||
|
||||
func (s *IntegrationDERPTestSuite) Join(
|
||||
endpoint, key, hostname string,
|
||||
tailscale dockertest.Resource,
|
||||
) {
|
||||
defer s.joinWaitGroup.Done()
|
||||
|
||||
command := []string{
|
||||
"tailscale",
|
||||
"up",
|
||||
"-login-server",
|
||||
endpoint,
|
||||
"--authkey",
|
||||
key,
|
||||
"--hostname",
|
||||
hostname,
|
||||
}
|
||||
|
||||
log.Println("Join command:", command)
|
||||
log.Printf("Running join command for %s\n", hostname)
|
||||
_, err := ExecuteCommand(
|
||||
&tailscale,
|
||||
command,
|
||||
[]string{},
|
||||
)
|
||||
assert.Nil(s.T(), err)
|
||||
log.Printf("%s joined\n", hostname)
|
||||
}
|
||||
|
||||
func (s *IntegrationDERPTestSuite) tailscaleContainer(identifier, version string, network dockertest.Network,
|
||||
) (string, *dockertest.Resource) {
|
||||
tailscaleBuildOptions := getDockerBuildOptions(version)
|
||||
|
||||
hostname := fmt.Sprintf(
|
||||
"tailscale-%s-%s",
|
||||
strings.Replace(version, ".", "-", -1),
|
||||
identifier,
|
||||
)
|
||||
tailscaleOptions := &dockertest.RunOptions{
|
||||
Name: hostname,
|
||||
Networks: []*dockertest.Network{&network},
|
||||
Cmd: []string{
|
||||
"tailscaled", "--tun=tsdev",
|
||||
},
|
||||
|
||||
// expose the host IP address, so we can access it from inside the container
|
||||
ExtraHosts: []string{"host.docker.internal:host-gateway", "headscale:host-gateway"},
|
||||
}
|
||||
|
||||
pts, err := s.pool.BuildAndRunWithBuildOptions(
|
||||
tailscaleBuildOptions,
|
||||
tailscaleOptions,
|
||||
DockerRestartPolicy,
|
||||
DockerAllowLocalIPv6,
|
||||
DockerAllowNetworkAdministration,
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not start tailscale container version %s: %s", version, err)
|
||||
}
|
||||
log.Printf("Created %s container\n", hostname)
|
||||
|
||||
return hostname, pts
|
||||
}
|
||||
|
||||
func (s *IntegrationDERPTestSuite) TearDownSuite() {
|
||||
}
|
||||
|
||||
func (s *IntegrationDERPTestSuite) HandleStats(
|
||||
suiteName string,
|
||||
stats *suite.SuiteInformation,
|
||||
) {
|
||||
s.stats = stats
|
||||
}
|
||||
|
||||
func (s *IntegrationDERPTestSuite) saveLog(
|
||||
resource *dockertest.Resource,
|
||||
basePath string,
|
||||
) error {
|
||||
err := os.MkdirAll(basePath, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
|
||||
err = s.pool.Client.Logs(
|
||||
docker.LogsOptions{
|
||||
Context: context.TODO(),
|
||||
Container: resource.Container.ID,
|
||||
OutputStream: &stdout,
|
||||
ErrorStream: &stderr,
|
||||
Tail: "all",
|
||||
RawTerminal: false,
|
||||
Stdout: true,
|
||||
Stderr: true,
|
||||
Follow: false,
|
||||
Timestamps: false,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("Saving logs for %s to %s\n", resource.Container.Name, basePath)
|
||||
|
||||
err = ioutil.WriteFile(
|
||||
path.Join(basePath, resource.Container.Name+".stdout.log"),
|
||||
[]byte(stdout.String()),
|
||||
0o644,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(
|
||||
path.Join(basePath, resource.Container.Name+".stderr.log"),
|
||||
[]byte(stdout.String()),
|
||||
0o644,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *IntegrationDERPTestSuite) TestPingAllPeersByHostname() {
|
||||
ips, err := getIPs(s.tailscales)
|
||||
assert.Nil(s.T(), err)
|
||||
for hostname, tailscale := range s.tailscales {
|
||||
for peername := range ips {
|
||||
if peername == hostname {
|
||||
continue
|
||||
}
|
||||
s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) {
|
||||
command := []string{
|
||||
"tailscale", "ping",
|
||||
"--timeout=10s",
|
||||
"--c=5",
|
||||
"--until-direct=false",
|
||||
peername,
|
||||
}
|
||||
|
||||
log.Printf(
|
||||
"Pinging using hostname from %s to %s\n",
|
||||
hostname,
|
||||
peername,
|
||||
)
|
||||
log.Println(command)
|
||||
result, err := ExecuteCommand(
|
||||
&tailscale,
|
||||
command,
|
||||
[]string{},
|
||||
)
|
||||
assert.Nil(t, err)
|
||||
log.Printf("Result for %s: %s\n", hostname, result)
|
||||
assert.Contains(t, result, "via DERP(headscale)")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *IntegrationDERPTestSuite) TestDERPSTUN() {
|
||||
headscaleSTUNAddr := fmt.Sprintf("localhost:%s", s.headscale.GetPort("3478/udp"))
|
||||
client := stun.NewClient()
|
||||
client.SetVerbose(true)
|
||||
client.SetVVerbose(true)
|
||||
client.SetServerAddr(headscaleSTUNAddr)
|
||||
_, _, err := client.Discover()
|
||||
assert.Nil(s.T(), err)
|
||||
}
|
@@ -29,13 +29,6 @@ import (
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
)
|
||||
|
||||
var tailscaleVersions = []string{"1.20.4", "1.18.2", "1.16.2", "1.14.3", "1.12.3"}
|
||||
|
||||
type TestNamespace struct {
|
||||
count int
|
||||
tailscales map[string]dockertest.Resource
|
||||
}
|
||||
|
||||
type IntegrationTestSuite struct {
|
||||
suite.Suite
|
||||
stats *suite.SuiteInformation
|
||||
@@ -54,11 +47,11 @@ func TestIntegrationTestSuite(t *testing.T) {
|
||||
|
||||
s.namespaces = map[string]TestNamespace{
|
||||
"thisspace": {
|
||||
count: 15,
|
||||
count: 10,
|
||||
tailscales: make(map[string]dockertest.Resource),
|
||||
},
|
||||
"otherspace": {
|
||||
count: 5,
|
||||
count: 3,
|
||||
tailscales: make(map[string]dockertest.Resource),
|
||||
},
|
||||
}
|
||||
@@ -175,16 +168,8 @@ func (s *IntegrationTestSuite) Join(
|
||||
func (s *IntegrationTestSuite) tailscaleContainer(
|
||||
namespace, identifier, version string,
|
||||
) (string, *dockertest.Resource) {
|
||||
tailscaleBuildOptions := &dockertest.BuildOptions{
|
||||
Dockerfile: "Dockerfile.tailscale",
|
||||
ContextDir: ".",
|
||||
BuildArgs: []docker.BuildArg{
|
||||
{
|
||||
Name: "TAILSCALE_VERSION",
|
||||
Value: version,
|
||||
},
|
||||
},
|
||||
}
|
||||
tailscaleBuildOptions := getDockerBuildOptions(version)
|
||||
|
||||
hostname := fmt.Sprintf(
|
||||
"%s-tailscale-%s-%s",
|
||||
namespace,
|
||||
@@ -207,7 +192,7 @@ func (s *IntegrationTestSuite) tailscaleContainer(
|
||||
DockerAllowNetworkAdministration,
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not start resource: %s", err)
|
||||
log.Fatalf("Could not start tailscale container version %s: %s", version, err)
|
||||
}
|
||||
log.Printf("Created %s container\n", hostname)
|
||||
|
||||
@@ -256,7 +241,7 @@ func (s *IntegrationTestSuite) SetupSuite() {
|
||||
if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
|
||||
s.headscale = *pheadscale
|
||||
} else {
|
||||
log.Fatalf("Could not start resource: %s", err)
|
||||
log.Fatalf("Could not start headscale container: %s", err)
|
||||
}
|
||||
log.Println("Created headscale container")
|
||||
|
||||
@@ -687,38 +672,6 @@ func (s *IntegrationTestSuite) TestMagicDNS() {
|
||||
}
|
||||
}
|
||||
|
||||
func getIPs(
|
||||
tailscales map[string]dockertest.Resource,
|
||||
) (map[string][]netaddr.IP, error) {
|
||||
ips := make(map[string][]netaddr.IP)
|
||||
for hostname, tailscale := range tailscales {
|
||||
command := []string{"tailscale", "ip"}
|
||||
|
||||
result, err := ExecuteCommand(
|
||||
&tailscale,
|
||||
command,
|
||||
[]string{},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, address := range strings.Split(result, "\n") {
|
||||
address = strings.TrimSuffix(address, "\n")
|
||||
if len(address) < 1 {
|
||||
continue
|
||||
}
|
||||
ip, err := netaddr.ParseIP(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ips[hostname] = append(ips[hostname], ip)
|
||||
}
|
||||
}
|
||||
|
||||
return ips, nil
|
||||
}
|
||||
|
||||
func getAPIURLs(
|
||||
tailscales map[string]dockertest.Resource,
|
||||
) (map[netaddr.IP]string, error) {
|
||||
|
@@ -14,6 +14,7 @@ dns_config:
|
||||
db_path: /tmp/integration_test_db.sqlite3
|
||||
private_key_path: private.key
|
||||
listen_addr: 0.0.0.0:8080
|
||||
metrics_listen_addr: 127.0.0.1:9090
|
||||
server_url: http://headscale:8080
|
||||
|
||||
derp:
|
||||
|
28
integration_test/etc_embedded_derp/config.yaml
Normal file
28
integration_test/etc_embedded_derp/config.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
log_level: trace
|
||||
acl_policy_path: ""
|
||||
db_type: sqlite3
|
||||
ephemeral_node_inactivity_timeout: 30m
|
||||
ip_prefixes:
|
||||
- fd7a:115c:a1e0::/48
|
||||
- 100.64.0.0/10
|
||||
dns_config:
|
||||
base_domain: headscale.net
|
||||
magic_dns: true
|
||||
domains: []
|
||||
nameservers:
|
||||
- 1.1.1.1
|
||||
db_path: /tmp/integration_test_db.sqlite3
|
||||
private_key_path: private.key
|
||||
listen_addr: 0.0.0.0:8443
|
||||
server_url: https://headscale:8443
|
||||
tls_cert_path: "/etc/headscale/tls/server.crt"
|
||||
tls_key_path: "/etc/headscale/tls/server.key"
|
||||
tls_client_auth_mode: disabled
|
||||
derp:
|
||||
server:
|
||||
enabled: true
|
||||
region_id: 999
|
||||
region_code: "headscale"
|
||||
region_name: "Headscale Embedded DERP"
|
||||
|
||||
stun_listen_addr: "0.0.0.0:3478"
|
22
integration_test/etc_embedded_derp/tls/server.crt
Normal file
22
integration_test/etc_embedded_derp/tls/server.crt
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIC8jCCAdqgAwIBAgIULbu+UbSTMG/LtxooLLh7BgSEyqEwDQYJKoZIhvcNAQEL
|
||||
BQAwFDESMBAGA1UEAwwJaGVhZHNjYWxlMCAXDTIyMDMwNTE2NDgwM1oYDzI1MjEx
|
||||
MTA0MTY0ODAzWjAUMRIwEAYDVQQDDAloZWFkc2NhbGUwggEiMA0GCSqGSIb3DQEB
|
||||
AQUAA4IBDwAwggEKAoIBAQDqcfpToLZUF0rlNwXkkt3lbyw4Cl4TJdx36o2PKaOK
|
||||
U+tze/IjRsCWeMwrcR1o9TNZcxsD+c2J48D1WATuQJlMeg+2UJXGaTGRKkkbPMy3
|
||||
5m7AFf/Q16UEOgm2NYjZaQ8faRGIMYURG/6sXmNeETJvBixpBev9yKJuVXgqHNS4
|
||||
NpEkNwdOCuAZXrmw0HCbiusawJOay4tFvhH14rav8Uimonl8UTNVXufMzyUOuoaQ
|
||||
TGflmzYX3hIoswRnTPlIWFoqObvx2Q8H+of3uQJXy0m8I6OrIoXLNxnqYMfFls79
|
||||
9SYgVc2jPsCbh5fwyRbx2Hof7sIZ1K/mNgxJRG1E3ZiLAgMBAAGjOjA4MBQGA1Ud
|
||||
EQQNMAuCCWhlYWRzY2FsZTALBgNVHQ8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUH
|
||||
AwEwDQYJKoZIhvcNAQELBQADggEBANGlVN7NCsJaKz0k0nhlRGK+tcxn2p1PXN/i
|
||||
Iy+JX8ahixPC4ocRwOhrXgb390ZXLLwq08HrWYRB/Wi1VUzCp5d8dVxvrR43dJ+v
|
||||
L2EOBiIKgcu2C3pWW1qRR46/EoXUU9kSH2VNBvIhNufi32kEOidoDzxtQf6qVCoF
|
||||
guUt1JkAqrynv1UvR/2ZRM/WzM/oJ8qfECwrwDxyYhkqU5Z5jCWg0C6kPIBvNdzt
|
||||
B0eheWS+ZxVwkePTR4e17kIafwknth3lo+orxVrq/xC+OVM1bGrt2ZyD64ZvEqQl
|
||||
w6kgbzBdLScAQptWOFThwhnJsg0UbYKimZsnYmjVEuN59TJv92M=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
(Expires on Nov 4 16:48:03 2521 GMT)
|
||||
|
28
integration_test/etc_embedded_derp/tls/server.key
Normal file
28
integration_test/etc_embedded_derp/tls/server.key
Normal file
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDqcfpToLZUF0rl
|
||||
NwXkkt3lbyw4Cl4TJdx36o2PKaOKU+tze/IjRsCWeMwrcR1o9TNZcxsD+c2J48D1
|
||||
WATuQJlMeg+2UJXGaTGRKkkbPMy35m7AFf/Q16UEOgm2NYjZaQ8faRGIMYURG/6s
|
||||
XmNeETJvBixpBev9yKJuVXgqHNS4NpEkNwdOCuAZXrmw0HCbiusawJOay4tFvhH1
|
||||
4rav8Uimonl8UTNVXufMzyUOuoaQTGflmzYX3hIoswRnTPlIWFoqObvx2Q8H+of3
|
||||
uQJXy0m8I6OrIoXLNxnqYMfFls799SYgVc2jPsCbh5fwyRbx2Hof7sIZ1K/mNgxJ
|
||||
RG1E3ZiLAgMBAAECggEBALu1Ni/u5Qy++YA8ZcN0s6UXNdhItLmv/q0kZuLQ+9et
|
||||
CT8VZfFInLndTdsaXenDKLHdryunviFA8SV+q7P2lMbek+Xs735EiyMnMBFWxLIZ
|
||||
FWNGOeQERGL19QCmLEOmEi2b+iWJQHlKaMWpbPXL3w11a+lKjIBNO4ALfoJ5QveZ
|
||||
cGMKsJdm/mpqBvLeNeh2eAFk3Gp6sT1g80Ge8NkgyzFBNIqnut0eerM15kPTc6Qz
|
||||
12JLaOXUuV3PrcB4PN4nOwrTDg88GDNOQtc1Pc9r4nOHyLfr8X7QEtj1wXSwmOuK
|
||||
d6ynMnAmoxVA9wEnupLbil1bzohRzpsTpkmDruYaBEECgYEA/Z09I8D6mt2NVqIE
|
||||
KyvLjBK39ijSV9r3/lvB2Ple2OOL5YQEd+yTrIFy+3zdUnDgD1zmNnXjmjvHZ9Lc
|
||||
IFf2o06AF84QLNB5gLPdDQkGNFdDqUxljBrfAfE3oANmPS/B0SijMGOOOiDO2FtO
|
||||
xl1nfRr78mswuRs9awoUWCdNRKUCgYEA7KaTYKIQW/FEjw9lshp74q5vbn6zoXF5
|
||||
7N8VkwI+bBVNvRbM9XZ8qhfgRdu9eXs5oL/N4mSYY54I8fA//pJ0Z2vpmureMm1V
|
||||
mL5WBUmSD9DIbAchoK+sRiQhVmNMBQC6cHMABA7RfXvBeGvWrm9pKCS6ZLgLjkjp
|
||||
PsmAcaXQcW8CgYEA2inAxljjOwUK6FNGsrxhxIT1qtNC3kCGxE+6WSNq67gSR8Vg
|
||||
8qiX//T7LEslOB3RIGYRwxd2St7RkgZZRZllmOWWWuPwFhzf6E7RAL2akLvggGov
|
||||
kG4tGEagSw2hjVDfsUT73ExHtMk0Jfmlsg33UC8+PDLpHtLH6qQpDAwC8+ECgYEA
|
||||
o+AqOIWhvHmT11l7O915Ip1WzvZwYADbxLsrDnVEUsZh4epTHjvh0kvcY6PqTqCV
|
||||
ZIrOANNWb811Nkz/k8NJVoD08PFp0xPBbZeIq/qpachTsfMyRzq/mobUiyUR9Hjv
|
||||
ooUQYr78NOApNsG+lWbTNBhS9wI4BlzZIECbcJe5g4MCgYEAndRoy8S+S0Hx/S8a
|
||||
O3hzXeDmivmgWqn8NVD4AKOovpkz4PaIVVQbAQkiNfAx8/DavPvjEKAbDezJ4ECV
|
||||
j7IsOWtDVI7pd6eF9fTcECwisrda8aUoiOap8AQb48153Vx+g2N4Vy3uH0xJs4cz
|
||||
TDALZPOBg8VlV+HEFDP43sp9Bf0=
|
||||
-----END PRIVATE KEY-----
|
@@ -41,7 +41,7 @@ type Namespace struct {
|
||||
// CreateNamespace creates a new Namespace. Returns error if could not be created
|
||||
// or another namespace already exists.
|
||||
func (h *Headscale) CreateNamespace(name string) (*Namespace, error) {
|
||||
err := CheckNamespaceName(name)
|
||||
err := CheckForFQDNRules(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -104,7 +104,7 @@ func (h *Headscale) RenameNamespace(oldName, newName string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = CheckNamespaceName(newName)
|
||||
err = CheckForFQDNRules(newName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -150,7 +150,7 @@ func (h *Headscale) ListNamespaces() ([]Namespace, error) {
|
||||
|
||||
// ListMachinesInNamespace gets all the nodes in a given namespace.
|
||||
func (h *Headscale) ListMachinesInNamespace(name string) ([]Machine, error) {
|
||||
err := CheckNamespaceName(name)
|
||||
err := CheckForFQDNRules(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -169,7 +169,7 @@ func (h *Headscale) ListMachinesInNamespace(name string) ([]Machine, error) {
|
||||
|
||||
// SetMachineNamespace assigns a Machine to a namespace.
|
||||
func (h *Headscale) SetMachineNamespace(machine *Machine, namespaceName string) error {
|
||||
err := CheckNamespaceName(namespaceName)
|
||||
err := CheckForFQDNRules(namespaceName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -237,9 +237,9 @@ func (n *Namespace) toProto() *v1.Namespace {
|
||||
}
|
||||
}
|
||||
|
||||
// NormalizeNamespaceName will replace forbidden chars in namespace
|
||||
// NormalizeToFQDNRules will replace forbidden chars in namespace
|
||||
// it can also return an error if the namespace doesn't respect RFC 952 and 1123.
|
||||
func NormalizeNamespaceName(name string, stripEmailDomain bool) (string, error) {
|
||||
func NormalizeToFQDNRules(name string, stripEmailDomain bool) (string, error) {
|
||||
name = strings.ToLower(name)
|
||||
name = strings.ReplaceAll(name, "'", "")
|
||||
atIdx := strings.Index(name, "@")
|
||||
@@ -263,7 +263,7 @@ func NormalizeNamespaceName(name string, stripEmailDomain bool) (string, error)
|
||||
return name, nil
|
||||
}
|
||||
|
||||
func CheckNamespaceName(name string) error {
|
||||
func CheckForFQDNRules(name string) error {
|
||||
if len(name) > labelHostnameLength {
|
||||
return fmt.Errorf(
|
||||
"Namespace must not be over 63 chars. %v doesn't comply with this rule: %w",
|
||||
|
@@ -233,7 +233,7 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
|
||||
c.Assert(found, check.Equals, true)
|
||||
}
|
||||
|
||||
func TestNormalizeNamespaceName(t *testing.T) {
|
||||
func TestNormalizeToFQDNRules(t *testing.T) {
|
||||
type args struct {
|
||||
name string
|
||||
stripEmailDomain bool
|
||||
@@ -310,10 +310,10 @@ func TestNormalizeNamespaceName(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := NormalizeNamespaceName(tt.args.name, tt.args.stripEmailDomain)
|
||||
got, err := NormalizeToFQDNRules(tt.args.name, tt.args.stripEmailDomain)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf(
|
||||
"NormalizeNamespaceName() error = %v, wantErr %v",
|
||||
"NormalizeToFQDNRules() error = %v, wantErr %v",
|
||||
err,
|
||||
tt.wantErr,
|
||||
)
|
||||
@@ -321,13 +321,13 @@ func TestNormalizeNamespaceName(t *testing.T) {
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("NormalizeNamespaceName() = %v, want %v", got, tt.want)
|
||||
t.Errorf("NormalizeToFQDNRules() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckNamespaceName(t *testing.T) {
|
||||
func TestCheckForFQDNRules(t *testing.T) {
|
||||
type args struct {
|
||||
name string
|
||||
}
|
||||
@@ -366,8 +366,8 @@ func TestCheckNamespaceName(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := CheckNamespaceName(tt.args.name); (err != nil) != tt.wantErr {
|
||||
t.Errorf("CheckNamespaceName() error = %v, wantErr %v", err, tt.wantErr)
|
||||
if err := CheckForFQDNRules(tt.args.name); (err != nil) != tt.wantErr {
|
||||
t.Errorf("CheckForFQDNRules() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
9
oidc.go
9
oidc.go
@@ -10,6 +10,7 @@ import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -129,6 +130,10 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
|
||||
|
||||
oauth2Token, err := h.oauth2Config.Exchange(context.Background(), code)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Caller().
|
||||
Msg("Could not exchange code for token")
|
||||
ctx.String(http.StatusBadRequest, "Could not exchange code for token")
|
||||
|
||||
return
|
||||
@@ -229,7 +234,7 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
|
||||
Str("machine", machine.Name).
|
||||
Msg("machine already registered, reauthenticating")
|
||||
|
||||
h.RefreshMachine(machine, *machine.Expiry)
|
||||
h.RefreshMachine(machine, time.Time{})
|
||||
|
||||
var content bytes.Buffer
|
||||
if err := oidcCallbackTemplate.Execute(&content, oidcCallbackTemplateConfig{
|
||||
@@ -253,7 +258,7 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
namespaceName, err := NormalizeNamespaceName(
|
||||
namespaceName, err := NormalizeToFQDNRules(
|
||||
claims.Email,
|
||||
h.cfg.OIDC.StripEmaildomain,
|
||||
)
|
||||
|
@@ -11,26 +11,118 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// AppleMobileConfig shows a simple message in the browser to point to the CLI
|
||||
// Listens in /register.
|
||||
func (h *Headscale) AppleMobileConfig(ctx *gin.Context) {
|
||||
// WindowsConfigMessage shows a simple message in the browser for how to configure the Windows Tailscale client.
|
||||
func (h *Headscale) WindowsConfigMessage(ctx *gin.Context) {
|
||||
winTemplate := template.Must(template.New("windows").Parse(`
|
||||
<html>
|
||||
<body>
|
||||
<h1>headscale</h1>
|
||||
<h2>Windows registry configuration</h2>
|
||||
<p>
|
||||
This page provides Windows registry information for the official Windows Tailscale client.
|
||||
<p>
|
||||
<p>
|
||||
The registry file will configure Tailscale to use <code>{{.URL}}</code> as its control server.
|
||||
<p>
|
||||
<h3>Caution</h3>
|
||||
<p>You should always download and inspect the registry file before installing it:</p>
|
||||
<pre><code>curl {{.URL}}/windows/tailscale.reg</code></pre>
|
||||
|
||||
<h2>Installation</h2>
|
||||
<p>Headscale can be set to the default server by running the registry file:</p>
|
||||
|
||||
<p>
|
||||
<a href="/windows/tailscale.reg" download="tailscale.reg">Windows registry file</a>
|
||||
</p>
|
||||
|
||||
<ol>
|
||||
<li>Download the registry file, then run it</li>
|
||||
<li>Follow the prompts</li>
|
||||
<li>Install and run the official windows Tailscale client</li>
|
||||
<li>When the installation has finished, start Tailscale, and log in by clicking the icon in the system tray</li>
|
||||
</ol>
|
||||
<p>Or</p>
|
||||
<p>Open command prompt with Administrator rights. Issue the following commands to add the required registry entries:</p>
|
||||
<pre>
|
||||
<code>REG ADD "HKLM\Software\Tailscale IPN" /v UnattendedMode /t REG_SZ /d always
|
||||
REG ADD "HKLM\Software\Tailscale IPN" /v LoginURL /t REG_SZ /d "{{.URL}}"</code></pre>
|
||||
<p>
|
||||
Restart Tailscale and log in.
|
||||
<p>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
|
||||
config := map[string]interface{}{
|
||||
"URL": h.cfg.ServerURL,
|
||||
}
|
||||
|
||||
var payload bytes.Buffer
|
||||
if err := winTemplate.Execute(&payload, config); err != nil {
|
||||
log.Error().
|
||||
Str("handler", "WindowsRegConfig").
|
||||
Err(err).
|
||||
Msg("Could not render Windows index template")
|
||||
ctx.Data(
|
||||
http.StatusInternalServerError,
|
||||
"text/html; charset=utf-8",
|
||||
[]byte("Could not render Windows index template"),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data(http.StatusOK, "text/html; charset=utf-8", payload.Bytes())
|
||||
}
|
||||
|
||||
// WindowsRegConfig generates and serves a .reg file configured with the Headscale server address.
|
||||
func (h *Headscale) WindowsRegConfig(ctx *gin.Context) {
|
||||
config := WindowsRegistryConfig{
|
||||
URL: h.cfg.ServerURL,
|
||||
}
|
||||
|
||||
var content bytes.Buffer
|
||||
if err := windowsRegTemplate.Execute(&content, config); err != nil {
|
||||
log.Error().
|
||||
Str("handler", "WindowsRegConfig").
|
||||
Err(err).
|
||||
Msg("Could not render Apple macOS template")
|
||||
ctx.Data(
|
||||
http.StatusInternalServerError,
|
||||
"text/html; charset=utf-8",
|
||||
[]byte("Could not render Windows registry template"),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data(
|
||||
http.StatusOK,
|
||||
"text/x-ms-regedit; charset=utf-8",
|
||||
content.Bytes(),
|
||||
)
|
||||
}
|
||||
|
||||
// AppleConfigMessage shows a simple message in the browser to point the user to the iOS/MacOS profile and instructions for how to install it.
|
||||
func (h *Headscale) AppleConfigMessage(ctx *gin.Context) {
|
||||
appleTemplate := template.Must(template.New("apple").Parse(`
|
||||
<html>
|
||||
<body>
|
||||
<h1>Apple configuration profiles</h1>
|
||||
<h1>headscale</h1>
|
||||
<h2>Apple configuration profiles</h2>
|
||||
<p>
|
||||
This page provides <a href="https://support.apple.com/guide/mdm/mdm-overview-mdmbf9e668/web">configuration profiles</a> for the official Tailscale clients for <a href="https://apps.apple.com/us/app/tailscale/id1470499037?ls=1">iOS</a> and <a href="https://apps.apple.com/ca/app/tailscale/id1475387142?mt=12">macOS</a>.
|
||||
</p>
|
||||
<p>
|
||||
The profiles will configure Tailscale.app to use {{.Url}} as its control server.
|
||||
The profiles will configure Tailscale.app to use <code>{{.URL}}</code> as its control server.
|
||||
</p>
|
||||
|
||||
<h3>Caution</h3>
|
||||
<p>You should always inspect the profile before installing it:</p>
|
||||
<p>You should always download and inspect the profile before installing it:</p>
|
||||
<!--
|
||||
<p><code>curl {{.Url}}/apple/ios</code></p>
|
||||
<pre><code>curl {{.URL}}/apple/ios</code></pre>
|
||||
-->
|
||||
<p><code>curl {{.Url}}/apple/macos</code></p>
|
||||
<pre><code>curl {{.URL}}/apple/macos</code></pre>
|
||||
|
||||
<h2>Profiles</h2>
|
||||
|
||||
@@ -192,6 +284,10 @@ func (h *Headscale) ApplePlatformConfig(ctx *gin.Context) {
|
||||
)
|
||||
}
|
||||
|
||||
type WindowsRegistryConfig struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
type AppleMobileConfig struct {
|
||||
UUID uuid.UUID
|
||||
URL string
|
||||
@@ -203,6 +299,14 @@ type AppleMobilePlatformConfig struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
var windowsRegTemplate = textTemplate.Must(
|
||||
textTemplate.New("windowsconfig").Parse(`Windows Registry Editor Version 5.00
|
||||
|
||||
[HKEY_LOCAL_MACHINE\SOFTWARE\Tailscale IPN]
|
||||
"UnattendedMode"="always"
|
||||
"LoginURL"="{{.URL}}"
|
||||
`))
|
||||
|
||||
var commonTemplate = textTemplate.Must(
|
||||
textTemplate.New("mobileconfig").Parse(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
81
poll.go
81
poll.go
@@ -83,7 +83,18 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
|
||||
Str("machine", machine.Name).
|
||||
Msg("Found machine in database")
|
||||
|
||||
machine.Name = req.Hostinfo.Hostname
|
||||
hname, err := NormalizeToFQDNRules(
|
||||
req.Hostinfo.Hostname,
|
||||
h.cfg.OIDC.StripEmaildomain,
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Str("func", "handleAuthKey").
|
||||
Str("hostinfo.name", req.Hostinfo.Hostname).
|
||||
Err(err)
|
||||
}
|
||||
machine.Name = hname
|
||||
machine.HostInfo = HostInfo(*req.Hostinfo)
|
||||
machine.DiscoKey = DiscoPublicKeyStripPrefix(req.DiscoKey)
|
||||
now := time.Now().UTC()
|
||||
@@ -164,32 +175,13 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
|
||||
Str("machine", machine.Name).
|
||||
Msg("Loading or creating update channel")
|
||||
|
||||
// TODO: could probably remove all that duplication once generics land.
|
||||
closeChanWithLog := func(channel interface{}, name string) {
|
||||
log.Trace().
|
||||
Str("handler", "PollNetMap").
|
||||
Str("machine", machine.Name).
|
||||
Str("channel", "Done").
|
||||
Msg(fmt.Sprintf("Closing %s channel", name))
|
||||
|
||||
switch c := channel.(type) {
|
||||
case (chan struct{}):
|
||||
close(c)
|
||||
|
||||
case (chan []byte):
|
||||
close(c)
|
||||
}
|
||||
}
|
||||
|
||||
const chanSize = 8
|
||||
updateChan := make(chan struct{}, chanSize)
|
||||
defer closeChanWithLog(updateChan, "updateChan")
|
||||
|
||||
pollDataChan := make(chan []byte, chanSize)
|
||||
defer closeChanWithLog(pollDataChan, "pollDataChan")
|
||||
defer closeChanWithLog(pollDataChan, machine.Name, "pollDataChan")
|
||||
|
||||
keepAliveChan := make(chan []byte)
|
||||
defer closeChanWithLog(keepAliveChan, "keepAliveChan")
|
||||
|
||||
if req.OmitPeers && !req.Stream {
|
||||
log.Info().
|
||||
@@ -262,7 +254,27 @@ func (h *Headscale) PollNetMapStream(
|
||||
updateChan chan struct{},
|
||||
) {
|
||||
{
|
||||
ctx, cancel := context.WithCancel(ctx.Request.Context())
|
||||
machine, err := h.GetMachineByMachineKey(machineKey)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
log.Warn().
|
||||
Str("handler", "PollNetMap").
|
||||
Msgf("Ignoring request, cannot find machine with key %s", machineKey.String())
|
||||
ctx.String(http.StatusUnauthorized, "")
|
||||
|
||||
return
|
||||
}
|
||||
log.Error().
|
||||
Str("handler", "PollNetMap").
|
||||
Msgf("Failed to fetch machine from the database with Machine key: %s", machineKey.String())
|
||||
ctx.String(http.StatusInternalServerError, "")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(ctx.Request.Context(), "machineName", machine.Name)
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
go h.scheduledPollWorker(
|
||||
@@ -553,8 +565,8 @@ func (h *Headscale) PollNetMapStream(
|
||||
|
||||
func (h *Headscale) scheduledPollWorker(
|
||||
ctx context.Context,
|
||||
updateChan chan<- struct{},
|
||||
keepAliveChan chan<- []byte,
|
||||
updateChan chan struct{},
|
||||
keepAliveChan chan []byte,
|
||||
machineKey key.MachinePublic,
|
||||
mapRequest tailcfg.MapRequest,
|
||||
machine *Machine,
|
||||
@@ -562,6 +574,17 @@ func (h *Headscale) scheduledPollWorker(
|
||||
keepAliveTicker := time.NewTicker(keepAliveInterval)
|
||||
updateCheckerTicker := time.NewTicker(updateCheckInterval)
|
||||
|
||||
defer closeChanWithLog(
|
||||
updateChan,
|
||||
fmt.Sprint(ctx.Value("machineName")),
|
||||
"updateChan",
|
||||
)
|
||||
defer closeChanWithLog(
|
||||
keepAliveChan,
|
||||
fmt.Sprint(ctx.Value("machineName")),
|
||||
"updateChan",
|
||||
)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -595,3 +618,13 @@ func (h *Headscale) scheduledPollWorker(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func closeChanWithLog[C chan []byte | chan struct{}](channel C, machine, name string) {
|
||||
log.Trace().
|
||||
Str("handler", "PollNetMap").
|
||||
Str("machine", machine).
|
||||
Str("channel", "Done").
|
||||
Msg(fmt.Sprintf("Closing %s channel", name))
|
||||
|
||||
close(channel)
|
||||
}
|
||||
|
@@ -1,39 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e -o pipefail
|
||||
commit="$1"
|
||||
versionglob="v[0-9].[0-9]*.[0-9]*"
|
||||
devsuffix=".dev"
|
||||
if [ -z "$commit" ]; then
|
||||
commit=`git log -n1 --first-parent "--format=format:%h"`
|
||||
fi
|
||||
|
||||
# automatically assign version
|
||||
#
|
||||
# handles the following cases:
|
||||
#
|
||||
# 0. no tags on the repository. Print "dev".
|
||||
#
|
||||
# 1. no local modifications and commit is directly tagged. Print tag.
|
||||
#
|
||||
# 2. no local modifications and commit is not tagged. Take greatest version tag in repo X.Y.Z and assign X.Y.(Z+1). Print that + $devsuffix + $timestamp.
|
||||
#
|
||||
# 3. local modifications. Print "dev".
|
||||
|
||||
tags=$(git tag)
|
||||
if [[ -z "$tags" ]]; then
|
||||
echo "dev"
|
||||
elif `git diff --quiet 2>/dev/null`; then
|
||||
tagged=$(git tag --points-at "$commit")
|
||||
if [[ -n "$tagged" ]] ; then
|
||||
echo $tagged
|
||||
else
|
||||
nearest_tag=$(git describe --tags --abbrev=0 --match "$versionglob" "$commit")
|
||||
v=$(echo $nearest_tag | perl -pe 's/(\d+)$/$1+1/e')
|
||||
isodate=$(TZ=UTC git log -n1 --format=%cd --date=iso "$commit")
|
||||
ts=$(TZ=UTC date --date="$isodate" "+%Y%m%d%H%M%S")
|
||||
echo "${v}${devsuffix}${ts}"
|
||||
fi
|
||||
else
|
||||
echo "dev"
|
||||
fi
|
12
tools.go
12
tools.go
@@ -1,12 +0,0 @@
|
||||
//go:build tools
|
||||
// +build tools
|
||||
|
||||
package tools
|
||||
|
||||
import (
|
||||
_ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway"
|
||||
_ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2"
|
||||
_ "github.com/infobloxopen/protoc-gen-gorm"
|
||||
_ "google.golang.org/grpc/cmd/protoc-gen-go-grpc"
|
||||
_ "google.golang.org/protobuf/cmd/protoc-gen-go"
|
||||
)
|
Reference in New Issue
Block a user