Compare commits

...

145 Commits

Author SHA1 Message Date
Juan Font
c50d3aa9bd Merge pull request #675 from juanfont/configurable-update-interval
Make tailnet updates check interval configurable
2022-07-12 13:34:49 +02:00
Juan Font Alonso
4ccff8bf28 Added the new parameter to the integration test params 2022-07-12 13:13:04 +02:00
Juan Font Alonso
5b5298b025 Renamed config param for node update check internal 2022-07-12 12:52:03 +02:00
Juan Font Alonso
8e0939f403 Updated changelog 2022-07-12 12:33:42 +02:00
Juan Font Alonso
cf3fc85196 Make tailnet updates check configurable 2022-07-12 12:27:28 +02:00
Juan Font
e0b15c18ce Merge pull request #667 from kradalby/rerun-docker
Make integration tests retry on failure.
2022-06-27 17:04:39 +02:00
Kristoffer Dalby
566b8c3df3 Fix issue were dockertest fails to start because of container mismatch 2022-06-27 12:07:30 +00:00
Kristoffer Dalby
32a6151df9 Rerun integration tests 5 times if error 2022-06-27 12:02:29 +00:00
Kristoffer Dalby
3777de7133 Use failnow for cli tests aswell 2022-06-27 12:00:21 +00:00
Kristoffer Dalby
8cae4f80d7 Fail tests instead of fatal
Currently we exit the program if the setup does not work, this can cause
is to leave containers and other resources behind since we dont run
TearDown. This change will just fail the test if we cant set up, which
should mean that the TearDown runs aswell.
2022-06-27 11:58:16 +00:00
Kristoffer Dalby
911c5bddce Make saving logs from tests an option (default false)
We currently have a bit of flaky logic which prevents the docker plugin
from cleaning up the containers if the tests or setup fatals or crashes,
this is due to a limitation in the save / passed stats handling.

This change makes it an environment variable which by default ditches
the logs and makes the containers clean up "correctly" in the teardown
method.
2022-06-27 11:56:37 +00:00
Juan Font
4a200c308b Merge pull request #656 from juanfont/abandon-gin
Drop Gin as web framework for TS2019 API
2022-06-26 15:54:41 +02:00
Juan Font Alonso
625e45b1cb Merge branch 'abandon-gin' of https://github.com/juanfont/headscale into abandon-gin 2022-06-26 14:25:05 +02:00
Juan Font Alonso
8551b0dde0 Fixed issue when in linting rampage 2022-06-26 14:24:57 +02:00
Juan Font
050782aff3 Merge branch 'main' into abandon-gin 2022-06-26 12:36:49 +02:00
Juan Font Alonso
00885dffe1 Fix implicit memory aliasing in for loop (lint 8/n) 2022-06-26 12:35:18 +02:00
Juan Font Alonso
ffcc72876c Lint fixes 7/n 2022-06-26 12:30:52 +02:00
Juan Font Alonso
fa91ece5b4 Lint fixes 6/n 2022-06-26 12:25:26 +02:00
Juan Font Alonso
c810b24eb9 Lint fixes 5/n 2022-06-26 12:21:35 +02:00
Juan Font Alonso
03ced0ecfe Lint fixes 4/n 2022-06-26 12:06:25 +02:00
Juan Font Alonso
c859bea0cf Lint fixes 3/n 2022-06-26 12:01:04 +02:00
Juan Font Alonso
a913d1b521 Lint fixes 2/n 2022-06-26 11:55:37 +02:00
Kristoffer Dalby
2464c92572 Merge pull request #665 from juanfont/update-contributors 2022-06-26 11:48:11 +02:00
Juan Font Alonso
10cd87e5a2 Lint fixes 1/n 2022-06-26 11:43:17 +02:00
Juan Font Alonso
58c336e7f4 updated nix flake go.sum 2022-06-26 11:31:31 +02:00
Juan Font
bb4a9583a7 Merge branch 'main' into abandon-gin 2022-06-26 11:08:48 +02:00
github-actions[bot]
7ae38346e5 docs(README): update contributors 2022-06-26 08:22:05 +00:00
Kristoffer Dalby
7604c0f691 Merge pull request #658 from juanfont/fix-segfault-when-not-runner 2022-06-26 10:21:27 +02:00
Kristoffer Dalby
f2f4c3f684 Merge branch 'main' into fix-segfault-when-not-runner 2022-06-26 09:52:15 +02:00
Kristoffer Dalby
34f489b1f4 Update cmd/headscale/cli/utils.go 2022-06-26 09:52:11 +02:00
Kristoffer Dalby
72d1d2630e Update cmd/headscale/cli/utils.go 2022-06-26 09:52:04 +02:00
Kristoffer Dalby
d559e23bc6 Merge pull request #651 from iSchluff/fix/db-shutdown 2022-06-26 09:51:45 +02:00
Kristoffer Dalby
4637400d29 Update CHANGELOG.md 2022-06-26 09:30:16 +02:00
Kristoffer Dalby
0fa943e4b7 Update CHANGELOG.md 2022-06-26 09:29:33 +02:00
Kristoffer Dalby
9707b1f540 Merge branch 'main' into fix/db-shutdown 2022-06-26 08:28:50 +01:00
Juan Font Alonso
657fb208d6 Flush buffered data on polling 2022-06-25 20:47:42 +02:00
Juan Font
647972c7cf Merge branch 'main' into fix-segfault-when-not-runner 2022-06-23 22:17:33 +02:00
Juan Font Alonso
39b58f7d4c Use a signal to close the longpolls on shutdown 2022-06-23 19:40:07 +02:00
Juan Font Alonso
c8378e8b7d Quick fix to segfault on CLI when Headscale is not running (fix #652) 2022-06-22 14:40:40 +02:00
Juan Font Alonso
d404ba102d Use request context to close when client disconnects 2022-06-20 21:47:02 +02:00
Juan Font Alonso
5e9004c407 Fix issues in the poll loop 2022-06-20 21:40:28 +02:00
Juan Font Alonso
8e63b53b0c Merge branch 'abandon-gin' of https://github.com/juanfont/headscale into abandon-gin 2022-06-20 21:38:03 +02:00
Juan Font Alonso
116bef25a7 Fixed wrong copy paste in Header 2022-06-20 21:19:49 +02:00
Juan Font
294975ba87 Merge branch 'main' into abandon-gin 2022-06-20 21:16:11 +02:00
Juan Font Alonso
51b8c659f1 Updated changelog 2022-06-20 21:13:12 +02:00
Juan Font Alonso
082fbead66 Added mux dependency 2022-06-20 21:12:23 +02:00
Juan Font Alonso
73c16ffc65 Fixed issue with the method used to send data 2022-06-20 20:30:08 +02:00
Juan Font Alonso
dec51348e6 Minor status change 2022-06-20 20:29:42 +02:00
Juan Font Alonso
b0b919efb0 Added more logging to derp server 2022-06-20 12:32:13 +02:00
Juan Font Alonso
396c3ecdf7 Remove Gin from the OIDC handlers 2022-06-20 12:31:19 +02:00
Juan Font Alonso
53e5c05b0a Remove gin from the poll handlers 2022-06-20 12:30:51 +02:00
Juan Font Alonso
dedeb4c181 Remove Gin from the Registration handler 2022-06-20 12:30:41 +02:00
Juan Font Alonso
e611063669 Migrate platform config out of Gin 2022-06-20 12:29:59 +02:00
Juan Font Alonso
6c9c9a401f Remove gin from DERP server 2022-06-18 19:51:37 +02:00
Juan Font
6da4396faa Merge pull request #654 from ChibangLW/main
Add version info to binary in Docker container
2022-06-18 18:48:35 +02:00
Juan Font Alonso
d89fb68a7a Switch to use gorilla's mux as muxer 2022-06-18 18:41:42 +02:00
Leon Lenzen
8d9462147c chore: use docker-meta version 2022-06-18 12:00:02 +02:00
Leon Lenzen
89b7fa6b06 chore: fix lint 2022-06-18 11:39:27 +02:00
Leon Lenzen
d4a550bb4c chore: add version to binary in containers 2022-06-18 11:36:09 +02:00
Juan Font Alonso
d5e331a2fb Remove Gin from OIDC callback 2022-06-17 17:42:17 +02:00
Juan Font Alonso
367da0fcc2 Remove Gin from simple endpoints for TS2019 2022-06-17 16:48:04 +02:00
Anton Schubert
8111b0aa83 update changelog 2022-06-17 11:07:35 +02:00
Anton Schubert
735440d1a3 add timeout for http shutdown, add db disconnect 2022-06-17 11:07:25 +02:00
Juan Font
3ae340527f Merge pull request #648 from juanfont/show-nodes-online
Send Online field of tailcfg.Node based on LastSeen
2022-06-16 19:26:13 +02:00
Juan Font
bfa9ed814d Merge branch 'main' into show-nodes-online 2022-06-16 18:53:25 +02:00
Juan Font Alonso
1e4678c02f Updated changelog 2022-06-16 18:48:32 +02:00
Juan Font Alonso
66fffd69ce Send Online field of tailcfg.Node based on LastSeen 2022-06-16 18:43:50 +02:00
Kristoffer Dalby
e3f99d670e Merge pull request #646 from juanfont/update-contributors 2022-06-16 16:08:31 +01:00
github-actions[bot]
360488abb4 docs(README): update contributors 2022-06-16 13:08:07 +00:00
Kristoffer Dalby
8dda44105e Merge pull request #643 from iSchluff/fix/dns-name-panic 2022-06-16 14:07:21 +01:00
Kristoffer Dalby
2215e17223 Merge branch 'main' into fix/dns-name-panic 2022-06-16 11:04:31 +01:00
Kristoffer Dalby
157db307f9 Merge pull request #642 from kradalby/ignore-integtest-dump 2022-06-16 08:35:47 +01:00
Juan Font
0bd39b2c5e Merge branch 'main' into ignore-integtest-dump 2022-06-16 00:25:45 +02:00
Anton Schubert
8f31ed51e1 fix occasional panic on registration
GenerateRandomStringDNSSafe will panic occasionally if the random base64
string contains too many - and _ due to the replacement. Fix by looping.
2022-06-15 12:22:57 +02:00
Kristoffer Dalby
d2d1f92836 Merge pull request #641 from juanfont/update-contributors 2022-06-12 22:57:32 +01:00
Kristoffer Dalby
c02819ab9f Ignore new dump file 2022-06-12 17:26:44 +00:00
github-actions[bot]
28a3a5bd61 docs(README): update contributors 2022-06-12 17:00:23 +00:00
Kristoffer Dalby
891815634b Merge pull request #639 from kradalby/ephemeral-error-msg 2022-06-12 17:59:48 +01:00
Kristoffer Dalby
8650328922 Remove debug output, it runs before we disable it 2022-06-12 16:40:43 +00:00
Kristoffer Dalby
7bd07e3b9b Merge branch 'main' into ephemeral-error-msg 2022-06-12 14:33:49 +01:00
Kristoffer Dalby
76195bb3ac Add warn if configuration could not be found 2022-06-12 13:32:16 +00:00
Juan Font
6afd492095 Merge pull request #638 from kradalby/update-nodes-derp
Simplify DERP maps update function
2022-06-12 15:26:20 +02:00
Kristoffer Dalby
c95bce4aea Update changelog 2022-06-12 13:18:49 +00:00
Kristoffer Dalby
fd3a1c13e3 Add a default to ephemeral_node_inactivity_timeout 2022-06-12 13:12:53 +00:00
Kristoffer Dalby
95824ac2ec MOve ephemeral inactivity config check to all the other config check 2022-06-12 13:12:43 +00:00
Kristoffer Dalby
a050158d11 Use new update state logic for derp maps 2022-06-12 12:27:37 +00:00
Kristoffer Dalby
e0ef601123 Merge pull request #636 from huskyii/fix_issue635 2022-06-12 12:53:19 +01:00
Jiang Zhu
9c5d485fdd fix issue 635 2022-06-12 17:01:17 +08:00
Juan Font
cb88b16207 Merge pull request #630 from kradalby/test-126
Add 1.26 to tests
2022-06-11 18:14:38 +02:00
Kristoffer Dalby
257c025975 Update build system 2022-06-11 15:42:06 +00:00
Kristoffer Dalby
50bdf9d3b9 Update vendor sha 2022-06-11 15:39:37 +00:00
Kristoffer Dalby
8d58894daa Tailscale 1.26 uses dnstype pointer 2022-06-11 15:34:02 +00:00
Kristoffer Dalby
43fa7f9fd5 Upgrade tailscale lib to 1.26 2022-06-11 15:34:02 +00:00
Kristoffer Dalby
f2a8bfeb9f Merge branch 'main' into test-126 2022-06-11 16:04:35 +01:00
Kristoffer Dalby
06bbeea37f Merge pull request #632 from juanfont/update-contributors 2022-06-11 16:04:21 +01:00
github-actions[bot]
e5f26f819a docs(README): update contributors 2022-06-11 14:35:56 +00:00
Kristoffer Dalby
a058f17946 Merge branch 'main' into test-126 2022-06-11 15:35:36 +01:00
Kristoffer Dalby
a4b4fc8b6c Merge pull request #624 from iSchluff/feature/configure-randomize-port 2022-06-11 15:35:24 +01:00
Kristoffer Dalby
ab35baaa29 Merge branch 'main' into feature/configure-randomize-port 2022-06-11 15:07:47 +01:00
Kristoffer Dalby
883bb92991 Merge pull request #618 from juanfont/acl-syntax-fixes 2022-06-11 15:07:29 +01:00
Kristoffer Dalby
bfb58de7b8 Add 1.26 to tests 2022-06-11 13:45:32 +00:00
Kristoffer Dalby
6faf2d63d0 Update integration dump tests 2022-06-11 13:31:30 +00:00
Kristoffer Dalby
569f3caab9 Use constants in tests 2022-06-11 13:17:07 +00:00
Kristoffer Dalby
7cd0f5e8a4 Merge branch 'main' into acl-syntax-fixes 2022-06-11 14:14:21 +01:00
Kristoffer Dalby
02cc6bcc05 Merge branch 'main' into feature/configure-randomize-port 2022-06-11 13:49:32 +01:00
Kristoffer Dalby
9ff09b73ad Update Changelog 2022-06-11 13:49:17 +01:00
Kristoffer Dalby
f93cf4b980 Merge pull request #628 from kradalby/acl-update-nodes 2022-06-11 13:32:57 +01:00
Juan Font Alonso
3d7be5b287 Minor rename 2022-06-11 14:12:53 +02:00
Juan Font Alonso
cdf41bd500 Merge branch 'acl-syntax-fixes' of https://github.com/juanfont/headscale into acl-syntax-fixes 2022-06-11 14:12:39 +02:00
Juan Font Alonso
735a6aaa39 Use const for IANA protcol numbers 2022-06-11 14:09:08 +02:00
Kristoffer Dalby
0c2648c188 Update the nodes after we have reloaded the ACL policy with sighup 2022-06-11 12:54:44 +01:00
Kristoffer Dalby
7e6291c21c Change Set state change function to filter instead of single namespace
This commit makes the setLastStateChangeToNow function take a list of
namespaces instead of a single namespace. If no namespaces is passed,
all namespaces will be updated. This means that the argument acts like a
filter.
2022-06-11 12:53:02 +01:00
Kristoffer Dalby
3f7749c6d4 Merge branch 'main' into feature/configure-randomize-port 2022-06-11 10:55:05 +01:00
Kristoffer Dalby
586c5411f1 Merge pull request #611 from huskyii/doc_openbsd 2022-06-11 10:54:28 +01:00
Jiang Zhu
2be16b581c 1) fix typo 2) another hard coded version 2022-06-11 17:23:01 +08:00
Jiang Zhu
06e22bf878 Merge branch 'juanfont:main' into doc_openbsd 2022-06-11 16:54:01 +08:00
Jiang Zhu
0b4b530809 remove the hardcoded version(suggested by @kradalby) 2022-06-11 16:41:52 +08:00
Kristoffer Dalby
efca3daa5c Merge pull request #612 from huskyii/enhance_cli_config 2022-06-10 20:38:11 +01:00
Kristoffer Dalby
fdefe46c40 Merge branch 'main' into enhance_cli_config 2022-06-10 20:18:33 +01:00
Anton Schubert
34be10840c add ability to set randomizeClientPort 2022-06-09 21:26:40 +02:00
Juan Font
80ad1db228 Merge branch 'main' into acl-syntax-fixes 2022-06-09 14:09:26 +02:00
Juan Font
e918ea89a3 Merge pull request #619 from majst01/simplify-split
Use strings.Cut to simplify logic
2022-06-09 14:08:49 +02:00
Juan Font Alonso
19b968849f Added missing file 2022-06-08 18:21:35 +02:00
Juan Font Alonso
5bc11891f5 Update internal docs with protocol usage 2022-06-08 18:15:38 +02:00
Juan Font Alonso
818d26b5f9 Updated changelog 2022-06-08 18:12:56 +02:00
Juan Font Alonso
c47354bdc3 Update internal docs to the new syntax 2022-06-08 18:12:47 +02:00
Stefan Majer
86ce0e0c66 Use strings.Cut to simplify logic 2022-06-08 18:09:11 +02:00
Juan Font Alonso
39f03b86c8 Added ACL test file 2022-06-08 18:06:25 +02:00
Juan Font Alonso
8287ba24b9 Do not lint the protocol magic numbers
I happily use https://pkg.go.dev/golang.org/x/net/internal/iana, but it is internal
2022-06-08 17:55:32 +02:00
Juan Font Alonso
ab1aac9f3e Improve ACLs by adding protocol parsing support 2022-06-08 17:43:59 +02:00
Juan Font Alonso
3e353004b8 Migrate ACLs syntax to new Tailscale format
Implements #617.

Tailscale has changed the format of their ACLs to use a more firewall-y terms ("users" & "ports" -> "src" & "dst"). They have also started using all-lowercase tags. This PR applies these changes.
2022-06-08 13:40:15 +02:00
Jiang Zhu
bcb04d38a5 Merge branch 'main' into enhance_cli_config
Extract LoadConfig from GetHeadscaleConfig, as they are conceptually
different operation, e.g.,
1) you can reload config through LoadConfig and do not get config
2) you can get config without reload config
2022-06-07 22:51:47 +08:00
Jiang Zhu
de0e2bf828 Merge branch 'main' into doc_openbsd 2022-06-07 22:31:32 +08:00
Kristoffer Dalby
8fed47a2be Merge pull request #616 from juanfont/update-contributors 2022-06-07 15:58:53 +02:00
github-actions[bot]
17d4968425 docs(README): update contributors 2022-06-07 06:16:00 +00:00
Kristoffer Dalby
54acee6880 Merge pull request #615 from demiflat/fix_typo 2022-06-07 08:15:17 +02:00
Darrell Kundel
a4e05d4db3 fix typo for GGO->CGO 2022-06-07 11:36:13 +08:00
Jiang Zhu
0c5a402206 add changelog 2022-06-05 23:15:21 +08:00
Jiang Zhu
8744eeeb19 ExecuteCommand set HEADSCALE_LOG_LEVEL to disabled 2022-06-05 23:14:49 +08:00
Jiang Zhu
ce13596077 add integration test for headscale -c 2022-06-05 23:13:58 +08:00
Jiang Zhu
402a29e50c impl heascale -c to specify config file 2022-06-05 18:25:09 +08:00
Jiang Zhu
0363e58467 cli.LoadConfig accepts config file now 2022-06-05 17:55:27 +08:00
Jiang Zhu
c8a14ccabb fix prettier 2022-06-05 16:01:53 +08:00
Jiang Zhu
1de29fd4e6 fix rcd link 2022-06-05 15:49:24 +08:00
Jiang Zhu
75a0155f73 add openbsd doc 2022-06-05 15:45:38 +08:00
61 changed files with 2829 additions and 827 deletions

View File

@@ -89,6 +89,8 @@ jobs:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new cache-to: type=local,dest=/tmp/.buildx-cache-new
build-args: |
VERSION=${{ steps.meta.outputs.version }}
- name: Prepare cache for next build - name: Prepare cache for next build
run: | run: |
rm -rf /tmp/.buildx-cache rm -rf /tmp/.buildx-cache
@@ -153,6 +155,8 @@ jobs:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache-debug cache-from: type=local,src=/tmp/.buildx-cache-debug
cache-to: type=local,dest=/tmp/.buildx-cache-debug-new cache-to: type=local,dest=/tmp/.buildx-cache-debug-new
build-args: |
VERSION=${{ steps.meta-debug.outputs.version }}
- name: Prepare cache for next build - name: Prepare cache for next build
run: | run: |
rm -rf /tmp/.buildx-cache-debug rm -rf /tmp/.buildx-cache-debug
@@ -217,6 +221,8 @@ jobs:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache-alpine cache-from: type=local,src=/tmp/.buildx-cache-alpine
cache-to: type=local,dest=/tmp/.buildx-cache-alpine-new cache-to: type=local,dest=/tmp/.buildx-cache-alpine-new
build-args: |
VERSION=${{ steps.meta-alpine.outputs.version }}
- name: Prepare cache for next build - name: Prepare cache for next build
run: | run: |
rm -rf /tmp/.buildx-cache-alpine rm -rf /tmp/.buildx-cache-alpine

View File

@@ -27,4 +27,9 @@ jobs:
- 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: nix develop --command -- make test_integration uses: nick-fields/retry@v2
with:
timeout_minutes: 240
max_attempts: 5
retry_on: error
command: nix develop --command -- make test_integration

2
.gitignore vendored
View File

@@ -31,3 +31,5 @@ test_output/
# Nix build output # Nix build output
result result
.direnv/ .direnv/
integration_test/etc/config.dump.yaml

View File

