mirror of
https://github.com/juanfont/headscale.git
synced 2025-08-21 10:47:28 +00:00
Compare commits
140 Commits
mandatory-
...
ts2021-imp
Author | SHA1 | Date | |
---|---|---|---|
![]() |
33d0d2d900 | ||
![]() |
30d0d20029 | ||
![]() |
4ac48811e1 | ||
![]() |
1df64acbac | ||
![]() |
841f811f35 | ||
![]() |
602edcd1d7 | ||
![]() |
0d7201ad7e | ||
![]() |
ca5732a7f3 | ||
![]() |
28efd92fca | ||
![]() |
7bb87a7300 | ||
![]() |
db8db0299e | ||
![]() |
e80954b6c8 | ||
![]() |
a23035aee7 | ||
![]() |
e51e6f487f | ||
![]() |
f78deaebb6 | ||
![]() |
4d2949bda9 | ||
![]() |
cb0899b534 | ||
![]() |
ecf5259693 | ||
![]() |
970dea5d68 | ||
![]() |
cd9807a1d3 | ||
![]() |
613dc61339 | ||
![]() |
3023323528 | ||
![]() |
2dfd8a9098 | ||
![]() |
c8ed1f0f43 | ||
![]() |
f9e2ce2c8c | ||
![]() |
886e95c00d | ||
![]() |
6dd9e93346 | ||
![]() |
2dacf839dc | ||
![]() |
8f6952acee | ||
![]() |
235a90276f | ||
![]() |
5c285afda5 | ||
![]() |
ae41e3ed06 | ||
![]() |
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 | ||
![]() |
3793e1ce8b | ||
![]() |
5082975289 | ||
![]() |
fc181333e5 | ||
![]() |
38418e940f | ||
![]() |
d77cb3ba21 | ||
![]() |
928544a24f | ||
![]() |
5e44266292 | ||
![]() |
32522cb482 | ||
![]() |
6d296a195d | ||
![]() |
834f39db31 | ||
![]() |
323a7d9c2e | ||
![]() |
55ba3021f1 | ||
![]() |
ce21718454 | ||
![]() |
e271851f5c | ||
![]() |
be59e8cc3c | ||
![]() |
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 | ||
![]() |
1eafe960b8 | ||
![]() |
749c92954c | ||
![]() |
2e04abf4bb | ||
![]() |
882c0c34c1 | ||
![]() |
61ebb713f2 | ||
![]() |
ac5ad42474 | ||
![]() |
9d4822b8c7 | ||
![]() |
466d03d574 | ||
![]() |
d43fec7f96 | ||
![]() |
62f4c205f5 | ||
![]() |
003c19004d | ||
![]() |
70274d528c | ||
![]() |
6d41279781 | ||
![]() |
1e7d7e510e | ||
![]() |
a806694d23 | ||
![]() |
06d85688fd | ||
![]() |
11ccae8e52 | ||
![]() |
9a60eeaf86 | ||
![]() |
c24de595f6 | ||
![]() |
63641a7b17 | ||
![]() |
a6570d33a6 | ||
![]() |
124d8a3424 | ||
![]() |
5de9de14a9 | ||
![]() |
15f8cb5034 |
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -6,6 +6,8 @@ labels: ["bug"]
|
|||||||
assignees: ""
|
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**
|
**Bug description**
|
||||||
|
|
||||||
<!-- A clear and concise description of what the bug is. Describe the expected bahavior
|
<!-- 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"
|
url: "https://github.com/juanfont/headscale/blob/main/docs"
|
||||||
about: "Find documentation about how to configure and run headscale."
|
about: "Find documentation about how to configure and run headscale."
|
||||||
- name: "headscale Discord community"
|
- 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."
|
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: ""
|
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**
|
**Feature request**
|
||||||
|
|
||||||
<!-- A clear and precise description of what new or changed feature you want. -->
|
<!-- 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: ""
|
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 -->
|
<!-- If you have a question, please consider using our Discord for asking questions -->
|
||||||
|
|
||||||
**Issue description**
|
**Issue description**
|
||||||
|
17
.github/workflows/build.yml
vendored
17
.github/workflows/build.yml
vendored
@@ -22,30 +22,21 @@ jobs:
|
|||||||
uses: tj-actions/changed-files@v14.1
|
uses: tj-actions/changed-files@v14.1
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
|
*.nix
|
||||||
go.*
|
go.*
|
||||||
**/*.go
|
**/*.go
|
||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- name: Setup Go
|
- uses: cachix/install-nix-action@v16
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
uses: actions/setup-go@v2
|
|
||||||
with:
|
|
||||||
go-version: "1.18.0"
|
|
||||||
|
|
||||||
- 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
|
- name: Run build
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: make build
|
run: nix build
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v2
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
with:
|
with:
|
||||||
name: headscale-linux
|
name: headscale-linux
|
||||||
path: headscale
|
path: result/bin/headscale
|
||||||
|
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -16,6 +16,7 @@ jobs:
|
|||||||
uses: tj-actions/changed-files@v14.1
|
uses: tj-actions/changed-files@v14.1
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
|
*.nix
|
||||||
go.*
|
go.*
|
||||||
**/*.go
|
**/*.go
|
||||||
integration_test/
|
integration_test/
|
||||||
@@ -45,6 +46,7 @@ jobs:
|
|||||||
uses: tj-actions/changed-files@v14.1
|
uses: tj-actions/changed-files@v14.1
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
|
*.nix
|
||||||
**/*.md
|
**/*.md
|
||||||
**/*.yml
|
**/*.yml
|
||||||
**/*.yaml
|
**/*.yaml
|
||||||
|
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
|
uses: tj-actions/changed-files@v14.1
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
|
*.nix
|
||||||
go.*
|
go.*
|
||||||
**/*.go
|
**/*.go
|
||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- name: Setup Go
|
- uses: cachix/install-nix-action@v16
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
uses: actions/setup-go@v2
|
|
||||||
with:
|
|
||||||
go-version: "1.18.0"
|
|
||||||
|
|
||||||
- name: Run Integration tests
|
- name: Run Integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
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
|
uses: tj-actions/changed-files@v14.1
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
|
*.nix
|
||||||
go.*
|
go.*
|
||||||
**/*.go
|
**/*.go
|
||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- name: Setup Go
|
- uses: cachix/install-nix-action@v16
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
uses: actions/setup-go@v2
|
|
||||||
with:
|
|
||||||
go-version: "1.18.0"
|
|
||||||
|
|
||||||
- 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
|
- name: Run tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: make test
|
run: nix develop --check
|
||||||
|
|
||||||
- name: Run build
|
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
run: make
|
|
||||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@@ -27,3 +27,7 @@ derp.yaml
|
|||||||
.idea
|
.idea
|
||||||
|
|
||||||
test_output/
|
test_output/
|
||||||
|
|
||||||
|
# Nix build output
|
||||||
|
result
|
||||||
|
.direnv/
|
||||||
|
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,6 +1,14 @@
|
|||||||
# CHANGELOG
|
# 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)
|
||||||
|
|
||||||
|
## 0.15.0 (2022-03-20)
|
||||||
|
|
||||||
**Note:** Take a backup of your database before upgrading.
|
**Note:** Take a backup of your database before upgrading.
|
||||||
|
|
||||||
@@ -19,7 +27,7 @@
|
|||||||
- Users can now use emails in ACL's groups [#372](https://github.com/juanfont/headscale/issues/372)
|
- 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 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)
|
- Add `/windows` endpoint for Windows configuration instructions + registry file download [#392](https://github.com/juanfont/headscale/pull/392)
|
||||||
- Added embedded DERP server into Headscale [#388](https://github.com/juanfont/headscale/pull/388)
|
- Added embedded DERP (and STUN) server into Headscale [#388](https://github.com/juanfont/headscale/pull/388)
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
@@ -29,6 +37,8 @@
|
|||||||
- Fix a limitation in the ACLs that prevented users to write rules with `*` as source [#374](https://github.com/juanfont/headscale/issues/374)
|
- 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)
|
- 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)
|
- 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)
|
## 0.14.0 (2022-02-24)
|
||||||
|
|
||||||
|
@@ -1,11 +1,12 @@
|
|||||||
FROM ubuntu:latest
|
FROM ubuntu:latest
|
||||||
|
|
||||||
ARG TAILSCALE_VERSION
|
ARG TAILSCALE_VERSION=*
|
||||||
|
ARG TAILSCALE_CHANNEL=stable
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y gnupg curl \
|
&& 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/${TAILSCALE_CHANNEL}/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.list | tee /etc/apt/sources.list.d/tailscale.list \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get install -y ca-certificates tailscale=${TAILSCALE_VERSION} dnsutils \
|
&& apt-get install -y ca-certificates tailscale=${TAILSCALE_VERSION} dnsutils \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
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
|
8
Makefile
8
Makefile
@@ -1,5 +1,5 @@
|
|||||||
# Calculate version
|
# 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))
|
rwildcard=$(foreach d,$(wildcard $1*),$(call rwildcard,$d/,$2) $(filter $(subst *,%,$2),$d))
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ PROTO_SOURCES = $(call rwildcard,,*.proto)
|
|||||||
|
|
||||||
|
|
||||||
build:
|
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
|
dev: lint test build
|
||||||
|
|
||||||
@@ -41,14 +41,14 @@ fmt:
|
|||||||
clang-format -style="{BasedOnStyle: Google, IndentWidth: 4, AlignConsecutiveDeclarations: true, AlignConsecutiveAssignments: true, ColumnLimit: 0}" -i $(PROTO_SOURCES)
|
clang-format -style="{BasedOnStyle: Google, IndentWidth: 4, AlignConsecutiveDeclarations: true, AlignConsecutiveAssignments: true, ColumnLimit: 0}" -i $(PROTO_SOURCES)
|
||||||
|
|
||||||
proto-lint:
|
proto-lint:
|
||||||
cd proto/ && buf lint
|
cd proto/ && go run github.com/bufbuild/buf/cmd/buf lint
|
||||||
|
|
||||||
compress: build
|
compress: build
|
||||||
upx --brute headscale
|
upx --brute headscale
|
||||||
|
|
||||||
generate:
|
generate:
|
||||||
rm -rf gen
|
rm -rf gen
|
||||||
buf generate proto
|
go run github.com/bufbuild/buf/cmd/buf generate proto
|
||||||
|
|
||||||
install-protobuf-plugins:
|
install-protobuf-plugins:
|
||||||
go install \
|
go install \
|
||||||
|
95
README.md
95
README.md
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
An open source, self-hosted implementation of the Tailscale control server.
|
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
|
**Note:** Always select the same GitHub tag as the released version you use
|
||||||
to ensure you have the correct example configuration and documentation.
|
to ensure you have the correct example configuration and documentation.
|
||||||
@@ -91,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)
|
To contribute to headscale you would need the lastest version of [Go](https://golang.org)
|
||||||
and [Buf](https://buf.build)(Protobuf generator).
|
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.
|
PRs and suggestions are welcome.
|
||||||
|
|
||||||
### Code style
|
### Code style
|
||||||
@@ -115,10 +119,12 @@ Check out the `.golangci.yaml` and `Makefile` to see the specific configuration.
|
|||||||
|
|
||||||
- Go
|
- Go
|
||||||
- Buf
|
- Buf
|
||||||
- Protobuf tools:
|
- Protobuf tools
|
||||||
|
|
||||||
|
Install and activate:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
make install-protobuf-plugins
|
nix develop
|
||||||
```
|
```
|
||||||
|
|
||||||
### Testing and building
|
### Testing and building
|
||||||
@@ -140,6 +146,12 @@ make test
|
|||||||
|
|
||||||
To build the program:
|
To build the program:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
nix build
|
||||||
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
make build
|
make build
|
||||||
```
|
```
|
||||||
@@ -206,6 +218,13 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Alessandro (Ale) Segala</b></sub>
|
<sub style="font-size:14px"><b>Alessandro (Ale) Segala</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
|
<a href=https://github.com/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">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/unreality>
|
<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/>
|
<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/>
|
||||||
@@ -214,12 +233,21 @@ make build
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/reynico>
|
<a href=https://github.com/mpldr>
|
||||||
<img src=https://avatars.githubusercontent.com/u/715768?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Nico/>
|
<img src=https://avatars.githubusercontent.com/u/33086936?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Moritz Poldrack/>
|
||||||
<br />
|
<br />
|
||||||
<sub style="font-size:14px"><b>Nico</b></sub>
|
<sub style="font-size:14px"><b>Moritz Poldrack</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
|
<a href=https://github.com/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">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/negbie>
|
<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/>
|
<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/>
|
||||||
@@ -234,8 +262,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Aaron Bieber</b></sub>
|
<sub style="font-size:14px"><b>Aaron Bieber</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/fdelucchijr>
|
<a href=https://github.com/fdelucchijr>
|
||||||
<img src=https://avatars.githubusercontent.com/u/69133647?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Fernando De Lucchi/>
|
<img src=https://avatars.githubusercontent.com/u/69133647?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Fernando De Lucchi/>
|
||||||
@@ -264,6 +290,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Michael G.</b></sub>
|
<sub style="font-size:14px"><b>Michael G.</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/ptman>
|
<a href=https://github.com/ptman>
|
||||||
<img src=https://avatars.githubusercontent.com/u/24669?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Paul Tötterman/>
|
<img src=https://avatars.githubusercontent.com/u/24669?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Paul Tötterman/>
|
||||||
@@ -271,6 +299,13 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Paul Tötterman</b></sub>
|
<sub style="font-size:14px"><b>Paul Tötterman</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<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">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/cmars>
|
<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/>
|
<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/>
|
||||||
@@ -278,8 +313,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Casey Marshall</b></sub>
|
<sub style="font-size:14px"><b>Casey Marshall</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/SilverBut>
|
<a href=https://github.com/SilverBut>
|
||||||
<img src=https://avatars.githubusercontent.com/u/6560655?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Silver Bullet/>
|
<img src=https://avatars.githubusercontent.com/u/6560655?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Silver Bullet/>
|
||||||
@@ -301,6 +334,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>lachy2849</b></sub>
|
<sub style="font-size:14px"><b>lachy2849</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/t56k>
|
<a href=https://github.com/t56k>
|
||||||
<img src=https://avatars.githubusercontent.com/u/12165422?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=thomas/>
|
<img src=https://avatars.githubusercontent.com/u/12165422?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=thomas/>
|
||||||
@@ -316,14 +351,12 @@ make build
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/artemklevtsov>
|
<a href=https://github.com/aofei>
|
||||||
<img src=https://avatars.githubusercontent.com/u/603798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Artem Klevtsov/>
|
<img src=https://avatars.githubusercontent.com/u/5037285?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Aofei Sheng/>
|
||||||
<br />
|
<br />
|
||||||
<sub style="font-size:14px"><b>Artem Klevtsov</b></sub>
|
<sub style="font-size:14px"><b>Aofei Sheng</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/awoimbee>
|
<a href=https://github.com/awoimbee>
|
||||||
<img src=https://avatars.githubusercontent.com/u/22431493?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Arthur Woimbée/>
|
<img src=https://avatars.githubusercontent.com/u/22431493?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Arthur Woimbée/>
|
||||||
@@ -338,6 +371,15 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Bryan Stenson</b></sub>
|
<sub style="font-size:14px"><b>Bryan Stenson</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<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">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/fkr>
|
<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/>
|
<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/>
|
||||||
@@ -366,8 +408,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Jamie Greeff</b></sub>
|
<sub style="font-size:14px"><b>Jamie Greeff</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/jimt>
|
<a href=https://github.com/jimt>
|
||||||
<img src=https://avatars.githubusercontent.com/u/180326?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jim Tittsler/>
|
<img src=https://avatars.githubusercontent.com/u/180326?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jim Tittsler/>
|
||||||
@@ -382,6 +422,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Pierre Carru</b></sub>
|
<sub style="font-size:14px"><b>Pierre Carru</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/rcursaru>
|
<a href=https://github.com/rcursaru>
|
||||||
<img src=https://avatars.githubusercontent.com/u/16259641?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=rcursaru/>
|
<img src=https://avatars.githubusercontent.com/u/16259641?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=rcursaru/>
|
||||||
@@ -410,8 +452,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Shaanan Cohney</b></sub>
|
<sub style="font-size:14px"><b>Shaanan Cohney</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/m-tanner-dev0>
|
<a href=https://github.com/m-tanner-dev0>
|
||||||
<img src=https://avatars.githubusercontent.com/u/97977342?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tanner/>
|
<img src=https://avatars.githubusercontent.com/u/97977342?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tanner/>
|
||||||
@@ -426,6 +466,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Teteros</b></sub>
|
<sub style="font-size:14px"><b>Teteros</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/gitter-badger>
|
<a href=https://github.com/gitter-badger>
|
||||||
<img src=https://avatars.githubusercontent.com/u/8518239?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=The Gitter Badger/>
|
<img src=https://avatars.githubusercontent.com/u/8518239?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=The Gitter Badger/>
|
||||||
@@ -454,8 +496,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Yang Bin</b></sub>
|
<sub style="font-size:14px"><b>Yang Bin</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/zekker6>
|
<a href=https://github.com/zekker6>
|
||||||
<img src=https://avatars.githubusercontent.com/u/1367798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Zakhar Bessarab/>
|
<img src=https://avatars.githubusercontent.com/u/1367798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Zakhar Bessarab/>
|
||||||
@@ -470,6 +510,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>ZiYuan</b></sub>
|
<sub style="font-size:14px"><b>ZiYuan</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/bravechamp>
|
<a href=https://github.com/bravechamp>
|
||||||
<img src=https://avatars.githubusercontent.com/u/48980452?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=bravechamp/>
|
<img src=https://avatars.githubusercontent.com/u/48980452?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=bravechamp/>
|
||||||
@@ -484,6 +526,13 @@ make build
|
|||||||
<sub style="font-size:14px"><b>derelm</b></sub>
|
<sub style="font-size:14px"><b>derelm</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
|
<a href=https://github.com/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>henning mueller</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/ignoramous>
|
<a href=https://github.com/ignoramous>
|
||||||
<img src=https://avatars.githubusercontent.com/u/852289?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ignoramous/>
|
<img src=https://avatars.githubusercontent.com/u/852289?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ignoramous/>
|
||||||
@@ -498,8 +547,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>lion24</b></sub>
|
<sub style="font-size:14px"><b>lion24</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/pernila>
|
<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/>
|
<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/>
|
||||||
@@ -507,6 +554,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>pernila</b></sub>
|
<sub style="font-size:14px"><b>pernila</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/Wakeful-Cloud>
|
<a href=https://github.com/Wakeful-Cloud>
|
||||||
<img src=https://avatars.githubusercontent.com/u/38930607?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Wakeful-Cloud/>
|
<img src=https://avatars.githubusercontent.com/u/38930607?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Wakeful-Cloud/>
|
||||||
|
181
api.go
181
api.go
@@ -9,6 +9,7 @@ import (
|
|||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -21,18 +22,50 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
reservedResponseHeaderSize = 4
|
|
||||||
RegisterMethodAuthKey = "authkey"
|
|
||||||
RegisterMethodOIDC = "oidc"
|
|
||||||
RegisterMethodCLI = "cli"
|
|
||||||
ErrRegisterMethodCLIDoesNotSupportExpire = Error(
|
ErrRegisterMethodCLIDoesNotSupportExpire = Error(
|
||||||
"machines registered with CLI does not support expire",
|
"machines registered with CLI does not support expire",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
reservedResponseHeaderSize = 4
|
||||||
|
RegisterMethodAuthKey = "authkey"
|
||||||
|
RegisterMethodOIDC = "oidc"
|
||||||
|
RegisterMethodCLI = "cli"
|
||||||
|
|
||||||
|
// The CapabilityVersion is used by Tailscale clients to indicate
|
||||||
|
// their codebase version. Tailscale clients can communicate over TS2021
|
||||||
|
// from CapabilityVersion 28.
|
||||||
|
// See https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go
|
||||||
|
NoiseCapabilityVersion = 28
|
||||||
|
)
|
||||||
|
|
||||||
// KeyHandler provides the Headscale pub key
|
// KeyHandler provides the Headscale pub key
|
||||||
// Listens in /key.
|
// Listens in /key.
|
||||||
func (h *Headscale) KeyHandler(ctx *gin.Context) {
|
func (h *Headscale) KeyHandler(ctx *gin.Context) {
|
||||||
|
// New Tailscale clients send a 'v' parameter to indicate the CurrentCapabilityVersion
|
||||||
|
v := ctx.Query("v")
|
||||||
|
if v != "" {
|
||||||
|
clientCapabilityVersion, err := strconv.Atoi(v)
|
||||||
|
if err != nil {
|
||||||
|
ctx.String(http.StatusBadRequest, "Invalid version")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if clientCapabilityVersion >= NoiseCapabilityVersion {
|
||||||
|
// Tailscale has a different key for the TS2021 protocol. Not sure why.
|
||||||
|
resp := tailcfg.OverTLSPublicKeyResponse{
|
||||||
|
LegacyPublicKey: h.privateKey.Public(),
|
||||||
|
PublicKey: h.noisePrivateKey.Public(),
|
||||||
|
}
|
||||||
|
ctx.JSON(http.StatusOK, resp)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Old clients don't send a 'v' parameter, so we send the legacy public key
|
||||||
ctx.Data(
|
ctx.Data(
|
||||||
http.StatusOK,
|
http.StatusOK,
|
||||||
"text/plain; charset=utf-8",
|
"text/plain; charset=utf-8",
|
||||||
@@ -169,7 +202,7 @@ func (h *Headscale) RegistrationHandler(ctx *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
h.registrationCache.Set(
|
h.registrationCache.Set(
|
||||||
machineKeyStr,
|
NodePublicKeyStripPrefix(req.NodeKey),
|
||||||
newMachine,
|
newMachine,
|
||||||
registerCacheExpiration,
|
registerCacheExpiration,
|
||||||
)
|
)
|
||||||
@@ -288,33 +321,61 @@ func (h *Headscale) getMapResponse(
|
|||||||
Msgf("Generated map response: %s", tailMapResponseToString(resp))
|
Msgf("Generated map response: %s", tailMapResponseToString(resp))
|
||||||
|
|
||||||
var respBody []byte
|
var respBody []byte
|
||||||
if req.Compress == "zstd" {
|
if machineKey.IsZero() {
|
||||||
src, err := json.Marshal(resp)
|
// The TS2021 protocol does not rely anymore on the machine key to
|
||||||
|
// encrypt in a NaCl box the map response. We just send it back
|
||||||
|
// unencrypted via the encrypted Noise channel.
|
||||||
|
// declare the incoming size on the first 4 bytes
|
||||||
|
respBody, err := json.Marshal(resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
Caller().
|
Caller().
|
||||||
Str("func", "getMapResponse").
|
|
||||||
Err(err).
|
Err(err).
|
||||||
Msg("Failed to marshal response for the client")
|
Msg("Cannot marshal map response")
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
encoder, _ := zstd.NewWriter(nil)
|
var srcCompressed []byte
|
||||||
srcCompressed := encoder.EncodeAll(src, nil)
|
if req.Compress == "zstd" {
|
||||||
respBody = h.privateKey.SealTo(machineKey, srcCompressed)
|
encoder, _ := zstd.NewWriter(nil)
|
||||||
|
srcCompressed = encoder.EncodeAll(respBody, nil)
|
||||||
|
} else {
|
||||||
|
srcCompressed = respBody
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make([]byte, reservedResponseHeaderSize)
|
||||||
|
binary.LittleEndian.PutUint32(data, uint32(len(srcCompressed)))
|
||||||
|
data = append(data, srcCompressed...)
|
||||||
|
|
||||||
|
return data, nil
|
||||||
} else {
|
} else {
|
||||||
respBody, err = encode(resp, &machineKey, h.privateKey)
|
if req.Compress == "zstd" {
|
||||||
if err != nil {
|
src, err := json.Marshal(resp)
|
||||||
return nil, err
|
if err != nil {
|
||||||
}
|
log.Error().
|
||||||
}
|
Caller().
|
||||||
// declare the incoming size on the first 4 bytes
|
Str("func", "getMapResponse").
|
||||||
data := make([]byte, reservedResponseHeaderSize)
|
Err(err).
|
||||||
binary.LittleEndian.PutUint32(data, uint32(len(respBody)))
|
Msg("Failed to marshal response for the client")
|
||||||
data = append(data, respBody...)
|
|
||||||
|
|
||||||
return data, nil
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder, _ := zstd.NewWriter(nil)
|
||||||
|
srcCompressed := encoder.EncodeAll(src, nil)
|
||||||
|
respBody = h.privateKey.SealTo(machineKey, srcCompressed)
|
||||||
|
} else {
|
||||||
|
respBody, err = encode(resp, &machineKey, h.privateKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// declare the incoming size on the first 4 bytes
|
||||||
|
data := make([]byte, reservedResponseHeaderSize)
|
||||||
|
binary.LittleEndian.PutUint32(data, uint32(len(respBody)))
|
||||||
|
data = append(data, respBody...)
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) getMapKeepAliveResponse(
|
func (h *Headscale) getMapKeepAliveResponse(
|
||||||
@@ -326,31 +387,36 @@ func (h *Headscale) getMapKeepAliveResponse(
|
|||||||
}
|
}
|
||||||
var respBody []byte
|
var respBody []byte
|
||||||
var err error
|
var err error
|
||||||
if mapRequest.Compress == "zstd" {
|
if machineKey.IsZero() {
|
||||||
src, err := json.Marshal(mapResponse)
|
// The TS2021 protocol does not rely anymore on the machine key.
|
||||||
if err != nil {
|
return json.Marshal(mapResponse)
|
||||||
log.Error().
|
|
||||||
Caller().
|
|
||||||
Str("func", "getMapKeepAliveResponse").
|
|
||||||
Err(err).
|
|
||||||
Msg("Failed to marshal keepalive response for the client")
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
encoder, _ := zstd.NewWriter(nil)
|
|
||||||
srcCompressed := encoder.EncodeAll(src, nil)
|
|
||||||
respBody = h.privateKey.SealTo(machineKey, srcCompressed)
|
|
||||||
} else {
|
} else {
|
||||||
respBody, err = encode(mapResponse, &machineKey, h.privateKey)
|
if mapRequest.Compress == "zstd" {
|
||||||
if err != nil {
|
src, err := json.Marshal(mapResponse)
|
||||||
return nil, err
|
if err != nil {
|
||||||
}
|
log.Error().
|
||||||
}
|
Caller().
|
||||||
data := make([]byte, reservedResponseHeaderSize)
|
Str("func", "getMapKeepAliveResponse").
|
||||||
binary.LittleEndian.PutUint32(data, uint32(len(respBody)))
|
Err(err).
|
||||||
data = append(data, respBody...)
|
Msg("Failed to marshal keepalive response for the client")
|
||||||
|
|
||||||
return data, nil
|
return nil, err
|
||||||
|
}
|
||||||
|
encoder, _ := zstd.NewWriter(nil)
|
||||||
|
srcCompressed := encoder.EncodeAll(src, nil)
|
||||||
|
respBody = h.privateKey.SealTo(machineKey, srcCompressed)
|
||||||
|
} else {
|
||||||
|
respBody, err = encode(mapResponse, &machineKey, h.privateKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data := make([]byte, reservedResponseHeaderSize)
|
||||||
|
binary.LittleEndian.PutUint32(data, uint32(len(respBody)))
|
||||||
|
data = append(data, respBody...)
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) handleMachineLogOut(
|
func (h *Headscale) handleMachineLogOut(
|
||||||
@@ -411,6 +477,7 @@ func (h *Headscale) handleMachineValidRegistration(
|
|||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
machineRegistrations.WithLabelValues("update", "web", "success", machine.Namespace.Name).
|
machineRegistrations.WithLabelValues("update", "web", "success", machine.Namespace.Name).
|
||||||
Inc()
|
Inc()
|
||||||
ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
|
ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
|
||||||
@@ -437,10 +504,10 @@ func (h *Headscale) handleMachineExpired(
|
|||||||
|
|
||||||
if h.cfg.OIDC.Issuer != "" {
|
if h.cfg.OIDC.Issuer != "" {
|
||||||
resp.AuthURL = fmt.Sprintf("%s/oidc/register/%s",
|
resp.AuthURL = fmt.Sprintf("%s/oidc/register/%s",
|
||||||
strings.TrimSuffix(h.cfg.ServerURL, "/"), machineKey.String())
|
strings.TrimSuffix(h.cfg.ServerURL, "/"), machine.NodeKey)
|
||||||
} else {
|
} else {
|
||||||
resp.AuthURL = fmt.Sprintf("%s/register?key=%s",
|
resp.AuthURL = fmt.Sprintf("%s/register?key=%s",
|
||||||
strings.TrimSuffix(h.cfg.ServerURL, "/"), machineKey.String())
|
strings.TrimSuffix(h.cfg.ServerURL, "/"), machine.NodeKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
respBody, err := encode(resp, &machineKey, h.privateKey)
|
respBody, err := encode(resp, &machineKey, h.privateKey)
|
||||||
@@ -455,6 +522,7 @@ func (h *Headscale) handleMachineExpired(
|
|||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
machineRegistrations.WithLabelValues("reauth", "web", "success", machine.Namespace.Name).
|
machineRegistrations.WithLabelValues("reauth", "web", "success", machine.Namespace.Name).
|
||||||
Inc()
|
Inc()
|
||||||
ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
|
ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
|
||||||
@@ -504,13 +572,21 @@ func (h *Headscale) handleMachineRegistrationNew(
|
|||||||
resp.AuthURL = fmt.Sprintf(
|
resp.AuthURL = fmt.Sprintf(
|
||||||
"%s/oidc/register/%s",
|
"%s/oidc/register/%s",
|
||||||
strings.TrimSuffix(h.cfg.ServerURL, "/"),
|
strings.TrimSuffix(h.cfg.ServerURL, "/"),
|
||||||
machineKey.String(),
|
NodePublicKeyStripPrefix(registerRequest.NodeKey),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
resp.AuthURL = fmt.Sprintf("%s/register?key=%s",
|
resp.AuthURL = fmt.Sprintf("%s/register?key=%s",
|
||||||
strings.TrimSuffix(h.cfg.ServerURL, "/"), MachinePublicKeyStripPrefix(machineKey))
|
strings.TrimSuffix(h.cfg.ServerURL, "/"), NodePublicKeyStripPrefix(registerRequest.NodeKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if machineKey.IsZero() {
|
||||||
|
// TS2021
|
||||||
|
ctx.JSON(http.StatusOK, resp)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Tailscale legacy protocol requires to encrypt the NaCl box with the MachineKey
|
||||||
respBody, err := encode(resp, &machineKey, h.privateKey)
|
respBody, err := encode(resp, &machineKey, h.privateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
@@ -524,7 +600,6 @@ func (h *Headscale) handleMachineRegistrationNew(
|
|||||||
ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
|
ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: check if any locks are needed around IP allocation.
|
|
||||||
func (h *Headscale) handleAuthKey(
|
func (h *Headscale) handleAuthKey(
|
||||||
ctx *gin.Context,
|
ctx *gin.Context,
|
||||||
machineKey key.MachinePublic,
|
machineKey key.MachinePublic,
|
||||||
@@ -573,7 +648,7 @@ func (h *Headscale) handleAuthKey(
|
|||||||
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
|
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
|
||||||
Inc()
|
Inc()
|
||||||
} else {
|
} else {
|
||||||
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error").Inc()
|
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", "unknown").Inc()
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
63
app.go
63
app.go
@@ -81,6 +81,7 @@ type Config struct {
|
|||||||
EphemeralNodeInactivityTimeout time.Duration
|
EphemeralNodeInactivityTimeout time.Duration
|
||||||
IPPrefixes []netaddr.IPPrefix
|
IPPrefixes []netaddr.IPPrefix
|
||||||
PrivateKeyPath string
|
PrivateKeyPath string
|
||||||
|
NoisePrivateKeyPath string
|
||||||
BaseDomain string
|
BaseDomain string
|
||||||
|
|
||||||
DERP DERPConfig
|
DERP DERPConfig
|
||||||
@@ -143,12 +144,15 @@ type CLIConfig struct {
|
|||||||
|
|
||||||
// Headscale represents the base app of the service.
|
// Headscale represents the base app of the service.
|
||||||
type Headscale struct {
|
type Headscale struct {
|
||||||
cfg Config
|
cfg Config
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
dbString string
|
dbString string
|
||||||
dbType string
|
dbType string
|
||||||
dbDebug bool
|
dbDebug bool
|
||||||
privateKey *key.MachinePrivate
|
privateKey *key.MachinePrivate
|
||||||
|
noisePrivateKey *key.MachinePrivate
|
||||||
|
|
||||||
|
noiseRouter *gin.Engine
|
||||||
|
|
||||||
DERPMap *tailcfg.DERPMap
|
DERPMap *tailcfg.DERPMap
|
||||||
DERPServer *DERPServer
|
DERPServer *DERPServer
|
||||||
@@ -188,11 +192,20 @@ func LookupTLSClientAuthMode(mode string) (tls.ClientAuthType, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewHeadscale(cfg Config) (*Headscale, error) {
|
func NewHeadscale(cfg Config) (*Headscale, error) {
|
||||||
privKey, err := readOrCreatePrivateKey(cfg.PrivateKeyPath)
|
privateKey, err := readOrCreatePrivateKey(cfg.PrivateKeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to read or create private key: %w", err)
|
return nil, fmt.Errorf("failed to read or create private key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
noisePrivateKey, err := readOrCreatePrivateKey(cfg.NoisePrivateKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read or create noise private key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if privateKey.Equal(*noisePrivateKey) {
|
||||||
|
return nil, fmt.Errorf("private key and noise private key are the same")
|
||||||
|
}
|
||||||
|
|
||||||
var dbString string
|
var dbString string
|
||||||
switch cfg.DBtype {
|
switch cfg.DBtype {
|
||||||
case Postgres:
|
case Postgres:
|
||||||
@@ -219,7 +232,8 @@ func NewHeadscale(cfg Config) (*Headscale, error) {
|
|||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
dbType: cfg.DBtype,
|
dbType: cfg.DBtype,
|
||||||
dbString: dbString,
|
dbString: dbString,
|
||||||
privateKey: privKey,
|
privateKey: privateKey,
|
||||||
|
noisePrivateKey: noisePrivateKey,
|
||||||
aclRules: tailcfg.FilterAllowAll, // default allowall
|
aclRules: tailcfg.FilterAllowAll, // default allowall
|
||||||
registrationCache: registrationCache,
|
registrationCache: registrationCache,
|
||||||
}
|
}
|
||||||
@@ -259,9 +273,10 @@ func NewHeadscale(cfg Config) (*Headscale, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Redirect to our TLS url.
|
// Redirect to our TLS url.
|
||||||
func (h *Headscale) redirect(w http.ResponseWriter, req *http.Request) {
|
func (h *Headscale) redirect(ctx *gin.Context) {
|
||||||
target := h.cfg.ServerURL + req.URL.RequestURI()
|
log.Trace().Msgf("Redirecting to TLS, path %s", ctx.Request.RequestURI)
|
||||||
http.Redirect(w, req, target, http.StatusFound)
|
target := h.cfg.ServerURL + ctx.Request.RequestURI
|
||||||
|
http.Redirect(ctx.Writer, ctx.Request, target, http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
// expireEphemeralNodes deletes ephemeral machine records that have not been
|
// expireEphemeralNodes deletes ephemeral machine records that have not been
|
||||||
@@ -464,11 +479,13 @@ func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *gin.Engine {
|
|||||||
"/health",
|
"/health",
|
||||||
func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"healthy": "ok"}) },
|
func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"healthy": "ok"}) },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
router.POST(ts2021UpgradePath, h.NoiseUpgradeHandler)
|
||||||
router.GET("/key", h.KeyHandler)
|
router.GET("/key", h.KeyHandler)
|
||||||
router.GET("/register", h.RegisterWebAPI)
|
router.GET("/register", h.RegisterWebAPI)
|
||||||
router.POST("/machine/:id/map", h.PollNetMapHandler)
|
router.POST("/machine/:id/map", h.PollNetMapHandler)
|
||||||
router.POST("/machine/:id", h.RegistrationHandler)
|
router.POST("/machine/:id", h.RegistrationHandler)
|
||||||
router.GET("/oidc/register/:mkey", h.RegisterOIDC)
|
router.GET("/oidc/register/:nkey", h.RegisterOIDC)
|
||||||
router.GET("/oidc/callback", h.OIDCCallback)
|
router.GET("/oidc/callback", h.OIDCCallback)
|
||||||
router.GET("/apple", h.AppleConfigMessage)
|
router.GET("/apple", h.AppleConfigMessage)
|
||||||
router.GET("/apple/:platform", h.ApplePlatformConfig)
|
router.GET("/apple/:platform", h.ApplePlatformConfig)
|
||||||
@@ -494,6 +511,15 @@ func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *gin.Engine {
|
|||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Headscale) createNoiseRouter() *gin.Engine {
|
||||||
|
router := gin.Default()
|
||||||
|
|
||||||
|
router.POST("/machine/register", h.NoiseRegistrationHandler)
|
||||||
|
router.POST("/machine/map", h.NoisePollNetMapHandler)
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
||||||
|
|
||||||
// Serve launches a GIN server with the Headscale API.
|
// Serve launches a GIN server with the Headscale API.
|
||||||
func (h *Headscale) Serve() error {
|
func (h *Headscale) Serve() error {
|
||||||
var err error
|
var err error
|
||||||
@@ -659,8 +685,14 @@ func (h *Headscale) Serve() error {
|
|||||||
// HTTP setup
|
// HTTP setup
|
||||||
//
|
//
|
||||||
|
|
||||||
|
// This is the regular router that we expose
|
||||||
|
// over our main Addr. It also serves the legacy Tailcale API
|
||||||
router := h.createRouter(grpcGatewayMux)
|
router := h.createRouter(grpcGatewayMux)
|
||||||
|
|
||||||
|
// This router is only served over the Noise connection,
|
||||||
|
// and exposes only the new API
|
||||||
|
h.noiseRouter = h.createNoiseRouter()
|
||||||
|
|
||||||
httpServer := &http.Server{
|
httpServer := &http.Server{
|
||||||
Addr: h.cfg.Addr,
|
Addr: h.cfg.Addr,
|
||||||
Handler: router,
|
Handler: router,
|
||||||
@@ -741,10 +773,14 @@ func (h *Headscale) getTLSSettings() (*tls.Config, error) {
|
|||||||
// Configuration via autocert with HTTP-01. This requires listening on
|
// Configuration via autocert with HTTP-01. This requires listening on
|
||||||
// port 80 for the certificate validation in addition to the headscale
|
// port 80 for the certificate validation in addition to the headscale
|
||||||
// service, which can be configured to run on any other port.
|
// service, which can be configured to run on any other port.
|
||||||
|
httpRouter := gin.Default()
|
||||||
|
httpRouter.POST(ts2021UpgradePath, h.NoiseUpgradeHandler)
|
||||||
|
httpRouter.NoRoute(h.redirect)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
log.Fatal().
|
log.Fatal().
|
||||||
Caller().
|
Caller().
|
||||||
Err(http.ListenAndServe(h.cfg.TLSLetsEncryptListen, certManager.HTTPHandler(http.HandlerFunc(h.redirect)))).
|
Err(http.ListenAndServe(h.cfg.TLSLetsEncryptListen, certManager.HTTPHandler(httpRouter))).
|
||||||
Msg("failed to set up a HTTP server")
|
Msg("failed to set up a HTTP server")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -782,6 +818,7 @@ func (h *Headscale) getTLSSettings() (*tls.Config, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) setLastStateChangeToNow(namespace string) {
|
func (h *Headscale) setLastStateChangeToNow(namespace string) {
|
||||||
|
log.Trace().Msgf("setting last state change to now for namespace %s", namespace)
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
lastStateUpdate.WithLabelValues("", "headscale").Set(float64(now.Unix()))
|
lastStateUpdate.WithLabelValues("", "headscale").Set(float64(now.Unix()))
|
||||||
h.lastStateChange.Store(namespace, now)
|
h.lastStateChange.Store(namespace, now)
|
||||||
|
@@ -23,7 +23,7 @@ func init() {
|
|||||||
apiKeysCmd.AddCommand(listAPIKeys)
|
apiKeysCmd.AddCommand(listAPIKeys)
|
||||||
|
|
||||||
createAPIKeyCmd.Flags().
|
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)
|
apiKeysCmd.AddCommand(createAPIKeyCmd)
|
||||||
|
|
||||||
|
@@ -31,7 +31,7 @@ func init() {
|
|||||||
createPreAuthKeyCmd.PersistentFlags().
|
createPreAuthKeyCmd.PersistentFlags().
|
||||||
Bool("ephemeral", false, "Preauthkey for ephemeral nodes")
|
Bool("ephemeral", false, "Preauthkey for ephemeral nodes")
|
||||||
createPreAuthKeyCmd.Flags().
|
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{
|
var preauthkeysCmd = &cobra.Command{
|
||||||
|
@@ -326,9 +326,10 @@ func getHeadscaleConfig() headscale.Config {
|
|||||||
GRPCAddr: viper.GetString("grpc_listen_addr"),
|
GRPCAddr: viper.GetString("grpc_listen_addr"),
|
||||||
GRPCAllowInsecure: viper.GetBool("grpc_allow_insecure"),
|
GRPCAllowInsecure: viper.GetBool("grpc_allow_insecure"),
|
||||||
|
|
||||||
IPPrefixes: prefixes,
|
IPPrefixes: prefixes,
|
||||||
PrivateKeyPath: absPath(viper.GetString("private_key_path")),
|
PrivateKeyPath: absPath(viper.GetString("private_key_path")),
|
||||||
BaseDomain: baseDomain,
|
NoisePrivateKeyPath: absPath(viper.GetString("noise_private_key_path")),
|
||||||
|
BaseDomain: baseDomain,
|
||||||
|
|
||||||
DERP: derpConfig,
|
DERP: derpConfig,
|
||||||
|
|
||||||
@@ -408,7 +409,7 @@ func getHeadscaleApp() (*headscale.Headscale, error) {
|
|||||||
aclPath := absPath(viper.GetString("acl_policy_path"))
|
aclPath := absPath(viper.GetString("acl_policy_path"))
|
||||||
err = app.LoadACLPolicy(aclPath)
|
err = app.LoadACLPolicy(aclPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Fatal().
|
||||||
Str("path", aclPath).
|
Str("path", aclPath).
|
||||||
Err(err).
|
Err(err).
|
||||||
Msg("Could not load the ACL policy")
|
Msg("Could not load the ACL policy")
|
||||||
|
@@ -41,6 +41,13 @@ grpc_allow_insecure: false
|
|||||||
# autogenerated if it's missing
|
# autogenerated if it's missing
|
||||||
private_key_path: /var/lib/headscale/private.key
|
private_key_path: /var/lib/headscale/private.key
|
||||||
|
|
||||||
|
# The Noise private key is used to encrypt the
|
||||||
|
# traffic between headscale and Tailscale clients when
|
||||||
|
# using the new Noise-based TS2021 protocol.
|
||||||
|
# The noise private key file which will be
|
||||||
|
# autogenerated if it's missing
|
||||||
|
noise_private_key_path: /var/lib/headscale/noise_private.key
|
||||||
|
|
||||||
# List of IP prefixes to allocate tailaddresses from.
|
# List of IP prefixes to allocate tailaddresses from.
|
||||||
# Each prefix consists of either an IPv4 or IPv6 address,
|
# Each prefix consists of either an IPv4 or IPv6 address,
|
||||||
# and the associated prefix length, delimited by a slash.
|
# and the associated prefix length, delimited by a slash.
|
||||||
|
@@ -12,4 +12,4 @@ regions:
|
|||||||
ipv6: "2604:a880:400:d1::828:b001"
|
ipv6: "2604:a880:400:d1::828:b001"
|
||||||
stunport: 0
|
stunport: 0
|
||||||
stunonly: false
|
stunonly: false
|
||||||
derptestport: 0
|
derpport: 0
|
||||||
|
4
derp.go
4
derp.go
@@ -148,7 +148,9 @@ func (h *Headscale) scheduledDERPMapUpdateWorker(cancelChan <-chan struct{}) {
|
|||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
log.Info().Msg("Fetching DERPMap updates")
|
log.Info().Msg("Fetching DERPMap updates")
|
||||||
h.DERPMap = GetDERPMap(h.cfg.DERP)
|
h.DERPMap = GetDERPMap(h.cfg.DERP)
|
||||||
h.DERPMap.Regions[h.DERPServer.region.RegionID] = &h.DERPServer.region
|
if h.cfg.DERP.ServerEnabled {
|
||||||
|
h.DERPMap.Regions[h.DERPServer.region.RegionID] = &h.DERPServer.region
|
||||||
|
}
|
||||||
|
|
||||||
namespaces, err := h.ListNamespaces()
|
namespaces, err := h.ListNamespaces()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
This page contains the official and community contributed documentation for `headscale`.
|
This page contains the official and community contributed documentation for `headscale`.
|
||||||
|
|
||||||
If you are having trouble with following the documentation or get unexpected results,
|
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
|
## Official documentation
|
||||||
|
|
||||||
|
@@ -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:
|
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
|
```shell
|
||||||
mkdir ./headscale && cd ./headscale
|
mkdir -p ./headscale/config
|
||||||
mkdir ./config
|
cd ./headscale
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Create an empty SQlite datebase in the headscale directory:
|
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.
|
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:
|
4. Start the headscale server while working in the host headscale directory:
|
||||||
|
|
||||||
@@ -61,6 +72,8 @@ docker run \
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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
|
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.
|
`headscale` instance becomes available and then detach so headscale runs in the background.
|
||||||
|
|
||||||
@@ -87,7 +100,8 @@ curl http://127.0.0.1:9090/metrics
|
|||||||
6. Create a namespace ([tailnet](https://tailscale.com/kb/1136/tailnet/)):
|
6. Create a namespace ([tailnet](https://tailscale.com/kb/1136/tailnet/)):
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker exec headscale -- headscale namespaces create myfirstnamespace
|
docker exec headscale \
|
||||||
|
headscale namespaces create myfirstnamespace
|
||||||
```
|
```
|
||||||
|
|
||||||
### Register a machine (normal login)
|
### Register a machine (normal login)
|
||||||
@@ -101,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:
|
To register a machine when running `headscale` in a container, take the headscale command and pass it to the container:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker exec headscale -- \
|
docker exec headscale \
|
||||||
headscale --namespace myfirstnamespace nodes register --key <YOU_+MACHINE_KEY>
|
headscale --namespace myfirstnamespace nodes register --key <YOU_+MACHINE_KEY>
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -110,7 +124,7 @@ docker exec headscale -- \
|
|||||||
Generate a key using the command line:
|
Generate a key using the command line:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker exec headscale -- \
|
docker exec headscale \
|
||||||
headscale --namespace myfirstnamespace preauthkeys create --reusable --expiration 24h
|
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)
|
# Directory for Database, and other variable data (like certificates)
|
||||||
mkdir -p /var/lib/headscale
|
mkdir -p /var/lib/headscale
|
||||||
|
# or if you create a headscale user:
|
||||||
|
useradd \
|
||||||
|
--create-home \
|
||||||
|
--home-dir /var/lib/headscale/ \
|
||||||
|
--system \
|
||||||
|
--user-group \
|
||||||
|
--shell /usr/bin/nologin \
|
||||||
|
headscale
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Create an empty SQLite database:
|
4. Create an empty SQLite database:
|
||||||
@@ -50,7 +58,7 @@ from the [headscale repository](../)
|
|||||||
6. Start the headscale server:
|
6. Start the headscale server:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
headscale serve
|
headscale serve
|
||||||
```
|
```
|
||||||
|
|
||||||
This command will start `headscale` in the current terminal session.
|
This command will start `headscale` in the current terminal session.
|
||||||
@@ -150,7 +158,7 @@ or run all headscale commands as the headscale user:
|
|||||||
su - headscale
|
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
|
```yaml
|
||||||
unix_socket: /var/run/headscale/headscale.sock
|
unix_socket: /var/run/headscale/headscale.sock
|
||||||
@@ -165,8 +173,7 @@ systemctl daemon-reload
|
|||||||
4. Enable and start the new `headscale` service:
|
4. Enable and start the new `headscale` service:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
systemctl enable headscale
|
systemctl enable --now headscale
|
||||||
systemctl start headscale
|
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Verify the headscale service:
|
5. Verify the headscale service:
|
||||||
@@ -178,7 +185,7 @@ systemctl status headscale
|
|||||||
Verify `headscale` is available:
|
Verify `headscale` is available:
|
||||||
|
|
||||||
```shell
|
```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.
|
`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 ${./.}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
107
go.mod
107
go.mod
@@ -3,72 +3,70 @@ module github.com/juanfont/headscale
|
|||||||
go 1.18
|
go 1.18
|
||||||
|
|
||||||
require (
|
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/ccding/go-stun/stun v0.0.0-20200514191101-4dc67bcdb029
|
||||||
github.com/coreos/go-oidc/v3 v3.1.0
|
github.com/coreos/go-oidc/v3 v3.1.0
|
||||||
github.com/efekarakus/termcolor v1.0.1
|
github.com/efekarakus/termcolor v1.0.1
|
||||||
github.com/fatih/set v0.2.1
|
github.com/fatih/set v0.2.1
|
||||||
github.com/gin-gonic/gin v1.7.7
|
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/gofrs/uuid v4.2.0+incompatible
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
|
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.3
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.10.0
|
||||||
github.com/infobloxopen/protoc-gen-gorm v1.1.0
|
github.com/klauspost/compress v1.15.1
|
||||||
github.com/klauspost/compress v1.14.4
|
|
||||||
github.com/ory/dockertest/v3 v3.8.1
|
github.com/ory/dockertest/v3 v3.8.1
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
github.com/philip-bui/grpc-zerolog v1.0.1
|
github.com/philip-bui/grpc-zerolog v1.0.1
|
||||||
github.com/prometheus/client_golang v1.12.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/rs/zerolog v1.26.1
|
||||||
github.com/spf13/cobra v1.3.0
|
github.com/spf13/cobra v1.4.0
|
||||||
github.com/spf13/viper v1.10.1
|
github.com/spf13/viper v1.11.0
|
||||||
github.com/stretchr/testify v1.7.0
|
github.com/stretchr/testify v1.7.1
|
||||||
github.com/tailscale/hujson v0.0.0-20211215203138-ffd971c5f362
|
github.com/tailscale/hujson v0.0.0-20220421170326-6583d0610064
|
||||||
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
|
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
|
||||||
github.com/zsais/go-gin-prometheus v0.1.0
|
github.com/zsais/go-gin-prometheus v0.1.0
|
||||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292
|
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4
|
||||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b
|
golang.org/x/net v0.0.0-20220412020605-290c469a71a5
|
||||||
|
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||||
google.golang.org/genproto v0.0.0-20220228195345-15d65a4533f7
|
google.golang.org/genproto v0.0.0-20220422154200-b37d22cd5731
|
||||||
google.golang.org/grpc v1.44.0
|
google.golang.org/grpc v1.46.0
|
||||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0
|
google.golang.org/protobuf v1.28.0
|
||||||
google.golang.org/protobuf v1.27.1
|
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||||
gorm.io/driver/postgres v1.3.1
|
gorm.io/driver/postgres v1.3.5
|
||||||
gorm.io/gorm v1.23.1
|
gorm.io/gorm v1.23.4
|
||||||
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6
|
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6
|
||||||
tailscale.com v1.22.0
|
tailscale.com v1.24.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
|
||||||
github.com/Microsoft/go-winio v0.5.2 // indirect
|
github.com/Microsoft/go-winio v0.5.1 // indirect
|
||||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
||||||
github.com/akutz/memconn v0.1.0 // indirect
|
github.com/akutz/memconn v0.1.0 // indirect
|
||||||
|
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect
|
||||||
github.com/atomicgo/cursor v0.0.1 // indirect
|
github.com/atomicgo/cursor v0.0.1 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.1.2 // indirect
|
github.com/cenkalti/backoff/v4 v4.1.2 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.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/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/denisenkom/go-mssqldb v0.12.0 // indirect
|
github.com/docker/cli v20.10.11+incompatible // indirect
|
||||||
github.com/docker/cli v20.10.12+incompatible // indirect
|
github.com/docker/docker v20.10.7+incompatible // indirect
|
||||||
github.com/docker/docker v20.10.12+incompatible // indirect
|
|
||||||
github.com/docker/go-connections v0.4.0 // indirect
|
github.com/docker/go-connections v0.4.0 // indirect
|
||||||
github.com/docker/go-units v0.4.0 // indirect
|
github.com/docker/go-units v0.4.0 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/glebarez/go-sqlite v1.14.8 // indirect
|
github.com/glebarez/go-sqlite v1.16.0 // indirect
|
||||||
github.com/go-playground/locales v0.14.0 // indirect
|
github.com/go-playground/locales v0.13.0 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
github.com/go-playground/universal-translator v0.17.0 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.10.0 // indirect
|
github.com/go-playground/validator/v10 v10.4.1 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.6.0 // indirect
|
|
||||||
github.com/gogo/protobuf v1.3.2 // 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/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-github v17.0.0+incompatible // indirect
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||||
@@ -79,38 +77,40 @@ require (
|
|||||||
github.com/imdario/mergo v0.3.12 // indirect
|
github.com/imdario/mergo v0.3.12 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||||
github.com/jackc/chunkreader/v2 v2.0.1 // 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/pgio v1.0.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgproto3/v2 v2.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/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
||||||
github.com/jackc/pgtype v1.10.0 // indirect
|
github.com/jackc/pgtype v1.11.0 // indirect
|
||||||
github.com/jackc/pgx/v4 v4.15.0 // indirect
|
github.com/jackc/pgx/v4 v4.16.0 // indirect
|
||||||
github.com/jinzhu/gorm v1.9.16 // indirect
|
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.4 // 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/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||||
github.com/kr/pretty v0.3.0 // indirect
|
github.com/kr/pretty v0.3.0 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/leodido/go-urn v1.2.1 // indirect
|
github.com/leodido/go-urn v1.2.0 // indirect
|
||||||
github.com/lib/pq v1.10.3 // indirect
|
|
||||||
github.com/magiconair/properties v1.8.6 // indirect
|
github.com/magiconair/properties v1.8.6 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.13 // 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/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/go-ps v1.0.0 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.4.3 // 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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // 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/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 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/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_model v0.2.0 // indirect
|
github.com/prometheus/client_model v0.2.0 // indirect
|
||||||
@@ -118,32 +118,31 @@ require (
|
|||||||
github.com/prometheus/procfs v0.7.3 // indirect
|
github.com/prometheus/procfs v0.7.3 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||||
github.com/rivo/uniseg v0.2.0 // 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/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/cast v1.4.1 // indirect
|
||||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/subosito/gotenv v1.2.0 // indirect
|
github.com/subosito/gotenv v1.2.0 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.7 // indirect
|
github.com/ugorji/go/codec v1.1.7 // indirect
|
||||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
|
||||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
||||||
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
|
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
|
||||||
go4.org/mem v0.0.0-20210711025021-927187094b94 // indirect
|
go4.org/mem v0.0.0-20210711025021-927187094b94 // indirect
|
||||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 // 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-20220412211240-33da011f77ad // indirect
|
||||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect
|
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||||
golang.org/x/text v0.3.7 // indirect
|
golang.org/x/text v0.3.7 // indirect
|
||||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
|
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
|
||||||
|
golang.zx2c4.com/wireguard/windows v0.4.10 // indirect
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
gopkg.in/ini.v1 v1.66.4 // indirect
|
gopkg.in/ini.v1 v1.66.4 // indirect
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
|
||||||
modernc.org/libc v1.14.5 // indirect
|
modernc.org/libc v1.14.12 // indirect
|
||||||
modernc.org/mathutil v1.4.1 // indirect
|
modernc.org/mathutil v1.4.1 // indirect
|
||||||
modernc.org/memory v1.0.5 // indirect
|
modernc.org/memory v1.0.7 // indirect
|
||||||
modernc.org/sqlite v1.14.7 // indirect
|
modernc.org/sqlite v1.16.0 // indirect
|
||||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
|
||||||
)
|
)
|
||||||
|
@@ -5,9 +5,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/juanfont/headscale/gen/go/headscale/v1"
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
|
"tailscale.com/types/key"
|
||||||
)
|
)
|
||||||
|
|
||||||
type headscaleV1APIServer struct { // v1.HeadscaleServiceServer
|
type headscaleV1APIServer struct { // v1.HeadscaleServiceServer
|
||||||
@@ -373,6 +374,7 @@ func (api headscaleV1APIServer) DebugCreateMachine(
|
|||||||
MachineKey: request.GetKey(),
|
MachineKey: request.GetKey(),
|
||||||
Name: request.GetName(),
|
Name: request.GetName(),
|
||||||
Namespace: *namespace,
|
Namespace: *namespace,
|
||||||
|
NodeKey: key.NewNode().Public().String(),
|
||||||
|
|
||||||
Expiry: &time.Time{},
|
Expiry: &time.Time{},
|
||||||
LastSeen: &time.Time{},
|
LastSeen: &time.Time{},
|
||||||
@@ -382,7 +384,7 @@ func (api headscaleV1APIServer) DebugCreateMachine(
|
|||||||
}
|
}
|
||||||
|
|
||||||
api.h.registrationCache.Set(
|
api.h.registrationCache.Set(
|
||||||
request.GetKey(),
|
newMachine.NodeKey,
|
||||||
newMachine,
|
newMachine,
|
||||||
registerCacheExpiration,
|
registerCacheExpiration,
|
||||||
)
|
)
|
||||||
|
@@ -72,7 +72,7 @@ func (s *IntegrationCLITestSuite) SetupTest() {
|
|||||||
if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
|
if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
|
||||||
s.headscale = *pheadscale
|
s.headscale = *pheadscale
|
||||||
} else {
|
} else {
|
||||||
log.Fatalf("Could not start resource: %s", err)
|
log.Fatalf("Could not start headscale container: %s", err)
|
||||||
}
|
}
|
||||||
fmt.Println("Created headscale container")
|
fmt.Println("Created headscale container")
|
||||||
|
|
||||||
|
@@ -20,7 +20,17 @@ var (
|
|||||||
IpPrefix4 = netaddr.MustParseIPPrefix("100.64.0.0/10")
|
IpPrefix4 = netaddr.MustParseIPPrefix("100.64.0.0/10")
|
||||||
IpPrefix6 = netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/48")
|
IpPrefix6 = netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/48")
|
||||||
|
|
||||||
tailscaleVersions = []string{"1.22.0", "1.20.4", "1.18.2", "1.16.2", "1.14.3", "1.12.3"}
|
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 {
|
type TestNamespace struct {
|
||||||
@@ -128,6 +138,49 @@ func DockerAllowNetworkAdministration(config *docker.HostConfig) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
func getIPs(
|
||||||
tailscales map[string]dockertest.Resource,
|
tailscales map[string]dockertest.Resource,
|
||||||
) (map[string][]netaddr.IP, error) {
|
) (map[string][]netaddr.IP, error) {
|
||||||
|
@@ -121,7 +121,7 @@ func (s *IntegrationDERPTestSuite) SetupSuite() {
|
|||||||
if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
|
if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
|
||||||
s.headscale = *pheadscale
|
s.headscale = *pheadscale
|
||||||
} else {
|
} else {
|
||||||
log.Fatalf("Could not start resource: %s", err)
|
log.Fatalf("Could not start headscale container: %s", err)
|
||||||
}
|
}
|
||||||
log.Println("Created headscale container to test DERP")
|
log.Println("Created headscale container to test DERP")
|
||||||
|
|
||||||
@@ -245,16 +245,8 @@ func (s *IntegrationDERPTestSuite) Join(
|
|||||||
|
|
||||||
func (s *IntegrationDERPTestSuite) tailscaleContainer(identifier, version string, network dockertest.Network,
|
func (s *IntegrationDERPTestSuite) tailscaleContainer(identifier, version string, network dockertest.Network,
|
||||||
) (string, *dockertest.Resource) {
|
) (string, *dockertest.Resource) {
|
||||||
tailscaleBuildOptions := &dockertest.BuildOptions{
|
tailscaleBuildOptions := getDockerBuildOptions(version)
|
||||||
Dockerfile: "Dockerfile.tailscale",
|
|
||||||
ContextDir: ".",
|
|
||||||
BuildArgs: []docker.BuildArg{
|
|
||||||
{
|
|
||||||
Name: "TAILSCALE_VERSION",
|
|
||||||
Value: version,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
hostname := fmt.Sprintf(
|
hostname := fmt.Sprintf(
|
||||||
"tailscale-%s-%s",
|
"tailscale-%s-%s",
|
||||||
strings.Replace(version, ".", "-", -1),
|
strings.Replace(version, ".", "-", -1),
|
||||||
@@ -279,7 +271,7 @@ func (s *IntegrationDERPTestSuite) tailscaleContainer(identifier, version string
|
|||||||
DockerAllowNetworkAdministration,
|
DockerAllowNetworkAdministration,
|
||||||
)
|
)
|
||||||
if err != nil {
|
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)
|
log.Printf("Created %s container\n", hostname)
|
||||||
|
|
||||||
|
@@ -168,16 +168,8 @@ func (s *IntegrationTestSuite) Join(
|
|||||||
func (s *IntegrationTestSuite) tailscaleContainer(
|
func (s *IntegrationTestSuite) tailscaleContainer(
|
||||||
namespace, identifier, version string,
|
namespace, identifier, version string,
|
||||||
) (string, *dockertest.Resource) {
|
) (string, *dockertest.Resource) {
|
||||||
tailscaleBuildOptions := &dockertest.BuildOptions{
|
tailscaleBuildOptions := getDockerBuildOptions(version)
|
||||||
Dockerfile: "Dockerfile.tailscale",
|
|
||||||
ContextDir: ".",
|
|
||||||
BuildArgs: []docker.BuildArg{
|
|
||||||
{
|
|
||||||
Name: "TAILSCALE_VERSION",
|
|
||||||
Value: version,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
hostname := fmt.Sprintf(
|
hostname := fmt.Sprintf(
|
||||||
"%s-tailscale-%s-%s",
|
"%s-tailscale-%s-%s",
|
||||||
namespace,
|
namespace,
|
||||||
@@ -200,7 +192,7 @@ func (s *IntegrationTestSuite) tailscaleContainer(
|
|||||||
DockerAllowNetworkAdministration,
|
DockerAllowNetworkAdministration,
|
||||||
)
|
)
|
||||||
if err != nil {
|
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)
|
log.Printf("Created %s container\n", hostname)
|
||||||
|
|
||||||
@@ -249,7 +241,7 @@ func (s *IntegrationTestSuite) SetupSuite() {
|
|||||||
if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
|
if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
|
||||||
s.headscale = *pheadscale
|
s.headscale = *pheadscale
|
||||||
} else {
|
} else {
|
||||||
log.Fatalf("Could not start resource: %s", err)
|
log.Fatalf("Could not start headscale container: %s", err)
|
||||||
}
|
}
|
||||||
log.Println("Created headscale container")
|
log.Println("Created headscale container")
|
||||||
|
|
||||||
|
@@ -13,6 +13,7 @@ dns_config:
|
|||||||
- 1.1.1.1
|
- 1.1.1.1
|
||||||
db_path: /tmp/integration_test_db.sqlite3
|
db_path: /tmp/integration_test_db.sqlite3
|
||||||
private_key_path: private.key
|
private_key_path: private.key
|
||||||
|
noise_private_key_path: noise_private.key
|
||||||
listen_addr: 0.0.0.0:8080
|
listen_addr: 0.0.0.0:8080
|
||||||
metrics_listen_addr: 127.0.0.1:9090
|
metrics_listen_addr: 127.0.0.1:9090
|
||||||
server_url: http://headscale:8080
|
server_url: http://headscale:8080
|
||||||
|
@@ -13,6 +13,7 @@ dns_config:
|
|||||||
- 1.1.1.1
|
- 1.1.1.1
|
||||||
db_path: /tmp/integration_test_db.sqlite3
|
db_path: /tmp/integration_test_db.sqlite3
|
||||||
private_key_path: private.key
|
private_key_path: private.key
|
||||||
|
noise_private_key_path: noise_private.key
|
||||||
listen_addr: 0.0.0.0:8443
|
listen_addr: 0.0.0.0:8443
|
||||||
server_url: https://headscale:8443
|
server_url: https://headscale:8443
|
||||||
tls_cert_path: "/etc/headscale/tls/server.crt"
|
tls_cert_path: "/etc/headscale/tls/server.crt"
|
||||||
|
36
machine.go
36
machine.go
@@ -335,7 +335,7 @@ func (h *Headscale) GetMachineByID(id uint64) (*Machine, error) {
|
|||||||
return &m, nil
|
return &m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMachineByMachineKey finds a Machine by ID and returns the Machine struct.
|
// GetMachineByMachineKey finds a Machine by its MachineKey and returns the Machine struct.
|
||||||
func (h *Headscale) GetMachineByMachineKey(
|
func (h *Headscale) GetMachineByMachineKey(
|
||||||
machineKey key.MachinePublic,
|
machineKey key.MachinePublic,
|
||||||
) (*Machine, error) {
|
) (*Machine, error) {
|
||||||
@@ -347,6 +347,19 @@ func (h *Headscale) GetMachineByMachineKey(
|
|||||||
return &m, nil
|
return &m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetMachineByNodeKeys finds a Machine by its current NodeKey or the old one, and returns the Machine struct.
|
||||||
|
func (h *Headscale) GetMachineByNodeKeys(
|
||||||
|
nodeKey key.NodePublic, oldNodeKey key.NodePublic,
|
||||||
|
) (*Machine, error) {
|
||||||
|
m := Machine{}
|
||||||
|
if result := h.db.Preload("Namespace").First(&m, "node_key = ? OR node_key = ?",
|
||||||
|
NodePublicKeyStripPrefix(nodeKey), NodePublicKeyStripPrefix(oldNodeKey)); result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return &m, nil
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateMachine takes a Machine struct pointer (typically already loaded from database
|
// UpdateMachine takes a Machine struct pointer (typically already loaded from database
|
||||||
// and updates it with the latest data from the database.
|
// and updates it with the latest data from the database.
|
||||||
func (h *Headscale) UpdateMachine(machine *Machine) error {
|
func (h *Headscale) UpdateMachine(machine *Machine) error {
|
||||||
@@ -362,6 +375,7 @@ func (h *Headscale) ExpireMachine(machine *Machine) {
|
|||||||
now := time.Now()
|
now := time.Now()
|
||||||
machine.Expiry = &now
|
machine.Expiry = &now
|
||||||
|
|
||||||
|
log.Trace().Msgf("Expiring machine %s", machine.Name)
|
||||||
h.setLastStateChangeToNow(machine.Namespace.Name)
|
h.setLastStateChangeToNow(machine.Namespace.Name)
|
||||||
|
|
||||||
h.db.Save(machine)
|
h.db.Save(machine)
|
||||||
@@ -374,6 +388,7 @@ func (h *Headscale) RefreshMachine(machine *Machine, expiry time.Time) {
|
|||||||
machine.LastSuccessfulUpdate = &now
|
machine.LastSuccessfulUpdate = &now
|
||||||
machine.Expiry = &expiry
|
machine.Expiry = &expiry
|
||||||
|
|
||||||
|
log.Trace().Msgf("Refreshing machine %s", machine.Name)
|
||||||
h.setLastStateChangeToNow(machine.Namespace.Name)
|
h.setLastStateChangeToNow(machine.Namespace.Name)
|
||||||
|
|
||||||
h.db.Save(machine)
|
h.db.Save(machine)
|
||||||
@@ -505,11 +520,14 @@ func (machine Machine) toNode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var machineKey key.MachinePublic
|
var machineKey key.MachinePublic
|
||||||
err = machineKey.UnmarshalText(
|
if machine.MachineKey != "" {
|
||||||
[]byte(MachinePublicKeyEnsurePrefix(machine.MachineKey)),
|
// MachineKey is only used in the legacy protocol
|
||||||
)
|
err = machineKey.UnmarshalText(
|
||||||
if err != nil {
|
[]byte(MachinePublicKeyEnsurePrefix(machine.MachineKey)),
|
||||||
return nil, fmt.Errorf("failed to parse machine public key: %w", err)
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse machine public key: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var discoKey key.DiscoPublic
|
var discoKey key.DiscoPublic
|
||||||
@@ -642,11 +660,11 @@ func (machine *Machine) toProto() *v1.Machine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) RegisterMachineFromAuthCallback(
|
func (h *Headscale) RegisterMachineFromAuthCallback(
|
||||||
machineKeyStr string,
|
nodeKeyStr string,
|
||||||
namespaceName string,
|
namespaceName string,
|
||||||
registrationMethod string,
|
registrationMethod string,
|
||||||
) (*Machine, error) {
|
) (*Machine, error) {
|
||||||
if machineInterface, ok := h.registrationCache.Get(machineKeyStr); ok {
|
if machineInterface, ok := h.registrationCache.Get(nodeKeyStr); ok {
|
||||||
if registrationMachine, ok := machineInterface.(Machine); ok {
|
if registrationMachine, ok := machineInterface.(Machine); ok {
|
||||||
namespace, err := h.GetNamespace(namespaceName)
|
namespace, err := h.GetNamespace(namespaceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -677,7 +695,7 @@ func (h *Headscale) RegisterMachine(machine Machine,
|
|||||||
) (*Machine, error) {
|
) (*Machine, error) {
|
||||||
log.Trace().
|
log.Trace().
|
||||||
Caller().
|
Caller().
|
||||||
Str("machine_key", machine.MachineKey).
|
Str("node_key", machine.NodeKey).
|
||||||
Msg("Registering machine")
|
Msg("Registering machine")
|
||||||
|
|
||||||
log.Trace().
|
log.Trace().
|
||||||
|
@@ -10,6 +10,7 @@ import (
|
|||||||
"gopkg.in/check.v1"
|
"gopkg.in/check.v1"
|
||||||
"inet.af/netaddr"
|
"inet.af/netaddr"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
|
"tailscale.com/types/key"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Suite) TestGetMachine(c *check.C) {
|
func (s *Suite) TestGetMachine(c *check.C) {
|
||||||
@@ -64,6 +65,35 @@ func (s *Suite) TestGetMachineByID(c *check.C) {
|
|||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Suite) TestGetMachineByNodeKeys(c *check.C) {
|
||||||
|
namespace, err := app.CreateNamespace("test")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
_, err = app.GetMachineByID(0)
|
||||||
|
c.Assert(err, check.NotNil)
|
||||||
|
|
||||||
|
nodeKey := key.NewNode()
|
||||||
|
oldNodeKey := key.NewNode()
|
||||||
|
|
||||||
|
machine := Machine{
|
||||||
|
ID: 0,
|
||||||
|
MachineKey: "foo",
|
||||||
|
NodeKey: NodePublicKeyStripPrefix(nodeKey.Public()),
|
||||||
|
DiscoKey: "faa",
|
||||||
|
Name: "testmachine",
|
||||||
|
NamespaceID: namespace.ID,
|
||||||
|
RegisterMethod: RegisterMethodAuthKey,
|
||||||
|
AuthKeyID: uint(pak.ID),
|
||||||
|
}
|
||||||
|
app.db.Save(&machine)
|
||||||
|
|
||||||
|
_, err = app.GetMachineByNodeKeys(nodeKey.Public(), oldNodeKey.Public())
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Suite) TestDeleteMachine(c *check.C) {
|
func (s *Suite) TestDeleteMachine(c *check.C) {
|
||||||
namespace, err := app.CreateNamespace("test")
|
namespace, err := app.CreateNamespace("test")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
126
noise.go
Normal file
126
noise.go
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
package headscale
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"golang.org/x/net/http2"
|
||||||
|
"golang.org/x/net/http2/h2c"
|
||||||
|
"tailscale.com/control/controlbase"
|
||||||
|
"tailscale.com/net/netutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
errWrongConnectionUpgrade = Error("wrong connection upgrade")
|
||||||
|
errCannotHijack = Error("cannot hijack connection")
|
||||||
|
errNoiseHandshakeFailed = Error("noise handshake failed")
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ts2021UpgradePath is the path that the server listens on for the WebSockets upgrade
|
||||||
|
ts2021UpgradePath = "/ts2021"
|
||||||
|
|
||||||
|
// upgradeHeader is the value of the Upgrade HTTP header used to
|
||||||
|
// indicate the Tailscale control protocol.
|
||||||
|
upgradeHeaderValue = "tailscale-control-protocol"
|
||||||
|
|
||||||
|
// handshakeHeaderName is the HTTP request header that can
|
||||||
|
// optionally contain base64-encoded initial handshake
|
||||||
|
// payload, to save an RTT.
|
||||||
|
handshakeHeaderName = "X-Tailscale-Handshake"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NoiseUpgradeHandler is to upgrade the connection and hijack the net.Conn
|
||||||
|
// in order to use the Noise-based TS2021 protocol. Listens in /ts2021
|
||||||
|
func (h *Headscale) NoiseUpgradeHandler(ctx *gin.Context) {
|
||||||
|
log.Trace().Caller().Msgf("Noise upgrade handler for client %s", ctx.ClientIP())
|
||||||
|
|
||||||
|
// Under normal circumpstances, we should be able to use the controlhttp.AcceptHTTP()
|
||||||
|
// function to do this - kindly left there by the Tailscale authors for us to use.
|
||||||
|
// (https://github.com/tailscale/tailscale/blob/main/control/controlhttp/server.go)
|
||||||
|
//
|
||||||
|
// However, Gin seems to be doing something funny/different with its writer (see AcceptHTTP code).
|
||||||
|
// This causes problems when the upgrade headers are sent in AcceptHTTP.
|
||||||
|
// So have getNoiseConnection() that is essentially an AcceptHTTP but using the native Gin methods.
|
||||||
|
noiseConn, err := h.getNoiseConnection(ctx)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("noise upgrade failed")
|
||||||
|
ctx.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
server := http.Server{}
|
||||||
|
server.Handler = h2c.NewHandler(h.noiseRouter, &http2.Server{})
|
||||||
|
server.Serve(netutil.NewOneConnListener(noiseConn, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// getNoiseConnection is basically AcceptHTTP from tailscale, but more _alla_ Gin
|
||||||
|
// TODO(juan): Figure out why we need to do this at all.
|
||||||
|
func (h *Headscale) getNoiseConnection(ctx *gin.Context) (*controlbase.Conn, error) {
|
||||||
|
next := ctx.GetHeader("Upgrade")
|
||||||
|
if next == "" {
|
||||||
|
ctx.String(http.StatusBadRequest, "missing next protocol")
|
||||||
|
|
||||||
|
return nil, errWrongConnectionUpgrade
|
||||||
|
}
|
||||||
|
if next != upgradeHeaderValue {
|
||||||
|
ctx.String(http.StatusBadRequest, "unknown next protocol")
|
||||||
|
|
||||||
|
return nil, errWrongConnectionUpgrade
|
||||||
|
}
|
||||||
|
|
||||||
|
initB64 := ctx.GetHeader(handshakeHeaderName)
|
||||||
|
if initB64 == "" {
|
||||||
|
ctx.String(http.StatusBadRequest, "missing Tailscale handshake header")
|
||||||
|
|
||||||
|
return nil, errWrongConnectionUpgrade
|
||||||
|
}
|
||||||
|
init, err := base64.StdEncoding.DecodeString(initB64)
|
||||||
|
if err != nil {
|
||||||
|
ctx.String(http.StatusBadRequest, "invalid tailscale handshake header")
|
||||||
|
|
||||||
|
return nil, errWrongConnectionUpgrade
|
||||||
|
}
|
||||||
|
|
||||||
|
hijacker, ok := ctx.Writer.(http.Hijacker)
|
||||||
|
if !ok {
|
||||||
|
log.Error().Caller().Err(err).Msgf("Hijack failed")
|
||||||
|
ctx.String(http.StatusInternalServerError, "HTTP does not support general TCP support")
|
||||||
|
|
||||||
|
return nil, errCannotHijack
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is what changes from the original AcceptHTTP() function.
|
||||||
|
ctx.Header("Upgrade", upgradeHeaderValue)
|
||||||
|
ctx.Header("Connection", "upgrade")
|
||||||
|
ctx.Status(http.StatusSwitchingProtocols)
|
||||||
|
ctx.Writer.WriteHeaderNow()
|
||||||
|
// end
|
||||||
|
|
||||||
|
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 nil, errCannotHijack
|
||||||
|
}
|
||||||
|
if err := conn.Flush(); err != nil {
|
||||||
|
netConn.Close()
|
||||||
|
|
||||||
|
return nil, errCannotHijack
|
||||||
|
}
|
||||||
|
netConn = netutil.NewDrainBufConn(netConn, conn.Reader)
|
||||||
|
|
||||||
|
nc, err := controlbase.Server(ctx.Request.Context(), netConn, *h.noisePrivateKey, init)
|
||||||
|
if err != nil {
|
||||||
|
netConn.Close()
|
||||||
|
|
||||||
|
return nil, errNoiseHandshakeFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
return nc, nil
|
||||||
|
}
|
551
noise_api.go
Normal file
551
noise_api.go
Normal file
@@ -0,0 +1,551 @@
|
|||||||
|
package headscale
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
|
"tailscale.com/types/key"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *Headscale) NoiseRegistrationHandler(ctx *gin.Context) {
|
||||||
|
log.Trace().Caller().Msgf("Noise registration handler for client %s", ctx.ClientIP())
|
||||||
|
body, _ := io.ReadAll(ctx.Request.Body)
|
||||||
|
req := tailcfg.RegisterRequest{}
|
||||||
|
if err := json.Unmarshal(body, &req); err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Err(err).
|
||||||
|
Msg("Cannot parse RegisterRequest")
|
||||||
|
machineRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc()
|
||||||
|
ctx.String(http.StatusInternalServerError, "Eek!")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Caller().
|
||||||
|
Str("nodekey", req.NodeKey.ShortString()).
|
||||||
|
Str("oldnodekey", req.OldNodeKey.ShortString()).Msg("Nodekys!")
|
||||||
|
|
||||||
|
now := time.Now().UTC()
|
||||||
|
machine, err := h.GetMachineByNodeKeys(req.NodeKey, req.OldNodeKey)
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
log.Info().Str("machine", req.Hostinfo.Hostname).Msg("New machine via Noise")
|
||||||
|
|
||||||
|
// If the machine has AuthKey set, handle registration via PreAuthKeys
|
||||||
|
if req.Auth.AuthKey != "" {
|
||||||
|
h.handleNoiseAuthKey(ctx, req)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hname, err := NormalizeToFQDNRules(
|
||||||
|
req.Hostinfo.Hostname,
|
||||||
|
h.cfg.OIDC.StripEmaildomain,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
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)
|
||||||
|
// We create the machine and then keep it around until a callback
|
||||||
|
// happens
|
||||||
|
newMachine := Machine{
|
||||||
|
MachineKey: "",
|
||||||
|
Name: hname,
|
||||||
|
NodeKey: NodePublicKeyStripPrefix(req.NodeKey),
|
||||||
|
LastSeen: &now,
|
||||||
|
Expiry: &time.Time{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if !req.Expiry.IsZero() {
|
||||||
|
log.Trace().
|
||||||
|
Caller().
|
||||||
|
Str("machine", req.Hostinfo.Hostname).
|
||||||
|
Time("expiry", req.Expiry).
|
||||||
|
Msg("Non-zero expiry time requested")
|
||||||
|
newMachine.Expiry = &req.Expiry
|
||||||
|
}
|
||||||
|
|
||||||
|
h.registrationCache.Set(
|
||||||
|
NodePublicKeyStripPrefix(req.NodeKey),
|
||||||
|
newMachine,
|
||||||
|
registerCacheExpiration,
|
||||||
|
)
|
||||||
|
|
||||||
|
h.handleMachineRegistrationNew(ctx, key.MachinePublic{}, req)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// The machine is already registered, so we need to pass through reauth or key update.
|
||||||
|
if machine != nil {
|
||||||
|
// If the NodeKey stored in headscale is the same as the key presented in a registration
|
||||||
|
// request, then we have a node that is either:
|
||||||
|
// - Trying to log out (sending a expiry in the past)
|
||||||
|
// - A valid, registered machine, looking for the node map
|
||||||
|
// - Expired machine wanting to reauthenticate
|
||||||
|
if machine.NodeKey == NodePublicKeyStripPrefix(req.NodeKey) {
|
||||||
|
// The client sends an Expiry in the past if the client is requesting to expire the key (aka logout)
|
||||||
|
// https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L648
|
||||||
|
if !req.Expiry.IsZero() && req.Expiry.UTC().Before(now) {
|
||||||
|
h.handleNoiseNodeLogOut(ctx, *machine)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If machine is not expired, and is register, we have a already accepted this machine,
|
||||||
|
// let it proceed with a valid registration
|
||||||
|
if !machine.isExpired() {
|
||||||
|
h.handleNoiseNodeValidRegistration(ctx, *machine)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The NodeKey we have matches OldNodeKey, which means this is a refresh after a key expiration
|
||||||
|
if machine.NodeKey == NodePublicKeyStripPrefix(req.OldNodeKey) &&
|
||||||
|
!machine.isExpired() {
|
||||||
|
h.handleNoiseNodeRefreshKey(ctx, req, *machine)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// The node has expired
|
||||||
|
h.handleNoiseNodeExpired(ctx, req, *machine)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NoisePollNetMapHandler takes care of /machine/:id/map
|
||||||
|
//
|
||||||
|
// This is the busiest endpoint, as it keeps the HTTP long poll that updates
|
||||||
|
// the clients when something in the network changes.
|
||||||
|
//
|
||||||
|
// The clients POST stuff like HostInfo and their Endpoints here, but
|
||||||
|
// only after their first request (marked with the ReadOnly field).
|
||||||
|
//
|
||||||
|
// At this moment the updates are sent in a quite horrendous way, but they kinda work.
|
||||||
|
func (h *Headscale) NoisePollNetMapHandler(ctx *gin.Context) {
|
||||||
|
log.Trace().
|
||||||
|
Caller().
|
||||||
|
Str("id", ctx.Param("id")).
|
||||||
|
Msg("PollNetMapHandler called")
|
||||||
|
body, _ := io.ReadAll(ctx.Request.Body)
|
||||||
|
|
||||||
|
req := tailcfg.MapRequest{}
|
||||||
|
if err := json.Unmarshal(body, &req); err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Err(err).
|
||||||
|
Msg("Cannot parse MapRequest")
|
||||||
|
ctx.String(http.StatusInternalServerError, "Eek!")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
machine, err := h.GetMachineByNodeKeys(req.NodeKey, key.NodePublic{})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
log.Warn().Caller().
|
||||||
|
Msgf("Ignoring request, cannot find node with node key %s", req.NodeKey.String())
|
||||||
|
ctx.String(http.StatusUnauthorized, "")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Msgf("Failed to fetch machine from the database with NodeKey: %s", req.NodeKey.String())
|
||||||
|
ctx.String(http.StatusInternalServerError, "")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Trace().Caller().
|
||||||
|
Str("NodeKey", req.NodeKey.ShortString()).
|
||||||
|
Str("machine", machine.Name).
|
||||||
|
Msg("Found machine in database")
|
||||||
|
|
||||||
|
hname, err := NormalizeToFQDNRules(
|
||||||
|
req.Hostinfo.Hostname,
|
||||||
|
h.cfg.OIDC.StripEmaildomain,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
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()
|
||||||
|
|
||||||
|
// update ACLRules with peer informations (to update server tags if necessary)
|
||||||
|
if h.aclPolicy != nil {
|
||||||
|
err = h.UpdateACLRules()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Str("func", "handleAuthKey").
|
||||||
|
Str("machine", machine.Name).
|
||||||
|
Err(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// From Tailscale client:
|
||||||
|
//
|
||||||
|
// ReadOnly is whether the client just wants to fetch the MapResponse,
|
||||||
|
// without updating their Endpoints. The Endpoints field will be ignored and
|
||||||
|
// LastSeen will not be updated and peers will not be notified of changes.
|
||||||
|
//
|
||||||
|
// The intended use is for clients to discover the DERP map at start-up
|
||||||
|
// before their first real endpoint update.
|
||||||
|
if !req.ReadOnly {
|
||||||
|
machine.Endpoints = req.Endpoints
|
||||||
|
machine.LastSeen = &now
|
||||||
|
}
|
||||||
|
h.db.Updates(machine)
|
||||||
|
|
||||||
|
data, err := h.getMapResponse(key.MachinePublic{}, req, machine)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Str("id", ctx.Param("id")).
|
||||||
|
Str("machine", machine.Name).
|
||||||
|
Err(err).
|
||||||
|
Msg("Failed to get Map response")
|
||||||
|
ctx.String(http.StatusInternalServerError, ":(")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We update our peers if the client is not sending ReadOnly in the MapRequest
|
||||||
|
// so we don't distribute its initial request (it comes with
|
||||||
|
// empty endpoints to peers)
|
||||||
|
|
||||||
|
// Details on the protocol can be found in https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L696
|
||||||
|
log.Debug().
|
||||||
|
Caller().
|
||||||
|
Str("id", ctx.Param("id")).
|
||||||
|
Str("machine", machine.Name).
|
||||||
|
Bool("readOnly", req.ReadOnly).
|
||||||
|
Bool("omitPeers", req.OmitPeers).
|
||||||
|
Bool("stream", req.Stream).
|
||||||
|
Msg("Noise client map request processed")
|
||||||
|
|
||||||
|
if req.ReadOnly {
|
||||||
|
log.Info().
|
||||||
|
Caller().
|
||||||
|
Str("machine", machine.Name).
|
||||||
|
Msg("Noise client is starting up. Probably interested in a DERP map")
|
||||||
|
// log.Info().Str("machine", machine.Name).Bytes("resp", data).Msg("Sending DERP map to client")
|
||||||
|
|
||||||
|
ctx.Data(http.StatusOK, "application/json; charset=utf-8", data)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// There has been an update to _any_ of the nodes that the other nodes would
|
||||||
|
// need to know about
|
||||||
|
log.Trace().Msgf("Updating peers for noise machine %s", machine.Name)
|
||||||
|
h.setLastStateChangeToNow(machine.Namespace.Name)
|
||||||
|
|
||||||
|
// The request is not ReadOnly, so we need to set up channels for updating
|
||||||
|
// peers via longpoll
|
||||||
|
|
||||||
|
// Only create update channel if it has not been created
|
||||||
|
log.Trace().
|
||||||
|
Caller().
|
||||||
|
Str("id", ctx.Param("id")).
|
||||||
|
Str("machine", machine.Name).
|
||||||
|
Msg("Noise loading or creating update channel")
|
||||||
|
|
||||||
|
// TODO: could probably remove all that duplication once generics land.
|
||||||
|
closeChanWithLog := func(channel interface{}, name string) {
|
||||||
|
log.Trace().
|
||||||
|
Caller().
|
||||||
|
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")
|
||||||
|
|
||||||
|
keepAliveChan := make(chan []byte)
|
||||||
|
defer closeChanWithLog(keepAliveChan, "keepAliveChan")
|
||||||
|
|
||||||
|
if req.OmitPeers && !req.Stream {
|
||||||
|
log.Info().
|
||||||
|
Caller().
|
||||||
|
Str("machine", machine.Name).
|
||||||
|
Msg("Noise client sent endpoint update and is ok with a response without peer list")
|
||||||
|
ctx.Data(http.StatusOK, "application/json; charset=utf-8", data)
|
||||||
|
|
||||||
|
// It sounds like we should update the nodes when we have received a endpoint update
|
||||||
|
// even tho the comments in the tailscale code dont explicitly say so.
|
||||||
|
updateRequestsFromNode.WithLabelValues(machine.Namespace.Name, machine.Name, "endpoint-update").
|
||||||
|
Inc()
|
||||||
|
updateChan <- struct{}{}
|
||||||
|
|
||||||
|
return
|
||||||
|
} else if req.OmitPeers && req.Stream {
|
||||||
|
log.Warn().
|
||||||
|
Caller().
|
||||||
|
Str("machine", machine.Name).
|
||||||
|
Msg("Ignoring request, don't know how to handle it")
|
||||||
|
ctx.String(http.StatusBadRequest, "")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().
|
||||||
|
Caller().
|
||||||
|
Str("machine", machine.Name).
|
||||||
|
Msg("Noise client is ready to access the tailnet")
|
||||||
|
log.Info().
|
||||||
|
Caller().
|
||||||
|
Str("machine", machine.Name).
|
||||||
|
Msg("Sending initial map")
|
||||||
|
pollDataChan <- data
|
||||||
|
|
||||||
|
log.Info().
|
||||||
|
Caller().
|
||||||
|
Str("machine", machine.Name).
|
||||||
|
Msg("Notifying peers")
|
||||||
|
updateRequestsFromNode.WithLabelValues(machine.Namespace.Name, machine.Name, "full-update").
|
||||||
|
Inc()
|
||||||
|
updateChan <- struct{}{}
|
||||||
|
|
||||||
|
h.PollNetMapStream(
|
||||||
|
ctx,
|
||||||
|
machine,
|
||||||
|
req,
|
||||||
|
key.MachinePublic{},
|
||||||
|
pollDataChan,
|
||||||
|
keepAliveChan,
|
||||||
|
updateChan,
|
||||||
|
)
|
||||||
|
log.Trace().
|
||||||
|
Caller().
|
||||||
|
Str("id", ctx.Param("id")).
|
||||||
|
Str("machine", machine.Name).
|
||||||
|
Msg("Finished stream, closing PollNetMap session")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Headscale) handleNoiseNodeValidRegistration(
|
||||||
|
ctx *gin.Context,
|
||||||
|
machine Machine,
|
||||||
|
) {
|
||||||
|
resp := tailcfg.RegisterResponse{}
|
||||||
|
|
||||||
|
// The machine registration is valid, respond with redirect to /map
|
||||||
|
log.Debug().
|
||||||
|
Str("machine", machine.Name).
|
||||||
|
Msg("Client is registered and we have the current NodeKey. All clear to /map")
|
||||||
|
|
||||||
|
resp.AuthURL = ""
|
||||||
|
resp.MachineAuthorized = true
|
||||||
|
resp.User = *machine.Namespace.toUser()
|
||||||
|
resp.Login = *machine.Namespace.toLogin()
|
||||||
|
|
||||||
|
machineRegistrations.WithLabelValues("update", "web", "success", machine.Namespace.Name).
|
||||||
|
Inc()
|
||||||
|
ctx.JSON(http.StatusOK, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Headscale) handleNoiseNodeLogOut(
|
||||||
|
ctx *gin.Context,
|
||||||
|
machine Machine,
|
||||||
|
) {
|
||||||
|
resp := tailcfg.RegisterResponse{}
|
||||||
|
|
||||||
|
log.Info().
|
||||||
|
Str("machine", machine.Name).
|
||||||
|
Msg("Client requested logout")
|
||||||
|
|
||||||
|
h.ExpireMachine(&machine)
|
||||||
|
|
||||||
|
resp.AuthURL = ""
|
||||||
|
resp.MachineAuthorized = false
|
||||||
|
resp.User = *machine.Namespace.toUser()
|
||||||
|
ctx.JSON(http.StatusOK, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Headscale) handleNoiseNodeRefreshKey(
|
||||||
|
ctx *gin.Context,
|
||||||
|
registerRequest tailcfg.RegisterRequest,
|
||||||
|
machine Machine,
|
||||||
|
) {
|
||||||
|
resp := tailcfg.RegisterResponse{}
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("machine", machine.Name).
|
||||||
|
Msg("We have the OldNodeKey in the database. This is a key refresh")
|
||||||
|
machine.NodeKey = NodePublicKeyStripPrefix(registerRequest.NodeKey)
|
||||||
|
h.db.Save(&machine)
|
||||||
|
|
||||||
|
resp.AuthURL = ""
|
||||||
|
resp.User = *machine.Namespace.toUser()
|
||||||
|
ctx.JSON(http.StatusOK, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Headscale) handleNoiseNodeExpired(
|
||||||
|
ctx *gin.Context,
|
||||||
|
registerRequest tailcfg.RegisterRequest,
|
||||||
|
machine Machine,
|
||||||
|
) {
|
||||||
|
resp := tailcfg.RegisterResponse{}
|
||||||
|
|
||||||
|
// The client has registered before, but has expired
|
||||||
|
log.Debug().
|
||||||
|
Caller().
|
||||||
|
Str("machine", machine.Name).
|
||||||
|
Msg("Machine registration has expired. Sending a authurl to register")
|
||||||
|
|
||||||
|
if registerRequest.Auth.AuthKey != "" {
|
||||||
|
h.handleNoiseAuthKey(ctx, registerRequest)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.cfg.OIDC.Issuer != "" {
|
||||||
|
resp.AuthURL = fmt.Sprintf("%s/oidc/register/%s",
|
||||||
|
strings.TrimSuffix(h.cfg.ServerURL, "/"), machine.NodeKey)
|
||||||
|
} else {
|
||||||
|
resp.AuthURL = fmt.Sprintf("%s/register?key=%s",
|
||||||
|
strings.TrimSuffix(h.cfg.ServerURL, "/"), machine.NodeKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
machineRegistrations.WithLabelValues("reauth", "web", "success", machine.Namespace.Name).
|
||||||
|
Inc()
|
||||||
|
ctx.JSON(http.StatusOK, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Headscale) handleNoiseAuthKey(
|
||||||
|
ctx *gin.Context,
|
||||||
|
registerRequest tailcfg.RegisterRequest,
|
||||||
|
) {
|
||||||
|
log.Debug().
|
||||||
|
Caller().
|
||||||
|
Str("machine", registerRequest.Hostinfo.Hostname).
|
||||||
|
Msgf("Processing auth key for %s over Noise", registerRequest.Hostinfo.Hostname)
|
||||||
|
resp := tailcfg.RegisterResponse{}
|
||||||
|
|
||||||
|
pak, err := h.checkKeyValidity(registerRequest.Auth.AuthKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Str("machine", registerRequest.Hostinfo.Hostname).
|
||||||
|
Err(err).
|
||||||
|
Msg("Failed authentication via AuthKey")
|
||||||
|
resp.MachineAuthorized = false
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusUnauthorized, resp)
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Str("machine", registerRequest.Hostinfo.Hostname).
|
||||||
|
Msg("Failed authentication via AuthKey over Noise")
|
||||||
|
|
||||||
|
if pak != nil {
|
||||||
|
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
|
||||||
|
Inc()
|
||||||
|
} else {
|
||||||
|
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", "unknown").Inc()
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Caller().
|
||||||
|
Str("machine", registerRequest.Hostinfo.Hostname).
|
||||||
|
Msg("Authentication key was valid, proceeding to acquire IP addresses")
|
||||||
|
|
||||||
|
nodeKey := NodePublicKeyStripPrefix(registerRequest.NodeKey)
|
||||||
|
|
||||||
|
// retrieve machine information if it exist
|
||||||
|
// The error is not important, because if it does not
|
||||||
|
// exist, then this is a new machine and we will move
|
||||||
|
// on to registration.
|
||||||
|
machine, _ := h.GetMachineByNodeKeys(registerRequest.NodeKey, registerRequest.OldNodeKey)
|
||||||
|
if machine != nil {
|
||||||
|
log.Trace().
|
||||||
|
Caller().
|
||||||
|
Str("machine", machine.Name).
|
||||||
|
Msg("machine already registered, refreshing with new auth key")
|
||||||
|
|
||||||
|
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: "",
|
||||||
|
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)
|
||||||
|
|
||||||
|
resp.MachineAuthorized = true
|
||||||
|
resp.User = *pak.Namespace.toUser()
|
||||||
|
|
||||||
|
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "success", pak.Namespace.Name).
|
||||||
|
Inc()
|
||||||
|
ctx.JSON(http.StatusOK, resp)
|
||||||
|
log.Info().
|
||||||
|
Caller().
|
||||||
|
Str("machine", registerRequest.Hostinfo.Hostname).
|
||||||
|
Str("ips", strings.Join(machine.IPAddresses.ToStringSlice(), ", ")).
|
||||||
|
Msg("Successfully authenticated via AuthKey on Noise")
|
||||||
|
}
|
43
oidc.go
43
oidc.go
@@ -10,6 +10,7 @@ import (
|
|||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -61,10 +62,10 @@ func (h *Headscale) initOIDC() error {
|
|||||||
|
|
||||||
// RegisterOIDC redirects to the OIDC provider for authentication
|
// RegisterOIDC redirects to the OIDC provider for authentication
|
||||||
// Puts machine key in cache so the callback can retrieve it using the oidc state param
|
// Puts machine key in cache so the callback can retrieve it using the oidc state param
|
||||||
// Listens in /oidc/register/:mKey.
|
// Listens in /oidc/register/:nKey.
|
||||||
func (h *Headscale) RegisterOIDC(ctx *gin.Context) {
|
func (h *Headscale) RegisterOIDC(ctx *gin.Context) {
|
||||||
machineKeyStr := ctx.Param("mkey")
|
nodeKeyStr := ctx.Param("nkey")
|
||||||
if machineKeyStr == "" {
|
if nodeKeyStr == "" {
|
||||||
ctx.String(http.StatusBadRequest, "Wrong params")
|
ctx.String(http.StatusBadRequest, "Wrong params")
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -72,7 +73,7 @@ func (h *Headscale) RegisterOIDC(ctx *gin.Context) {
|
|||||||
|
|
||||||
log.Trace().
|
log.Trace().
|
||||||
Caller().
|
Caller().
|
||||||
Str("machine_key", machineKeyStr).
|
Str("node_key", nodeKeyStr).
|
||||||
Msg("Received oidc register call")
|
Msg("Received oidc register call")
|
||||||
|
|
||||||
randomBlob := make([]byte, randomByteSize)
|
randomBlob := make([]byte, randomByteSize)
|
||||||
@@ -88,7 +89,7 @@ func (h *Headscale) RegisterOIDC(ctx *gin.Context) {
|
|||||||
stateStr := hex.EncodeToString(randomBlob)[:32]
|
stateStr := hex.EncodeToString(randomBlob)[:32]
|
||||||
|
|
||||||
// place the machine key into the state cache, so it can be retrieved later
|
// place the machine key into the state cache, so it can be retrieved later
|
||||||
h.registrationCache.Set(stateStr, machineKeyStr, registerCacheExpiration)
|
h.registrationCache.Set(stateStr, nodeKeyStr, registerCacheExpiration)
|
||||||
|
|
||||||
authURL := h.oauth2Config.AuthCodeURL(stateStr)
|
authURL := h.oauth2Config.AuthCodeURL(stateStr)
|
||||||
log.Debug().Msgf("Redirecting to %s for authentication", authURL)
|
log.Debug().Msgf("Redirecting to %s for authentication", authURL)
|
||||||
@@ -113,7 +114,7 @@ var oidcCallbackTemplate = template.Must(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// OIDCCallback handles the callback from the OIDC endpoint
|
// OIDCCallback handles the callback from the OIDC endpoint
|
||||||
// Retrieves the mkey from the state cache and adds the machine to the users email namespace
|
// Retrieves the nkey from the state cache and adds the machine to the users email namespace
|
||||||
// TODO: A confirmation page for new machines should be added to avoid phishing vulnerabilities
|
// TODO: A confirmation page for new machines should be added to avoid phishing vulnerabilities
|
||||||
// TODO: Add groups information from OIDC tokens into machine HostInfo
|
// TODO: Add groups information from OIDC tokens into machine HostInfo
|
||||||
// Listens in /oidc/callback.
|
// Listens in /oidc/callback.
|
||||||
@@ -129,6 +130,10 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
|
|||||||
|
|
||||||
oauth2Token, err := h.oauth2Config.Exchange(context.Background(), code)
|
oauth2Token, err := h.oauth2Config.Exchange(context.Background(), code)
|
||||||
if err != nil {
|
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")
|
ctx.String(http.StatusBadRequest, "Could not exchange code for token")
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -183,32 +188,32 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// retrieve machinekey from state cache
|
// retrieve machinekey from state cache
|
||||||
machineKeyIf, machineKeyFound := h.registrationCache.Get(state)
|
nodeKeyIf, machineKeyFound := h.registrationCache.Get(state)
|
||||||
|
|
||||||
if !machineKeyFound {
|
if !machineKeyFound {
|
||||||
log.Error().
|
log.Error().
|
||||||
Msg("requested machine state key expired before authorisation completed")
|
Msg("requested node state key expired before authorisation completed")
|
||||||
ctx.String(http.StatusBadRequest, "state has expired")
|
ctx.String(http.StatusBadRequest, "state has expired")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
machineKeyFromCache, machineKeyOK := machineKeyIf.(string)
|
nodeKeyFromCache, nodeKeyOK := nodeKeyIf.(string)
|
||||||
|
|
||||||
var machineKey key.MachinePublic
|
var nodeKey key.NodePublic
|
||||||
err = machineKey.UnmarshalText(
|
err = nodeKey.UnmarshalText(
|
||||||
[]byte(MachinePublicKeyEnsurePrefix(machineKeyFromCache)),
|
[]byte(NodePublicKeyEnsurePrefix(nodeKeyFromCache)),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
Msg("could not parse machine public key")
|
Msg("could not parse node public key")
|
||||||
ctx.String(http.StatusBadRequest, "could not parse public key")
|
ctx.String(http.StatusBadRequest, "could not parse public key")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !machineKeyOK {
|
if !nodeKeyOK {
|
||||||
log.Error().Msg("could not get machine key from cache")
|
log.Error().Msg("could not get node key from cache")
|
||||||
ctx.String(
|
ctx.String(
|
||||||
http.StatusInternalServerError,
|
http.StatusInternalServerError,
|
||||||
"could not get machine key from cache",
|
"could not get machine key from cache",
|
||||||
@@ -221,7 +226,7 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
|
|||||||
// The error is not important, because if it does not
|
// The error is not important, because if it does not
|
||||||
// exist, then this is a new machine and we will move
|
// exist, then this is a new machine and we will move
|
||||||
// on to registration.
|
// on to registration.
|
||||||
machine, _ := h.GetMachineByMachineKey(machineKey)
|
machine, _ := h.GetMachineByNodeKeys(nodeKey, key.NodePublic{})
|
||||||
|
|
||||||
if machine != nil {
|
if machine != nil {
|
||||||
log.Trace().
|
log.Trace().
|
||||||
@@ -229,7 +234,7 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
|
|||||||
Str("machine", machine.Name).
|
Str("machine", machine.Name).
|
||||||
Msg("machine already registered, reauthenticating")
|
Msg("machine already registered, reauthenticating")
|
||||||
|
|
||||||
h.RefreshMachine(machine, *machine.Expiry)
|
h.RefreshMachine(machine, time.Time{})
|
||||||
|
|
||||||
var content bytes.Buffer
|
var content bytes.Buffer
|
||||||
if err := oidcCallbackTemplate.Execute(&content, oidcCallbackTemplateConfig{
|
if err := oidcCallbackTemplate.Execute(&content, oidcCallbackTemplateConfig{
|
||||||
@@ -300,10 +305,10 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
machineKeyStr := MachinePublicKeyStripPrefix(machineKey)
|
nodeKeyStr := NodePublicKeyStripPrefix(nodeKey)
|
||||||
|
|
||||||
_, err = h.RegisterMachineFromAuthCallback(
|
_, err = h.RegisterMachineFromAuthCallback(
|
||||||
machineKeyStr,
|
nodeKeyStr,
|
||||||
namespace.Name,
|
namespace.Name,
|
||||||
RegisterMethodOIDC,
|
RegisterMethodOIDC,
|
||||||
)
|
)
|
||||||
|
68
poll.go
68
poll.go
@@ -64,8 +64,8 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
log.Warn().
|
log.Warn().
|
||||||
Str("handler", "PollNetMap").
|
Caller().
|
||||||
Msgf("Ignoring request, cannot find machine with key %s", machineKey.String())
|
Msgf("Ignoring request (client %s), cannot find machine with key %s", ctx.ClientIP(), machineKey.String())
|
||||||
ctx.String(http.StatusUnauthorized, "")
|
ctx.String(http.StatusUnauthorized, "")
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -163,6 +163,7 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
|
|||||||
|
|
||||||
// There has been an update to _any_ of the nodes that the other nodes would
|
// There has been an update to _any_ of the nodes that the other nodes would
|
||||||
// need to know about
|
// need to know about
|
||||||
|
log.Trace().Msgf("Updating peers for machine %s", machine.Name)
|
||||||
h.setLastStateChangeToNow(machine.Namespace.Name)
|
h.setLastStateChangeToNow(machine.Namespace.Name)
|
||||||
|
|
||||||
// The request is not ReadOnly, so we need to set up channels for updating
|
// The request is not ReadOnly, so we need to set up channels for updating
|
||||||
@@ -175,32 +176,13 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
|
|||||||
Str("machine", machine.Name).
|
Str("machine", machine.Name).
|
||||||
Msg("Loading or creating update channel")
|
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
|
const chanSize = 8
|
||||||
updateChan := make(chan struct{}, chanSize)
|
updateChan := make(chan struct{}, chanSize)
|
||||||
defer closeChanWithLog(updateChan, "updateChan")
|
|
||||||
|
|
||||||
pollDataChan := make(chan []byte, chanSize)
|
pollDataChan := make(chan []byte, chanSize)
|
||||||
defer closeChanWithLog(pollDataChan, "pollDataChan")
|
defer closeChanWithLog(pollDataChan, machine.Name, "pollDataChan")
|
||||||
|
|
||||||
keepAliveChan := make(chan []byte)
|
keepAliveChan := make(chan []byte)
|
||||||
defer closeChanWithLog(keepAliveChan, "keepAliveChan")
|
|
||||||
|
|
||||||
if req.OmitPeers && !req.Stream {
|
if req.OmitPeers && !req.Stream {
|
||||||
log.Info().
|
log.Info().
|
||||||
@@ -260,7 +242,7 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
|
|||||||
Msg("Finished stream, closing PollNetMap session")
|
Msg("Finished stream, closing PollNetMap session")
|
||||||
}
|
}
|
||||||
|
|
||||||
// PollNetMapStream takes care of /machine/:id/map
|
// PollNetMapStream takes care of /map
|
||||||
// stream logic, ensuring we communicate updates and data
|
// stream logic, ensuring we communicate updates and data
|
||||||
// to the connected clients.
|
// to the connected clients.
|
||||||
func (h *Headscale) PollNetMapStream(
|
func (h *Headscale) PollNetMapStream(
|
||||||
@@ -273,7 +255,9 @@ func (h *Headscale) PollNetMapStream(
|
|||||||
updateChan chan struct{},
|
updateChan chan struct{},
|
||||||
) {
|
) {
|
||||||
{
|
{
|
||||||
ctx, cancel := context.WithCancel(ctx.Request.Context())
|
ctx := context.WithValue(ctx.Request.Context(), "machineName", machine.Name)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
go h.scheduledPollWorker(
|
go h.scheduledPollWorker(
|
||||||
@@ -388,10 +372,7 @@ func (h *Headscale) PollNetMapStream(
|
|||||||
Str("channel", "keepAlive").
|
Str("channel", "keepAlive").
|
||||||
Int("bytes", len(data)).
|
Int("bytes", len(data)).
|
||||||
Msg("Keep alive sent successfully")
|
Msg("Keep alive sent successfully")
|
||||||
// TODO(kradalby): Abstract away all the database calls, this can cause race conditions
|
// TODO(kradalbCne(machine)
|
||||||
// when an outdated machine object is kept alive, e.g. db is update from
|
|
||||||
// command line, but then overwritten.
|
|
||||||
err = h.UpdateMachine(machine)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
Str("handler", "PollNetMapStream").
|
Str("handler", "PollNetMapStream").
|
||||||
@@ -453,7 +434,7 @@ func (h *Headscale) PollNetMapStream(
|
|||||||
Err(err).
|
Err(err).
|
||||||
Msg("Could not get the map update")
|
Msg("Could not get the map update")
|
||||||
}
|
}
|
||||||
_, err = writer.Write(data)
|
nBytes, err := writer.Write(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
Str("handler", "PollNetMapStream").
|
Str("handler", "PollNetMapStream").
|
||||||
@@ -470,7 +451,7 @@ func (h *Headscale) PollNetMapStream(
|
|||||||
Str("handler", "PollNetMapStream").
|
Str("handler", "PollNetMapStream").
|
||||||
Str("machine", machine.Name).
|
Str("machine", machine.Name).
|
||||||
Str("channel", "update").
|
Str("channel", "update").
|
||||||
Msg("Updated Map has been sent")
|
Msgf("Updated Map has been sent (%d bytes)", nBytes)
|
||||||
updateRequestsSentToNode.WithLabelValues(machine.Namespace.Name, machine.Name, "success").
|
updateRequestsSentToNode.WithLabelValues(machine.Namespace.Name, machine.Name, "success").
|
||||||
Inc()
|
Inc()
|
||||||
|
|
||||||
@@ -564,8 +545,8 @@ func (h *Headscale) PollNetMapStream(
|
|||||||
|
|
||||||
func (h *Headscale) scheduledPollWorker(
|
func (h *Headscale) scheduledPollWorker(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
updateChan chan<- struct{},
|
updateChan chan struct{},
|
||||||
keepAliveChan chan<- []byte,
|
keepAliveChan chan []byte,
|
||||||
machineKey key.MachinePublic,
|
machineKey key.MachinePublic,
|
||||||
mapRequest tailcfg.MapRequest,
|
mapRequest tailcfg.MapRequest,
|
||||||
machine *Machine,
|
machine *Machine,
|
||||||
@@ -573,6 +554,17 @@ func (h *Headscale) scheduledPollWorker(
|
|||||||
keepAliveTicker := time.NewTicker(keepAliveInterval)
|
keepAliveTicker := time.NewTicker(keepAliveInterval)
|
||||||
updateCheckerTicker := time.NewTicker(updateCheckInterval)
|
updateCheckerTicker := time.NewTicker(updateCheckInterval)
|
||||||
|
|
||||||
|
defer closeChanWithLog(
|
||||||
|
updateChan,
|
||||||
|
fmt.Sprint(ctx.Value("machineName")),
|
||||||
|
"updateChan",
|
||||||
|
)
|
||||||
|
defer closeChanWithLog(
|
||||||
|
keepAliveChan,
|
||||||
|
fmt.Sprint(ctx.Value("machineName")),
|
||||||
|
"updateChan",
|
||||||
|
)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
@@ -597,7 +589,7 @@ func (h *Headscale) scheduledPollWorker(
|
|||||||
|
|
||||||
case <-updateCheckerTicker.C:
|
case <-updateCheckerTicker.C:
|
||||||
log.Debug().
|
log.Debug().
|
||||||
Str("func", "scheduledPollWorker").
|
Caller().
|
||||||
Str("machine", machine.Name).
|
Str("machine", machine.Name).
|
||||||
Msg("Sending update request")
|
Msg("Sending update request")
|
||||||
updateRequestsFromNode.WithLabelValues(machine.Namespace.Name, machine.Name, "scheduled-update").
|
updateRequestsFromNode.WithLabelValues(machine.Namespace.Name, machine.Name, "scheduled-update").
|
||||||
@@ -606,3 +598,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