@@ -2,6 +2,10 @@
## 0.16.0 (2022-xx-xx) ## 0.16.0 (2022-xx-xx)
### BREAKING
- Old ACL syntax is no longer supported ("users" & "ports" -> "src" & "dst"). Please check [the new syntax](https://tailscale.com/kb/1018/acls/).
### Changes ### Changes
- **Drop** armhf (32-bit ARM) support. [#609](https://github.com/juanfont/headscale/pull/609) - **Drop** armhf (32-bit ARM) support. [#609](https://github.com/juanfont/headscale/pull/609)
@@ -22,6 +26,14 @@
- This change disables the logs by default - This change disables the logs by default
- Use [Prometheus]'s duration parser, supporting days (`d`), weeks (`w`) and years (`y`) [#598](https://github.com/juanfont/headscale/pull/598) - Use [Prometheus]'s duration parser, supporting days (`d`), weeks (`w`) and years (`y`) [#598](https://github.com/juanfont/headscale/pull/598)
- Add support for reloading ACLs with SIGHUP [#601](https://github.com/juanfont/headscale/pull/601) - Add support for reloading ACLs with SIGHUP [#601](https://github.com/juanfont/headscale/pull/601)
- Use new ACL syntax [#618](https://github.com/juanfont/headscale/pull/618)
- Add -c option to specify config file from command line [#285](https://github.com/juanfont/headscale/issues/285) [#612](https://github.com/juanfont/headscale/pull/601)
- Add configuration option to allow Tailscale clients to use a random WireGuard port. [kb/1181/firewalls](https://tailscale.com/kb/1181/firewalls) [#624](https://github.com/juanfont/headscale/pull/624)
- Improve obtuse UX regarding missing configuration (`ephemeral_node_inactivity_timeout` not set) [#639](https://github.com/juanfont/headscale/pull/639)
- Fix nodes being shown as 'offline' in `tailscale status` [#648](https://github.com/juanfont/headscale/pull/648)
- Improve shutdown behaviour [#651](https://github.com/juanfont/headscale/pull/651)
- Drop Gin as web framework in Headscale [648](https://github.com/juanfont/headscale/pull/648)
- Make tailnet node updates check interval configurable [#675](https://github.com/juanfont/headscale/pull/675)
## 0.15.0 (2022-03-20) ## 0.15.0 (2022-03-20)

View File

@@ -1,5 +1,6 @@
# Builder image # Builder image
FROM docker.io/golang:1.18.0-bullseye AS build FROM docker.io/golang:1.18.0-bullseye AS build
ARG VERSION=dev
ENV GOPATH /go ENV GOPATH /go
WORKDIR /go/src/headscale WORKDIR /go/src/headscale
@@ -8,7 +9,7 @@ RUN go mod download
COPY . . COPY . .
RUN GGO_ENABLED=0 GOOS=linux go install -a ./cmd/headscale RUN CGO_ENABLED=0 GOOS=linux go install -ldflags="-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$VERSION" -a ./cmd/headscale
RUN strip /go/bin/headscale RUN strip /go/bin/headscale
RUN test -e /go/bin/headscale RUN test -e /go/bin/headscale

View File

@@ -1,5 +1,6 @@
# Builder image # Builder image
FROM docker.io/golang:1.18.0-alpine AS build FROM docker.io/golang:1.18.0-alpine AS build
ARG VERSION=dev
ENV GOPATH /go ENV GOPATH /go
WORKDIR /go/src/headscale WORKDIR /go/src/headscale
@@ -9,7 +10,7 @@ RUN go mod download
COPY . . COPY . .
RUN GGO_ENABLED=0 GOOS=linux go install -a ./cmd/headscale RUN CGO_ENABLED=0 GOOS=linux go install -ldflags="-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$VERSION" -a ./cmd/headscale
RUN strip /go/bin/headscale RUN strip /go/bin/headscale
RUN test -e /go/bin/headscale RUN test -e /go/bin/headscale

View File

@@ -1,5 +1,6 @@
# Builder image # Builder image
FROM docker.io/golang:1.18.0-bullseye AS build FROM docker.io/golang:1.18.0-bullseye AS build
ARG VERSION=dev
ENV GOPATH /go ENV GOPATH /go
WORKDIR /go/src/headscale WORKDIR /go/src/headscale
@@ -8,7 +9,7 @@ RUN go mod download
COPY . . COPY . .
RUN GGO_ENABLED=0 GOOS=linux go install -a ./cmd/headscale RUN CGO_ENABLED=0 GOOS=linux go install -ldflags="-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$VERSION" -a ./cmd/headscale
RUN test -e /go/bin/headscale RUN test -e /go/bin/headscale
# Debug image # Debug image

View File

@@ -1,5 +1,5 @@
# Calculate version # Calculate version
version = $(git describe --always --tags --dirty) version ?= $(shell 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))

View File

@@ -195,6 +195,15 @@ make build
<sub style="font-size:14px"><b>Nico</b></sub> <sub style="font-size:14px"><b>Nico</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/huskyii>
<img src=https://avatars.githubusercontent.com/u/5499746?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jiang Zhu/>
<br />
<sub style="font-size:14px"><b>Jiang Zhu</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/e-zk> <a href=https://github.com/e-zk>
<img src=https://avatars.githubusercontent.com/u/58356365?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=e-zk/> <img src=https://avatars.githubusercontent.com/u/58356365?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=e-zk/>
@@ -202,8 +211,6 @@ make build
<sub style="font-size:14px"><b>e-zk</b></sub> <sub style="font-size:14px"><b>e-zk</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/arch4ngel> <a href=https://github.com/arch4ngel>
<img src=https://avatars.githubusercontent.com/u/11574161?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Justin Angel/> <img src=https://avatars.githubusercontent.com/u/11574161?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Justin Angel/>
@@ -239,6 +246,8 @@ make build
<sub style="font-size:14px"><b>ohdearaugustin</b></sub> <sub style="font-size:14px"><b>ohdearaugustin</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/Niek> <a href=https://github.com/Niek>
<img src=https://avatars.githubusercontent.com/u/213140?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Niek van der Maas/> <img src=https://avatars.githubusercontent.com/u/213140?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Niek van der Maas/>
@@ -246,8 +255,6 @@ make build
<sub style="font-size:14px"><b>Niek van der Maas</b></sub> <sub style="font-size:14px"><b>Niek van der Maas</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/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/>
@@ -262,6 +269,13 @@ 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>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/iSchluff>
<img src=https://avatars.githubusercontent.com/u/1429641?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Anton Schubert/>
<br />
<sub style="font-size:14px"><b>Anton Schubert</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/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/>
@@ -276,6 +290,8 @@ make build
<sub style="font-size:14px"><b>Hoàng Đức Hiếu</b></sub> <sub style="font-size:14px"><b>Hoàng Đức Hiếu</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/>
@@ -290,8 +306,13 @@ make build
<sub style="font-size:14px"><b>Deon Thomas</b></sub> <sub style="font-size:14px"><b>Deon Thomas</b></sub>
</a> </a>
</td> </td>
</tr> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<tr> <a href=https://github.com/ChibangLW>
<img src=https://avatars.githubusercontent.com/u/22293464?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ChibangLW/>
<br />
<sub style="font-size:14px"><b>ChibangLW</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/mevansam> <a href=https://github.com/mevansam>
<img src=https://avatars.githubusercontent.com/u/403630?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Mevan Samaratunga/> <img src=https://avatars.githubusercontent.com/u/403630?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Mevan Samaratunga/>
@@ -313,6 +334,22 @@ make build
<sub style="font-size:14px"><b>Paul Tötterman</b></sub> <sub style="font-size:14px"><b>Paul Tötterman</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/samson4649>
<img src=https://avatars.githubusercontent.com/u/12725953?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Samuel Lock/>
<br />
<sub style="font-size:14px"><b>Samuel Lock</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/majst01>
<img src=https://avatars.githubusercontent.com/u/410110?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Stefan Majer/>
<br />
<sub style="font-size:14px"><b>Stefan Majer</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/artemklevtsov> <a href=https://github.com/artemklevtsov>
<img src=https://avatars.githubusercontent.com/u/603798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Artem Klevtsov/> <img src=https://avatars.githubusercontent.com/u/603798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Artem Klevtsov/>
@@ -334,8 +371,6 @@ make build
<sub style="font-size:14px"><b>Pavlos Vinieratos</b></sub> <sub style="font-size:14px"><b>Pavlos Vinieratos</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/SilverBut> <a href=https://github.com/SilverBut>
<img src=https://avatars.githubusercontent.com/u/6560655?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Silver Bullet/> <img src=https://avatars.githubusercontent.com/u/6560655?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Silver Bullet/>
@@ -343,13 +378,8 @@ make build
<sub style="font-size:14px"><b>Silver Bullet</b></sub> <sub style="font-size:14px"><b>Silver Bullet</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> </tr>
<a href=https://github.com/majst01> <tr>
<img src=https://avatars.githubusercontent.com/u/410110?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Stefan Majer/>
<br />
<sub style="font-size:14px"><b>Stefan Majer</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/lachy2849> <a href=https://github.com/lachy2849>
<img src=https://avatars.githubusercontent.com/u/98844035?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=lachy2849/> <img src=https://avatars.githubusercontent.com/u/98844035?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=lachy2849/>
@@ -378,15 +408,6 @@ make build
<sub style="font-size:14px"><b>Antoine POPINEAU</b></sub> <sub style="font-size:14px"><b>Antoine POPINEAU</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/iSchluff>
<img src=https://avatars.githubusercontent.com/u/1429641?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Anton Schubert/>
<br />
<sub style="font-size:14px"><b>Anton Schubert</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/aofei> <a href=https://github.com/aofei>
<img src=https://avatars.githubusercontent.com/u/5037285?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Aofei Sheng/> <img src=https://avatars.githubusercontent.com/u/5037285?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Aofei Sheng/>
@@ -401,6 +422,8 @@ make build
<sub style="font-size:14px"><b>Arthur Woimbée</b></sub> <sub style="font-size:14px"><b>Arthur Woimbée</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/stensonb> <a href=https://github.com/stensonb>
<img src=https://avatars.githubusercontent.com/u/933389?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Bryan Stenson/> <img src=https://avatars.githubusercontent.com/u/933389?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Bryan Stenson/>
@@ -415,6 +438,13 @@ make build
<sub style="font-size:14px"><b> Carson Yang</b></sub> <sub style="font-size:14px"><b> Carson Yang</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/kundel>
<img src=https://avatars.githubusercontent.com/u/10158899?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=kundel/>
<br />
<sub style="font-size:14px"><b>kundel</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/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/>
@@ -422,8 +452,6 @@ make build
<sub style="font-size:14px"><b>Felix Kronlage-Dammers</b></sub> <sub style="font-size:14px"><b>Felix Kronlage-Dammers</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/felixonmars> <a href=https://github.com/felixonmars>
<img src=https://avatars.githubusercontent.com/u/1006477?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Felix Yan/> <img src=https://avatars.githubusercontent.com/u/1006477?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Felix Yan/>
@@ -438,6 +466,8 @@ make build
<sub style="font-size:14px"><b>JJGadgets</b></sub> <sub style="font-size:14px"><b>JJGadgets</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/madjam002> <a href=https://github.com/madjam002>
<img src=https://avatars.githubusercontent.com/u/679137?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jamie Greeff/> <img src=https://avatars.githubusercontent.com/u/679137?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jamie Greeff/>
@@ -466,8 +496,6 @@ make build
<sub style="font-size:14px"><b>rcursaru</b></sub> <sub style="font-size:14px"><b>rcursaru</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/renovate-bot> <a href=https://github.com/renovate-bot>
<img src=https://avatars.githubusercontent.com/u/25180681?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=WhiteSource Renovate/> <img src=https://avatars.githubusercontent.com/u/25180681?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=WhiteSource Renovate/>
@@ -482,6 +510,8 @@ make build
<sub style="font-size:14px"><b>Ryan Fowler</b></sub> <sub style="font-size:14px"><b>Ryan Fowler</b></sub>
</a> </a>
</td> </td>
</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/shaananc> <a href=https://github.com/shaananc>
<img src=https://avatars.githubusercontent.com/u/2287839?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Shaanan Cohney/> <img src=https://avatars.githubusercontent.com/u/2287839?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Shaanan Cohney/>
@@ -510,8 +540,6 @@ make build
<sub style="font-size:14px"><b>The Gitter Badger</b></sub> <sub style="font-size:14px"><b>The Gitter Badger</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/tianon> <a href=https://github.com/tianon>
<img src=https://avatars.githubusercontent.com/u/161631?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tianon Gravi/> <img src=https://avatars.githubusercontent.com/u/161631?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tianon Gravi/>
@@ -526,6 +554,8 @@ make build
<sub style="font-size:14px"><b>Tjerk Woudsma</b></sub> <sub style="font-size:14px"><b>Tjerk Woudsma</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/y0ngb1n> <a href=https://github.com/y0ngb1n>
<img src=https://avatars.githubusercontent.com/u/25719408?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Yang Bin/> <img src=https://avatars.githubusercontent.com/u/25719408?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Yang Bin/>
@@ -554,8 +584,6 @@ make build
<sub style="font-size:14px"><b>derelm</b></sub> <sub style="font-size:14px"><b>derelm</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/nning> <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/> <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/>
@@ -570,6 +598,8 @@ make build
<sub style="font-size:14px"><b>ignoramous</b></sub> <sub style="font-size:14px"><b>ignoramous</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/lion24> <a href=https://github.com/lion24>
<img src=https://avatars.githubusercontent.com/u/1382102?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=lion24/> <img src=https://avatars.githubusercontent.com/u/1382102?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=lion24/>

107
acls.go
View File

@@ -23,6 +23,7 @@ const (
errInvalidGroup = Error("invalid group") errInvalidGroup = Error("invalid group")
errInvalidTag = Error("invalid tag") errInvalidTag = Error("invalid tag")
errInvalidPortFormat = Error("invalid port format") errInvalidPortFormat = Error("invalid port format")
errWildcardIsNeeded = Error("wildcard as port is required for the protocol")
) )
const ( const (
@@ -36,6 +37,23 @@ const (
expectedTokenItems = 2 expectedTokenItems = 2
) )
// For some reason golang.org/x/net/internal/iana is an internal package.
const (
protocolICMP = 1 // Internet Control Message
protocolIGMP = 2 // Internet Group Management
protocolIPv4 = 4 // IPv4 encapsulation
protocolTCP = 6 // Transmission Control
protocolEGP = 8 // Exterior Gateway Protocol
protocolIGP = 9 // any private interior gateway (used by Cisco for their IGRP)
protocolUDP = 17 // User Datagram
protocolGRE = 47 // Generic Routing Encapsulation
protocolESP = 50 // Encap Security Payload
protocolAH = 51 // Authentication Header
protocolIPv6ICMP = 58 // ICMP for IPv6
protocolSCTP = 132 // Stream Control Transmission Protocol
ProtocolFC = 133 // Fibre Channel
)
// LoadACLPolicy loads the ACL policy from the specify path, and generates the ACL rules. // LoadACLPolicy loads the ACL policy from the specify path, and generates the ACL rules.
func (h *Headscale) LoadACLPolicy(path string) error { func (h *Headscale) LoadACLPolicy(path string) error {
log.Debug(). log.Debug().
@@ -123,23 +141,31 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
} }
srcIPs := []string{} srcIPs := []string{}
for innerIndex, user := range acl.Users { for innerIndex, src := range acl.Sources {
srcs, err := h.generateACLPolicySrcIP(machines, *h.aclPolicy, user) srcs, err := h.generateACLPolicySrcIP(machines, *h.aclPolicy, src)
if err != nil { if err != nil {
log.Error(). log.Error().
Msgf("Error parsing ACL %d, User %d", index, innerIndex) Msgf("Error parsing ACL %d, Source %d", index, innerIndex)
return nil, err return nil, err
} }
srcIPs = append(srcIPs, srcs...) srcIPs = append(srcIPs, srcs...)
} }
protocols, needsWildcard, err := parseProtocol(acl.Protocol)
if err != nil {
log.Error().
Msgf("Error parsing ACL %d. protocol unknown %s", index, acl.Protocol)
return nil, err
}
destPorts := []tailcfg.NetPortRange{} destPorts := []tailcfg.NetPortRange{}
for innerIndex, ports := range acl.Ports { for innerIndex, dest := range acl.Destinations {
dests, err := h.generateACLPolicyDestPorts(machines, *h.aclPolicy, ports) dests, err := h.generateACLPolicyDest(machines, *h.aclPolicy, dest, needsWildcard)
if err != nil { if err != nil {
log.Error(). log.Error().
Msgf("Error parsing ACL %d, Port %d", index, innerIndex) Msgf("Error parsing ACL %d, Destination %d", index, innerIndex)
return nil, err return nil, err
} }
@@ -149,6 +175,7 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
rules = append(rules, tailcfg.FilterRule{ rules = append(rules, tailcfg.FilterRule{
SrcIPs: srcIPs, SrcIPs: srcIPs,
DstPorts: destPorts, DstPorts: destPorts,
IPProto: protocols,
}) })
} }
@@ -158,17 +185,18 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
func (h *Headscale) generateACLPolicySrcIP( func (h *Headscale) generateACLPolicySrcIP(
machines []Machine, machines []Machine,
aclPolicy ACLPolicy, aclPolicy ACLPolicy,
u string, src string,
) ([]string, error) { ) ([]string, error) {
return expandAlias(machines, aclPolicy, u, h.cfg.OIDC.StripEmaildomain) return expandAlias(machines, aclPolicy, src, h.cfg.OIDC.StripEmaildomain)
} }
func (h *Headscale) generateACLPolicyDestPorts( func (h *Headscale) generateACLPolicyDest(
machines []Machine, machines []Machine,
aclPolicy ACLPolicy, aclPolicy ACLPolicy,
d string, dest string,
needsWildcard bool,
) ([]tailcfg.NetPortRange, error) { ) ([]tailcfg.NetPortRange, error) {
tokens := strings.Split(d, ":") tokens := strings.Split(dest, ":")
if len(tokens) < expectedTokenItems || len(tokens) > 3 { if len(tokens) < expectedTokenItems || len(tokens) > 3 {
return nil, errInvalidPortFormat return nil, errInvalidPortFormat
} }
@@ -195,7 +223,7 @@ func (h *Headscale) generateACLPolicyDestPorts(
if err != nil { if err != nil {
return nil, err return nil, err
} }
ports, err := expandPorts(tokens[len(tokens)-1]) ports, err := expandPorts(tokens[len(tokens)-1], needsWildcard)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -214,6 +242,54 @@ func (h *Headscale) generateACLPolicyDestPorts(
return dests, nil return dests, nil
} }
// parseProtocol reads the proto field of the ACL and generates a list of
// protocols that will be allowed, following the IANA IP protocol number
// https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml
//
// If the ACL proto field is empty, it allows ICMPv4, ICMPv6, TCP, and UDP,
// as per Tailscale behaviour (see tailcfg.FilterRule).
//
// Also returns a boolean indicating if the protocol
// requires all the destinations to use wildcard as port number (only TCP,
// UDP and SCTP support specifying ports).
func parseProtocol(protocol string) ([]int, bool, error) {
switch protocol {
case "":
return []int{protocolICMP, protocolIPv6ICMP, protocolTCP, protocolUDP}, false, nil
case "igmp":
return []int{protocolIGMP}, true, nil
case "ipv4", "ip-in-ip":
return []int{protocolIPv4}, true, nil
case "tcp":
return []int{protocolTCP}, false, nil
case "egp":
return []int{protocolEGP}, true, nil
case "igp":
return []int{protocolIGP}, true, nil
case "udp":
return []int{protocolUDP}, false, nil
case "gre":
return []int{protocolGRE}, true, nil
case "esp":
return []int{protocolESP}, true, nil
case "ah":
return []int{protocolAH}, true, nil
case "sctp":
return []int{protocolSCTP}, false, nil
case "icmp":
return []int{protocolICMP, protocolIPv6ICMP}, true, nil
default:
protocolNumber, err := strconv.Atoi(protocol)
if err != nil {
return nil, false, err
}
needsWildcard := protocolNumber != protocolTCP && protocolNumber != protocolUDP && protocolNumber != protocolSCTP
return []int{protocolNumber}, needsWildcard, nil
}
}
// expandalias has an input of either // expandalias has an input of either
// - a namespace // - a namespace
// - a group // - a group
@@ -268,6 +344,7 @@ func expandAlias(
alias, alias,
) )
} }
return ips, nil return ips, nil
} else { } else {
return ips, err return ips, err
@@ -359,13 +436,17 @@ func excludeCorrectlyTaggedNodes(
return out return out
} }
func expandPorts(portsStr string) (*[]tailcfg.PortRange, error) { func expandPorts(portsStr string, needsWildcard bool) (*[]tailcfg.PortRange, error) {
if portsStr == "*" { if portsStr == "*" {
return &[]tailcfg.PortRange{ return &[]tailcfg.PortRange{
{First: portRangeBegin, Last: portRangeEnd}, {First: portRangeBegin, Last: portRangeEnd},
}, nil }, nil
} }
if needsWildcard {
return nil, errWildcardIsNeeded
}
ports := []tailcfg.PortRange{} ports := []tailcfg.PortRange{}
for _, portStr := range strings.Split(portsStr, ",") { for _, portStr := range strings.Split(portsStr, ",") {
rang := strings.Split(portStr, "-") rang := strings.Split(portStr, "-")

View File

@@ -62,7 +62,7 @@ func (s *Suite) TestBasicRule(c *check.C) {
func (s *Suite) TestInvalidAction(c *check.C) { func (s *Suite) TestInvalidAction(c *check.C) {
app.aclPolicy = &ACLPolicy{ app.aclPolicy = &ACLPolicy{
ACLs: []ACL{ ACLs: []ACL{
{Action: "invalidAction", Users: []string{"*"}, Ports: []string{"*:*"}}, {Action: "invalidAction", Sources: []string{"*"}, Destinations: []string{"*:*"}},
}, },
} }
err := app.UpdateACLRules() err := app.UpdateACLRules()
@@ -70,14 +70,14 @@ func (s *Suite) TestInvalidAction(c *check.C) {
} }
func (s *Suite) TestInvalidGroupInGroup(c *check.C) { func (s *Suite) TestInvalidGroupInGroup(c *check.C) {
// this ACL is wrong because the group in users sections doesn't exist // this ACL is wrong because the group in Sources sections doesn't exist
app.aclPolicy = &ACLPolicy{ app.aclPolicy = &ACLPolicy{
Groups: Groups{ Groups: Groups{
"group:test": []string{"foo"}, "group:test": []string{"foo"},
"group:error": []string{"foo", "group:test"}, "group:error": []string{"foo", "group:test"},
}, },
ACLs: []ACL{ ACLs: []ACL{
{Action: "accept", Users: []string{"group:error"}, Ports: []string{"*:*"}}, {Action: "accept", Sources: []string{"group:error"}, Destinations: []string{"*:*"}},
}, },
} }
err := app.UpdateACLRules() err := app.UpdateACLRules()
@@ -88,7 +88,7 @@ func (s *Suite) TestInvalidTagOwners(c *check.C) {
// this ACL is wrong because no tagOwners own the requested tag for the server // this ACL is wrong because no tagOwners own the requested tag for the server
app.aclPolicy = &ACLPolicy{ app.aclPolicy = &ACLPolicy{
ACLs: []ACL{ ACLs: []ACL{
{Action: "accept", Users: []string{"tag:foo"}, Ports: []string{"*:*"}}, {Action: "accept", Sources: []string{"tag:foo"}, Destinations: []string{"*:*"}},
}, },
} }
err := app.UpdateACLRules() err := app.UpdateACLRules()
@@ -97,8 +97,8 @@ func (s *Suite) TestInvalidTagOwners(c *check.C) {
// this test should validate that we can expand a group in a TagOWner section and // this test should validate that we can expand a group in a TagOWner section and
// match properly the IP's of the related hosts. The owner is valid and the tag is also valid. // match properly the IP's of the related hosts. The owner is valid and the tag is also valid.
// the tag is matched in the Users section. // the tag is matched in the Sources section.
func (s *Suite) TestValidExpandTagOwnersInUsers(c *check.C) { func (s *Suite) TestValidExpandTagOwnersInSources(c *check.C) {
namespace, err := app.CreateNamespace("user1") namespace, err := app.CreateNamespace("user1")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@@ -131,7 +131,7 @@ func (s *Suite) TestValidExpandTagOwnersInUsers(c *check.C) {
Groups: Groups{"group:test": []string{"user1", "user2"}}, Groups: Groups{"group:test": []string{"user1", "user2"}},
TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}}, TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}},
ACLs: []ACL{ ACLs: []ACL{
{Action: "accept", Users: []string{"tag:test"}, Ports: []string{"*:*"}}, {Action: "accept", Sources: []string{"tag:test"}, Destinations: []string{"*:*"}},
}, },
} }
err = app.UpdateACLRules() err = app.UpdateACLRules()
@@ -143,8 +143,8 @@ func (s *Suite) TestValidExpandTagOwnersInUsers(c *check.C) {
// this test should validate that we can expand a group in a TagOWner section and // this test should validate that we can expand a group in a TagOWner section and
// match properly the IP's of the related hosts. The owner is valid and the tag is also valid. // match properly the IP's of the related hosts. The owner is valid and the tag is also valid.
// the tag is matched in the Ports section. // the tag is matched in the Destinations section.
func (s *Suite) TestValidExpandTagOwnersInPorts(c *check.C) { func (s *Suite) TestValidExpandTagOwnersInDestinations(c *check.C) {
namespace, err := app.CreateNamespace("user1") namespace, err := app.CreateNamespace("user1")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@@ -177,7 +177,7 @@ func (s *Suite) TestValidExpandTagOwnersInPorts(c *check.C) {
Groups: Groups{"group:test": []string{"user1", "user2"}}, Groups: Groups{"group:test": []string{"user1", "user2"}},
TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}}, TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}},
ACLs: []ACL{ ACLs: []ACL{
{Action: "accept", Users: []string{"*"}, Ports: []string{"tag:test:*"}}, {Action: "accept", Sources: []string{"*"}, Destinations: []string{"tag:test:*"}},
}, },
} }
err = app.UpdateACLRules() err = app.UpdateACLRules()
@@ -222,7 +222,7 @@ func (s *Suite) TestInvalidTagValidNamespace(c *check.C) {
app.aclPolicy = &ACLPolicy{ app.aclPolicy = &ACLPolicy{
TagOwners: TagOwners{"tag:test": []string{"user1"}}, TagOwners: TagOwners{"tag:test": []string{"user1"}},
ACLs: []ACL{ ACLs: []ACL{
{Action: "accept", Users: []string{"user1"}, Ports: []string{"*:*"}}, {Action: "accept", Sources: []string{"user1"}, Destinations: []string{"*:*"}},
}, },
} }
err = app.UpdateACLRules() err = app.UpdateACLRules()
@@ -287,9 +287,9 @@ func (s *Suite) TestValidTagInvalidNamespace(c *check.C) {
TagOwners: TagOwners{"tag:webapp": []string{"user1"}}, TagOwners: TagOwners{"tag:webapp": []string{"user1"}},
ACLs: []ACL{ ACLs: []ACL{
{ {
Action: "accept", Action: "accept",
Users: []string{"user1"}, Sources: []string{"user1"},
Ports: []string{"tag:webapp:80,443"}, Destinations: []string{"tag:webapp:80,443"},
}, },
}, },
} }
@@ -321,6 +321,20 @@ func (s *Suite) TestPortRange(c *check.C) {
c.Assert(rules[0].DstPorts[0].Ports.Last, check.Equals, uint16(5500)) c.Assert(rules[0].DstPorts[0].Ports.Last, check.Equals, uint16(5500))
} }
func (s *Suite) TestProtocolParsing(c *check.C) {
err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_protocols.hujson")
c.Assert(err, check.IsNil)
rules, err := app.generateACLRules()
c.Assert(err, check.IsNil)
c.Assert(rules, check.NotNil)
c.Assert(rules, check.HasLen, 3)
c.Assert(rules[0].IPProto[0], check.Equals, protocolTCP)
c.Assert(rules[1].IPProto[0], check.Equals, protocolUDP)
c.Assert(rules[2].IPProto[1], check.Equals, protocolIPv6ICMP)
}
func (s *Suite) TestPortWildcard(c *check.C) { func (s *Suite) TestPortWildcard(c *check.C) {
err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_wildcards.hujson") err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_wildcards.hujson")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@@ -628,7 +642,8 @@ func Test_expandTagOwners(t *testing.T) {
func Test_expandPorts(t *testing.T) { func Test_expandPorts(t *testing.T) {
type args struct { type args struct {
portsStr string portsStr string
needsWildcard bool
} }
tests := []struct { tests := []struct {
name string name string
@@ -638,15 +653,29 @@ func Test_expandPorts(t *testing.T) {
}{ }{
{ {
name: "wildcard", name: "wildcard",
args: args{portsStr: "*"}, args: args{portsStr: "*", needsWildcard: true},
want: &[]tailcfg.PortRange{ want: &[]tailcfg.PortRange{
{First: portRangeBegin, Last: portRangeEnd}, {First: portRangeBegin, Last: portRangeEnd},
}, },
wantErr: false, wantErr: false,
}, },
{ {
name: "two ports", name: "needs wildcard but does not require it",
args: args{portsStr: "80,443"}, args: args{portsStr: "*", needsWildcard: false},
want: &[]tailcfg.PortRange{
{First: portRangeBegin, Last: portRangeEnd},
},
wantErr: false,
},
{
name: "needs wildcard but gets port",
args: args{portsStr: "80,443", needsWildcard: true},
want: nil,
wantErr: true,
},
{
name: "two Destinations",
args: args{portsStr: "80,443", needsWildcard: false},
want: &[]tailcfg.PortRange{ want: &[]tailcfg.PortRange{
{First: 80, Last: 80}, {First: 80, Last: 80},
{First: 443, Last: 443}, {First: 443, Last: 443},
@@ -655,7 +684,7 @@ func Test_expandPorts(t *testing.T) {
}, },
{ {
name: "a range and a port", name: "a range and a port",
args: args{portsStr: "80-1024,443"}, args: args{portsStr: "80-1024,443", needsWildcard: false},
want: &[]tailcfg.PortRange{ want: &[]tailcfg.PortRange{
{First: 80, Last: 1024}, {First: 80, Last: 1024},
{First: 443, Last: 443}, {First: 443, Last: 443},
@@ -664,38 +693,38 @@ func Test_expandPorts(t *testing.T) {
}, },
{ {
name: "out of bounds", name: "out of bounds",
args: args{portsStr: "854038"}, args: args{portsStr: "854038", needsWildcard: false},
want: nil, want: nil,
wantErr: true, wantErr: true,
}, },
{ {
name: "wrong port", name: "wrong port",
args: args{portsStr: "85a38"}, args: args{portsStr: "85a38", needsWildcard: false},
want: nil, want: nil,
wantErr: true, wantErr: true,
}, },
{ {
name: "wrong port in first", name: "wrong port in first",
args: args{portsStr: "a-80"}, args: args{portsStr: "a-80", needsWildcard: false},
want: nil, want: nil,
wantErr: true, wantErr: true,
}, },
{ {
name: "wrong port in last", name: "wrong port in last",
args: args{portsStr: "80-85a38"}, args: args{portsStr: "80-85a38", needsWildcard: false},
want: nil, want: nil,
wantErr: true, wantErr: true,
}, },
{ {
name: "wrong port format", name: "wrong port format",
args: args{portsStr: "80-85a38-3"}, args: args{portsStr: "80-85a38-3", needsWildcard: false},
want: nil, want: nil,
wantErr: true, wantErr: true,
}, },
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
got, err := expandPorts(test.args.portsStr) got, err := expandPorts(test.args.portsStr, test.args.needsWildcard)
if (err != nil) != test.wantErr { if (err != nil) != test.wantErr {
t.Errorf("expandPorts() error = %v, wantErr %v", err, test.wantErr) t.Errorf("expandPorts() error = %v, wantErr %v", err, test.wantErr)

View File

@@ -11,18 +11,19 @@ import (
// ACLPolicy represents a Tailscale ACL Policy. // ACLPolicy represents a Tailscale ACL Policy.
type ACLPolicy struct { type ACLPolicy struct {
Groups Groups `json:"Groups" yaml:"Groups"` Groups Groups `json:"groups" yaml:"groups"`
Hosts Hosts `json:"Hosts" yaml:"Hosts"` Hosts Hosts `json:"hosts" yaml:"hosts"`
TagOwners TagOwners `json:"TagOwners" yaml:"TagOwners"` TagOwners TagOwners `json:"tagOwners" yaml:"tagOwners"`
ACLs []ACL `json:"ACLs" yaml:"ACLs"` ACLs []ACL `json:"acls" yaml:"acls"`
Tests []ACLTest `json:"Tests" yaml:"Tests"` Tests []ACLTest `json:"tests" yaml:"tests"`
} }
// ACL is a basic rule for the ACL Policy. // ACL is a basic rule for the ACL Policy.
type ACL struct { type ACL struct {
Action string `json:"Action" yaml:"Action"` Action string `json:"action" yaml:"action"`
Users []string `json:"Users" yaml:"Users"` Protocol string `json:"proto" yaml:"proto"`
Ports []string `json:"Ports" yaml:"Ports"` Sources []string `json:"src" yaml:"src"`
Destinations []string `json:"dst" yaml:"dst"`
} }
// Groups references a series of alias in the ACL rules. // Groups references a series of alias in the ACL rules.
@@ -36,9 +37,9 @@ type TagOwners map[string][]string
// ACLTest is not implemented, but should be use to check if a certain rule is allowed. // ACLTest is not implemented, but should be use to check if a certain rule is allowed.
type ACLTest struct { type ACLTest struct {
User string `json:"User" yaml:"User"` Source string `json:"src" yaml:"src"`
Allow []string `json:"Allow" yaml:"Allow"` Accept []string `json:"accept" yaml:"accept"`
Deny []string `json:"Deny,omitempty" yaml:"Deny,omitempty"` Deny []string `json:"deny,omitempty" yaml:"deny,omitempty"`
} }
// UnmarshalJSON allows to parse the Hosts directly into netaddr objects. // UnmarshalJSON allows to parse the Hosts directly into netaddr objects.

296
api.go
View File

@@ -12,7 +12,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gorilla/mux"
"github.com/klauspost/compress/zstd" "github.com/klauspost/compress/zstd"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gorm.io/gorm" "gorm.io/gorm"
@@ -32,12 +32,19 @@ const (
// 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.Data( writer http.ResponseWriter,
http.StatusOK, req *http.Request,
"text/plain; charset=utf-8", ) {
[]byte(MachinePublicKeyStripPrefix(h.privateKey.Public())), writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
) writer.WriteHeader(http.StatusOK)
_, err := writer.Write([]byte(MachinePublicKeyStripPrefix(h.privateKey.Public())))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
} }
type registerWebAPITemplateConfig struct { type registerWebAPITemplateConfig struct {
@@ -63,10 +70,21 @@ var registerWebAPITemplate = template.Must(
// RegisterWebAPI shows a simple message in the browser to point to the CLI // RegisterWebAPI shows a simple message in the browser to point to the CLI
// Listens in /register. // Listens in /register.
func (h *Headscale) RegisterWebAPI(ctx *gin.Context) { func (h *Headscale) RegisterWebAPI(
machineKeyStr := ctx.Query("key") writer http.ResponseWriter,
req *http.Request,
) {
machineKeyStr := req.URL.Query().Get("key")
if machineKeyStr == "" { if machineKeyStr == "" {
ctx.String(http.StatusBadRequest, "Wrong params") writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusBadRequest)
_, err := writer.Write([]byte("Wrong params"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return return
} }
@@ -79,21 +97,48 @@ func (h *Headscale) RegisterWebAPI(ctx *gin.Context) {
Str("func", "RegisterWebAPI"). Str("func", "RegisterWebAPI").
Err(err). Err(err).
Msg("Could not render register web API template") Msg("Could not render register web API template")
ctx.Data( writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
http.StatusInternalServerError, writer.WriteHeader(http.StatusInternalServerError)
"text/html; charset=utf-8", _, err = writer.Write([]byte("Could not render register web API template"))
[]byte("Could not render register web API template"), if err != nil {
) log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return
} }
ctx.Data(http.StatusOK, "text/html; charset=utf-8", content.Bytes()) writer.Header().Set("Content-Type", "text/html; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, err := writer.Write(content.Bytes())
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
} }
// RegistrationHandler handles the actual registration process of a machine // RegistrationHandler handles the actual registration process of a machine
// Endpoint /machine/:id. // Endpoint /machine/:mkey.
func (h *Headscale) RegistrationHandler(ctx *gin.Context) { func (h *Headscale) RegistrationHandler(
body, _ := io.ReadAll(ctx.Request.Body) writer http.ResponseWriter,
machineKeyStr := ctx.Param("id") req *http.Request,
) {
vars := mux.Vars(req)
machineKeyStr, ok := vars["mkey"]
if !ok || machineKeyStr == "" {
log.Error().
Str("handler", "RegistrationHandler").
Msg("No machine ID in request")
http.Error(writer, "No machine ID in request", http.StatusBadRequest)
return
}
body, _ := io.ReadAll(req.Body)
var machineKey key.MachinePublic var machineKey key.MachinePublic
err := machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machineKeyStr))) err := machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machineKeyStr)))
@@ -103,19 +148,19 @@ func (h *Headscale) RegistrationHandler(ctx *gin.Context) {
Err(err). Err(err).
Msg("Cannot parse machine key") Msg("Cannot parse machine key")
machineRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc() machineRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc()
ctx.String(http.StatusInternalServerError, "Sad!") http.Error(writer, "Cannot parse machine key", http.StatusBadRequest)
return return
} }
req := tailcfg.RegisterRequest{} registerRequest := tailcfg.RegisterRequest{}
err = decode(body, &req, &machineKey, h.privateKey) err = decode(body, &registerRequest, &machineKey, h.privateKey)
if err != nil { if err != nil {
log.Error(). log.Error().
Caller(). Caller().
Err(err). Err(err).
Msg("Cannot decode message") Msg("Cannot decode message")
machineRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc() machineRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc()
ctx.String(http.StatusInternalServerError, "Very sad!") http.Error(writer, "Cannot decode message", http.StatusBadRequest)
return return
} }
@@ -123,23 +168,23 @@ func (h *Headscale) RegistrationHandler(ctx *gin.Context) {
now := time.Now().UTC() now := time.Now().UTC()
machine, err := h.GetMachineByMachineKey(machineKey) machine, err := h.GetMachineByMachineKey(machineKey)
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
log.Info().Str("machine", req.Hostinfo.Hostname).Msg("New machine") log.Info().Str("machine", registerRequest.Hostinfo.Hostname).Msg("New machine")
machineKeyStr := MachinePublicKeyStripPrefix(machineKey) machineKeyStr := MachinePublicKeyStripPrefix(machineKey)
// If the machine has AuthKey set, handle registration via PreAuthKeys // If the machine has AuthKey set, handle registration via PreAuthKeys
if req.Auth.AuthKey != "" { if registerRequest.Auth.AuthKey != "" {
h.handleAuthKey(ctx, machineKey, req) h.handleAuthKey(writer, req, machineKey, registerRequest)
return return
} }
givenName, err := h.GenerateGivenName(req.Hostinfo.Hostname) givenName, err := h.GenerateGivenName(registerRequest.Hostinfo.Hostname)
if err != nil { if err != nil {
log.Error(). log.Error().
Caller(). Caller().
Str("func", "RegistrationHandler"). Str("func", "RegistrationHandler").
Str("hostinfo.name", req.Hostinfo.Hostname). Str("hostinfo.name", registerRequest.Hostinfo.Hostname).
Err(err) Err(err)
return return
@@ -151,20 +196,20 @@ func (h *Headscale) RegistrationHandler(ctx *gin.Context) {
// happens // happens
newMachine := Machine{ newMachine := Machine{
MachineKey: machineKeyStr, MachineKey: machineKeyStr,
Hostname: req.Hostinfo.Hostname, Hostname: registerRequest.Hostinfo.Hostname,
GivenName: givenName, GivenName: givenName,
NodeKey: NodePublicKeyStripPrefix(req.NodeKey), NodeKey: NodePublicKeyStripPrefix(registerRequest.NodeKey),
LastSeen: &now, LastSeen: &now,
Expiry: &time.Time{}, Expiry: &time.Time{},
} }
if !req.Expiry.IsZero() { if !registerRequest.Expiry.IsZero() {
log.Trace(). log.Trace().
Caller(). Caller().
Str("machine", req.Hostinfo.Hostname). Str("machine", registerRequest.Hostinfo.Hostname).
Time("expiry", req.Expiry). Time("expiry", registerRequest.Expiry).
Msg("Non-zero expiry time requested") Msg("Non-zero expiry time requested")
newMachine.Expiry = &req.Expiry newMachine.Expiry = &registerRequest.Expiry
} }
h.registrationCache.Set( h.registrationCache.Set(
@@ -173,7 +218,7 @@ func (h *Headscale) RegistrationHandler(ctx *gin.Context) {
registerCacheExpiration, registerCacheExpiration,
) )
h.handleMachineRegistrationNew(ctx, machineKey, req) h.handleMachineRegistrationNew(writer, req, machineKey, registerRequest)
return return
} }
@@ -185,11 +230,11 @@ func (h *Headscale) RegistrationHandler(ctx *gin.Context) {
// - Trying to log out (sending a expiry in the past) // - Trying to log out (sending a expiry in the past)
// - A valid, registered machine, looking for the node map // - A valid, registered machine, looking for the node map
// - Expired machine wanting to reauthenticate // - Expired machine wanting to reauthenticate
if machine.NodeKey == NodePublicKeyStripPrefix(req.NodeKey) { if machine.NodeKey == NodePublicKeyStripPrefix(registerRequest.NodeKey) {
// The client sends an Expiry in the past if the client is requesting to expire the key (aka logout) // 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 // https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L648
if !req.Expiry.IsZero() && req.Expiry.UTC().Before(now) { if !registerRequest.Expiry.IsZero() && registerRequest.Expiry.UTC().Before(now) {
h.handleMachineLogOut(ctx, machineKey, *machine) h.handleMachineLogOut(writer, req, machineKey, *machine)
return return
} }
@@ -197,22 +242,22 @@ func (h *Headscale) RegistrationHandler(ctx *gin.Context) {
// If machine is not expired, and is register, we have a already accepted this machine, // If machine is not expired, and is register, we have a already accepted this machine,
// let it proceed with a valid registration // let it proceed with a valid registration
if !machine.isExpired() { if !machine.isExpired() {
h.handleMachineValidRegistration(ctx, machineKey, *machine) h.handleMachineValidRegistration(writer, req, machineKey, *machine)
return return
} }
} }
// The NodeKey we have matches OldNodeKey, which means this is a refresh after a key expiration // The NodeKey we have matches OldNodeKey, which means this is a refresh after a key expiration
if machine.NodeKey == NodePublicKeyStripPrefix(req.OldNodeKey) && if machine.NodeKey == NodePublicKeyStripPrefix(registerRequest.OldNodeKey) &&
!machine.isExpired() { !machine.isExpired() {
h.handleMachineRefreshKey(ctx, machineKey, req, *machine) h.handleMachineRefreshKey(writer, req, machineKey, registerRequest, *machine)
return return
} }
// The machine has expired // The machine has expired
h.handleMachineExpired(ctx, machineKey, req, *machine) h.handleMachineExpired(writer, req, machineKey, registerRequest, *machine)
return return
} }
@@ -220,12 +265,12 @@ func (h *Headscale) RegistrationHandler(ctx *gin.Context) {
func (h *Headscale) getMapResponse( func (h *Headscale) getMapResponse(
machineKey key.MachinePublic, machineKey key.MachinePublic,
req tailcfg.MapRequest, mapRequest tailcfg.MapRequest,
machine *Machine, machine *Machine,
) ([]byte, error) { ) ([]byte, error) {
log.Trace(). log.Trace().
Str("func", "getMapResponse"). Str("func", "getMapResponse").
Str("machine", req.Hostinfo.Hostname). Str("machine", mapRequest.Hostinfo.Hostname).
Msg("Creating Map response") Msg("Creating Map response")
node, err := machine.toNode(h.cfg.BaseDomain, h.cfg.DNSConfig, true) node, err := machine.toNode(h.cfg.BaseDomain, h.cfg.DNSConfig, true)
if err != nil { if err != nil {
@@ -279,18 +324,19 @@ func (h *Headscale) getMapResponse(
DERPMap: h.DERPMap, DERPMap: h.DERPMap,
UserProfiles: profiles, UserProfiles: profiles,
Debug: &tailcfg.Debug{ Debug: &tailcfg.Debug{
DisableLogTail: !h.cfg.LogTail.Enabled, DisableLogTail: !h.cfg.LogTail.Enabled,
RandomizeClientPort: h.cfg.RandomizeClientPort,
}, },
} }
log.Trace(). log.Trace().
Str("func", "getMapResponse"). Str("func", "getMapResponse").
Str("machine", req.Hostinfo.Hostname). Str("machine", mapRequest.Hostinfo.Hostname).
// Interface("payload", resp). // Interface("payload", resp).
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 mapRequest.Compress == "zstd" {
src, err := json.Marshal(resp) src, err := json.Marshal(resp)
if err != nil { if err != nil {
log.Error(). log.Error().
@@ -356,7 +402,8 @@ func (h *Headscale) getMapKeepAliveResponse(
} }
func (h *Headscale) handleMachineLogOut( func (h *Headscale) handleMachineLogOut(
ctx *gin.Context, writer http.ResponseWriter,
req *http.Request,
machineKey key.MachinePublic, machineKey key.MachinePublic,
machine Machine, machine Machine,
) { ) {
@@ -366,7 +413,17 @@ func (h *Headscale) handleMachineLogOut(
Str("machine", machine.Hostname). Str("machine", machine.Hostname).
Msg("Client requested logout") Msg("Client requested logout")
h.ExpireMachine(&machine) err := h.ExpireMachine(&machine)
if err != nil {
log.Error().
Caller().
Str("func", "handleMachineLogOut").
Err(err).
Msg("Failed to expire machine")
http.Error(writer, "Internal server error", http.StatusInternalServerError)
return
}
resp.AuthURL = "" resp.AuthURL = ""
resp.MachineAuthorized = false resp.MachineAuthorized = false
@@ -377,15 +434,25 @@ func (h *Headscale) handleMachineLogOut(
Caller(). Caller().
Err(err). Err(err).
Msg("Cannot encode message") Msg("Cannot encode message")
ctx.String(http.StatusInternalServerError, "") http.Error(writer, "Internal server error", http.StatusInternalServerError)
return return
} }
ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, err = writer.Write(respBody)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
} }
func (h *Headscale) handleMachineValidRegistration( func (h *Headscale) handleMachineValidRegistration(
ctx *gin.Context, writer http.ResponseWriter,
req *http.Request,
machineKey key.MachinePublic, machineKey key.MachinePublic,
machine Machine, machine Machine,
) { ) {
@@ -409,17 +476,27 @@ func (h *Headscale) handleMachineValidRegistration(
Msg("Cannot encode message") Msg("Cannot encode message")
machineRegistrations.WithLabelValues("update", "web", "error", machine.Namespace.Name). machineRegistrations.WithLabelValues("update", "web", "error", machine.Namespace.Name).
Inc() Inc()
ctx.String(http.StatusInternalServerError, "") http.Error(writer, "Internal server error", http.StatusInternalServerError)
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)
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, err = writer.Write(respBody)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
} }
func (h *Headscale) handleMachineExpired( func (h *Headscale) handleMachineExpired(
ctx *gin.Context, writer http.ResponseWriter,
req *http.Request,
machineKey key.MachinePublic, machineKey key.MachinePublic,
registerRequest tailcfg.RegisterRequest, registerRequest tailcfg.RegisterRequest,
machine Machine, machine Machine,
@@ -432,7 +509,7 @@ func (h *Headscale) handleMachineExpired(
Msg("Machine registration has expired. Sending a authurl to register") Msg("Machine registration has expired. Sending a authurl to register")
if registerRequest.Auth.AuthKey != "" { if registerRequest.Auth.AuthKey != "" {
h.handleAuthKey(ctx, machineKey, registerRequest) h.handleAuthKey(writer, req, machineKey, registerRequest)
return return
} }
@@ -453,17 +530,27 @@ func (h *Headscale) handleMachineExpired(
Msg("Cannot encode message") Msg("Cannot encode message")
machineRegistrations.WithLabelValues("reauth", "web", "error", machine.Namespace.Name). machineRegistrations.WithLabelValues("reauth", "web", "error", machine.Namespace.Name).
Inc() Inc()
ctx.String(http.StatusInternalServerError, "") http.Error(writer, "Internal server error", http.StatusInternalServerError)
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)
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, err = writer.Write(respBody)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
} }
func (h *Headscale) handleMachineRefreshKey( func (h *Headscale) handleMachineRefreshKey(
ctx *gin.Context, writer http.ResponseWriter,
req *http.Request,
machineKey key.MachinePublic, machineKey key.MachinePublic,
registerRequest tailcfg.RegisterRequest, registerRequest tailcfg.RegisterRequest,
machine Machine, machine Machine,
@@ -480,7 +567,7 @@ func (h *Headscale) handleMachineRefreshKey(
Caller(). Caller().
Err(err). Err(err).
Msg("Failed to update machine key in the database") Msg("Failed to update machine key in the database")
ctx.String(http.StatusInternalServerError, "Internal server error") http.Error(writer, "Internal server error", http.StatusInternalServerError)
return return
} }
@@ -493,15 +580,25 @@ func (h *Headscale) handleMachineRefreshKey(
Caller(). Caller().
Err(err). Err(err).
Msg("Cannot encode message") Msg("Cannot encode message")
ctx.String(http.StatusInternalServerError, "Internal server error") http.Error(writer, "Internal server error", http.StatusInternalServerError)
return return
} }
ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, err = writer.Write(respBody)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
} }
func (h *Headscale) handleMachineRegistrationNew( func (h *Headscale) handleMachineRegistrationNew(
ctx *gin.Context, writer http.ResponseWriter,
req *http.Request,
machineKey key.MachinePublic, machineKey key.MachinePublic,
registerRequest tailcfg.RegisterRequest, registerRequest tailcfg.RegisterRequest,
) { ) {
@@ -528,16 +625,26 @@ func (h *Headscale) handleMachineRegistrationNew(
Caller(). Caller().
Err(err). Err(err).
Msg("Cannot encode message") Msg("Cannot encode message")
ctx.String(http.StatusInternalServerError, "") http.Error(writer, "Internal server error", http.StatusInternalServerError)
return return
} }
ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, err = writer.Write(respBody)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
} }
// TODO: check if any locks are needed around IP allocation. // TODO: check if any locks are needed around IP allocation.
func (h *Headscale) handleAuthKey( func (h *Headscale) handleAuthKey(
ctx *gin.Context, writer http.ResponseWriter,
req *http.Request,
machineKey key.MachinePublic, machineKey key.MachinePublic,
registerRequest tailcfg.RegisterRequest, registerRequest tailcfg.RegisterRequest,
) { ) {
@@ -566,14 +673,23 @@ func (h *Headscale) handleAuthKey(
Str("machine", registerRequest.Hostinfo.Hostname). Str("machine", registerRequest.Hostinfo.Hostname).
Err(err). Err(err).
Msg("Cannot encode message") Msg("Cannot encode message")
ctx.String(http.StatusInternalServerError, "") http.Error(writer, "Internal server error", http.StatusInternalServerError)
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name). machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
Inc() Inc()
return return
} }
ctx.Data(http.StatusUnauthorized, "application/json; charset=utf-8", respBody) writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusUnauthorized)
_, err = writer.Write(respBody)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
log.Error(). log.Error().
Caller(). Caller().
Str("func", "handleAuthKey"). Str("func", "handleAuthKey").
@@ -610,7 +726,16 @@ func (h *Headscale) handleAuthKey(
machine.NodeKey = nodeKey machine.NodeKey = nodeKey
machine.AuthKeyID = uint(pak.ID) machine.AuthKeyID = uint(pak.ID)
h.RefreshMachine(machine, registerRequest.Expiry) err := h.RefreshMachine(machine, registerRequest.Expiry)
if err != nil {
log.Error().
Caller().
Str("machine", machine.Hostname).
Err(err).
Msg("Failed to refresh machine")
return
}
} else { } else {
now := time.Now().UTC() now := time.Now().UTC()
@@ -647,16 +772,24 @@ func (h *Headscale) handleAuthKey(
Msg("could not register machine") Msg("could not register machine")
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name). machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
Inc() Inc()
ctx.String( http.Error(writer, "Internal server error", http.StatusInternalServerError)
http.StatusInternalServerError,
"could not register machine",
)
return return
} }
} }
h.UsePreAuthKey(pak) err = h.UsePreAuthKey(pak)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to use pre-auth key")
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
Inc()
http.Error(writer, "Internal server error", http.StatusInternalServerError)
return
}
resp.MachineAuthorized = true resp.MachineAuthorized = true
resp.User = *pak.Namespace.toUser() resp.User = *pak.Namespace.toUser()
@@ -670,13 +803,22 @@ func (h *Headscale) handleAuthKey(
Msg("Cannot encode message") Msg("Cannot encode message")
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name). machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
Inc() Inc()
ctx.String(http.StatusInternalServerError, "Extremely sad!") http.Error(writer, "Internal server error", http.StatusInternalServerError)
return return
} }
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "success", pak.Namespace.Name). machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "success", pak.Namespace.Name).
Inc() Inc()
ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody) writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, err = writer.Write(respBody)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
log.Info(). log.Info().
Str("func", "handleAuthKey"). Str("func", "handleAuthKey").
Str("machine", registerRequest.Hostinfo.Hostname). Str("machine", registerRequest.Hostinfo.Hostname).

View File

@@ -13,7 +13,6 @@ import (
const ( const (
apiPrefixLength = 7 apiPrefixLength = 7
apiKeyLength = 32 apiKeyLength = 32
apiKeyParts = 2
errAPIKeyFailedToParse = Error("Failed to parse ApiKey") errAPIKeyFailedToParse = Error("Failed to parse ApiKey")
) )
@@ -115,9 +114,9 @@ func (h *Headscale) ExpireAPIKey(key *APIKey) error {
} }
func (h *Headscale) ValidateAPIKey(keyStr string) (bool, error) { func (h *Headscale) ValidateAPIKey(keyStr string) (bool, error) {
prefix, hash, err := splitAPIKey(keyStr) prefix, hash, found := strings.Cut(keyStr, ".")
if err != nil { if !found {
return false, fmt.Errorf("failed to validate api key: %w", err) return false, errAPIKeyFailedToParse
} }
key, err := h.GetAPIKey(prefix) key, err := h.GetAPIKey(prefix)
@@ -136,15 +135,6 @@ func (h *Headscale) ValidateAPIKey(keyStr string) (bool, error) {
return true, nil return true, nil
} }
func splitAPIKey(key string) (string, string, error) {
parts := strings.Split(key, ".")
if len(parts) != apiKeyParts {
return "", "", errAPIKeyFailedToParse
}
return parts[0], parts[1], nil
}
func (key *APIKey) toProto() *v1.ApiKey { func (key *APIKey) toProto() *v1.ApiKey {
protoKey := v1.ApiKey{ protoKey := v1.ApiKey{
Id: key.ID, Id: key.ID,

230
app.go
View File

@@ -18,6 +18,7 @@ import (
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gorilla/mux"
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1" v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
@@ -54,12 +55,13 @@ const (
) )
const ( const (
AuthPrefix = "Bearer " AuthPrefix = "Bearer "
Postgres = "postgres" Postgres = "postgres"
Sqlite = "sqlite3" Sqlite = "sqlite3"
updateInterval = 5000 updateInterval = 5000
HTTPReadTimeout = 30 * time.Second HTTPReadTimeout = 30 * time.Second
privateKeyFileMode = 0o600 HTTPShutdownTimeout = 3 * time.Second
privateKeyFileMode = 0o600
registerCacheExpiration = time.Minute * 15 registerCacheExpiration = time.Minute * 15
registerCacheCleanup = time.Minute * 20 registerCacheCleanup = time.Minute * 20
@@ -92,6 +94,8 @@ type Headscale struct {
registrationCache *cache.Cache registrationCache *cache.Cache
ipAllocationMutex sync.Mutex ipAllocationMutex sync.Mutex
shutdownChan chan struct{}
} }
// Look up the TLS constant relative to user-supplied TLS client // Look up the TLS constant relative to user-supplied TLS client
@@ -168,7 +172,7 @@ func NewHeadscale(cfg *Config) (*Headscale, error) {
magicDNSDomains := generateMagicDNSRootDomains(app.cfg.IPPrefixes) magicDNSDomains := generateMagicDNSRootDomains(app.cfg.IPPrefixes)
// we might have routes already from Split DNS // we might have routes already from Split DNS
if app.cfg.DNSConfig.Routes == nil { if app.cfg.DNSConfig.Routes == nil {
app.cfg.DNSConfig.Routes = make(map[string][]dnstype.Resolver) app.cfg.DNSConfig.Routes = make(map[string][]*dnstype.Resolver)
} }
for _, d := range magicDNSDomains { for _, d := range magicDNSDomains {
app.cfg.DNSConfig.Routes[d.WithoutTrailingDot()] = nil app.cfg.DNSConfig.Routes[d.WithoutTrailingDot()] = nil
@@ -326,48 +330,74 @@ func (h *Headscale) grpcAuthenticationInterceptor(ctx context.Context,
return handler(ctx, req) return handler(ctx, req)
} }
func (h *Headscale) httpAuthenticationMiddleware(ctx *gin.Context) { func (h *Headscale) httpAuthenticationMiddleware(next http.Handler) http.Handler {
log.Trace(). return http.HandlerFunc(func(
Caller(). writer http.ResponseWriter,
Str("client_address", ctx.ClientIP()). req *http.Request,
Msg("HTTP authentication invoked") ) {
log.Trace().
authHeader := ctx.GetHeader("authorization")
if !strings.HasPrefix(authHeader, AuthPrefix) {
log.Error().
Caller(). Caller().
Str("client_address", ctx.ClientIP()). Str("client_address", req.RemoteAddr).
Msg(`missing "Bearer " prefix in "Authorization" header`) Msg("HTTP authentication invoked")
ctx.AbortWithStatus(http.StatusUnauthorized)
return authHeader := req.Header.Get("authorization")
}
valid, err := h.ValidateAPIKey(strings.TrimPrefix(authHeader, AuthPrefix)) if !strings.HasPrefix(authHeader, AuthPrefix) {
if err != nil { log.Error().
log.Error(). Caller().
Caller(). Str("client_address", req.RemoteAddr).
Err(err). Msg(`missing "Bearer " prefix in "Authorization" header`)
Str("client_address", ctx.ClientIP()). writer.WriteHeader(http.StatusUnauthorized)
Msg("failed to validate token") _, err := writer.Write([]byte("Unauthorized"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
ctx.AbortWithStatus(http.StatusInternalServerError) return
}
return valid, err := h.ValidateAPIKey(strings.TrimPrefix(authHeader, AuthPrefix))
} if err != nil {
log.Error().
Caller().
Err(err).
Str("client_address", req.RemoteAddr).
Msg("failed to validate token")
if !valid { writer.WriteHeader(http.StatusInternalServerError)
log.Info(). _, err := writer.Write([]byte("Unauthorized"))
Str("client_address", ctx.ClientIP()). if err != nil {
Msg("invalid token") log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
ctx.AbortWithStatus(http.StatusUnauthorized) return
}
return if !valid {
} log.Info().
Str("client_address", req.RemoteAddr).
Msg("invalid token")
ctx.Next() writer.WriteHeader(http.StatusUnauthorized)
_, err := writer.Write([]byte("Unauthorized"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return
}
next.ServeHTTP(writer, req)
})
} }
// ensureUnixSocketIsAbsent will check if the given path for headscales unix socket is clear // ensureUnixSocketIsAbsent will check if the given path for headscales unix socket is clear
@@ -390,39 +420,48 @@ func (h *Headscale) createPrometheusRouter() *gin.Engine {
return promRouter return promRouter
} }
func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *gin.Engine { func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *mux.Router {
router := gin.Default() router := mux.NewRouter()
router.GET( router.HandleFunc(
"/health", "/health",
func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"healthy": "ok"}) }, func(writer http.ResponseWriter, req *http.Request) {
) writer.WriteHeader(http.StatusOK)
router.GET("/key", h.KeyHandler) _, err := writer.Write([]byte("{\"healthy\": \"ok\"}"))
router.GET("/register", h.RegisterWebAPI) if err != nil {
router.POST("/machine/:id/map", h.PollNetMapHandler) log.Error().
router.POST("/machine/:id", h.RegistrationHandler) Caller().
router.GET("/oidc/register/:mkey", h.RegisterOIDC) Err(err).
router.GET("/oidc/callback", h.OIDCCallback) Msg("Failed to write response")
router.GET("/apple", h.AppleConfigMessage) }
router.GET("/apple/:platform", h.ApplePlatformConfig) }).Methods(http.MethodGet)
router.GET("/windows", h.WindowsConfigMessage)
router.GET("/windows/tailscale.reg", h.WindowsRegConfig) router.HandleFunc("/key", h.KeyHandler).Methods(http.MethodGet)
router.GET("/swagger", SwaggerUI) router.HandleFunc("/register", h.RegisterWebAPI).Methods(http.MethodGet)
router.GET("/swagger/v1/openapiv2.json", SwaggerAPIv1) router.HandleFunc("/machine/{mkey}/map", h.PollNetMapHandler).Methods(http.MethodPost)
router.HandleFunc("/machine/{mkey}", h.RegistrationHandler).Methods(http.MethodPost)
router.HandleFunc("/oidc/register/{mkey}", h.RegisterOIDC).Methods(http.MethodGet)
router.HandleFunc("/oidc/callback", h.OIDCCallback).Methods(http.MethodGet)
router.HandleFunc("/apple", h.AppleConfigMessage).Methods(http.MethodGet)
router.HandleFunc("/apple/{platform}", h.ApplePlatformConfig).Methods(http.MethodGet)
router.HandleFunc("/windows", h.WindowsConfigMessage).Methods(http.MethodGet)
router.HandleFunc("/windows/tailscale.reg", h.WindowsRegConfig).Methods(http.MethodGet)
router.HandleFunc("/swagger", SwaggerUI).Methods(http.MethodGet)
router.HandleFunc("/swagger/v1/openapiv2.json", SwaggerAPIv1).Methods(http.MethodGet)
if h.cfg.DERP.ServerEnabled { if h.cfg.DERP.ServerEnabled {
router.Any("/derp", h.DERPHandler) router.HandleFunc("/derp", h.DERPHandler)
router.Any("/derp/probe", h.DERPProbeHandler) router.HandleFunc("/derp/probe", h.DERPProbeHandler)
router.Any("/bootstrap-dns", h.DERPBootstrapDNSHandler) router.HandleFunc("/bootstrap-dns", h.DERPBootstrapDNSHandler)
} }
api := router.Group("/api") api := router.PathPrefix("/api").Subrouter()
api.Use(h.httpAuthenticationMiddleware) api.Use(h.httpAuthenticationMiddleware)
{ {
api.Any("/v1/*any", gin.WrapF(grpcMux.ServeHTTP)) api.HandleFunc("/v1/*any", grpcMux.ServeHTTP)
} }
router.NoRoute(stdoutHandler) router.PathPrefix("/").HandlerFunc(stdoutHandler)
return router return router
} }
@@ -630,6 +669,7 @@ func (h *Headscale) Serve() error {
Msgf("listening and serving metrics on: %s", h.cfg.MetricsAddr) Msgf("listening and serving metrics on: %s", h.cfg.MetricsAddr)
// Handle common process-killing signals so we can gracefully shut down: // Handle common process-killing signals so we can gracefully shut down:
h.shutdownChan = make(chan struct{})
sigc := make(chan os.Signal, 1) sigc := make(chan os.Signal, 1)
signal.Notify(sigc, signal.Notify(sigc,
syscall.SIGHUP, syscall.SIGHUP,
@@ -657,7 +697,9 @@ func (h *Headscale) Serve() error {
} }
log.Info(). log.Info().
Str("path", aclPath). Str("path", aclPath).
Msg("ACL policy successfully reloaded") Msg("ACL policy successfully reloaded, notifying nodes of change")
h.setLastStateChangeToNow()
} }
default: default:
@@ -665,9 +707,16 @@ func (h *Headscale) Serve() error {
Str("signal", sig.String()). Str("signal", sig.String()).
Msg("Received signal to stop, shutting down gracefully") Msg("Received signal to stop, shutting down gracefully")
h.shutdownChan <- struct{}{}
// Gracefully shut down servers // Gracefully shut down servers
promHTTPServer.Shutdown(ctx) ctx, cancel := context.WithTimeout(context.Background(), HTTPShutdownTimeout)
httpServer.Shutdown(ctx) if err := promHTTPServer.Shutdown(ctx); err != nil {
log.Error().Err(err).Msg("Failed to shutdown prometheus http")
}
if err := httpServer.Shutdown(ctx); err != nil {
log.Error().Err(err).Msg("Failed to shutdown http")
}
grpcSocket.GracefulStop() grpcSocket.GracefulStop()
// Close network listeners // Close network listeners
@@ -678,7 +727,21 @@ func (h *Headscale) Serve() error {
// Stop listening (and unlink the socket if unix type): // Stop listening (and unlink the socket if unix type):
socketListener.Close() socketListener.Close()
// Close db connections
db, err := h.db.DB()
if err != nil {
log.Error().Err(err).Msg("Failed to get db handle")
}
err = db.Close()
if err != nil {
log.Error().Err(err).Msg("Failed to close db")
}
log.Info().
Msg("Headscale stopped")
// And we're done: // And we're done:
cancel()
os.Exit(0) os.Exit(0)
} }
} }
@@ -756,13 +819,25 @@ func (h *Headscale) getTLSSettings() (*tls.Config, error) {
} }
} }
func (h *Headscale) setLastStateChangeToNow(namespace string) { func (h *Headscale) setLastStateChangeToNow(namespaces ...string) {
var err error
now := time.Now().UTC() now := time.Now().UTC()
lastStateUpdate.WithLabelValues("", "headscale").Set(float64(now.Unix()))
if h.lastStateChange == nil { if len(namespaces) == 0 {
h.lastStateChange = xsync.NewMapOf[time.Time]() namespaces, err = h.ListNamespacesStr()
if err != nil {
log.Error().Caller().Err(err).Msg("failed to fetch all namespaces, failing to update last changed state.")
}
}
for _, namespace := range namespaces {
lastStateUpdate.WithLabelValues(namespace, "headscale").Set(float64(now.Unix()))
if h.lastStateChange == nil {
h.lastStateChange = xsync.NewMapOf[time.Time]()
}
h.lastStateChange.Store(namespace, now)
} }
h.lastStateChange.Store(namespace, now)
} }
func (h *Headscale) getLastStateChange(namespaces ...string) time.Time { func (h *Headscale) getLastStateChange(namespaces ...string) time.Time {
@@ -797,13 +872,16 @@ func (h *Headscale) getLastStateChange(namespaces ...string) time.Time {
} }
} }
func stdoutHandler(ctx *gin.Context) { func stdoutHandler(
body, _ := io.ReadAll(ctx.Request.Body) writer http.ResponseWriter,
req *http.Request,
) {
body, _ := io.ReadAll(req.Body)
log.Trace(). log.Trace().
Interface("header", ctx.Request.Header). Interface("header", req.Header).
Interface("proto", ctx.Request.Proto). Interface("proto", req.Proto).
Interface("url", ctx.Request.URL). Interface("url", req.URL).
Bytes("body", body). Bytes("body", body).
Msg("Request did not match") Msg("Request did not match")
} }

View File

@@ -0,0 +1,28 @@
package cli
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func init() {
rootCmd.AddCommand(dumpConfigCmd)
}
var dumpConfigCmd = &cobra.Command{
Use: "dumpConfig",
Short: "dump current config to /etc/headscale/config.dump.yaml, integration test only",
Hidden: true,
Args: func(cmd *cobra.Command, args []string) error {
return nil
},
Run: func(cmd *cobra.Command, args []string) {
err := viper.WriteConfigAs("/etc/headscale/config.dump.yaml")
if err != nil {
//nolint
fmt.Println("Failed to dump config")
}
},
}

View File

@@ -3,17 +3,75 @@ package cli
import ( import (
"fmt" "fmt"
"os" "os"
"runtime"
"github.com/juanfont/headscale"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/tcnksm/go-latest"
) )
var cfgFile string = ""
func init() { func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().
StringVarP(&cfgFile, "config", "c", "", "config file (default is /etc/headscale/config.yaml)")
rootCmd.PersistentFlags(). rootCmd.PersistentFlags().
StringP("output", "o", "", "Output format. Empty for human-readable, 'json', 'json-line' or 'yaml'") StringP("output", "o", "", "Output format. Empty for human-readable, 'json', 'json-line' or 'yaml'")
rootCmd.PersistentFlags(). rootCmd.PersistentFlags().
Bool("force", false, "Disable prompts and forces the execution") Bool("force", false, "Disable prompts and forces the execution")
} }
func initConfig() {
if cfgFile != "" {
err := headscale.LoadConfig(cfgFile, true)
if err != nil {
log.Fatal().Caller().Err(err)
}
} else {
err := headscale.LoadConfig("", false)
if err != nil {
log.Fatal().Caller().Err(err)
}
}
cfg, err := headscale.GetHeadscaleConfig()
if err != nil {
log.Fatal().Caller().Err(err)
}
machineOutput := HasMachineOutputFlag()
zerolog.SetGlobalLevel(cfg.LogLevel)
// If the user has requested a "machine" readable format,
// then disable login so the output remains valid.
if machineOutput {
zerolog.SetGlobalLevel(zerolog.Disabled)
}
if !cfg.DisableUpdateCheck && !machineOutput {
if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") &&
Version != "dev" {
githubTag := &latest.GithubTag{
Owner: "juanfont",
Repository: "headscale",
}
res, err := latest.Check(githubTag, Version)
if err == nil && res.Outdated {
//nolint
fmt.Printf(
"An updated version of Headscale has been found (%s vs. your current %s). Check it out https://github.com/juanfont/headscale/releases\n",
res.Current,
Version,
)
}
}
}
}
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
Use: "headscale", Use: "headscale",
Short: "headscale - a Tailscale control server", Short: "headscale - a Tailscale control server",

View File

@@ -7,12 +7,10 @@ import (
"fmt" "fmt"
"os" "os"
"reflect" "reflect"
"time"
"github.com/juanfont/headscale" "github.com/juanfont/headscale"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1" v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/spf13/viper"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/credentials/insecure"
@@ -29,21 +27,6 @@ func getHeadscaleApp() (*headscale.Headscale, error) {
return nil, fmt.Errorf("failed to load configuration while creating headscale instance: %w", err) return nil, fmt.Errorf("failed to load configuration while creating headscale instance: %w", err)
} }
// Minimum inactivity time out is keepalive timeout (60s) plus a few seconds
// to avoid races
minInactivityTimeout, _ := time.ParseDuration("65s")
if viper.GetDuration("ephemeral_node_inactivity_timeout") <= minInactivityTimeout {
// TODO: Find a better way to return this text
//nolint
err := fmt.Errorf(
"ephemeral_node_inactivity_timeout (%s) is set too low, must be more than %s",
viper.GetString("ephemeral_node_inactivity_timeout"),
minInactivityTimeout,
)
return nil, err
}
app, err := headscale.NewHeadscale(cfg) app, err := headscale.NewHeadscale(cfg)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -72,6 +55,7 @@ func getHeadscaleCLIClient() (context.Context, v1.HeadscaleServiceClient, *grpc.
Err(err). Err(err).
Caller(). Caller().
Msgf("Failed to load configuration") Msgf("Failed to load configuration")
os.Exit(-1) // we get here if logging is suppressed (i.e., json output)
} }
log.Debug(). log.Debug().
@@ -133,6 +117,7 @@ func getHeadscaleCLIClient() (context.Context, v1.HeadscaleServiceClient, *grpc.
conn, err := grpc.DialContext(ctx, address, grpcOptions...) conn, err := grpc.DialContext(ctx, address, grpcOptions...)
if err != nil { if err != nil {
log.Fatal().Caller().Err(err).Msgf("Could not connect: %v", err) log.Fatal().Caller().Err(err).Msgf("Could not connect: %v", err)
os.Exit(-1) // we get here if logging is suppressed (i.e., json output)
} }
client := v1.NewHeadscaleServiceClient(conn) client := v1.NewHeadscaleServiceClient(conn)

View File

@@ -1,17 +1,13 @@
package main package main
import ( import (
"fmt"
"os" "os"
"runtime"
"time" "time"
"github.com/efekarakus/termcolor" "github.com/efekarakus/termcolor"
"github.com/juanfont/headscale"
"github.com/juanfont/headscale/cmd/headscale/cli" "github.com/juanfont/headscale/cmd/headscale/cli"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/tcnksm/go-latest"
) )
func main() { func main() {
@@ -43,39 +39,5 @@ func main() {
NoColor: !colors, NoColor: !colors,
}) })
cfg, err := headscale.GetHeadscaleConfig()
if err != nil {
log.Fatal().Caller().Err(err)
}
machineOutput := cli.HasMachineOutputFlag()
zerolog.SetGlobalLevel(cfg.LogLevel)
// If the user has requested a "machine" readable format,
// then disable login so the output remains valid.
if machineOutput {
zerolog.SetGlobalLevel(zerolog.Disabled)
}
if !cfg.DisableUpdateCheck && !machineOutput {
if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") &&
cli.Version != "dev" {
githubTag := &latest.GithubTag{
Owner: "juanfont",
Repository: "headscale",
}
res, err := latest.Check(githubTag, cli.Version)
if err == nil && res.Outdated {
//nolint
fmt.Printf(
"An updated version of Headscale has been found (%s vs. your current %s). Check it out https://github.com/juanfont/headscale/releases\n",
res.Current,
cli.Version,
)
}
}
}
cli.Execute() cli.Execute()
} }

View File

@@ -27,6 +27,51 @@ func (s *Suite) SetUpSuite(c *check.C) {
func (s *Suite) TearDownSuite(c *check.C) { func (s *Suite) TearDownSuite(c *check.C) {
} }
func (*Suite) TestConfigFileLoading(c *check.C) {
tmpDir, err := ioutil.TempDir("", "headscale")
if err != nil {
c.Fatal(err)
}
defer os.RemoveAll(tmpDir)
path, err := os.Getwd()
if err != nil {
c.Fatal(err)
}
cfgFile := filepath.Join(tmpDir, "config.yaml")
// Symlink the example config file
err = os.Symlink(
filepath.Clean(path+"/../../config-example.yaml"),
cfgFile,
)
if err != nil {
c.Fatal(err)
}
// Load example config, it should load without validation errors
err = headscale.LoadConfig(cfgFile, true)
c.Assert(err, check.IsNil)
// Test that config file was interpreted correctly
c.Assert(viper.GetString("server_url"), check.Equals, "http://127.0.0.1:8080")
c.Assert(viper.GetString("listen_addr"), check.Equals, "0.0.0.0:8080")
c.Assert(viper.GetString("metrics_listen_addr"), check.Equals, "127.0.0.1:9090")
c.Assert(viper.GetString("db_type"), check.Equals, "sqlite3")
c.Assert(viper.GetString("db_path"), check.Equals, "/var/lib/headscale/db.sqlite")
c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "")
c.Assert(viper.GetString("tls_letsencrypt_listen"), check.Equals, ":http")
c.Assert(viper.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01")
c.Assert(viper.GetStringSlice("dns_config.nameservers")[0], check.Equals, "1.1.1.1")
c.Assert(
headscale.GetFileMode("unix_socket_permission"),
check.Equals,
fs.FileMode(0o770),
)
c.Assert(viper.GetBool("logtail.enabled"), check.Equals, false)
}
func (*Suite) TestConfigLoading(c *check.C) { func (*Suite) TestConfigLoading(c *check.C) {
tmpDir, err := ioutil.TempDir("", "headscale") tmpDir, err := ioutil.TempDir("", "headscale")
if err != nil { if err != nil {
@@ -49,7 +94,7 @@ func (*Suite) TestConfigLoading(c *check.C) {
} }
// Load example config, it should load without validation errors // Load example config, it should load without validation errors
err = headscale.LoadConfig(tmpDir) err = headscale.LoadConfig(tmpDir, false)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
// Test that config file was interpreted correctly // Test that config file was interpreted correctly
@@ -68,6 +113,7 @@ func (*Suite) TestConfigLoading(c *check.C) {
fs.FileMode(0o770), fs.FileMode(0o770),
) )
c.Assert(viper.GetBool("logtail.enabled"), check.Equals, false) c.Assert(viper.GetBool("logtail.enabled"), check.Equals, false)
c.Assert(viper.GetBool("randomize_client_port"), check.Equals, false)
} }
func (*Suite) TestDNSConfigLoading(c *check.C) { func (*Suite) TestDNSConfigLoading(c *check.C) {
@@ -92,7 +138,7 @@ func (*Suite) TestDNSConfigLoading(c *check.C) {
} }
// Load example config, it should load without validation errors // Load example config, it should load without validation errors
err = headscale.LoadConfig(tmpDir) err = headscale.LoadConfig(tmpDir, false)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
dnsConfig, baseDomain := headscale.GetDNSConfig() dnsConfig, baseDomain := headscale.GetDNSConfig()
@@ -125,7 +171,7 @@ func (*Suite) TestTLSConfigValidation(c *check.C) {
writeConfig(c, tmpDir, configYaml) writeConfig(c, tmpDir, configYaml)
// Check configuration validation errors (1) // Check configuration validation errors (1)
err = headscale.LoadConfig(tmpDir) err = headscale.LoadConfig(tmpDir, false)
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
// check.Matches can not handle multiline strings // check.Matches can not handle multiline strings
tmp := strings.ReplaceAll(err.Error(), "\n", "***") tmp := strings.ReplaceAll(err.Error(), "\n", "***")
@@ -150,6 +196,6 @@ func (*Suite) TestTLSConfigValidation(c *check.C) {
"---\nserver_url: \"http://127.0.0.1:8080\"\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"TLS-ALPN-01\"", "---\nserver_url: \"http://127.0.0.1:8080\"\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"TLS-ALPN-01\"",
) )
writeConfig(c, tmpDir, configYaml) writeConfig(c, tmpDir, configYaml)
err = headscale.LoadConfig(tmpDir) err = headscale.LoadConfig(tmpDir, false)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
} }

View File

@@ -103,6 +103,12 @@ disable_check_updates: false
# Time before an inactive ephemeral node is deleted? # Time before an inactive ephemeral node is deleted?
ephemeral_node_inactivity_timeout: 30m ephemeral_node_inactivity_timeout: 30m
# Period to check for node updates in the tailnet. A value too low will severily affect
# CPU consumption of Headscale. A value too high (over 60s) will cause problems
# to the nodes, as they won't get updates or keep alive messages in time.
# In case of doubts, do not touch the default 10s.
node_update_check_interval: 10s
# SQLite config # SQLite config
db_type: sqlite3 db_type: sqlite3
db_path: /var/lib/headscale/db.sqlite db_path: /var/lib/headscale/db.sqlite
@@ -244,3 +250,8 @@ logtail:
# As there is currently no support for overriding the log server in headscale, this is # As there is currently no support for overriding the log server in headscale, this is
# disabled by default. Enabling this will make your clients send logs to Tailscale Inc. # disabled by default. Enabling this will make your clients send logs to Tailscale Inc.
enabled: false enabled: false
# Enabling this option makes devices prefer a random port for WireGuard traffic over the
# default static port 41641. This option is intended as a workaround for some buggy
# firewall devices. See https://tailscale.com/kb/1181/firewalls/ for more information.
randomize_client_port: false

View File

@@ -26,6 +26,7 @@ type Config struct {
GRPCAddr string GRPCAddr string
GRPCAllowInsecure bool GRPCAllowInsecure bool
EphemeralNodeInactivityTimeout time.Duration EphemeralNodeInactivityTimeout time.Duration
NodeUpdateCheckInterval time.Duration
IPPrefixes []netaddr.IPPrefix IPPrefixes []netaddr.IPPrefix
PrivateKeyPath string PrivateKeyPath string
BaseDomain string BaseDomain string
@@ -54,7 +55,8 @@ type Config struct {
OIDC OIDCConfig OIDC OIDCConfig
LogTail LogTailConfig LogTail LogTailConfig
RandomizeClientPort bool
CLI CLIConfig CLI CLIConfig
@@ -114,15 +116,19 @@ type ACLConfig struct {
PolicyPath string PolicyPath string
} }
func LoadConfig(path string) error { func LoadConfig(path string, isFile bool) error {
viper.SetConfigName("config") if isFile {
if path == "" { viper.SetConfigFile(path)
viper.AddConfigPath("/etc/headscale/")
viper.AddConfigPath("$HOME/.headscale")
viper.AddConfigPath(".")
} else { } else {
// For testing viper.SetConfigName("config")
viper.AddConfigPath(path) if path == "" {
viper.AddConfigPath("/etc/headscale/")
viper.AddConfigPath("$HOME/.headscale")
viper.AddConfigPath(".")
} else {
// For testing
viper.AddConfigPath(path)
}
} }
viper.SetEnvPrefix("headscale") viper.SetEnvPrefix("headscale")
@@ -153,8 +159,15 @@ func LoadConfig(path string) error {
viper.SetDefault("oidc.strip_email_domain", true) viper.SetDefault("oidc.strip_email_domain", true)
viper.SetDefault("logtail.enabled", false) viper.SetDefault("logtail.enabled", false)
viper.SetDefault("randomize_client_port", false)
viper.SetDefault("ephemeral_node_inactivity_timeout", "120s")
viper.SetDefault("node_update_check_interval", "10s")
if err := viper.ReadInConfig(); err != nil { if err := viper.ReadInConfig(); err != nil {
log.Warn().Err(err).Msg("Failed to read configuration from disk")
return fmt.Errorf("fatal error reading config file: %w", err) return fmt.Errorf("fatal error reading config file: %w", err)
} }
@@ -196,6 +209,26 @@ func LoadConfig(path string) error {
EnforcedClientAuth) EnforcedClientAuth)
} }
// Minimum inactivity time out is keepalive timeout (60s) plus a few seconds
// to avoid races
minInactivityTimeout, _ := time.ParseDuration("65s")
if viper.GetDuration("ephemeral_node_inactivity_timeout") <= minInactivityTimeout {
errorText += fmt.Sprintf(
"Fatal config error: ephemeral_node_inactivity_timeout (%s) is set too low, must be more than %s",
viper.GetString("ephemeral_node_inactivity_timeout"),
minInactivityTimeout,
)
}
maxNodeUpdateCheckInterval, _ := time.ParseDuration("60s")
if viper.GetDuration("node_update_check_interval") > maxNodeUpdateCheckInterval {
errorText += fmt.Sprintf(
"Fatal config error: node_update_check_interval (%s) is set too high, must be less than %s",
viper.GetString("node_update_check_interval"),
maxNodeUpdateCheckInterval,
)
}
if errorText != "" { if errorText != "" {
//nolint //nolint
return errors.New(strings.TrimSuffix(errorText, "\n")) return errors.New(strings.TrimSuffix(errorText, "\n"))
@@ -297,7 +330,7 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) {
nameserversStr := viper.GetStringSlice("dns_config.nameservers") nameserversStr := viper.GetStringSlice("dns_config.nameservers")
nameservers := make([]netaddr.IP, len(nameserversStr)) nameservers := make([]netaddr.IP, len(nameserversStr))
resolvers := make([]dnstype.Resolver, len(nameserversStr)) resolvers := make([]*dnstype.Resolver, len(nameserversStr))
for index, nameserverStr := range nameserversStr { for index, nameserverStr := range nameserversStr {
nameserver, err := netaddr.ParseIP(nameserverStr) nameserver, err := netaddr.ParseIP(nameserverStr)
@@ -309,7 +342,7 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) {
} }
nameservers[index] = nameserver nameservers[index] = nameserver
resolvers[index] = dnstype.Resolver{ resolvers[index] = &dnstype.Resolver{
Addr: nameserver.String(), Addr: nameserver.String(),
} }
} }
@@ -320,13 +353,13 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) {
if viper.IsSet("dns_config.restricted_nameservers") { if viper.IsSet("dns_config.restricted_nameservers") {
if len(dnsConfig.Nameservers) > 0 { if len(dnsConfig.Nameservers) > 0 {
dnsConfig.Routes = make(map[string][]dnstype.Resolver) dnsConfig.Routes = make(map[string][]*dnstype.Resolver)
restrictedDNS := viper.GetStringMapStringSlice( restrictedDNS := viper.GetStringMapStringSlice(
"dns_config.restricted_nameservers", "dns_config.restricted_nameservers",
) )
for domain, restrictedNameservers := range restrictedDNS { for domain, restrictedNameservers := range restrictedDNS {
restrictedResolvers := make( restrictedResolvers := make(
[]dnstype.Resolver, []*dnstype.Resolver,
len(restrictedNameservers), len(restrictedNameservers),
) )
for index, nameserverStr := range restrictedNameservers { for index, nameserverStr := range restrictedNameservers {
@@ -337,7 +370,7 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) {
Err(err). Err(err).
Msgf("Could not parse restricted nameserver IP: %s", nameserverStr) Msgf("Could not parse restricted nameserver IP: %s", nameserverStr)
} }
restrictedResolvers[index] = dnstype.Resolver{ restrictedResolvers[index] = &dnstype.Resolver{
Addr: nameserver.String(), Addr: nameserver.String(),
} }
} }
@@ -377,14 +410,10 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) {
} }
func GetHeadscaleConfig() (*Config, error) { func GetHeadscaleConfig() (*Config, error) {
err := LoadConfig("")
if err != nil {
return nil, err
}
dnsConfig, baseDomain := GetDNSConfig() dnsConfig, baseDomain := GetDNSConfig()
derpConfig := GetDERPConfig() derpConfig := GetDERPConfig()
logConfig := GetLogTailConfig() logConfig := GetLogTailConfig()
randomizeClientPort := viper.GetBool("randomize_client_port")
configuredPrefixes := viper.GetStringSlice("ip_prefixes") configuredPrefixes := viper.GetStringSlice("ip_prefixes")
parsedPrefixes := make([]netaddr.IPPrefix, 0, len(configuredPrefixes)+1) parsedPrefixes := make([]netaddr.IPPrefix, 0, len(configuredPrefixes)+1)
@@ -461,6 +490,10 @@ func GetHeadscaleConfig() (*Config, error) {
"ephemeral_node_inactivity_timeout", "ephemeral_node_inactivity_timeout",
), ),
NodeUpdateCheckInterval: viper.GetDuration(
"node_update_check_interval",
),
DBtype: viper.GetString("db_type"), DBtype: viper.GetString("db_type"),
DBpath: AbsolutePathFromConfigPath(viper.GetString("db_path")), DBpath: AbsolutePathFromConfigPath(viper.GetString("db_path")),
DBhost: viper.GetString("db_host"), DBhost: viper.GetString("db_host"),
@@ -490,7 +523,8 @@ func GetHeadscaleConfig() (*Config, error) {
StripEmaildomain: viper.GetBool("oidc.strip_email_domain"), StripEmaildomain: viper.GetBool("oidc.strip_email_domain"),
}, },
LogTail: logConfig, LogTail: logConfig,
RandomizeClientPort: randomizeClientPort,
CLI: CLIConfig{ CLI: CLIConfig{
Address: viper.GetString("cli.address"), Address: viper.GetString("cli.address"),

5
db.go
View File

@@ -89,7 +89,7 @@ func (h *Headscale) initDB() error {
log.Error().Err(err).Msg("Error accessing db") log.Error().Err(err).Msg("Error accessing db")
} }
for _, machine := range machines { for item, machine := range machines {
if machine.GivenName == "" { if machine.GivenName == "" {
normalizedHostname, err := NormalizeToFQDNRules( normalizedHostname, err := NormalizeToFQDNRules(
machine.Hostname, machine.Hostname,
@@ -103,7 +103,7 @@ func (h *Headscale) initDB() error {
Msg("Failed to normalize machine hostname in DB migration") Msg("Failed to normalize machine hostname in DB migration")
} }
err = h.RenameMachine(&machine, normalizedHostname) err = h.RenameMachine(&machines[item], normalizedHostname)
if err != nil { if err != nil {
log.Error(). log.Error().
Caller(). Caller().
@@ -111,7 +111,6 @@ func (h *Headscale) initDB() error {
Err(err). Err(err).
Msg("Failed to save normalized machine name in DB migration") Msg("Failed to save normalized machine name in DB migration")
} }
} }
} }
} }

11
derp.go
View File

@@ -152,16 +152,7 @@ func (h *Headscale) scheduledDERPMapUpdateWorker(cancelChan <-chan struct{}) {
h.DERPMap.Regions[h.DERPServer.region.RegionID] = &h.DERPServer.region h.DERPMap.Regions[h.DERPServer.region.RegionID] = &h.DERPServer.region
} }
namespaces, err := h.ListNamespaces() h.setLastStateChangeToNow()
if err != nil {
log.Error().
Err(err).
Msg("Failed to fetch namespaces")
}
for _, namespace := range namespaces {
h.setLastStateChangeToNow(namespace.Name)
}
} }
} }
} }

View File

@@ -2,6 +2,7 @@ package headscale
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
@@ -10,7 +11,6 @@ import (
"strings" "strings"
"time" "time"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"tailscale.com/derp" "tailscale.com/derp"
"tailscale.com/net/stun" "tailscale.com/net/stun"
@@ -30,6 +30,7 @@ type DERPServer struct {
} }
func (h *Headscale) NewDERPServer() (*DERPServer, error) { func (h *Headscale) NewDERPServer() (*DERPServer, error) {
log.Trace().Caller().Msg("Creating new embedded DERP server")
server := derp.NewServer(key.NodePrivate(*h.privateKey), log.Info().Msgf) server := derp.NewServer(key.NodePrivate(*h.privateKey), log.Info().Msgf)
region, err := h.generateRegionLocalDERP() region, err := h.generateRegionLocalDERP()
if err != nil { if err != nil {
@@ -87,30 +88,48 @@ func (h *Headscale) generateRegionLocalDERP() (tailcfg.DERPRegion, error) {
} }
localDERPregion.Nodes[0].STUNPort = portSTUN localDERPregion.Nodes[0].STUNPort = portSTUN
log.Info().Caller().Msgf("DERP region: %+v", localDERPregion)
return localDERPregion, nil return localDERPregion, nil
} }
func (h *Headscale) DERPHandler(ctx *gin.Context) { func (h *Headscale) DERPHandler(
log.Trace().Caller().Msgf("/derp request from %v", ctx.ClientIP()) writer http.ResponseWriter,
up := strings.ToLower(ctx.Request.Header.Get("Upgrade")) req *http.Request,
) {
log.Trace().Caller().Msgf("/derp request from %v", req.RemoteAddr)
up := strings.ToLower(req.Header.Get("Upgrade"))
if up != "websocket" && up != "derp" { if up != "websocket" && up != "derp" {
if up != "" { if up != "" {
log.Warn().Caller().Msgf("Weird websockets connection upgrade: %q", up) log.Warn().Caller().Msgf("Weird websockets connection upgrade: %q", up)
} }
ctx.String(http.StatusUpgradeRequired, "DERP requires connection upgrade") writer.Header().Set("Content-Type", "text/plain")
writer.WriteHeader(http.StatusUpgradeRequired)
_, err := writer.Write([]byte("DERP requires connection upgrade"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return return
} }
fastStart := ctx.Request.Header.Get(fastStartHeader) == "1" fastStart := req.Header.Get(fastStartHeader) == "1"
hijacker, ok := ctx.Writer.(http.Hijacker) hijacker, ok := writer.(http.Hijacker)
if !ok { if !ok {
log.Error().Caller().Msg("DERP requires Hijacker interface from Gin") log.Error().Caller().Msg("DERP requires Hijacker interface from Gin")
ctx.String( writer.Header().Set("Content-Type", "text/plain")
http.StatusInternalServerError, writer.WriteHeader(http.StatusInternalServerError)
"HTTP does not support general TCP support", _, err := writer.Write([]byte("HTTP does not support general TCP support"))
) if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return return
} }
@@ -118,13 +137,19 @@ func (h *Headscale) DERPHandler(ctx *gin.Context) {
netConn, conn, err := hijacker.Hijack() netConn, conn, err := hijacker.Hijack()
if err != nil { if err != nil {
log.Error().Caller().Err(err).Msgf("Hijack failed") log.Error().Caller().Err(err).Msgf("Hijack failed")
ctx.String( writer.Header().Set("Content-Type", "text/plain")
http.StatusInternalServerError, writer.WriteHeader(http.StatusInternalServerError)
"HTTP does not support general TCP support", _, err = writer.Write([]byte("HTTP does not support general TCP support"))
) if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return return
} }
log.Trace().Caller().Msgf("Hijacked connection from %v", req.RemoteAddr)
if !fastStart { if !fastStart {
pubKey := h.privateKey.Public() pubKey := h.privateKey.Public()
@@ -143,12 +168,23 @@ func (h *Headscale) DERPHandler(ctx *gin.Context) {
// DERPProbeHandler is the endpoint that js/wasm clients hit to measure // DERPProbeHandler is the endpoint that js/wasm clients hit to measure
// DERP latency, since they can't do UDP STUN queries. // DERP latency, since they can't do UDP STUN queries.
func (h *Headscale) DERPProbeHandler(ctx *gin.Context) { func (h *Headscale) DERPProbeHandler(
switch ctx.Request.Method { writer http.ResponseWriter,
req *http.Request,
) {
switch req.Method {
case "HEAD", "GET": case "HEAD", "GET":
ctx.Writer.Header().Set("Access-Control-Allow-Origin", "*") writer.Header().Set("Access-Control-Allow-Origin", "*")
writer.WriteHeader(http.StatusOK)
default: default:
ctx.String(http.StatusMethodNotAllowed, "bogus probe method") writer.WriteHeader(http.StatusMethodNotAllowed)
_, err := writer.Write([]byte("bogus probe method"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
} }
} }
@@ -159,15 +195,18 @@ func (h *Headscale) DERPProbeHandler(ctx *gin.Context) {
// The initial implementation is here https://github.com/tailscale/tailscale/pull/1406 // The initial implementation is here https://github.com/tailscale/tailscale/pull/1406
// They have a cache, but not clear if that is really necessary at Headscale, uh, scale. // They have a cache, but not clear if that is really necessary at Headscale, uh, scale.
// An example implementation is found here https://derp.tailscale.com/bootstrap-dns // An example implementation is found here https://derp.tailscale.com/bootstrap-dns
func (h *Headscale) DERPBootstrapDNSHandler(ctx *gin.Context) { func (h *Headscale) DERPBootstrapDNSHandler(
writer http.ResponseWriter,
req *http.Request,
) {
dnsEntries := make(map[string][]net.IP) dnsEntries := make(map[string][]net.IP)
resolvCtx, cancel := context.WithTimeout(context.Background(), time.Minute) resolvCtx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel() defer cancel()
var r net.Resolver var resolver net.Resolver
for _, region := range h.DERPMap.Regions { for _, region := range h.DERPMap.Regions {
for _, node := range region.Nodes { // we don't care if we override some nodes for _, node := range region.Nodes { // we don't care if we override some nodes
addrs, err := r.LookupIP(resolvCtx, "ip", node.HostName) addrs, err := resolver.LookupIP(resolvCtx, "ip", node.HostName)
if err != nil { if err != nil {
log.Trace(). log.Trace().
Caller(). Caller().
@@ -179,7 +218,15 @@ func (h *Headscale) DERPBootstrapDNSHandler(ctx *gin.Context) {
dnsEntries[node.HostName] = addrs dnsEntries[node.HostName] = addrs
} }
} }
ctx.JSON(http.StatusOK, dnsEntries) writer.Header().Set("Content-Type", "application/json")
writer.WriteHeader(http.StatusOK)
err := json.NewEncoder(writer).Encode(dnsEntries)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
} }
// ServeSTUN starts a STUN server on the configured addr. // ServeSTUN starts a STUN server on the configured addr.

View File

@@ -161,7 +161,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
Hostname: "test_get_shared_nodes_1", Hostname: "test_get_shared_nodes_1",
NamespaceID: namespaceShared1.ID, NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1, Namespace: *namespaceShared1,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
@@ -178,7 +178,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Hostname: "test_get_shared_nodes_2", Hostname: "test_get_shared_nodes_2",
NamespaceID: namespaceShared2.ID, NamespaceID: namespaceShared2.ID,
Namespace: *namespaceShared2, Namespace: *namespaceShared2,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
@@ -195,7 +195,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Hostname: "test_get_shared_nodes_3", Hostname: "test_get_shared_nodes_3",
NamespaceID: namespaceShared3.ID, NamespaceID: namespaceShared3.ID,
Namespace: *namespaceShared3, Namespace: *namespaceShared3,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
@@ -212,7 +212,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Hostname: "test_get_shared_nodes_4", Hostname: "test_get_shared_nodes_4",
NamespaceID: namespaceShared1.ID, NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1, Namespace: *namespaceShared1,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
@@ -223,7 +223,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
baseDomain := "foobar.headscale.net" baseDomain := "foobar.headscale.net"
dnsConfigOrig := tailcfg.DNSConfig{ dnsConfigOrig := tailcfg.DNSConfig{
Routes: make(map[string][]dnstype.Resolver), Routes: make(map[string][]*dnstype.Resolver),
Domains: []string{baseDomain}, Domains: []string{baseDomain},
Proxied: true, Proxied: true,
} }
@@ -304,7 +304,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
Hostname: "test_get_shared_nodes_1", Hostname: "test_get_shared_nodes_1",
NamespaceID: namespaceShared1.ID, NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1, Namespace: *namespaceShared1,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
@@ -321,7 +321,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Hostname: "test_get_shared_nodes_2", Hostname: "test_get_shared_nodes_2",
NamespaceID: namespaceShared2.ID, NamespaceID: namespaceShared2.ID,
Namespace: *namespaceShared2, Namespace: *namespaceShared2,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
@@ -338,7 +338,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Hostname: "test_get_shared_nodes_3", Hostname: "test_get_shared_nodes_3",
NamespaceID: namespaceShared3.ID, NamespaceID: namespaceShared3.ID,
Namespace: *namespaceShared3, Namespace: *namespaceShared3,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
@@ -355,7 +355,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Hostname: "test_get_shared_nodes_4", Hostname: "test_get_shared_nodes_4",
NamespaceID: namespaceShared1.ID, NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1, Namespace: *namespaceShared1,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
@@ -366,7 +366,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
baseDomain := "foobar.headscale.net" baseDomain := "foobar.headscale.net"
dnsConfigOrig := tailcfg.DNSConfig{ dnsConfigOrig := tailcfg.DNSConfig{
Routes: make(map[string][]dnstype.Resolver), Routes: make(map[string][]*dnstype.Resolver),
Domains: []string{baseDomain}, Domains: []string{baseDomain},
Proxied: false, Proxied: false,
} }

View File

@@ -27,6 +27,7 @@ written by community members. It is _not_ verified by `headscale` developers.
**It might be outdated and it might miss necessary steps**. **It might be outdated and it might miss necessary steps**.
- [Running headscale in a container](running-headscale-container.md) - [Running headscale in a container](running-headscale-container.md)
- [Running headscale on OpenBSD](running-headscale-openbsd.md)
## Misc ## Misc

View File

@@ -33,7 +33,7 @@ Note: Namespaces will be created automatically when users authenticate with the
Headscale server. Headscale server.
ACLs could be written either on [huJSON](https://github.com/tailscale/hujson) ACLs could be written either on [huJSON](https://github.com/tailscale/hujson)
or Yaml. Check the [test ACLs](../tests/acls) for further information. or YAML. Check the [test ACLs](../tests/acls) for further information.
When registering the servers we will need to add the flag When registering the servers we will need to add the flag
`--advertised-tags=tag:<tag1>,tag:<tag2>`, and the user (namespace) that is `--advertised-tags=tag:<tag1>,tag:<tag2>`, and the user (namespace) that is
@@ -83,8 +83,8 @@ Here are the ACL's to implement the same permissions as above:
// boss have access to all servers // boss have access to all servers
{ {
"action": "accept", "action": "accept",
"users": ["group:boss"], "src": ["group:boss"],
"ports": [ "dst": [
"tag:prod-databases:*", "tag:prod-databases:*",
"tag:prod-app-servers:*", "tag:prod-app-servers:*",
"tag:internal:*", "tag:internal:*",
@@ -93,11 +93,12 @@ Here are the ACL's to implement the same permissions as above:
] ]
}, },
// admin have only access to administrative ports of the servers // admin have only access to administrative ports of the servers, in tcp/22
{ {
"action": "accept", "action": "accept",
"users": ["group:admin"], "src": ["group:admin"],
"ports": [ "proto": "tcp",
"dst": [
"tag:prod-databases:22", "tag:prod-databases:22",
"tag:prod-app-servers:22", "tag:prod-app-servers:22",
"tag:internal:22", "tag:internal:22",
@@ -106,12 +107,26 @@ Here are the ACL's to implement the same permissions as above:
] ]
}, },
// we also allow admin to ping the servers
{
"action": "accept",
"src": ["group:admin"],
"proto": "icmp",
"dst": [
"tag:prod-databases:*",
"tag:prod-app-servers:*",
"tag:internal:*",
"tag:dev-databases:*",
"tag:dev-app-servers:*"
]
},
// developers have access to databases servers and application servers on all ports // developers have access to databases servers and application servers on all ports
// they can only view the applications servers in prod and have no access to databases servers in production // they can only view the applications servers in prod and have no access to databases servers in production
{ {
"action": "accept", "action": "accept",
"users": ["group:dev"], "src": ["group:dev"],
"ports": [ "dst": [
"tag:dev-databases:*", "tag:dev-databases:*",
"tag:dev-app-servers:*", "tag:dev-app-servers:*",
"tag:prod-app-servers:80,443" "tag:prod-app-servers:80,443"
@@ -124,37 +139,38 @@ Here are the ACL's to implement the same permissions as above:
// https://github.com/juanfont/headscale/issues/502 // https://github.com/juanfont/headscale/issues/502
{ {
"action": "accept", "action": "accept",
"users": ["group:dev"], "src": ["group:dev"],
"ports": ["10.20.0.0/16:443,5432", "router.internal:0"] "dst": ["10.20.0.0/16:443,5432", "router.internal:0"]
}, },
// servers should be able to talk to database. Database should not be able to initiate connections to // servers should be able to talk to database in tcp/5432. Database should not be able to initiate connections to
// applications servers // applications servers
{ {
"action": "accept", "action": "accept",
"users": ["tag:dev-app-servers"], "src": ["tag:dev-app-servers"],
"ports": ["tag:dev-databases:5432"] "proto": "tcp",
"dst": ["tag:dev-databases:5432"]
}, },
{ {
"action": "accept", "action": "accept",
"users": ["tag:prod-app-servers"], "src": ["tag:prod-app-servers"],
"ports": ["tag:prod-databases:5432"] "dst": ["tag:prod-databases:5432"]
}, },
// interns have access to dev-app-servers only in reading mode // interns have access to dev-app-servers only in reading mode
{ {
"action": "accept", "action": "accept",
"users": ["group:intern"], "src": ["group:intern"],
"ports": ["tag:dev-app-servers:80,443"] "dst": ["tag:dev-app-servers:80,443"]
}, },
// We still have to allow internal namespaces communications since nothing guarantees that each user have // We still have to allow internal namespaces communications since nothing guarantees that each user have
// their own namespaces. // their own namespaces.
{ "action": "accept", "users": ["boss"], "ports": ["boss:*"] }, { "action": "accept", "src": ["boss"], "dst": ["boss:*"] },
{ "action": "accept", "users": ["dev1"], "ports": ["dev1:*"] }, { "action": "accept", "src": ["dev1"], "dst": ["dev1:*"] },
{ "action": "accept", "users": ["dev2"], "ports": ["dev2:*"] }, { "action": "accept", "src": ["dev2"], "dst": ["dev2:*"] },
{ "action": "accept", "users": ["admin1"], "ports": ["admin1:*"] }, { "action": "accept", "src": ["admin1"], "dst": ["admin1:*"] },
{ "action": "accept", "users": ["intern1"], "ports": ["intern1:*"] } { "action": "accept", "src": ["intern1"], "dst": ["intern1:*"] }
] ]
} }
``` ```

View File

@@ -0,0 +1,206 @@
# Running headscale on OpenBSD
## Goal
This documentation has the goal of showing a user how-to install and run `headscale` on OpenBSD 7.1.
In additional to the "get up and running section", there is an optional [rc.d section](#running-headscale-in-the-background-with-rcd)
describing how to make `headscale` run properly in a server environment.
## Install `headscale`
1. Install from ports (Not Recommend)
As of OpenBSD 7.1, there's a headscale in ports collection, however, it's severely outdated(v0.12.4).
You can install it via `pkg_add headscale`.
2. Install from source on OpenBSD 7.1
```shell
# Install prerequistes
# 1. go v1.18+: headscale newer than 0.15 needs go 1.18+ to compile
# 2. gmake: Makefile in the headscale repo is written in GNU make syntax
pkg_add -D snap go
pkg_add gmake
git clone https://github.com/juanfont/headscale.git
cd headscale
# optionally checkout a release
# option a. you can find offical relase at https://github.com/juanfont/headscale/releases/latest
# option b. get latest tag, this may be a beta release
latestTag=$(git describe --tags `git rev-list --tags --max-count=1`)
git checkout $latestTag
gmake build
# make it executable
chmod a+x headscale
# copy it to /usr/local/sbin
cp headscale /usr/local/sbin
```
3. Install from source via cross compile
```shell
# Install prerequistes
# 1. go v1.18+: headscale newer than 0.15 needs go 1.18+ to compile
# 2. gmake: Makefile in the headscale repo is written in GNU make syntax
git clone https://github.com/juanfont/headscale.git
cd headscale
# optionally checkout a release
# option a. you can find offical relase at https://github.com/juanfont/headscale/releases/latest
# option b. get latest tag, this may be a beta release
latestTag=$(git describe --tags `git rev-list --tags --max-count=1`)
git checkout $latestTag
make build GOOS=openbsd
# copy headscale to openbsd machine and put it in /usr/local/sbin
```
## Configure and run `headscale`
1. Prepare a directory to hold `headscale` configuration and the [SQLite](https://www.sqlite.org/) database:
```shell
# Directory for configuration
mkdir -p /etc/headscale
# Directory for Database, and other variable data (like certificates)
mkdir -p /var/lib/headscale
```
2. Create an empty SQLite database:
```shell
touch /var/lib/headscale/db.sqlite
```
3. Create a `headscale` configuration:
```shell
touch /etc/headscale/config.yaml
```
It is **strongly recommended** to copy and modify the [example configuration](../config-example.yaml)
from the [headscale repository](../)
4. Start the headscale server:
```shell
headscale serve
```
This command will start `headscale` in the current terminal session.
---
To continue the tutorial, open a new terminal and let it run in the background.
Alternatively use terminal emulators like [tmux](https://github.com/tmux/tmux).
To run `headscale` in the background, please follow the steps in the [rc.d section](#running-headscale-in-the-background-with-rcd) before continuing.
5. Verify `headscale` is running:
Verify `headscale` is available:
```shell
curl http://127.0.0.1:9090/metrics
```
6. Create a namespace ([tailnet](https://tailscale.com/kb/1136/tailnet/)):
```shell
headscale namespaces create myfirstnamespace
```
### Register a machine (normal login)
On a client machine, execute the `tailscale` login command:
```shell
tailscale up --login-server YOUR_HEADSCALE_URL
```
Register the machine:
```shell
headscale --namespace myfirstnamespace nodes register --key <YOU_+MACHINE_KEY>
```
### Register machine using a pre authenticated key
Generate a key using the command line:
```shell
headscale --namespace myfirstnamespace preauthkeys create --reusable --expiration 24h
```
This will return a pre-authenticated key that can be used to connect a node to `headscale` during the `tailscale` command:
```shell
tailscale up --login-server <YOUR_HEADSCALE_URL> --authkey <YOUR_AUTH_KEY>
```
## Running `headscale` in the background with rc.d
This section demonstrates how to run `headscale` as a service in the background with [rc.d](https://man.openbsd.org/rc.d).
1. Create a rc.d service at `/etc/rc.d/headscale` containing:
```shell
#!/bin/ksh
daemon="/usr/local/sbin/headscale"
daemon_logger="daemon.info"
daemon_user="root"
daemon_flags="serve"
daemon_timeout=60
. /etc/rc.d/rc.subr
rc_bg=YES
rc_reload=NO
rc_cmd $1
```
2. `/etc/rc.d/headscale` needs execute permission:
```shell
chmod a+x /etc/rc.d/headscale
```
3. Start `headscale` service:
```shell
rcctl start headscale
```
4. Make `headscale` service start at boot:
```shell
rcctl enable headscale
```
5. Verify the headscale service:
```shell
rcctl check headscale
```
Verify `headscale` is available:
```shell
curl http://127.0.0.1:9090/metrics
```
`headscale` will now run in the background and start at boot.

12
flake.lock generated
View File

@@ -2,11 +2,11 @@
"nodes": { "nodes": {
"flake-utils": { "flake-utils": {
"locked": { "locked": {
"lastModified": 1644229661, "lastModified": 1653893745,
"narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=", "narHash": "sha256-0jntwV3Z8//YwuOjzhV2sgJJPt+HY6KhU7VZUL0fKZQ=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "3cecb5b042f7f209c56ffd8371b2711a290ec797", "rev": "1ed9fb1935d260de5fe1c2f7ee0ebaae17ed2fa1",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -17,11 +17,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1653733789, "lastModified": 1654847188,
"narHash": "sha256-VIYazYCWNvcFNns2XQkHx/mVmCZ3oebZv8W2LS1gLQE=", "narHash": "sha256-MC+eP7XOGE1LAswOPqdcGoUqY9mEQ3ZaaxamVTbc0hM=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "d1086907f56c5a6c33c0c2e8dc9f42ef6988294f", "rev": "8b66e3f2ebcc644b78cec9d6f152192f4e7d322f",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -24,7 +24,7 @@
# When updating go.mod or go.sum, a new sha will need to be calculated, # 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. # update this if you have a mismatch after doing a change to thos files.
vendorSha256 = "sha256-b6qPOO/NmcXsAsSRWZlYXZKyRAF++DsL4TEZzRhQhME="; vendorSha256 = "sha256-T6rH+aqofFmCPxDfoA5xd3kNUJeZkT4GRyuFEnenps8=";
ldflags = [ "-s" "-w" "-X github.com/juanfont/headscale/cmd/headscale/cli.Version=v${version}" ]; ldflags = [ "-s" "-w" "-X github.com/juanfont/headscale/cmd/headscale/cli.Version=v${version}" ];
}; };

31
go.mod
View File

@@ -11,24 +11,27 @@ require (
github.com/gin-gonic/gin v1.7.7 github.com/gin-gonic/gin v1.7.7
github.com/glebarez/sqlite v1.4.3 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/gorilla/mux v1.8.0
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
github.com/grpc-ecosystem/grpc-gateway/v2 v2.10.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.10.0
github.com/klauspost/compress v1.15.1 github.com/klauspost/compress v1.15.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/prometheus/common v0.32.1
github.com/pterm/pterm v0.12.41 github.com/pterm/pterm v0.12.41
github.com/puzpuzpuz/xsync v1.2.1
github.com/rs/zerolog v1.26.1 github.com/rs/zerolog v1.26.1
github.com/spf13/cobra v1.4.0 github.com/spf13/cobra v1.4.0
github.com/spf13/viper v1.11.0 github.com/spf13/viper v1.11.0
github.com/stretchr/testify v1.7.1 github.com/stretchr/testify v1.7.1
github.com/tailscale/hujson v0.0.0-20220421170326-6583d0610064 github.com/tailscale/hujson v0.0.0-20220506202205-92b4b88a9e17
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
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-20220411220226-7b82a4e95df4 golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 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-20220513210516-0976fa681c29
google.golang.org/genproto v0.0.0-20220422154200-b37d22cd5731 google.golang.org/genproto v0.0.0-20220422154200-b37d22cd5731
google.golang.org/grpc v1.46.0 google.golang.org/grpc v1.46.0
google.golang.org/protobuf v1.28.0 google.golang.org/protobuf v1.28.0
@@ -38,12 +41,12 @@ require (
gorm.io/driver/postgres v1.3.5 gorm.io/driver/postgres v1.3.5
gorm.io/gorm v1.23.4 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.24.0 tailscale.com v1.26.0
) )
require ( require (
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
github.com/Microsoft/go-winio v0.5.1 // indirect github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/akutz/memconn v0.1.0 // indirect github.com/akutz/memconn v0.1.0 // indirect
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect
@@ -53,8 +56,8 @@ require (
github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 // 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/docker/cli v20.10.11+incompatible // indirect github.com/docker/cli v20.10.16+incompatible // indirect
github.com/docker/docker v20.10.7+incompatible // indirect github.com/docker/docker v20.10.16+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.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
@@ -65,7 +68,7 @@ require (
github.com/go-playground/validator/v10 v10.4.1 // indirect github.com/go-playground/validator/v10 v10.4.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // 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-cmp v0.5.8 // indirect
github.com/google/go-github v17.0.0+incompatible // indirect github.com/google/go-github v17.0.0+incompatible // indirect
github.com/google/go-querystring v1.1.0 // indirect github.com/google/go-querystring v1.1.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
@@ -105,17 +108,15 @@ require (
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // 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-rc1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 // indirect
github.com/opencontainers/runc v1.0.2 // 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/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
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect github.com/prometheus/procfs v0.7.3 // indirect
github.com/puzpuzpuz/xsync v1.2.1 // 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-0.20211023094830-115ce09fd6b4 // indirect github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 // indirect
@@ -133,8 +134,8 @@ require (
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-20220412020605-290c469a71a5 // indirect golang.org/x/net v0.0.0-20220516155154-20f960328961 // indirect
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a // indirect
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // 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

624
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -40,13 +40,13 @@ func (s *IntegrationCLITestSuite) SetupTest() {
if ppool, err := dockertest.NewPool(""); err == nil { if ppool, err := dockertest.NewPool(""); err == nil {
s.pool = *ppool s.pool = *ppool
} else { } else {
log.Fatalf("Could not connect to docker: %s", err) s.FailNow(fmt.Sprintf("Could not connect to docker: %s", err), "")
} }
if pnetwork, err := s.pool.CreateNetwork("headscale-test"); err == nil { if pnetwork, err := s.pool.CreateNetwork("headscale-test"); err == nil {
s.network = *pnetwork s.network = *pnetwork
} else { } else {
log.Fatalf("Could not create network: %s", err) s.FailNow(fmt.Sprintf("Could not create network: %s", err), "")
} }
headscaleBuildOptions := &dockertest.BuildOptions{ headscaleBuildOptions := &dockertest.BuildOptions{
@@ -56,7 +56,7 @@ func (s *IntegrationCLITestSuite) SetupTest() {
currentPath, err := os.Getwd() currentPath, err := os.Getwd()
if err != nil { if err != nil {
log.Fatalf("Could not determine current path: %s", err) s.FailNow(fmt.Sprintf("Could not determine current path: %s", err), "")
} }
headscaleOptions := &dockertest.RunOptions{ headscaleOptions := &dockertest.RunOptions{
@@ -68,11 +68,16 @@ func (s *IntegrationCLITestSuite) SetupTest() {
Cmd: []string{"headscale", "serve"}, Cmd: []string{"headscale", "serve"},
} }
err = s.pool.RemoveContainerByName(headscaleHostname)
if err != nil {
s.FailNow(fmt.Sprintf("Could not remove existing container before building test: %s", err), "")
}
fmt.Println("Creating headscale container") fmt.Println("Creating headscale container")
if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil { if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
s.headscale = *pheadscale s.headscale = *pheadscale
} else { } else {
log.Fatalf("Could not start headscale container: %s", err) s.FailNow(fmt.Sprintf("Could not start headscale container: %s", err), "")
} }
fmt.Println("Created headscale container") fmt.Println("Created headscale container")
@@ -1721,3 +1726,43 @@ func (s *IntegrationCLITestSuite) TestNodeMoveCommand() {
assert.Equal(s.T(), machine.Namespace, oldNamespace) assert.Equal(s.T(), machine.Namespace, oldNamespace)
} }
func (s *IntegrationCLITestSuite) TestLoadConfigFromCommand() {
// TODO: make sure defaultConfig is not same as altConfig
defaultConfig, err := os.ReadFile("integration_test/etc/config.dump.gold.yaml")
assert.Nil(s.T(), err)
altConfig, err := os.ReadFile("integration_test/etc/alt-config.dump.gold.yaml")
assert.Nil(s.T(), err)
_, err = ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"dumpConfig",
},
[]string{},
)
assert.Nil(s.T(), err)
defaultDumpConfig, err := os.ReadFile("integration_test/etc/config.dump.yaml")
assert.Nil(s.T(), err)
assert.YAMLEq(s.T(), string(defaultConfig), string(defaultDumpConfig))
_, err = ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"-c",
"/etc/headscale/alt-config.yaml",
"dumpConfig",
},
[]string{},
)
assert.Nil(s.T(), err)
altDumpConfig, err := os.ReadFile("integration_test/etc/config.dump.yaml")
assert.Nil(s.T(), err)
assert.YAMLEq(s.T(), string(altConfig), string(altDumpConfig))
}

View File

@@ -6,7 +6,10 @@ package headscale
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"os"
"strconv"
"strings" "strings"
"time" "time"
@@ -16,16 +19,21 @@ import (
"inet.af/netaddr" "inet.af/netaddr"
) )
const DOCKER_EXECUTE_TIMEOUT = 10 * time.Second const (
DOCKER_EXECUTE_TIMEOUT = 10 * time.Second
)
var ( var (
errEnvVarEmpty = errors.New("getenv: environment variable empty")
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{ tailscaleVersions = []string{
"head", "head",
"unstable", "unstable",
"1.24.0", "1.26.0",
"1.24.2",
"1.22.2", "1.22.2",
"1.20.4", "1.20.4",
"1.18.2", "1.18.2",
@@ -282,3 +290,25 @@ func getMagicFQDN(
return hostnames, nil return hostnames, nil
} }
func GetEnvStr(key string) (string, error) {
v := os.Getenv(key)
if v == "" {
return v, errEnvVarEmpty
}
return v, nil
}
func GetEnvBool(key string) (bool, error) {
s, err := GetEnvStr(key)
if err != nil {
return false, err
}
v, err := strconv.ParseBool(s)
if err != nil {
return false, err
}
return v, nil
}

View File

@@ -40,41 +40,50 @@ type IntegrationDERPTestSuite struct {
pool dockertest.Pool pool dockertest.Pool
networks map[int]dockertest.Network // so we keep the containers isolated networks map[int]dockertest.Network // so we keep the containers isolated
headscale dockertest.Resource headscale dockertest.Resource
saveLogs bool
tailscales map[string]dockertest.Resource tailscales map[string]dockertest.Resource
joinWaitGroup sync.WaitGroup joinWaitGroup sync.WaitGroup
} }
func TestDERPIntegrationTestSuite(t *testing.T) { func TestDERPIntegrationTestSuite(t *testing.T) {
saveLogs, err := GetEnvBool("HEADSCALE_INTEGRATION_SAVE_LOG")
if err != nil {
saveLogs = false
}
s := new(IntegrationDERPTestSuite) s := new(IntegrationDERPTestSuite)
s.tailscales = make(map[string]dockertest.Resource) s.tailscales = make(map[string]dockertest.Resource)
s.networks = make(map[int]dockertest.Network) s.networks = make(map[int]dockertest.Network)
s.saveLogs = saveLogs
suite.Run(t, s) suite.Run(t, s)
// HandleStats, which allows us to check if we passed and save logs // HandleStats, which allows us to check if we passed and save logs
// is called after TearDown, so we cannot tear down containers before // is called after TearDown, so we cannot tear down containers before
// we have potentially saved the logs. // we have potentially saved the logs.
for _, tailscale := range s.tailscales { if s.saveLogs {
if err := s.pool.Purge(&tailscale); err != nil { for _, tailscale := range s.tailscales {
if err := s.pool.Purge(&tailscale); err != nil {
log.Printf("Could not purge resource: %s\n", err)
}
}
if !s.stats.Passed() {
err := s.saveLog(&s.headscale, "test_output")
if err != nil {
log.Printf("Could not save log: %s\n", err)
}
}
if err := s.pool.Purge(&s.headscale); err != nil {
log.Printf("Could not purge resource: %s\n", err) log.Printf("Could not purge resource: %s\n", err)
} }
}
if !s.stats.Passed() { for _, network := range s.networks {
err := s.saveLog(&s.headscale, "test_output") if err := network.Close(); err != nil {
if err != nil { log.Printf("Could not close network: %s\n", err)
log.Printf("Could not save log: %s\n", err) }
}
}
if err := s.pool.Purge(&s.headscale); err != nil {
log.Printf("Could not purge resource: %s\n", err)
}
for _, network := range s.networks {
if err := network.Close(); err != nil {
log.Printf("Could not close network: %s\n", err)
} }
} }
} }
@@ -83,14 +92,14 @@ func (s *IntegrationDERPTestSuite) SetupSuite() {
if ppool, err := dockertest.NewPool(""); err == nil { if ppool, err := dockertest.NewPool(""); err == nil {
s.pool = *ppool s.pool = *ppool
} else { } else {
log.Fatalf("Could not connect to docker: %s", err) s.FailNow(fmt.Sprintf("Could not connect to docker: %s", err), "")
} }
for i := 0; i < totalContainers; i++ { for i := 0; i < totalContainers; i++ {
if pnetwork, err := s.pool.CreateNetwork(fmt.Sprintf("headscale-derp-%d", i)); err == nil { if pnetwork, err := s.pool.CreateNetwork(fmt.Sprintf("headscale-derp-%d", i)); err == nil {
s.networks[i] = *pnetwork s.networks[i] = *pnetwork
} else { } else {
log.Fatalf("Could not create network: %s", err) s.FailNow(fmt.Sprintf("Could not create network: %s", err), "")
} }
} }
@@ -101,7 +110,7 @@ func (s *IntegrationDERPTestSuite) SetupSuite() {
currentPath, err := os.Getwd() currentPath, err := os.Getwd()
if err != nil { if err != nil {
log.Fatalf("Could not determine current path: %s", err) s.FailNow(fmt.Sprintf("Could not determine current path: %s", err), "")
} }
headscaleOptions := &dockertest.RunOptions{ headscaleOptions := &dockertest.RunOptions{
@@ -120,11 +129,16 @@ func (s *IntegrationDERPTestSuite) SetupSuite() {
}, },
} }
err = s.pool.RemoveContainerByName(headscaleHostname)
if err != nil {
s.FailNow(fmt.Sprintf("Could not remove existing container before building test: %s", err), "")
}
log.Println("Creating headscale container") log.Println("Creating headscale container")
if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil { if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
s.headscale = *pheadscale s.headscale = *pheadscale
} else { } else {
log.Fatalf("Could not start headscale container: %s", err) s.FailNow(fmt.Sprintf("Could not start headscale container: %s", err), "")
} }
log.Println("Created headscale container to test DERP") log.Println("Created headscale container to test DERP")
@@ -290,6 +304,23 @@ func (s *IntegrationDERPTestSuite) tailscaleContainer(
} }
func (s *IntegrationDERPTestSuite) TearDownSuite() { func (s *IntegrationDERPTestSuite) TearDownSuite() {
if !s.saveLogs {
for _, tailscale := range s.tailscales {
if err := s.pool.Purge(&tailscale); err != nil {
log.Printf("Could not purge resource: %s\n", err)
}
}
if err := s.pool.Purge(&s.headscale); err != nil {
log.Printf("Could not purge resource: %s\n", err)
}
for _, network := range s.networks {
if err := network.Close(); err != nil {
log.Printf("Could not close network: %s\n", err)
}
}
}
} }
func (s *IntegrationDERPTestSuite) HandleStats( func (s *IntegrationDERPTestSuite) HandleStats(

View File

@@ -36,6 +36,7 @@ type IntegrationTestSuite struct {
pool dockertest.Pool pool dockertest.Pool
network dockertest.Network network dockertest.Network
headscale dockertest.Resource headscale dockertest.Resource
saveLogs bool
namespaces map[string]TestNamespace namespaces map[string]TestNamespace
@@ -43,6 +44,11 @@ type IntegrationTestSuite struct {
} }
func TestIntegrationTestSuite(t *testing.T) { func TestIntegrationTestSuite(t *testing.T) {
saveLogs, err := GetEnvBool("HEADSCALE_INTEGRATION_SAVE_LOG")
if err != nil {
saveLogs = false
}
s := new(IntegrationTestSuite) s := new(IntegrationTestSuite)
s.namespaces = map[string]TestNamespace{ s.namespaces = map[string]TestNamespace{
@@ -55,32 +61,35 @@ func TestIntegrationTestSuite(t *testing.T) {
tailscales: make(map[string]dockertest.Resource), tailscales: make(map[string]dockertest.Resource),
}, },
} }
s.saveLogs = saveLogs
suite.Run(t, s) suite.Run(t, s)
// HandleStats, which allows us to check if we passed and save logs // HandleStats, which allows us to check if we passed and save logs
// is called after TearDown, so we cannot tear down containers before // is called after TearDown, so we cannot tear down containers before
// we have potentially saved the logs. // we have potentially saved the logs.
for _, scales := range s.namespaces { if s.saveLogs {
for _, tailscale := range scales.tailscales { for _, scales := range s.namespaces {
if err := s.pool.Purge(&tailscale); err != nil { for _, tailscale := range scales.tailscales {
log.Printf("Could not purge resource: %s\n", err) if err := s.pool.Purge(&tailscale); err != nil {
log.Printf("Could not purge resource: %s\n", err)
}
} }
} }
}
if !s.stats.Passed() { if !s.stats.Passed() {
err := s.saveLog(&s.headscale, "test_output") err := s.saveLog(&s.headscale, "test_output")
if err != nil { if err != nil {
log.Printf("Could not save log: %s\n", err) log.Printf("Could not save log: %s\n", err)
}
}
if err := s.pool.Purge(&s.headscale); err != nil {
log.Printf("Could not purge resource: %s\n", err)
} }
}
if err := s.pool.Purge(&s.headscale); err != nil {
log.Printf("Could not purge resource: %s\n", err)
}
if err := s.network.Close(); err != nil { if err := s.network.Close(); err != nil {
log.Printf("Could not close network: %s\n", err) log.Printf("Could not close network: %s\n", err)
}
} }
} }
@@ -209,13 +218,13 @@ func (s *IntegrationTestSuite) SetupSuite() {
if ppool, err := dockertest.NewPool(""); err == nil { if ppool, err := dockertest.NewPool(""); err == nil {
s.pool = *ppool s.pool = *ppool
} else { } else {
log.Fatalf("Could not connect to docker: %s", err) s.FailNow(fmt.Sprintf("Could not connect to docker: %s", err), "")
} }
if pnetwork, err := s.pool.CreateNetwork("headscale-test"); err == nil { if pnetwork, err := s.pool.CreateNetwork("headscale-test"); err == nil {
s.network = *pnetwork s.network = *pnetwork
} else { } else {
log.Fatalf("Could not create network: %s", err) s.FailNow(fmt.Sprintf("Could not create network: %s", err), "")
} }
headscaleBuildOptions := &dockertest.BuildOptions{ headscaleBuildOptions := &dockertest.BuildOptions{
@@ -225,7 +234,7 @@ func (s *IntegrationTestSuite) SetupSuite() {
currentPath, err := os.Getwd() currentPath, err := os.Getwd()
if err != nil { if err != nil {
log.Fatalf("Could not determine current path: %s", err) s.FailNow(fmt.Sprintf("Could not determine current path: %s", err), "")
} }
headscaleOptions := &dockertest.RunOptions{ headscaleOptions := &dockertest.RunOptions{
@@ -237,11 +246,16 @@ func (s *IntegrationTestSuite) SetupSuite() {
Cmd: []string{"headscale", "serve"}, Cmd: []string{"headscale", "serve"},
} }
err = s.pool.RemoveContainerByName(headscaleHostname)
if err != nil {
s.FailNow(fmt.Sprintf("Could not remove existing container before building test: %s", err), "")
}
log.Println("Creating headscale container") log.Println("Creating headscale container")
if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil { if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
s.headscale = *pheadscale s.headscale = *pheadscale
} else { } else {
log.Fatalf("Could not start headscale container: %s", err) s.FailNow(fmt.Sprintf("Could not start headscale container: %s", err), "")
} }
log.Println("Created headscale container") log.Println("Created headscale container")
@@ -338,6 +352,23 @@ func (s *IntegrationTestSuite) SetupSuite() {
} }
func (s *IntegrationTestSuite) TearDownSuite() { func (s *IntegrationTestSuite) TearDownSuite() {
if !s.saveLogs {
for _, scales := range s.namespaces {
for _, tailscale := range scales.tailscales {
if err := s.pool.Purge(&tailscale); err != nil {
log.Printf("Could not purge resource: %s\n", err)
}
}
}
if err := s.pool.Purge(&s.headscale); err != nil {
log.Printf("Could not purge resource: %s\n", err)
}
if err := s.network.Close(); err != nil {
log.Printf("Could not close network: %s\n", err)
}
}
} }
func (s *IntegrationTestSuite) HandleStats( func (s *IntegrationTestSuite) HandleStats(

View File

@@ -0,0 +1,47 @@
acl_policy_path: ""
cli:
insecure: false
timeout: 5s
db_path: /tmp/integration_test_db.sqlite3
db_type: sqlite3
derp:
auto_update_enabled: false
server:
enabled: false
stun:
enabled: true
update_frequency: 1m
urls:
- https://controlplane.tailscale.com/derpmap/default
dns_config:
base_domain: headscale.net
domains: []
magic_dns: true
nameservers:
- 1.1.1.1
ephemeral_node_inactivity_timeout: 30m
node_update_check_interval: 10s
grpc_allow_insecure: false
grpc_listen_addr: :50443
ip_prefixes:
- fd7a:115c:a1e0::/48
- 100.64.0.0/10
listen_addr: 0.0.0.0:18080
log_level: disabled
logtail:
enabled: false
metrics_listen_addr: 127.0.0.1:19090
oidc:
scope:
- openid
- profile
- email
strip_email_domain: true
private_key_path: private.key
server_url: http://headscale:18080
tls_client_auth_mode: relaxed
tls_letsencrypt_cache_dir: /var/www/.cache
tls_letsencrypt_challenge_type: HTTP-01
unix_socket: /var/run/headscale.sock
unix_socket_permission: "0o770"
randomize_client_port: false

View File

@@ -0,0 +1,25 @@
log_level: trace
acl_policy_path: ""
db_type: sqlite3
ephemeral_node_inactivity_timeout: 30m
node_update_check_interval: 10s
ip_prefixes:
- fd7a:115c:a1e0::/48
- 100.64.0.0/10
dns_config:
base_domain: headscale.net
magic_dns: true
domains: []
nameservers:
- 1.1.1.1
db_path: /tmp/integration_test_db.sqlite3
private_key_path: private.key
listen_addr: 0.0.0.0:18080
metrics_listen_addr: 127.0.0.1:19090
server_url: http://headscale:18080
derp:
urls:
- https://controlplane.tailscale.com/derpmap/default
auto_update_enabled: false
update_frequency: 1m

View File

@@ -0,0 +1,47 @@
acl_policy_path: ""
cli:
insecure: false
timeout: 5s
db_path: /tmp/integration_test_db.sqlite3
db_type: sqlite3
derp:
auto_update_enabled: false
server:
enabled: false
stun:
enabled: true
update_frequency: 1m
urls:
- https://controlplane.tailscale.com/derpmap/default
dns_config:
base_domain: headscale.net
domains: []
magic_dns: true
nameservers:
- 1.1.1.1
ephemeral_node_inactivity_timeout: 30m
node_update_check_interval: 10s
grpc_allow_insecure: false
grpc_listen_addr: :50443
ip_prefixes:
- fd7a:115c:a1e0::/48
- 100.64.0.0/10
listen_addr: 0.0.0.0:8080
log_level: disabled
logtail:
enabled: false
metrics_listen_addr: 127.0.0.1:9090
oidc:
scope:
- openid
- profile
- email
strip_email_domain: true
private_key_path: private.key
server_url: http://headscale:8080
tls_client_auth_mode: relaxed
tls_letsencrypt_cache_dir: /var/www/.cache
tls_letsencrypt_challenge_type: HTTP-01
unix_socket: /var/run/headscale.sock
unix_socket_permission: "0o770"
randomize_client_port: false

View File

@@ -2,6 +2,7 @@ log_level: trace
acl_policy_path: "" acl_policy_path: ""
db_type: sqlite3 db_type: sqlite3
ephemeral_node_inactivity_timeout: 30m ephemeral_node_inactivity_timeout: 30m
node_update_check_interval: 10s
ip_prefixes: ip_prefixes:
- fd7a:115c:a1e0::/48 - fd7a:115c:a1e0::/48
- 100.64.0.0/10 - 100.64.0.0/10

View File

@@ -2,6 +2,7 @@ log_level: trace
acl_policy_path: "" acl_policy_path: ""
db_type: sqlite3 db_type: sqlite3
ephemeral_node_inactivity_timeout: 30m ephemeral_node_inactivity_timeout: 30m
node_update_check_interval: 10s
ip_prefixes: ip_prefixes:
- fd7a:115c:a1e0::/48 - fd7a:115c:a1e0::/48
- 100.64.0.0/10 - 100.64.0.0/10

View File

@@ -27,6 +27,7 @@ const (
errCouldNotConvertMachineInterface = Error("failed to convert machine interface") errCouldNotConvertMachineInterface = Error("failed to convert machine interface")
errHostnameTooLong = Error("Hostname too long") errHostnameTooLong = Error("Hostname too long")
MachineGivenNameHashLength = 8 MachineGivenNameHashLength = 8
MachineGivenNameTrimSize = 2
) )
const ( const (
@@ -637,6 +638,10 @@ func (machine Machine) toNode(
hostInfo := machine.GetHostInfo() hostInfo := machine.GetHostInfo()
// A node is Online if it is connected to the control server,
// and we now we update LastSeen every keepAliveInterval duration at least.
online := machine.LastSeen.After(time.Now().Add(-keepAliveInterval))
node := tailcfg.Node{ node := tailcfg.Node{
ID: tailcfg.NodeID(machine.ID), // this is the actual ID ID: tailcfg.NodeID(machine.ID), // this is the actual ID
StableID: tailcfg.StableNodeID( StableID: tailcfg.StableNodeID(
@@ -653,6 +658,7 @@ func (machine Machine) toNode(
Endpoints: machine.Endpoints, Endpoints: machine.Endpoints,
DERP: derp, DERP: derp,
Online: &online,
Hostinfo: hostInfo.View(), Hostinfo: hostInfo.View(),
Created: machine.CreatedAt, Created: machine.CreatedAt,
LastSeen: machine.LastSeen, LastSeen: machine.LastSeen,
@@ -893,7 +899,7 @@ func (machine *Machine) RoutesToProto() *v1.Routes {
func (h *Headscale) GenerateGivenName(suppliedName string) (string, error) { func (h *Headscale) GenerateGivenName(suppliedName string) (string, error) {
// If a hostname is or will be longer than 63 chars after adding the hash, // If a hostname is or will be longer than 63 chars after adding the hash,
// it needs to be trimmed. // it needs to be trimmed.
trimmedHostnameLength := labelHostnameLength - MachineGivenNameHashLength - 2 trimmedHostnameLength := labelHostnameLength - MachineGivenNameHashLength - MachineGivenNameTrimSize
normalizedHostname, err := NormalizeToFQDNRules( normalizedHostname, err := NormalizeToFQDNRules(
suppliedName, suppliedName,

View File

@@ -188,8 +188,8 @@ func (s *Suite) TestGetACLFilteredPeers(c *check.C) {
Hosts: map[string]netaddr.IPPrefix{}, Hosts: map[string]netaddr.IPPrefix{},
TagOwners: map[string][]string{}, TagOwners: map[string][]string{},
ACLs: []ACL{ ACLs: []ACL{
{Action: "accept", Users: []string{"admin"}, Ports: []string{"*:*"}}, {Action: "accept", Sources: []string{"admin"}, Destinations: []string{"*:*"}},
{Action: "accept", Users: []string{"test"}, Ports: []string{"test:*"}}, {Action: "accept", Sources: []string{"test"}, Destinations: []string{"test:*"}},
}, },
Tests: []ACLTest{}, Tests: []ACLTest{},
} }
@@ -249,10 +249,12 @@ func (s *Suite) TestExpireMachine(c *check.C) {
machineFromDB, err := app.GetMachine("test", "testmachine") machineFromDB, err := app.GetMachine("test", "testmachine")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(machineFromDB, check.NotNil)
c.Assert(machineFromDB.isExpired(), check.Equals, false) c.Assert(machineFromDB.isExpired(), check.Equals, false)
app.ExpireMachine(machineFromDB) err = app.ExpireMachine(machineFromDB)
c.Assert(err, check.IsNil)
c.Assert(machineFromDB.isExpired(), check.Equals, true) c.Assert(machineFromDB.isExpired(), check.Equals, true)
} }
@@ -918,6 +920,7 @@ func TestHeadscale_GenerateGivenName(t *testing.T) {
err, err,
tt.wantErr, tt.wantErr,
) )
return return
} }

View File

@@ -148,6 +148,21 @@ func (h *Headscale) ListNamespaces() ([]Namespace, error) {
return namespaces, nil return namespaces, nil
} }
func (h *Headscale) ListNamespacesStr() ([]string, error) {
namespaces, err := h.ListNamespaces()
if err != nil {
return []string{}, err
}
namespaceStrs := make([]string, len(namespaces))
for index, namespace := range namespaces {
namespaceStrs[index] = namespace.Name
}
return namespaceStrs, nil
}
// ListMachinesInNamespace gets all the nodes in a given namespace. // ListMachinesInNamespace gets all the nodes in a given namespace.
func (h *Headscale) ListMachinesInNamespace(name string) ([]Machine, error) { func (h *Headscale) ListMachinesInNamespace(name string) ([]Machine, error) {
err := CheckForFQDNRules(name) err := CheckForFQDNRules(name)

256
oidc.go
View File

@@ -13,7 +13,7 @@ import (
"time" "time"
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
"github.com/gin-gonic/gin" "github.com/gorilla/mux"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"tailscale.com/types/key" "tailscale.com/types/key"
@@ -63,10 +63,17 @@ 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/:mKey.
func (h *Headscale) RegisterOIDC(ctx *gin.Context) { func (h *Headscale) RegisterOIDC(
machineKeyStr := ctx.Param("mkey") writer http.ResponseWriter,
if machineKeyStr == "" { req *http.Request,
ctx.String(http.StatusBadRequest, "Wrong params") ) {
vars := mux.Vars(req)
machineKeyStr, ok := vars["mkey"]
if !ok || machineKeyStr == "" {
log.Error().
Caller().
Msg("Missing machine key in URL")
http.Error(writer, "Missing machine key in URL", http.StatusBadRequest)
return return
} }
@@ -81,7 +88,7 @@ func (h *Headscale) RegisterOIDC(ctx *gin.Context) {
log.Error(). log.Error().
Caller(). Caller().
Msg("could not read 16 bytes from rand") Msg("could not read 16 bytes from rand")
ctx.String(http.StatusInternalServerError, "could not read 16 bytes from rand") http.Error(writer, "Internal server error", http.StatusInternalServerError)
return return
} }
@@ -101,7 +108,7 @@ func (h *Headscale) RegisterOIDC(ctx *gin.Context) {
authURL := h.oauth2Config.AuthCodeURL(stateStr, extras...) authURL := h.oauth2Config.AuthCodeURL(stateStr, extras...)
log.Debug().Msgf("Redirecting to %s for authentication", authURL) log.Debug().Msgf("Redirecting to %s for authentication", authURL)
ctx.Redirect(http.StatusFound, authURL) http.Redirect(writer, req, authURL, http.StatusFound)
} }
type oidcCallbackTemplateConfig struct { type oidcCallbackTemplateConfig struct {
@@ -125,12 +132,23 @@ var oidcCallbackTemplate = template.Must(
// 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.
func (h *Headscale) OIDCCallback(ctx *gin.Context) { func (h *Headscale) OIDCCallback(
code := ctx.Query("code") writer http.ResponseWriter,
state := ctx.Query("state") req *http.Request,
) {
code := req.URL.Query().Get("code")
state := req.URL.Query().Get("state")
if code == "" || state == "" { if code == "" || state == "" {
ctx.String(http.StatusBadRequest, "Wrong params") writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusBadRequest)
_, err := writer.Write([]byte("Wrong params"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return return
} }
@@ -141,7 +159,15 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
Err(err). Err(err).
Caller(). Caller().
Msg("Could not exchange code for token") Msg("Could not exchange code for token")
ctx.String(http.StatusBadRequest, "Could not exchange code for token") writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusBadRequest)
_, err := writer.Write([]byte("Could not exchange code for token"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return return
} }
@@ -154,7 +180,15 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
rawIDToken, rawIDTokenOK := oauth2Token.Extra("id_token").(string) rawIDToken, rawIDTokenOK := oauth2Token.Extra("id_token").(string)
if !rawIDTokenOK { if !rawIDTokenOK {
ctx.String(http.StatusBadRequest, "Could not extract ID Token") writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusBadRequest)
_, err := writer.Write([]byte("Could not extract ID Token"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return return
} }
@@ -167,7 +201,15 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
Err(err). Err(err).
Caller(). Caller().
Msg("failed to verify id token") Msg("failed to verify id token")
ctx.String(http.StatusBadRequest, "Failed to verify id token") writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusBadRequest)
_, err := writer.Write([]byte("Failed to verify id token"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return return
} }
@@ -186,10 +228,15 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
Err(err). Err(err).
Caller(). Caller().
Msg("Failed to decode id token claims") Msg("Failed to decode id token claims")
ctx.String( writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
http.StatusBadRequest, writer.WriteHeader(http.StatusBadRequest)
"Failed to decode id token claims", _, err := writer.Write([]byte("Failed to decode id token claims"))
) if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return return
} }
@@ -199,10 +246,15 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
if at := strings.LastIndex(claims.Email, "@"); at < 0 || if at := strings.LastIndex(claims.Email, "@"); at < 0 ||
!IsStringInSlice(h.cfg.OIDC.AllowedDomains, claims.Email[at+1:]) { !IsStringInSlice(h.cfg.OIDC.AllowedDomains, claims.Email[at+1:]) {
log.Error().Msg("authenticated principal does not match any allowed domain") log.Error().Msg("authenticated principal does not match any allowed domain")
ctx.String( writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
http.StatusBadRequest, writer.WriteHeader(http.StatusBadRequest)
"unauthorized principal (domain mismatch)", _, err := writer.Write([]byte("unauthorized principal (domain mismatch)"))
) if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return return
} }
@@ -212,7 +264,15 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
if len(h.cfg.OIDC.AllowedUsers) > 0 && if len(h.cfg.OIDC.AllowedUsers) > 0 &&
!IsStringInSlice(h.cfg.OIDC.AllowedUsers, claims.Email) { !IsStringInSlice(h.cfg.OIDC.AllowedUsers, claims.Email) {
log.Error().Msg("authenticated principal does not match any allowed user") log.Error().Msg("authenticated principal does not match any allowed user")
ctx.String(http.StatusBadRequest, "unauthorized principal (user mismatch)") writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusBadRequest)
_, err := writer.Write([]byte("unauthorized principal (user mismatch)"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return return
} }
@@ -223,7 +283,15 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
if !machineKeyFound { if !machineKeyFound {
log.Error(). log.Error().
Msg("requested machine state key expired before authorisation completed") Msg("requested machine state key expired before authorisation completed")
ctx.String(http.StatusBadRequest, "state has expired") writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusBadRequest)
_, err := writer.Write([]byte("state has expired"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return return
} }
@@ -237,17 +305,30 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
if err != nil { if err != nil {
log.Error(). log.Error().
Msg("could not parse machine public key") Msg("could not parse machine public key")
ctx.String(http.StatusBadRequest, "could not parse public key") writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusBadRequest)
_, err := writer.Write([]byte("could not parse public key"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return return
} }
if !machineKeyOK { if !machineKeyOK {
log.Error().Msg("could not get machine key from cache") log.Error().Msg("could not get machine key from cache")
ctx.String( writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
http.StatusInternalServerError, writer.WriteHeader(http.StatusInternalServerError)
"could not get machine key from cache", _, err := writer.Write([]byte("could not get machine key from cache"))
) if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return return
} }
@@ -264,7 +345,16 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
Str("machine", machine.Hostname). Str("machine", machine.Hostname).
Msg("machine already registered, reauthenticating") Msg("machine already registered, reauthenticating")
h.RefreshMachine(machine, time.Time{}) err := h.RefreshMachine(machine, time.Time{})
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to refresh machine")
http.Error(writer, "Failed to refresh machine", http.StatusInternalServerError)
return
}
var content bytes.Buffer var content bytes.Buffer
if err := oidcCallbackTemplate.Execute(&content, oidcCallbackTemplateConfig{ if err := oidcCallbackTemplate.Execute(&content, oidcCallbackTemplateConfig{
@@ -276,14 +366,29 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
Str("type", "reauthenticate"). Str("type", "reauthenticate").
Err(err). Err(err).
Msg("Could not render OIDC callback template") Msg("Could not render OIDC callback template")
ctx.Data(
http.StatusInternalServerError, writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
"text/html; charset=utf-8", writer.WriteHeader(http.StatusInternalServerError)
[]byte("Could not render OIDC callback template"), _, err := writer.Write([]byte("Could not render OIDC callback template"))
) if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return
} }
ctx.Data(http.StatusOK, "text/html; charset=utf-8", content.Bytes()) writer.Header().Set("Content-Type", "text/html; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, err = writer.Write(content.Bytes())
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return return
} }
@@ -294,10 +399,15 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
) )
if err != nil { if err != nil {
log.Error().Err(err).Caller().Msgf("couldn't normalize email") log.Error().Err(err).Caller().Msgf("couldn't normalize email")
ctx.String( writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
http.StatusInternalServerError, writer.WriteHeader(http.StatusInternalServerError)
"couldn't normalize email", _, err := writer.Write([]byte("couldn't normalize email"))
) if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return return
} }
@@ -314,10 +424,15 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
Err(err). Err(err).
Caller(). Caller().
Msgf("could not create new namespace '%s'", namespaceName) Msgf("could not create new namespace '%s'", namespaceName)
ctx.String( writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
http.StatusInternalServerError, writer.WriteHeader(http.StatusInternalServerError)
"could not create new namespace", _, err := writer.Write([]byte("could not create namespace"))
) if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return return
} }
@@ -327,10 +442,15 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
Err(err). Err(err).
Str("namespace", namespaceName). Str("namespace", namespaceName).
Msg("could not find or create namespace") Msg("could not find or create namespace")
ctx.String( writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
http.StatusInternalServerError, writer.WriteHeader(http.StatusInternalServerError)
"could not find or create namespace", _, err := writer.Write([]byte("could not find or create namespace"))
) if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return return
} }
@@ -347,10 +467,15 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
Caller(). Caller().
Err(err). Err(err).
Msg("could not register machine") Msg("could not register machine")
ctx.String( writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
http.StatusInternalServerError, writer.WriteHeader(http.StatusInternalServerError)
"could not register machine", _, err := writer.Write([]byte("could not register machine"))
) if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return return
} }
@@ -365,12 +490,27 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
Str("type", "authenticate"). Str("type", "authenticate").
Err(err). Err(err).
Msg("Could not render OIDC callback template") Msg("Could not render OIDC callback template")
ctx.Data(
http.StatusInternalServerError, writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
"text/html; charset=utf-8", writer.WriteHeader(http.StatusInternalServerError)
[]byte("Could not render OIDC callback template"), _, err := writer.Write([]byte("Could not render OIDC callback template"))
) if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return
} }
ctx.Data(http.StatusOK, "text/html; charset=utf-8", content.Bytes()) writer.Header().Set("Content-Type", "text/html; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, err = writer.Write(content.Bytes())
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
} }

View File

@@ -6,13 +6,16 @@ import (
"net/http" "net/http"
textTemplate "text/template" textTemplate "text/template"
"github.com/gin-gonic/gin"
"github.com/gofrs/uuid" "github.com/gofrs/uuid"
"github.com/gorilla/mux"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
// WindowsConfigMessage shows a simple message in the browser for how to configure the Windows Tailscale client. // WindowsConfigMessage shows a simple message in the browser for how to configure the Windows Tailscale client.
func (h *Headscale) WindowsConfigMessage(ctx *gin.Context) { func (h *Headscale) WindowsConfigMessage(
writer http.ResponseWriter,
req *http.Request,
) {
winTemplate := template.Must(template.New("windows").Parse(` winTemplate := template.Must(template.New("windows").Parse(`
<html> <html>
<body> <body>
@@ -63,20 +66,36 @@ REG ADD "HKLM\Software\Tailscale IPN" /v LoginURL /t REG_SZ /d "{{.URL}}"</code>
Str("handler", "WindowsRegConfig"). Str("handler", "WindowsRegConfig").
Err(err). Err(err).
Msg("Could not render Windows index template") Msg("Could not render Windows index template")
ctx.Data(
http.StatusInternalServerError, writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
"text/html; charset=utf-8", writer.WriteHeader(http.StatusInternalServerError)
[]byte("Could not render Windows index template"), _, err := writer.Write([]byte("Could not render Windows index template"))
) if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return return
} }
ctx.Data(http.StatusOK, "text/html; charset=utf-8", payload.Bytes()) writer.Header().Set("Content-Type", "text/html; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, err := writer.Write(payload.Bytes())
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
} }
// WindowsRegConfig generates and serves a .reg file configured with the Headscale server address. // WindowsRegConfig generates and serves a .reg file configured with the Headscale server address.
func (h *Headscale) WindowsRegConfig(ctx *gin.Context) { func (h *Headscale) WindowsRegConfig(
writer http.ResponseWriter,
req *http.Request,
) {
config := WindowsRegistryConfig{ config := WindowsRegistryConfig{
URL: h.cfg.ServerURL, URL: h.cfg.ServerURL,
} }
@@ -87,24 +106,36 @@ func (h *Headscale) WindowsRegConfig(ctx *gin.Context) {
Str("handler", "WindowsRegConfig"). Str("handler", "WindowsRegConfig").
Err(err). Err(err).
Msg("Could not render Apple macOS template") Msg("Could not render Apple macOS template")
ctx.Data(
http.StatusInternalServerError, writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
"text/html; charset=utf-8", writer.WriteHeader(http.StatusInternalServerError)
[]byte("Could not render Windows registry template"), _, err := writer.Write([]byte("Could not render Windows registry template"))
) if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return return
} }
ctx.Data( writer.Header().Set("Content-Type", "text/x-ms-regedit; charset=utf-8")
http.StatusOK, writer.WriteHeader(http.StatusOK)
"text/x-ms-regedit; charset=utf-8", _, err := writer.Write(content.Bytes())
content.Bytes(), if err != nil {
) log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
} }
// AppleConfigMessage shows a simple message in the browser to point the user to the iOS/MacOS profile and instructions for how to install it. // AppleConfigMessage shows a simple message in the browser to point the user to the iOS/MacOS profile and instructions for how to install it.
func (h *Headscale) AppleConfigMessage(ctx *gin.Context) { func (h *Headscale) AppleConfigMessage(
writer http.ResponseWriter,
req *http.Request,
) {
appleTemplate := template.Must(template.New("apple").Parse(` appleTemplate := template.Must(template.New("apple").Parse(`
<html> <html>
<body> <body>
@@ -165,20 +196,45 @@ func (h *Headscale) AppleConfigMessage(ctx *gin.Context) {
Str("handler", "AppleMobileConfig"). Str("handler", "AppleMobileConfig").
Err(err). Err(err).
Msg("Could not render Apple index template") Msg("Could not render Apple index template")
ctx.Data(
http.StatusInternalServerError, writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
"text/html; charset=utf-8", writer.WriteHeader(http.StatusInternalServerError)
[]byte("Could not render Apple index template"), _, err := writer.Write([]byte("Could not render Apple index template"))
) if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return return
} }
ctx.Data(http.StatusOK, "text/html; charset=utf-8", payload.Bytes()) writer.Header().Set("Content-Type", "text/html; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, err := writer.Write(payload.Bytes())
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
} }
func (h *Headscale) ApplePlatformConfig(ctx *gin.Context) { func (h *Headscale) ApplePlatformConfig(
platform := ctx.Param("platform") writer http.ResponseWriter,
req *http.Request,
) {
vars := mux.Vars(req)
platform, ok := vars["platform"]
if !ok {
log.Error().
Str("handler", "ApplePlatformConfig").
Msg("No platform specified")
http.Error(writer, "No platform specified", http.StatusBadRequest)
return
}
id, err := uuid.NewV4() id, err := uuid.NewV4()
if err != nil { if err != nil {
@@ -186,11 +242,16 @@ func (h *Headscale) ApplePlatformConfig(ctx *gin.Context) {
Str("handler", "ApplePlatformConfig"). Str("handler", "ApplePlatformConfig").
Err(err). Err(err).
Msg("Failed not create UUID") Msg("Failed not create UUID")
ctx.Data(
http.StatusInternalServerError, writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
"text/html; charset=utf-8", writer.WriteHeader(http.StatusInternalServerError)
[]byte("Failed to create UUID"), _, err := writer.Write([]byte("Failed to create UUID"))
) if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return return
} }
@@ -201,11 +262,16 @@ func (h *Headscale) ApplePlatformConfig(ctx *gin.Context) {
Str("handler", "ApplePlatformConfig"). Str("handler", "ApplePlatformConfig").
Err(err). Err(err).
Msg("Failed not create UUID") Msg("Failed not create UUID")
ctx.Data(
http.StatusInternalServerError, writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
"text/html; charset=utf-8", writer.WriteHeader(http.StatusInternalServerError)
[]byte("Failed to create UUID"), _, err := writer.Write([]byte("Failed to create content UUID"))
) if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return return
} }
@@ -224,11 +290,16 @@ func (h *Headscale) ApplePlatformConfig(ctx *gin.Context) {
Str("handler", "ApplePlatformConfig"). Str("handler", "ApplePlatformConfig").
Err(err). Err(err).
Msg("Could not render Apple macOS template") Msg("Could not render Apple macOS template")
ctx.Data(
http.StatusInternalServerError, writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
"text/html; charset=utf-8", writer.WriteHeader(http.StatusInternalServerError)
[]byte("Could not render Apple macOS template"), _, err := writer.Write([]byte("Could not render Apple macOS template"))
) if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return return
} }
@@ -238,20 +309,29 @@ func (h *Headscale) ApplePlatformConfig(ctx *gin.Context) {
Str("handler", "ApplePlatformConfig"). Str("handler", "ApplePlatformConfig").
Err(err). Err(err).
Msg("Could not render Apple iOS template") Msg("Could not render Apple iOS template")
ctx.Data(
http.StatusInternalServerError, writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
"text/html; charset=utf-8", writer.WriteHeader(http.StatusInternalServerError)
[]byte("Could not render Apple iOS template"), _, err := writer.Write([]byte("Could not render Apple iOS template"))
) if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return return
} }
default: default:
ctx.Data( writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
http.StatusOK, writer.WriteHeader(http.StatusBadRequest)
"text/html; charset=utf-8", _, err := writer.Write([]byte("Invalid platform, only ios and macos is supported"))
[]byte("Invalid platform, only ios and macos is supported"), if err != nil {
) log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return return
} }
@@ -268,20 +348,29 @@ func (h *Headscale) ApplePlatformConfig(ctx *gin.Context) {
Str("handler", "ApplePlatformConfig"). Str("handler", "ApplePlatformConfig").
Err(err). Err(err).
Msg("Could not render Apple platform template") Msg("Could not render Apple platform template")
ctx.Data(
http.StatusInternalServerError, writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
"text/html; charset=utf-8", writer.WriteHeader(http.StatusInternalServerError)
[]byte("Could not render Apple platform template"), _, err := writer.Write([]byte("Could not render Apple platform template"))
) if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return return
} }
ctx.Data( writer.Header().Set("Content-Type", "application/x-apple-aspen-config; charset=utf-8")
http.StatusOK, writer.WriteHeader(http.StatusOK)
"application/x-apple-aspen-config; charset=utf-8", _, err = writer.Write(content.Bytes())
content.Bytes(), if err != nil {
) log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
} }
type WindowsRegistryConfig struct { type WindowsRegistryConfig struct {

279
poll.go
View File

@@ -8,7 +8,7 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gorilla/mux"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gorm.io/gorm" "gorm.io/gorm"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
@@ -16,8 +16,7 @@ import (
) )
const ( const (
keepAliveInterval = 60 * time.Second keepAliveInterval = 60 * time.Second
updateCheckInterval = 10 * time.Second
) )
type contextKey string type contextKey string
@@ -33,13 +32,25 @@ const machineNameContextKey = contextKey("machineName")
// only after their first request (marked with the ReadOnly field). // 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. // At this moment the updates are sent in a quite horrendous way, but they kinda work.
func (h *Headscale) PollNetMapHandler(ctx *gin.Context) { func (h *Headscale) PollNetMapHandler(
writer http.ResponseWriter,
req *http.Request,
) {
vars := mux.Vars(req)
machineKeyStr, ok := vars["mkey"]
if !ok || machineKeyStr == "" {
log.Error().
Str("handler", "PollNetMap").
Msg("No machine key in request")
http.Error(writer, "No machine key in request", http.StatusBadRequest)
return
}
log.Trace(). log.Trace().
Str("handler", "PollNetMap"). Str("handler", "PollNetMap").
Str("id", ctx.Param("id")). Str("id", machineKeyStr).
Msg("PollNetMapHandler called") Msg("PollNetMapHandler called")
body, _ := io.ReadAll(ctx.Request.Body) body, _ := io.ReadAll(req.Body)
machineKeyStr := ctx.Param("id")
var machineKey key.MachinePublic var machineKey key.MachinePublic
err := machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machineKeyStr))) err := machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machineKeyStr)))
@@ -48,18 +59,19 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
Str("handler", "PollNetMap"). Str("handler", "PollNetMap").
Err(err). Err(err).
Msg("Cannot parse client key") Msg("Cannot parse client key")
ctx.String(http.StatusBadRequest, "")
http.Error(writer, "Cannot parse client key", http.StatusBadRequest)
return return
} }
req := tailcfg.MapRequest{} mapRequest := tailcfg.MapRequest{}
err = decode(body, &req, &machineKey, h.privateKey) err = decode(body, &mapRequest, &machineKey, h.privateKey)
if err != nil { if err != nil {
log.Error(). log.Error().
Str("handler", "PollNetMap"). Str("handler", "PollNetMap").
Err(err). Err(err).
Msg("Cannot decode message") Msg("Cannot decode message")
ctx.String(http.StatusBadRequest, "") http.Error(writer, "Cannot decode message", http.StatusBadRequest)
return return
} }
@@ -70,26 +82,27 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
log.Warn(). log.Warn().
Str("handler", "PollNetMap"). Str("handler", "PollNetMap").
Msgf("Ignoring request, cannot find machine with key %s", machineKey.String()) Msgf("Ignoring request, cannot find machine with key %s", machineKey.String())
ctx.String(http.StatusUnauthorized, "")
http.Error(writer, "", http.StatusUnauthorized)
return return
} }
log.Error(). log.Error().
Str("handler", "PollNetMap"). Str("handler", "PollNetMap").
Msgf("Failed to fetch machine from the database with Machine key: %s", machineKey.String()) Msgf("Failed to fetch machine from the database with Machine key: %s", machineKey.String())
ctx.String(http.StatusInternalServerError, "") http.Error(writer, "", http.StatusInternalServerError)
return return
} }
log.Trace(). log.Trace().
Str("handler", "PollNetMap"). Str("handler", "PollNetMap").
Str("id", ctx.Param("id")). Str("id", machineKeyStr).
Str("machine", machine.Hostname). Str("machine", machine.Hostname).
Msg("Found machine in database") Msg("Found machine in database")
machine.Hostname = req.Hostinfo.Hostname machine.Hostname = mapRequest.Hostinfo.Hostname
machine.HostInfo = HostInfo(*req.Hostinfo) machine.HostInfo = HostInfo(*mapRequest.Hostinfo)
machine.DiscoKey = DiscoPublicKeyStripPrefix(req.DiscoKey) machine.DiscoKey = DiscoPublicKeyStripPrefix(mapRequest.DiscoKey)
now := time.Now().UTC() now := time.Now().UTC()
// update ACLRules with peer informations (to update server tags if necessary) // update ACLRules with peer informations (to update server tags if necessary)
@@ -111,8 +124,8 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
// //
// The intended use is for clients to discover the DERP map at start-up // The intended use is for clients to discover the DERP map at start-up
// before their first real endpoint update. // before their first real endpoint update.
if !req.ReadOnly { if !mapRequest.ReadOnly {
machine.Endpoints = req.Endpoints machine.Endpoints = mapRequest.Endpoints
machine.LastSeen = &now machine.LastSeen = &now
} }
@@ -120,25 +133,25 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
if err != nil { if err != nil {
log.Error(). log.Error().
Str("handler", "PollNetMap"). Str("handler", "PollNetMap").
Str("id", ctx.Param("id")). Str("id", machineKeyStr).
Str("machine", machine.Hostname). Str("machine", machine.Hostname).
Err(err). Err(err).
Msg("Failed to persist/update machine in the database") Msg("Failed to persist/update machine in the database")
ctx.String(http.StatusInternalServerError, ":(") http.Error(writer, "", http.StatusInternalServerError)
return return
} }
} }
data, err := h.getMapResponse(machineKey, req, machine) data, err := h.getMapResponse(machineKey, mapRequest, machine)
if err != nil { if err != nil {
log.Error(). log.Error().
Str("handler", "PollNetMap"). Str("handler", "PollNetMap").
Str("id", ctx.Param("id")). Str("id", machineKeyStr).
Str("machine", machine.Hostname). Str("machine", machine.Hostname).
Err(err). Err(err).
Msg("Failed to get Map response") Msg("Failed to get Map response")
ctx.String(http.StatusInternalServerError, ":(") http.Error(writer, "", http.StatusInternalServerError)
return return
} }
@@ -150,19 +163,28 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
// Details on the protocol can be found in https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L696 // Details on the protocol can be found in https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L696
log.Debug(). log.Debug().
Str("handler", "PollNetMap"). Str("handler", "PollNetMap").
Str("id", ctx.Param("id")). Str("id", machineKeyStr).
Str("machine", machine.Hostname). Str("machine", machine.Hostname).
Bool("readOnly", req.ReadOnly). Bool("readOnly", mapRequest.ReadOnly).
Bool("omitPeers", req.OmitPeers). Bool("omitPeers", mapRequest.OmitPeers).
Bool("stream", req.Stream). Bool("stream", mapRequest.Stream).
Msg("Client map request processed") Msg("Client map request processed")
if req.ReadOnly { if mapRequest.ReadOnly {
log.Info(). log.Info().
Str("handler", "PollNetMap"). Str("handler", "PollNetMap").
Str("machine", machine.Hostname). Str("machine", machine.Hostname).
Msg("Client is starting up. Probably interested in a DERP map") Msg("Client is starting up. Probably interested in a DERP map")
ctx.Data(http.StatusOK, "application/json; charset=utf-8", data)
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, err := writer.Write(data)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return return
} }
@@ -177,7 +199,7 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
// Only create update channel if it has not been created // Only create update channel if it has not been created
log.Trace(). log.Trace().
Str("handler", "PollNetMap"). Str("handler", "PollNetMap").
Str("id", ctx.Param("id")). Str("id", machineKeyStr).
Str("machine", machine.Hostname). Str("machine", machine.Hostname).
Msg("Loading or creating update channel") Msg("Loading or creating update channel")
@@ -189,13 +211,20 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
keepAliveChan := make(chan []byte) keepAliveChan := make(chan []byte)
if req.OmitPeers && !req.Stream { if mapRequest.OmitPeers && !mapRequest.Stream {
log.Info(). log.Info().
Str("handler", "PollNetMap"). Str("handler", "PollNetMap").
Str("machine", machine.Hostname). Str("machine", machine.Hostname).
Msg("Client sent endpoint update and is ok with a response without peer list") Msg("Client sent endpoint update and is ok with a response without peer list")
ctx.Data(http.StatusOK, "application/json; charset=utf-8", data) writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, err := writer.Write(data)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
// It sounds like we should update the nodes when we have received a endpoint update // 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. // even tho the comments in the tailscale code dont explicitly say so.
updateRequestsFromNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "endpoint-update"). updateRequestsFromNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "endpoint-update").
@@ -203,12 +232,12 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
updateChan <- struct{}{} updateChan <- struct{}{}
return return
} else if req.OmitPeers && req.Stream { } else if mapRequest.OmitPeers && mapRequest.Stream {
log.Warn(). log.Warn().
Str("handler", "PollNetMap"). Str("handler", "PollNetMap").
Str("machine", machine.Hostname). Str("machine", machine.Hostname).
Msg("Ignoring request, don't know how to handle it") Msg("Ignoring request, don't know how to handle it")
ctx.String(http.StatusBadRequest, "") http.Error(writer, "", http.StatusBadRequest)
return return
} }
@@ -232,9 +261,10 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
updateChan <- struct{}{} updateChan <- struct{}{}
h.PollNetMapStream( h.PollNetMapStream(
ctx, writer,
machine,
req, req,
machine,
mapRequest,
machineKey, machineKey,
pollDataChan, pollDataChan,
keepAliveChan, keepAliveChan,
@@ -242,7 +272,7 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
) )
log.Trace(). log.Trace().
Str("handler", "PollNetMap"). Str("handler", "PollNetMap").
Str("id", ctx.Param("id")). Str("id", machineKeyStr).
Str("machine", machine.Hostname). Str("machine", machine.Hostname).
Msg("Finished stream, closing PollNetMap session") Msg("Finished stream, closing PollNetMap session")
} }
@@ -251,7 +281,8 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
// 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(
ctx *gin.Context, writer http.ResponseWriter,
req *http.Request,
machine *Machine, machine *Machine,
mapRequest tailcfg.MapRequest, mapRequest tailcfg.MapRequest,
machineKey key.MachinePublic, machineKey key.MachinePublic,
@@ -259,51 +290,31 @@ func (h *Headscale) PollNetMapStream(
keepAliveChan chan []byte, keepAliveChan chan []byte,
updateChan chan struct{}, updateChan chan struct{},
) { ) {
{ ctx := context.WithValue(req.Context(), machineNameContextKey, machine.Hostname)
machine, err := h.GetMachineByMachineKey(machineKey)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
log.Warn().
Str("handler", "PollNetMap").
Msgf("Ignoring request, cannot find machine with key %s", machineKey.String())
ctx.String(http.StatusUnauthorized, "")
return ctx, cancel := context.WithCancel(ctx)
} defer cancel()
log.Error().
Str("handler", "PollNetMap").
Msgf("Failed to fetch machine from the database with Machine key: %s", machineKey.String())
ctx.String(http.StatusInternalServerError, "")
return go h.scheduledPollWorker(
} ctx,
updateChan,
keepAliveChan,
machineKey,
mapRequest,
machine,
)
ctx := context.WithValue(ctx.Request.Context(), machineNameContextKey, machine.Hostname) log.Trace().
Str("handler", "PollNetMapStream").
Str("machine", machine.Hostname).
Msg("Waiting for data to stream...")
ctx, cancel := context.WithCancel(ctx) log.Trace().
defer cancel() Str("handler", "PollNetMapStream").
Str("machine", machine.Hostname).
go h.scheduledPollWorker( Msgf("pollData is %#v, keepAliveChan is %#v, updateChan is %#v", pollDataChan, keepAliveChan, updateChan)
ctx,
updateChan,
keepAliveChan,
machineKey,
mapRequest,
machine,
)
}
ctx.Stream(func(writer io.Writer) bool {
log.Trace().
Str("handler", "PollNetMapStream").
Str("machine", machine.Hostname).
Msg("Waiting for data to stream...")
log.Trace().
Str("handler", "PollNetMapStream").
Str("machine", machine.Hostname).
Msgf("pollData is %#v, keepAliveChan is %#v, updateChan is %#v", pollDataChan, keepAliveChan, updateChan)
for {
select { select {
case data := <-pollDataChan: case data := <-pollDataChan:
log.Trace(). log.Trace().
@@ -321,8 +332,21 @@ func (h *Headscale) PollNetMapStream(
Err(err). Err(err).
Msg("Cannot write data") Msg("Cannot write data")
return false return
} }
flusher, ok := writer.(http.Flusher)
if !ok {
log.Error().
Caller().
Str("handler", "PollNetMapStream").
Str("machine", machine.Hostname).
Str("channel", "pollData").
Msg("Cannot cast writer to http.Flusher")
} else {
flusher.Flush()
}
log.Trace(). log.Trace().
Str("handler", "PollNetMapStream"). Str("handler", "PollNetMapStream").
Str("machine", machine.Hostname). Str("machine", machine.Hostname).
@@ -343,7 +367,7 @@ func (h *Headscale) PollNetMapStream(
// client has been removed from database // client has been removed from database
// since the stream opened, terminate connection. // since the stream opened, terminate connection.
return false return
} }
now := time.Now().UTC() now := time.Now().UTC()
machine.LastSeen = &now machine.LastSeen = &now
@@ -360,16 +384,16 @@ func (h *Headscale) PollNetMapStream(
Str("channel", "pollData"). Str("channel", "pollData").
Err(err). Err(err).
Msg("Cannot update machine LastSuccessfulUpdate") Msg("Cannot update machine LastSuccessfulUpdate")
} else {
log.Trace(). return
Str("handler", "PollNetMapStream").
Str("machine", machine.Hostname).
Str("channel", "pollData").
Int("bytes", len(data)).
Msg("Machine entry in database updated successfully after sending pollData")
} }
return true log.Trace().
Str("handler", "PollNetMapStream").
Str("machine", machine.Hostname).
Str("channel", "pollData").
Int("bytes", len(data)).
Msg("Machine entry in database updated successfully after sending data")
case data := <-keepAliveChan: case data := <-keepAliveChan:
log.Trace(). log.Trace().
@@ -387,8 +411,20 @@ func (h *Headscale) PollNetMapStream(
Err(err). Err(err).
Msg("Cannot write keep alive message") Msg("Cannot write keep alive message")
return false return
} }
flusher, ok := writer.(http.Flusher)
if !ok {
log.Error().
Caller().
Str("handler", "PollNetMapStream").
Str("machine", machine.Hostname).
Str("channel", "keepAlive").
Msg("Cannot cast writer to http.Flusher")
} else {
flusher.Flush()
}
log.Trace(). log.Trace().
Str("handler", "PollNetMapStream"). Str("handler", "PollNetMapStream").
Str("machine", machine.Hostname). Str("machine", machine.Hostname).
@@ -409,7 +445,7 @@ func (h *Headscale) PollNetMapStream(
// client has been removed from database // client has been removed from database
// since the stream opened, terminate connection. // since the stream opened, terminate connection.
return false return
} }
now := time.Now().UTC() now := time.Now().UTC()
machine.LastSeen = &now machine.LastSeen = &now
@@ -421,16 +457,16 @@ func (h *Headscale) PollNetMapStream(
Str("channel", "keepAlive"). Str("channel", "keepAlive").
Err(err). Err(err).
Msg("Cannot update machine LastSeen") Msg("Cannot update machine LastSeen")
} else {
log.Trace(). return
Str("handler", "PollNetMapStream").
Str("machine", machine.Hostname).
Str("channel", "keepAlive").
Int("bytes", len(data)).
Msg("Machine updated successfully after sending keep alive")
} }
return true log.Trace().
Str("handler", "PollNetMapStream").
Str("machine", machine.Hostname).
Str("channel", "keepAlive").
Int("bytes", len(data)).
Msg("Machine updated successfully after sending keep alive")
case <-updateChan: case <-updateChan:
log.Trace(). log.Trace().
@@ -440,6 +476,7 @@ func (h *Headscale) PollNetMapStream(
Msg("Received a request for update") Msg("Received a request for update")
updateRequestsReceivedOnChannel.WithLabelValues(machine.Namespace.Name, machine.Hostname). updateRequestsReceivedOnChannel.WithLabelValues(machine.Namespace.Name, machine.Hostname).
Inc() Inc()
if h.isOutdated(machine) { if h.isOutdated(machine) {
var lastUpdate time.Time var lastUpdate time.Time
if machine.LastSuccessfulUpdate != nil { if machine.LastSuccessfulUpdate != nil {
@@ -459,6 +496,8 @@ func (h *Headscale) PollNetMapStream(
Str("channel", "update"). Str("channel", "update").
Err(err). Err(err).
Msg("Could not get the map update") Msg("Could not get the map update")
return
} }
_, err = writer.Write(data) _, err = writer.Write(data)
if err != nil { if err != nil {
@@ -471,8 +510,21 @@ func (h *Headscale) PollNetMapStream(
updateRequestsSentToNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "failed"). updateRequestsSentToNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "failed").
Inc() Inc()
return false return
} }
flusher, ok := writer.(http.Flusher)
if !ok {
log.Error().
Caller().
Str("handler", "PollNetMapStream").
Str("machine", machine.Hostname).
Str("channel", "update").
Msg("Cannot cast writer to http.Flusher")
} else {
flusher.Flush()
}
log.Trace(). log.Trace().
Str("handler", "PollNetMapStream"). Str("handler", "PollNetMapStream").
Str("machine", machine.Hostname). Str("machine", machine.Hostname).
@@ -499,7 +551,7 @@ func (h *Headscale) PollNetMapStream(
// client has been removed from database // client has been removed from database
// since the stream opened, terminate connection. // since the stream opened, terminate connection.
return false return
} }
now := time.Now().UTC() now := time.Now().UTC()
@@ -515,6 +567,8 @@ func (h *Headscale) PollNetMapStream(
Str("channel", "update"). Str("channel", "update").
Err(err). Err(err).
Msg("Cannot update machine LastSuccessfulUpdate") Msg("Cannot update machine LastSuccessfulUpdate")
return
} }
} else { } else {
var lastUpdate time.Time var lastUpdate time.Time
@@ -529,9 +583,7 @@ func (h *Headscale) PollNetMapStream(
Msgf("%s is up to date", machine.Hostname) Msgf("%s is up to date", machine.Hostname)
} }
return true case <-ctx.Done():
case <-ctx.Request.Context().Done():
log.Info(). log.Info().
Str("handler", "PollNetMapStream"). Str("handler", "PollNetMapStream").
Str("machine", machine.Hostname). Str("machine", machine.Hostname).
@@ -550,7 +602,7 @@ func (h *Headscale) PollNetMapStream(
// client has been removed from database // client has been removed from database
// since the stream opened, terminate connection. // since the stream opened, terminate connection.
return false return
} }
now := time.Now().UTC() now := time.Now().UTC()
machine.LastSeen = &now machine.LastSeen = &now
@@ -564,9 +616,18 @@ func (h *Headscale) PollNetMapStream(
Msg("Cannot update machine LastSeen") Msg("Cannot update machine LastSeen")
} }
return false // The connection has been closed, so we can stop polling.
return
case <-h.shutdownChan:
log.Info().
Str("handler", "PollNetMapStream").
Str("machine", machine.Hostname).
Msg("The long-poll handler is shutting down")
return
} }
}) }
} }
func (h *Headscale) scheduledPollWorker( func (h *Headscale) scheduledPollWorker(
@@ -578,7 +639,7 @@ func (h *Headscale) scheduledPollWorker(
machine *Machine, machine *Machine,
) { ) {
keepAliveTicker := time.NewTicker(keepAliveInterval) keepAliveTicker := time.NewTicker(keepAliveInterval)
updateCheckerTicker := time.NewTicker(updateCheckInterval) updateCheckerTicker := time.NewTicker(h.cfg.NodeUpdateCheckInterval)
defer closeChanWithLog( defer closeChanWithLog(
updateChan, updateChan,

View File

@@ -28,7 +28,7 @@ func (s *Suite) TestGetRoutes(c *check.C) {
MachineKey: "foo", MachineKey: "foo",
NodeKey: "bar", NodeKey: "bar",
DiscoKey: "faa", DiscoKey: "faa",
Hostname: "test_get_route_machine", Hostname: "test_get_route_machine",
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
@@ -79,7 +79,7 @@ func (s *Suite) TestGetEnableRoutes(c *check.C) {
MachineKey: "foo", MachineKey: "foo",
NodeKey: "bar", NodeKey: "bar",
DiscoKey: "faa", DiscoKey: "faa",
Hostname: "test_enable_route_machine", Hostname: "test_enable_route_machine",
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),

View File

@@ -6,14 +6,16 @@ import (
"html/template" "html/template"
"net/http" "net/http"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
//go:embed gen/openapiv2/headscale/v1/headscale.swagger.json //go:embed gen/openapiv2/headscale/v1/headscale.swagger.json
var apiV1JSON []byte var apiV1JSON []byte
func SwaggerUI(ctx *gin.Context) { func SwaggerUI(
writer http.ResponseWriter,
req *http.Request,
) {
swaggerTemplate := template.Must(template.New("swagger").Parse(` swaggerTemplate := template.Must(template.New("swagger").Parse(`
<html> <html>
<head> <head>
@@ -52,18 +54,41 @@ func SwaggerUI(ctx *gin.Context) {
Caller(). Caller().
Err(err). Err(err).
Msg("Could not render Swagger") Msg("Could not render Swagger")
ctx.Data(
http.StatusInternalServerError, writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
"text/html; charset=utf-8", writer.WriteHeader(http.StatusInternalServerError)
[]byte("Could not render Swagger"), _, err := writer.Write([]byte("Could not render Swagger"))
) if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return return
} }
ctx.Data(http.StatusOK, "text/html; charset=utf-8", payload.Bytes()) writer.Header().Set("Content-Type", "text/html; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, err := writer.Write(payload.Bytes())
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
} }
func SwaggerAPIv1(ctx *gin.Context) { func SwaggerAPIv1(
ctx.Data(http.StatusOK, "application/json; charset=utf-8", apiV1JSON) writer http.ResponseWriter,
req *http.Request,
) {
writer.Header().Set("Content-Type", "application/json; charset=utf-88")
writer.WriteHeader(http.StatusOK)
if _, err := writer.Write(apiV1JSON); err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
} }

View File

@@ -1,6 +1,6 @@
{ {
// Declare static groups of users beyond those in the identity service. // Declare static groups of users beyond those in the identity service.
"Groups": { "groups": {
"group:example": [ "group:example": [
"user1@example.com", "user1@example.com",
"user2@example.com", "user2@example.com",
@@ -11,12 +11,12 @@
], ],
}, },
// Declare hostname aliases to use in place of IP addresses or subnets. // Declare hostname aliases to use in place of IP addresses or subnets.
"Hosts": { "hosts": {
"example-host-1": "100.100.100.100", "example-host-1": "100.100.100.100",
"example-host-2": "100.100.101.100/24", "example-host-2": "100.100.101.100/24",
}, },
// Define who is allowed to use which tags. // Define who is allowed to use which tags.
"TagOwners": { "tagOwners": {
// Everyone in the montreal-admins or global-admins group are // Everyone in the montreal-admins or global-admins group are
// allowed to tag servers as montreal-webserver. // allowed to tag servers as montreal-webserver.
"tag:montreal-webserver": [ "tag:montreal-webserver": [
@@ -29,17 +29,17 @@
], ],
}, },
// Access control lists. // Access control lists.
"ACLs": [ "acls": [
// Engineering users, plus the president, can access port 22 (ssh) // Engineering users, plus the president, can access port 22 (ssh)
// and port 3389 (remote desktop protocol) on all servers, and all // and port 3389 (remote desktop protocol) on all servers, and all
// ports on git-server or ci-server. // ports on git-server or ci-server.
{ {
"Action": "accept", "action": "accept",
"Users": [ "src": [
"group:example2", "group:example2",
"192.168.1.0/24" "192.168.1.0/24"
], ],
"Ports": [ "dst": [
"*:22,3389", "*:22,3389",
"git-server:*", "git-server:*",
"ci-server:*" "ci-server:*"
@@ -48,22 +48,22 @@
// Allow engineer users to access any port on a device tagged with // Allow engineer users to access any port on a device tagged with
// tag:production. // tag:production.
{ {
"Action": "accept", "action": "accept",
"Users": [ "src": [
"group:example" "group:example"
], ],
"Ports": [ "dst": [
"tag:production:*" "tag:production:*"
], ],
}, },
// Allow servers in the my-subnet host and 192.168.1.0/24 to access hosts // Allow servers in the my-subnet host and 192.168.1.0/24 to access hosts
// on both networks. // on both networks.
{ {
"Action": "accept", "action": "accept",
"Users": [ "src": [
"example-host-2", "example-host-2",
], ],
"Ports": [ "dst": [
"example-host-1:*", "example-host-1:*",
"192.168.1.0/24:*" "192.168.1.0/24:*"
], ],
@@ -72,22 +72,22 @@
// Comment out this section if you want to define specific ACL // Comment out this section if you want to define specific ACL
// restrictions above. // restrictions above.
{ {
"Action": "accept", "action": "accept",
"Users": [ "src": [
"*" "*"
], ],
"Ports": [ "dst": [
"*:*" "*:*"
], ],
}, },
// All users in Montreal are allowed to access the Montreal web // All users in Montreal are allowed to access the Montreal web
// servers. // servers.
{ {
"Action": "accept", "action": "accept",
"Users": [ "src": [
"example-host-1" "example-host-1"
], ],
"Ports": [ "dst": [
"tag:montreal-webserver:80,443" "tag:montreal-webserver:80,443"
], ],
}, },
@@ -96,30 +96,30 @@
// In contrast, this doesn't grant API servers the right to initiate // In contrast, this doesn't grant API servers the right to initiate
// any connections. // any connections.
{ {
"Action": "accept", "action": "accept",
"Users": [ "src": [
"tag:montreal-webserver" "tag:montreal-webserver"
], ],
"Ports": [ "dst": [
"tag:api-server:443" "tag:api-server:443"
], ],
}, },
], ],
// Declare tests to check functionality of ACL rules // Declare tests to check functionality of ACL rules
"Tests": [ "tests": [
{ {
"User": "user1@example.com", "src": "user1@example.com",
"Allow": [ "accept": [
"example-host-1:22", "example-host-1:22",
"example-host-2:80" "example-host-2:80"
], ],
"Deny": [ "deny": [
"exapmle-host-2:100" "exapmle-host-2:100"
], ],
}, },
{ {
"User": "user2@example.com", "src": "user2@example.com",
"Allow": [ "accept": [
"100.60.3.4:22" "100.60.3.4:22"
], ],
}, },

View File

@@ -3,19 +3,19 @@
{ {
"Hosts": { "hosts": {
"host-1": "100.100.100.100", "host-1": "100.100.100.100",
"subnet-1": "100.100.101.100/24", "subnet-1": "100.100.101.100/24",
}, },
"ACLs": [ "acls": [
{ {
"Action": "accept", "action": "accept",
"Users": [ "src": [
"subnet-1", "subnet-1",
"192.168.1.0/24" "192.168.1.0/24"
], ],
"Ports": [ "dst": [
"*:22,3389", "*:22,3389",
"host-1:*", "host-1:*",
], ],

View File

@@ -1,24 +1,24 @@
// This ACL is used to test group expansion // This ACL is used to test group expansion
{ {
"Groups": { "groups": {
"group:example": [ "group:example": [
"testnamespace", "testnamespace",
], ],
}, },
"Hosts": { "hosts": {
"host-1": "100.100.100.100", "host-1": "100.100.100.100",
"subnet-1": "100.100.101.100/24", "subnet-1": "100.100.101.100/24",
}, },
"ACLs": [ "acls": [
{ {
"Action": "accept", "action": "accept",
"Users": [ "src": [
"group:example", "group:example",
], ],
"Ports": [ "dst": [
"host-1:*", "host-1:*",
], ],
}, },

View File

@@ -1,18 +1,18 @@
// This ACL is used to test namespace expansion // This ACL is used to test namespace expansion
{ {
"Hosts": { "hosts": {
"host-1": "100.100.100.100", "host-1": "100.100.100.100",
"subnet-1": "100.100.101.100/24", "subnet-1": "100.100.101.100/24",
}, },
"ACLs": [ "acls": [
{ {
"Action": "accept", "action": "accept",
"Users": [ "src": [
"testnamespace", "testnamespace",
], ],
"Ports": [ "dst": [
"host-1:*", "host-1:*",
], ],
}, },

View File

@@ -0,0 +1,41 @@
// This ACL is used to test wildcards
{
"hosts": {
"host-1": "100.100.100.100",
"subnet-1": "100.100.101.100/24",
},
"acls": [
{
"Action": "accept",
"src": [
"*",
],
"proto": "tcp",
"dst": [
"host-1:*",
],
},
{
"Action": "accept",
"src": [
"*",
],
"proto": "udp",
"dst": [
"host-1:53",
],
},
{
"Action": "accept",
"src": [
"*",
],
"proto": "icmp",
"dst": [
"host-1:*",
],
},
],
}

View File

@@ -1,18 +1,18 @@
// This ACL is used to test the port range expansion // This ACL is used to test the port range expansion
{ {
"Hosts": { "hosts": {
"host-1": "100.100.100.100", "host-1": "100.100.100.100",
"subnet-1": "100.100.101.100/24", "subnet-1": "100.100.101.100/24",
}, },
"ACLs": [ "acls": [
{ {
"Action": "accept", "action": "accept",
"Users": [ "src": [
"subnet-1", "subnet-1",
], ],
"Ports": [ "dst": [
"host-1:5400-5500", "host-1:5400-5500",
], ],
}, },

View File

@@ -1,18 +1,18 @@
// This ACL is used to test wildcards // This ACL is used to test wildcards
{ {
"Hosts": { "hosts": {
"host-1": "100.100.100.100", "host-1": "100.100.100.100",
"subnet-1": "100.100.101.100/24", "subnet-1": "100.100.101.100/24",
}, },
"ACLs": [ "acls": [
{ {
"Action": "accept", "Action": "accept",
"Users": [ "src": [
"*", "*",
], ],
"Ports": [ "dst": [
"host-1:*", "host-1:*",
], ],
}, },

View File

@@ -1,10 +1,10 @@
--- ---
Hosts: hosts:
host-1: 100.100.100.100/32 host-1: 100.100.100.100/32
subnet-1: 100.100.101.100/24 subnet-1: 100.100.101.100/24
ACLs: acls:
- Action: accept - action: accept
Users: src:
- "*" - "*"
Ports: dst:
- host-1:* - host-1:*

View File

@@ -1,18 +1,18 @@
{ {
// Declare static groups of users beyond those in the identity service. // Declare static groups of users beyond those in the identity service.
"Groups": { "groups": {
"group:example": [ "group:example": [
"user1@example.com", "user1@example.com",
"user2@example.com", "user2@example.com",
], ],
}, },
// Declare hostname aliases to use in place of IP addresses or subnets. // Declare hostname aliases to use in place of IP addresses or subnets.
"Hosts": { "hosts": {
"example-host-1": "100.100.100.100", "example-host-1": "100.100.100.100",
"example-host-2": "100.100.101.100/24", "example-host-2": "100.100.101.100/24",
}, },
// Define who is allowed to use which tags. // Define who is allowed to use which tags.
"TagOwners": { "tagOwners": {
// Everyone in the montreal-admins or global-admins group are // Everyone in the montreal-admins or global-admins group are
// allowed to tag servers as montreal-webserver. // allowed to tag servers as montreal-webserver.
"tag:montreal-webserver": [ "tag:montreal-webserver": [
@@ -26,17 +26,17 @@
], ],
}, },
// Access control lists. // Access control lists.
"ACLs": [ "acls": [
// Engineering users, plus the president, can access port 22 (ssh) // Engineering users, plus the president, can access port 22 (ssh)
// and port 3389 (remote desktop protocol) on all servers, and all // and port 3389 (remote desktop protocol) on all servers, and all
// ports on git-server or ci-server. // ports on git-server or ci-server.
{ {
"Action": "accept", "action": "accept",
"Users": [ "src": [
"group:engineering", "group:engineering",
"president@example.com" "president@example.com"
], ],
"Ports": [ "dst": [
"*:22,3389", "*:22,3389",
"git-server:*", "git-server:*",
"ci-server:*" "ci-server:*"
@@ -45,23 +45,23 @@
// Allow engineer users to access any port on a device tagged with // Allow engineer users to access any port on a device tagged with
// tag:production. // tag:production.
{ {
"Action": "accept", "action": "accept",
"Users": [ "src": [
"group:engineers" "group:engineers"
], ],
"Ports": [ "dst": [
"tag:production:*" "tag:production:*"
], ],
}, },
// Allow servers in the my-subnet host and 192.168.1.0/24 to access hosts // Allow servers in the my-subnet host and 192.168.1.0/24 to access hosts
// on both networks. // on both networks.
{ {
"Action": "accept", "action": "accept",
"Users": [ "src": [
"my-subnet", "my-subnet",
"192.168.1.0/24" "192.168.1.0/24"
], ],
"Ports": [ "dst": [
"my-subnet:*", "my-subnet:*",
"192.168.1.0/24:*" "192.168.1.0/24:*"
], ],
@@ -70,22 +70,22 @@
// Comment out this section if you want to define specific ACL // Comment out this section if you want to define specific ACL
// restrictions above. // restrictions above.
{ {
"Action": "accept", "action": "accept",
"Users": [ "src": [
"*" "*"
], ],
"Ports": [ "dst": [
"*:*" "*:*"
], ],
}, },
// All users in Montreal are allowed to access the Montreal web // All users in Montreal are allowed to access the Montreal web
// servers. // servers.
{ {
"Action": "accept", "action": "accept",
"Users": [ "src": [
"group:montreal-users" "group:montreal-users"
], ],
"Ports": [ "dst": [
"tag:montreal-webserver:80,443" "tag:montreal-webserver:80,443"
], ],
}, },
@@ -94,30 +94,30 @@
// In contrast, this doesn't grant API servers the right to initiate // In contrast, this doesn't grant API servers the right to initiate
// any connections. // any connections.
{ {
"Action": "accept", "action": "accept",
"Users": [ "src": [
"tag:montreal-webserver" "tag:montreal-webserver"
], ],
"Ports": [ "dst": [
"tag:api-server:443" "tag:api-server:443"
], ],
}, },
], ],
// Declare tests to check functionality of ACL rules // Declare tests to check functionality of ACL rules
"Tests": [ "tests": [
{ {
"User": "user1@example.com", "src": "user1@example.com",
"Allow": [ "accept": [
"example-host-1:22", "example-host-1:22",
"example-host-2:80" "example-host-2:80"
], ],
"Deny": [ "deny": [
"exapmle-host-2:100" "exapmle-host-2:100"
], ],
}, },
{ {
"User": "user2@example.com", "src": "user2@example.com",
"Allow": [ "accept": [
"100.60.3.4:22" "100.60.3.4:22"
], ],
}, },

View File

@@ -324,12 +324,18 @@ func GenerateRandomStringURLSafe(n int) (string, error) {
// It will return an error if the system's secure random // It will return an error if the system's secure random
// number generator fails to function correctly, in which // number generator fails to function correctly, in which
// case the caller should not continue. // case the caller should not continue.
func GenerateRandomStringDNSSafe(n int) (string, error) { func GenerateRandomStringDNSSafe(size int) (string, error) {
str, err := GenerateRandomStringURLSafe(n) var str string
var err error
for len(str) < size {
str, err = GenerateRandomStringURLSafe(size)
if err != nil {
return "", err
}
str = strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(str, "_", ""), "-", ""))
}
str = strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(str, "_", ""), "-", "")) return str[:size], nil
return str[:n], err
} }
func IsStringInSlice(slice []string, str string) bool { func IsStringInSlice(slice []string, str string) bool {

View File

@@ -34,7 +34,7 @@ func (s *Suite) TestGetUsedIps(c *check.C) {
MachineKey: "foo", MachineKey: "foo",
NodeKey: "bar", NodeKey: "bar",
DiscoKey: "faa", DiscoKey: "faa",
Hostname: "testmachine", Hostname: "testmachine",
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
@@ -82,7 +82,7 @@ func (s *Suite) TestGetMultiIp(c *check.C) {
MachineKey: "foo", MachineKey: "foo",
NodeKey: "bar", NodeKey: "bar",
DiscoKey: "faa", DiscoKey: "faa",
Hostname: "testmachine", Hostname: "testmachine",
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
@@ -172,7 +172,7 @@ func (s *Suite) TestGetAvailableIpMachineWithoutIP(c *check.C) {
MachineKey: "foo", MachineKey: "foo",
NodeKey: "bar", NodeKey: "bar",
DiscoKey: "faa", DiscoKey: "faa",
Hostname: "testmachine", Hostname: "testmachine",
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
@@ -185,3 +185,15 @@ func (s *Suite) TestGetAvailableIpMachineWithoutIP(c *check.C) {
c.Assert(len(ips2), check.Equals, 1) c.Assert(len(ips2), check.Equals, 1)
c.Assert(ips2[0].String(), check.Equals, expected.String()) c.Assert(ips2[0].String(), check.Equals, expected.String())
} }
func (s *Suite) TestGenerateRandomStringDNSSafe(c *check.C) {
for i := 0; i < 100000; i++ {
str, err := GenerateRandomStringDNSSafe(8)
if err != nil {
c.Error(err)
}
if len(str) != 8 {
c.Error("invalid length", len(str), str)
}
}
}