Compare commits

...

756 Commits

Author SHA1 Message Date
Juan Font Alonso
5539ef1f8f Reduce the number of containers 2022-08-04 21:36:38 +02:00
Juan Font Alonso
100f7190f3 Temporary fix integration tests with dedicated Dockerfile 2022-07-31 10:44:09 +02:00
Juan Font
f9c4d577e2 Merge pull request #680 from ohdearaugustin/topic/speedup-build
Topic/speedup build
2022-07-28 23:07:32 +02:00
Juan Font
9826b518bd Merge branch 'main' into topic/speedup-build 2022-07-28 22:58:07 +02:00
Juan Font
32a8f06486 Merge pull request #689 from restanrm/fix-duplicate-tags-returned-by-api
Remove duplicate tags if sent by the client
2022-07-28 22:52:35 +02:00
Juan Font
2ab2b8656b Merge branch 'main' into fix-duplicate-tags-returned-by-api 2022-07-27 00:37:07 +02:00
Juan Font
d9ab98e47f Merge branch 'main' into topic/speedup-build 2022-07-26 18:45:41 +02:00
Juan Font
9d584bb0d3 Merge pull request #692 from juanfont/update-runc-dependencies
Update runc dependencies to fix security notification
2022-07-26 17:20:51 +02:00
Juan Font
4f725ba9e1 Merge branch 'main' into update-runc-dependencies 2022-07-26 13:59:35 +02:00
Juan Font
b75a113c91 Merge pull request #688 from juanfont/prepare-cl-0.17.0
Prepare changelog structure for 0.17.0
2022-07-26 13:59:24 +02:00
Juan Font Alonso
75af83bb81 Update checksum for nix 2022-07-26 12:11:15 +02:00
Juan Font
0f6f0c3b6b Merge branch 'main' into prepare-cl-0.17.0 2022-07-26 12:05:28 +02:00
Juan Font Alonso
b344524a6d Update runc dependencies to fix security notification 2022-07-26 12:02:58 +02:00
Juan Font Alonso
6f4d5a532e fixed linting errors 2022-07-26 11:25:20 +02:00
Juan Font
2d83c70173 Merge pull request #670 from iSchluff/feature/db-health-check
ping db in health check
2022-07-26 00:40:23 +02:00
Adrien Raffin-Caboisse
c90e862460 fix(grpc): add more checks for tag validation 2022-07-25 14:01:41 +02:00
Adrien Raffin-Caboisse
c46a34e6b8 fix(machine): remove duplicate in forcedTags 2022-07-25 11:04:30 +02:00
Juan Font Alonso
693f59ba2f Prepare changelog structure for 0.17.0 2022-07-25 10:35:21 +02:00
Juan Font
abae078855 Merge branch 'main' into feature/db-health-check 2022-07-24 22:10:16 +02:00
Juan Font
0212db3fad Merge pull request #687 from huskyii/node_ls
more intuitive output of node ls
2022-07-24 12:06:41 +02:00
Jiang Zhu
49354f678e update CHANGELOG 2022-07-23 04:47:37 +08:00
Jiang Zhu
dc94570c4a more intuitive output of node ls 2022-07-23 01:33:11 +08:00
Kristoffer Dalby
51b1027aec Merge pull request #686 from juanfont/update-contributors 2022-07-22 18:56:49 +02:00
github-actions[bot]
936adb7d2c docs(README): update contributors 2022-07-22 07:36:16 +00:00
Juan Font
581d1f3bfa Merge pull request #668 from GrigoriyMikhalkin/graceful-shutdown
graceful shutdown fix
2022-07-22 09:35:40 +02:00
Juan Font
7c87ef6c86 Merge branch 'main' into graceful-shutdown 2022-07-22 09:06:46 +02:00
Juan Font
1a9a9b718d Merge pull request #684 from juanfont/fix-api-mux
Fix API router
2022-07-22 09:06:06 +02:00
Juan Font Alonso
6c9f3420e2 Updated changelog 2022-07-21 23:59:44 +02:00
Juan Font Alonso
a4d0efbe8d Fix API router 2022-07-21 23:57:07 +02:00
Grigoriy Mikhalkin
56858a56db Revert "decompose OIDCCallback method"
This reverts commit 395caaad42.
2022-07-21 23:54:35 +02:00
Grigoriy Mikhalkin
395caaad42 decompose OIDCCallback method 2022-07-21 23:47:20 +02:00
Grigoriy Mikhalkin
3f0639c87d graceful shutdown lint fixes 2022-07-21 23:47:20 +02:00
Grigoriy Mikhalkin
889eff265f graceful shutdown fix 2022-07-21 23:47:20 +02:00
Kristoffer Dalby
c6eb7be7fb Merge pull request #683 from juanfont/update-contributors 2022-07-20 10:57:38 +02:00
github-actions[bot]
02c7a46b97 docs(README): update contributors 2022-07-20 07:21:19 +00:00
Kristoffer Dalby
ea7b3baa8b Merge pull request #677 from huskyii/remove_gin 2022-07-20 09:20:24 +02:00
Jiang Zhu
5724f4607c fix nix build 2022-07-19 20:45:32 +08:00
Jiang Zhu
b755d47652 update CHANGELOG 2022-07-19 20:45:23 +08:00
ohdearaugustin
96221cc4f7 docs: add bulding container docs 2022-07-17 21:18:04 +02:00
ohdearaugustin
34d261179e Speedup docker container build 2022-07-17 21:18:04 +02:00
ohdearaugustin
091b05f155 Change build os 2022-07-17 21:18:04 +02:00
Jiang Zhu
aca5646032 remove gin completely, ~2MB reduction on final binary 2022-07-16 02:03:46 +08:00
Kristoffer Dalby
7e9abbeaec Merge pull request #676 from juanfont/update-contributors 2022-07-15 09:15:18 +01:00
Anton Schubert
c6aaa37f2d ping db in health check 2022-07-12 22:56:53 +02:00
github-actions[bot]
b8c3387892 docs(README): update contributors 2022-07-12 11:35:28 +00:00
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
Kristoffer Dalby
b0acbed329 Merge pull request #608 from kradalby/config-rework 2022-06-05 20:18:20 +02:00
Kristoffer Dalby
1b2967320b Merge branch 'main' into config-rework 2022-06-05 17:57:15 +02:00
Kristoffer Dalby
90f6be0c98 Rename one char var 2022-06-05 17:52:28 +02:00
Kristoffer Dalby
78ed610b50 Switch config to pointer 2022-06-05 17:47:26 +02:00
Kristoffer Dalby
af891808f6 Make get config load the config, use config in main method 2022-06-05 17:47:12 +02: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
Kristoffer Dalby
adb55bcfe9 Merge pull request #610 from huskyii/fix_pie_build 2022-06-04 12:37:02 +02:00
Jiang Zhu
2201ec8905 some GOOS do not support pie build, detect in makefile and fall back to non-pie build 2022-06-04 18:11:10 +08:00
Kristoffer Dalby
39f6fdef1a Merge pull request #609 from kradalby/add-arm64-darwin-drop-32 2022-06-04 11:42:51 +02:00
Kristoffer Dalby
699aa5cf38 Merge branch 'main' into add-arm64-darwin-drop-32 2022-06-03 19:36:01 +02:00
Kristoffer Dalby
1486adb25a Update changelog 2022-06-03 17:35:47 +00:00
Kristoffer Dalby
2653c2f5e8 Drop arm32 (armhf) for linux and add Darwin arm64 2022-06-03 17:34:06 +00:00
Kristoffer Dalby
7b7244dac2 Merge pull request #607 from juanfont/update-contributors 2022-06-03 11:01:07 +02:00
github-actions[bot]
571ce2b0b9 docs(README): update contributors 2022-06-03 08:49:19 +00:00
Kristoffer Dalby
c3db5ed749 Merge remote-tracking branch 'upstream/main' into config-rework 2022-06-03 10:49:08 +02:00
Kristoffer Dalby
0797148076 Merge pull request #601 from kradalby/signals-reload-acl 2022-06-03 10:48:43 +02:00
Kristoffer Dalby
24c9530eee Add loglevel and disable update to config struct 2022-06-03 10:37:45 +02:00
Kristoffer Dalby
679cf7c0d7 Merge branch 'main' into signals-reload-acl 2022-06-03 10:23:53 +02:00
Kristoffer Dalby
19b6405332 Merge pull request #597 from kradalby/db-error-handling 2022-06-03 10:23:21 +02:00
Kristoffer Dalby
aee8aa1c61 Move TLS config into its own struct 2022-06-03 10:14:14 +02:00
Kristoffer Dalby
5514a862dc Update headscale read config tests 2022-06-03 09:26:46 +02:00
Kristoffer Dalby
1ea8bb782c Move all read config logic to config.go 2022-06-03 09:26:36 +02:00
Kristoffer Dalby
35722cd5aa Move FilePerm function from cli to headscale 2022-06-03 09:24:36 +02:00
Kristoffer Dalby
533ecee252 Move config struct to its own file 2022-06-03 09:05:41 +02:00
Kristoffer Dalby
f1db2d0c8e Merge branch 'main' into signals-reload-acl 2022-06-02 11:12:15 +02:00
Kristoffer Dalby
6f6fb4dcd6 Merge branch 'main' into db-error-handling 2022-06-02 11:11:58 +02:00
Kristoffer Dalby
b1ba7ba685 Merge pull request #602 from iSchluff/fix/forced-tags-with-tagOwner 2022-06-02 11:11:43 +02:00
Anton Schubert
6dccfee862 Fix forced Tags with legitimate tagOwners
Also replace loops contains
2022-06-01 15:43:32 +02:00
Kristoffer Dalby
6f32b80b2b Update changelog 2022-05-31 14:30:11 +02:00
Kristoffer Dalby
2feed18b28 Support reloading ACLs with SIGHUP
Also continously listen for signals, not just once.
2022-05-31 14:28:23 +02:00
Kristoffer Dalby
36dca3516a Move Abspath function to headscale utils 2022-05-31 14:28:23 +02:00
Kristoffer Dalby
06129277ed Rename abspath function to describe what it does 2022-05-31 14:28:23 +02:00
Kristoffer Dalby
6b1482daee Use config object instead of viper for policy path 2022-05-31 14:28:23 +02:00
Kristoffer Dalby
24e4787a64 Make ACL policy part of the config struct 2022-05-31 14:28:23 +02:00
Kristoffer Dalby
5bfae22c8f Make config get function global 2022-05-31 14:28:23 +02:00
Kristoffer Dalby
3e078f0494 Fix logtail config function name 2022-05-31 14:28:01 +02:00
Kristoffer Dalby
0b4f59b82b Improve signal handling
This commit starts to wire up better signal handling, it starts with
handling shutdown a bit better, using the graceful shutdown for all the
listeners we use.

It also adds the initial switch case for handling config and acl reload,
which is to be implemented.
2022-05-31 14:28:00 +02:00
Kristoffer Dalby
a19af04582 Fix errors introduced by merge 2022-05-31 11:03:08 +02:00
Kristoffer Dalby
0676aa11a9 Merge branch 'main' into db-error-handling 2022-05-31 10:18:13 +02:00
Kristoffer Dalby
be25bbce92 Merge pull request #560 from kradalby/rename-fixess 2022-05-31 10:14:48 +02:00
Kristoffer Dalby
5ecfbbaf5d Fix pointer in machine save call 2022-05-31 10:05:00 +02:00
Kristoffer Dalby
7f7cd737dc Merge branch 'main' into rename-fixess 2022-05-31 09:45:49 +02:00
Kristoffer Dalby
b472e5a689 Merge pull request #599 from kradalby/parse-duration-improv 2022-05-31 09:45:36 +02:00
Kristoffer Dalby
25c674ed32 Merge branch 'main' into parse-duration-improv 2022-05-31 09:43:18 +02:00
Kristoffer Dalby
3d93cf9e2d Update changelog 2022-05-31 09:42:50 +02:00
Kristoffer Dalby
f7edea5f40 Merge branch 'main' into rename-fixess 2022-05-31 09:42:20 +02:00
Kristoffer Dalby
d26e220fb9 Merge pull request #598 from kradalby/parse-duration-improv 2022-05-31 09:42:00 +02:00
Kristoffer Dalby
d860270733 Use Prometheus duration parser (support days and weeks) 2022-05-30 16:10:39 +02:00
Kristoffer Dalby
a09633e859 Check errors of more database calls 2022-05-30 15:39:24 +02:00
Kristoffer Dalby
a1837a4d69 Merge branch 'main' into db-error-handling 2022-05-30 15:31:56 +02:00
Kristoffer Dalby
52cc3bc8eb Check all errors for db.Save 2022-05-30 15:31:06 +02:00
Kristoffer Dalby
9175aca094 Merge branch 'main' into rename-fixess 2022-05-30 15:29:50 +02:00
Kristoffer Dalby
848727a21d Merge pull request #596 from kradalby/disable-logcatcher 2022-05-30 15:29:35 +02:00
Kristoffer Dalby
df7d5fa2b9 Fix lint 2022-05-30 14:58:40 +02:00
Kristoffer Dalby
86dfc91dd5 update readme 2022-05-30 14:57:49 +02:00
Kristoffer Dalby
7f66d9184b Add config test 2022-05-30 14:57:43 +02:00
Kristoffer Dalby
ff5f31b87e Disable logtail for clients 2022-05-30 14:52:50 +02:00
Kristoffer Dalby
a0c465c2eb Wire up setting to enable/disable logtail 2022-05-30 14:47:41 +02:00
Kristoffer Dalby
d11279e615 Merge branch 'main' into rename-fixess 2022-05-30 13:38:28 +02:00
Kristoffer Dalby
266aac9e61 Update CHANGELOG 2022-05-30 13:35:40 +02:00
Kristoffer Dalby
4ffd3eacb0 Override golangci-lint to use go 1.17 2022-05-30 13:35:40 +02:00
Kristoffer Dalby
a443255b3e Validate isOutdated against all namespaces
This commit makes isOutdated validate a nodes necessity to update
against all namespaces, and not just the nodes own namespace (which made
more sense before).

getLastStateChange is now uses the passed namespaces as a filter,
meaning that not requesting any namespace will give you the total last
updated state.

In addition, the sync.Map is exchanged for a variant that uses generics
which allows us to remove some casting logic.
2022-05-30 13:35:40 +02:00
Kristoffer Dalby
a992840c9b Give UpdateMachine a more meaningful name 2022-05-30 13:35:40 +02:00
Kristoffer Dalby
dbc1d981c9 Revert golines 2022-05-30 13:35:28 +02:00
github-actions[bot]
9993f51b5e docs(README): update contributors 2022-05-30 13:35:28 +02:00
Kristoffer Dalby
3a3fc0a4be Update headscale checksum 2022-05-30 12:03:16 +02:00
Kristoffer Dalby
5316dd9c27 Use new nix stable (22.05) 2022-05-30 11:59:22 +02:00
Kristoffer Dalby
59a1a85a2b Change to a go generics set implementation, no more casting 🎉 2022-05-30 11:49:35 +02:00
Kristoffer Dalby
fc502e1e79 Update golines and fix go mod checksum 2022-05-30 11:13:31 +02:00
Kristoffer Dalby
405de9e0f8 Merge pull request #595 from juanfont/update-contributors 2022-05-29 18:13:40 +01:00
Kristoffer Dalby
6eac5046c6 Merge branch 'main' into rename-fixess 2022-05-29 17:21:51 +01:00
github-actions[bot]
f7f722af52 docs(README): update contributors 2022-05-29 14:49:25 +00:00
Kristoffer Dalby
583f6eeedd Merge pull request #594 from juanfont/update-contributors 2022-05-29 15:48:41 +01:00
github-actions[bot]
bec35b4965 docs(README): update contributors 2022-05-29 11:03:33 +00:00
Kristoffer Dalby
e596d8287c Merge pull request #593 from juanfont/update-contributors 2022-05-29 12:02:49 +01:00
github-actions[bot]
6c903d2d93 docs(README): update contributors 2022-05-29 10:23:00 +00:00
Kristoffer Dalby
914431b94a Merge pull request #591 from pvinis/patch-2 2022-05-29 11:22:26 +01:00
Kristoffer Dalby
11da7436c7 Merge branch 'main' into patch-2 2022-05-29 11:19:29 +01:00
Kristoffer Dalby
0f532aa5c1 Merge pull request #590 from pvinis/patch-1 2022-05-29 11:19:14 +01:00
Pavlos Vinieratos
835828fe92 link fix 2022-05-28 20:48:59 +03:00
Pavlos Vinieratos
fff1011ed8 typo 2022-05-28 20:46:01 +03:00
Kristoffer Dalby
ef497caa1b Merge pull request #2 from juanfont/fix-rename-integration-tests 2022-05-28 17:14:41 +01:00
Juan Font Alonso
4f3f0542d4 Fix some issues in testing with new hostname handling 2022-05-28 12:54:57 +02:00
Kristoffer Dalby
5fa987519d move populate to after when given_name exist 2022-05-23 17:33:07 +01:00
Kristoffer Dalby
77ceeaf5fd Test magic dns with the correct urls 2022-05-18 21:18:04 +02:00
Kristoffer Dalby
4a9d3bedf9 Use new names to resolve magic dns 2022-05-18 20:14:18 +02:00
Kristoffer Dalby
802eb931d1 Make sure givenname is set for preauthkeys 2022-05-17 22:02:18 +02:00
Kristoffer Dalby
9ebeb3d7e4 Retreive hostnames from headscale, now that they are random 2022-05-17 22:11:51 +02:00
Kristoffer Dalby
e631c6f7e0 Merge master 2022-05-16 21:41:46 +02:00
Kristoffer Dalby
163e5c29e4 fix trace log message 2022-05-16 20:35:35 +02:00
Kristoffer Dalby
4aae917f74 Require GivenName to be unique 2022-05-16 20:33:47 +02:00
Kristoffer Dalby
9b393eb861 Add integration cli tests for rename command 2022-05-16 20:32:56 +02:00
Kristoffer Dalby
5fa3016703 Generate unique givennames for hosts joining (and debug added) 2022-05-16 20:32:38 +02:00
Kristoffer Dalby
03cccd60a6 Reword FQDN normalize errors to not _only_ cover namespaces 2022-05-16 20:31:32 +02:00
Kristoffer Dalby
177c21b294 Add helper function to create a unique givenname 2022-05-16 20:30:43 +02:00
Kristoffer Dalby
f4873d9387 Fix rename cli error 2022-05-16 20:29:31 +02:00
Kristoffer Dalby
747d64cdae Merge pull request #558 from restanrm/feat-list-tags-of-machines 2022-05-16 19:03:48 +01:00
Kristoffer Dalby
c9efd5c132 Merge branch 'main' into feat-list-tags-of-machines 2022-05-16 16:35:52 +01:00
Kristoffer Dalby
546ddd2a84 Merge pull request #510 from reynico/acls-doc 2022-05-16 16:32:51 +01:00
Kristoffer Dalby
2edb5428f9 Merge branch 'main' into acls-doc 2022-05-16 16:32:18 +01:00
Adrien Raffin-Caboisse
9f082125fa fix: remove version pinning for golangci-lint it does not work 2022-05-16 16:48:04 +02:00
Adrien Raffin-Caboisse
11582105ab fix: flake.nex update sha256 2022-05-16 15:26:00 +02:00
Adrien Raffin-Caboisse
c4e69fe2c3 fix: ignore exhaust linter 2022-05-16 15:13:16 +02:00
Adrien Raffin-Caboisse
4435a4f19d chore: apply lint recommendations 2022-05-16 14:59:46 +02:00
Adrien Raffin-Caboisse
02ae7a0563 fix: pin version of golangci-lint to match dev config 2022-05-16 14:40:45 +02:00
Adrien Raffin-Caboisse
852dc0f4de feat: add golangci-lint in nix develop 2022-05-16 14:40:05 +02:00
Adrien Raffin-Caboisse
844ad15109 fix: revert previous commit and add exclusion of linter 2022-05-16 14:29:05 +02:00
Adrien Raffin-Caboisse
522e892099 fix: remove unknown linters:
When running in CI, I obtained the following error:

```
  Running [/home/runner/golangci-lint-1.41.0-linux-amd64/golangci-lint run --out-format=github-actions --new-from-patch=/tmp/tmp-1795-28vaWZek2jfM/pull.patch --new=false --new-from-rev=] in [] ...
  level=error msg="Running error: unknown linters: 'ireturn,maintidx', run 'golangci-lint linters' to see the list of supported linters"
```
2022-05-16 14:17:31 +02:00
Adrien Raffin-Caboisse
0445f404ec fix: pin version of golangci-lint in GA 2022-05-16 14:12:49 +02:00
Adrien Raffin-Caboisse
bc1909fa22 Merge branch 'feat-list-tags-of-machines' of github.com:restanrm/headscale into feat-list-tags-of-machines 2022-05-16 11:17:36 +02:00
Adrien Raffin-Caboisse
ca71830963 docs: add small documentation on getTags func 2022-05-16 11:16:07 +02:00
Kristoffer Dalby
a28eebfca3 Merge branch 'main' into feat-list-tags-of-machines 2022-05-15 12:11:28 +01:00
Kristoffer Dalby
0d31ea08c3 Merge pull request #578 from samson4649/main 2022-05-15 11:58:40 +01:00
Samuel Lock
614c003704 updated changelog 2022-05-14 22:36:04 +10:00
Adrien Raffin-Caboisse
b511295349 fix: integration tests result
Execute command doesn't fail, the result is passed in json content.
2022-05-13 13:02:40 +02:00
Adrien Raffin-Caboisse
fcdc292647 fix: update tag in db if acl is enabled 2022-05-13 13:00:32 +02:00
Adrien Raffin-Caboisse
09836cd150 chore: update vendorSha after update of go.mod and go.sum 2022-05-13 12:19:32 +02:00
Adrien Raffin-Caboisse
49ec9943b9 fix: loop over result machines instead of startup machines 2022-05-13 12:14:11 +02:00
Adrien Raffin-Caboisse
72c1edaaa4 Merge remote-tracking branch 'origin/main' into feat-list-tags-of-machines 2022-05-13 11:56:06 +02:00
Adrien Raffin-Caboisse
294ed7a751 docs: update changelog 2022-05-13 11:51:31 +02:00
Adrien Raffin-Caboisse
31c0062d5e feat: add integration tests for tag support 2022-05-13 11:47:22 +02:00
Adrien Raffin-Caboisse
63d920510d feat: improve nodes list with inputs from @deonthomasgy
cf: https://github.com/juanfont/headscale/compare/main...deonthomasgy:dev/thomas/show-tags
2022-05-13 11:46:28 +02:00
Adrien Raffin-Caboisse
16f9691e80 fix: ignore emptyPolicy errors for db insertion 2022-05-13 11:20:40 +02:00
Adrien Raffin-Caboisse
209d003832 feat: handle insert into database error 2022-05-13 11:09:28 +02:00
Adrien Raffin-Caboisse
62cfd60e38 feat: add validation of tags 2022-05-13 10:18:01 +02:00
Adrien Raffin-Caboisse
fdbc9657bc feat: return error if validation is failed 2022-05-13 10:14:38 +02:00
Adrien Raffin-Caboisse
ad4401aa40 fix: remove debug code 2022-05-13 10:14:36 +02:00
Samuel Lock
c26280c331 modified code to satisfy golangci-lint and added integration test 2022-05-11 09:31:24 +10:00
Kristoffer Dalby
b028a7dfc9 Merge branch 'main' into main 2022-05-10 22:33:33 +01:00
Kristoffer Dalby
41cd0d30eb Merge pull request #576 from juanfont/update-contributors 2022-05-10 18:26:14 +01:00
Samuel Lock
8be9e9655c fixed issue #360 2022-05-10 20:51:14 +10:00
github-actions[bot]
31bdba7456 docs(README): update contributors 2022-05-08 21:57:04 +00:00
Kristoffer Dalby
d6e1d10b12 Merge pull request #573 from deonthomasgy/patch-1 2022-05-08 22:56:32 +01:00
Kristoffer Dalby
21268f7abe Merge branch 'main' into patch-1 2022-05-08 22:55:19 +01:00
Juan Font
91b95ff707 Merge pull request #574 from deonthomasgy/main
show ipv4 address first in node list
2022-05-08 23:32:25 +02:00
Deon Thomas
6ed79b7bb8 order Ip Address, IPv4 first, cleanup 2022-05-08 15:21:10 -04:00
Deon Thomas
b4f5ed6618 order ip address output, IPv4 first 2022-05-08 15:06:12 -04:00
Deon Thomas
ed46491a3d fixed typo
not => note
2022-05-07 16:23:26 -04:00
Adrien Raffin-Caboisse
dc8c20e002 fix: handle empty aclPolicy for integration tests 2022-05-04 22:56:55 +02:00
Adrien Raffin-Caboisse
68417cc888 fix(go): add missing updated files 2022-05-03 20:37:06 +02:00
Adrien Raffin-Caboisse
a2fb5b2b9d Merge remote-tracking branch 'origin/main' into feat-list-tags-of-machines 2022-05-03 20:35:28 +02:00
Kristoffer Dalby
3fbfc5a649 Merge pull request #570 from juanfont/update-contributors 2022-05-02 22:23:30 +01:00
github-actions[bot]
00535a2016 docs(README): update contributors 2022-05-02 21:12:01 +00:00
Kristoffer Dalby
fd452d52ca Merge pull request #565 from apognu/dev/oidc-custom-config 2022-05-02 22:11:14 +01:00
Antoine POPINEAU
7cc58af932 Allow more configuration over the OIDC flow.
Adds knobs to configure three aspects of the OpenID Connect flow:

 * Custom scopes to override the default "openid profile email".
 * Custom parameters to be added to the Authorize Endpoint request.
 * Domain allowlisting for authenticated principals.
 * User allowlisting for authenticated principals.
2022-05-02 17:11:07 +02:00
Kristoffer Dalby
ddb87af5ce Merge pull request #569 from Kazauwa/362-add-move-command 2022-05-02 12:38:00 +01:00
Igor Perepilitsyn
b9ea83fed8 check that new command does not break nodes list output 2022-05-02 15:04:24 +04:00
Igor Perepilitsyn
e279224484 add integrations tests 2022-05-02 14:00:09 +04:00
Igor Perepilitsyn
12d8f0f4b0 remove redundant lines of code, fix response when output is not plain text 2022-05-02 14:00:00 +04:00
Igor Perepilitsyn
6ba68d150c correctly update machine namespace 2022-05-02 13:58:28 +04:00
Igor Perepilitsyn
1b3a7bbf03 apply styling fixes 2022-05-02 08:32:33 +04:00
Igor Perepilitsyn
4e686f8b77 add unit test 2022-05-01 21:40:18 +04:00
Igor Perepilitsyn
62c780a448 update changelog 2022-05-01 17:56:02 +04:00
Igor Perepilitsyn
bc055edf12 add command for moving node between namespaces 2022-05-01 17:55:34 +04:00
Igor Perepilitsyn
47c72a4e2e add rpc method for moving node 2022-05-01 17:55:34 +04:00
Juan Font
02a78e5a45 Merge pull request #568 from juanfont/reduce-containers-int-tests
Reduce the number of containers in integration tests
2022-05-01 08:07:29 +02:00
Juan Font Alonso
01d9a2f589 Fixed linting issues 2022-04-30 23:48:28 +02:00
Juan Font
5403f215bc Reduce the number of containers in integration tests 2022-04-30 21:19:54 +00:00
Kristoffer Dalby
96e2955ba7 Merge pull request #566 from juanfont/fix-spurious-updates 2022-04-30 22:12:57 +01:00
Juan Font
03659c4175 Updated changelog 2022-04-30 14:50:55 +00:00
Juan Font
843e2bd9b6 Do not setLastStateChangeToNow every 5 seconds 2022-04-30 14:47:16 +00:00
Kristoffer Dalby
28efd92fca Merge pull request #559 from kradalby/update-deps 2022-04-28 10:17:54 +01:00
Kristoffer Dalby
7bb87a7300 Update vendor sha 2022-04-26 19:57:49 +00:00
Adrien Raffin-Caboisse
fec8cda16a fix: fix linting issue on my computer 2022-04-25 22:33:53 +02:00
Adrien Raffin-Caboisse
2c448d4a5c chore: apply linting 2022-04-25 22:27:44 +02:00
Adrien Raffin-Caboisse
3d302441b6 fix: order error in the tests 2022-04-25 22:17:23 +02:00
Adrien Raffin-Caboisse
8061abe279 refact: use generics for contains functions 2022-04-25 22:17:23 +02:00
Adrien Raffin-Caboisse
ea9aaa6022 feat: update functions to use set command 2022-04-25 22:17:23 +02:00
Adrien Raffin-Caboisse
cc9eeda889 feat: updating cli to match the set command 2022-04-25 22:17:23 +02:00
Adrien Raffin-Caboisse
25f1dcf724 feat: update generated files 2022-04-25 22:17:23 +02:00
Adrien Raffin-Caboisse
31debf7055 feat: rewrite proto to only update tags of machine 2022-04-25 22:17:23 +02:00
Kristoffer Dalby
db8db0299e Resolve merge 2022-04-25 16:58:06 +00:00
Juan Font
e80954b6c8 Merge pull request #482 from kradalby/flake-build-env
Add Nix reproducible build system
2022-04-25 18:38:44 +02:00
Kristoffer Dalby
8504d0d8ba Move todo to correct file 2022-04-24 21:12:45 +01:00
Kristoffer Dalby
7ef8cd881c Fix comment 2022-04-24 21:10:50 +01:00
Kristoffer Dalby
79704dc9b0 Update command with new fields 2022-04-24 20:57:15 +01:00
Kristoffer Dalby
06c928bc52 Migrate name and nickname fields 2022-04-24 20:56:42 +01:00
Kristoffer Dalby
62808cbd86 Bubble error up to user for rename 2022-04-24 20:56:28 +01:00
Kristoffer Dalby
14994cb6cc Use new logic and fields for dns 2022-04-24 20:55:54 +01:00
Kristoffer Dalby
6b79679cb4 Generate from proto 2022-04-24 20:55:20 +01:00
Kristoffer Dalby
caf79f6910 Change nickname to givenname in proto 2022-04-24 20:55:11 +01:00
Kristoffer Dalby
6e2768097a Rename name -> hostname, nickname -> givenname 2022-04-24 20:54:38 +01:00
Kristoffer Dalby
8845938881 Merge branch 'main' into main 2022-04-24 09:48:00 +02:00
Kristoffer Dalby
a23035aee7 update rest of deps 2022-04-24 07:21:50 +00:00
Kristoffer Dalby
e51e6f487f Resolve merge conflict 2022-04-23 08:31:57 +00:00
Kristoffer Dalby
f78deaebb6 Add new tailscale to integration tests 2022-04-23 08:30:13 +00:00
Kristoffer Dalby
4d2949bda9 Upgrade tailscale dep 2022-04-23 08:29:26 +00:00
Kristoffer Dalby
cb0899b534 Update vendor shar 2022-04-23 00:02:57 +02:00
Kristoffer Dalby
ecf5259693 resolve merge conflict 2022-04-22 22:34:20 +02:00
Kristoffer Dalby
3a90079ab8 Merge branch 'main' into feat-list-tags-of-machines 2022-04-22 22:27:36 +02:00
Kristoffer Dalby
970dea5d68 Merge pull request #557 from mpldr/remove-buf-installation 2022-04-22 08:16:03 +01:00
Kristoffer Dalby
cd9807a1d3 Merge branch 'main' into flake-build-env 2022-04-22 07:52:29 +01:00
Kristoffer Dalby
613dc61339 Merge branch 'main' into remove-buf-installation 2022-04-22 07:51:40 +01:00
Adrien Raffin-Caboisse
b9fee36f6e fix: linting 2022-04-21 23:56:58 +02:00
Adrien Raffin-Caboisse
17d6624bb9 chore: fix lint 2022-04-21 23:49:21 +02:00
Adrien Raffin-Caboisse
f53bb63b2d fix: move tag command to subcommand of nodes 2022-04-21 23:43:20 +02:00
Adrien Raffin-Caboisse
ea7bcfffbb Merge branch 'main' into feat-list-tags-of-machines 2022-04-21 20:38:51 +02:00
Moritz Poldrack
3023323528 remove necessary buf installation
This commit adds buf as a regular dependency and go running it instead
of requiring installing buf to one's own GOBIN.
2022-04-21 20:23:21 +02:00
Kristoffer Dalby
2dfd8a9098 Merge pull request #556 from juanfont/update-contributors 2022-04-21 12:53:14 +01:00
github-actions[bot]
c8ed1f0f43 docs(README): update contributors 2022-04-21 11:51:30 +00:00
Kristoffer Dalby
f9e2ce2c8c Merge pull request #551 from mpldr/patch-1 2022-04-21 12:50:50 +01:00
Moritz Poldrack
886e95c00d Merge branch 'main' into patch-1 2022-04-21 11:00:33 +02:00
Moritz Poldrack
6dd9e93346 expanded arguments in useradd to be easier to understand for beginners 2022-04-21 11:00:17 +02:00
Kristoffer Dalby
2dacf839dc Upgrade tailscale dep 2022-04-21 08:35:08 +00:00
Kristoffer Dalby
8f6952acee Merge branch 'main' into flake-build-env 2022-04-21 09:23:23 +01:00
Kristoffer Dalby
235a90276f Merge pull request #531 from juanfont/suggest-english 2022-04-21 09:21:37 +01:00
Kristoffer Dalby
5c285afda5 Merge branch 'main' into flake-build-env 2022-04-21 09:21:10 +01:00
Kristoffer Dalby
db930af50e Merge branch 'main' into patch-1 2022-04-21 09:13:34 +01:00
Kristoffer Dalby
ffa570e877 Merge branch 'main' into suggest-english 2022-04-21 09:13:08 +01:00
Kristoffer Dalby
96ae78f422 Merge pull request #553 from kradalby/fix-discord-link 2022-04-21 09:12:49 +01:00
Kristoffer Dalby
580c72bf16 Update discord link so it does not grant temp memberships 2022-04-21 09:06:30 +01:00
Kristoffer Dalby
9254afff2d Add direnv and nix output to gitignore 2022-04-21 09:06:13 +01:00
Moritz Poldrack
7ce0bd053c removed leading whitespace 2022-04-16 23:58:05 +02:00
Moritz Poldrack
41a8c14acb add information on how to create a headscale user 2022-04-16 23:56:57 +02:00
Moritz Poldrack
be2487f4c0 Clarified systemd friendly path
Suggested-by: Rendezvous
2022-04-16 23:52:06 +02:00
Adrien Raffin-Caboisse
4651c44dde feat: print tags in nodes list 2022-04-16 13:32:00 +02:00
Adrien Raffin-Caboisse
4fcc5e253c chore: fmt for grpc file 2022-04-16 13:15:18 +02:00
Adrien Raffin-Caboisse
89a1a56328 feat: add unit tests and fmt 2022-04-16 13:15:04 +02:00
Adrien Raffin-Caboisse
db1528bc73 feat: add invalid and valid tags to grpc response 2022-04-16 12:27:54 +02:00
Adrien Raffin-Caboisse
587bdc75de feat: add valid and invalid fields
Also change ID in proto for ForcedTags since the previous ID's should be reserved for commented fields
2022-04-16 11:30:51 +02:00
Adrien Raffin-Caboisse
98f54c9f7f chore: apply format and lint 2022-04-15 18:27:57 +02:00
Adrien Raffin-Caboisse
cd1d10761f feat(acls): add support for forced tags 2022-04-15 18:01:13 +02:00
Adrien Raffin-Caboisse
9de9bc23f8 feat(cli): add tag subcommand to add and remove tags 2022-04-15 16:12:35 +02:00
Adrien Raffin-Caboisse
02f68ebac8 feat: add forcedTags field and update proto 2022-04-15 16:00:08 +02:00
Kristoffer Dalby
dd3f24b83f Merge branch 'main' into suggest-english 2022-04-12 18:16:39 +01:00
Kristoffer Dalby
bc63c577a9 Merge pull request #537 from reynico/exit-if-acl-wrong 2022-04-12 18:15:15 +01:00
Kristoffer Dalby
57c81e4153 Merge branch 'main' into exit-if-acl-wrong 2022-04-12 17:01:40 +01:00
Kristoffer Dalby
556ca5fec7 Merge pull request #544 from mpldr/makefile-improvements 2022-04-12 17:00:58 +01:00
Kristoffer Dalby
93682ab708 Merge branch 'main' into makefile-improvements 2022-04-12 17:00:30 +01:00
Kristoffer Dalby
6eeee8e5c7 Merge pull request #545 from mpldr/fix-discord-invite 2022-04-12 16:59:40 +01:00
Kristoffer Dalby
d195847d8f Merge branch 'main' into fix-discord-invite 2022-04-12 16:59:04 +01:00
Moritz Poldrack
3d8dc9d2bf fix discord invite
Fixes: https://github.com/juanfont/headscale/issues/533
2022-04-11 19:08:07 +02:00
Moritz Poldrack
8601dd1f42 fixed CGO disabling 2022-04-11 14:57:12 +02:00
Kristoffer Dalby
3abdc870d8 Merge branch 'main' into makefile-improvements 2022-04-11 11:49:55 +01:00
Kristoffer Dalby
367f8489db Merge pull request #542 from mpldr/issue-342-send-on-closed-channel 2022-04-11 11:48:53 +01:00
Moritz Poldrack
c312f8bf4a set up Makefile for reproducible builds 2022-04-11 08:56:40 +02:00
Moritz Poldrack
1f43c39f93 replaced version-at-commit script with git-describe call 2022-04-11 08:54:12 +02:00
Moritz Poldrack
9f03a012fb updated changelog 2022-04-10 22:47:52 +02:00
Moritz Poldrack
22dd61d849 fixed the issue of sending on closed channel
This commit fixes the issue of headscale crashing after sending on a
closed channel by moving the channel close to the sender side, instead
of the creator. closeChanWithLog is also implemented with generics now.

Fixes: https://github.com/juanfont/headscale/issues/342
Signed-off-by: Moritz Poldrack <git@moritz.sh>
2022-04-10 17:31:09 +02:00
Kristoffer Dalby
a92f6abc6e Merge pull request #541 from juanfont/update-contributors 2022-04-10 10:04:44 +01:00
github-actions[bot]
9cdaa9730b docs(README): update contributors 2022-04-10 09:03:16 +00:00
Kristoffer Dalby
5d67ed0ce1 Merge pull request #540 from yangchuansheng/dev 2022-04-10 10:02:42 +01:00
Carson Yang
62d774b6ee Fix key name about derp port 2022-04-10 09:53:27 +08:00
Kristoffer Dalby
a14f50eeca Merge pull request #538 from artemklevtsov/patch-1 2022-04-09 18:49:47 +01:00
Artem Klevtsov
98e98a8adb Fix wrong metrics port in docs
It should be 9090.
2022-04-09 16:24:57 +07:00
Nico Rey
fa7ef3df2f make linter happy 2022-04-07 15:21:26 -03:00
Nico Rey
c3324371d6 Update changelog 2022-04-06 18:41:13 -03:00
Nico Rey
6e08241712 Exit Headscale if ACL policy file cannot be parsed 2022-04-06 11:05:08 -03:00
Kristoffer Dalby
c07dd3f14f Merge pull request #534 from nning/main 2022-04-06 08:35:39 +01:00
henning mueller
b2ae9b6cac fix: Remove days from expiry option value examples 2022-04-05 18:45:29 +02:00
Juan Font
57536b020e Merge branch 'main' into suggest-english 2022-04-02 11:47:11 +02:00
Juan Font Alonso
0003e30084 Suggest English as lingua franca 2022-04-02 11:45:18 +02:00
Juan Font
23be13b113 Merge pull request #528 from juanfont/update-contributors
docs(README): update contributors
2022-03-29 23:12:16 +02:00
github-actions[bot]
5e44266292 docs(README): update contributors 2022-03-28 15:06:39 +00:00
Kristoffer Dalby
32522cb482 Merge pull request #521 from Niek/patch-1 2022-03-28 16:06:00 +01:00
Niek van der Maas
6d296a195d Update docs/running-headscale-container.md
Co-authored-by: Kristoffer Dalby <kradalby@kradalby.no>
2022-03-28 08:58:33 +02:00
Niek van der Maas
3272febfb3 Change publish interface 2022-03-26 13:33:31 +01:00
Niek van der Maas
7dae780be1 Update docs/running-headscale-container.md
Co-authored-by: Kristoffer Dalby <kradalby@kradalby.no>
2022-03-25 23:44:55 +01:00
Niek van der Maas
73f1c06f65 Fix long line 2022-03-25 07:46:01 +00:00
Niek van der Maas
b60727b205 Merge branch 'main' into patch-1 2022-03-25 08:44:16 +01:00
Niek van der Maas
8cee31d8d7 Fix prettier 2022-03-25 07:43:15 +00:00
Juan Font
b5aace6d3a Merge pull request #519 from hdhoang/pak-counter
Fix labels cardinality error when registering unknown pre-auth key
2022-03-25 00:25:19 +01:00
Niek van der Maas
7e286c570e Docker docs enhancements
While configuring a Docker setup I noticed that the docs could use some enhancements.
2022-03-22 13:45:30 +01:00
hdhoang
52fd13bfc4 Fix labels cardinality error when registering unknown pre-auth key 2022-03-21 15:49:14 +07:00
Kristoffer Dalby
b8e4aeede8 Upgrade golines 2022-03-20 22:39:43 +00:00
Kristoffer Dalby
9a632c17d1 Merge pull request #518 from juanfont/update-contributors 2022-03-20 14:42:41 +00:00
github-actions[bot]
8758ee1c4d docs(README): update contributors 2022-03-20 14:18:39 +00:00
Kristoffer Dalby
150ae1846a Merge pull request #517 from juanfont/changelog-prep-0.15
Prepare CHANGELOG for v0.15.0
2022-03-20 14:18:01 +00:00
Juan Font
452286552c Update CHANGELOG.md to include future 0.16.0
Co-authored-by: Kristoffer Dalby <kradalby@kradalby.no>
2022-03-20 15:07:22 +01:00
Juan Font Alonso
631cf58ff0 Added date for 0.15.0 in changelog 2022-03-20 13:36:25 +01:00
Juan Font
8a2c0e88f4 Merge pull request #513 from juanfont/unstable-integration-tests
Add Tailscale unstable channel and repo HEAD to integration tests
2022-03-20 13:35:23 +01:00
Juan Font Alonso
af6a47fdd3 Changelog updated 2022-03-20 12:36:30 +01:00
Juan Font
94d910557f Merge branch 'main' into unstable-integration-tests 2022-03-20 12:34:04 +01:00
Juan Font Alonso
a8a683d3cc Added default values in Dockerfile.tailscale 2022-03-20 12:33:41 +01:00
Juan Font Alonso
a1caa5b45c Minor improvements on logging 2022-03-20 12:31:18 +01:00
Juan Font Alonso
f42868f67f Docker requires lowercase for the container names 2022-03-20 12:30:56 +01:00
Juan Font Alonso
a6455653c0 Added missing package 2022-03-20 12:30:08 +01:00
Kristoffer Dalby
c8aa653275 Merge branch 'main' into main 2022-03-19 09:36:36 +00:00
Kristoffer Dalby
91e5cbd793 Add direnv flake support 2022-03-19 09:23:03 +00:00
Kristoffer Dalby
79fc74c7a4 Merge branch 'main' into acls-doc 2022-03-18 22:43:17 +00:00
Kristoffer Dalby
c8503075e0 Merge pull request #514 from aofei/main 2022-03-18 21:18:22 +00:00
Kristoffer Dalby
4068a7b00b Merge branch 'main' into main 2022-03-18 21:02:05 +00:00
Kristoffer Dalby
daae2fe549 Merge pull request #512 from restanrm/feat-add-debug-log 2022-03-18 21:01:16 +00:00
Kristoffer Dalby
47bbb85a20 Merge branch 'main' into acls-doc 2022-03-18 20:44:44 +00:00
Kristoffer Dalby
739653fa71 Merge branch 'main' into feat-add-debug-log 2022-03-18 20:44:21 +00:00
Kristoffer Dalby
304109a6c5 Merge pull request #511 from restanrm/fix-machine-registration-expired 2022-03-18 20:44:05 +00:00
Kristoffer Dalby
c29af96a19 Merge branch 'main' into main 2022-03-18 20:42:44 +00:00
Kristoffer Dalby
d21e9d29d1 Merge branch 'main' into feat-add-debug-log 2022-03-18 19:41:32 +00:00
Kristoffer Dalby
b65bd5baa8 Merge branch 'main' into fix-machine-registration-expired 2022-03-18 19:41:26 +00:00
Juan Font Alonso
0165b89941 Fixed paths 2022-03-18 19:35:09 +01:00
Kristoffer Dalby
53b62f3f39 Merge pull request #499 from juanfont/mandatory-stun 2022-03-18 18:28:37 +00:00
Kristoffer Dalby
cd2914ab3b Merge branch 'main' into mandatory-stun 2022-03-18 17:44:12 +00:00
Kristoffer Dalby
e85b97143c Merge pull request #509 from kradalby/go118 2022-03-18 17:43:41 +00:00
Aofei Sheng
1eafe960b8 fix: possible panic in Headscale.scheduledDERPMapUpdateWorker
There is a possible nil pointer dereference panic in the
`Headscale.scheduledDERPMapUpdateWorker`. Such as when the embedded
DERP server is disabled.
2022-03-19 01:20:25 +08:00
Juan Font Alonso
749c92954c Add Tailscale unstable channel and repo HEAD to integration tests
In preparation for the implementation of the new TS2021 protocol (Tailscale control protocol v2) we are expanding the test infrastructure
2022-03-18 17:05:28 +01:00
Juan Font Alonso
db9ba17920 Added missing file 2022-03-18 13:10:35 +01:00
Juan Font Alonso
d5ce7d7523 Prettier 2022-03-18 13:09:57 +01:00
Juan Font Alonso
2e6687209b Make STUN server mandatory if DERP embedded is enabled 2022-03-18 12:58:00 +01:00
Adrien Raffin-Caboisse
2e04abf4bb feat(oidc): add debug log 2022-03-18 09:40:12 +01:00
Adrien Raffin-Caboisse
882c0c34c1 chore(changelog): update changelog 2022-03-18 09:34:18 +01:00
Adrien Raffin-Caboisse
61ebb713f2 fix(oidc): Reset expiry for reauthentication
The previous code resetted the expiry time to be expired.  So the machine was never reauthenticated
2022-03-18 09:32:07 +01:00
Kristoffer Dalby
ac5ad42474 Fix integration nix 2022-03-18 08:24:21 +00:00
Nico Rey
d68d7d5a6f Docs/ACLs: Add a network diagram to help explain ACLs 2022-03-17 19:58:56 -03:00
Nico Rey
bff9036f14 Docs/ACLs: Add router examples with subnets 2022-03-17 19:58:34 -03:00
Nico Rey
8b08c2a918 Docs/ACLs: Namespaces are created automatically 2022-03-17 19:24:39 -03:00
Nico Rey
b9f0fabb5c Docs/ACLs: Wording, add intermediary router example 2022-03-17 19:23:37 -03:00
Kristoffer Dalby
9d4822b8c7 Actually set up nix 2022-03-17 18:20:01 +00:00
Kristoffer Dalby
466d03d574 Nixify integration test 2022-03-17 18:18:51 +00:00
Kristoffer Dalby
d43fec7f96 Merge branch 'main' into flake-build-env 2022-03-17 18:11:21 +00:00
Kristoffer Dalby
62f4c205f5 Run binary build with nix 2022-03-17 18:11:04 +00:00
Kristoffer Dalby
003c19004d Run tests with nix 2022-03-17 18:10:50 +00:00
Kristoffer Dalby
70274d528c Add nix to runn on lint and integration 2022-03-17 18:08:49 +00:00
Kristoffer Dalby
6d41279781 Upgrade to go 1.18 2022-03-17 18:07:26 +00:00
Kristoffer Dalby
b781446e86 Upgrade to go 1.18 2022-03-17 17:43:11 +00:00
Kristoffer Dalby
1c9b1c0579 Merge pull request #507 from juanfont/update-contributors 2022-03-17 07:28:42 +00:00
github-actions[bot]
ade9552736 docs(README): update contributors 2022-03-17 06:38:00 +00:00
Kristoffer Dalby
68403cb76e Merge pull request #505 from y0ngb1n/fix-docs-metrics-endpoint 2022-03-17 06:37:27 +00:00
Yang Bin
537ecb8db0 docs: fixed /metrics endpoint 8080 → 9090, reference config-example.yaml 2022-03-17 09:25:42 +08:00
Juan Font Alonso
8f5875efe4 Reorg errors 2022-03-16 19:46:59 +01:00
Juan Font
98ac88d5ef Changed comment position
Co-authored-by: Kristoffer Dalby <kradalby@kradalby.no>
2022-03-16 18:45:34 +01:00
Kristoffer Dalby
d13338a9fb Merge branch 'main' into mandatory-stun 2022-03-16 07:18:18 +00:00
Juan Font
1579ffb66a Merge pull request #500 from bravechamp/patch-1
Fix API access
2022-03-15 14:53:23 +01:00
bravechamp
0bfa5302a7 Fix API access
By allowing API keys to be validated
2022-03-15 16:05:56 +03:00
Juan Font Alonso
b8aad5451d Make STUN run by default when embedded DERP is enabled
This commit also allows to set an external STUN server, while running the embedded DERP server (without embedded STUN)
2022-03-15 13:22:25 +01:00
bravechamp
60ee04674d Normalize nickname before saving to database 2022-03-13 21:55:36 +00:00
bravechamp
9901d6b2e7 Ability to clear nickname 2022-03-13 21:10:41 +00:00
bravechamp
663e8384a3 Nickname support 2022-03-13 21:03:20 +00:00
Kristoffer Dalby
61440c42d3 Merge pull request #496 from juanfont/update-contributors
docs(README): update contributors
2022-03-10 19:58:24 +00:00
github-actions[bot]
18ee6274e1 docs(README): update contributors 2022-03-10 19:50:59 +00:00
Kristoffer Dalby
0abfbdc18a Merge pull request #495 from appbricks/appbricks/main-bug-fix
Regression bug fix when re-authenticating machine with auth-key
2022-03-10 19:50:23 +00:00
Mevan Samaratunga
082a852c5e fixed linting recommendation 2022-03-10 10:40:20 -05:00
Mevan Samaratunga
af081e9fd3 fixed lint errors 2022-03-10 10:22:21 -05:00
Mevan Samaratunga
8b5e8b7dfc Refresh expired machine on re-auth - closes #489 2022-03-10 08:59:28 -05:00
Kristoffer Dalby
1e7d7e510e Update go sha for flake 2022-03-08 17:17:02 +00:00
Kristoffer Dalby
a806694d23 fix gosum merge 2022-03-08 17:11:46 +00:00
Kristoffer Dalby
62d7fae056 Merge pull request #311 from restanrm/docs-acl-modifications
Issues with current ACL implementation and solution proposal
2022-03-08 17:08:17 +00:00
Kristoffer Dalby
06d85688fd set version based on git rev 2022-03-08 17:07:25 +00:00
Kristoffer Dalby
dd219d0ff6 Merge branch 'main' into docs-acl-modifications 2022-03-08 17:05:59 +00:00
Kristoffer Dalby
6087e1cf6f Merge pull request #488 from juanfont/update-contributors 2022-03-08 17:02:47 +00:00
github-actions[bot]
c47fb1ae54 docs(README): update contributors 2022-03-08 16:50:11 +00:00
Kristoffer Dalby
48cec3cd90 Merge pull request #486 from e-zk/main 2022-03-08 16:49:32 +00:00
Kristoffer Dalby
e54c508c10 Merge branch 'main' into main 2022-03-08 16:05:41 +00:00
Kristoffer Dalby
941e9d9b0f Merge pull request #388 from juanfont/embedded-derp 2022-03-08 16:05:30 +00:00
Kristoffer Dalby
11ccae8e52 Merge branch 'main' into flake-build-env 2022-03-08 16:04:52 +00:00
Juan Font Alonso
b803240dc1 Added new line for prettier 2022-03-08 12:21:08 +01:00
Juan Font Alonso
bdbf620ece Merge branch 'embedded-derp' of https://github.com/juanfont/headscale into embedded-derp 2022-03-08 12:16:43 +01:00
Juan Font
e5d22b8a70 Merge branch 'main' into embedded-derp 2022-03-08 12:16:34 +01:00
Juan Font Alonso
05c5e2280b Updated CHANGELOG and README 2022-03-08 12:15:05 +01:00
Juan Font Alonso
b41d89946a Merge branch 'embedded-derp' of https://github.com/juanfont/headscale into embedded-derp 2022-03-08 12:11:59 +01:00
Juan Font Alonso
cc0c88a63a Added small integration test for stun 2022-03-08 12:11:51 +01:00
e-zk
c06689dec1 fix: make register html/template consistent with other html
- makes the html/template for /register follow the same formatting
  as /apple and /windows
- adds a <title> element
- minor change for consistency's sake
2022-03-08 18:34:46 +10:00
Kristoffer Dalby
b85dd7abbd Merge pull request #484 from juanfont/prtemplate-fix
Fix checkboxes in PR template
2022-03-08 07:29:30 +00:00
Kristoffer Dalby
6aeaff43aa Fix checkboxes in PR template 2022-03-08 07:21:04 +00:00
Kristoffer Dalby
dd26cbd193 Merge branch 'main' into embedded-derp 2022-03-08 07:18:51 +00:00
Kristoffer Dalby
9a60eeaf86 Merge branch 'main' into flake-build-env 2022-03-08 07:18:21 +00:00
Kristoffer Dalby
b0ae3240fd Merge pull request #387 from restanrm/fix-magic-dns-and-uppercase-letters
Fix magic dns and uppercase letters
2022-03-08 07:17:45 +00:00
Adrien Raffin-Caboisse
41efe98953 fix: apply fmt and fix missing name changes 2022-03-07 23:20:30 +01:00
Adrien Raffin-Caboisse
2b68c90778 chore: update changelog 2022-03-07 23:14:39 +01:00
Adrien Raffin-Caboisse
f19c048569 fix: change normalization function name 2022-03-07 22:55:54 +01:00
Adrien Raffin-Caboisse
6cc8bbc24f feat(api): add normalisation at machine register step 2022-03-07 22:46:29 +01:00
Kristoffer Dalby
c24de595f6 Add example commands for docker 2022-03-07 17:41:16 +00:00
Kristoffer Dalby
63641a7b17 Correct pkgs call 2022-03-07 17:37:53 +00:00
Kristoffer Dalby
a6570d33a6 Add option to build docker image
This commit adds dockerbuild to flakes.nix:

```
nix build .#headscale-docker
```

This uses the Nix infra to build and _does not_ use Dockerfile.

It currently works on Linux (no macOS)
2022-03-07 17:18:41 +00:00
Kristoffer Dalby
124d8a3424 Update readme with nix notes 2022-03-07 16:58:07 +00:00
Kristoffer Dalby
5de9de14a9 Add flake build file
This commit adds a flake.nix build file, it can be used for three
things:

Build `headscale` from local or straight from git:

nix build
or
nix build github:juanfont/headscale

Run and Build `headscale` from local or straight from git:

nix run
or
nix run github:juanfont/headscale

Set up a development environment including all our tools,
- linters
- protobuf tooling
- compilers

nix develop
2022-03-07 16:51:38 +00:00
Kristoffer Dalby
15f8cb5034 Remove hacky go tool install 2022-03-07 07:40:56 +00:00
Juan Font Alonso
03452a8dca Prettied 2022-03-07 00:29:40 +01:00
Juan Font Alonso
15ed71315c Merge branch 'embedded-derp' of https://github.com/juanfont/headscale into embedded-derp 2022-03-06 23:47:31 +01:00
Juan Font Alonso
05df8e947a Added missing file 2022-03-06 23:47:14 +01:00
Juan Font Alonso
b3fa66dbd2 Check for DERP in test 2022-03-06 23:46:16 +01:00
Juan Font Alonso
a27b386123 Clarified expiration dates 2022-03-06 23:45:01 +01:00
Juan Font Alonso
580db9b58f Mention that STUN is UDP 2022-03-06 23:19:21 +01:00
Adrien Raffin-Caboisse
1114449601 change: update name of method to check and normalize Domain name 2022-03-06 20:46:17 +01:00
Juan Font
b47de07eea Update Dockerfile.tailscale
Co-authored-by: Kristoffer Dalby <kradalby@kradalby.no>
2022-03-06 20:42:27 +01:00
Juan Font
e1fcf0da26 Added more version
Co-authored-by: Kristoffer Dalby <kradalby@kradalby.no>
2022-03-06 20:40:55 +01:00
Juan Font
dcf3ea567c Merge branch 'main' into fix-magic-dns-and-uppercase-letters 2022-03-06 17:37:48 +01:00
Juan Font Alonso
de2ea83b3b Linting here and there 2022-03-06 17:35:54 +01:00
Juan Font Alonso
eb06054a7b Make DERP Region configurable 2022-03-06 17:25:21 +01:00
Juan Font Alonso
eb500155e8 Make STUN server configurable 2022-03-06 17:00:56 +01:00
Juan Font Alonso
dc909ba6d7 Improved logging on startup 2022-03-06 16:54:19 +01:00
Juan Font Alonso
70910c4595 Working /bootstrap-dns DERP helper 2022-03-06 01:23:35 +01:00
Juan Font Alonso
54c3e00a1f Merge local DERP server region with other configured DERP sources 2022-03-05 20:04:31 +01:00
Juan Font Alonso
e78c002f5a Fix minor issue 2022-03-05 19:48:30 +01:00
Juan Font Alonso
237f7f1027 Merge branch 'main' into embedded-derp 2022-03-05 19:42:29 +01:00
Juan Font Alonso
992efbd84a Added missing private TLS key 2022-03-05 19:35:15 +01:00
Juan Font Alonso
e9eb90fa76 Added integration tests for the embedded DERP server 2022-03-05 19:34:06 +01:00
Juan Font Alonso
88378c22fb Rename the file to derp_server.go for coherence 2022-03-05 19:31:50 +01:00
Juan Font Alonso
b742379627 Do not use the term embedded 2022-03-05 19:30:30 +01:00
Juan Font Alonso
df37d1a639 Do not offer the option to be DERP insecure
Websockets, in which DERP is based, requires a TLS certificate. At the same time,
if we use a certificate it must be valid... otherwise Tailscale wont connect (does not
have an Insecure option). So there is no option to expose insecure here
2022-03-05 19:19:21 +01:00
Juan Font Alonso
758b1ba1cb Renamed configuration items of the DERP server 2022-03-05 16:22:02 +01:00
Kristoffer Dalby
435ee36d78 Merge pull request #394 from juanfont/renovateaction/dockerfiles 2022-03-05 00:41:22 +00:00
Renovate Bot
35efd8f95a chore(deps): update dependency docker.io/golang to v1.17.8 2022-03-05 00:09:36 +00:00
Juan Font Alonso
09d78c7a05 Even more stuff moved to common 2022-03-04 13:54:59 +01:00
Kristoffer Dalby
60655c5242 Merge pull request #393 from juanfont/update-contributors 2022-03-04 12:30:20 +00:00
Juan Font Alonso
22d2443281 Move more stuff to common 2022-03-04 13:26:45 +01:00
github-actions[bot]
a70669fca7 docs(README): update contributors 2022-03-04 11:04:12 +00:00
Kristoffer Dalby
0720473033 Merge pull request #392 from e-zk/windows-endpoint 2022-03-04 11:03:33 +00:00
Kristoffer Dalby
e799307e74 Merge branch 'main' into windows-endpoint 2022-03-04 10:47:52 +00:00
e-zk
575f33d183 docs: fix comments to comply with golangci-lint 2022-03-04 20:35:09 +10:00
Juan Font Alonso
607c1eb316 Be consistent with uppercase DERP 2022-03-04 11:31:41 +01:00
e-zk
d69dada8ff feat(windows): rename apple_mobileconfig.go => platform_config.go
rename apple_mobileconfig.go to platform_config.go since the file
includes configuration info for multiple platforms now.
2022-03-04 20:03:49 +10:00
e-zk
f9e0c13890 docs: update CHANGELOG 2022-03-04 19:53:57 +10:00
e-zk
12a50ac8ac feat(windows): add /windows endpoint for Windows configuration
- registry file /windows/tailscale.reg is generated, filling in the
  associated control server URL
- also includes CLI instructions
- fix /apple incorrect template: 'Url' is supposed to be '.URL'
2022-03-04 19:53:44 +10:00
e-zk
b342cf0240 feat(windows): cleanup /apple endpoint
- rename the gin function to AppleConfigMessage
- use <pre> + <code> for code blocks
- add headscale heading
- reword some sections
2022-03-04 19:53:29 +10:00
Kristoffer Dalby
e3ff87b7ef Merge pull request #389 from e-zk/main 2022-03-04 07:26:35 +00:00
zakaria
745696b310 docs: fix mistake in ACME challenge type comment 2022-03-04 12:11:43 +10:00
Juan Font Alonso
23cde8445f Merge branch 'main' into embedded-derp 2022-03-04 00:04:59 +01:00
Juan Font Alonso
9d43f589ae Added missing deps 2022-03-04 00:04:28 +01:00
Juan Font Alonso
897d480f4d Add an embedded DERP server to Headscale
This series of commit will be adding an embedded DERP server (and STUN) to Headscale,
thus making it completely self-contained and not dependant in other infrastructure.
2022-03-04 00:01:31 +01:00
Adrien Raffin-Caboisse
6f172a6e4c fix(acls): remove dead error code 2022-03-03 23:53:08 +01:00
Adrien Raffin-Caboisse
44a5372c53 fix(poll): Normalize hostname
This function is called often. Normalization of the hostname will be written in database.
2022-03-03 23:52:25 +01:00
Kristoffer Dalby
f2ea6fb30f Merge pull request #384 from restanrm/fix-issue-with-empty-namespace-and-acl-evaluation 2022-03-03 08:43:37 +00:00
Adrien Raffin-Caboisse
4a4952899b feat(acls): add some logs and skip error
logs looks like the following
```
2022-03-02T20:43:08Z DBG Expanding alias=app-test
2022-03-02T20:43:08Z DBG Expanding alias=kube-test
2022-03-02T20:43:08Z DBG Expanding alias=test
2022-03-02T20:43:08Z WRN No IPs found with the alias test
2022-03-02T20:43:08Z DBG Expanding alias=prod
2022-03-02T20:43:08Z WRN No IPs found with the alias prod
2022-03-02T20:43:08Z DBG Expanding alias=prod
2022-03-02T20:43:08Z WRN No IPs found with the alias prod
```
2022-03-02 21:54:43 +01:00
Kristoffer Dalby
b72a8aa7d1 Merge pull request #381 from juanfont/update-contributors 2022-03-02 14:18:09 +00:00
github-actions[bot]
e301d0d1df docs(README): update contributors 2022-03-02 13:44:26 +00:00
Kristoffer Dalby
75ca91b0f7 Merge pull request #380 from juanfont/update-contributors
docs(README): update contributors
2022-03-02 13:43:53 +00:00
github-actions[bot]
e208ccc982 docs(README): update contributors 2022-03-02 13:42:25 +00:00
Kristoffer Dalby
71a62697aa Merge pull request #379 from juanfont/kradalby-patch-1
Second contributor attempt
2022-03-02 13:41:50 +00:00
Kristoffer Dalby
f9c0597875 Second contributor attempt 2022-03-02 13:40:37 +00:00
Kristoffer Dalby
aa3eb5171a Merge pull request #344 from reynico/metrics-listen 2022-03-02 13:06:29 +00:00
Nico Rey
dcc46af8de Changelog: add breaking change 2022-03-02 09:22:29 -03:00
Kristoffer Dalby
b61500670c Merge branch 'main' into metrics-listen 2022-03-02 11:35:33 +00:00
Kristoffer Dalby
ccec534e19 Merge pull request #377 from juanfont/smarter-contribute-pipeline 2022-03-02 11:17:02 +00:00
Kristoffer Dalby
9b10457209 Merge branch 'main' into smarter-contribute-pipeline 2022-03-02 11:14:50 +00:00
Kristoffer Dalby
9a8f605cba Merge pull request #371 from kradalby/use-specific-database-typess 2022-03-02 11:14:04 +00:00
Kristoffer Dalby
1246267ead Merge branch 'main' into smarter-contribute-pipeline 2022-03-02 10:43:07 +00:00
Kristoffer Dalby
a0a56d43f8 Merge branch 'main' into use-specific-database-typess 2022-03-02 10:29:34 +00:00
Kristoffer Dalby
63d87110f6 Merge pull request #376 from e-zk/feat/command-aliases 2022-03-02 10:28:18 +00:00
Kristoffer Dalby
7c99d963e2 Merge branch 'main' into feat/command-aliases 2022-03-02 10:06:38 +00:00
zakaria
a614f158be docs: update changelog 2022-03-02 19:53:07 +10:00
Kristoffer Dalby
2b6a5173da Allow upstream delete continue on failure 2022-03-02 09:12:00 +00:00
Kristoffer Dalby
32ac690494 Update contributors.yml 2022-03-02 09:08:30 +00:00
Kristoffer Dalby
0835bffc3c Update changelog 2022-03-02 08:15:21 +00:00
Kristoffer Dalby
c80e364f02 Remove always nil error 2022-03-02 08:15:14 +00:00
Kristoffer Dalby
5b169010be Resolve merge conflict 2022-03-02 08:11:50 +00:00
Kristoffer Dalby
eeded85d9c Merge pull request #366 from kradalby/registration-simplification 2022-03-02 08:02:26 +00:00
Kristoffer Dalby
e4d81bbb16 Merge branch 'main' into registration-simplification 2022-03-02 07:31:02 +00:00
Kristoffer Dalby
1f8c7f427b Add comment 2022-03-02 07:29:56 +00:00
Kristoffer Dalby
ef422e6988 Protect against expiry nil 2022-03-02 07:29:56 +00:00
Kristoffer Dalby
ec4dc68524 Use correct machinekey format for oidc reg 2022-03-02 07:29:56 +00:00
Kristoffer Dalby
86ade72c19 Remove err check 2022-03-02 07:29:56 +00:00
Kristoffer Dalby
0c0653df8b Merge pull request #375 from restanrm/fix-limitations-in-source-acls-rules
Fix limitations in source acls rules
2022-03-02 07:02:29 +00:00
zakaria
12b3b5f8f1 feat(aliases): add aliases for preauthkeys command
- `preauthkey`, `authkey`, `pre` are aliases for `preauthkey` command
- `ls`, `show` are aliases for `list` subcommand
- `c`, `new` are aliases for `create` subcommand
- `revoke`, `exp`, `e` are aliases for `expire` subcommand
2022-03-02 15:42:12 +10:00
zakaria
052dbfe440 feat(aliases): add aliases for apikeys command
- `apikey`, `api` are aliases for `apikeys` command
- `ls`, `show` are aliases for `list` subcommand
- `c`, `new` are aliases for `create` subcommand
- `revoke`, `exp`, `e` are aliases for the `expire` subcommand
2022-03-02 15:32:35 +10:00
zakaria
5310f8692b feat(aliases): add aliases for namespaces command
- `namespace`, `ns`, `user`, `users` are aliases for `namespaces`
   command
- `c`, `new` are aliases for the `create` subcommand
- `delete` is an alias for the `destroy` subcommand
- `mv` is an alias for the `rename` subcommand
- `ls`, `show` are aliases for the `list` subcommand
2022-03-02 14:35:20 +10:00
zakaria
aff6b84250 feat(aliases): add 'gen' alias for 'generate' command 2022-03-02 14:29:33 +10:00
zakaria
21eee912a3 feat(aliases): add aliases for nodes command
- `node`, `machine`, `machines` are aliases for `nodes` command
- `ls`, `show` aliases for `list` subcommand
- `logout`, `exp`, `e` are aliases for `expire` subcommand
- `del` is an alias for `delete` subcommand
2022-03-02 14:28:03 +10:00
zakaria
dbb2af0238 feat(aliases): add aliases for route command
- `r` is alias for `route` command
- `ls`, or `show` is alias for `list` subcommand
2022-03-02 14:27:56 +10:00
Adrien Raffin-Caboisse
77fe0b01f7 docs: update changelog 2022-03-01 22:50:22 +01:00
Adrien Raffin-Caboisse
361b4f7f4f fix(machine): allow to use * in ACL sources 2022-03-01 22:48:21 +01:00
Kristoffer Dalby
dec4ee5f73 Merge pull request #373 from restanrm/feat-email-in-acls 2022-03-01 21:18:13 +00:00
Adrien Raffin-Caboisse
b2dca80e7a docs: update changelog 2022-03-01 21:16:33 +01:00
Adrien Raffin-Caboisse
a455a874ad feat(acls): normalize the group name 2022-03-01 21:10:52 +01:00
Kristoffer Dalby
49cd761bf6 Use new machine types in tests 2022-03-01 16:34:35 +00:00
Kristoffer Dalby
6477e6a583 Use new machine types 2022-03-01 16:34:24 +00:00
Kristoffer Dalby
8a95fe517a Use specific types for all fields on machine (no datatypes.json)
This commit removes the need for datatypes.JSON and makes the code a bit
cleaner by allowing us to use proper types throughout the code when it
comes to hostinfo and other datatypes on the machine object.

This allows us to remove alot of unmarshal/marshal operations and remove
a lot of obsolete error checks.

This following commits will clean away a lot of untyped data and
uneccessary error checks.
2022-03-01 16:31:25 +00:00
Kristoffer Dalby
a9d4fa89dc Merge branch 'main' into registration-simplification 2022-03-01 15:53:06 +01:00
Kristoffer Dalby
94c5474212 Merge pull request #369 from kradalby/update-dependencies
Update dependencies
2022-03-01 15:52:27 +01:00
Kristoffer Dalby
d34d617935 Merge branch 'main' into registration-simplification 2022-03-01 15:18:24 +01:00
Kristoffer Dalby
573008757d Merge branch 'main' into update-dependencies 2022-03-01 15:16:56 +01:00
Kristoffer Dalby
4c74043f72 Merge pull request #359 from kradalby/yaml-acls
Add YAML support to ACLs
2022-03-01 15:16:37 +01:00
Kristoffer Dalby
0551b34de5 Merge branch 'main' into update-dependencies 2022-03-01 14:51:57 +01:00
Kristoffer Dalby
105812421e Merge branch 'main' into yaml-acls 2022-03-01 14:49:37 +01:00
Kristoffer Dalby
4a9fd3a680 Merge pull request #368 from kradalby/apple-profile-fix
Fix apple profile issue being generated with escaped characters
2022-03-01 14:49:07 +01:00
Kristoffer Dalby
1cb39d914c Update dependencies 2022-03-01 07:35:17 +00:00
Kristoffer Dalby
5157f356cb Fix apple profile issue being generated with escaped characters 2022-03-01 07:30:35 +00:00
Kristoffer Dalby
7c63412df5 Remove todo 2022-02-28 23:02:41 +00:00
Kristoffer Dalby
82cb6b9ddc Cleanup some unreachable code 2022-02-28 23:00:41 +00:00
Kristoffer Dalby
379017602c Reformat and add db backup note 2022-02-28 22:50:35 +00:00
Kristoffer Dalby
8bef04d8df Remove sorted todo 2022-02-28 22:45:42 +00:00
Kristoffer Dalby
5e92ddad43 Remove redundant caches
This commit removes the two extra caches (oidc, requested time) and uses
the new central registration cache instead. The requested time is
unified into the main machine object and the oidc key is just added to
the same cache, as a string with the state as a key instead of machine
key.
2022-02-28 22:42:30 +00:00
Kristoffer Dalby
e64bee778f Regenerate proto 2022-02-28 22:21:14 +00:00
Kristoffer Dalby
5e1b12948e Remove registered field from proto 2022-02-28 22:21:06 +00:00
Kristoffer Dalby
eea8e7ba6f Update changelog 2022-02-28 22:11:31 +00:00
Kristoffer Dalby
78251ce8ec Remove registrated field
This commit removes the field from the database and does a DB migration
**removing** all unregistered machines from headscale.

This means that from this version, all machines in the database is
considered registered.
2022-02-28 18:05:03 +00:00
Kristoffer Dalby
a8649d83c4 Remove all references to Machine.Registered from tests 2022-02-28 17:42:03 +00:00
Kristoffer Dalby
16b21e8158 Remove all references to Machine.Registered 2022-02-28 16:55:57 +00:00
Kristoffer Dalby
35616eb861 Fix oidc error were namespace isnt created #365 2022-02-28 16:41:28 +00:00
Kristoffer Dalby
e7bef56718 Remove reference to registered in integration test 2022-02-28 16:36:29 +00:00
Kristoffer Dalby
c6b87de959 Remove poorly aged test 2022-02-28 16:36:16 +00:00
Kristoffer Dalby
50053e616a Ignore complexity linter 2022-02-28 16:35:08 +00:00
Kristoffer Dalby
54cc3c067f Implement new machine register parameter 2022-02-28 16:34:50 +00:00
Kristoffer Dalby
402a76070f Reuse machine structure for parameters, named parameters 2022-02-28 16:34:28 +00:00
Nico Rey
9a61725e9f Metrics: Disable toggle. Set default port to 9090 2022-02-28 10:40:02 -03:00
Kristoffer Dalby
6126d6d9b5 Merge branch 'main' into metrics-listen 2022-02-28 14:24:25 +01:00
Kristoffer Dalby
469551bc5d Register new machines needing callback in memory
This commit stores temporary registration data in cache, instead of
memory allowing us to only have actually registered machines in the
database.
2022-02-28 08:06:39 +00:00
Kristoffer Dalby
1caa6f5d69 Add todo for JSON datatype 2022-02-27 18:48:25 +01:00
Kristoffer Dalby
ecc26432fd Fix excessive replace 2022-02-27 18:48:12 +01:00
Kristoffer Dalby
caffbd8956 Update cli registration with new method 2022-02-27 18:42:43 +01:00
Kristoffer Dalby
fd1e4a1dcd Generalise registration for openid 2022-02-27 18:42:24 +01:00
Kristoffer Dalby
acb945841c Generalise registration for pre auth keys 2022-02-27 18:42:15 +01:00
Kristoffer Dalby
c58ce6f60c Generalise the registration method to DRY stuff up 2022-02-27 18:40:10 +01:00
Kristoffer Dalby
d6f6939c54 Update changelog 2022-02-27 09:08:29 +01:00
Kristoffer Dalby
e0b9a317f4 Add note to config example 2022-02-27 09:05:08 +01:00
Kristoffer Dalby
c159eb7541 Add basic test of yaml parsing 2022-02-27 09:04:59 +01:00
Kristoffer Dalby
8a3a0b6403 Add YAML support to ACLs 2022-02-27 09:04:48 +01:00
Kristoffer Dalby
67d6c8f946 Remove oversensitive tracing output 2022-02-27 09:04:27 +01:00
Nico Rey
06e6c29a5b metrics: make metrics endpoint toggleable 2022-02-25 18:36:03 -03:00
Nico Rey
a9122c3de3 prometheus: replace default port by a port between the recommended prometheus range 2022-02-25 18:21:20 -03:00
Kristoffer Dalby
b1bd17f316 Merge pull request #350 from restanrm/feat-oidc-login-as-namespace 2022-02-25 13:22:26 +01:00
Adrien Raffin-Caboisse
b39faa124a Merge remote-tracking branch 'origin/main' into feat-oidc-login-as-namespace 2022-02-25 11:28:17 +01:00
Kristoffer Dalby
8689a39c96 Merge pull request #357 from kradalby/make-namespace-to-users
Remove boundaries between Namespaces
2022-02-25 11:01:41 +01:00
Kristoffer Dalby
bae8ed3e70 Merge branch 'main' into make-namespace-to-users 2022-02-25 10:39:12 +01:00
Kristoffer Dalby
08c7076667 Merge pull request #346 from kradalby/integration-test-concurrent-join
Fix ip allocation bug, make integration tests faster
2022-02-25 10:37:35 +01:00
Kristoffer Dalby
91b50550ee Update readme and glossary to reflect features and goals 2022-02-25 10:34:35 +01:00
Kristoffer Dalby
2c7064462a Update changelog 2022-02-25 10:30:58 +01:00
Kristoffer Dalby
d9e7f37280 Uncomment previous test and update them for no boundries 2022-02-25 10:27:27 +01:00
Kristoffer Dalby
e03b3d558f Remove boundries between namespaces 2022-02-25 10:26:34 +01:00
Kristoffer Dalby
2fd36dd254 Resolve merge 2022-02-25 09:08:15 +00:00
Kristoffer Dalby
381598663d Merge pull request #347 from kradalby/remove-shared
Remove the concept of "shared nodes"
2022-02-25 10:06:58 +01:00
Kristoffer Dalby
6d699d3c29 Update changelog 2022-02-25 08:44:16 +00:00
Kristoffer Dalby
ebe59a5a27 Fix utils tests, use ipset datastructure 2022-02-25 08:28:22 +00:00
Nico
d55c79e75b Merge branch 'main' into metrics-listen 2022-02-24 10:41:07 -03:00
Kristoffer Dalby
eda0a9f88a Lock allocation of IP address
current logic is not safe as it will allow an IP that isnt persisted to
the DB to be given out multiple times if machines joins in quick
succession.

This adds a lock around the "get ip" and machine registration and save
to DB so we ensure thiis isnt happning.

Currently this had to be done three places, which is silly, and outlined
in #294.
2022-02-24 13:18:18 +00:00
Adrien Raffin-Caboisse
47e8442d91 Update CHANGELOG.md
Co-authored-by: Kristoffer Dalby <kradalby@kradalby.no>
2022-02-24 13:34:48 +01:00
Adrien Raffin-Caboisse
f9ce32fe1a Update CHANGELOG.md
Co-authored-by: Kristoffer Dalby <kradalby@kradalby.no>
2022-02-24 13:34:36 +01:00
Kristoffer Dalby
189e883f91 Resolve merge 2022-02-24 11:41:54 +00:00
Kristoffer Dalby
aa506503e2 Merge branch 'main' into feat-oidc-login-as-namespace 2022-02-24 11:40:34 +00:00
Kristoffer Dalby
9c2c09fce7 Merge branch 'main' into remove-shared 2022-02-24 11:39:44 +00:00
Kristoffer Dalby
9687e6768d Remove retry from integration tests 2022-02-24 11:29:53 +00:00
Kristoffer Dalby
fb85c78e8a Fail integration tests fast 2022-02-24 11:28:34 +00:00
Kristoffer Dalby
d27f2bc538 Merge branch 'main' into metrics-listen 2022-02-24 11:16:57 +00:00
Kristoffer Dalby
69f220fe5c Merge branch 'main' into feat-oidc-login-as-namespace 2022-02-24 11:01:32 +00:00
Adrien Raffin-Caboisse
95453cba75 Merge branch 'main' into feat-oidc-login-as-namespace 2022-02-23 17:56:45 +01:00
Adrien Raffin-Caboisse
ae6a20e4d9 fix: add valid test identified by linter 2022-02-23 14:28:25 +01:00
Adrien Raffin-Caboisse
046116656b chore: update formatting 2022-02-23 14:22:21 +01:00
Adrien Raffin-Caboisse
972bef1194 feat: add length error if hostname too long 2022-02-23 14:21:46 +01:00
Adrien Raffin-Caboisse
4f1f235a2e feat: add strip_email_domain to normalization of namespace 2022-02-23 14:03:07 +01:00
Adrien Raffin-Caboisse
7e4709c13f fix(namespace): remove name validation for destroy and get 2022-02-23 13:35:57 +01:00
Adrien Raffin-Caboisse
cef0a2b0b3 fix(namespaces_test): fix missing namespace name 2022-02-23 11:40:48 +01:00
Adrien Raffin-Caboisse
fcdbe7c510 fix(utils_test): fix namespace name 2022-02-23 11:38:20 +01:00
Adrien Raffin-Caboisse
995731a29c fix(namespace): checknamespace name before actions
I keep the check server side because it's better from a security point of view.
2022-02-23 11:32:16 +01:00
Adrien Raffin-Caboisse
45727dbb21 feat(namespace): add check function for namespace 2022-02-23 11:32:14 +01:00
Adrien Raffin-Caboisse
fe0b43eaaf chore: update changelog 2022-02-22 21:20:59 +01:00
Adrien Raffin-Caboisse
afd4a3706e chore: update formating 2022-02-22 21:05:39 +01:00
Adrien Raffin-Caboisse
717250adb3 feat: removing matchmap from headscale 2022-02-22 20:58:08 +01:00
Adrien Raffin-Caboisse
0191ea93ff feat(oidc): bind email to namespace 2022-02-22 19:59:15 +01:00
Adrien Raffin-Caboisse
92ffac625e feat(namespace): add normalization function for namespace 2022-02-22 19:59:12 +01:00
Kristoffer Dalby
638a84adb9 Merge branch 'main' into integration-test-concurrent-join 2022-02-22 16:49:32 +00:00
Kristoffer Dalby
ec58979ce0 Merge branch 'main' into remove-shared 2022-02-22 16:48:14 +00:00
Kristoffer Dalby
7e6e093f17 Merge branch 'integration-test-concurrent-join' of github.com:kradalby/headscale into integration-test-concurrent-join 2022-02-22 16:19:28 +00:00
Kristoffer Dalby
4962335860 Remove dependency on CGO
This commit changes the SQLite dependency to one that does not depend on
CGO. It uses a C-to-Go translated sqlite library that is Pure go.
2022-02-22 16:18:25 +00:00
Kristoffer Dalby
f7eeb979fb Add timeout 2022-02-22 13:46:59 +00:00
Kristoffer Dalby
fe2f75d13d Allow integration test to retry 2022-02-22 07:40:56 +00:00
Kristoffer Dalby
8dca40535f Test if we can join headscale in parallell to speed up 2022-02-21 23:16:39 +00:00
Kristoffer Dalby
f4c302f1fb Uncomment tests that will failed in transition period 2022-02-21 23:10:20 +00:00
Kristoffer Dalby
4ca8181dcb Remove sharing from integration tests 2022-02-21 23:04:10 +00:00
Kristoffer Dalby
24a8e198a1 Remove sharing references across the code 2022-02-21 23:01:35 +00:00
Kristoffer Dalby
9411ec47c3 Remove sharing class and tests 2022-02-21 22:53:30 +00:00
Kristoffer Dalby
1e8f4dbdff Drop shared node table 2022-02-21 22:52:55 +00:00
Kristoffer Dalby
9399754489 Remove protobuf share/unshare generated go 2022-02-21 22:48:27 +00:00
Kristoffer Dalby
9d1752acbc Remove protobuf share/unshare 2022-02-21 22:48:14 +00:00
Kristoffer Dalby
6da2a19d10 Remove grpc share/unshare functions 2022-02-21 22:45:04 +00:00
Kristoffer Dalby
9ceac5c0fc Remove CLI and tests for Shared node 2022-02-21 22:44:08 +00:00
Nico Rey
e3bcc88880 Linter: make linter happy 2022-02-21 15:22:36 -03:00
Nico Rey
14e49885fb metrics/kustomize: update Kustomize examples 2022-02-21 12:51:25 -03:00
Nico Rey
fbc1843889 metrics/tests: update tests 2022-02-21 12:51:05 -03:00
Nico Rey
45d5ab30ff metrics/cfg: add a new entry for the Prometheus listen address 2022-02-21 12:50:44 -03:00
Nico Rey
d5fd7a5c00 metrics: add a new router and listener for Prometheus' metrics endpoint 2022-02-21 12:50:15 -03:00
Kristoffer Dalby
397b6fc4bf Merge branch 'main' into docs-acl-modifications 2022-02-18 20:13:10 +00:00
Adrien Raffin-Caboisse
55d746d3f5 docs(acls-proposal): wording comment
A hidden thing was implied in this document is that each person should have his own namespace.
Hidden information in spicification isn't good.
Thank's @kradalby for pointing it out.
2022-02-16 09:16:25 +01:00
Adrien Raffin-Caboisse
c364c2a382 chore(acl-proposals): apply prettier 2022-02-15 09:53:22 +01:00
Adrien Raffin-Caboisse
e540679dbd docs(acl-proposals): integrate comments 2022-02-15 09:52:05 +01:00
Adrien Raffin-Caboisse
86b329d8bf chore(docs): create proposals directory 2022-02-15 09:27:33 +01:00
Adrien Raffin-Caboisse
7bdd7748e4 fix(acl): add missing internal namespace communications 2022-02-10 12:03:03 +01:00
Adrien Raffin-Caboisse
0426212348 docs(acls): add example use case 2022-02-10 10:42:26 +01:00
Adrien Raffin-Caboisse
85cf443ac6 docs(acls): Issues with ACL and proposition 2022-02-08 16:59:35 +01:00
124 changed files with 10411 additions and 5102 deletions

View File

@@ -3,6 +3,7 @@
// development
integration_test.go
integration_test/
!integration_test/etc_embedded_derp/tls/server.crt
Dockerfile*
docker-compose*

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

View File

@@ -6,6 +6,8 @@ labels: ["bug"]
assignees: ""
---
<!-- Headscale is a multinational community across the globe. Our common language is English. Please consider raising the bug report in this language. -->
**Bug description**
<!-- A clear and concise description of what the bug is. Describe the expected bahavior

View File

@@ -7,5 +7,5 @@ contact_links:
url: "https://github.com/juanfont/headscale/blob/main/docs"
about: "Find documentation about how to configure and run headscale."
- name: "headscale Discord community"
url: "https://discord.com/invite/XcQxk2VHjx"
url: "https://discord.gg/xGj2TuqyxY"
about: "Please ask and answer questions about usage of headscale here."

View File

@@ -6,6 +6,8 @@ labels: ["enhancement"]
assignees: ""
---
<!-- Headscale is a multinational community across the globe. Our common language is English. Please consider raising the feature request in this language. -->
**Feature request**
<!-- A clear and precise description of what new or changed feature you want. -->

View File

@@ -6,6 +6,8 @@ labels: ["bug"]
assignees: ""
---
<!-- Headscale is a multinational community across the globe. Our common language is English. Please consider raising the issue in this language. -->
<!-- If you have a question, please consider using our Discord for asking questions -->
**Issue description**

View File

@@ -1,10 +1,10 @@
<!-- Please tick if the following things apply. You… -->
- [] read the [CONTRIBUTING guidelines](README.md#user-content-contributing)
- [] raised a GitHub issue or discussed it on the projects chat beforehand
- [] added unit tests
- [] added integration tests
- [] updated documentation if needed
- [] updated CHANGELOG.md
- [ ] read the [CONTRIBUTING guidelines](README.md#contributing)
- [ ] raised a GitHub issue or discussed it on the projects chat beforehand
- [ ] added unit tests
- [ ] added integration tests
- [ ] updated documentation if needed
- [ ] updated CHANGELOG.md
<!-- If applicable, please reference the issue using `Fixes #XXX` and add tests to cover your new code. -->

View File

@@ -22,30 +22,21 @@ jobs:
uses: tj-actions/changed-files@v14.1
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- name: Setup Go
- uses: cachix/install-nix-action@v16
if: steps.changed-files.outputs.any_changed == 'true'
uses: actions/setup-go@v2
with:
go-version: "1.17.7"
- name: Install dependencies
if: steps.changed-files.outputs.any_changed == 'true'
run: |
go version
sudo apt update
sudo apt install -y make
- name: Run build
if: steps.changed-files.outputs.any_changed == 'true'
run: make build
run: nix build
- uses: actions/upload-artifact@v2
if: steps.changed-files.outputs.any_changed == 'true'
with:
name: headscale-linux
path: headscale
path: result/bin/headscale

View File

@@ -10,6 +10,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Delete upstream contributor branch
# Allow continue on failure to account for when the
# upstream branch is deleted or does not exist.
continue-on-error: true
run: git push origin --delete update-contributors
- name: Create up-to-date contributors branch
run: git checkout -B update-contributors
- name: Push empty contributors branch
run: git push origin update-contributors
- name: Switch back to main
run: git checkout main
- uses: BobAnkh/add-contributors@v0.2.2
with:
CONTRIBUTOR: "## Contributors"

View File

@@ -16,6 +16,7 @@ jobs:
uses: tj-actions/changed-files@v14.1
with:
files: |
*.nix
go.*
**/*.go
integration_test/
@@ -25,7 +26,7 @@ jobs:
if: steps.changed-files.outputs.any_changed == 'true'
uses: golangci/golangci-lint-action@v2
with:
version: latest
version: v1.46.1
# Only block PRs on new problems.
# If this is not enabled, we will end up having PRs
@@ -45,6 +46,7 @@ jobs:
uses: tj-actions/changed-files@v14.1
with:
files: |
*.nix
**/*.md
**/*.yml
**/*.yaml

View File

@@ -18,7 +18,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.17.7
go-version: 1.18.0
- name: Install dependencies
run: |
@@ -89,6 +89,8 @@ jobs:
platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new
build-args: |
VERSION=${{ steps.meta.outputs.version }}
- name: Prepare cache for next build
run: |
rm -rf /tmp/.buildx-cache
@@ -153,6 +155,8 @@ jobs:
platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache-debug
cache-to: type=local,dest=/tmp/.buildx-cache-debug-new
build-args: |
VERSION=${{ steps.meta-debug.outputs.version }}
- name: Prepare cache for next build
run: |
rm -rf /tmp/.buildx-cache-debug
@@ -217,6 +221,8 @@ jobs:
platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache-alpine
cache-to: type=local,dest=/tmp/.buildx-cache-alpine-new
build-args: |
VERSION=${{ steps.meta-alpine.outputs.version }}
- name: Prepare cache for next build
run: |
rm -rf /tmp/.buildx-cache-alpine

View File

@@ -16,17 +16,20 @@ jobs:
uses: tj-actions/changed-files@v14.1
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- name: Setup Go
- uses: cachix/install-nix-action@v16
if: steps.changed-files.outputs.any_changed == 'true'
uses: actions/setup-go@v2
with:
go-version: "1.17.7"
- name: Run Integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: go test -tags integration -timeout 30m
uses: nick-fields/retry@v2
with:
timeout_minutes: 240
max_attempts: 5
retry_on: error
command: nix develop --command -- make test_integration

View File

@@ -16,28 +16,15 @@ jobs:
uses: tj-actions/changed-files@v14.1
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- name: Setup Go
- uses: cachix/install-nix-action@v16
if: steps.changed-files.outputs.any_changed == 'true'
uses: actions/setup-go@v2
with:
go-version: "1.17.7"
- name: Install dependencies
if: steps.changed-files.outputs.any_changed == 'true'
run: |
go version
sudo apt update
sudo apt install -y make
- name: Run tests
if: steps.changed-files.outputs.any_changed == 'true'
run: make test
- name: Run build
if: steps.changed-files.outputs.any_changed == 'true'
run: make
run: nix develop --check

6
.gitignore vendored
View File

@@ -27,3 +27,9 @@ derp.yaml
.idea
test_output/
# Nix build output
result
.direnv/
integration_test/etc/config.dump.yaml

View File

@@ -24,11 +24,14 @@ linters:
- tagliatelle
- godox
- ireturn
- execinquery
- exhaustruct
# We should strive to enable these:
- wrapcheck
- dupl
- makezero
- maintidx
# We might want to enable this, but it might be a lot of work
- cyclop

View File

@@ -1,7 +1,7 @@
---
before:
hooks:
- go mod tidy -compat=1.17
- go mod tidy -compat=1.18
release:
prerelease: auto
@@ -21,17 +21,15 @@ builds:
ldflags:
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
- id: linux-armhf
- id: darwin-arm64
main: ./cmd/headscale/headscale.go
mod_timestamp: "{{ .CommitTimestamp }}"
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
goarch:
- arm
goarm:
- "7"
- arm64
flags:
- -mod=readonly
ldflags:
@@ -65,7 +63,7 @@ archives:
- id: golang-cross
builds:
- darwin-amd64
- linux-armhf
- darwin-arm64
- linux-amd64
- linux-arm64
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"

View File

@@ -1,18 +1,89 @@
# CHANGELOG
**TBD (TBD):**
## 0.17.0 (2022-xx-xx)
**0.14.0 (2022-xx-xx):**
## 0.16.0 (2022-07-25)
**UPCOMING BREAKING**:
From the **next** version (`0.15.0`), all machines will be able to communicate regardless of
**Note:** Take a backup of your database before upgrading.
### BREAKING
- Old ACL syntax is no longer supported ("users" & "ports" -> "src" & "dst"). Please check [the new syntax](https://tailscale.com/kb/1018/acls/).
### Changes
- **Drop** armhf (32-bit ARM) support. [#609](https://github.com/juanfont/headscale/pull/609)
- Headscale fails to serve if the ACL policy file cannot be parsed [#537](https://github.com/juanfont/headscale/pull/537)
- Fix labels cardinality error when registering unknown pre-auth key [#519](https://github.com/juanfont/headscale/pull/519)
- Fix send on closed channel crash in polling [#542](https://github.com/juanfont/headscale/pull/542)
- Fixed spurious calls to setLastStateChangeToNow from ephemeral nodes [#566](https://github.com/juanfont/headscale/pull/566)
- Add command for moving nodes between namespaces [#362](https://github.com/juanfont/headscale/issues/362)
- Added more configuration parameters for OpenID Connect (scopes, free-form paramters, domain and user allowlist)
- Add command to set tags on a node [#525](https://github.com/juanfont/headscale/issues/525)
- Add command to view tags of nodes [#356](https://github.com/juanfont/headscale/issues/356)
- Add --all (-a) flag to enable routes command [#360](https://github.com/juanfont/headscale/issues/360)
- Fix issue where nodes was not updated across namespaces [#560](https://github.com/juanfont/headscale/pull/560)
- Add the ability to rename a nodes name [#560](https://github.com/juanfont/headscale/pull/560)
- Node DNS names are now unique, a random suffix will be added when a node joins
- This change contains database changes, remember to **backup** your database before upgrading
- Add option to enable/disable logtail (Tailscale's logging infrastructure) [#596](https://github.com/juanfont/headscale/pull/596)
- 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)
- 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) [677](https://github.com/juanfont/headscale/pull/677)
- Make tailnet node updates check interval configurable [#675](https://github.com/juanfont/headscale/pull/675)
- Fix regression with HTTP API [#684](https://github.com/juanfont/headscale/pull/684)
- nodes ls now print both Hostname and Name(Issue [#647](https://github.com/juanfont/headscale/issues/647) PR [#687](https://github.com/juanfont/headscale/pull/687))
## 0.15.0 (2022-03-20)
**Note:** Take a backup of your database before upgrading.
### BREAKING
- Boundaries between Namespaces has been removed and all nodes can communicate by default [#357](https://github.com/juanfont/headscale/pull/357)
- To limit access between nodes, use [ACLs](./docs/acls.md).
- `/metrics` is now a configurable host:port endpoint: [#344](https://github.com/juanfont/headscale/pull/344). You must update your `config.yaml` file to include:
```yaml
metrics_listen_addr: 127.0.0.1:9090
```
### Features
- Add support for writing ACL files with YAML [#359](https://github.com/juanfont/headscale/pull/359)
- Users can now use emails in ACL's groups [#372](https://github.com/juanfont/headscale/issues/372)
- Add shorthand aliases for commands and subcommands [#376](https://github.com/juanfont/headscale/pull/376)
- Add `/windows` endpoint for Windows configuration instructions + registry file download [#392](https://github.com/juanfont/headscale/pull/392)
- Added embedded DERP (and STUN) server into Headscale [#388](https://github.com/juanfont/headscale/pull/388)
### Changes
- Fix a bug were the same IP could be assigned to multiple hosts if joined in quick succession [#346](https://github.com/juanfont/headscale/pull/346)
- Simplify the code behind registration of machines [#366](https://github.com/juanfont/headscale/pull/366)
- Nodes are now only written to database if they are registrated successfully
- Fix a limitation in the ACLs that prevented users to write rules with `*` as source [#374](https://github.com/juanfont/headscale/issues/374)
- Reduce the overhead of marshal/unmarshal for Hostinfo, routes and endpoints by using specific types in Machine [#371](https://github.com/juanfont/headscale/pull/371)
- Apply normalization function to FQDN on hostnames when hosts registers and retrieve informations [#363](https://github.com/juanfont/headscale/issues/363)
- Fix a bug that prevented the use of `tailscale logout` with OIDC [#508](https://github.com/juanfont/headscale/issues/508)
- Added Tailscale repo HEAD and unstable releases channel to the integration tests targets [#513](https://github.com/juanfont/headscale/pull/513)
## 0.14.0 (2022-02-24)
**UPCOMING ### BREAKING
From the **next\*\* version (`0.15.0`), all machines will be able to communicate regardless of
if they are in the same namespace. This means that the behaviour currently limited to ACLs
will become default. From version `0.15.0`, all limitation of communications must be done
with ACLs.
This is a part of aligning `headscale`'s behaviour with Tailscale's upstream behaviour.
**BREAKING**:
### BREAKING
- ACLs have been rewritten to align with the bevaviour Tailscale Control Panel provides. **NOTE:** This is only active if you use ACLs
- Namespaces are now treated as Users
@@ -20,24 +91,28 @@ This is a part of aligning `headscale`'s behaviour with Tailscale's upstream beh
- Tags should now work correctly and adding a host to Headscale should now reload the rules.
- The documentation have a [fictional example](docs/acls.md) that should cover some use cases of the ACLs features
**Features**:
### Features
- Add support for configurable mTLS [docs](docs/tls.md#configuring-mutual-tls-authentication-mtls) [#297](https://github.com/juanfont/headscale/pull/297)
**Changes**:
### Changes
- Remove dependency on CGO (switch from CGO SQLite to pure Go) [#346](https://github.com/juanfont/headscale/pull/346)
**0.13.0 (2022-02-18):**
**Features**:
### Features
- Add IPv6 support to the prefix assigned to namespaces
- Add API Key support
- Enable remote control of `headscale` via CLI [docs](docs/remote-cli.md)
- Enable HTTP API (beta, subject to change)
- OpenID Connect users will be mapped per namespaces
- Each user will get its own namespace, created if it does not exist
- `oidc.domain_map` option has been removed
- `strip_email_domain` option has been added (see [config-example.yaml](./config_example.yaml))
**Changes**:
### Changes
- `ip_prefix` is now superseded by `ip_prefixes` in the configuration [#208](https://github.com/juanfont/headscale/pull/208)
- Upgrade `tailscale` (1.20.4) and other dependencies to latest [#314](https://github.com/juanfont/headscale/pull/314)
@@ -46,35 +121,35 @@ This is a part of aligning `headscale`'s behaviour with Tailscale's upstream beh
**0.12.4 (2022-01-29):**
**Changes**:
### Changes
- Make gRPC Unix Socket permissions configurable [#292](https://github.com/juanfont/headscale/pull/292)
- Trim whitespace before reading Private Key from file [#289](https://github.com/juanfont/headscale/pull/289)
- Add new command to generate a private key for `headscale` [#290](https://github.com/juanfont/headscale/pull/290)
- Fixed issue where hosts deleted from control server may be written back to the database, as long as they are connected to the control server [#278](https://github.com/juanfont/headscale/pull/278)
**0.12.3 (2022-01-13):**
## 0.12.3 (2022-01-13)
**Changes**:
### Changes
- Added Alpine container [#270](https://github.com/juanfont/headscale/pull/270)
- Minor updates in dependencies [#271](https://github.com/juanfont/headscale/pull/271)
**0.12.2 (2022-01-11):**
## 0.12.2 (2022-01-11)
Happy New Year!
**Changes**:
### Changes
- Fix Docker release [#258](https://github.com/juanfont/headscale/pull/258)
- Rewrite main docs [#262](https://github.com/juanfont/headscale/pull/262)
- Improve Docker docs [#263](https://github.com/juanfont/headscale/pull/263)
**0.12.1 (2021-12-24):**
## 0.12.1 (2021-12-24)
(We are skipping 0.12.0 to correct a mishap done weeks ago with the version tagging)
**BREAKING**:
### BREAKING
- Upgrade to Tailscale 1.18 [#229](https://github.com/juanfont/headscale/pull/229)
- This change requires a new format for private key, private keys are now generated automatically:
@@ -82,19 +157,19 @@ Happy New Year!
2. Restart `headscale`, a new key will be generated.
3. Restart all Tailscale clients to fetch the new key
**Changes**:
### Changes
- Unify configuration example [#197](https://github.com/juanfont/headscale/pull/197)
- Add stricter linting and formatting [#223](https://github.com/juanfont/headscale/pull/223)
**Features**:
### Features
- Add gRPC and HTTP API (HTTP API is currently disabled) [#204](https://github.com/juanfont/headscale/pull/204)
- Use gRPC between the CLI and the server [#206](https://github.com/juanfont/headscale/pull/206), [#212](https://github.com/juanfont/headscale/pull/212)
- Beta OpenID Connect support [#126](https://github.com/juanfont/headscale/pull/126), [#227](https://github.com/juanfont/headscale/pull/227)
**0.11.0 (2021-10-25):**
## 0.11.0 (2021-10-25)
**BREAKING**:
### BREAKING
- Make headscale fetch DERP map from URL and file [#196](https://github.com/juanfont/headscale/pull/196)

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
# Builder image
FROM docker.io/golang:1.17.7-bullseye AS build
FROM --platform=$BUILDPLATFORM docker.io/golang:1.18.0-bullseye AS build
ARG VERSION=dev
ENV GOPATH /go
WORKDIR /go/src/headscale
@@ -8,7 +9,8 @@ RUN go mod download
COPY . .
RUN GGO_ENABLED=0 GOOS=linux go install -a ./cmd/headscale
ARG TARGETOS TARGETARCH
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
# Debug image

View File

@@ -1,11 +1,17 @@
FROM ubuntu:latest
ARG TAILSCALE_VERSION
ARG TAILSCALE_VERSION=*
ARG TAILSCALE_CHANNEL=stable
RUN apt-get update \
&& apt-get install -y gnupg curl \
&& curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/focal.gpg | apt-key add - \
&& curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/focal.list | tee /etc/apt/sources.list.d/tailscale.list \
&& curl -fsSL https://pkgs.tailscale.com/${TAILSCALE_CHANNEL}/ubuntu/focal.gpg | apt-key add - \
&& curl -fsSL https://pkgs.tailscale.com/${TAILSCALE_CHANNEL}/ubuntu/focal.list | tee /etc/apt/sources.list.d/tailscale.list \
&& apt-get update \
&& apt-get install -y tailscale=${TAILSCALE_VERSION} dnsutils \
&& apt-get install -y ca-certificates tailscale=${TAILSCALE_VERSION} dnsutils \
&& rm -rf /var/lib/apt/lists/*
ADD integration_test/etc_embedded_derp/tls/server.crt /usr/local/share/ca-certificates/
RUN chmod 644 /usr/local/share/ca-certificates/server.crt
RUN update-ca-certificates

21
Dockerfile.tailscale-HEAD Normal file
View File

@@ -0,0 +1,21 @@
FROM golang:latest
RUN apt-get update \
&& apt-get install -y ca-certificates dnsutils git iptables \
&& rm -rf /var/lib/apt/lists/*
RUN git clone https://github.com/tailscale/tailscale.git
WORKDIR tailscale
RUN sh build_dist.sh tailscale.com/cmd/tailscale
RUN sh build_dist.sh tailscale.com/cmd/tailscaled
RUN cp tailscale /usr/local/bin/
RUN cp tailscaled /usr/local/bin/
ADD integration_test/etc_embedded_derp/tls/server.crt /usr/local/share/ca-certificates/
RUN chmod 644 /usr/local/share/ca-certificates/server.crt
RUN update-ca-certificates

View File

@@ -0,0 +1,21 @@
# Builder image
FROM docker.io/golang:1.18.0-bullseye AS build
ARG VERSION=dev
ENV GOPATH /go
WORKDIR /go/src/headscale
COPY go.mod go.sum /go/src/headscale/
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /go/bin/headscale -ldflags="-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$VERSION" -a ./cmd/headscale
RUN test -e /go/bin/headscale
# Production image
FROM gcr.io/distroless/base-debian11
COPY --from=build /go/bin/headscale /bin/headscale
ENV TZ UTC
EXPOSE 8080/tcp
CMD ["headscale"]

View File

@@ -1,8 +1,15 @@
# Calculate version
version = $(shell ./scripts/version-at-commit.sh)
version ?= $(shell git describe --always --tags --dirty)
rwildcard=$(foreach d,$(wildcard $1*),$(call rwildcard,$d/,$2) $(filter $(subst *,%,$2),$d))
# Determine if OS supports pie
GOOS ?= $(shell uname | tr '[:upper:]' '[:lower:]')
ifeq ($(filter $(GOOS), openbsd netbsd soloaris plan9), )
pieflags = -buildmode=pie
else
endif
# GO_SOURCES = $(wildcard *.go)
# PROTO_SOURCES = $(wildcard **/*.proto)
GO_SOURCES = $(call rwildcard,,*.go)
@@ -10,7 +17,7 @@ PROTO_SOURCES = $(call rwildcard,,*.proto)
build:
GGO_ENABLED=0 go build -ldflags "-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$(version)" cmd/headscale/headscale.go
GOOS=$(GOOS) CGO_ENABLED=0 go build -trimpath $(pieflags) -mod=readonly -ldflags "-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$(version)" cmd/headscale/headscale.go
dev: lint test build
@@ -18,11 +25,14 @@ test:
@go test -coverprofile=coverage.out ./...
test_integration:
go test -tags integration -timeout 30m -count=1 ./...
go test -failfast -tags integration -timeout 30m -count=1 ./...
test_integration_cli:
go test -tags integration -v integration_cli_test.go integration_common_test.go
test_integration_derp:
go test -tags integration -v integration_embedded_derp_test.go integration_common_test.go
coverprofile_func:
go tool cover -func=coverage.out
@@ -38,14 +48,14 @@ fmt:
clang-format -style="{BasedOnStyle: Google, IndentWidth: 4, AlignConsecutiveDeclarations: true, AlignConsecutiveAssignments: true, ColumnLimit: 0}" -i $(PROTO_SOURCES)
proto-lint:
cd proto/ && buf lint
cd proto/ && go run github.com/bufbuild/buf/cmd/buf lint
compress: build
upx --brute headscale
generate:
rm -rf gen
buf generate proto
go run github.com/bufbuild/buf/cmd/buf generate proto
install-protobuf-plugins:
go install \

325
README.md
View File

@@ -2,44 +2,68 @@
![ci](https://github.com/juanfont/headscale/actions/workflows/test.yml/badge.svg)
An open source, self-hosted implementation of the Tailscale coordination server.
An open source, self-hosted implementation of the Tailscale control server.
Join our [Discord](https://discord.gg/XcQxk2VHjx) server for a chat.
Join our [Discord](https://discord.gg/c84AZQhmpx) server for a chat.
**Note:** Always select the same GitHub tag as the released version you use to ensure you have the correct example configuration and documentation. The `main` branch might contain unreleased changes.
**Note:** Always select the same GitHub tag as the released version you use
to ensure you have the correct example configuration and documentation.
The `main` branch might contain unreleased changes.
## Overview
## What is Tailscale
Tailscale is [a modern VPN](https://tailscale.com/) built on top of [Wireguard](https://www.wireguard.com/). It [works like an overlay network](https://tailscale.com/blog/how-tailscale-works/) between the computers of your networks - using [NAT traversal](https://tailscale.com/blog/how-nat-traversal-works/).
Tailscale is [a modern VPN](https://tailscale.com/) built on top of
[Wireguard](https://www.wireguard.com/).
It [works like an overlay network](https://tailscale.com/blog/how-tailscale-works/)
between the computers of your networks - using
[NAT traversal](https://tailscale.com/blog/how-nat-traversal-works/).
Everything in Tailscale is Open Source, except the GUI clients for proprietary OS (Windows and macOS/iOS), and the 'coordination/control server'.
Everything in Tailscale is Open Source, except the GUI clients for proprietary OS
(Windows and macOS/iOS), and the control server.
The control server works as an exchange point of Wireguard public keys for the nodes in the Tailscale network. It also assigns the IP addresses of the clients, creates the boundaries between each user, enables sharing machines between users, and exposes the advertised routes of your nodes.
The control server works as an exchange point of Wireguard public keys for the
nodes in the Tailscale network. It assigns the IP addresses of the clients,
creates the boundaries between each user, enables sharing machines between users,
and exposes the advertised routes of your nodes.
headscale implements this coordination server.
A [Tailscale network (tailnet)](https://tailscale.com/kb/1136/tailnet/) is private
network which Tailscale assigns to a user in terms of private users or an
organisation.
## Design goal
`headscale` aims to implement a self-hosted, open source alternative to the Tailscale
control server. `headscale` has a narrower scope and an instance of `headscale`
implements a _single_ Tailnet, which is typically what a single organisation, or
home/personal setup would use.
`headscale` uses terms that maps to Tailscale's control server, consult the
[glossary](./docs/glossary.md) for explainations.
## Support
If you like `headscale` and find it useful, there is sponsorship and donation buttons available in the repo.
If you like `headscale` and find it useful, there is a sponsorship and donation
buttons available in the repo.
If you would like to sponsor features, bugs or prioritisation, reach out to one of the maintainers.
If you would like to sponsor features, bugs or prioritisation, reach out to
one of the maintainers.
## Status
## Features
- [x] Base functionality (nodes can communicate with each other)
- [x] Node registration through the web flow
- [x] Network changes are relayed to the nodes
- [x] Namespaces support (~tailnets in Tailscale.com naming)
- [x] Routing (advertise & accept, including exit nodes)
- [x] Node registration via pre-auth keys (including reusable keys, and ephemeral node support)
- [x] JSON-formatted output
- [x] ACLs
- [x] Taildrop (File Sharing)
- [x] Support for alternative IP ranges in the tailnets (default Tailscale's 100.64.0.0/10)
- [x] DNS (passing DNS servers to nodes)
- [x] Single-Sign-On (via Open ID Connect)
- [x] Share nodes between namespaces
- [x] MagicDNS (see `docs/`)
- Full "base" support of Tailscale's features
- Configurable DNS
- [Split DNS](https://tailscale.com/kb/1054/dns/#using-dns-settings-in-the-admin-console)
- Node registration
- Single-Sign-On (via Open ID Connect)
- Pre authenticated key
- Taildrop (File Sharing)
- [Access control lists](https://tailscale.com/kb/1018/acls/)
- [MagicDNS](https://tailscale.com/kb/1081/magicdns)
- Support for multiple IP ranges in the tailnet
- Dual stack (IPv4 and IPv6)
- Routing advertising (including exit nodes)
- Ephemeral nodes
- Embedded [DERP server](https://tailscale.com/blog/how-tailscale-works/#encrypted-tcp-relays-derp)
## Client OS support
@@ -53,10 +77,6 @@ If you would like to sponsor features, bugs or prioritisation, reach out to one
| Android | [You need to compile the client yourself](https://github.com/juanfont/headscale/issues/58#issuecomment-885255270) |
| iOS | Not yet |
## Roadmap
Suggestions/PRs welcomed!
## Running headscale
Please have a look at the documentation under [`docs/`](docs/).
@@ -68,11 +88,19 @@ Please have a look at the documentation under [`docs/`](docs/).
## Contributing
To contribute to Headscale you would need the lastest version of [Go](https://golang.org) and [Buf](https://buf.build)(Protobuf generator).
To contribute to headscale you would need the lastest version of [Go](https://golang.org)
and [Buf](https://buf.build)(Protobuf generator).
We recommend using [Nix](https://nixos.org/) to setup a development environment. This can
be done with `nix develop`, which will install the tools and give you a shell.
This guarantees that you will have the same dev env as `headscale` maintainers.
PRs and suggestions are welcome.
### Code style
To ensure we have some consistency with a growing number of contributions, this project has adopted linting and style/formatting rules:
To ensure we have some consistency with a growing number of contributions,
this project has adopted linting and style/formatting rules:
The **Go** code is linted with [`golangci-lint`](https://golangci-lint.run) and
formatted with [`golines`](https://github.com/segmentio/golines) (width 88) and
@@ -91,15 +119,18 @@ Check out the `.golangci.yaml` and `Makefile` to see the specific configuration.
- Go
- Buf
- Protobuf tools:
- Protobuf tools
Install and activate:
```shell
make install-protobuf-plugins
nix develop
```
### Testing and building
Some parts of the project require the generation of Go code from Protobuf (if changes are made in `proto/`) and it must be (re-)generated with:
Some parts of the project require the generation of Go code from Protobuf
(if changes are made in `proto/`) and it must be (re-)generated with:
```shell
make generate
@@ -115,6 +146,12 @@ make test
To build the program:
```shell
nix build
```
or
```shell
make build
```
@@ -137,6 +174,13 @@ make build
<sub style="font-size:14px"><b>Juan Font</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/restanrm>
<img src=https://avatars.githubusercontent.com/u/4344371?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Adrien Raffin-Caboisse/>
<br />
<sub style="font-size:14px"><b>Adrien Raffin-Caboisse</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/cure>
<img src=https://avatars.githubusercontent.com/u/149135?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Ward Vandewege/>
@@ -145,17 +189,33 @@ make build
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/ohdearaugustin>
<img src=https://avatars.githubusercontent.com/u/14001491?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ohdearaugustin/>
<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>ohdearaugustin</b></sub>
<sub style="font-size:14px"><b>Jiang Zhu</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/restanrm>
<img src=https://avatars.githubusercontent.com/u/4344371?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Adrien Raffin-Caboisse/>
<a href=https://github.com/reynico>
<img src=https://avatars.githubusercontent.com/u/715768?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Nico/>
<br />
<sub style="font-size:14px"><b>Adrien Raffin-Caboisse</b></sub>
<sub style="font-size:14px"><b>Nico</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/e-zk>
<img src=https://avatars.githubusercontent.com/u/58356365?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=e-zk/>
<br />
<sub style="font-size:14px"><b>e-zk</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/arch4ngel>
<img src=https://avatars.githubusercontent.com/u/11574161?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Justin Angel/>
<br />
<sub style="font-size:14px"><b>Justin Angel</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
@@ -165,8 +225,6 @@ make build
<sub style="font-size:14px"><b>Alessandro (Ale) Segala</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/unreality>
<img src=https://avatars.githubusercontent.com/u/352522?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=unreality/>
@@ -174,6 +232,29 @@ make build
<sub style="font-size:14px"><b>unreality</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/mpldr>
<img src=https://avatars.githubusercontent.com/u/33086936?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Moritz Poldrack/>
<br />
<sub style="font-size:14px"><b>Moritz Poldrack</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/ohdearaugustin>
<img src=https://avatars.githubusercontent.com/u/14001491?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ohdearaugustin/>
<br />
<sub style="font-size:14px"><b>ohdearaugustin</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/Niek>
<img src=https://avatars.githubusercontent.com/u/213140?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Niek van der Maas/>
<br />
<sub style="font-size:14px"><b>Niek van der Maas</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/negbie>
<img src=https://avatars.githubusercontent.com/u/20154956?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Eugen Biegler/>
@@ -188,6 +269,13 @@ make build
<sub style="font-size:14px"><b>Aaron Bieber</b></sub>
</a>
</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">
<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/>
@@ -195,6 +283,15 @@ make build
<sub style="font-size:14px"><b>Fernando De Lucchi</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/GrigoriyMikhalkin>
<img src=https://avatars.githubusercontent.com/u/3637857?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=GrigoriyMikhalkin/>
<br />
<sub style="font-size:14px"><b>GrigoriyMikhalkin</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/hdhoang>
<img src=https://avatars.githubusercontent.com/u/12537?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Hoàng Đức Hiếu/>
@@ -202,6 +299,34 @@ make build
<sub style="font-size:14px"><b>Hoàng Đức Hiếu</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/bravechamp>
<img src=https://avatars.githubusercontent.com/u/48980452?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=bravechamp/>
<br />
<sub style="font-size:14px"><b>bravechamp</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/deonthomasgy>
<img src=https://avatars.githubusercontent.com/u/150036?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Deon Thomas/>
<br />
<sub style="font-size:14px"><b>Deon Thomas</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<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">
<a href=https://github.com/mevansam>
<img src=https://avatars.githubusercontent.com/u/403630?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Mevan Samaratunga/>
<br />
<sub style="font-size:14px"><b>Mevan Samaratunga</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/dragetd>
<img src=https://avatars.githubusercontent.com/u/3639577?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Michael G./>
@@ -219,17 +344,10 @@ make build
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/cmars>
<img src=https://avatars.githubusercontent.com/u/23741?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Casey Marshall/>
<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>Casey Marshall</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<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/>
<br />
<sub style="font-size:14px"><b>Silver Bullet</b></sub>
<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">
@@ -240,10 +358,40 @@ make build
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/lachy-2849>
<img src=https://avatars.githubusercontent.com/u/98844035?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=lachy-2849/>
<a href=https://github.com/artemklevtsov>
<img src=https://avatars.githubusercontent.com/u/603798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Artem Klevtsov/>
<br />
<sub style="font-size:14px"><b>lachy-2849</b></sub>
<sub style="font-size:14px"><b>Artem Klevtsov</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/cmars>
<img src=https://avatars.githubusercontent.com/u/23741?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Casey Marshall/>
<br />
<sub style="font-size:14px"><b>Casey Marshall</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/pvinis>
<img src=https://avatars.githubusercontent.com/u/100233?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Pavlos Vinieratos/>
<br />
<sub style="font-size:14px"><b>Pavlos Vinieratos</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/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/>
<br />
<sub style="font-size:14px"><b>Silver Bullet</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/lachy2849>
<img src=https://avatars.githubusercontent.com/u/98844035?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=lachy2849/>
<br />
<sub style="font-size:14px"><b>lachy2849</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
@@ -253,8 +401,6 @@ make build
<sub style="font-size:14px"><b>thomas</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/aberoham>
<img src=https://avatars.githubusercontent.com/u/586805?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Abraham Ingersoll/>
@@ -263,12 +409,21 @@ make build
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/artemklevtsov>
<img src=https://avatars.githubusercontent.com/u/603798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Artem Klevtsov/>
<a href=https://github.com/apognu>
<img src=https://avatars.githubusercontent.com/u/3017182?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Antoine POPINEAU/>
<br />
<sub style="font-size:14px"><b>Artem Klevtsov</b></sub>
<sub style="font-size:14px"><b>Antoine POPINEAU</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/aofei>
<img src=https://avatars.githubusercontent.com/u/5037285?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Aofei Sheng/>
<br />
<sub style="font-size:14px"><b>Aofei Sheng</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/awoimbee>
<img src=https://avatars.githubusercontent.com/u/22431493?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Arthur Woimbée/>
@@ -283,6 +438,20 @@ make build
<sub style="font-size:14px"><b>Bryan Stenson</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/yangchuansheng>
<img src=https://avatars.githubusercontent.com/u/15308462?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt= Carson Yang/>
<br />
<sub style="font-size:14px"><b> Carson Yang</b></sub>
</a>
</td>
<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">
<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/>
@@ -334,6 +503,15 @@ make build
<sub style="font-size:14px"><b>rcursaru</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/renovate-bot>
<img src=https://avatars.githubusercontent.com/u/25180681?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=WhiteSource Renovate/>
<br />
<sub style="font-size:14px"><b>WhiteSource Renovate</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/ryanfowler>
<img src=https://avatars.githubusercontent.com/u/2668821?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Ryan Fowler/>
@@ -341,8 +519,6 @@ make build
<sub style="font-size:14px"><b>Ryan Fowler</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/shaananc>
<img src=https://avatars.githubusercontent.com/u/2287839?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Shaanan Cohney/>
@@ -378,6 +554,8 @@ make build
<sub style="font-size:14px"><b>Tianon Gravi</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/woudsma>
<img src=https://avatars.githubusercontent.com/u/6162978?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tjerk Woudsma/>
@@ -385,8 +563,13 @@ make build
<sub style="font-size:14px"><b>Tjerk Woudsma</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/y0ngb1n>
<img src=https://avatars.githubusercontent.com/u/25719408?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Yang Bin/>
<br />
<sub style="font-size:14px"><b>Yang Bin</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/zekker6>
<img src=https://avatars.githubusercontent.com/u/1367798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Zakhar Bessarab/>
@@ -396,9 +579,9 @@ make build
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/Bpazy>
<img src=https://avatars.githubusercontent.com/u/9838749?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ZiYuan/>
<img src=https://avatars.githubusercontent.com/u/9838749?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Ziyuan Han/>
<br />
<sub style="font-size:14px"><b>ZiYuan</b></sub>
<sub style="font-size:14px"><b>Ziyuan Han</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
@@ -409,12 +592,14 @@ make build
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/e-zk>
<img src=https://avatars.githubusercontent.com/u/58356365?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=e-zk/>
<a href=https://github.com/nning>
<img src=https://avatars.githubusercontent.com/u/557430?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=henning mueller/>
<br />
<sub style="font-size:14px"><b>e-zk</b></sub>
<sub style="font-size:14px"><b>henning mueller</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/ignoramous>
<img src=https://avatars.githubusercontent.com/u/852289?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ignoramous/>
@@ -429,8 +614,6 @@ make build
<sub style="font-size:14px"><b>lion24</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/pernila>
<img src=https://avatars.githubusercontent.com/u/12460060?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=pernila/>

278
acls.go
View File

@@ -2,25 +2,28 @@ package headscale
import (
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/rs/zerolog/log"
"github.com/tailscale/hujson"
"gopkg.in/yaml.v3"
"inet.af/netaddr"
"tailscale.com/tailcfg"
)
const (
errEmptyPolicy = Error("empty policy")
errInvalidAction = Error("invalid action")
errInvalidUserSection = Error("invalid user section")
errInvalidGroup = Error("invalid group")
errInvalidTag = Error("invalid tag")
errInvalidPortFormat = Error("invalid port format")
errEmptyPolicy = Error("empty policy")
errInvalidAction = Error("invalid action")
errInvalidGroup = Error("invalid group")
errInvalidTag = Error("invalid tag")
errInvalidPortFormat = Error("invalid port format")
errWildcardIsNeeded = Error("wildcard as port is required for the protocol")
)
const (
@@ -34,6 +37,23 @@ const (
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.
func (h *Headscale) LoadACLPolicy(path string) error {
log.Debug().
@@ -53,16 +73,36 @@ func (h *Headscale) LoadACLPolicy(path string) error {
return err
}
ast, err := hujson.Parse(policyBytes)
if err != nil {
return err
}
ast.Standardize()
policyBytes = ast.Pack()
err = json.Unmarshal(policyBytes, &policy)
if err != nil {
return err
switch filepath.Ext(path) {
case ".yml", ".yaml":
log.Debug().
Str("path", path).
Bytes("file", policyBytes).
Msg("Loading ACLs from YAML")
err := yaml.Unmarshal(policyBytes, &policy)
if err != nil {
return err
}
log.Trace().
Interface("policy", policy).
Msg("Loaded policy from YAML")
default:
ast, err := hujson.Parse(policyBytes)
if err != nil {
return err
}
ast.Standardize()
policyBytes = ast.Pack()
err = json.Unmarshal(policyBytes, &policy)
if err != nil {
return err
}
}
if policy.IsZero() {
return errEmptyPolicy
}
@@ -90,7 +130,7 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
return nil, errEmptyPolicy
}
machines, err := h.ListAllMachines()
machines, err := h.ListMachines()
if err != nil {
return nil, err
}
@@ -101,23 +141,31 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
}
srcIPs := []string{}
for innerIndex, user := range acl.Users {
srcs, err := h.generateACLPolicySrcIP(machines, *h.aclPolicy, user)
for innerIndex, src := range acl.Sources {
srcs, err := h.generateACLPolicySrcIP(machines, *h.aclPolicy, src)
if err != nil {
log.Error().
Msgf("Error parsing ACL %d, User %d", index, innerIndex)
Msgf("Error parsing ACL %d, Source %d", index, innerIndex)
return nil, err
}
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{}
for innerIndex, ports := range acl.Ports {
dests, err := h.generateACLPolicyDestPorts(machines, *h.aclPolicy, ports)
for innerIndex, dest := range acl.Destinations {
dests, err := h.generateACLPolicyDest(machines, *h.aclPolicy, dest, needsWildcard)
if err != nil {
log.Error().
Msgf("Error parsing ACL %d, Port %d", index, innerIndex)
Msgf("Error parsing ACL %d, Destination %d", index, innerIndex)
return nil, err
}
@@ -127,6 +175,7 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
rules = append(rules, tailcfg.FilterRule{
SrcIPs: srcIPs,
DstPorts: destPorts,
IPProto: protocols,
})
}
@@ -136,17 +185,18 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
func (h *Headscale) generateACLPolicySrcIP(
machines []Machine,
aclPolicy ACLPolicy,
u string,
src string,
) ([]string, error) {
return expandAlias(machines, aclPolicy, u)
return expandAlias(machines, aclPolicy, src, h.cfg.OIDC.StripEmaildomain)
}
func (h *Headscale) generateACLPolicyDestPorts(
func (h *Headscale) generateACLPolicyDest(
machines []Machine,
aclPolicy ACLPolicy,
d string,
dest string,
needsWildcard bool,
) ([]tailcfg.NetPortRange, error) {
tokens := strings.Split(d, ":")
tokens := strings.Split(dest, ":")
if len(tokens) < expectedTokenItems || len(tokens) > 3 {
return nil, errInvalidPortFormat
}
@@ -164,11 +214,16 @@ func (h *Headscale) generateACLPolicyDestPorts(
alias = fmt.Sprintf("%s:%s", tokens[0], tokens[1])
}
expanded, err := expandAlias(machines, aclPolicy, alias)
expanded, err := expandAlias(
machines,
aclPolicy,
alias,
h.cfg.OIDC.StripEmaildomain,
)
if err != nil {
return nil, err
}
ports, err := expandPorts(tokens[len(tokens)-1])
ports, err := expandPorts(tokens[len(tokens)-1], needsWildcard)
if err != nil {
return nil, err
}
@@ -187,6 +242,54 @@ func (h *Headscale) generateACLPolicyDestPorts(
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
// - a namespace
// - a group
@@ -196,14 +299,19 @@ func expandAlias(
machines []Machine,
aclPolicy ACLPolicy,
alias string,
stripEmailDomain bool,
) ([]string, error) {
ips := []string{}
if alias == "*" {
return []string{"*"}, nil
}
log.Debug().
Str("alias", alias).
Msg("Expanding")
if strings.HasPrefix(alias, "group:") {
namespaces, err := expandGroup(aclPolicy, alias)
namespaces, err := expandGroup(aclPolicy, alias, stripEmailDomain)
if err != nil {
return ips, err
}
@@ -218,24 +326,38 @@ func expandAlias(
}
if strings.HasPrefix(alias, "tag:") {
owners, err := expandTagOwners(aclPolicy, alias)
if err != nil {
return ips, err
// check for forced tags
for _, machine := range machines {
if contains(machine.ForcedTags, alias) {
ips = append(ips, machine.IPAddresses.ToStringSlice()...)
}
}
// find tag owners
owners, err := expandTagOwners(aclPolicy, alias, stripEmailDomain)
if err != nil {
if errors.Is(err, errInvalidTag) {
if len(ips) == 0 {
return ips, fmt.Errorf(
"%w. %v isn't owned by a TagOwner and no forced tags are defined",
errInvalidTag,
alias,
)
}
return ips, nil
} else {
return ips, err
}
}
// filter out machines per tag owner
for _, namespace := range owners {
machines := filterMachinesByNamespace(machines, namespace)
for _, machine := range machines {
if len(machine.HostInfo) == 0 {
continue
}
hi, err := machine.GetHostInfo()
if err != nil {
return ips, err
}
for _, t := range hi.RequestTags {
if alias == t {
ips = append(ips, machine.IPAddresses.ToStringSlice()...)
}
hi := machine.GetHostInfo()
if contains(hi.RequestTags, alias) {
ips = append(ips, machine.IPAddresses.ToStringSlice()...)
}
}
}
@@ -245,10 +367,8 @@ func expandAlias(
// if alias is a namespace
nodes := filterMachinesByNamespace(machines, alias)
nodes, err := excludeCorrectlyTaggedNodes(aclPolicy, nodes, alias)
if err != nil {
return ips, err
}
nodes = excludeCorrectlyTaggedNodes(aclPolicy, nodes, alias)
for _, n := range nodes {
ips = append(ips, n.IPAddresses.ToStringSlice()...)
}
@@ -273,7 +393,9 @@ func expandAlias(
return []string{cidr.String()}, nil
}
return ips, errInvalidUserSection
log.Warn().Msgf("No IPs found with the alias %v", alias)
return ips, nil
}
// excludeCorrectlyTaggedNodes will remove from the list of input nodes the ones
@@ -283,48 +405,48 @@ func excludeCorrectlyTaggedNodes(
aclPolicy ACLPolicy,
nodes []Machine,
namespace string,
) ([]Machine, error) {
) []Machine {
out := []Machine{}
tags := []string{}
for tag, ns := range aclPolicy.TagOwners {
if containsString(ns, namespace) {
if contains(ns, namespace) {
tags = append(tags, tag)
}
}
// for each machine if tag is in tags list, don't append it.
for _, machine := range nodes {
if len(machine.HostInfo) == 0 {
out = append(out, machine)
hi := machine.GetHostInfo()
continue
}
hi, err := machine.GetHostInfo()
if err != nil {
return out, err
}
found := false
for _, t := range hi.RequestTags {
if containsString(tags, t) {
if contains(tags, t) {
found = true
break
}
}
if len(machine.ForcedTags) > 0 {
found = true
}
if !found {
out = append(out, machine)
}
}
return out, nil
return out
}
func expandPorts(portsStr string) (*[]tailcfg.PortRange, error) {
func expandPorts(portsStr string, needsWildcard bool) (*[]tailcfg.PortRange, error) {
if portsStr == "*" {
return &[]tailcfg.PortRange{
{First: portRangeBegin, Last: portRangeEnd},
}, nil
}
if needsWildcard {
return nil, errWildcardIsNeeded
}
ports := []tailcfg.PortRange{}
for _, portStr := range strings.Split(portsStr, ",") {
rang := strings.Split(portStr, "-")
@@ -374,7 +496,11 @@ func filterMachinesByNamespace(machines []Machine, namespace string) []Machine {
// expandTagOwners will return a list of namespace. An owner can be either a namespace or a group
// a group cannot be composed of groups.
func expandTagOwners(aclPolicy ACLPolicy, tag string) ([]string, error) {
func expandTagOwners(
aclPolicy ACLPolicy,
tag string,
stripEmailDomain bool,
) ([]string, error) {
var owners []string
ows, ok := aclPolicy.TagOwners[tag]
if !ok {
@@ -386,7 +512,7 @@ func expandTagOwners(aclPolicy ACLPolicy, tag string) ([]string, error) {
}
for _, owner := range ows {
if strings.HasPrefix(owner, "group:") {
gs, err := expandGroup(aclPolicy, owner)
gs, err := expandGroup(aclPolicy, owner, stripEmailDomain)
if err != nil {
return []string{}, err
}
@@ -401,8 +527,13 @@ func expandTagOwners(aclPolicy ACLPolicy, tag string) ([]string, error) {
// expandGroup will return the list of namespace inside the group
// after some validation.
func expandGroup(aclPolicy ACLPolicy, group string) ([]string, error) {
groups, ok := aclPolicy.Groups[group]
func expandGroup(
aclPolicy ACLPolicy,
group string,
stripEmailDomain bool,
) ([]string, error) {
outGroups := []string{}
aclGroups, ok := aclPolicy.Groups[group]
if !ok {
return []string{}, fmt.Errorf(
"group %v isn't registered. %w",
@@ -410,14 +541,23 @@ func expandGroup(aclPolicy ACLPolicy, group string) ([]string, error) {
errInvalidGroup,
)
}
for _, g := range groups {
if strings.HasPrefix(g, "group:") {
for _, group := range aclGroups {
if strings.HasPrefix(group, "group:") {
return []string{}, fmt.Errorf(
"%w. A group cannot be composed of groups. https://tailscale.com/kb/1018/acls/#groups",
errInvalidGroup,
)
}
grp, err := NormalizeToFQDNRules(group, stripEmailDomain)
if err != nil {
return []string{}, fmt.Errorf(
"failed to normalize group %q, err: %w",
group,
errInvalidGroup,
)
}
outGroups = append(outGroups, grp)
}
return groups, nil
return outGroups, nil
}

View File

@@ -6,7 +6,6 @@ import (
"testing"
"gopkg.in/check.v1"
"gorm.io/datatypes"
"inet.af/netaddr"
"tailscale.com/tailcfg"
)
@@ -63,7 +62,7 @@ func (s *Suite) TestBasicRule(c *check.C) {
func (s *Suite) TestInvalidAction(c *check.C) {
app.aclPolicy = &ACLPolicy{
ACLs: []ACL{
{Action: "invalidAction", Users: []string{"*"}, Ports: []string{"*:*"}},
{Action: "invalidAction", Sources: []string{"*"}, Destinations: []string{"*:*"}},
},
}
err := app.UpdateACLRules()
@@ -71,14 +70,14 @@ func (s *Suite) TestInvalidAction(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{
Groups: Groups{
"group:test": []string{"foo"},
"group:error": []string{"foo", "group:test"},
},
ACLs: []ACL{
{Action: "accept", Users: []string{"group:error"}, Ports: []string{"*:*"}},
{Action: "accept", Sources: []string{"group:error"}, Destinations: []string{"*:*"}},
},
}
err := app.UpdateACLRules()
@@ -89,7 +88,7 @@ func (s *Suite) TestInvalidTagOwners(c *check.C) {
// this ACL is wrong because no tagOwners own the requested tag for the server
app.aclPolicy = &ACLPolicy{
ACLs: []ACL{
{Action: "accept", Users: []string{"tag:foo"}, Ports: []string{"*:*"}},
{Action: "accept", Sources: []string{"tag:foo"}, Destinations: []string{"*:*"}},
},
}
err := app.UpdateACLRules()
@@ -98,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
// 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.
func (s *Suite) TestValidExpandTagOwnersInUsers(c *check.C) {
// the tag is matched in the Sources section.
func (s *Suite) TestValidExpandTagOwnersInSources(c *check.C) {
namespace, err := app.CreateNamespace("user1")
c.Assert(err, check.IsNil)
@@ -108,21 +107,23 @@ func (s *Suite) TestValidExpandTagOwnersInUsers(c *check.C) {
_, err = app.GetMachine("user1", "testmachine")
c.Assert(err, check.NotNil)
hostInfo := []byte(
"{\"OS\":\"centos\",\"Hostname\":\"testmachine\",\"RequestTags\":[\"tag:test\"]}",
)
hostInfo := tailcfg.Hostinfo{
OS: "centos",
Hostname: "testmachine",
RequestTags: []string{"tag:test"},
}
machine := Machine{
ID: 0,
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testmachine",
Hostname: "testmachine",
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
HostInfo: datatypes.JSON(hostInfo),
HostInfo: HostInfo(hostInfo),
}
app.db.Save(&machine)
@@ -130,7 +131,7 @@ func (s *Suite) TestValidExpandTagOwnersInUsers(c *check.C) {
Groups: Groups{"group:test": []string{"user1", "user2"}},
TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}},
ACLs: []ACL{
{Action: "accept", Users: []string{"tag:test"}, Ports: []string{"*:*"}},
{Action: "accept", Sources: []string{"tag:test"}, Destinations: []string{"*:*"}},
},
}
err = app.UpdateACLRules()
@@ -142,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
// 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.
func (s *Suite) TestValidExpandTagOwnersInPorts(c *check.C) {
// the tag is matched in the Destinations section.
func (s *Suite) TestValidExpandTagOwnersInDestinations(c *check.C) {
namespace, err := app.CreateNamespace("user1")
c.Assert(err, check.IsNil)
@@ -152,21 +153,23 @@ func (s *Suite) TestValidExpandTagOwnersInPorts(c *check.C) {
_, err = app.GetMachine("user1", "testmachine")
c.Assert(err, check.NotNil)
hostInfo := []byte(
"{\"OS\":\"centos\",\"Hostname\":\"testmachine\",\"RequestTags\":[\"tag:test\"]}",
)
hostInfo := tailcfg.Hostinfo{
OS: "centos",
Hostname: "testmachine",
RequestTags: []string{"tag:test"},
}
machine := Machine{
ID: 1,
MachineKey: "12345",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testmachine",
Hostname: "testmachine",
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
HostInfo: datatypes.JSON(hostInfo),
HostInfo: HostInfo(hostInfo),
}
app.db.Save(&machine)
@@ -174,7 +177,7 @@ func (s *Suite) TestValidExpandTagOwnersInPorts(c *check.C) {
Groups: Groups{"group:test": []string{"user1", "user2"}},
TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}},
ACLs: []ACL{
{Action: "accept", Users: []string{"*"}, Ports: []string{"tag:test:*"}},
{Action: "accept", Sources: []string{"*"}, Destinations: []string{"tag:test:*"}},
},
}
err = app.UpdateACLRules()
@@ -196,28 +199,30 @@ func (s *Suite) TestInvalidTagValidNamespace(c *check.C) {
_, err = app.GetMachine("user1", "testmachine")
c.Assert(err, check.NotNil)
hostInfo := []byte(
"{\"OS\":\"centos\",\"Hostname\":\"testmachine\",\"RequestTags\":[\"tag:foo\"]}",
)
hostInfo := tailcfg.Hostinfo{
OS: "centos",
Hostname: "testmachine",
RequestTags: []string{"tag:foo"},
}
machine := Machine{
ID: 1,
MachineKey: "12345",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testmachine",
Hostname: "testmachine",
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
HostInfo: datatypes.JSON(hostInfo),
HostInfo: HostInfo(hostInfo),
}
app.db.Save(&machine)
app.aclPolicy = &ACLPolicy{
TagOwners: TagOwners{"tag:test": []string{"user1"}},
ACLs: []ACL{
{Action: "accept", Users: []string{"user1"}, Ports: []string{"*:*"}},
{Action: "accept", Sources: []string{"user1"}, Destinations: []string{"*:*"}},
},
}
err = app.UpdateACLRules()
@@ -239,38 +244,42 @@ func (s *Suite) TestValidTagInvalidNamespace(c *check.C) {
_, err = app.GetMachine("user1", "webserver")
c.Assert(err, check.NotNil)
hostInfo := []byte(
"{\"OS\":\"centos\",\"Hostname\":\"webserver\",\"RequestTags\":[\"tag:webapp\"]}",
)
hostInfo := tailcfg.Hostinfo{
OS: "centos",
Hostname: "webserver",
RequestTags: []string{"tag:webapp"},
}
machine := Machine{
ID: 1,
MachineKey: "12345",
NodeKey: "bar",
DiscoKey: "faa",
Name: "webserver",
Hostname: "webserver",
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
HostInfo: datatypes.JSON(hostInfo),
HostInfo: HostInfo(hostInfo),
}
app.db.Save(&machine)
_, err = app.GetMachine("user1", "user")
hostInfo = []byte("{\"OS\":\"debian\",\"Hostname\":\"user\"}")
hostInfo2 := tailcfg.Hostinfo{
OS: "debian",
Hostname: "Hostname",
}
c.Assert(err, check.NotNil)
machine = Machine{
ID: 2,
MachineKey: "56789",
NodeKey: "bar2",
DiscoKey: "faab",
Name: "user",
Hostname: "user",
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
HostInfo: datatypes.JSON(hostInfo),
HostInfo: HostInfo(hostInfo2),
}
app.db.Save(&machine)
@@ -278,9 +287,9 @@ func (s *Suite) TestValidTagInvalidNamespace(c *check.C) {
TagOwners: TagOwners{"tag:webapp": []string{"user1"}},
ACLs: []ACL{
{
Action: "accept",
Users: []string{"user1"},
Ports: []string{"tag:webapp:80,443"},
Action: "accept",
Sources: []string{"user1"},
Destinations: []string{"tag:webapp:80,443"},
},
},
}
@@ -312,6 +321,20 @@ func (s *Suite) TestPortRange(c *check.C) {
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) {
err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_wildcards.hujson")
c.Assert(err, check.IsNil)
@@ -328,6 +351,22 @@ func (s *Suite) TestPortWildcard(c *check.C) {
c.Assert(rules[0].SrcIPs[0], check.Equals, "*")
}
func (s *Suite) TestPortWildcardYAML(c *check.C) {
err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_wildcards.yaml")
c.Assert(err, check.IsNil)
rules, err := app.generateACLRules()
c.Assert(err, check.IsNil)
c.Assert(rules, check.NotNil)
c.Assert(rules, check.HasLen, 1)
c.Assert(rules[0].DstPorts, check.HasLen, 1)
c.Assert(rules[0].DstPorts[0].Ports.First, check.Equals, uint16(0))
c.Assert(rules[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535))
c.Assert(rules[0].SrcIPs, check.HasLen, 1)
c.Assert(rules[0].SrcIPs[0], check.Equals, "*")
}
func (s *Suite) TestPortNamespace(c *check.C) {
namespace, err := app.CreateNamespace("testnamespace")
c.Assert(err, check.IsNil)
@@ -343,9 +382,8 @@ func (s *Suite) TestPortNamespace(c *check.C) {
MachineKey: "12345",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testmachine",
Hostname: "testmachine",
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: ips,
AuthKeyID: uint(pak.ID),
@@ -386,9 +424,8 @@ func (s *Suite) TestPortGroup(c *check.C) {
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testmachine",
Hostname: "testmachine",
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: ips,
AuthKeyID: uint(pak.ID),
@@ -414,8 +451,9 @@ func (s *Suite) TestPortGroup(c *check.C) {
func Test_expandGroup(t *testing.T) {
type args struct {
aclPolicy ACLPolicy
group string
aclPolicy ACLPolicy
group string
stripEmailDomain bool
}
tests := []struct {
name string
@@ -432,7 +470,8 @@ func Test_expandGroup(t *testing.T) {
"group:foo": []string{"user2", "user3"},
},
},
group: "group:test",
group: "group:test",
stripEmailDomain: true,
},
want: []string{"user1", "user2", "user3"},
wantErr: false,
@@ -446,15 +485,54 @@ func Test_expandGroup(t *testing.T) {
"group:foo": []string{"user2", "user3"},
},
},
group: "group:undefined",
group: "group:undefined",
stripEmailDomain: true,
},
want: []string{},
wantErr: true,
},
{
name: "Expand emails in group",
args: args{
aclPolicy: ACLPolicy{
Groups: Groups{
"group:admin": []string{
"joe.bar@gmail.com",
"john.doe@yahoo.fr",
},
},
},
group: "group:admin",
stripEmailDomain: true,
},
want: []string{"joe.bar", "john.doe"},
wantErr: false,
},
{
name: "Expand emails in group",
args: args{
aclPolicy: ACLPolicy{
Groups: Groups{
"group:admin": []string{
"joe.bar@gmail.com",
"john.doe@yahoo.fr",
},
},
},
group: "group:admin",
stripEmailDomain: false,
},
want: []string{"joe.bar.gmail.com", "john.doe.yahoo.fr"},
wantErr: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := expandGroup(test.args.aclPolicy, test.args.group)
got, err := expandGroup(
test.args.aclPolicy,
test.args.group,
test.args.stripEmailDomain,
)
if (err != nil) != test.wantErr {
t.Errorf("expandGroup() error = %v, wantErr %v", err, test.wantErr)
@@ -469,8 +547,9 @@ func Test_expandGroup(t *testing.T) {
func Test_expandTagOwners(t *testing.T) {
type args struct {
aclPolicy ACLPolicy
tag string
aclPolicy ACLPolicy
tag string
stripEmailDomain bool
}
tests := []struct {
name string
@@ -484,7 +563,8 @@ func Test_expandTagOwners(t *testing.T) {
aclPolicy: ACLPolicy{
TagOwners: TagOwners{"tag:test": []string{"user1"}},
},
tag: "tag:test",
tag: "tag:test",
stripEmailDomain: true,
},
want: []string{"user1"},
wantErr: false,
@@ -496,7 +576,8 @@ func Test_expandTagOwners(t *testing.T) {
Groups: Groups{"group:foo": []string{"user1", "user2"}},
TagOwners: TagOwners{"tag:test": []string{"group:foo"}},
},
tag: "tag:test",
tag: "tag:test",
stripEmailDomain: true,
},
want: []string{"user1", "user2"},
wantErr: false,
@@ -508,7 +589,8 @@ func Test_expandTagOwners(t *testing.T) {
Groups: Groups{"group:foo": []string{"user1", "user2"}},
TagOwners: TagOwners{"tag:test": []string{"group:foo", "user3"}},
},
tag: "tag:test",
tag: "tag:test",
stripEmailDomain: true,
},
want: []string{"user1", "user2", "user3"},
wantErr: false,
@@ -519,7 +601,8 @@ func Test_expandTagOwners(t *testing.T) {
aclPolicy: ACLPolicy{
TagOwners: TagOwners{"tag:foo": []string{"group:foo", "user1"}},
},
tag: "tag:test",
tag: "tag:test",
stripEmailDomain: true,
},
want: []string{},
wantErr: true,
@@ -531,7 +614,8 @@ func Test_expandTagOwners(t *testing.T) {
Groups: Groups{"group:bar": []string{"user1", "user2"}},
TagOwners: TagOwners{"tag:test": []string{"group:foo", "user2"}},
},
tag: "tag:test",
tag: "tag:test",
stripEmailDomain: true,
},
want: []string{},
wantErr: true,
@@ -539,7 +623,11 @@ func Test_expandTagOwners(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := expandTagOwners(test.args.aclPolicy, test.args.tag)
got, err := expandTagOwners(
test.args.aclPolicy,
test.args.tag,
test.args.stripEmailDomain,
)
if (err != nil) != test.wantErr {
t.Errorf("expandTagOwners() error = %v, wantErr %v", err, test.wantErr)
@@ -554,7 +642,8 @@ func Test_expandTagOwners(t *testing.T) {
func Test_expandPorts(t *testing.T) {
type args struct {
portsStr string
portsStr string
needsWildcard bool
}
tests := []struct {
name string
@@ -564,15 +653,29 @@ func Test_expandPorts(t *testing.T) {
}{
{
name: "wildcard",
args: args{portsStr: "*"},
args: args{portsStr: "*", needsWildcard: true},
want: &[]tailcfg.PortRange{
{First: portRangeBegin, Last: portRangeEnd},
},
wantErr: false,
},
{
name: "two ports",
args: args{portsStr: "80,443"},
name: "needs wildcard but does not require it",
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{
{First: 80, Last: 80},
{First: 443, Last: 443},
@@ -581,7 +684,7 @@ func Test_expandPorts(t *testing.T) {
},
{
name: "a range and a port",
args: args{portsStr: "80-1024,443"},
args: args{portsStr: "80-1024,443", needsWildcard: false},
want: &[]tailcfg.PortRange{
{First: 80, Last: 1024},
{First: 443, Last: 443},
@@ -590,38 +693,38 @@ func Test_expandPorts(t *testing.T) {
},
{
name: "out of bounds",
args: args{portsStr: "854038"},
args: args{portsStr: "854038", needsWildcard: false},
want: nil,
wantErr: true,
},
{
name: "wrong port",
args: args{portsStr: "85a38"},
args: args{portsStr: "85a38", needsWildcard: false},
want: nil,
wantErr: true,
},
{
name: "wrong port in first",
args: args{portsStr: "a-80"},
args: args{portsStr: "a-80", needsWildcard: false},
want: nil,
wantErr: true,
},
{
name: "wrong port in last",
args: args{portsStr: "80-85a38"},
args: args{portsStr: "80-85a38", needsWildcard: false},
want: nil,
wantErr: true,
},
{
name: "wrong port format",
args: args{portsStr: "80-85a38-3"},
args: args{portsStr: "80-85a38-3", needsWildcard: false},
want: nil,
wantErr: true,
},
}
for _, test := range tests {
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 {
t.Errorf("expandPorts() error = %v, wantErr %v", err, test.wantErr)
@@ -701,9 +804,10 @@ func Test_listMachinesInNamespace(t *testing.T) {
// nolint
func Test_expandAlias(t *testing.T) {
type args struct {
machines []Machine
aclPolicy ACLPolicy
alias string
machines []Machine
aclPolicy ACLPolicy
alias string
stripEmailDomain bool
}
tests := []struct {
name string
@@ -723,7 +827,8 @@ func Test_expandAlias(t *testing.T) {
},
},
},
aclPolicy: ACLPolicy{},
aclPolicy: ACLPolicy{},
stripEmailDomain: true,
},
want: []string{"*"},
wantErr: false,
@@ -761,6 +866,7 @@ func Test_expandAlias(t *testing.T) {
aclPolicy: ACLPolicy{
Groups: Groups{"group:accountant": []string{"joe", "marc"}},
},
stripEmailDomain: true,
},
want: []string{"100.64.0.1", "100.64.0.2", "100.64.0.3"},
wantErr: false,
@@ -798,6 +904,7 @@ func Test_expandAlias(t *testing.T) {
aclPolicy: ACLPolicy{
Groups: Groups{"group:accountant": []string{"joe", "marc"}},
},
stripEmailDomain: true,
},
want: []string{},
wantErr: true,
@@ -805,9 +912,10 @@ func Test_expandAlias(t *testing.T) {
{
name: "simple ipaddress",
args: args{
alias: "10.0.0.3",
machines: []Machine{},
aclPolicy: ACLPolicy{},
alias: "10.0.0.3",
machines: []Machine{},
aclPolicy: ACLPolicy{},
stripEmailDomain: true,
},
want: []string{"10.0.0.3"},
wantErr: false,
@@ -822,6 +930,7 @@ func Test_expandAlias(t *testing.T) {
"homeNetwork": netaddr.MustParseIPPrefix("192.168.1.0/24"),
},
},
stripEmailDomain: true,
},
want: []string{"192.168.1.0/24"},
wantErr: false,
@@ -829,9 +938,10 @@ func Test_expandAlias(t *testing.T) {
{
name: "simple host",
args: args{
alias: "10.0.0.1",
machines: []Machine{},
aclPolicy: ACLPolicy{},
alias: "10.0.0.1",
machines: []Machine{},
aclPolicy: ACLPolicy{},
stripEmailDomain: true,
},
want: []string{"10.0.0.1"},
wantErr: false,
@@ -839,9 +949,10 @@ func Test_expandAlias(t *testing.T) {
{
name: "simple CIDR",
args: args{
alias: "10.0.0.0/16",
machines: []Machine{},
aclPolicy: ACLPolicy{},
alias: "10.0.0.0/16",
machines: []Machine{},
aclPolicy: ACLPolicy{},
stripEmailDomain: true,
},
want: []string{"10.0.0.0/16"},
wantErr: false,
@@ -856,18 +967,22 @@ func Test_expandAlias(t *testing.T) {
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: []byte(
"{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:hr-webserver\"]}",
),
HostInfo: HostInfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:hr-webserver"},
},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: []byte(
"{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:hr-webserver\"]}",
),
HostInfo: HostInfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:hr-webserver"},
},
},
{
IPAddresses: MachineAddresses{
@@ -885,6 +1000,7 @@ func Test_expandAlias(t *testing.T) {
aclPolicy: ACLPolicy{
TagOwners: TagOwners{"tag:hr-webserver": []string{"joe"}},
},
stripEmailDomain: true,
},
want: []string{"100.64.0.1", "100.64.0.2"},
wantErr: false,
@@ -925,10 +1041,95 @@ func Test_expandAlias(t *testing.T) {
"tag:accountant-webserver": []string{"group:accountant"},
},
},
stripEmailDomain: true,
},
want: []string{},
wantErr: true,
},
{
name: "Forced tag defined",
args: args{
alias: "tag:hr-webserver",
machines: []Machine{
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
ForcedTags: []string{"tag:hr-webserver"},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
ForcedTags: []string{"tag:hr-webserver"},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
},
Namespace: Namespace{Name: "marc"},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.4"),
},
Namespace: Namespace{Name: "mickael"},
},
},
aclPolicy: ACLPolicy{},
stripEmailDomain: true,
},
want: []string{"100.64.0.1", "100.64.0.2"},
wantErr: false,
},
{
name: "Forced tag with legitimate tagOwner",
args: args{
alias: "tag:hr-webserver",
machines: []Machine{
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
ForcedTags: []string{"tag:hr-webserver"},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:hr-webserver"},
},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
},
Namespace: Namespace{Name: "marc"},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.4"),
},
Namespace: Namespace{Name: "mickael"},
},
},
aclPolicy: ACLPolicy{
TagOwners: TagOwners{
"tag:hr-webserver": []string{"joe"},
},
},
stripEmailDomain: true,
},
want: []string{"100.64.0.1", "100.64.0.2"},
wantErr: false,
},
{
name: "list host in namespace without correctly tagged servers",
args: args{
@@ -939,18 +1140,22 @@ func Test_expandAlias(t *testing.T) {
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: []byte(
"{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:accountant-webserver\"]}",
),
HostInfo: HostInfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:accountant-webserver"},
},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: []byte(
"{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:accountant-webserver\"]}",
),
HostInfo: HostInfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:accountant-webserver"},
},
},
{
IPAddresses: MachineAddresses{
@@ -968,6 +1173,7 @@ func Test_expandAlias(t *testing.T) {
aclPolicy: ACLPolicy{
TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}},
},
stripEmailDomain: true,
},
want: []string{"100.64.0.4"},
wantErr: false,
@@ -979,6 +1185,7 @@ func Test_expandAlias(t *testing.T) {
test.args.machines,
test.args.aclPolicy,
test.args.alias,
test.args.stripEmailDomain,
)
if (err != nil) != test.wantErr {
t.Errorf("expandAlias() error = %v, wantErr %v", err, test.wantErr)
@@ -1016,18 +1223,63 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: []byte(
"{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:accountant-webserver\"]}",
),
HostInfo: HostInfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:accountant-webserver"},
},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: []byte(
"{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:accountant-webserver\"]}",
),
HostInfo: HostInfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:accountant-webserver"},
},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.4"),
},
Namespace: Namespace{Name: "joe"},
},
},
namespace: "joe",
},
want: []Machine{
{
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.4")},
Namespace: Namespace{Name: "joe"},
},
},
},
{
name: "exclude nodes with valid tags and with forced tags",
args: args{
aclPolicy: ACLPolicy{
TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}},
},
nodes: []Machine{
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:accountant-webserver"},
},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
ForcedTags: []string{"tag:accountant-webserver"},
},
{
IPAddresses: MachineAddresses{
@@ -1044,7 +1296,6 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
Namespace: Namespace{Name: "joe"},
},
},
wantErr: false,
},
{
name: "all nodes have invalid tags, don't exclude them",
@@ -1058,18 +1309,22 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: []byte(
"{\"OS\":\"centos\",\"Hostname\":\"hr-web1\",\"RequestTags\":[\"tag:hr-webserver\"]}",
),
HostInfo: HostInfo{
OS: "centos",
Hostname: "hr-web1",
RequestTags: []string{"tag:hr-webserver"},
},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: []byte(
"{\"OS\":\"centos\",\"Hostname\":\"hr-web2\",\"RequestTags\":[\"tag:hr-webserver\"]}",
),
HostInfo: HostInfo{
OS: "centos",
Hostname: "hr-web2",
RequestTags: []string{"tag:hr-webserver"},
},
},
{
IPAddresses: MachineAddresses{
@@ -1086,18 +1341,22 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: []byte(
"{\"OS\":\"centos\",\"Hostname\":\"hr-web1\",\"RequestTags\":[\"tag:hr-webserver\"]}",
),
HostInfo: HostInfo{
OS: "centos",
Hostname: "hr-web1",
RequestTags: []string{"tag:hr-webserver"},
},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: []byte(
"{\"OS\":\"centos\",\"Hostname\":\"hr-web2\",\"RequestTags\":[\"tag:hr-webserver\"]}",
),
HostInfo: HostInfo{
OS: "centos",
Hostname: "hr-web2",
RequestTags: []string{"tag:hr-webserver"},
},
},
{
IPAddresses: MachineAddresses{
@@ -1106,25 +1365,15 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
Namespace: Namespace{Name: "joe"},
},
},
wantErr: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := excludeCorrectlyTaggedNodes(
got := excludeCorrectlyTaggedNodes(
test.args.aclPolicy,
test.args.nodes,
test.args.namespace,
)
if (err != nil) != test.wantErr {
t.Errorf(
"excludeCorrectlyTaggedNodes() error = %v, wantErr %v",
err,
test.wantErr,
)
return
}
if !reflect.DeepEqual(got, test.want) {
t.Errorf("excludeCorrectlyTaggedNodes() = %v, want %v", got, test.want)
}

View File

@@ -5,23 +5,25 @@ import (
"strings"
"github.com/tailscale/hujson"
"gopkg.in/yaml.v3"
"inet.af/netaddr"
)
// ACLPolicy represents a Tailscale ACL Policy.
type ACLPolicy struct {
Groups Groups `json:"Groups"`
Hosts Hosts `json:"Hosts"`
TagOwners TagOwners `json:"TagOwners"`
ACLs []ACL `json:"ACLs"`
Tests []ACLTest `json:"Tests"`
Groups Groups `json:"groups" yaml:"groups"`
Hosts Hosts `json:"hosts" yaml:"hosts"`
TagOwners TagOwners `json:"tagOwners" yaml:"tagOwners"`
ACLs []ACL `json:"acls" yaml:"acls"`
Tests []ACLTest `json:"tests" yaml:"tests"`
}
// ACL is a basic rule for the ACL Policy.
type ACL struct {
Action string `json:"Action"`
Users []string `json:"Users"`
Ports []string `json:"Ports"`
Action string `json:"action" yaml:"action"`
Protocol string `json:"proto" yaml:"proto"`
Sources []string `json:"src" yaml:"src"`
Destinations []string `json:"dst" yaml:"dst"`
}
// Groups references a series of alias in the ACL rules.
@@ -35,9 +37,9 @@ type TagOwners map[string][]string
// ACLTest is not implemented, but should be use to check if a certain rule is allowed.
type ACLTest struct {
User string `json:"User"`
Allow []string `json:"Allow"`
Deny []string `json:"Deny,omitempty"`
Source string `json:"src" yaml:"src"`
Accept []string `json:"accept" yaml:"accept"`
Deny []string `json:"deny,omitempty" yaml:"deny,omitempty"`
}
// UnmarshalJSON allows to parse the Hosts directly into netaddr objects.
@@ -69,6 +71,27 @@ func (hosts *Hosts) UnmarshalJSON(data []byte) error {
return nil
}
// UnmarshalYAML allows to parse the Hosts directly into netaddr objects.
func (hosts *Hosts) UnmarshalYAML(data []byte) error {
newHosts := Hosts{}
hostIPPrefixMap := make(map[string]string)
err := yaml.Unmarshal(data, &hostIPPrefixMap)
if err != nil {
return err
}
for host, prefixStr := range hostIPPrefixMap {
prefix, err := netaddr.ParseIPPrefix(prefixStr)
if err != nil {
return err
}
newHosts[host] = prefix
}
*hosts = newHosts
return nil
}
// IsZero is perhaps a bit naive here.
func (policy ACLPolicy) IsZero() bool {
if len(policy.Groups) == 0 && len(policy.Hosts) == 0 && len(policy.ACLs) == 0 {

557
api.go
View File

@@ -12,7 +12,7 @@ import (
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/gorilla/mux"
"github.com/klauspost/compress/zstd"
"github.com/rs/zerolog/log"
"gorm.io/gorm"
@@ -22,7 +22,7 @@ import (
const (
reservedResponseHeaderSize = 4
RegisterMethodAuthKey = "authKey"
RegisterMethodAuthKey = "authkey"
RegisterMethodOIDC = "oidc"
RegisterMethodCLI = "cli"
ErrRegisterMethodCLIDoesNotSupportExpire = Error(
@@ -30,14 +30,59 @@ const (
)
)
func (h *Headscale) HealthHandler(
writer http.ResponseWriter,
req *http.Request,
) {
respond := func(err error) {
writer.Header().Set("Content-Type", "application/health+json; charset=utf-8")
res := struct {
Status string `json:"status"`
}{
Status: "pass",
}
if err != nil {
writer.WriteHeader(http.StatusInternalServerError)
log.Error().Caller().Err(err).Msg("health check failed")
res.Status = "fail"
}
buf, err := json.Marshal(res)
if err != nil {
log.Error().Caller().Err(err).Msg("marshal failed")
}
_, err = writer.Write(buf)
if err != nil {
log.Error().Caller().Err(err).Msg("write failed")
}
}
if err := h.pingDB(); err != nil {
respond(err)
return
}
respond(nil)
}
// KeyHandler provides the Headscale pub key
// Listens in /key.
func (h *Headscale) KeyHandler(ctx *gin.Context) {
ctx.Data(
http.StatusOK,
"text/plain; charset=utf-8",
[]byte(MachinePublicKeyStripPrefix(h.privateKey.Public())),
)
func (h *Headscale) KeyHandler(
writer http.ResponseWriter,
req *http.Request,
) {
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 {
@@ -45,29 +90,39 @@ type registerWebAPITemplateConfig struct {
}
var registerWebAPITemplate = template.Must(
template.New("registerweb").Parse(`<html>
template.New("registerweb").Parse(`
<html>
<head>
<title>Registration - Headscale</title>
</head>
<body>
<h1>headscale</h1>
<p>
Run the command below in the headscale server to add this machine to your network:
</p>
<p>
<code>
<b>headscale -n NAMESPACE nodes register --key {{.Key}}</b>
</code>
</p>
<h1>headscale</h1>
<h2>Machine registration</h2>
<p>
Run the command below in the headscale server to add this machine to your network:
</p>
<pre><code>headscale -n NAMESPACE nodes register --key {{.Key}}</code></pre>
</body>
</html>`),
)
</html>
`))
// RegisterWebAPI shows a simple message in the browser to point to the CLI
// Listens in /register.
func (h *Headscale) RegisterWebAPI(ctx *gin.Context) {
machineKeyStr := ctx.Query("key")
func (h *Headscale) RegisterWebAPI(
writer http.ResponseWriter,
req *http.Request,
) {
machineKeyStr := req.URL.Query().Get("key")
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
}
@@ -80,21 +135,48 @@ func (h *Headscale) RegisterWebAPI(ctx *gin.Context) {
Str("func", "RegisterWebAPI").
Err(err).
Msg("Could not render register web API template")
ctx.Data(
http.StatusInternalServerError,
"text/html; charset=utf-8",
[]byte("Could not render register web API template"),
)
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusInternalServerError)
_, err = writer.Write([]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
// Endpoint /machine/:id.
func (h *Headscale) RegistrationHandler(ctx *gin.Context) {
body, _ := io.ReadAll(ctx.Request.Body)
machineKeyStr := ctx.Param("id")
// Endpoint /machine/:mkey.
func (h *Headscale) RegistrationHandler(
writer http.ResponseWriter,
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
err := machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machineKeyStr)))
@@ -104,19 +186,19 @@ func (h *Headscale) RegistrationHandler(ctx *gin.Context) {
Err(err).
Msg("Cannot parse machine key")
machineRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc()
ctx.String(http.StatusInternalServerError, "Sad!")
http.Error(writer, "Cannot parse machine key", http.StatusBadRequest)
return
}
req := tailcfg.RegisterRequest{}
err = decode(body, &req, &machineKey, h.privateKey)
registerRequest := tailcfg.RegisterRequest{}
err = decode(body, &registerRequest, &machineKey, h.privateKey)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot decode message")
machineRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc()
ctx.String(http.StatusInternalServerError, "Very sad!")
http.Error(writer, "Cannot decode message", http.StatusBadRequest)
return
}
@@ -124,36 +206,73 @@ func (h *Headscale) RegistrationHandler(ctx *gin.Context) {
now := time.Now().UTC()
machine, err := h.GetMachineByMachineKey(machineKey)
if errors.Is(err, gorm.ErrRecordNotFound) {
log.Info().Str("machine", req.Hostinfo.Hostname).Msg("New machine")
newMachine := Machine{
Expiry: &time.Time{},
MachineKey: MachinePublicKeyStripPrefix(machineKey),
Name: req.Hostinfo.Hostname,
}
if err := h.db.Create(&newMachine).Error; err != nil {
log.Error().
Caller().
Err(err).
Msg("Could not create row")
machineRegistrations.WithLabelValues("unknown", "web", "error", machine.Namespace.Name).
Inc()
log.Info().Str("machine", registerRequest.Hostinfo.Hostname).Msg("New machine")
machineKeyStr := MachinePublicKeyStripPrefix(machineKey)
// If the machine has AuthKey set, handle registration via PreAuthKeys
if registerRequest.Auth.AuthKey != "" {
h.handleAuthKey(writer, req, machineKey, registerRequest)
return
}
machine = &newMachine
givenName, err := h.GenerateGivenName(registerRequest.Hostinfo.Hostname)
if err != nil {
log.Error().
Caller().
Str("func", "RegistrationHandler").
Str("hostinfo.name", registerRequest.Hostinfo.Hostname).
Err(err)
return
}
// The machine did not have a key to authenticate, which means
// that we rely on a method that calls back some how (OpenID or CLI)
// We create the machine and then keep it around until a callback
// happens
newMachine := Machine{
MachineKey: machineKeyStr,
Hostname: registerRequest.Hostinfo.Hostname,
GivenName: givenName,
NodeKey: NodePublicKeyStripPrefix(registerRequest.NodeKey),
LastSeen: &now,
Expiry: &time.Time{},
}
if !registerRequest.Expiry.IsZero() {
log.Trace().
Caller().
Str("machine", registerRequest.Hostinfo.Hostname).
Time("expiry", registerRequest.Expiry).
Msg("Non-zero expiry time requested")
newMachine.Expiry = &registerRequest.Expiry
}
h.registrationCache.Set(
machineKeyStr,
newMachine,
registerCacheExpiration,
)
h.handleMachineRegistrationNew(writer, req, machineKey, registerRequest)
return
}
if machine.Registered {
// The machine is already registered, so we need to pass through reauth or key update.
if machine != nil {
// If the NodeKey stored in headscale is the same as the key presented in a registration
// request, then we have a node that is either:
// - Trying to log out (sending a expiry in the past)
// - A valid, registered machine, looking for the node map
// - Expired machine wanting to reauthenticate
if machine.NodeKey == NodePublicKeyStripPrefix(req.NodeKey) {
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)
// https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L648
if !req.Expiry.IsZero() && req.Expiry.UTC().Before(now) {
h.handleMachineLogOut(ctx, machineKey, *machine)
if !registerRequest.Expiry.IsZero() && registerRequest.Expiry.UTC().Before(now) {
h.handleMachineLogOut(writer, req, machineKey, *machine)
return
}
@@ -161,44 +280,35 @@ func (h *Headscale) RegistrationHandler(ctx *gin.Context) {
// If machine is not expired, and is register, we have a already accepted this machine,
// let it proceed with a valid registration
if !machine.isExpired() {
h.handleMachineValidRegistration(ctx, machineKey, *machine)
h.handleMachineValidRegistration(writer, req, machineKey, *machine)
return
}
}
// 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() {
h.handleMachineRefreshKey(ctx, machineKey, req, *machine)
h.handleMachineRefreshKey(writer, req, machineKey, registerRequest, *machine)
return
}
// The machine has expired
h.handleMachineExpired(ctx, machineKey, req, *machine)
h.handleMachineExpired(writer, req, machineKey, registerRequest, *machine)
return
}
// If the machine has AuthKey set, handle registration via PreAuthKeys
if req.Auth.AuthKey != "" {
h.handleAuthKey(ctx, machineKey, req, *machine)
return
}
h.handleMachineRegistrationNew(ctx, machineKey, req, *machine)
}
func (h *Headscale) getMapResponse(
machineKey key.MachinePublic,
req tailcfg.MapRequest,
mapRequest tailcfg.MapRequest,
machine *Machine,
) ([]byte, error) {
log.Trace().
Str("func", "getMapResponse").
Str("machine", req.Hostinfo.Hostname).
Str("machine", mapRequest.Hostinfo.Hostname).
Msg("Creating Map response")
node, err := machine.toNode(h.cfg.BaseDomain, h.cfg.DNSConfig, true)
if err != nil {
@@ -251,16 +361,20 @@ func (h *Headscale) getMapResponse(
PacketFilter: h.aclRules,
DERPMap: h.DERPMap,
UserProfiles: profiles,
Debug: &tailcfg.Debug{
DisableLogTail: !h.cfg.LogTail.Enabled,
RandomizeClientPort: h.cfg.RandomizeClientPort,
},
}
log.Trace().
Str("func", "getMapResponse").
Str("machine", req.Hostinfo.Hostname).
Str("machine", mapRequest.Hostinfo.Hostname).
// Interface("payload", resp).
Msgf("Generated map response: %s", tailMapResponseToString(resp))
var respBody []byte
if req.Compress == "zstd" {
if mapRequest.Compress == "zstd" {
src, err := json.Marshal(resp)
if err != nil {
log.Error().
@@ -326,17 +440,28 @@ func (h *Headscale) getMapKeepAliveResponse(
}
func (h *Headscale) handleMachineLogOut(
ctx *gin.Context,
writer http.ResponseWriter,
req *http.Request,
machineKey key.MachinePublic,
machine Machine,
) {
resp := tailcfg.RegisterResponse{}
log.Info().
Str("machine", machine.Name).
Str("machine", machine.Hostname).
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.MachineAuthorized = false
@@ -347,15 +472,25 @@ func (h *Headscale) handleMachineLogOut(
Caller().
Err(err).
Msg("Cannot encode message")
ctx.String(http.StatusInternalServerError, "")
http.Error(writer, "Internal server error", http.StatusInternalServerError)
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(
ctx *gin.Context,
writer http.ResponseWriter,
req *http.Request,
machineKey key.MachinePublic,
machine Machine,
) {
@@ -363,7 +498,7 @@ func (h *Headscale) handleMachineValidRegistration(
// The machine registration is valid, respond with redirect to /map
log.Debug().
Str("machine", machine.Name).
Str("machine", machine.Hostname).
Msg("Client is registered and we have the current NodeKey. All clear to /map")
resp.AuthURL = ""
@@ -379,17 +514,27 @@ func (h *Headscale) handleMachineValidRegistration(
Msg("Cannot encode message")
machineRegistrations.WithLabelValues("update", "web", "error", machine.Namespace.Name).
Inc()
ctx.String(http.StatusInternalServerError, "")
http.Error(writer, "Internal server error", http.StatusInternalServerError)
return
}
machineRegistrations.WithLabelValues("update", "web", "success", machine.Namespace.Name).
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(
ctx *gin.Context,
writer http.ResponseWriter,
req *http.Request,
machineKey key.MachinePublic,
registerRequest tailcfg.RegisterRequest,
machine Machine,
@@ -398,11 +543,11 @@ func (h *Headscale) handleMachineExpired(
// The client has registered before, but has expired
log.Debug().
Str("machine", machine.Name).
Str("machine", machine.Hostname).
Msg("Machine registration has expired. Sending a authurl to register")
if registerRequest.Auth.AuthKey != "" {
h.handleAuthKey(ctx, machineKey, registerRequest, machine)
h.handleAuthKey(writer, req, machineKey, registerRequest)
return
}
@@ -423,17 +568,27 @@ func (h *Headscale) handleMachineExpired(
Msg("Cannot encode message")
machineRegistrations.WithLabelValues("reauth", "web", "error", machine.Namespace.Name).
Inc()
ctx.String(http.StatusInternalServerError, "")
http.Error(writer, "Internal server error", http.StatusInternalServerError)
return
}
machineRegistrations.WithLabelValues("reauth", "web", "success", machine.Namespace.Name).
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(
ctx *gin.Context,
writer http.ResponseWriter,
req *http.Request,
machineKey key.MachinePublic,
registerRequest tailcfg.RegisterRequest,
machine Machine,
@@ -441,10 +596,19 @@ func (h *Headscale) handleMachineRefreshKey(
resp := tailcfg.RegisterResponse{}
log.Debug().
Str("machine", machine.Name).
Str("machine", machine.Hostname).
Msg("We have the OldNodeKey in the database. This is a key refresh")
machine.NodeKey = NodePublicKeyStripPrefix(registerRequest.NodeKey)
h.db.Save(&machine)
if err := h.db.Save(&machine).Error; err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to update machine key in the database")
http.Error(writer, "Internal server error", http.StatusInternalServerError)
return
}
resp.AuthURL = ""
resp.User = *machine.Namespace.toUser()
@@ -454,24 +618,33 @@ func (h *Headscale) handleMachineRefreshKey(
Caller().
Err(err).
Msg("Cannot encode message")
ctx.String(http.StatusInternalServerError, "Extremely sad!")
http.Error(writer, "Internal server error", http.StatusInternalServerError)
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(
ctx *gin.Context,
writer http.ResponseWriter,
req *http.Request,
machineKey key.MachinePublic,
registerRequest tailcfg.RegisterRequest,
machine Machine,
) {
resp := tailcfg.RegisterResponse{}
// The machine registration is new, redirect the client to the registration URL
log.Debug().
Str("machine", machine.Name).
Str("machine", registerRequest.Hostinfo.Hostname).
Msg("The node is sending us a new NodeKey, sending auth url")
if h.cfg.OIDC.Issuer != "" {
resp.AuthURL = fmt.Sprintf(
@@ -484,55 +657,49 @@ func (h *Headscale) handleMachineRegistrationNew(
strings.TrimSuffix(h.cfg.ServerURL, "/"), MachinePublicKeyStripPrefix(machineKey))
}
if !registerRequest.Expiry.IsZero() {
log.Trace().
Caller().
Str("machine", machine.Name).
Time("expiry", registerRequest.Expiry).
Msg("Non-zero expiry time requested, adding to cache")
h.requestedExpiryCache.Set(
machineKey.String(),
registerRequest.Expiry,
requestedExpiryCacheExpiration,
)
}
machine.NodeKey = NodePublicKeyStripPrefix(registerRequest.NodeKey)
// save the NodeKey
h.db.Save(&machine)
respBody, err := encode(resp, &machineKey, h.privateKey)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot encode message")
ctx.String(http.StatusInternalServerError, "")
http.Error(writer, "Internal server error", http.StatusInternalServerError)
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.
func (h *Headscale) handleAuthKey(
ctx *gin.Context,
writer http.ResponseWriter,
req *http.Request,
machineKey key.MachinePublic,
registerRequest tailcfg.RegisterRequest,
machine Machine,
) {
machineKeyStr := MachinePublicKeyStripPrefix(machineKey)
log.Debug().
Str("func", "handleAuthKey").
Str("machine", registerRequest.Hostinfo.Hostname).
Msgf("Processing auth key for %s", registerRequest.Hostinfo.Hostname)
resp := tailcfg.RegisterResponse{}
pak, err := h.checkKeyValidity(registerRequest.Auth.AuthKey)
if err != nil {
log.Error().
Caller().
Str("func", "handleAuthKey").
Str("machine", machine.Name).
Str("machine", registerRequest.Hostinfo.Hostname).
Err(err).
Msg("Failed authentication via AuthKey")
resp.MachineAuthorized = false
@@ -541,71 +708,126 @@ func (h *Headscale) handleAuthKey(
log.Error().
Caller().
Str("func", "handleAuthKey").
Str("machine", machine.Name).
Str("machine", registerRequest.Hostinfo.Hostname).
Err(err).
Msg("Cannot encode message")
ctx.String(http.StatusInternalServerError, "")
machineRegistrations.WithLabelValues("new", "authkey", "error", machine.Namespace.Name).
http.Error(writer, "Internal server error", http.StatusInternalServerError)
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
Inc()
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().
Caller().
Str("func", "handleAuthKey").
Str("machine", machine.Name).
Str("machine", registerRequest.Hostinfo.Hostname).
Msg("Failed authentication via AuthKey")
machineRegistrations.WithLabelValues("new", "authkey", "error", machine.Namespace.Name).
Inc()
if pak != nil {
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
Inc()
} else {
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", "unknown").Inc()
}
return
}
if machine.isRegistered() {
log.Debug().
Str("func", "handleAuthKey").
Str("machine", registerRequest.Hostinfo.Hostname).
Msg("Authentication key was valid, proceeding to acquire IP addresses")
nodeKey := NodePublicKeyStripPrefix(registerRequest.NodeKey)
// retrieve machine information if it exist
// The error is not important, because if it does not
// exist, then this is a new machine and we will move
// on to registration.
machine, _ := h.GetMachineByMachineKey(machineKey)
if machine != nil {
log.Trace().
Caller().
Str("machine", machine.Name).
Msg("machine already registered, reauthenticating")
Str("machine", machine.Hostname).
Msg("machine already registered, refreshing with new auth key")
h.RefreshMachine(&machine, registerRequest.Expiry)
} else {
log.Debug().
Str("func", "handleAuthKey").
Str("machine", machine.Name).
Msg("Authentication key was valid, proceeding to acquire IP addresses")
ips, err := h.getAvailableIPs()
machine.NodeKey = nodeKey
machine.AuthKeyID = uint(pak.ID)
err := h.RefreshMachine(machine, registerRequest.Expiry)
if err != nil {
log.Error().
Caller().
Str("func", "handleAuthKey").
Str("machine", machine.Name).
Msg("Failed to find an available IP address")
machineRegistrations.WithLabelValues("new", "authkey", "error", machine.Namespace.Name).
Inc()
Str("machine", machine.Hostname).
Err(err).
Msg("Failed to refresh machine")
return
}
log.Info().
Str("func", "handleAuthKey").
Str("machine", machine.Name).
Str("ips", strings.Join(ips.ToStringSlice(), ",")).
Msgf("Assigning %s to %s", strings.Join(ips.ToStringSlice(), ","), machine.Name)
} else {
now := time.Now().UTC()
machine.Expiry = &registerRequest.Expiry
machine.AuthKeyID = uint(pak.ID)
machine.IPAddresses = ips
machine.NamespaceID = pak.NamespaceID
givenName, err := h.GenerateGivenName(registerRequest.Hostinfo.Hostname)
if err != nil {
log.Error().
Caller().
Str("func", "RegistrationHandler").
Str("hostinfo.name", registerRequest.Hostinfo.Hostname).
Err(err)
machine.NodeKey = NodePublicKeyStripPrefix(registerRequest.NodeKey)
// we update it just in case
machine.Registered = true
machine.RegisterMethod = RegisterMethodAuthKey
h.db.Save(&machine)
return
}
machineToRegister := Machine{
Hostname: registerRequest.Hostinfo.Hostname,
GivenName: givenName,
NamespaceID: pak.Namespace.ID,
MachineKey: machineKeyStr,
RegisterMethod: RegisterMethodAuthKey,
Expiry: &registerRequest.Expiry,
NodeKey: nodeKey,
LastSeen: &now,
AuthKeyID: uint(pak.ID),
}
machine, err = h.RegisterMachine(
machineToRegister,
)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("could not register machine")
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
Inc()
http.Error(writer, "Internal server error", http.StatusInternalServerError)
return
}
}
pak.Used = true
h.db.Save(&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.User = *pak.Namespace.toUser()
@@ -614,21 +836,30 @@ func (h *Headscale) handleAuthKey(
log.Error().
Caller().
Str("func", "handleAuthKey").
Str("machine", machine.Name).
Str("machine", registerRequest.Hostinfo.Hostname).
Err(err).
Msg("Cannot encode message")
machineRegistrations.WithLabelValues("new", "authkey", "error", machine.Namespace.Name).
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
Inc()
ctx.String(http.StatusInternalServerError, "Extremely sad!")
http.Error(writer, "Internal server error", http.StatusInternalServerError)
return
}
machineRegistrations.WithLabelValues("new", "authkey", "success", machine.Namespace.Name).
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "success", pak.Namespace.Name).
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().
Str("func", "handleAuthKey").
Str("machine", machine.Name).
Str("machine", registerRequest.Hostinfo.Hostname).
Str("ips", strings.Join(machine.IPAddresses.ToStringSlice(), ", ")).
Msg("Successfully authenticated via AuthKey")
}

View File

@@ -13,7 +13,6 @@ import (
const (
apiPrefixLength = 7
apiKeyLength = 32
apiKeyParts = 2
errAPIKeyFailedToParse = Error("Failed to parse ApiKey")
)
@@ -57,7 +56,10 @@ func (h *Headscale) CreateAPIKey(
Hash: hash,
Expiration: expiration,
}
h.db.Save(&key)
if err := h.db.Save(&key).Error; err != nil {
return "", nil, fmt.Errorf("failed to save API key to database: %w", err)
}
return keyStr, &key, nil
}
@@ -112,9 +114,9 @@ func (h *Headscale) ExpireAPIKey(key *APIKey) error {
}
func (h *Headscale) ValidateAPIKey(keyStr string) (bool, error) {
prefix, hash, err := splitAPIKey(keyStr)
if err != nil {
return false, fmt.Errorf("failed to validate api key: %w", err)
prefix, hash, found := strings.Cut(keyStr, ".")
if !found {
return false, errAPIKeyFailedToParse
}
key, err := h.GetAPIKey(prefix)
@@ -133,15 +135,6 @@ func (h *Headscale) ValidateAPIKey(keyStr string) (bool, error) {
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 {
protoKey := v1.ApiKey{
Id: key.ID,

515
app.go
View File

@@ -6,10 +6,8 @@ import (
"errors"
"fmt"
"io"
"io/fs"
"net"
"net/http"
"net/url"
"os"
"os/signal"
"sort"
@@ -19,15 +17,16 @@ import (
"time"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/gin-gonic/gin"
"github.com/gorilla/mux"
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/patrickmn/go-cache"
zerolog "github.com/philip-bui/grpc-zerolog"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/puzpuzpuz/xsync"
zl "github.com/rs/zerolog"
"github.com/rs/zerolog/log"
ginprometheus "github.com/zsais/go-gin-prometheus"
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert"
"golang.org/x/oauth2"
@@ -41,118 +40,62 @@ import (
"google.golang.org/grpc/reflection"
"google.golang.org/grpc/status"
"gorm.io/gorm"
"inet.af/netaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/dnstype"
"tailscale.com/types/key"
)
const (
AuthPrefix = "Bearer "
Postgres = "postgres"
Sqlite = "sqlite3"
updateInterval = 5000
HTTPReadTimeout = 30 * time.Second
privateKeyFileMode = 0o600
requestedExpiryCacheExpiration = time.Minute * 5
requestedExpiryCacheCleanupInterval = time.Minute * 10
errSTUNAddressNotSet = Error("STUN address not set")
errUnsupportedDatabase = Error("unsupported DB")
errUnsupportedLetsEncryptChallengeType = Error(
"unknown value for Lets Encrypt challenge type",
)
)
const (
AuthPrefix = "Bearer "
Postgres = "postgres"
Sqlite = "sqlite3"
updateInterval = 5000
HTTPReadTimeout = 30 * time.Second
HTTPShutdownTimeout = 3 * time.Second
privateKeyFileMode = 0o600
registerCacheExpiration = time.Minute * 15
registerCacheCleanup = time.Minute * 20
DisabledClientAuth = "disabled"
RelaxedClientAuth = "relaxed"
EnforcedClientAuth = "enforced"
)
// Config contains the initial Headscale configuration.
type Config struct {
ServerURL string
Addr string
GRPCAddr string
GRPCAllowInsecure bool
EphemeralNodeInactivityTimeout time.Duration
IPPrefixes []netaddr.IPPrefix
PrivateKeyPath string
BaseDomain string
DERP DERPConfig
DBtype string
DBpath string
DBhost string
DBport int
DBname string
DBuser string
DBpass string
TLSLetsEncryptListen string
TLSLetsEncryptHostname string
TLSLetsEncryptCacheDir string
TLSLetsEncryptChallengeType string
TLSCertPath string
TLSKeyPath string
TLSClientAuthMode tls.ClientAuthType
ACMEURL string
ACMEEmail string
DNSConfig *tailcfg.DNSConfig
UnixSocket string
UnixSocketPermission fs.FileMode
OIDC OIDCConfig
CLI CLIConfig
}
type OIDCConfig struct {
Issuer string
ClientID string
ClientSecret string
MatchMap map[string]string
}
type DERPConfig struct {
URLs []url.URL
Paths []string
AutoUpdate bool
UpdateFrequency time.Duration
}
type CLIConfig struct {
Address string
APIKey string
Timeout time.Duration
Insecure bool
}
// Headscale represents the base app of the service.
type Headscale struct {
cfg Config
cfg *Config
db *gorm.DB
dbString string
dbType string
dbDebug bool
privateKey *key.MachinePrivate
DERPMap *tailcfg.DERPMap
DERPMap *tailcfg.DERPMap
DERPServer *DERPServer
aclPolicy *ACLPolicy
aclRules []tailcfg.FilterRule
lastStateChange sync.Map
lastStateChange *xsync.MapOf[time.Time]
oidcProvider *oidc.Provider
oauth2Config *oauth2.Config
oidcStateCache *cache.Cache
oidcProvider *oidc.Provider
oauth2Config *oauth2.Config
requestedExpiryCache *cache.Cache
registrationCache *cache.Cache
ipAllocationMutex sync.Mutex
shutdownChan chan struct{}
pollNetMapStreamWG sync.WaitGroup
}
// Look up the TLS constant relative to user-supplied TLS client
@@ -176,8 +119,7 @@ func LookupTLSClientAuthMode(mode string) (tls.ClientAuthType, bool) {
}
}
// NewHeadscale returns the Headscale app.
func NewHeadscale(cfg Config) (*Headscale, error) {
func NewHeadscale(cfg *Config) (*Headscale, error) {
privKey, err := readOrCreatePrivateKey(cfg.PrivateKeyPath)
if err != nil {
return nil, fmt.Errorf("failed to read or create private key: %w", err)
@@ -200,18 +142,19 @@ func NewHeadscale(cfg Config) (*Headscale, error) {
return nil, errUnsupportedDatabase
}
requestedExpiryCache := cache.New(
requestedExpiryCacheExpiration,
requestedExpiryCacheCleanupInterval,
registrationCache := cache.New(
registerCacheExpiration,
registerCacheCleanup,
)
app := Headscale{
cfg: cfg,
dbType: cfg.DBtype,
dbString: dbString,
privateKey: privKey,
aclRules: tailcfg.FilterAllowAll, // default allowall
requestedExpiryCache: requestedExpiryCache,
cfg: cfg,
dbType: cfg.DBtype,
dbString: dbString,
privateKey: privKey,
aclRules: tailcfg.FilterAllowAll, // default allowall
registrationCache: registrationCache,
pollNetMapStreamWG: sync.WaitGroup{},
}
err = app.initDB()
@@ -230,13 +173,21 @@ func NewHeadscale(cfg Config) (*Headscale, error) {
magicDNSDomains := generateMagicDNSRootDomains(app.cfg.IPPrefixes)
// we might have routes already from Split DNS
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 {
app.cfg.DNSConfig.Routes[d.WithoutTrailingDot()] = nil
}
}
if cfg.DERP.ServerEnabled {
embeddedDERPServer, err := app.NewDERPServer()
if err != nil {
return nil, err
}
app.DERPServer = embeddedDERPServer
}
return &app, nil
}
@@ -274,33 +225,38 @@ func (h *Headscale) expireEphemeralNodesWorker() {
return
}
expiredFound := false
for _, machine := range machines {
if machine.AuthKey != nil && machine.LastSeen != nil &&
machine.AuthKey.Ephemeral &&
time.Now().
After(machine.LastSeen.Add(h.cfg.EphemeralNodeInactivityTimeout)) {
expiredFound = true
log.Info().
Str("machine", machine.Name).
Str("machine", machine.Hostname).
Msg("Ephemeral client removed from database")
err = h.db.Unscoped().Delete(machine).Error
if err != nil {
log.Error().
Err(err).
Str("machine", machine.Name).
Str("machine", machine.Hostname).
Msg("🤮 Cannot delete ephemeral machine from the database")
}
}
}
h.setLastStateChangeToNow(namespace.Name)
if expiredFound {
h.setLastStateChangeToNow(namespace.Name)
}
}
}
func (h *Headscale) grpcAuthenticationInterceptor(ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler) (interface{}, error) {
handler grpc.UnaryHandler,
) (interface{}, error) {
// Check if the request is coming from the on-server client.
// This is not secure, but it is to maintain maintainability
// with the "legacy" database-based client
@@ -375,50 +331,74 @@ func (h *Headscale) grpcAuthenticationInterceptor(ctx context.Context,
return handler(ctx, req)
}
func (h *Headscale) httpAuthenticationMiddleware(ctx *gin.Context) {
log.Trace().
Caller().
Str("client_address", ctx.ClientIP()).
Msg("HTTP authentication invoked")
authHeader := ctx.GetHeader("authorization")
if !strings.HasPrefix(authHeader, AuthPrefix) {
log.Error().
func (h *Headscale) httpAuthenticationMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(
writer http.ResponseWriter,
req *http.Request,
) {
log.Trace().
Caller().
Str("client_address", ctx.ClientIP()).
Msg(`missing "Bearer " prefix in "Authorization" header`)
ctx.AbortWithStatus(http.StatusUnauthorized)
Str("client_address", req.RemoteAddr).
Msg("HTTP authentication invoked")
return
}
authHeader := req.Header.Get("authorization")
ctx.AbortWithStatus(http.StatusUnauthorized)
if !strings.HasPrefix(authHeader, AuthPrefix) {
log.Error().
Caller().
Str("client_address", req.RemoteAddr).
Msg(`missing "Bearer " prefix in "Authorization" header`)
writer.WriteHeader(http.StatusUnauthorized)
_, err := writer.Write([]byte("Unauthorized"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
valid, err := h.ValidateAPIKey(strings.TrimPrefix(authHeader, AuthPrefix))
if err != nil {
log.Error().
Caller().
Err(err).
Str("client_address", ctx.ClientIP()).
Msg("failed to validate token")
return
}
ctx.AbortWithStatus(http.StatusInternalServerError)
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")
return
}
writer.WriteHeader(http.StatusInternalServerError)
_, err := writer.Write([]byte("Unauthorized"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
if !valid {
log.Info().
Str("client_address", ctx.ClientIP()).
Msg("invalid token")
return
}
ctx.AbortWithStatus(http.StatusUnauthorized)
if !valid {
log.Info().
Str("client_address", req.RemoteAddr).
Msg("invalid token")
return
}
writer.WriteHeader(http.StatusUnauthorized)
_, err := writer.Write([]byte("Unauthorized"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
ctx.Next()
return
}
next.ServeHTTP(writer, req)
})
}
// ensureUnixSocketIsAbsent will check if the given path for headscales unix socket is clear
@@ -432,34 +412,34 @@ func (h *Headscale) ensureUnixSocketIsAbsent() error {
return os.Remove(h.cfg.UnixSocket)
}
func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *gin.Engine {
router := gin.Default()
func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *mux.Router {
router := mux.NewRouter()
prometheus := ginprometheus.NewPrometheus("gin")
prometheus.Use(router)
router.HandleFunc("/health", h.HealthHandler).Methods(http.MethodGet)
router.HandleFunc("/key", h.KeyHandler).Methods(http.MethodGet)
router.HandleFunc("/register", h.RegisterWebAPI).Methods(http.MethodGet)
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)
router.GET(
"/health",
func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"healthy": "ok"}) },
)
router.GET("/key", h.KeyHandler)
router.GET("/register", h.RegisterWebAPI)
router.POST("/machine/:id/map", h.PollNetMapHandler)
router.POST("/machine/:id", h.RegistrationHandler)
router.GET("/oidc/register/:mkey", h.RegisterOIDC)
router.GET("/oidc/callback", h.OIDCCallback)
router.GET("/apple", h.AppleMobileConfig)
router.GET("/apple/:platform", h.ApplePlatformConfig)
router.GET("/swagger", SwaggerUI)
router.GET("/swagger/v1/openapiv2.json", SwaggerAPIv1)
api := router.Group("/api")
api.Use(h.httpAuthenticationMiddleware)
{
api.Any("/v1/*any", gin.WrapF(grpcMux.ServeHTTP))
if h.cfg.DERP.ServerEnabled {
router.HandleFunc("/derp", h.DERPHandler)
router.HandleFunc("/derp/probe", h.DERPProbeHandler)
router.HandleFunc("/bootstrap-dns", h.DERPBootstrapDNSHandler)
}
router.NoRoute(stdoutHandler)
apiRouter := router.PathPrefix("/api").Subrouter()
apiRouter.Use(h.httpAuthenticationMiddleware)
apiRouter.PathPrefix("/v1/").HandlerFunc(grpcMux.ServeHTTP)
router.PathPrefix("/").HandlerFunc(stdoutHandler)
return router
}
@@ -471,6 +451,16 @@ func (h *Headscale) Serve() error {
// Fetch an initial DERP Map before we start serving
h.DERPMap = GetDERPMap(h.cfg.DERP)
if h.cfg.DERP.ServerEnabled {
// When embedded DERP is enabled we always need a STUN server
if h.cfg.DERP.STUNAddr == "" {
return errSTUNAddressNotSet
}
h.DERPMap.Regions[h.DERPServer.region.RegionID] = &h.DERPServer.region
go h.ServeSTUN()
}
if h.cfg.DERP.AutoUpdate {
derpMapCancelChannel := make(chan struct{})
defer func() { derpMapCancelChannel <- struct{}{} }()
@@ -512,19 +502,6 @@ func (h *Headscale) Serve() error {
return fmt.Errorf("failed change permission of gRPC socket: %w", err)
}
// Handle common process-killing signals so we can gracefully shut down:
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
go func(c chan os.Signal) {
// Wait for a SIGINT or SIGKILL:
sig := <-c
log.Printf("Caught signal %s: shutting down.", sig)
// Stop listening (and unlink the socket if unix type):
socketListener.Close()
// And we're done:
os.Exit(0)
}(sigc)
grpcGatewayMux := runtime.NewServeMux()
// Make the grpc-gateway connect to grpc over socket
@@ -578,6 +555,8 @@ func (h *Headscale) Serve() error {
// https://github.com/soheilhy/cmux/issues/68
// https://github.com/soheilhy/cmux/issues/91
var grpcServer *grpc.Server
var grpcListener net.Listener
if tlsConfig != nil || h.cfg.GRPCAllowInsecure {
log.Info().Msgf("Enabling remote gRPC at %s", h.cfg.GRPCAddr)
@@ -598,12 +577,12 @@ func (h *Headscale) Serve() error {
log.Warn().Msg("gRPC is running without security")
}
grpcServer := grpc.NewServer(grpcOptions...)
grpcServer = grpc.NewServer(grpcOptions...)
v1.RegisterHeadscaleServiceServer(grpcServer, newHeadscaleV1APIServer(h))
reflection.Register(grpcServer)
grpcListener, err := net.Listen("tcp", h.cfg.GRPCAddr)
grpcListener, err = net.Listen("tcp", h.cfg.GRPCAddr)
if err != nil {
return fmt.Errorf("failed to bind to TCP address: %w", err)
}
@@ -648,12 +627,124 @@ func (h *Headscale) Serve() error {
log.Info().
Msgf("listening and serving HTTP on: %s", h.cfg.Addr)
promMux := http.NewServeMux()
promMux.Handle("/metrics", promhttp.Handler())
promHTTPServer := &http.Server{
Addr: h.cfg.MetricsAddr,
Handler: promMux,
ReadTimeout: HTTPReadTimeout,
WriteTimeout: 0,
}
var promHTTPListener net.Listener
promHTTPListener, err = net.Listen("tcp", h.cfg.MetricsAddr)
if err != nil {
return fmt.Errorf("failed to bind to TCP address: %w", err)
}
errorGroup.Go(func() error { return promHTTPServer.Serve(promHTTPListener) })
log.Info().
Msgf("listening and serving metrics on: %s", h.cfg.MetricsAddr)
// Handle common process-killing signals so we can gracefully shut down:
h.shutdownChan = make(chan struct{})
sigc := make(chan os.Signal, 1)
signal.Notify(sigc,
syscall.SIGHUP,
syscall.SIGINT,
syscall.SIGTERM,
syscall.SIGQUIT,
syscall.SIGHUP)
sigFunc := func(c chan os.Signal) {
// Wait for a SIGINT or SIGKILL:
for {
sig := <-c
switch sig {
case syscall.SIGHUP:
log.Info().
Str("signal", sig.String()).
Msg("Received SIGHUP, reloading ACL and Config")
// TODO(kradalby): Reload config on SIGHUP
if h.cfg.ACL.PolicyPath != "" {
aclPath := AbsolutePathFromConfigPath(h.cfg.ACL.PolicyPath)
err := h.LoadACLPolicy(aclPath)
if err != nil {
log.Error().Err(err).Msg("Failed to reload ACL policy")
}
log.Info().
Str("path", aclPath).
Msg("ACL policy successfully reloaded, notifying nodes of change")
h.setLastStateChangeToNow()
}
default:
log.Info().
Str("signal", sig.String()).
Msg("Received signal to stop, shutting down gracefully")
close(h.shutdownChan)
h.pollNetMapStreamWG.Wait()
// Gracefully shut down servers
ctx, cancel := context.WithTimeout(context.Background(), HTTPShutdownTimeout)
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()
if grpcServer != nil {
grpcServer.GracefulStop()
grpcListener.Close()
}
// Close network listeners
promHTTPListener.Close()
httpListener.Close()
grpcGatewayConn.Close()
// Stop listening (and unlink the socket if unix type):
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:
cancel()
os.Exit(0)
}
}
}
errorGroup.Go(func() error {
sigFunc(sigc)
return nil
})
return errorGroup.Wait()
}
func (h *Headscale) getTLSSettings() (*tls.Config, error) {
var err error
if h.cfg.TLSLetsEncryptHostname != "" {
if h.cfg.TLS.LetsEncrypt.Hostname != "" {
if !strings.HasPrefix(h.cfg.ServerURL, "https://") {
log.Warn().
Msg("Listening with TLS but ServerURL does not start with https://")
@@ -661,29 +752,29 @@ func (h *Headscale) getTLSSettings() (*tls.Config, error) {
certManager := autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(h.cfg.TLSLetsEncryptHostname),
Cache: autocert.DirCache(h.cfg.TLSLetsEncryptCacheDir),
HostPolicy: autocert.HostWhitelist(h.cfg.TLS.LetsEncrypt.Hostname),
Cache: autocert.DirCache(h.cfg.TLS.LetsEncrypt.CacheDir),
Client: &acme.Client{
DirectoryURL: h.cfg.ACMEURL,
},
Email: h.cfg.ACMEEmail,
}
switch h.cfg.TLSLetsEncryptChallengeType {
case "TLS-ALPN-01":
switch h.cfg.TLS.LetsEncrypt.ChallengeType {
case tlsALPN01ChallengeType:
// Configuration via autocert with TLS-ALPN-01 (https://tools.ietf.org/html/rfc8737)
// The RFC requires that the validation is done on port 443; in other words, headscale
// must be reachable on port 443.
return certManager.TLSConfig(), nil
case "HTTP-01":
case http01ChallengeType:
// Configuration via autocert with HTTP-01. This requires listening on
// port 80 for the certificate validation in addition to the headscale
// service, which can be configured to run on any other port.
go func() {
log.Fatal().
Caller().
Err(http.ListenAndServe(h.cfg.TLSLetsEncryptListen, certManager.HTTPHandler(http.HandlerFunc(h.redirect)))).
Err(http.ListenAndServe(h.cfg.TLS.LetsEncrypt.Listen, certManager.HTTPHandler(http.HandlerFunc(h.redirect)))).
Msg("failed to set up a HTTP server")
}()
@@ -692,7 +783,7 @@ func (h *Headscale) getTLSSettings() (*tls.Config, error) {
default:
return nil, errUnsupportedLetsEncryptChallengeType
}
} else if h.cfg.TLSCertPath == "" {
} else if h.cfg.TLS.CertPath == "" {
if !strings.HasPrefix(h.cfg.ServerURL, "http://") {
log.Warn().Msg("Listening without TLS but ServerURL does not start with http://")
}
@@ -705,36 +796,59 @@ func (h *Headscale) getTLSSettings() (*tls.Config, error) {
log.Info().Msg(fmt.Sprintf(
"Client authentication (mTLS) is \"%s\". See the docs to learn about configuring this setting.",
h.cfg.TLSClientAuthMode))
h.cfg.TLS.ClientAuthMode))
tlsConfig := &tls.Config{
ClientAuth: h.cfg.TLSClientAuthMode,
ClientAuth: h.cfg.TLS.ClientAuthMode,
NextProtos: []string{"http/1.1"},
Certificates: make([]tls.Certificate, 1),
MinVersion: tls.VersionTLS12,
}
tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(h.cfg.TLSCertPath, h.cfg.TLSKeyPath)
tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(h.cfg.TLS.CertPath, h.cfg.TLS.KeyPath)
return tlsConfig, err
}
}
func (h *Headscale) setLastStateChangeToNow(namespace string) {
func (h *Headscale) setLastStateChangeToNow(namespaces ...string) {
var err error
now := time.Now().UTC()
lastStateUpdate.WithLabelValues("", "headscale").Set(float64(now.Unix()))
h.lastStateChange.Store(namespace, now)
if len(namespaces) == 0 {
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)
}
}
func (h *Headscale) getLastStateChange(namespaces ...string) time.Time {
times := []time.Time{}
for _, namespace := range namespaces {
if wrapped, ok := h.lastStateChange.Load(namespace); ok {
lastChange, _ := wrapped.(time.Time)
times = append(times, lastChange)
// getLastStateChange takes a list of namespaces as a "filter", if no namespaces
// are past, then use the entier list of namespaces and look for the last update
if len(namespaces) > 0 {
for _, namespace := range namespaces {
if lastChange, ok := h.lastStateChange.Load(namespace); ok {
times = append(times, lastChange)
}
}
} else {
h.lastStateChange.Range(func(key string, value time.Time) bool {
times = append(times, value)
return true
})
}
sort.Slice(times, func(i, j int) bool {
@@ -750,13 +864,16 @@ func (h *Headscale) getLastStateChange(namespaces ...string) time.Time {
}
}
func stdoutHandler(ctx *gin.Context) {
body, _ := io.ReadAll(ctx.Request.Body)
func stdoutHandler(
writer http.ResponseWriter,
req *http.Request,
) {
body, _ := io.ReadAll(req.Body)
log.Trace().
Interface("header", ctx.Request.Header).
Interface("proto", ctx.Request.Proto).
Interface("url", ctx.Request.URL).
Interface("header", req.Header).
Interface("proto", req.Proto).
Interface("url", req.URL).
Bytes("body", body).
Msg("Request did not match")
}

View File

@@ -5,7 +5,6 @@ import (
"os"
"testing"
"github.com/patrickmn/go-cache"
"gopkg.in/check.v1"
"inet.af/netaddr"
)
@@ -47,13 +46,9 @@ func (s *Suite) ResetDB(c *check.C) {
}
app = Headscale{
cfg: cfg,
cfg: &cfg,
dbType: "sqlite3",
dbString: tmpDir + "/headscale_test.db",
requestedExpiryCache: cache.New(
requestedExpiryCacheExpiration,
requestedExpiryCacheCleanupInterval,
),
}
err = app.initDB()
if err != nil {

View File

@@ -1,266 +0,0 @@
package headscale
import (
"bytes"
"html/template"
"net/http"
"github.com/gin-gonic/gin"
"github.com/gofrs/uuid"
"github.com/rs/zerolog/log"
)
// AppleMobileConfig shows a simple message in the browser to point to the CLI
// Listens in /register.
func (h *Headscale) AppleMobileConfig(ctx *gin.Context) {
appleTemplate := template.Must(template.New("apple").Parse(`
<html>
<body>
<h1>Apple configuration profiles</h1>
<p>
This page provides <a href="https://support.apple.com/guide/mdm/mdm-overview-mdmbf9e668/web">configuration profiles</a> for the official Tailscale clients for <a href="https://apps.apple.com/us/app/tailscale/id1470499037?ls=1">iOS</a> and <a href="https://apps.apple.com/ca/app/tailscale/id1475387142?mt=12">macOS</a>.
</p>
<p>
The profiles will configure Tailscale.app to use {{.Url}} as its control server.
</p>
<h3>Caution</h3>
<p>You should always inspect the profile before installing it:</p>
<!--
<p><code>curl {{.Url}}/apple/ios</code></p>
-->
<p><code>curl {{.Url}}/apple/macos</code></p>
<h2>Profiles</h2>
<!--
<h3>iOS</h3>
<p>
<a href="/apple/ios" download="headscale_ios.mobileconfig">iOS profile</a>
</p>
-->
<h3>macOS</h3>
<p>Headscale can be set to the default server by installing a Headscale configuration profile:</p>
<p>
<a href="/apple/macos" download="headscale_macos.mobileconfig">macOS profile</a>
</p>
<ol>
<li>Download the profile, then open it. When it has been opened, there should be a notification that a profile can be installed</li>
<li>Open System Preferences and go to "Profiles"</li>
<li>Find and install the Headscale profile</li>
<li>Restart Tailscale.app and log in</li>
</ol>
<p>Or</p>
<p>Use your terminal to configure the default setting for Tailscale by issuing:</p>
<code>defaults write io.tailscale.ipn.macos ControlURL {{.URL}}</code>
<p>Restart Tailscale.app and log in.</p>
</body>
</html>`))
config := map[string]interface{}{
"URL": h.cfg.ServerURL,
}
var payload bytes.Buffer
if err := appleTemplate.Execute(&payload, config); err != nil {
log.Error().
Str("handler", "AppleMobileConfig").
Err(err).
Msg("Could not render Apple index template")
ctx.Data(
http.StatusInternalServerError,
"text/html; charset=utf-8",
[]byte("Could not render Apple index template"),
)
return
}
ctx.Data(http.StatusOK, "text/html; charset=utf-8", payload.Bytes())
}
func (h *Headscale) ApplePlatformConfig(ctx *gin.Context) {
platform := ctx.Param("platform")
id, err := uuid.NewV4()
if err != nil {
log.Error().
Str("handler", "ApplePlatformConfig").
Err(err).
Msg("Failed not create UUID")
ctx.Data(
http.StatusInternalServerError,
"text/html; charset=utf-8",
[]byte("Failed to create UUID"),
)
return
}
contentID, err := uuid.NewV4()
if err != nil {
log.Error().
Str("handler", "ApplePlatformConfig").
Err(err).
Msg("Failed not create UUID")
ctx.Data(
http.StatusInternalServerError,
"text/html; charset=utf-8",
[]byte("Failed to create UUID"),
)
return
}
platformConfig := AppleMobilePlatformConfig{
UUID: contentID,
URL: h.cfg.ServerURL,
}
var payload bytes.Buffer
switch platform {
case "macos":
if err := macosTemplate.Execute(&payload, platformConfig); err != nil {
log.Error().
Str("handler", "ApplePlatformConfig").
Err(err).
Msg("Could not render Apple macOS template")
ctx.Data(
http.StatusInternalServerError,
"text/html; charset=utf-8",
[]byte("Could not render Apple macOS template"),
)
return
}
case "ios":
if err := iosTemplate.Execute(&payload, platformConfig); err != nil {
log.Error().
Str("handler", "ApplePlatformConfig").
Err(err).
Msg("Could not render Apple iOS template")
ctx.Data(
http.StatusInternalServerError,
"text/html; charset=utf-8",
[]byte("Could not render Apple iOS template"),
)
return
}
default:
ctx.Data(
http.StatusOK,
"text/html; charset=utf-8",
[]byte("Invalid platform, only ios and macos is supported"),
)
return
}
config := AppleMobileConfig{
UUID: id,
URL: h.cfg.ServerURL,
Payload: payload.String(),
}
var content bytes.Buffer
if err := commonTemplate.Execute(&content, config); err != nil {
log.Error().
Str("handler", "ApplePlatformConfig").
Err(err).
Msg("Could not render Apple platform template")
ctx.Data(
http.StatusInternalServerError,
"text/html; charset=utf-8",
[]byte("Could not render Apple platform template"),
)
return
}
ctx.Data(
http.StatusOK,
"application/x-apple-aspen-config; charset=utf-8",
content.Bytes(),
)
}
type AppleMobileConfig struct {
UUID uuid.UUID
URL string
Payload string
}
type AppleMobilePlatformConfig struct {
UUID uuid.UUID
URL string
}
var commonTemplate = template.Must(
template.New("mobileconfig").Parse(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PayloadUUID</key>
<string>{{.UUID}}</string>
<key>PayloadDisplayName</key>
<string>Headscale</string>
<key>PayloadDescription</key>
<string>Configure Tailscale login server to: {{.URL}}</string>
<key>PayloadIdentifier</key>
<string>com.github.juanfont.headscale</string>
<key>PayloadRemovalDisallowed</key>
<false/>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PayloadContent</key>
<array>
{{.Payload}}
</array>
</dict>
</plist>`),
)
var iosTemplate = template.Must(template.New("iosTemplate").Parse(`
<dict>
<key>PayloadType</key>
<string>io.tailscale.ipn.ios</string>
<key>PayloadUUID</key>
<string>{{.UUID}}</string>
<key>PayloadIdentifier</key>
<string>com.github.juanfont.headscale</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PayloadEnabled</key>
<true/>
<key>ControlURL</key>
<string>{{.URL}}</string>
</dict>
`))
var macosTemplate = template.Must(template.New("macosTemplate").Parse(`
<dict>
<key>PayloadType</key>
<string>io.tailscale.ipn.macos</string>
<key>PayloadUUID</key>
<string>{{.UUID}}</string>
<key>PayloadIdentifier</key>
<string>com.github.juanfont.headscale</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PayloadEnabled</key>
<true/>
<key>ControlURL</key>
<string>{{.URL}}</string>
</dict>
`))

View File

@@ -1,41 +0,0 @@
package headscale
import (
"time"
"gopkg.in/check.v1"
"inet.af/netaddr"
)
func (s *Suite) TestRegisterMachine(c *check.C) {
namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil)
now := time.Now().UTC()
machine := Machine{
ID: 0,
MachineKey: "8ce002a935f8c394e55e78fbbb410576575ff8ec5cfa2e627e4b807f1be15b0e",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testmachine",
NamespaceID: namespace.ID,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("10.0.0.1")},
Expiry: &now,
}
err = app.db.Save(&machine).Error
c.Assert(err, check.IsNil)
_, err = app.GetMachine(namespace.Name, machine.Name)
c.Assert(err, check.IsNil)
machineAfterRegistering, err := app.RegisterMachine(
machine.MachineKey,
namespace.Name,
)
c.Assert(err, check.IsNil)
c.Assert(machineAfterRegistering.Registered, check.Equals, true)
_, err = machineAfterRegistering.GetHostInfo()
c.Assert(err, check.IsNil)
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/juanfont/headscale"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/prometheus/common/model"
"github.com/pterm/pterm"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
@@ -15,7 +16,7 @@ import (
const (
// 90 days.
DefaultAPIKeyExpiry = 90 * 24 * time.Hour
DefaultAPIKeyExpiry = "90d"
)
func init() {
@@ -23,7 +24,7 @@ func init() {
apiKeysCmd.AddCommand(listAPIKeys)
createAPIKeyCmd.Flags().
DurationP("expiration", "e", DefaultAPIKeyExpiry, "Human-readable expiration of the key (30m, 24h, 365d...)")
StringP("expiration", "e", DefaultAPIKeyExpiry, "Human-readable expiration of the key (e.g. 30m, 24h)")
apiKeysCmd.AddCommand(createAPIKeyCmd)
@@ -36,13 +37,15 @@ func init() {
}
var apiKeysCmd = &cobra.Command{
Use: "apikeys",
Short: "Handle the Api keys in Headscale",
Use: "apikeys",
Short: "Handle the Api keys in Headscale",
Aliases: []string{"apikey", "api"},
}
var listAPIKeys = &cobra.Command{
Use: "list",
Short: "List the Api keys for headscale",
Use: "list",
Short: "List the Api keys for headscale",
Aliases: []string{"ls", "show"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
@@ -107,6 +110,7 @@ var createAPIKeyCmd = &cobra.Command{
Creates a new Api key, the Api key is only visible on creation
and cannot be retrieved again.
If you loose a key, create a new one and revoke (expire) the old one.`,
Aliases: []string{"c", "new"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
@@ -115,10 +119,22 @@ If you loose a key, create a new one and revoke (expire) the old one.`,
request := &v1.CreateApiKeyRequest{}
duration, _ := cmd.Flags().GetDuration("expiration")
expiration := time.Now().UTC().Add(duration)
durationStr, _ := cmd.Flags().GetString("expiration")
log.Trace().Dur("expiration", duration).Msg("expiration has been set")
duration, err := model.ParseDuration(durationStr)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Could not parse duration: %s\n", err),
output,
)
return
}
expiration := time.Now().UTC().Add(time.Duration(duration))
log.Trace().Dur("expiration", time.Duration(duration)).Msg("expiration has been set")
request.Expiration = timestamppb.New(expiration)
@@ -144,7 +160,7 @@ If you loose a key, create a new one and revoke (expire) the old one.`,
var expireAPIKeyCmd = &cobra.Command{
Use: "expire",
Short: "Expire an ApiKey",
Aliases: []string{"revoke"},
Aliases: []string{"revoke", "exp", "e"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")

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

@@ -13,8 +13,9 @@ func init() {
}
var generateCmd = &cobra.Command{
Use: "generate",
Short: "Generate commands",
Use: "generate",
Short: "Generate commands",
Aliases: []string{"gen"},
}
var generatePrivateKeyCmd = &cobra.Command{

View File

@@ -25,13 +25,15 @@ const (
)
var namespaceCmd = &cobra.Command{
Use: "namespaces",
Short: "Manage the namespaces of Headscale",
Use: "namespaces",
Short: "Manage the namespaces of Headscale",
Aliases: []string{"namespace", "ns", "user", "users"},
}
var createNamespaceCmd = &cobra.Command{
Use: "create NAME",
Short: "Creates a new namespace",
Use: "create NAME",
Short: "Creates a new namespace",
Aliases: []string{"c", "new"},
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return errMissingParameter
@@ -72,8 +74,9 @@ var createNamespaceCmd = &cobra.Command{
}
var destroyNamespaceCmd = &cobra.Command{
Use: "destroy NAME",
Short: "Destroys a namespace",
Use: "destroy NAME",
Short: "Destroys a namespace",
Aliases: []string{"delete"},
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return errMissingParameter
@@ -144,8 +147,9 @@ var destroyNamespaceCmd = &cobra.Command{
}
var listNamespacesCmd = &cobra.Command{
Use: "list",
Short: "List all the namespaces",
Use: "list",
Short: "List all the namespaces",
Aliases: []string{"ls", "show"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
@@ -197,8 +201,9 @@ var listNamespacesCmd = &cobra.Command{
}
var renameNamespaceCmd = &cobra.Command{
Use: "rename OLD_NAME NEW_NAME",
Short: "Renames a namespace",
Use: "rename OLD_NAME NEW_NAME",
Short: "Renames a namespace",
Aliases: []string{"mv"},
Args: func(cmd *cobra.Command, args []string) error {
expectedArguments := 2
if len(args) < expectedArguments {

View File

@@ -13,12 +13,14 @@ import (
"github.com/pterm/pterm"
"github.com/spf13/cobra"
"google.golang.org/grpc/status"
"inet.af/netaddr"
"tailscale.com/types/key"
)
func init() {
rootCmd.AddCommand(nodeCmd)
listNodesCmd.Flags().StringP("namespace", "n", "", "Filter by namespace")
listNodesCmd.Flags().BoolP("tags", "t", false, "Show tags")
nodeCmd.AddCommand(listNodesCmd)
registerNodeCmd.Flags().StringP("namespace", "n", "", "Namespace")
@@ -40,6 +42,13 @@ func init() {
}
nodeCmd.AddCommand(expireNodeCmd)
renameNodeCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
err = renameNodeCmd.MarkFlagRequired("identifier")
if err != nil {
log.Fatalf(err.Error())
}
nodeCmd.AddCommand(renameNodeCmd)
deleteNodeCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
err = deleteNodeCmd.MarkFlagRequired("identifier")
if err != nil {
@@ -47,34 +56,36 @@ func init() {
}
nodeCmd.AddCommand(deleteNodeCmd)
shareMachineCmd.Flags().StringP("namespace", "n", "", "Namespace")
err = shareMachineCmd.MarkFlagRequired("namespace")
if err != nil {
log.Fatalf(err.Error())
}
shareMachineCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
err = shareMachineCmd.MarkFlagRequired("identifier")
if err != nil {
log.Fatalf(err.Error())
}
nodeCmd.AddCommand(shareMachineCmd)
moveNodeCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
unshareMachineCmd.Flags().StringP("namespace", "n", "", "Namespace")
err = unshareMachineCmd.MarkFlagRequired("namespace")
err = moveNodeCmd.MarkFlagRequired("identifier")
if err != nil {
log.Fatalf(err.Error())
}
unshareMachineCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
err = unshareMachineCmd.MarkFlagRequired("identifier")
moveNodeCmd.Flags().StringP("namespace", "n", "", "New namespace")
err = moveNodeCmd.MarkFlagRequired("namespace")
if err != nil {
log.Fatalf(err.Error())
}
nodeCmd.AddCommand(unshareMachineCmd)
nodeCmd.AddCommand(moveNodeCmd)
tagCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
err = tagCmd.MarkFlagRequired("identifier")
if err != nil {
log.Fatalf(err.Error())
}
tagCmd.Flags().
StringSliceP("tags", "t", []string{}, "List of tags to add to the node")
nodeCmd.AddCommand(tagCmd)
}
var nodeCmd = &cobra.Command{
Use: "nodes",
Short: "Manage the nodes of Headscale",
Use: "nodes",
Short: "Manage the nodes of Headscale",
Aliases: []string{"node", "machine", "machines"},
}
var registerNodeCmd = &cobra.Command{
@@ -128,8 +139,9 @@ var registerNodeCmd = &cobra.Command{
}
var listNodesCmd = &cobra.Command{
Use: "list",
Short: "List nodes",
Use: "list",
Short: "List nodes",
Aliases: []string{"ls", "show"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
namespace, err := cmd.Flags().GetString("namespace")
@@ -138,6 +150,12 @@ var listNodesCmd = &cobra.Command{
return
}
showTags, err := cmd.Flags().GetBool("tags")
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error getting tags flag: %s", err), output)
return
}
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
@@ -164,7 +182,7 @@ var listNodesCmd = &cobra.Command{
return
}
tableData, err := nodesToPtables(namespace, response.Machines)
tableData, err := nodesToPtables(namespace, showTags, response.Machines)
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error converting to table: %s", err), output)
@@ -188,7 +206,7 @@ var expireNodeCmd = &cobra.Command{
Use: "expire",
Short: "Expire (log out) a machine in your network",
Long: "Expiring a node will keep the node in the database and force it to reauthenticate.",
Aliases: []string{"logout"},
Aliases: []string{"logout", "exp", "e"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
@@ -229,9 +247,58 @@ var expireNodeCmd = &cobra.Command{
},
}
var renameNodeCmd = &cobra.Command{
Use: "rename NEW_NAME",
Short: "Renames a machine in your network",
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
identifier, err := cmd.Flags().GetUint64("identifier")
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error converting ID to integer: %s", err),
output,
)
return
}
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
newName := ""
if len(args) > 0 {
newName = args[0]
}
request := &v1.RenameMachineRequest{
MachineId: identifier,
NewName: newName,
}
response, err := client.RenameMachine(ctx, request)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf(
"Cannot rename machine: %s\n",
status.Convert(err).Message(),
),
output,
)
return
}
SuccessOutput(response.Machine, "Machine renamed", output)
},
}
var deleteNodeCmd = &cobra.Command{
Use: "delete",
Short: "Delete a node",
Use: "delete",
Short: "Delete a node",
Aliases: []string{"del"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
@@ -317,70 +384,29 @@ var deleteNodeCmd = &cobra.Command{
},
}
func sharingWorker(
cmd *cobra.Command,
) (string, *v1.Machine, *v1.Namespace, error) {
output, _ := cmd.Flags().GetString("output")
namespaceStr, err := cmd.Flags().GetString("namespace")
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output)
return "", nil, nil, err
}
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
identifier, err := cmd.Flags().GetUint64("identifier")
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error converting ID to integer: %s", err), output)
return "", nil, nil, err
}
machineRequest := &v1.GetMachineRequest{
MachineId: identifier,
}
machineResponse, err := client.GetMachine(ctx, machineRequest)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error getting node node: %s", status.Convert(err).Message()),
output,
)
return "", nil, nil, err
}
namespaceRequest := &v1.GetNamespaceRequest{
Name: namespaceStr,
}
namespaceResponse, err := client.GetNamespace(ctx, namespaceRequest)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error getting node node: %s", status.Convert(err).Message()),
output,
)
return "", nil, nil, err
}
return output, machineResponse.GetMachine(), namespaceResponse.GetNamespace(), nil
}
var shareMachineCmd = &cobra.Command{
Use: "share",
Short: "Shares a node from the current namespace to the specified one",
var moveNodeCmd = &cobra.Command{
Use: "move",
Short: "Move node to another namespace",
Aliases: []string{"mv"},
Run: func(cmd *cobra.Command, args []string) {
output, machine, namespace, err := sharingWorker(cmd)
output, _ := cmd.Flags().GetString("output")
identifier, err := cmd.Flags().GetUint64("identifier")
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Failed to fetch namespace or machine: %s", err),
fmt.Sprintf("Error converting ID to integer: %s", err),
output,
)
return
}
namespace, err := cmd.Flags().GetString("namespace")
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error getting namespace: %s", err),
output,
)
@@ -391,82 +417,72 @@ var shareMachineCmd = &cobra.Command{
defer cancel()
defer conn.Close()
request := &v1.ShareMachineRequest{
MachineId: machine.Id,
Namespace: namespace.Name,
getRequest := &v1.GetMachineRequest{
MachineId: identifier,
}
response, err := client.ShareMachine(ctx, request)
_, err = client.GetMachine(ctx, getRequest)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error sharing node: %s", status.Convert(err).Message()),
fmt.Sprintf(
"Error getting node: %s",
status.Convert(err).Message(),
),
output,
)
return
}
SuccessOutput(response.Machine, "Node shared", output)
},
}
moveRequest := &v1.MoveMachineRequest{
MachineId: identifier,
Namespace: namespace,
}
var unshareMachineCmd = &cobra.Command{
Use: "unshare",
Short: "Unshares a node from the specified namespace",
Run: func(cmd *cobra.Command, args []string) {
output, machine, namespace, err := sharingWorker(cmd)
moveResponse, err := client.MoveMachine(ctx, moveRequest)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Failed to fetch namespace or machine: %s", err),
fmt.Sprintf(
"Error moving node: %s",
status.Convert(err).Message(),
),
output,
)
return
}
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
request := &v1.UnshareMachineRequest{
MachineId: machine.Id,
Namespace: namespace.Name,
}
response, err := client.UnshareMachine(ctx, request)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error unsharing node: %s", status.Convert(err).Message()),
output,
)
return
}
SuccessOutput(response.Machine, "Node unshared", output)
SuccessOutput(moveResponse.Machine, "Node moved to another namespace", output)
},
}
func nodesToPtables(
currentNamespace string,
showTags bool,
machines []*v1.Machine,
) (pterm.TableData, error) {
tableData := pterm.TableData{
{
"ID",
"Name",
"NodeKey",
"Namespace",
"IP addresses",
"Ephemeral",
"Last seen",
"Online",
"Expired",
},
tableHeader := []string{
"ID",
"Hostname",
"Name",
"NodeKey",
"Namespace",
"IP addresses",
"Ephemeral",
"Last seen",
"Online",
"Expired",
}
if showTags {
tableHeader = append(tableHeader, []string{
"ForcedTags",
"InvalidTags",
"ValidTags",
}...)
}
tableData := pterm.TableData{tableHeader}
for _, machine := range machines {
var ephemeral bool
@@ -510,6 +526,26 @@ func nodesToPtables(
expired = pterm.LightRed("yes")
}
var forcedTags string
for _, tag := range machine.ForcedTags {
forcedTags += "," + tag
}
forcedTags = strings.TrimLeft(forcedTags, ",")
var invalidTags string
for _, tag := range machine.InvalidTags {
if !contains(machine.ForcedTags, tag) {
invalidTags += "," + pterm.LightRed(tag)
}
}
invalidTags = strings.TrimLeft(invalidTags, ",")
var validTags string
for _, tag := range machine.ValidTags {
if !contains(machine.ForcedTags, tag) {
validTags += "," + pterm.LightGreen(tag)
}
}
validTags = strings.TrimLeft(validTags, ",")
var namespace string
if currentNamespace == "" || (currentNamespace == machine.Namespace.Name) {
namespace = pterm.LightMagenta(machine.Namespace.Name)
@@ -517,21 +553,95 @@ func nodesToPtables(
// Shared into this namespace
namespace = pterm.LightYellow(machine.Namespace.Name)
}
var IPV4Address string
var IPV6Address string
for _, addr := range machine.IpAddresses {
if netaddr.MustParseIP(addr).Is4() {
IPV4Address = addr
} else {
IPV6Address = addr
}
}
nodeData := []string{
strconv.FormatUint(machine.Id, headscale.Base10),
machine.Name,
machine.GetGivenName(),
nodeKey.ShortString(),
namespace,
strings.Join([]string{IPV4Address, IPV6Address}, ", "),
strconv.FormatBool(ephemeral),
lastSeenTime,
online,
expired,
}
if showTags {
nodeData = append(nodeData, []string{forcedTags, invalidTags, validTags}...)
}
tableData = append(
tableData,
[]string{
strconv.FormatUint(machine.Id, headscale.Base10),
machine.Name,
nodeKey.ShortString(),
namespace,
strings.Join(machine.IpAddresses, ", "),
strconv.FormatBool(ephemeral),
lastSeenTime,
online,
expired,
},
nodeData,
)
}
return tableData, nil
}
var tagCmd = &cobra.Command{
Use: "tag",
Short: "Manage the tags of a node",
Aliases: []string{"tags", "t"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
// retrieve flags from CLI
identifier, err := cmd.Flags().GetUint64("identifier")
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error converting ID to integer: %s", err),
output,
)
return
}
tagsToSet, err := cmd.Flags().GetStringSlice("tags")
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error retrieving list of tags to add to machine, %v", err),
output,
)
return
}
// Sending tags to machine
request := &v1.SetTagsRequest{
MachineId: identifier,
Tags: tagsToSet,
}
resp, err := client.SetTags(ctx, request)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error while sending tags to headscale: %s", err),
output,
)
return
}
if resp != nil {
SuccessOutput(
resp.GetMachine(),
"Machine updated",
output,
)
}
},
}

View File

@@ -6,6 +6,7 @@ import (
"time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/prometheus/common/model"
"github.com/pterm/pterm"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
@@ -13,7 +14,7 @@ import (
)
const (
DefaultPreAuthKeyExpiry = 1 * time.Hour
DefaultPreAuthKeyExpiry = "1h"
)
func init() {
@@ -31,17 +32,19 @@ func init() {
createPreAuthKeyCmd.PersistentFlags().
Bool("ephemeral", false, "Preauthkey for ephemeral nodes")
createPreAuthKeyCmd.Flags().
DurationP("expiration", "e", DefaultPreAuthKeyExpiry, "Human-readable expiration of the key (30m, 24h, 365d...)")
StringP("expiration", "e", DefaultPreAuthKeyExpiry, "Human-readable expiration of the key (e.g. 30m, 24h)")
}
var preauthkeysCmd = &cobra.Command{
Use: "preauthkeys",
Short: "Handle the preauthkeys in Headscale",
Use: "preauthkeys",
Short: "Handle the preauthkeys in Headscale",
Aliases: []string{"preauthkey", "authkey", "pre"},
}
var listPreAuthKeys = &cobra.Command{
Use: "list",
Short: "List the preauthkeys for this namespace",
Use: "list",
Short: "List the preauthkeys for this namespace",
Aliases: []string{"ls", "show"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
@@ -118,8 +121,9 @@ var listPreAuthKeys = &cobra.Command{
}
var createPreAuthKeyCmd = &cobra.Command{
Use: "create",
Short: "Creates a new preauthkey in the specified namespace",
Use: "create",
Short: "Creates a new preauthkey in the specified namespace",
Aliases: []string{"c", "new"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
@@ -145,10 +149,22 @@ var createPreAuthKeyCmd = &cobra.Command{
Ephemeral: ephemeral,
}
duration, _ := cmd.Flags().GetDuration("expiration")
expiration := time.Now().UTC().Add(duration)
durationStr, _ := cmd.Flags().GetString("expiration")
log.Trace().Dur("expiration", duration).Msg("expiration has been set")
duration, err := model.ParseDuration(durationStr)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Could not parse duration: %s\n", err),
output,
)
return
}
expiration := time.Now().UTC().Add(time.Duration(duration))
log.Trace().Dur("expiration", time.Duration(duration)).Msg("expiration has been set")
request.Expiration = timestamppb.New(expiration)
@@ -172,8 +188,9 @@ var createPreAuthKeyCmd = &cobra.Command{
}
var expirePreAuthKeyCmd = &cobra.Command{
Use: "expire KEY",
Short: "Expire a preauthkey",
Use: "expire KEY",
Short: "Expire a preauthkey",
Aliases: []string{"revoke", "exp", "e"},
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return errMissingParameter

View File

@@ -3,17 +3,75 @@ package cli
import (
"fmt"
"os"
"runtime"
"github.com/juanfont/headscale"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/tcnksm/go-latest"
)
var cfgFile string = ""
func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().
StringVarP(&cfgFile, "config", "c", "", "config file (default is /etc/headscale/config.yaml)")
rootCmd.PersistentFlags().
StringP("output", "o", "", "Output format. Empty for human-readable, 'json', 'json-line' or 'yaml'")
rootCmd.PersistentFlags().
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{
Use: "headscale",
Short: "headscale - a Tailscale control server",

View File

@@ -24,6 +24,8 @@ func init() {
enableRouteCmd.Flags().
StringSliceP("route", "r", []string{}, "List (or repeated flags) of routes to enable")
enableRouteCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
enableRouteCmd.Flags().BoolP("all", "a", false, "All routes from host")
err = enableRouteCmd.MarkFlagRequired("identifier")
if err != nil {
log.Fatalf(err.Error())
@@ -35,13 +37,15 @@ func init() {
}
var routesCmd = &cobra.Command{
Use: "routes",
Short: "Manage the routes of Headscale",
Use: "routes",
Short: "Manage the routes of Headscale",
Aliases: []string{"r", "route"},
}
var listRoutesCmd = &cobra.Command{
Use: "list",
Short: "List routes advertised and enabled by a given node",
Use: "list",
Short: "List routes advertised and enabled by a given node",
Aliases: []string{"ls", "show"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
@@ -123,21 +127,43 @@ omit the route you do not want to enable.
return
}
routes, err := cmd.Flags().GetStringSlice("route")
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error getting routes from flag: %s", err),
output,
)
return
}
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
var routes []string
isAll, _ := cmd.Flags().GetBool("all")
if isAll {
response, err := client.GetMachineRoute(ctx, &v1.GetMachineRouteRequest{
MachineId: machineID,
})
if err != nil {
ErrorOutput(
err,
fmt.Sprintf(
"Cannot get machine routes: %s\n",
status.Convert(err).Message(),
),
output,
)
return
}
routes = response.GetRoutes().GetAdvertisedRoutes()
} else {
routes, err = cmd.Flags().GetStringSlice("route")
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error getting routes from flag: %s", err),
output,
)
return
}
}
request := &v1.EnableMachineRoutesRequest{
MachineId: machineID,
Routes: routes,

View File

@@ -1,8 +1,7 @@
package cli
import (
"log"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
@@ -17,14 +16,14 @@ var serveCmd = &cobra.Command{
return nil
},
Run: func(cmd *cobra.Command, args []string) {
h, err := getHeadscaleApp()
app, err := getHeadscaleApp()
if err != nil {
log.Fatalf("Error initializing: %s", err)
log.Fatal().Caller().Err(err).Msg("Error initializing")
}
err = h.Serve()
err = app.Serve()
if err != nil {
log.Fatalf("Error initializing: %s", err)
log.Fatal().Caller().Err(err).Msg("Error starting server")
}
},
}

View File

@@ -4,380 +4,29 @@ import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io/fs"
"net/url"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"reflect"
"github.com/juanfont/headscale"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"gopkg.in/yaml.v2"
"inet.af/netaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/dnstype"
)
const (
PermissionFallback = 0o700
HeadscaleDateTimeFormat = "2006-01-02 15:04:05"
)
func LoadConfig(path string) error {
viper.SetConfigName("config")
if path == "" {
viper.AddConfigPath("/etc/headscale/")
viper.AddConfigPath("$HOME/.headscale")
viper.AddConfigPath(".")
} else {
// For testing
viper.AddConfigPath(path)
}
viper.SetEnvPrefix("headscale")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
viper.SetDefault("tls_letsencrypt_cache_dir", "/var/www/.cache")
viper.SetDefault("tls_letsencrypt_challenge_type", "HTTP-01")
viper.SetDefault("tls_client_auth_mode", "relaxed")
viper.SetDefault("log_level", "info")
viper.SetDefault("dns_config", nil)
viper.SetDefault("unix_socket", "/var/run/headscale.sock")
viper.SetDefault("unix_socket_permission", "0o770")
viper.SetDefault("grpc_listen_addr", ":50443")
viper.SetDefault("grpc_allow_insecure", false)
viper.SetDefault("cli.timeout", "5s")
viper.SetDefault("cli.insecure", false)
if err := viper.ReadInConfig(); err != nil {
return fmt.Errorf("fatal error reading config file: %w", err)
}
// Collect any validation errors and return them all at once
var errorText string
if (viper.GetString("tls_letsencrypt_hostname") != "") &&
((viper.GetString("tls_cert_path") != "") || (viper.GetString("tls_key_path") != "")) {
errorText += "Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both\n"
}
if (viper.GetString("tls_letsencrypt_hostname") != "") &&
(viper.GetString("tls_letsencrypt_challenge_type") == "TLS-ALPN-01") &&
(!strings.HasSuffix(viper.GetString("listen_addr"), ":443")) {
// this is only a warning because there could be something sitting in front of headscale that redirects the traffic (e.g. an iptables rule)
log.Warn().
Msg("Warning: when using tls_letsencrypt_hostname with TLS-ALPN-01 as challenge type, headscale must be reachable on port 443, i.e. listen_addr should probably end in :443")
}
if (viper.GetString("tls_letsencrypt_challenge_type") != "HTTP-01") &&
(viper.GetString("tls_letsencrypt_challenge_type") != "TLS-ALPN-01") {
errorText += "Fatal config error: the only supported values for tls_letsencrypt_challenge_type are HTTP-01 and TLS-ALPN-01\n"
}
if !strings.HasPrefix(viper.GetString("server_url"), "http://") &&
!strings.HasPrefix(viper.GetString("server_url"), "https://") {
errorText += "Fatal config error: server_url must start with https:// or http://\n"
}
_, authModeValid := headscale.LookupTLSClientAuthMode(
viper.GetString("tls_client_auth_mode"),
)
if !authModeValid {
errorText += fmt.Sprintf(
"Invalid tls_client_auth_mode supplied: %s. Accepted values: %s, %s, %s.",
viper.GetString("tls_client_auth_mode"),
headscale.DisabledClientAuth,
headscale.RelaxedClientAuth,
headscale.EnforcedClientAuth)
}
if errorText != "" {
//nolint
return errors.New(strings.TrimSuffix(errorText, "\n"))
} else {
return nil
}
}
func GetDERPConfig() headscale.DERPConfig {
urlStrs := viper.GetStringSlice("derp.urls")
urls := make([]url.URL, len(urlStrs))
for index, urlStr := range urlStrs {
urlAddr, err := url.Parse(urlStr)
if err != nil {
log.Error().
Str("url", urlStr).
Err(err).
Msg("Failed to parse url, ignoring...")
}
urls[index] = *urlAddr
}
paths := viper.GetStringSlice("derp.paths")
autoUpdate := viper.GetBool("derp.auto_update_enabled")
updateFrequency := viper.GetDuration("derp.update_frequency")
return headscale.DERPConfig{
URLs: urls,
Paths: paths,
AutoUpdate: autoUpdate,
UpdateFrequency: updateFrequency,
}
}
func GetDNSConfig() (*tailcfg.DNSConfig, string) {
if viper.IsSet("dns_config") {
dnsConfig := &tailcfg.DNSConfig{}
if viper.IsSet("dns_config.nameservers") {
nameserversStr := viper.GetStringSlice("dns_config.nameservers")
nameservers := make([]netaddr.IP, len(nameserversStr))
resolvers := make([]dnstype.Resolver, len(nameserversStr))
for index, nameserverStr := range nameserversStr {
nameserver, err := netaddr.ParseIP(nameserverStr)
if err != nil {
log.Error().
Str("func", "getDNSConfig").
Err(err).
Msgf("Could not parse nameserver IP: %s", nameserverStr)
}
nameservers[index] = nameserver
resolvers[index] = dnstype.Resolver{
Addr: nameserver.String(),
}
}
dnsConfig.Nameservers = nameservers
dnsConfig.Resolvers = resolvers
}
if viper.IsSet("dns_config.restricted_nameservers") {
if len(dnsConfig.Nameservers) > 0 {
dnsConfig.Routes = make(map[string][]dnstype.Resolver)
restrictedDNS := viper.GetStringMapStringSlice(
"dns_config.restricted_nameservers",
)
for domain, restrictedNameservers := range restrictedDNS {
restrictedResolvers := make(
[]dnstype.Resolver,
len(restrictedNameservers),
)
for index, nameserverStr := range restrictedNameservers {
nameserver, err := netaddr.ParseIP(nameserverStr)
if err != nil {
log.Error().
Str("func", "getDNSConfig").
Err(err).
Msgf("Could not parse restricted nameserver IP: %s", nameserverStr)
}
restrictedResolvers[index] = dnstype.Resolver{
Addr: nameserver.String(),
}
}
dnsConfig.Routes[domain] = restrictedResolvers
}
} else {
log.Warn().
Msg("Warning: dns_config.restricted_nameservers is set, but no nameservers are configured. Ignoring restricted_nameservers.")
}
}
if viper.IsSet("dns_config.domains") {
dnsConfig.Domains = viper.GetStringSlice("dns_config.domains")
}
if viper.IsSet("dns_config.magic_dns") {
magicDNS := viper.GetBool("dns_config.magic_dns")
if len(dnsConfig.Nameservers) > 0 {
dnsConfig.Proxied = magicDNS
} else if magicDNS {
log.Warn().
Msg("Warning: dns_config.magic_dns is set, but no nameservers are configured. Ignoring magic_dns.")
}
}
var baseDomain string
if viper.IsSet("dns_config.base_domain") {
baseDomain = viper.GetString("dns_config.base_domain")
} else {
baseDomain = "headscale.net" // does not really matter when MagicDNS is not enabled
}
return dnsConfig, baseDomain
}
return nil, ""
}
func absPath(path string) string {
// If a relative path is provided, prefix it with the the directory where
// the config file was found.
if (path != "") && !strings.HasPrefix(path, string(os.PathSeparator)) {
dir, _ := filepath.Split(viper.ConfigFileUsed())
if dir != "" {
path = filepath.Join(dir, path)
}
}
return path
}
func getHeadscaleConfig() headscale.Config {
dnsConfig, baseDomain := GetDNSConfig()
derpConfig := GetDERPConfig()
configuredPrefixes := viper.GetStringSlice("ip_prefixes")
parsedPrefixes := make([]netaddr.IPPrefix, 0, len(configuredPrefixes)+1)
legacyPrefixField := viper.GetString("ip_prefix")
if len(legacyPrefixField) > 0 {
log.
Warn().
Msgf(
"%s, %s",
"use of 'ip_prefix' for configuration is deprecated",
"please see 'ip_prefixes' in the shipped example.",
)
legacyPrefix, err := netaddr.ParseIPPrefix(legacyPrefixField)
if err != nil {
panic(fmt.Errorf("failed to parse ip_prefix: %w", err))
}
parsedPrefixes = append(parsedPrefixes, legacyPrefix)
}
for i, prefixInConfig := range configuredPrefixes {
prefix, err := netaddr.ParseIPPrefix(prefixInConfig)
if err != nil {
panic(fmt.Errorf("failed to parse ip_prefixes[%d]: %w", i, err))
}
parsedPrefixes = append(parsedPrefixes, prefix)
}
prefixes := make([]netaddr.IPPrefix, 0, len(parsedPrefixes))
{
// dedup
normalizedPrefixes := make(map[string]int, len(parsedPrefixes))
for i, p := range parsedPrefixes {
normalized, _ := p.Range().Prefix()
normalizedPrefixes[normalized.String()] = i
}
// convert back to list
for _, i := range normalizedPrefixes {
prefixes = append(prefixes, parsedPrefixes[i])
}
}
if len(prefixes) < 1 {
prefixes = append(prefixes, netaddr.MustParseIPPrefix("100.64.0.0/10"))
log.Warn().
Msgf("'ip_prefixes' not configured, falling back to default: %v", prefixes)
}
tlsClientAuthMode, _ := headscale.LookupTLSClientAuthMode(
viper.GetString("tls_client_auth_mode"),
)
return headscale.Config{
ServerURL: viper.GetString("server_url"),
Addr: viper.GetString("listen_addr"),
GRPCAddr: viper.GetString("grpc_listen_addr"),
GRPCAllowInsecure: viper.GetBool("grpc_allow_insecure"),
IPPrefixes: prefixes,
PrivateKeyPath: absPath(viper.GetString("private_key_path")),
BaseDomain: baseDomain,
DERP: derpConfig,
EphemeralNodeInactivityTimeout: viper.GetDuration(
"ephemeral_node_inactivity_timeout",
),
DBtype: viper.GetString("db_type"),
DBpath: absPath(viper.GetString("db_path")),
DBhost: viper.GetString("db_host"),
DBport: viper.GetInt("db_port"),
DBname: viper.GetString("db_name"),
DBuser: viper.GetString("db_user"),
DBpass: viper.GetString("db_pass"),
TLSLetsEncryptHostname: viper.GetString("tls_letsencrypt_hostname"),
TLSLetsEncryptListen: viper.GetString("tls_letsencrypt_listen"),
TLSLetsEncryptCacheDir: absPath(
viper.GetString("tls_letsencrypt_cache_dir"),
),
TLSLetsEncryptChallengeType: viper.GetString("tls_letsencrypt_challenge_type"),
TLSCertPath: absPath(viper.GetString("tls_cert_path")),
TLSKeyPath: absPath(viper.GetString("tls_key_path")),
TLSClientAuthMode: tlsClientAuthMode,
DNSConfig: dnsConfig,
ACMEEmail: viper.GetString("acme_email"),
ACMEURL: viper.GetString("acme_url"),
UnixSocket: viper.GetString("unix_socket"),
UnixSocketPermission: GetFileMode("unix_socket_permission"),
OIDC: headscale.OIDCConfig{
Issuer: viper.GetString("oidc.issuer"),
ClientID: viper.GetString("oidc.client_id"),
ClientSecret: viper.GetString("oidc.client_secret"),
},
CLI: headscale.CLIConfig{
Address: viper.GetString("cli.address"),
APIKey: viper.GetString("cli.api_key"),
Timeout: viper.GetDuration("cli.timeout"),
Insecure: viper.GetBool("cli.insecure"),
},
}
}
func getHeadscaleApp() (*headscale.Headscale, error) {
// 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
cfg, err := headscale.GetHeadscaleConfig()
if err != nil {
return nil, fmt.Errorf("failed to load configuration while creating headscale instance: %w", err)
}
cfg := getHeadscaleConfig()
cfg.OIDC.MatchMap = loadOIDCMatchMap()
app, err := headscale.NewHeadscale(cfg)
if err != nil {
return nil, err
@@ -385,11 +34,11 @@ func getHeadscaleApp() (*headscale.Headscale, error) {
// We are doing this here, as in the future could be cool to have it also hot-reload
if viper.GetString("acl_policy_path") != "" {
aclPath := absPath(viper.GetString("acl_policy_path"))
if cfg.ACL.PolicyPath != "" {
aclPath := headscale.AbsolutePathFromConfigPath(cfg.ACL.PolicyPath)
err = app.LoadACLPolicy(aclPath)
if err != nil {
log.Error().
log.Fatal().
Str("path", aclPath).
Err(err).
Msg("Could not load the ACL policy")
@@ -400,7 +49,14 @@ func getHeadscaleApp() (*headscale.Headscale, error) {
}
func getHeadscaleCLIClient() (context.Context, v1.HeadscaleServiceClient, *grpc.ClientConn, context.CancelFunc) {
cfg := getHeadscaleConfig()
cfg, err := headscale.GetHeadscaleConfig()
if err != nil {
log.Fatal().
Err(err).
Caller().
Msgf("Failed to load configuration")
os.Exit(-1) // we get here if logging is suppressed (i.e., json output)
}
log.Debug().
Dur("timeout", cfg.CLI.Timeout).
@@ -461,6 +117,7 @@ func getHeadscaleCLIClient() (context.Context, v1.HeadscaleServiceClient, *grpc.
conn, err := grpc.DialContext(ctx, address, grpcOptions...)
if err != nil {
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)
@@ -534,25 +191,12 @@ func (tokenAuth) RequireTransportSecurity() bool {
return true
}
// loadOIDCMatchMap is a wrapper around viper to verifies that the keys in
// the match map is valid regex strings.
func loadOIDCMatchMap() map[string]string {
strMap := viper.GetStringMapString("oidc.domain_map")
for oidcMatcher := range strMap {
_ = regexp.MustCompile(oidcMatcher)
func contains[T string](ts []T, t T) bool {
for _, v := range ts {
if reflect.DeepEqual(v, t) {
return true
}
}
return strMap
}
func GetFileMode(key string) fs.FileMode {
modeStr := viper.GetString(key)
mode, err := strconv.ParseUint(modeStr, headscale.Base8, headscale.BitSize64)
if err != nil {
return PermissionFallback
}
return fs.FileMode(mode)
return false
}

View File

@@ -1,17 +1,13 @@
package main
import (
"fmt"
"os"
"runtime"
"time"
"github.com/efekarakus/termcolor"
"github.com/juanfont/headscale/cmd/headscale/cli"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
"github.com/tcnksm/go-latest"
)
func main() {
@@ -43,44 +39,5 @@ func main() {
NoColor: !colors,
})
if err := cli.LoadConfig(""); err != nil {
log.Fatal().Caller().Err(err)
}
machineOutput := cli.HasMachineOutputFlag()
logLevel := viper.GetString("log_level")
level, err := zerolog.ParseLevel(logLevel)
if err != nil {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
} else {
zerolog.SetGlobalLevel(level)
}
// If the user has requested a "machine" readable format,
// then disable login so the output remains valid.
if machineOutput {
zerolog.SetGlobalLevel(zerolog.Disabled)
}
if !viper.GetBool("disable_check_updates") && !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()
}

View File

@@ -8,7 +8,7 @@ import (
"strings"
"testing"
"github.com/juanfont/headscale/cmd/headscale/cli"
"github.com/juanfont/headscale"
"github.com/spf13/viper"
"gopkg.in/check.v1"
)
@@ -27,6 +27,51 @@ func (s *Suite) SetUpSuite(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) {
tmpDir, err := ioutil.TempDir("", "headscale")
if err != nil {
@@ -49,12 +94,13 @@ func (*Suite) TestConfigLoading(c *check.C) {
}
// Load example config, it should load without validation errors
err = cli.LoadConfig(tmpDir)
err = headscale.LoadConfig(tmpDir, false)
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, "")
@@ -62,10 +108,12 @@ func (*Suite) TestConfigLoading(c *check.C) {
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(
cli.GetFileMode("unix_socket_permission"),
headscale.GetFileMode("unix_socket_permission"),
check.Equals,
fs.FileMode(0o770),
)
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) {
@@ -90,10 +138,10 @@ func (*Suite) TestDNSConfigLoading(c *check.C) {
}
// Load example config, it should load without validation errors
err = cli.LoadConfig(tmpDir)
err = headscale.LoadConfig(tmpDir, false)
c.Assert(err, check.IsNil)
dnsConfig, baseDomain := cli.GetDNSConfig()
dnsConfig, baseDomain := headscale.GetDNSConfig()
c.Assert(dnsConfig.Nameservers[0].String(), check.Equals, "1.1.1.1")
c.Assert(dnsConfig.Resolvers[0].Addr, check.Equals, "1.1.1.1")
@@ -123,7 +171,7 @@ func (*Suite) TestTLSConfigValidation(c *check.C) {
writeConfig(c, tmpDir, configYaml)
// Check configuration validation errors (1)
err = cli.LoadConfig(tmpDir)
err = headscale.LoadConfig(tmpDir, false)
c.Assert(err, check.NotNil)
// check.Matches can not handle multiline strings
tmp := strings.ReplaceAll(err.Error(), "\n", "***")
@@ -148,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\"",
)
writeConfig(c, tmpDir, configYaml)
err = cli.LoadConfig(tmpDir)
err = headscale.LoadConfig(tmpDir, false)
c.Assert(err, check.IsNil)
}

View File

@@ -16,6 +16,12 @@ server_url: http://127.0.0.1:8080
#
listen_addr: 0.0.0.0:8080
# Address to listen to /metrics, you may want
# to keep this endpoint private to your internal
# network
#
metrics_listen_addr: 127.0.0.1:9090
# Address to listen for gRPC.
# gRPC is used for controlling a headscale server
# remotely with the CLI
@@ -49,6 +55,26 @@ ip_prefixes:
# headscale needs a list of DERP servers that can be presented
# to the clients.
derp:
server:
# If enabled, runs the embedded DERP server and merges it into the rest of the DERP config
# The Headscale server_url defined above MUST be using https, DERP requires TLS to be in place
enabled: false
# Region ID to use for the embedded DERP server.
# The local DERP prevails if the region ID collides with other region ID coming from
# the regular DERP config.
region_id: 999
# Region code and name are displayed in the Tailscale UI to identify a DERP region
region_code: "headscale"
region_name: "Headscale Embedded DERP"
# Listens in UDP at the configured address for STUN connections to help on NAT traversal.
# When the embedded DERP server is enabled stun_listen_addr MUST be defined.
#
# For more details on how this works, check this great article: https://tailscale.com/blog/how-tailscale-works/
stun_listen_addr: "0.0.0.0:3478"
# List of externally available DERP maps encoded in JSON
urls:
- https://controlplane.tailscale.com/derpmap/default
@@ -77,6 +103,12 @@ disable_check_updates: false
# Time before an inactive ephemeral node is deleted?
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
db_type: sqlite3
db_path: /var/lib/headscale/db.sqlite
@@ -117,7 +149,7 @@ tls_client_auth_mode: relaxed
tls_letsencrypt_cache_dir: /var/lib/headscale/cache
# Type of ACME challenge to use, currently supported types:
# HTTP-01 or TLS_ALPN-01
# HTTP-01 or TLS-ALPN-01
# See [docs/tls.md](docs/tls.md) for more information
tls_letsencrypt_challenge_type: HTTP-01
# When HTTP-01 challenge is chosen, letsencrypt must set up a
@@ -132,7 +164,8 @@ tls_key_path: ""
log_level: info
# Path to a file containg ACL policies.
# Recommended path: /etc/headscale/acl.hujson
# ACLs can be defined as YAML or HUJSON.
# https://tailscale.com/kb/1018/acls/
acl_policy_path: ""
## DNS
@@ -187,7 +220,38 @@ unix_socket_permission: "0770"
# client_id: "your-oidc-client-id"
# client_secret: "your-oidc-client-secret"
#
# # Domain map is used to map incomming users (by their email) to
# # a namespace. The key can be a string, or regex.
# domain_map:
# ".*": default-namespace
# Customize the scopes used in the OIDC flow, defaults to "openid", "profile" and "email" and add custom query
# parameters to the Authorize Endpoint request. Scopes default to "openid", "profile" and "email".
#
# scope: ["openid", "profile", "email", "custom"]
# extra_params:
# domain_hint: example.com
#
# List allowed principal domains and/or users. If an authenticated user's domain is not in this list, the
# authentication request will be rejected.
#
# allowed_domains:
# - example.com
# allowed_users:
# - alice@example.com
#
# If `strip_email_domain` is set to `true`, the domain part of the username email address will be removed.
# This will transform `first-name.last-name@example.com` to the namespace `first-name.last-name`
# If `strip_email_domain` is set to `false` the domain part will NOT be removed resulting to the following
# namespace: `first-name.last-name.example.com`
#
# strip_email_domain: true
# Logtail configuration
# Logtail is Tailscales logging and auditing infrastructure, it allows the control panel
# to instruct tailscale nodes to log their activity to a remote server.
logtail:
# Enable logtail for this headscales clients.
# 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.
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

543
config.go Normal file
View File

@@ -0,0 +1,543 @@
package headscale
import (
"crypto/tls"
"errors"
"fmt"
"io/fs"
"net/url"
"strings"
"time"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
"inet.af/netaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/dnstype"
)
const (
tlsALPN01ChallengeType = "TLS-ALPN-01"
http01ChallengeType = "HTTP-01"
)
// Config contains the initial Headscale configuration.
type Config struct {
ServerURL string
Addr string
MetricsAddr string
GRPCAddr string
GRPCAllowInsecure bool
EphemeralNodeInactivityTimeout time.Duration
NodeUpdateCheckInterval time.Duration
IPPrefixes []netaddr.IPPrefix
PrivateKeyPath string
BaseDomain string
LogLevel zerolog.Level
DisableUpdateCheck bool
DERP DERPConfig
DBtype string
DBpath string
DBhost string
DBport int
DBname string
DBuser string
DBpass string
TLS TLSConfig
ACMEURL string
ACMEEmail string
DNSConfig *tailcfg.DNSConfig
UnixSocket string
UnixSocketPermission fs.FileMode
OIDC OIDCConfig
LogTail LogTailConfig
RandomizeClientPort bool
CLI CLIConfig
ACL ACLConfig
}
type TLSConfig struct {
CertPath string
KeyPath string
ClientAuthMode tls.ClientAuthType
LetsEncrypt LetsEncryptConfig
}
type LetsEncryptConfig struct {
Listen string
Hostname string
CacheDir string
ChallengeType string
}
type OIDCConfig struct {
Issuer string
ClientID string
ClientSecret string
Scope []string
ExtraParams map[string]string
AllowedDomains []string
AllowedUsers []string
StripEmaildomain bool
}
type DERPConfig struct {
ServerEnabled bool
ServerRegionID int
ServerRegionCode string
ServerRegionName string
STUNAddr string
URLs []url.URL
Paths []string
AutoUpdate bool
UpdateFrequency time.Duration
}
type LogTailConfig struct {
Enabled bool
}
type CLIConfig struct {
Address string
APIKey string
Timeout time.Duration
Insecure bool
}
type ACLConfig struct {
PolicyPath string
}
func LoadConfig(path string, isFile bool) error {
if isFile {
viper.SetConfigFile(path)
} else {
viper.SetConfigName("config")
if path == "" {
viper.AddConfigPath("/etc/headscale/")
viper.AddConfigPath("$HOME/.headscale")
viper.AddConfigPath(".")
} else {
// For testing
viper.AddConfigPath(path)
}
}
viper.SetEnvPrefix("headscale")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
viper.SetDefault("tls_letsencrypt_cache_dir", "/var/www/.cache")
viper.SetDefault("tls_letsencrypt_challenge_type", http01ChallengeType)
viper.SetDefault("tls_client_auth_mode", "relaxed")
viper.SetDefault("log_level", "info")
viper.SetDefault("dns_config", nil)
viper.SetDefault("derp.server.enabled", false)
viper.SetDefault("derp.server.stun.enabled", true)
viper.SetDefault("unix_socket", "/var/run/headscale.sock")
viper.SetDefault("unix_socket_permission", "0o770")
viper.SetDefault("grpc_listen_addr", ":50443")
viper.SetDefault("grpc_allow_insecure", false)
viper.SetDefault("cli.timeout", "5s")
viper.SetDefault("cli.insecure", false)
viper.SetDefault("oidc.scope", []string{oidc.ScopeOpenID, "profile", "email"})
viper.SetDefault("oidc.strip_email_domain", true)
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 {
log.Warn().Err(err).Msg("Failed to read configuration from disk")
return fmt.Errorf("fatal error reading config file: %w", err)
}
// Collect any validation errors and return them all at once
var errorText string
if (viper.GetString("tls_letsencrypt_hostname") != "") &&
((viper.GetString("tls_cert_path") != "") || (viper.GetString("tls_key_path") != "")) {
errorText += "Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both\n"
}
if (viper.GetString("tls_letsencrypt_hostname") != "") &&
(viper.GetString("tls_letsencrypt_challenge_type") == tlsALPN01ChallengeType) &&
(!strings.HasSuffix(viper.GetString("listen_addr"), ":443")) {
// this is only a warning because there could be something sitting in front of headscale that redirects the traffic (e.g. an iptables rule)
log.Warn().
Msg("Warning: when using tls_letsencrypt_hostname with TLS-ALPN-01 as challenge type, headscale must be reachable on port 443, i.e. listen_addr should probably end in :443")
}
if (viper.GetString("tls_letsencrypt_challenge_type") != http01ChallengeType) &&
(viper.GetString("tls_letsencrypt_challenge_type") != tlsALPN01ChallengeType) {
errorText += "Fatal config error: the only supported values for tls_letsencrypt_challenge_type are HTTP-01 and TLS-ALPN-01\n"
}
if !strings.HasPrefix(viper.GetString("server_url"), "http://") &&
!strings.HasPrefix(viper.GetString("server_url"), "https://") {
errorText += "Fatal config error: server_url must start with https:// or http://\n"
}
_, authModeValid := LookupTLSClientAuthMode(
viper.GetString("tls_client_auth_mode"),
)
if !authModeValid {
errorText += fmt.Sprintf(
"Invalid tls_client_auth_mode supplied: %s. Accepted values: %s, %s, %s.",
viper.GetString("tls_client_auth_mode"),
DisabledClientAuth,
RelaxedClientAuth,
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 != "" {
//nolint
return errors.New(strings.TrimSuffix(errorText, "\n"))
} else {
return nil
}
}
func GetTLSConfig() TLSConfig {
tlsClientAuthMode, _ := LookupTLSClientAuthMode(
viper.GetString("tls_client_auth_mode"),
)
return TLSConfig{
LetsEncrypt: LetsEncryptConfig{
Hostname: viper.GetString("tls_letsencrypt_hostname"),
Listen: viper.GetString("tls_letsencrypt_listen"),
CacheDir: AbsolutePathFromConfigPath(
viper.GetString("tls_letsencrypt_cache_dir"),
),
ChallengeType: viper.GetString("tls_letsencrypt_challenge_type"),
},
CertPath: AbsolutePathFromConfigPath(
viper.GetString("tls_cert_path"),
),
KeyPath: AbsolutePathFromConfigPath(
viper.GetString("tls_key_path"),
),
ClientAuthMode: tlsClientAuthMode,
}
}
func GetDERPConfig() DERPConfig {
serverEnabled := viper.GetBool("derp.server.enabled")
serverRegionID := viper.GetInt("derp.server.region_id")
serverRegionCode := viper.GetString("derp.server.region_code")
serverRegionName := viper.GetString("derp.server.region_name")
stunAddr := viper.GetString("derp.server.stun_listen_addr")
if serverEnabled && stunAddr == "" {
log.Fatal().
Msg("derp.server.stun_listen_addr must be set if derp.server.enabled is true")
}
urlStrs := viper.GetStringSlice("derp.urls")
urls := make([]url.URL, len(urlStrs))
for index, urlStr := range urlStrs {
urlAddr, err := url.Parse(urlStr)
if err != nil {
log.Error().
Str("url", urlStr).
Err(err).
Msg("Failed to parse url, ignoring...")
}
urls[index] = *urlAddr
}
paths := viper.GetStringSlice("derp.paths")
autoUpdate := viper.GetBool("derp.auto_update_enabled")
updateFrequency := viper.GetDuration("derp.update_frequency")
return DERPConfig{
ServerEnabled: serverEnabled,
ServerRegionID: serverRegionID,
ServerRegionCode: serverRegionCode,
ServerRegionName: serverRegionName,
STUNAddr: stunAddr,
URLs: urls,
Paths: paths,
AutoUpdate: autoUpdate,
UpdateFrequency: updateFrequency,
}
}
func GetLogTailConfig() LogTailConfig {
enabled := viper.GetBool("logtail.enabled")
return LogTailConfig{
Enabled: enabled,
}
}
func GetACLConfig() ACLConfig {
policyPath := viper.GetString("acl_policy_path")
return ACLConfig{
PolicyPath: policyPath,
}
}
func GetDNSConfig() (*tailcfg.DNSConfig, string) {
if viper.IsSet("dns_config") {
dnsConfig := &tailcfg.DNSConfig{}
if viper.IsSet("dns_config.nameservers") {
nameserversStr := viper.GetStringSlice("dns_config.nameservers")
nameservers := make([]netaddr.IP, len(nameserversStr))
resolvers := make([]*dnstype.Resolver, len(nameserversStr))
for index, nameserverStr := range nameserversStr {
nameserver, err := netaddr.ParseIP(nameserverStr)
if err != nil {
log.Error().
Str("func", "getDNSConfig").
Err(err).
Msgf("Could not parse nameserver IP: %s", nameserverStr)
}
nameservers[index] = nameserver
resolvers[index] = &dnstype.Resolver{
Addr: nameserver.String(),
}
}
dnsConfig.Nameservers = nameservers
dnsConfig.Resolvers = resolvers
}
if viper.IsSet("dns_config.restricted_nameservers") {
if len(dnsConfig.Nameservers) > 0 {
dnsConfig.Routes = make(map[string][]*dnstype.Resolver)
restrictedDNS := viper.GetStringMapStringSlice(
"dns_config.restricted_nameservers",
)
for domain, restrictedNameservers := range restrictedDNS {
restrictedResolvers := make(
[]*dnstype.Resolver,
len(restrictedNameservers),
)
for index, nameserverStr := range restrictedNameservers {
nameserver, err := netaddr.ParseIP(nameserverStr)
if err != nil {
log.Error().
Str("func", "getDNSConfig").
Err(err).
Msgf("Could not parse restricted nameserver IP: %s", nameserverStr)
}
restrictedResolvers[index] = &dnstype.Resolver{
Addr: nameserver.String(),
}
}
dnsConfig.Routes[domain] = restrictedResolvers
}
} else {
log.Warn().
Msg("Warning: dns_config.restricted_nameservers is set, but no nameservers are configured. Ignoring restricted_nameservers.")
}
}
if viper.IsSet("dns_config.domains") {
dnsConfig.Domains = viper.GetStringSlice("dns_config.domains")
}
if viper.IsSet("dns_config.magic_dns") {
magicDNS := viper.GetBool("dns_config.magic_dns")
if len(dnsConfig.Nameservers) > 0 {
dnsConfig.Proxied = magicDNS
} else if magicDNS {
log.Warn().
Msg("Warning: dns_config.magic_dns is set, but no nameservers are configured. Ignoring magic_dns.")
}
}
var baseDomain string
if viper.IsSet("dns_config.base_domain") {
baseDomain = viper.GetString("dns_config.base_domain")
} else {
baseDomain = "headscale.net" // does not really matter when MagicDNS is not enabled
}
return dnsConfig, baseDomain
}
return nil, ""
}
func GetHeadscaleConfig() (*Config, error) {
dnsConfig, baseDomain := GetDNSConfig()
derpConfig := GetDERPConfig()
logConfig := GetLogTailConfig()
randomizeClientPort := viper.GetBool("randomize_client_port")
configuredPrefixes := viper.GetStringSlice("ip_prefixes")
parsedPrefixes := make([]netaddr.IPPrefix, 0, len(configuredPrefixes)+1)
logLevelStr := viper.GetString("log_level")
logLevel, err := zerolog.ParseLevel(logLevelStr)
if err != nil {
logLevel = zerolog.DebugLevel
}
legacyPrefixField := viper.GetString("ip_prefix")
if len(legacyPrefixField) > 0 {
log.
Warn().
Msgf(
"%s, %s",
"use of 'ip_prefix' for configuration is deprecated",
"please see 'ip_prefixes' in the shipped example.",
)
legacyPrefix, err := netaddr.ParseIPPrefix(legacyPrefixField)
if err != nil {
panic(fmt.Errorf("failed to parse ip_prefix: %w", err))
}
parsedPrefixes = append(parsedPrefixes, legacyPrefix)
}
for i, prefixInConfig := range configuredPrefixes {
prefix, err := netaddr.ParseIPPrefix(prefixInConfig)
if err != nil {
panic(fmt.Errorf("failed to parse ip_prefixes[%d]: %w", i, err))
}
parsedPrefixes = append(parsedPrefixes, prefix)
}
prefixes := make([]netaddr.IPPrefix, 0, len(parsedPrefixes))
{
// dedup
normalizedPrefixes := make(map[string]int, len(parsedPrefixes))
for i, p := range parsedPrefixes {
normalized, _ := p.Range().Prefix()
normalizedPrefixes[normalized.String()] = i
}
// convert back to list
for _, i := range normalizedPrefixes {
prefixes = append(prefixes, parsedPrefixes[i])
}
}
if len(prefixes) < 1 {
prefixes = append(prefixes, netaddr.MustParseIPPrefix("100.64.0.0/10"))
log.Warn().
Msgf("'ip_prefixes' not configured, falling back to default: %v", prefixes)
}
return &Config{
ServerURL: viper.GetString("server_url"),
Addr: viper.GetString("listen_addr"),
MetricsAddr: viper.GetString("metrics_listen_addr"),
GRPCAddr: viper.GetString("grpc_listen_addr"),
GRPCAllowInsecure: viper.GetBool("grpc_allow_insecure"),
DisableUpdateCheck: viper.GetBool("disable_check_updates"),
LogLevel: logLevel,
IPPrefixes: prefixes,
PrivateKeyPath: AbsolutePathFromConfigPath(
viper.GetString("private_key_path"),
),
BaseDomain: baseDomain,
DERP: derpConfig,
EphemeralNodeInactivityTimeout: viper.GetDuration(
"ephemeral_node_inactivity_timeout",
),
NodeUpdateCheckInterval: viper.GetDuration(
"node_update_check_interval",
),
DBtype: viper.GetString("db_type"),
DBpath: AbsolutePathFromConfigPath(viper.GetString("db_path")),
DBhost: viper.GetString("db_host"),
DBport: viper.GetInt("db_port"),
DBname: viper.GetString("db_name"),
DBuser: viper.GetString("db_user"),
DBpass: viper.GetString("db_pass"),
TLS: GetTLSConfig(),
DNSConfig: dnsConfig,
ACMEEmail: viper.GetString("acme_email"),
ACMEURL: viper.GetString("acme_url"),
UnixSocket: viper.GetString("unix_socket"),
UnixSocketPermission: GetFileMode("unix_socket_permission"),
OIDC: OIDCConfig{
Issuer: viper.GetString("oidc.issuer"),
ClientID: viper.GetString("oidc.client_id"),
ClientSecret: viper.GetString("oidc.client_secret"),
Scope: viper.GetStringSlice("oidc.scope"),
ExtraParams: viper.GetStringMapString("oidc.extra_params"),
AllowedDomains: viper.GetStringSlice("oidc.allowed_domains"),
AllowedUsers: viper.GetStringSlice("oidc.allowed_users"),
StripEmaildomain: viper.GetBool("oidc.strip_email_domain"),
},
LogTail: logConfig,
RandomizeClientPort: randomizeClientPort,
CLI: CLIConfig{
Address: viper.GetString("cli.address"),
APIKey: viper.GetString("cli.api_key"),
Timeout: viper.GetDuration("cli.timeout"),
Insecure: viper.GetBool("cli.insecure"),
},
ACL: GetACLConfig(),
}, nil
}

168
db.go
View File

@@ -1,13 +1,20 @@
package headscale
import (
"context"
"database/sql/driver"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/glebarez/sqlite"
"github.com/rs/zerolog/log"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"inet.af/netaddr"
"tailscale.com/tailcfg"
)
const (
@@ -33,12 +40,82 @@ func (h *Headscale) initDB() error {
}
_ = db.Migrator().RenameColumn(&Machine{}, "ip_address", "ip_addresses")
_ = db.Migrator().RenameColumn(&Machine{}, "name", "hostname")
// GivenName is used as the primary source of DNS names, make sure
// the field is populated and normalized if it was not when the
// machine was registered.
_ = db.Migrator().RenameColumn(&Machine{}, "nickname", "given_name")
// If the Machine table has a column for registered,
// find all occourences of "false" and drop them. Then
// remove the column.
if db.Migrator().HasColumn(&Machine{}, "registered") {
log.Info().
Msg(`Database has legacy "registered" column in machine, removing...`)
machines := Machines{}
if err := h.db.Not("registered").Find(&machines).Error; err != nil {
log.Error().Err(err).Msg("Error accessing db")
}
for _, machine := range machines {
log.Info().
Str("machine", machine.Hostname).
Str("machine_key", machine.MachineKey).
Msg("Deleting unregistered machine")
if err := h.db.Delete(&Machine{}, machine.ID).Error; err != nil {
log.Error().
Err(err).
Str("machine", machine.Hostname).
Str("machine_key", machine.MachineKey).
Msg("Error deleting unregistered machine")
}
}
err := db.Migrator().DropColumn(&Machine{}, "registered")
if err != nil {
log.Error().Err(err).Msg("Error dropping registered column")
}
}
err = db.AutoMigrate(&Machine{})
if err != nil {
return err
}
if db.Migrator().HasColumn(&Machine{}, "given_name") {
machines := Machines{}
if err := h.db.Find(&machines).Error; err != nil {
log.Error().Err(err).Msg("Error accessing db")
}
for item, machine := range machines {
if machine.GivenName == "" {
normalizedHostname, err := NormalizeToFQDNRules(
machine.Hostname,
h.cfg.OIDC.StripEmaildomain,
)
if err != nil {
log.Error().
Caller().
Str("hostname", machine.Hostname).
Err(err).
Msg("Failed to normalize machine hostname in DB migration")
}
err = h.RenameMachine(&machines[item], normalizedHostname)
if err != nil {
log.Error().
Caller().
Str("hostname", machine.Hostname).
Err(err).
Msg("Failed to save normalized machine name in DB migration")
}
}
}
}
err = db.AutoMigrate(&KV{})
if err != nil {
return err
@@ -54,10 +131,7 @@ func (h *Headscale) initDB() error {
return err
}
err = db.AutoMigrate(&SharedMachine{})
if err != nil {
return err
}
_ = db.Migrator().DropTable("shared_machines")
err = db.AutoMigrate(&APIKey{})
if err != nil {
@@ -140,7 +214,91 @@ func (h *Headscale) setValue(key string, value string) error {
return nil
}
h.db.Create(keyValue)
if err := h.db.Create(keyValue).Error; err != nil {
return fmt.Errorf("failed to create key value pair in the database: %w", err)
}
return nil
}
func (h *Headscale) pingDB() error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
db, err := h.db.DB()
if err != nil {
return err
}
return db.PingContext(ctx)
}
// This is a "wrapper" type around tailscales
// Hostinfo to allow us to add database "serialization"
// methods. This allows us to use a typed values throughout
// the code and not have to marshal/unmarshal and error
// check all over the code.
type HostInfo tailcfg.Hostinfo
func (hi *HostInfo) Scan(destination interface{}) error {
switch value := destination.(type) {
case []byte:
return json.Unmarshal(value, hi)
case string:
return json.Unmarshal([]byte(value), hi)
default:
return fmt.Errorf("%w: unexpected data type %T", errMachineAddressesInvalid, destination)
}
}
// Value return json value, implement driver.Valuer interface.
func (hi HostInfo) Value() (driver.Value, error) {
bytes, err := json.Marshal(hi)
return string(bytes), err
}
type IPPrefixes []netaddr.IPPrefix
func (i *IPPrefixes) Scan(destination interface{}) error {
switch value := destination.(type) {
case []byte:
return json.Unmarshal(value, i)
case string:
return json.Unmarshal([]byte(value), i)
default:
return fmt.Errorf("%w: unexpected data type %T", errMachineAddressesInvalid, destination)
}
}
// Value return json value, implement driver.Valuer interface.
func (i IPPrefixes) Value() (driver.Value, error) {
bytes, err := json.Marshal(i)
return string(bytes), err
}
type StringList []string
func (i *StringList) Scan(destination interface{}) error {
switch value := destination.(type) {
case []byte:
return json.Unmarshal(value, i)
case string:
return json.Unmarshal([]byte(value), i)
default:
return fmt.Errorf("%w: unexpected data type %T", errMachineAddressesInvalid, destination)
}
}
// Value return json value, implement driver.Valuer interface.
func (i StringList) Value() (driver.Value, error) {
bytes, err := json.Marshal(i)
return string(bytes), err
}

View File

@@ -12,4 +12,4 @@ regions:
ipv6: "2604:a880:400:d1::828:b001"
stunport: 0
stunonly: false
derptestport: 0
derpport: 0

12
derp.go
View File

@@ -148,17 +148,11 @@ func (h *Headscale) scheduledDERPMapUpdateWorker(cancelChan <-chan struct{}) {
case <-ticker.C:
log.Info().Msg("Fetching DERPMap updates")
h.DERPMap = GetDERPMap(h.cfg.DERP)
namespaces, err := h.ListNamespaces()
if err != nil {
log.Error().
Err(err).
Msg("Failed to fetch namespaces")
if h.cfg.DERP.ServerEnabled {
h.DERPMap.Regions[h.DERPServer.region.RegionID] = &h.DERPServer.region
}
for _, namespace := range namespaces {
h.setLastStateChangeToNow(namespace.Name)
}
h.setLastStateChangeToNow()
}
}
}

287
derp_server.go Normal file
View File

@@ -0,0 +1,287 @@
package headscale
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/rs/zerolog/log"
"tailscale.com/derp"
"tailscale.com/net/stun"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
// fastStartHeader is the header (with value "1") that signals to the HTTP
// server that the DERP HTTP client does not want the HTTP 101 response
// headers and it will begin writing & reading the DERP protocol immediately
// following its HTTP request.
const fastStartHeader = "Derp-Fast-Start"
type DERPServer struct {
tailscaleDERP *derp.Server
region tailcfg.DERPRegion
}
func (h *Headscale) NewDERPServer() (*DERPServer, error) {
log.Trace().Caller().Msg("Creating new embedded DERP server")
server := derp.NewServer(key.NodePrivate(*h.privateKey), log.Info().Msgf)
region, err := h.generateRegionLocalDERP()
if err != nil {
return nil, err
}
return &DERPServer{server, region}, nil
}
func (h *Headscale) generateRegionLocalDERP() (tailcfg.DERPRegion, error) {
serverURL, err := url.Parse(h.cfg.ServerURL)
if err != nil {
return tailcfg.DERPRegion{}, err
}
var host string
var port int
host, portStr, err := net.SplitHostPort(serverURL.Host)
if err != nil {
if serverURL.Scheme == "https" {
host = serverURL.Host
port = 443
} else {
host = serverURL.Host
port = 80
}
} else {
port, err = strconv.Atoi(portStr)
if err != nil {
return tailcfg.DERPRegion{}, err
}
}
localDERPregion := tailcfg.DERPRegion{
RegionID: h.cfg.DERP.ServerRegionID,
RegionCode: h.cfg.DERP.ServerRegionCode,
RegionName: h.cfg.DERP.ServerRegionName,
Avoid: false,
Nodes: []*tailcfg.DERPNode{
{
Name: fmt.Sprintf("%d", h.cfg.DERP.ServerRegionID),
RegionID: h.cfg.DERP.ServerRegionID,
HostName: host,
DERPPort: port,
},
},
}
_, portSTUNStr, err := net.SplitHostPort(h.cfg.DERP.STUNAddr)
if err != nil {
return tailcfg.DERPRegion{}, err
}
portSTUN, err := strconv.Atoi(portSTUNStr)
if err != nil {
return tailcfg.DERPRegion{}, err
}
localDERPregion.Nodes[0].STUNPort = portSTUN
log.Info().Caller().Msgf("DERP region: %+v", localDERPregion)
return localDERPregion, nil
}
func (h *Headscale) DERPHandler(
writer http.ResponseWriter,
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 != "" {
log.Warn().Caller().Msgf("Weird websockets connection upgrade: %q", up)
}
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
}
fastStart := req.Header.Get(fastStartHeader) == "1"
hijacker, ok := writer.(http.Hijacker)
if !ok {
log.Error().Caller().Msg("DERP requires Hijacker interface from Gin")
writer.Header().Set("Content-Type", "text/plain")
writer.WriteHeader(http.StatusInternalServerError)
_, 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
}
netConn, conn, err := hijacker.Hijack()
if err != nil {
log.Error().Caller().Err(err).Msgf("Hijack failed")
writer.Header().Set("Content-Type", "text/plain")
writer.WriteHeader(http.StatusInternalServerError)
_, 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
}
log.Trace().Caller().Msgf("Hijacked connection from %v", req.RemoteAddr)
if !fastStart {
pubKey := h.privateKey.Public()
pubKeyStr := pubKey.UntypedHexString() // nolint
fmt.Fprintf(conn, "HTTP/1.1 101 Switching Protocols\r\n"+
"Upgrade: DERP\r\n"+
"Connection: Upgrade\r\n"+
"Derp-Version: %v\r\n"+
"Derp-Public-Key: %s\r\n\r\n",
derp.ProtocolVersion,
pubKeyStr)
}
h.DERPServer.tailscaleDERP.Accept(netConn, conn, netConn.RemoteAddr().String())
}
// DERPProbeHandler is the endpoint that js/wasm clients hit to measure
// DERP latency, since they can't do UDP STUN queries.
func (h *Headscale) DERPProbeHandler(
writer http.ResponseWriter,
req *http.Request,
) {
switch req.Method {
case "HEAD", "GET":
writer.Header().Set("Access-Control-Allow-Origin", "*")
writer.WriteHeader(http.StatusOK)
default:
writer.WriteHeader(http.StatusMethodNotAllowed)
_, err := writer.Write([]byte("bogus probe method"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
}
}
// DERPBootstrapDNSHandler implements the /bootsrap-dns endpoint
// Described in https://github.com/tailscale/tailscale/issues/1405,
// this endpoint provides a way to help a client when it fails to start up
// because its DNS are broken.
// The initial implementation is here https://github.com/tailscale/tailscale/pull/1406
// They have a cache, but not clear if that is really necessary at Headscale, uh, scale.
// An example implementation is found here https://derp.tailscale.com/bootstrap-dns
func (h *Headscale) DERPBootstrapDNSHandler(
writer http.ResponseWriter,
req *http.Request,
) {
dnsEntries := make(map[string][]net.IP)
resolvCtx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
var resolver net.Resolver
for _, region := range h.DERPMap.Regions {
for _, node := range region.Nodes { // we don't care if we override some nodes
addrs, err := resolver.LookupIP(resolvCtx, "ip", node.HostName)
if err != nil {
log.Trace().
Caller().
Err(err).
Msgf("bootstrap DNS lookup failed %q", node.HostName)
continue
}
dnsEntries[node.HostName] = addrs
}
}
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.
func (h *Headscale) ServeSTUN() {
packetConn, err := net.ListenPacket("udp", h.cfg.DERP.STUNAddr)
if err != nil {
log.Fatal().Msgf("failed to open STUN listener: %v", err)
}
log.Info().Msgf("STUN server started at %s", packetConn.LocalAddr())
udpConn, ok := packetConn.(*net.UDPConn)
if !ok {
log.Fatal().Msg("STUN listener is not a UDP listener")
}
serverSTUNListener(context.Background(), udpConn)
}
func serverSTUNListener(ctx context.Context, packetConn *net.UDPConn) {
var buf [64 << 10]byte
var (
bytesRead int
udpAddr *net.UDPAddr
err error
)
for {
bytesRead, udpAddr, err = packetConn.ReadFromUDP(buf[:])
if err != nil {
if ctx.Err() != nil {
return
}
log.Error().Caller().Err(err).Msgf("STUN ReadFrom")
time.Sleep(time.Second)
continue
}
log.Trace().Caller().Msgf("STUN request from %v", udpAddr)
pkt := buf[:bytesRead]
if !stun.Is(pkt) {
log.Trace().Caller().Msgf("UDP packet is not STUN")
continue
}
txid, err := stun.ParseBindingRequest(pkt)
if err != nil {
log.Trace().Caller().Err(err).Msgf("STUN parse error")
continue
}
res := stun.Response(txid, udpAddr.IP, uint16(udpAddr.Port))
_, err = packetConn.WriteTo(res, udpAddr)
if err != nil {
log.Trace().Caller().Err(err).Msgf("Issue writing to UDP")
continue
}
}
}

18
dns.go
View File

@@ -4,7 +4,7 @@ import (
"fmt"
"strings"
"github.com/fatih/set"
mapset "github.com/deckarep/golang-set/v2"
"inet.af/netaddr"
"tailscale.com/tailcfg"
"tailscale.com/util/dnsname"
@@ -165,27 +165,17 @@ func getMapResponseDNSConfig(
dnsConfig.Domains,
fmt.Sprintf(
"%s.%s",
strings.ReplaceAll(
machine.Namespace.Name,
"@",
".",
), // Replace @ with . for valid domain for machine
machine.Namespace.Name,
baseDomain,
),
)
namespaceSet := set.New(set.ThreadSafe)
namespaceSet := mapset.NewSet[Namespace]()
namespaceSet.Add(machine.Namespace)
for _, p := range peers {
namespaceSet.Add(p.Namespace)
}
for _, ns := range namespaceSet.List() {
namespace, ok := ns.(Namespace)
if !ok {
dnsConfig = dnsConfigOrig
continue
}
for _, namespace := range namespaceSet.ToSlice() {
dnsRoute := fmt.Sprintf("%v.%v", namespace.Name, baseDomain)
dnsConfig.Routes[dnsRoute] = nil
}

View File

@@ -161,17 +161,16 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
Name: "test_get_shared_nodes_1",
Hostname: "test_get_shared_nodes_1",
NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")},
AuthKeyID: uint(preAuthKeyInShared1.ID),
}
app.db.Save(machineInShared1)
_, err = app.GetMachine(namespaceShared1.Name, machineInShared1.Name)
_, err = app.GetMachine(namespaceShared1.Name, machineInShared1.Hostname)
c.Assert(err, check.IsNil)
machineInShared2 := &Machine{
@@ -179,17 +178,16 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Name: "test_get_shared_nodes_2",
Hostname: "test_get_shared_nodes_2",
NamespaceID: namespaceShared2.ID,
Namespace: *namespaceShared2,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.2")},
AuthKeyID: uint(preAuthKeyInShared2.ID),
}
app.db.Save(machineInShared2)
_, err = app.GetMachine(namespaceShared2.Name, machineInShared2.Name)
_, err = app.GetMachine(namespaceShared2.Name, machineInShared2.Hostname)
c.Assert(err, check.IsNil)
machineInShared3 := &Machine{
@@ -197,17 +195,16 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Name: "test_get_shared_nodes_3",
Hostname: "test_get_shared_nodes_3",
NamespaceID: namespaceShared3.ID,
Namespace: *namespaceShared3,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.3")},
AuthKeyID: uint(preAuthKeyInShared3.ID),
}
app.db.Save(machineInShared3)
_, err = app.GetMachine(namespaceShared3.Name, machineInShared3.Name)
_, err = app.GetMachine(namespaceShared3.Name, machineInShared3.Hostname)
c.Assert(err, check.IsNil)
machine2InShared1 := &Machine{
@@ -215,22 +212,18 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Name: "test_get_shared_nodes_4",
Hostname: "test_get_shared_nodes_4",
NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")},
AuthKeyID: uint(PreAuthKey2InShared1.ID),
}
app.db.Save(machine2InShared1)
err = app.AddSharedMachineToNamespace(machineInShared2, namespaceShared1)
c.Assert(err, check.IsNil)
baseDomain := "foobar.headscale.net"
dnsConfigOrig := tailcfg.DNSConfig{
Routes: make(map[string][]dnstype.Resolver),
Routes: make(map[string][]*dnstype.Resolver),
Domains: []string{baseDomain},
Proxied: true,
}
@@ -245,7 +238,8 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
peersOfMachineInShared1,
)
c.Assert(dnsConfig, check.NotNil)
c.Assert(len(dnsConfig.Routes), check.Equals, 2)
c.Assert(len(dnsConfig.Routes), check.Equals, 3)
domainRouteShared1 := fmt.Sprintf("%s.%s", namespaceShared1.Name, baseDomain)
_, ok := dnsConfig.Routes[domainRouteShared1]
@@ -257,7 +251,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
domainRouteShared3 := fmt.Sprintf("%s.%s", namespaceShared3.Name, baseDomain)
_, ok = dnsConfig.Routes[domainRouteShared3]
c.Assert(ok, check.Equals, false)
c.Assert(ok, check.Equals, true)
}
func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
@@ -310,17 +304,16 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
Name: "test_get_shared_nodes_1",
Hostname: "test_get_shared_nodes_1",
NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")},
AuthKeyID: uint(preAuthKeyInShared1.ID),
}
app.db.Save(machineInShared1)
_, err = app.GetMachine(namespaceShared1.Name, machineInShared1.Name)
_, err = app.GetMachine(namespaceShared1.Name, machineInShared1.Hostname)
c.Assert(err, check.IsNil)
machineInShared2 := &Machine{
@@ -328,17 +321,16 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Name: "test_get_shared_nodes_2",
Hostname: "test_get_shared_nodes_2",
NamespaceID: namespaceShared2.ID,
Namespace: *namespaceShared2,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.2")},
AuthKeyID: uint(preAuthKeyInShared2.ID),
}
app.db.Save(machineInShared2)
_, err = app.GetMachine(namespaceShared2.Name, machineInShared2.Name)
_, err = app.GetMachine(namespaceShared2.Name, machineInShared2.Hostname)
c.Assert(err, check.IsNil)
machineInShared3 := &Machine{
@@ -346,17 +338,16 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Name: "test_get_shared_nodes_3",
Hostname: "test_get_shared_nodes_3",
NamespaceID: namespaceShared3.ID,
Namespace: *namespaceShared3,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.3")},
AuthKeyID: uint(preAuthKeyInShared3.ID),
}
app.db.Save(machineInShared3)
_, err = app.GetMachine(namespaceShared3.Name, machineInShared3.Name)
_, err = app.GetMachine(namespaceShared3.Name, machineInShared3.Hostname)
c.Assert(err, check.IsNil)
machine2InShared1 := &Machine{
@@ -364,22 +355,18 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Name: "test_get_shared_nodes_4",
Hostname: "test_get_shared_nodes_4",
NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")},
AuthKeyID: uint(preAuthKey2InShared1.ID),
}
app.db.Save(machine2InShared1)
err = app.AddSharedMachineToNamespace(machineInShared2, namespaceShared1)
c.Assert(err, check.IsNil)
baseDomain := "foobar.headscale.net"
dnsConfigOrig := tailcfg.DNSConfig{
Routes: make(map[string][]dnstype.Resolver),
Routes: make(map[string][]*dnstype.Resolver),
Domains: []string{baseDomain},
Proxied: false,
}

View File

@@ -3,7 +3,7 @@
This page contains the official and community contributed documentation for `headscale`.
If you are having trouble with following the documentation or get unexpected results,
please ask on [Discord](https://discord.gg/XcQxk2VHjx) instead of opening an Issue.
please ask on [Discord](https://discord.gg/c84AZQhmpx) instead of opening an Issue.
## Official documentation
@@ -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**.
- [Running headscale in a container](running-headscale-container.md)
- [Running headscale on OpenBSD](running-headscale-openbsd.md)
## Misc

View File

@@ -5,12 +5,16 @@ ACL's are the most useful).
We have a small company with a boss, an admin, two developers and an intern.
The boss should have access to all servers but not to the users hosts. Admin
The boss should have access to all servers but not to the user's hosts. Admin
should also have access to all hosts except that their permissions should be
limited to maintaining the hosts (for example purposes). The developers can do
anything they want on dev hosts, but only watch on productions hosts. Intern
anything they want on dev hosts but only watch on productions hosts. Intern
can only interact with the development servers.
There's an additional server that acts as a router, connecting the VPN users
to an internal network `10.20.0.0/16`. Developers must have access to those
internal resources.
Each user have at least a device connected to the network and we have some
servers.
@@ -19,22 +23,19 @@ servers.
- app-server1.prod
- app-server1.dev
- billing.internal
- router.internal
## Setup of the network
![ACL implementation example](images/headscale-acl-network.png)
Let's create the namespaces. Each user should have his own namespace. The users
here are represented as namespaces.
## ACL setup
```bash
headscale namespaces create boss
headscale namespaces create admin1
headscale namespaces create dev1
headscale namespaces create dev2
headscale namespaces create intern1
```
Note: Namespaces will be created automatically when users authenticate with the
Headscale server.
We don't need to create namespaces for the servers because the servers will be
tagged. When registering the servers we will need to add the flag
ACLs could be written either on [huJSON](https://github.com/tailscale/hujson)
or YAML. Check the [test ACLs](../tests/acls) for further information.
When registering the servers we will need to add the flag
`--advertised-tags=tag:<tag1>,tag:<tag2>`, and the user (namespace) that is
registering the server should be allowed to do it. Since anyone can add tags to
a server they can register, the check of the tags is done on headscale server
@@ -70,12 +71,20 @@ Here are the ACL's to implement the same permissions as above:
// interns cannot add servers
},
// hosts should be defined using its IP addresses and a subnet mask.
// to define a single host, use a /32 mask. You cannot use DNS entries here,
// as they're prone to be hijacked by replacing their IP addresses.
// see https://github.com/tailscale/tailscale/issues/3800 for more information.
"Hosts": {
"postgresql.internal": "10.20.0.2/32",
"webservers.internal": "10.20.10.1/29"
},
"acls": [
// boss have access to all servers
{
"action": "accept",
"users": ["group:boss"],
"ports": [
"src": ["group:boss"],
"dst": [
"tag:prod-databases:*",
"tag:prod-app-servers:*",
"tag:internal:*",
@@ -84,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",
"users": ["group:admin"],
"ports": [
"src": ["group:admin"],
"proto": "tcp",
"dst": [
"tag:prod-databases:22",
"tag:prod-app-servers:22",
"tag:internal:22",
@@ -97,45 +107,70 @@ 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
// they can only view the applications servers in prod and have no access to databases servers in production
{
"action": "accept",
"users": ["group:dev"],
"ports": [
"src": ["group:dev"],
"dst": [
"tag:dev-databases:*",
"tag:dev-app-servers:*",
"tag:prod-app-servers:80,443"
]
},
// developers have access to the internal network through the router.
// the internal network is composed of HTTPS endpoints and Postgresql
// database servers. There's an additional rule to allow traffic to be
// forwarded to the internal subnet, 10.20.0.0/16. See this issue
// https://github.com/juanfont/headscale/issues/502
{
"action": "accept",
"src": ["group:dev"],
"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
{
"action": "accept",
"users": ["tag:dev-app-servers"],
"ports": ["tag:dev-databases:5432"]
"src": ["tag:dev-app-servers"],
"proto": "tcp",
"dst": ["tag:dev-databases:5432"]
},
{
"action": "accept",
"users": ["tag:prod-app-servers"],
"ports": ["tag:prod-databases:5432"]
"src": ["tag:prod-app-servers"],
"dst": ["tag:prod-databases:5432"]
},
// interns have access to dev-app-servers only in reading mode
{
"action": "accept",
"users": ["group:intern"],
"ports": ["tag:dev-app-servers:80,443"]
"src": ["group:intern"],
"dst": ["tag:dev-app-servers:80,443"]
},
// We still have to allow internal namespaces communications since nothing guarantees that each user have
// their own namespaces.
{ "action": "accept", "users": ["boss"], "ports": ["boss:*"] },
{ "action": "accept", "users": ["dev1"], "ports": ["dev1:*"] },
{ "action": "accept", "users": ["dev2"], "ports": ["dev2:*"] },
{ "action": "accept", "users": ["admin1"], "ports": ["admin1:*"] },
{ "action": "accept", "users": ["intern1"], "ports": ["intern1:*"] }
{ "action": "accept", "src": ["boss"], "dst": ["boss:*"] },
{ "action": "accept", "src": ["dev1"], "dst": ["dev1:*"] },
{ "action": "accept", "src": ["dev2"], "dst": ["dev2:*"] },
{ "action": "accept", "src": ["admin1"], "dst": ["admin1:*"] },
{ "action": "accept", "src": ["intern1"], "dst": ["intern1:*"] }
]
}
```

View File

@@ -0,0 +1,32 @@
# Build docker from scratch
The Dockerfiles included in the repository are using the [buildx plugin](https://docs.docker.com/buildx/working-with-buildx/). This plugin is includes in docker newer than Docker-ce CLI 19.03.2. The plugin is used to be able to build different container arches. Building the Dockerfiles without buildx is not possible.
# Build native
To build the container on the native arch you can just use:
```
$ sudo docker buildx build -t headscale:custom-arch .
```
For example: This will build a amd64(x86_64) container if your hostsystem is amd64(x86_64). Or a arm64 container on a arm64 hostsystem (raspberry pi4).
# Build cross platform
To build a arm64 container on a amd64 hostsystem you could use:
```
$ sudo docker buildx build --platform linux/arm64 -t headscale:custom-arm64 .
```
**Import: Currently arm32 build are not supported as there is a problem with a library used by headscale. Hopefully this will be fixed soon.**
# Build multiple arches
To build multiple archres you could use:
```
$ sudo docker buildx create --use
$ sudo docker buildx build --platform linux/amd64,linux/arm64 .
```

View File

@@ -5,4 +5,5 @@ metadata:
data:
server_url: $(PUBLIC_PROTO)://$(PUBLIC_HOSTNAME)
listen_addr: "0.0.0.0:8080"
metrics_listen_addr: "127.0.0.1:9090"
ephemeral_node_inactivity_timeout: "30m"

View File

@@ -25,6 +25,11 @@ spec:
configMapKeyRef:
name: headscale-config
key: listen_addr
- name: METRICS_LISTEN_ADDR
valueFrom:
configMapKeyRef:
name: headscale-config
key: metrics_listen_addr
- name: DERP_MAP_PATH
value: /vol/config/derp.yaml
- name: EPHEMERAL_NODE_INACTIVITY_TIMEOUT

View File

@@ -26,6 +26,11 @@ spec:
configMapKeyRef:
name: headscale-config
key: listen_addr
- name: METRICS_LISTEN_ADDR
valueFrom:
configMapKeyRef:
name: headscale-config
key: metrics_listen_addr
- name: DERP_MAP_PATH
value: /vol/config/derp.yaml
- name: EPHEMERAL_NODE_INACTIVITY_TIMEOUT

View File

@@ -1,3 +1,6 @@
# Glossary
- Namespace: Collection of Tailscale nodes that can see each other. In Tailscale.com this is called Tailnet.
| Term | Description |
| --------- | --------------------------------------------------------------------------------------------------------------------- |
| Machine | A machine is a single entity connected to `headscale`, typically an installation of Tailscale. Also known as **Node** |
| Namespace | A namespace is a logical grouping of machines "owned" by the same entity, in Tailscale, this is typically a User |

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

362
docs/proposals/001-acls.md Normal file
View File

@@ -0,0 +1,362 @@
# ACLs
A key component of tailscale is the notion of Tailnet. This notion is hidden
but the implications that it have on how to use tailscale are not.
For tailscale an [tailnet](https://tailscale.com/kb/1136/tailnet/) is the
following:
> For personal users, you are a tailnet of many devices and one person. Each
> device gets a private Tailscale IP address in the CGNAT range and every
> device can talk directly to every other device, wherever they are on the
> internet.
>
> For businesses and organizations, a tailnet is many devices and many users.
> It can be based on your Microsoft Active Directory, your Google Workspace, a
> GitHub organization, Okta tenancy, or other identity provider namespace. All
> of the devices and users in your tailnet can be seen by the tailnet
> administrators in the Tailscale admin console. There you can apply
> tailnet-wide configuration, such as ACLs that affect visibility of devices
> inside your tailnet, DNS settings, and more.
## Current implementation and issues
Currently in headscale, the namespaces are used both as tailnet and users. The
issue is that if we want to use the ACL's we can't use both at the same time.
Tailnet's cannot communicate with each others. So we can't have an ACL that
authorize tailnet (namespace) A to talk to tailnet (namespace) B.
We also can't write ACLs based on the users (namespaces in headscale) since all
devices belong to the same user.
With the current implementation the only ACL that we can user is to associate
each headscale IP to a host manually then write the ACLs according to this
manual mapping.
```json
{
"hosts": {
"host1": "100.64.0.1",
"server": "100.64.0.2"
},
"acls": [
{ "action": "accept", "users": ["host1"], "ports": ["host2:80,443"] }
]
}
```
While this works, it requires a lot of manual editing on the configuration and
to keep track of all devices IP address.
## Proposition for a next implementation
In order to ease the use of ACL's we need to split the tailnet and users
notion.
A solution could be to consider a headscale server (in it's entirety) as a
tailnet.
For personal users the default behavior could either allow all communications
between all namespaces (like tailscale) or dissallow all communications between
namespaces (current behavior).
For businesses and organisations, viewing a headscale instance a single tailnet
would allow users (namespace) to talk to each other with the ACLs. As described
in tailscale's documentation [[1]], a server should be tagged and personnal
devices should be tied to a user. Translated in headscale's terms each user can
have multiple devices and all those devices should be in the same namespace.
The servers should be tagged and used as such.
This implementation would render useless the sharing feature that is currently
implemented since an ACL could do the same. Simplifying to only one user
interface to do one thing is easier and less confusing for the users.
To better suit the ACLs in this proposition, it's advised to consider that each
namespaces belong to one person. This person can have multiple devices, they
will all be considered as the same user in the ACLs. OIDC feature wouldn't need
to map people to namespace, just create a namespace if the person isn't
registered yet.
As a sidenote, users would like to write ACLs as YAML. We should offer users
the ability to rules in either format (HuJSON or YAML).
[1]: https://tailscale.com/kb/1068/acl-tags/
## Example
Let's build an example use case for a small business (It may be the place where
ACL's are the most useful).
We have a small company with a boss, an admin, two developper and an intern.
The boss should have access to all servers but not to the users hosts. Admin
should also have access to all hosts except that their permissions should be
limited to maintaining the hosts (for example purposes). The developers can do
anything they want on dev hosts, but only watch on productions hosts. Intern
can only interact with the development servers.
Each user have at least a device connected to the network and we have some
servers.
- database.prod
- database.dev
- app-server1.prod
- app-server1.dev
- billing.internal
### Current headscale implementation
Let's create some namespaces
```bash
headscale namespaces create prod
headscale namespaces create dev
headscale namespaces create internal
headscale namespaces create users
headscale nodes register -n users boss-computer
headscale nodes register -n users admin1-computer
headscale nodes register -n users dev1-computer
headscale nodes register -n users dev1-phone
headscale nodes register -n users dev2-computer
headscale nodes register -n users intern1-computer
headscale nodes register -n prod database
headscale nodes register -n prod app-server1
headscale nodes register -n dev database
headscale nodes register -n dev app-server1
headscale nodes register -n internal billing
headscale nodes list
ID | Name | Namespace | IP address
1 | boss-computer | users | 100.64.0.1
2 | admin1-computer | users | 100.64.0.2
3 | dev1-computer | users | 100.64.0.3
4 | dev1-phone | users | 100.64.0.4
5 | dev2-computer | users | 100.64.0.5
6 | intern1-computer | users | 100.64.0.6
7 | database | prod | 100.64.0.7
8 | app-server1 | prod | 100.64.0.8
9 | database | dev | 100.64.0.9
10 | app-server1 | dev | 100.64.0.10
11 | internal | internal | 100.64.0.11
```
In order to only allow the communications related to our description above we
need to add the following ACLs
```json
{
"hosts": {
"boss-computer": "100.64.0.1",
"admin1-computer": "100.64.0.2",
"dev1-computer": "100.64.0.3",
"dev1-phone": "100.64.0.4",
"dev2-computer": "100.64.0.5",
"intern1-computer": "100.64.0.6",
"prod-app-server1": "100.64.0.8"
},
"groups": {
"group:dev": ["dev1-computer", "dev1-phone", "dev2-computer"],
"group:admin": ["admin1-computer"],
"group:boss": ["boss-computer"],
"group:intern": ["intern1-computer"]
},
"acls": [
// boss have access to all servers but no users hosts
{
"action": "accept",
"users": ["group:boss"],
"ports": ["prod:*", "dev:*", "internal:*"]
},
// admin have access to adminstration port (lets only consider port 22 here)
{
"action": "accept",
"users": ["group:admin"],
"ports": ["prod:22", "dev:22", "internal:22"]
},
// dev can do anything on dev servers and check access on prod servers
{
"action": "accept",
"users": ["group:dev"],
"ports": ["dev:*", "prod-app-server1:80,443"]
},
// interns only have access to port 80 and 443 on dev servers (lame internship)
{ "action": "accept", "users": ["group:intern"], "ports": ["dev:80,443"] },
// users can access their own devices
{
"action": "accept",
"users": ["dev1-computer"],
"ports": ["dev1-phone:*"]
},
{
"action": "accept",
"users": ["dev1-phone"],
"ports": ["dev1-computer:*"]
},
// internal namespace communications should still be allowed within the namespace
{ "action": "accept", "users": ["dev"], "ports": ["dev:*"] },
{ "action": "accept", "users": ["prod"], "ports": ["prod:*"] },
{ "action": "accept", "users": ["internal"], "ports": ["internal:*"] }
]
}
```
Since communications between namespace isn't possible we also have to share the
devices between the namespaces.
```bash
// add boss host to prod, dev and internal network
headscale nodes share -i 1 -n prod
headscale nodes share -i 1 -n dev
headscale nodes share -i 1 -n internal
// add admin computer to prod, dev and internal network
headscale nodes share -i 2 -n prod
headscale nodes share -i 2 -n dev
headscale nodes share -i 2 -n internal
// add all dev to prod and dev network
headscale nodes share -i 3 -n dev
headscale nodes share -i 4 -n dev
headscale nodes share -i 3 -n prod
headscale nodes share -i 4 -n prod
headscale nodes share -i 5 -n dev
headscale nodes share -i 5 -n prod
headscale nodes share -i 6 -n dev
```
This fake network have not been tested but it should work. Operating it could
be quite tedious if the company grows. Each time a new user join we have to add
it to a group, and share it to the correct namespaces. If the user want
multiple devices we have to allow communication to each of them one by one. If
business conduct a change in the organisations we may have to rewrite all acls
and reorganise all namespaces.
If we add servers in production we should also update the ACLs to allow dev
access to certain category of them (only app servers for example).
### example based on the proposition in this document
Let's create the namespaces
```bash
headscale namespaces create boss
headscale namespaces create admin1
headscale namespaces create dev1
headscale namespaces create dev2
headscale namespaces create intern1
```
We don't need to create namespaces for the servers because the servers will be
tagged. When registering the servers we will need to add the flag
`--advertised-tags=tag:<tag1>,tag:<tag2>`, and the user (namespace) that is
registering the server should be allowed to do it. Since anyone can add tags to
a server they can register, the check of the tags is done on headscale server
and only valid tags are applied. A tag is valid if the namespace that is
registering it is allowed to do it.
Here are the ACL's to implement the same permissions as above:
```json
{
// groups are simpler and only list the namespaces name
"groups": {
"group:boss": ["boss"],
"group:dev": ["dev1", "dev2"],
"group:admin": ["admin1"],
"group:intern": ["intern1"]
},
"tagOwners": {
// the administrators can add servers in production
"tag:prod-databases": ["group:admin"],
"tag:prod-app-servers": ["group:admin"],
// the boss can tag any server as internal
"tag:internal": ["group:boss"],
// dev can add servers for dev purposes as well as admins
"tag:dev-databases": ["group:admin", "group:dev"],
"tag:dev-app-servers": ["group:admin", "group:dev"]
// interns cannot add servers
},
"acls": [
// boss have access to all servers
{
"action": "accept",
"users": ["group:boss"],
"ports": [
"tag:prod-databases:*",
"tag:prod-app-servers:*",
"tag:internal:*",
"tag:dev-databases:*",
"tag:dev-app-servers:*"
]
},
// admin have only access to administrative ports of the servers
{
"action": "accept",
"users": ["group:admin"],
"ports": [
"tag:prod-databases:22",
"tag:prod-app-servers:22",
"tag:internal:22",
"tag:dev-databases:22",
"tag:dev-app-servers:22"
]
},
{
"action": "accept",
"users": ["group:dev"],
"ports": [
"tag:dev-databases:*",
"tag:dev-app-servers:*",
"tag:prod-app-servers:80,443"
]
},
// servers should be able to talk to database. Database should not be able to initiate connections to server
{
"action": "accept",
"users": ["tag:dev-app-servers"],
"ports": ["tag:dev-databases:5432"]
},
{
"action": "accept",
"users": ["tag:prod-app-servers"],
"ports": ["tag:prod-databases:5432"]
},
// interns have access to dev-app-servers only in reading mode
{
"action": "accept",
"users": ["group:intern"],
"ports": ["tag:dev-app-servers:80,443"]
},
// we still have to allow internal namespaces communications since nothing guarantees that each user have their own namespaces. This could be talked over.
{ "action": "accept", "users": ["boss"], "ports": ["boss:*"] },
{ "action": "accept", "users": ["dev1"], "ports": ["dev1:*"] },
{ "action": "accept", "users": ["dev2"], "ports": ["dev2:*"] },
{ "action": "accept", "users": ["admin1"], "ports": ["admin1:*"] },
{ "action": "accept", "users": ["intern1"], "ports": ["intern1:*"] }
]
}
```
With this implementation, the sharing step is not necessary. Maintenance cost
of the ACL file is lower and less tedious (no need to map hostname and IP's
into it).

View File

@@ -24,7 +24,7 @@ To create a API key, log into your `headscale` server and generate a key:
headscale apikeys create --expiration 90d
```
Copy the output of the command and save it for later. Please not that you can not retrieve a key again,
Copy the output of the command and save it for later. Please note that you can not retrieve a key again,
if the key is lost, expire the old one, and create a new key.
To list the keys currently assosicated with the server:

View File

@@ -14,8 +14,8 @@ not work with alternatives like [Podman](https://podman.io). The Docker image ca
1. Prepare a directory on the host Docker node in your directory of choice, used to hold `headscale` configuration and the [SQLite](https://www.sqlite.org/) database:
```shell
mkdir ./headscale && cd ./headscale
mkdir ./config
mkdir -p ./headscale/config
cd ./headscale
```
2. Create an empty SQlite datebase in the headscale directory:
@@ -45,6 +45,17 @@ touch ./config/config.yaml
```
Modify the config file to your preferences before launching Docker container.
Here are some settings that you likely want:
```yaml
server_url: http://your-host-name:8080 # Change to your hostname or host IP
# Listen to 0.0.0.0 so it's accessible outside the container
metrics_listen_addr: 0.0.0.0:9090
# The default /var/lib/headscale path is not writable in the container
private_key_path: /etc/headscale/private.key
# The default /var/lib/headscale path is not writable in the container
db_path: /etc/headscale/db.sqlite
```
4. Start the headscale server while working in the host headscale directory:
@@ -55,11 +66,14 @@ docker run \
--rm \
--volume $(pwd)/config:/etc/headscale/ \
--publish 127.0.0.1:8080:8080 \
--publish 127.0.0.1:9090:9090 \
headscale/headscale:<VERSION> \
headscale serve
```
Note: use `0.0.0.0:8080:8080` instead of `127.0.0.1:8080:8080` if you want to expose the container externally.
This command will mount `config/` under `/etc/headscale`, forward port 8080 out of the container so the
`headscale` instance becomes available and then detach so headscale runs in the background.
@@ -80,13 +94,14 @@ docker ps
Verify `headscale` is available:
```shell
curl http://127.0.0.1:8080/metrics
curl http://127.0.0.1:9090/metrics
```
6. Create a namespace ([tailnet](https://tailscale.com/kb/1136/tailnet/)):
```shell
docker exec headscale -- headscale namespaces create myfirstnamespace
docker exec headscale \
headscale namespaces create myfirstnamespace
```
### Register a machine (normal login)
@@ -100,7 +115,7 @@ tailscale up --login-server YOUR_HEADSCALE_URL
To register a machine when running `headscale` in a container, take the headscale command and pass it to the container:
```shell
docker exec headscale -- \
docker exec headscale \
headscale --namespace myfirstnamespace nodes register --key <YOU_+MACHINE_KEY>
```
@@ -109,7 +124,7 @@ docker exec headscale -- \
Generate a key using the command line:
```shell
docker exec headscale -- \
docker exec headscale \
headscale --namespace myfirstnamespace preauthkeys create --reusable --expiration 24h
```

View File

@@ -30,6 +30,14 @@ mkdir -p /etc/headscale
# Directory for Database, and other variable data (like certificates)
mkdir -p /var/lib/headscale
# or if you create a headscale user:
useradd \
--create-home \
--home-dir /var/lib/headscale/ \
--system \
--user-group \
--shell /usr/bin/nologin \
headscale
```
4. Create an empty SQLite database:
@@ -50,7 +58,7 @@ from the [headscale repository](../)
6. Start the headscale server:
```shell
headscale serve
headscale serve
```
This command will start `headscale` in the current terminal session.
@@ -67,7 +75,7 @@ To run `headscale` in the background, please follow the steps in the [SystemD se
Verify `headscale` is available:
```shell
curl http://127.0.0.1:8080/metrics
curl http://127.0.0.1:9090/metrics
```
8. Create a namespace ([tailnet](https://tailscale.com/kb/1136/tailnet/)):
@@ -150,7 +158,7 @@ or run all headscale commands as the headscale user:
su - headscale
```
2. In `/etc/headscale/config.yaml`, override the default `headscale` unix socket with a SystemD friendly path:
2. In `/etc/headscale/config.yaml`, override the default `headscale` unix socket with path that is writable by the `headscale` user or group:
```yaml
unix_socket: /var/run/headscale/headscale.sock
@@ -165,8 +173,7 @@ systemctl daemon-reload
4. Enable and start the new `headscale` service:
```shell
systemctl enable headscale
systemctl start headscale
systemctl enable --now headscale
```
5. Verify the headscale service:
@@ -178,7 +185,7 @@ systemctl status headscale
Verify `headscale` is available:
```shell
curl http://127.0.0.1:8080/metrics
curl http://127.0.0.1:9090/metrics
```
`headscale` will now run in the background and start at boot.

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.

43
flake.lock generated Normal file
View File

@@ -0,0 +1,43 @@
{
"nodes": {
"flake-utils": {
"locked": {
"lastModified": 1653893745,
"narHash": "sha256-0jntwV3Z8//YwuOjzhV2sgJJPt+HY6KhU7VZUL0fKZQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "1ed9fb1935d260de5fe1c2f7ee0ebaae17ed2fa1",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1654847188,
"narHash": "sha256-MC+eP7XOGE1LAswOPqdcGoUqY9mEQ3ZaaxamVTbc0hM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "8b66e3f2ebcc644b78cec9d6f152192f4e7d322f",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-22.05",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

168
flake.nix Normal file
View File

@@ -0,0 +1,168 @@
{
description = "headscale - Open Source Tailscale Control server";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-22.05";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils, ... }:
let
headscaleVersion = if (self ? shortRev) then self.shortRev else "dev";
in
{
overlay = final: prev:
let
pkgs = nixpkgs.legacyPackages.${prev.system};
in
rec {
headscale =
pkgs.buildGo118Module rec {
pname = "headscale";
version = headscaleVersion;
src = pkgs.lib.cleanSource self;
# When updating go.mod or go.sum, a new sha will need to be calculated,
# update this if you have a mismatch after doing a change to thos files.
vendorSha256 = "sha256-2o78hsi0B9U5NOcYXRqkBmg34p71J/R8FibXsgwEcSo=";
ldflags = [ "-s" "-w" "-X github.com/juanfont/headscale/cmd/headscale/cli.Version=v${version}" ];
};
golines =
pkgs.buildGoModule rec {
pname = "golines";
version = "0.9.0";
src = pkgs.fetchFromGitHub {
owner = "segmentio";
repo = "golines";
rev = "v${version}";
sha256 = "sha256-BUXEg+4r9L/gqe4DhTlhN55P3jWt7ZyWFQycO6QePrw=";
};
vendorSha256 = "sha256-sEzWUeVk5GB0H41wrp12P8sBWRjg0FHUX6ABDEEBqK8=";
nativeBuildInputs = [ pkgs.installShellFiles ];
};
golangci-lint = prev.golangci-lint.override {
# Override https://github.com/NixOS/nixpkgs/pull/166801 which changed this
# to buildGo118Module because it does not build on Darwin.
inherit (prev) buildGoModule;
};
# golangci-lint =
# pkgs.buildGo117Module rec {
# pname = "golangci-lint";
# version = "1.46.2";
#
# src = pkgs.fetchFromGitHub {
# owner = "golangci";
# repo = "golangci-lint";
# rev = "v${version}";
# sha256 = "sha256-7sDAwWz+qoB/ngeH35tsJ5FZUfAQvQsU6kU9rUHIHMk=";
# };
#
# vendorSha256 = "sha256-w38OKN6HPoz37utG/2QSPMai55IRDXCIIymeMe6ogIU=";
#
# nativeBuildInputs = [ pkgs.installShellFiles ];
# };
protoc-gen-grpc-gateway =
pkgs.buildGoModule rec {
pname = "grpc-gateway";
version = "2.8.0";
src = pkgs.fetchFromGitHub {
owner = "grpc-ecosystem";
repo = "grpc-gateway";
rev = "v${version}";
sha256 = "sha256-8eBBBYJ+tBjB2fgPMX/ZlbN3eeS75e8TAZYOKXs6hcg=";
};
vendorSha256 = "sha256-AW2Gn/mlZyLMwF+NpK59eiOmQrYWW/9HPjbunYc9Ij4=";
nativeBuildInputs = [ pkgs.installShellFiles ];
subPackages = [ "protoc-gen-grpc-gateway" "protoc-gen-openapiv2" ];
};
};
} // flake-utils.lib.eachDefaultSystem
(system:
let
pkgs = import nixpkgs {
overlays = [ self.overlay ];
inherit system;
};
buildDeps = with pkgs; [ git go_1_18 gnumake ];
devDeps = with pkgs;
buildDeps ++ [
golangci-lint
golines
nodePackages.prettier
# Protobuf dependencies
protobuf
protoc-gen-go
protoc-gen-go-grpc
protoc-gen-grpc-gateway
buf
clang-tools # clang-format
];
# Add entry to build a docker image with headscale
# caveat: only works on Linux
#
# Usage:
# nix build .#headscale-docker
# docker load < result
headscale-docker = pkgs.dockerTools.buildLayeredImage {
name = "headscale";
tag = headscaleVersion;
contents = [ pkgs.headscale ];
config.Entrypoint = [ (pkgs.headscale + "/bin/headscale") ];
};
in
rec {
# `nix develop`
devShell = pkgs.mkShell { buildInputs = devDeps; };
# `nix build`
packages = with pkgs; {
inherit headscale;
inherit headscale-docker;
};
defaultPackage = pkgs.headscale;
# `nix run`
apps.headscale = flake-utils.lib.mkApp {
drv = packages.headscale;
};
defaultApp = apps.headscale;
checks = {
format = pkgs.runCommand "check-format"
{
buildInputs = with pkgs; [
gnumake
nixpkgs-fmt
golangci-lint
nodePackages.prettier
golines
clang-tools
];
} ''
${pkgs.nixpkgs-fmt}/bin/nixpkgs-fmt ${./.}
${pkgs.golangci-lint}/bin/golangci-lint run --fix --timeout 10m
${pkgs.nodePackages.prettier}/bin/prettier --write '**/**.{ts,js,md,yaml,yml,sass,css,scss,html}'
${pkgs.golines}/bin/golines --max-len=88 --base-formatter=gofumpt -w ${./.}
${pkgs.clang-tools}/bin/clang-format -style="{BasedOnStyle: Google, IndentWidth: 4, AlignConsecutiveDeclarations: true, AlignConsecutiveAssignments: true, ColumnLimit: 0}" -i ${./.}
'';
};
});
}

View File

@@ -7,12 +7,11 @@
package v1
import (
reflect "reflect"
sync "sync"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
)
const (

View File

@@ -7,12 +7,11 @@
package v1
import (
reflect "reflect"
sync "sync"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
)
const (

View File

@@ -7,11 +7,10 @@
package v1
import (
reflect "reflect"
_ "google.golang.org/genproto/googleapis/api/annotations"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
)
const (
@@ -37,7 +36,7 @@ var file_headscale_v1_headscale_proto_rawDesc = []byte{
0x6f, 0x74, 0x6f, 0x1a, 0x19, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76,
0x31, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x19,
0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x69,
0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x32, 0xcb, 0x15, 0x0a, 0x10, 0x48, 0x65,
0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x32, 0xb1, 0x16, 0x0a, 0x10, 0x48, 0x65,
0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x77,
0x0a, 0x0c, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x21,
0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65,
@@ -120,100 +119,106 @@ var file_headscale_v1_headscale_proto_rawDesc = []byte{
0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
0x24, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1e, 0x12, 0x1c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31,
0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x7b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x65, 0x5f, 0x69, 0x64, 0x7d, 0x12, 0x80, 0x01, 0x0a, 0x0f, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74,
0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x24, 0x2e, 0x68, 0x65, 0x61, 0x64,
0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65,
0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
0x25, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52,
0x65, 0x5f, 0x69, 0x64, 0x7d, 0x12, 0x74, 0x0a, 0x07, 0x53, 0x65, 0x74, 0x54, 0x61, 0x67, 0x73,
0x12, 0x1c, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e,
0x53, 0x65, 0x74, 0x54, 0x61, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d,
0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65,
0x74, 0x54, 0x61, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2c, 0x82,
0xd3, 0xe4, 0x93, 0x02, 0x26, 0x22, 0x21, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6d,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x7b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f,
0x69, 0x64, 0x7d, 0x2f, 0x74, 0x61, 0x67, 0x73, 0x3a, 0x01, 0x2a, 0x12, 0x80, 0x01, 0x0a, 0x0f,
0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12,
0x24, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52,
0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x22, 0x18,
0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f,
0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x12, 0x7e, 0x0a, 0x0d, 0x44, 0x65, 0x6c, 0x65,
0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x22, 0x2e, 0x68, 0x65, 0x61, 0x64,
0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e,
0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c,
0x65, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x22, 0x24, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1e, 0x2a, 0x1c, 0x2f, 0x61, 0x70, 0x69,
0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x7b, 0x6d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x12, 0x85, 0x01, 0x0a, 0x0d, 0x45, 0x78, 0x70,
0x69, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x22, 0x2e, 0x68, 0x65, 0x61,
0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65,
0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23,
0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78,
0x70, 0x69, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x22, 0x2b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x25, 0x22, 0x23, 0x2f, 0x61, 0x70,
0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x7b, 0x6d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65,
0x12, 0x6e, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73,
0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e,
0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e,
0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x17, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x11, 0x12,
0x0f, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x12, 0x8d, 0x01, 0x0a, 0x0c, 0x53, 0x68, 0x61, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x65, 0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31,
0x2e, 0x53, 0x68, 0x61, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65,
0x2e, 0x76, 0x31, 0x2e, 0x53, 0x68, 0x61, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x36, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x30,
0x22, 0x2e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x65, 0x2f, 0x7b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x73,
0x68, 0x61, 0x72, 0x65, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x7d,
0x12, 0x95, 0x01, 0x0a, 0x0e, 0x55, 0x6e, 0x73, 0x68, 0x61, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68,
0x69, 0x6e, 0x65, 0x12, 0x23, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e,
0x76, 0x31, 0x2e, 0x55, 0x6e, 0x73, 0x68, 0x61, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73,
0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x6e, 0x73, 0x68, 0x61, 0x72, 0x65, 0x4d,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x38,
0x82, 0xd3, 0xe4, 0x93, 0x02, 0x32, 0x22, 0x30, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f,
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x7b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x75, 0x6e, 0x73, 0x68, 0x61, 0x72, 0x65, 0x2f, 0x7b, 0x6e, 0x61,
0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x7d, 0x12, 0x8b, 0x01, 0x0a, 0x0f, 0x47, 0x65, 0x74,
0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x24, 0x2e, 0x68,
0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4d,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x25, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76,
0x31, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x6f, 0x75, 0x74,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c,
0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x20, 0x82, 0xd3,
0xe4, 0x93, 0x02, 0x1a, 0x22, 0x18, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x12, 0x7e,
0x0a, 0x0d, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12,
0x22, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44,
0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e,
0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x24, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1e,
0x2a, 0x1c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x65, 0x2f, 0x7b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x12, 0x85,
0x01, 0x0a, 0x0d, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x12, 0x22, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e,
0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65,
0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2b, 0x82, 0xd3, 0xe4, 0x93, 0x02,
0x25, 0x12, 0x23, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69,
0x25, 0x22, 0x23, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69,
0x6e, 0x65, 0x2f, 0x7b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x2f,
0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x97, 0x01, 0x0a, 0x13, 0x45, 0x6e, 0x61, 0x62, 0x6c,
0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x28,
0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x12, 0x90, 0x01, 0x0a, 0x0d, 0x52, 0x65, 0x6e, 0x61, 0x6d,
0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x22, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73,
0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x4d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x68,
0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6e, 0x61,
0x6d, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x22, 0x36, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x30, 0x22, 0x2e, 0x2f, 0x61, 0x70, 0x69, 0x2f,
0x76, 0x31, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x7b, 0x6d, 0x61, 0x63, 0x68,
0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x72, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x2f, 0x7b,
0x6e, 0x65, 0x77, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x12, 0x6e, 0x0a, 0x0c, 0x4c, 0x69, 0x73,
0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64,
0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68,
0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74,
0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x22, 0x17, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x11, 0x12, 0x0f, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76,
0x31, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x82, 0x01, 0x0a, 0x0b, 0x4d, 0x6f,
0x76, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x20, 0x2e, 0x68, 0x65, 0x61, 0x64,
0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x6f, 0x76, 0x65, 0x4d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x68, 0x65,
0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x6f, 0x76, 0x65, 0x4d,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2e,
0x82, 0xd3, 0xe4, 0x93, 0x02, 0x28, 0x22, 0x26, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f,
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x7b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x8b,
0x01, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x6f, 0x75,
0x74, 0x65, 0x12, 0x24, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76,
0x31, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x6f, 0x75, 0x74,
0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73,
0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69,
0x6e, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
0x2b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x25, 0x12, 0x23, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31,
0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x7b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x65, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x97, 0x01, 0x0a,
0x13, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x6f,
0x75, 0x74, 0x65, 0x73, 0x12, 0x28, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65,
0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29,
0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e,
0x61, 0x62, 0x6c, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65,
0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73,
0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x4d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x22, 0x2b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x25, 0x22, 0x23, 0x2f, 0x61, 0x70,
0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x7b, 0x6d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73,
0x12, 0x70, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79,
0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e,
0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e,
0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x22,
0x0e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x69, 0x6b, 0x65, 0x79, 0x3a,
0x01, 0x2a, 0x12, 0x77, 0x0a, 0x0c, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x41, 0x70, 0x69, 0x4b,
0x65, 0x79, 0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76,
0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c,
0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65,
0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02,
0x1a, 0x22, 0x15, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x69, 0x6b, 0x65,
0x79, 0x2f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x3a, 0x01, 0x2a, 0x12, 0x6a, 0x0a, 0x0b, 0x4c,
0x69, 0x73, 0x74, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x20, 0x2e, 0x68, 0x65, 0x61,
0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x70,
0x69, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x68,
0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74,
0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
0x16, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x12, 0x0e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31,
0x2f, 0x61, 0x70, 0x69, 0x6b, 0x65, 0x79, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75,
0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x75, 0x61, 0x6e, 0x66, 0x6f, 0x6e, 0x74, 0x2f, 0x68,
0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f,
0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2b, 0x82, 0xd3, 0xe4, 0x93, 0x02,
0x25, 0x22, 0x23, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69,
0x6e, 0x65, 0x2f, 0x7b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x2f,
0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x70, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61,
0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x70, 0x69, 0x4b,
0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x65, 0x61, 0x64,
0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41,
0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x19, 0x82,
0xd3, 0xe4, 0x93, 0x02, 0x13, 0x22, 0x0e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x61,
0x70, 0x69, 0x6b, 0x65, 0x79, 0x3a, 0x01, 0x2a, 0x12, 0x77, 0x0a, 0x0c, 0x45, 0x78, 0x70, 0x69,
0x72, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73,
0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x41, 0x70,
0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x65,
0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72,
0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x22, 0x15, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31,
0x2f, 0x61, 0x70, 0x69, 0x6b, 0x65, 0x79, 0x2f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x3a, 0x01,
0x2a, 0x12, 0x6a, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x73,
0x12, 0x20, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e,
0x4c, 0x69, 0x73, 0x74, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76,
0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x16, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x12, 0x0e, 0x2f,
0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x69, 0x6b, 0x65, 0x79, 0x42, 0x29, 0x5a,
0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x75, 0x61, 0x6e,
0x66, 0x6f, 0x6e, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x67,
0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var file_headscale_v1_headscale_proto_goTypes = []interface{}{
@@ -227,38 +232,40 @@ var file_headscale_v1_headscale_proto_goTypes = []interface{}{
(*ListPreAuthKeysRequest)(nil), // 7: headscale.v1.ListPreAuthKeysRequest
(*DebugCreateMachineRequest)(nil), // 8: headscale.v1.DebugCreateMachineRequest
(*GetMachineRequest)(nil), // 9: headscale.v1.GetMachineRequest
(*RegisterMachineRequest)(nil), // 10: headscale.v1.RegisterMachineRequest
(*DeleteMachineRequest)(nil), // 11: headscale.v1.DeleteMachineRequest
(*ExpireMachineRequest)(nil), // 12: headscale.v1.ExpireMachineRequest
(*ListMachinesRequest)(nil), // 13: headscale.v1.ListMachinesRequest
(*ShareMachineRequest)(nil), // 14: headscale.v1.ShareMachineRequest
(*UnshareMachineRequest)(nil), // 15: headscale.v1.UnshareMachineRequest
(*GetMachineRouteRequest)(nil), // 16: headscale.v1.GetMachineRouteRequest
(*EnableMachineRoutesRequest)(nil), // 17: headscale.v1.EnableMachineRoutesRequest
(*CreateApiKeyRequest)(nil), // 18: headscale.v1.CreateApiKeyRequest
(*ExpireApiKeyRequest)(nil), // 19: headscale.v1.ExpireApiKeyRequest
(*ListApiKeysRequest)(nil), // 20: headscale.v1.ListApiKeysRequest
(*GetNamespaceResponse)(nil), // 21: headscale.v1.GetNamespaceResponse
(*CreateNamespaceResponse)(nil), // 22: headscale.v1.CreateNamespaceResponse
(*RenameNamespaceResponse)(nil), // 23: headscale.v1.RenameNamespaceResponse
(*DeleteNamespaceResponse)(nil), // 24: headscale.v1.DeleteNamespaceResponse
(*ListNamespacesResponse)(nil), // 25: headscale.v1.ListNamespacesResponse
(*CreatePreAuthKeyResponse)(nil), // 26: headscale.v1.CreatePreAuthKeyResponse
(*ExpirePreAuthKeyResponse)(nil), // 27: headscale.v1.ExpirePreAuthKeyResponse
(*ListPreAuthKeysResponse)(nil), // 28: headscale.v1.ListPreAuthKeysResponse
(*DebugCreateMachineResponse)(nil), // 29: headscale.v1.DebugCreateMachineResponse
(*GetMachineResponse)(nil), // 30: headscale.v1.GetMachineResponse
(*RegisterMachineResponse)(nil), // 31: headscale.v1.RegisterMachineResponse
(*DeleteMachineResponse)(nil), // 32: headscale.v1.DeleteMachineResponse
(*ExpireMachineResponse)(nil), // 33: headscale.v1.ExpireMachineResponse
(*ListMachinesResponse)(nil), // 34: headscale.v1.ListMachinesResponse
(*ShareMachineResponse)(nil), // 35: headscale.v1.ShareMachineResponse
(*UnshareMachineResponse)(nil), // 36: headscale.v1.UnshareMachineResponse
(*GetMachineRouteResponse)(nil), // 37: headscale.v1.GetMachineRouteResponse
(*EnableMachineRoutesResponse)(nil), // 38: headscale.v1.EnableMachineRoutesResponse
(*CreateApiKeyResponse)(nil), // 39: headscale.v1.CreateApiKeyResponse
(*ExpireApiKeyResponse)(nil), // 40: headscale.v1.ExpireApiKeyResponse
(*ListApiKeysResponse)(nil), // 41: headscale.v1.ListApiKeysResponse
(*SetTagsRequest)(nil), // 10: headscale.v1.SetTagsRequest
(*RegisterMachineRequest)(nil), // 11: headscale.v1.RegisterMachineRequest
(*DeleteMachineRequest)(nil), // 12: headscale.v1.DeleteMachineRequest
(*ExpireMachineRequest)(nil), // 13: headscale.v1.ExpireMachineRequest
(*RenameMachineRequest)(nil), // 14: headscale.v1.RenameMachineRequest
(*ListMachinesRequest)(nil), // 15: headscale.v1.ListMachinesRequest
(*MoveMachineRequest)(nil), // 16: headscale.v1.MoveMachineRequest
(*GetMachineRouteRequest)(nil), // 17: headscale.v1.GetMachineRouteRequest
(*EnableMachineRoutesRequest)(nil), // 18: headscale.v1.EnableMachineRoutesRequest
(*CreateApiKeyRequest)(nil), // 19: headscale.v1.CreateApiKeyRequest
(*ExpireApiKeyRequest)(nil), // 20: headscale.v1.ExpireApiKeyRequest
(*ListApiKeysRequest)(nil), // 21: headscale.v1.ListApiKeysRequest
(*GetNamespaceResponse)(nil), // 22: headscale.v1.GetNamespaceResponse
(*CreateNamespaceResponse)(nil), // 23: headscale.v1.CreateNamespaceResponse
(*RenameNamespaceResponse)(nil), // 24: headscale.v1.RenameNamespaceResponse
(*DeleteNamespaceResponse)(nil), // 25: headscale.v1.DeleteNamespaceResponse
(*ListNamespacesResponse)(nil), // 26: headscale.v1.ListNamespacesResponse
(*CreatePreAuthKeyResponse)(nil), // 27: headscale.v1.CreatePreAuthKeyResponse
(*ExpirePreAuthKeyResponse)(nil), // 28: headscale.v1.ExpirePreAuthKeyResponse
(*ListPreAuthKeysResponse)(nil), // 29: headscale.v1.ListPreAuthKeysResponse
(*DebugCreateMachineResponse)(nil), // 30: headscale.v1.DebugCreateMachineResponse
(*GetMachineResponse)(nil), // 31: headscale.v1.GetMachineResponse
(*SetTagsResponse)(nil), // 32: headscale.v1.SetTagsResponse
(*RegisterMachineResponse)(nil), // 33: headscale.v1.RegisterMachineResponse
(*DeleteMachineResponse)(nil), // 34: headscale.v1.DeleteMachineResponse
(*ExpireMachineResponse)(nil), // 35: headscale.v1.ExpireMachineResponse
(*RenameMachineResponse)(nil), // 36: headscale.v1.RenameMachineResponse
(*ListMachinesResponse)(nil), // 37: headscale.v1.ListMachinesResponse
(*MoveMachineResponse)(nil), // 38: headscale.v1.MoveMachineResponse
(*GetMachineRouteResponse)(nil), // 39: headscale.v1.GetMachineRouteResponse
(*EnableMachineRoutesResponse)(nil), // 40: headscale.v1.EnableMachineRoutesResponse
(*CreateApiKeyResponse)(nil), // 41: headscale.v1.CreateApiKeyResponse
(*ExpireApiKeyResponse)(nil), // 42: headscale.v1.ExpireApiKeyResponse
(*ListApiKeysResponse)(nil), // 43: headscale.v1.ListApiKeysResponse
}
var file_headscale_v1_headscale_proto_depIdxs = []int32{
0, // 0: headscale.v1.HeadscaleService.GetNamespace:input_type -> headscale.v1.GetNamespaceRequest
@@ -271,40 +278,42 @@ var file_headscale_v1_headscale_proto_depIdxs = []int32{
7, // 7: headscale.v1.HeadscaleService.ListPreAuthKeys:input_type -> headscale.v1.ListPreAuthKeysRequest
8, // 8: headscale.v1.HeadscaleService.DebugCreateMachine:input_type -> headscale.v1.DebugCreateMachineRequest
9, // 9: headscale.v1.HeadscaleService.GetMachine:input_type -> headscale.v1.GetMachineRequest
10, // 10: headscale.v1.HeadscaleService.RegisterMachine:input_type -> headscale.v1.RegisterMachineRequest
11, // 11: headscale.v1.HeadscaleService.DeleteMachine:input_type -> headscale.v1.DeleteMachineRequest
12, // 12: headscale.v1.HeadscaleService.ExpireMachine:input_type -> headscale.v1.ExpireMachineRequest
13, // 13: headscale.v1.HeadscaleService.ListMachines:input_type -> headscale.v1.ListMachinesRequest
14, // 14: headscale.v1.HeadscaleService.ShareMachine:input_type -> headscale.v1.ShareMachineRequest
15, // 15: headscale.v1.HeadscaleService.UnshareMachine:input_type -> headscale.v1.UnshareMachineRequest
16, // 16: headscale.v1.HeadscaleService.GetMachineRoute:input_type -> headscale.v1.GetMachineRouteRequest
17, // 17: headscale.v1.HeadscaleService.EnableMachineRoutes:input_type -> headscale.v1.EnableMachineRoutesRequest
18, // 18: headscale.v1.HeadscaleService.CreateApiKey:input_type -> headscale.v1.CreateApiKeyRequest
19, // 19: headscale.v1.HeadscaleService.ExpireApiKey:input_type -> headscale.v1.ExpireApiKeyRequest
20, // 20: headscale.v1.HeadscaleService.ListApiKeys:input_type -> headscale.v1.ListApiKeysRequest
21, // 21: headscale.v1.HeadscaleService.GetNamespace:output_type -> headscale.v1.GetNamespaceResponse
22, // 22: headscale.v1.HeadscaleService.CreateNamespace:output_type -> headscale.v1.CreateNamespaceResponse
23, // 23: headscale.v1.HeadscaleService.RenameNamespace:output_type -> headscale.v1.RenameNamespaceResponse
24, // 24: headscale.v1.HeadscaleService.DeleteNamespace:output_type -> headscale.v1.DeleteNamespaceResponse
25, // 25: headscale.v1.HeadscaleService.ListNamespaces:output_type -> headscale.v1.ListNamespacesResponse
26, // 26: headscale.v1.HeadscaleService.CreatePreAuthKey:output_type -> headscale.v1.CreatePreAuthKeyResponse
27, // 27: headscale.v1.HeadscaleService.ExpirePreAuthKey:output_type -> headscale.v1.ExpirePreAuthKeyResponse
28, // 28: headscale.v1.HeadscaleService.ListPreAuthKeys:output_type -> headscale.v1.ListPreAuthKeysResponse
29, // 29: headscale.v1.HeadscaleService.DebugCreateMachine:output_type -> headscale.v1.DebugCreateMachineResponse
30, // 30: headscale.v1.HeadscaleService.GetMachine:output_type -> headscale.v1.GetMachineResponse
31, // 31: headscale.v1.HeadscaleService.RegisterMachine:output_type -> headscale.v1.RegisterMachineResponse
32, // 32: headscale.v1.HeadscaleService.DeleteMachine:output_type -> headscale.v1.DeleteMachineResponse
33, // 33: headscale.v1.HeadscaleService.ExpireMachine:output_type -> headscale.v1.ExpireMachineResponse
34, // 34: headscale.v1.HeadscaleService.ListMachines:output_type -> headscale.v1.ListMachinesResponse
35, // 35: headscale.v1.HeadscaleService.ShareMachine:output_type -> headscale.v1.ShareMachineResponse
36, // 36: headscale.v1.HeadscaleService.UnshareMachine:output_type -> headscale.v1.UnshareMachineResponse
37, // 37: headscale.v1.HeadscaleService.GetMachineRoute:output_type -> headscale.v1.GetMachineRouteResponse
38, // 38: headscale.v1.HeadscaleService.EnableMachineRoutes:output_type -> headscale.v1.EnableMachineRoutesResponse
39, // 39: headscale.v1.HeadscaleService.CreateApiKey:output_type -> headscale.v1.CreateApiKeyResponse
40, // 40: headscale.v1.HeadscaleService.ExpireApiKey:output_type -> headscale.v1.ExpireApiKeyResponse
41, // 41: headscale.v1.HeadscaleService.ListApiKeys:output_type -> headscale.v1.ListApiKeysResponse
21, // [21:42] is the sub-list for method output_type
0, // [0:21] is the sub-list for method input_type
10, // 10: headscale.v1.HeadscaleService.SetTags:input_type -> headscale.v1.SetTagsRequest
11, // 11: headscale.v1.HeadscaleService.RegisterMachine:input_type -> headscale.v1.RegisterMachineRequest
12, // 12: headscale.v1.HeadscaleService.DeleteMachine:input_type -> headscale.v1.DeleteMachineRequest
13, // 13: headscale.v1.HeadscaleService.ExpireMachine:input_type -> headscale.v1.ExpireMachineRequest
14, // 14: headscale.v1.HeadscaleService.RenameMachine:input_type -> headscale.v1.RenameMachineRequest
15, // 15: headscale.v1.HeadscaleService.ListMachines:input_type -> headscale.v1.ListMachinesRequest
16, // 16: headscale.v1.HeadscaleService.MoveMachine:input_type -> headscale.v1.MoveMachineRequest
17, // 17: headscale.v1.HeadscaleService.GetMachineRoute:input_type -> headscale.v1.GetMachineRouteRequest
18, // 18: headscale.v1.HeadscaleService.EnableMachineRoutes:input_type -> headscale.v1.EnableMachineRoutesRequest
19, // 19: headscale.v1.HeadscaleService.CreateApiKey:input_type -> headscale.v1.CreateApiKeyRequest
20, // 20: headscale.v1.HeadscaleService.ExpireApiKey:input_type -> headscale.v1.ExpireApiKeyRequest
21, // 21: headscale.v1.HeadscaleService.ListApiKeys:input_type -> headscale.v1.ListApiKeysRequest
22, // 22: headscale.v1.HeadscaleService.GetNamespace:output_type -> headscale.v1.GetNamespaceResponse
23, // 23: headscale.v1.HeadscaleService.CreateNamespace:output_type -> headscale.v1.CreateNamespaceResponse
24, // 24: headscale.v1.HeadscaleService.RenameNamespace:output_type -> headscale.v1.RenameNamespaceResponse
25, // 25: headscale.v1.HeadscaleService.DeleteNamespace:output_type -> headscale.v1.DeleteNamespaceResponse
26, // 26: headscale.v1.HeadscaleService.ListNamespaces:output_type -> headscale.v1.ListNamespacesResponse
27, // 27: headscale.v1.HeadscaleService.CreatePreAuthKey:output_type -> headscale.v1.CreatePreAuthKeyResponse
28, // 28: headscale.v1.HeadscaleService.ExpirePreAuthKey:output_type -> headscale.v1.ExpirePreAuthKeyResponse
29, // 29: headscale.v1.HeadscaleService.ListPreAuthKeys:output_type -> headscale.v1.ListPreAuthKeysResponse
30, // 30: headscale.v1.HeadscaleService.DebugCreateMachine:output_type -> headscale.v1.DebugCreateMachineResponse
31, // 31: headscale.v1.HeadscaleService.GetMachine:output_type -> headscale.v1.GetMachineResponse
32, // 32: headscale.v1.HeadscaleService.SetTags:output_type -> headscale.v1.SetTagsResponse
33, // 33: headscale.v1.HeadscaleService.RegisterMachine:output_type -> headscale.v1.RegisterMachineResponse
34, // 34: headscale.v1.HeadscaleService.DeleteMachine:output_type -> headscale.v1.DeleteMachineResponse
35, // 35: headscale.v1.HeadscaleService.ExpireMachine:output_type -> headscale.v1.ExpireMachineResponse
36, // 36: headscale.v1.HeadscaleService.RenameMachine:output_type -> headscale.v1.RenameMachineResponse
37, // 37: headscale.v1.HeadscaleService.ListMachines:output_type -> headscale.v1.ListMachinesResponse
38, // 38: headscale.v1.HeadscaleService.MoveMachine:output_type -> headscale.v1.MoveMachineResponse
39, // 39: headscale.v1.HeadscaleService.GetMachineRoute:output_type -> headscale.v1.GetMachineRouteResponse
40, // 40: headscale.v1.HeadscaleService.EnableMachineRoutes:output_type -> headscale.v1.EnableMachineRoutesResponse
41, // 41: headscale.v1.HeadscaleService.CreateApiKey:output_type -> headscale.v1.CreateApiKeyResponse
42, // 42: headscale.v1.HeadscaleService.ExpireApiKey:output_type -> headscale.v1.ExpireApiKeyResponse
43, // 43: headscale.v1.HeadscaleService.ListApiKeys:output_type -> headscale.v1.ListApiKeysResponse
22, // [22:44] is the sub-list for method output_type
0, // [0:22] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name

View File

@@ -449,6 +449,74 @@ func local_request_HeadscaleService_GetMachine_0(ctx context.Context, marshaler
}
func request_HeadscaleService_SetTags_0(ctx context.Context, marshaler runtime.Marshaler, client HeadscaleServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq SetTagsRequest
var metadata runtime.ServerMetadata
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["machine_id"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "machine_id")
}
protoReq.MachineId, err = runtime.Uint64(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "machine_id", err)
}
msg, err := client.SetTags(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_HeadscaleService_SetTags_0(ctx context.Context, marshaler runtime.Marshaler, server HeadscaleServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq SetTagsRequest
var metadata runtime.ServerMetadata
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["machine_id"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "machine_id")
}
protoReq.MachineId, err = runtime.Uint64(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "machine_id", err)
}
msg, err := server.SetTags(ctx, &protoReq)
return msg, metadata, err
}
var (
filter_HeadscaleService_RegisterMachine_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
)
@@ -589,6 +657,78 @@ func local_request_HeadscaleService_ExpireMachine_0(ctx context.Context, marshal
}
func request_HeadscaleService_RenameMachine_0(ctx context.Context, marshaler runtime.Marshaler, client HeadscaleServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq RenameMachineRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["machine_id"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "machine_id")
}
protoReq.MachineId, err = runtime.Uint64(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "machine_id", err)
}
val, ok = pathParams["new_name"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "new_name")
}
protoReq.NewName, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "new_name", err)
}
msg, err := client.RenameMachine(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_HeadscaleService_RenameMachine_0(ctx context.Context, marshaler runtime.Marshaler, server HeadscaleServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq RenameMachineRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["machine_id"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "machine_id")
}
protoReq.MachineId, err = runtime.Uint64(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "machine_id", err)
}
val, ok = pathParams["new_name"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "new_name")
}
protoReq.NewName, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "new_name", err)
}
msg, err := server.RenameMachine(ctx, &protoReq)
return msg, metadata, err
}
var (
filter_HeadscaleService_ListMachines_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
)
@@ -625,8 +765,12 @@ func local_request_HeadscaleService_ListMachines_0(ctx context.Context, marshale
}
func request_HeadscaleService_ShareMachine_0(ctx context.Context, marshaler runtime.Marshaler, client HeadscaleServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ShareMachineRequest
var (
filter_HeadscaleService_MoveMachine_0 = &utilities.DoubleArray{Encoding: map[string]int{"machine_id": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}
)
func request_HeadscaleService_MoveMachine_0(ctx context.Context, marshaler runtime.Marshaler, client HeadscaleServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq MoveMachineRequest
var metadata runtime.ServerMetadata
var (
@@ -646,23 +790,20 @@ func request_HeadscaleService_ShareMachine_0(ctx context.Context, marshaler runt
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "machine_id", err)
}
val, ok = pathParams["namespace"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "namespace")
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_HeadscaleService_MoveMachine_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
protoReq.Namespace, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "namespace", err)
}
msg, err := client.ShareMachine(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
msg, err := client.MoveMachine(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_HeadscaleService_ShareMachine_0(ctx context.Context, marshaler runtime.Marshaler, server HeadscaleServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ShareMachineRequest
func local_request_HeadscaleService_MoveMachine_0(ctx context.Context, marshaler runtime.Marshaler, server HeadscaleServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq MoveMachineRequest
var metadata runtime.ServerMetadata
var (
@@ -682,89 +823,14 @@ func local_request_HeadscaleService_ShareMachine_0(ctx context.Context, marshale
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "machine_id", err)
}
val, ok = pathParams["namespace"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "namespace")
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_HeadscaleService_MoveMachine_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
protoReq.Namespace, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "namespace", err)
}
msg, err := server.ShareMachine(ctx, &protoReq)
return msg, metadata, err
}
func request_HeadscaleService_UnshareMachine_0(ctx context.Context, marshaler runtime.Marshaler, client HeadscaleServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq UnshareMachineRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["machine_id"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "machine_id")
}
protoReq.MachineId, err = runtime.Uint64(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "machine_id", err)
}
val, ok = pathParams["namespace"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "namespace")
}
protoReq.Namespace, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "namespace", err)
}
msg, err := client.UnshareMachine(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_HeadscaleService_UnshareMachine_0(ctx context.Context, marshaler runtime.Marshaler, server HeadscaleServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq UnshareMachineRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["machine_id"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "machine_id")
}
protoReq.MachineId, err = runtime.Uint64(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "machine_id", err)
}
val, ok = pathParams["namespace"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "namespace")
}
protoReq.Namespace, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "namespace", err)
}
msg, err := server.UnshareMachine(ctx, &protoReq)
msg, err := server.MoveMachine(ctx, &protoReq)
return msg, metadata, err
}
@@ -1213,6 +1279,29 @@ func RegisterHeadscaleServiceHandlerServer(ctx context.Context, mux *runtime.Ser
})
mux.Handle("POST", pattern_HeadscaleService_SetTags_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/headscale.v1.HeadscaleService/SetTags", runtime.WithHTTPPathPattern("/api/v1/machine/{machine_id}/tags"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_HeadscaleService_SetTags_0(rctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_HeadscaleService_SetTags_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_HeadscaleService_RegisterMachine_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@@ -1282,6 +1371,29 @@ func RegisterHeadscaleServiceHandlerServer(ctx context.Context, mux *runtime.Ser
})
mux.Handle("POST", pattern_HeadscaleService_RenameMachine_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/headscale.v1.HeadscaleService/RenameMachine", runtime.WithHTTPPathPattern("/api/v1/machine/{machine_id}/rename/{new_name}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_HeadscaleService_RenameMachine_0(rctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_HeadscaleService_RenameMachine_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_HeadscaleService_ListMachines_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@@ -1305,18 +1417,18 @@ func RegisterHeadscaleServiceHandlerServer(ctx context.Context, mux *runtime.Ser
})
mux.Handle("POST", pattern_HeadscaleService_ShareMachine_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle("POST", pattern_HeadscaleService_MoveMachine_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/headscale.v1.HeadscaleService/ShareMachine", runtime.WithHTTPPathPattern("/api/v1/machine/{machine_id}/share/{namespace}"))
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/headscale.v1.HeadscaleService/MoveMachine", runtime.WithHTTPPathPattern("/api/v1/machine/{machine_id}/namespace"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_HeadscaleService_ShareMachine_0(rctx, inboundMarshaler, server, req, pathParams)
resp, md, err := local_request_HeadscaleService_MoveMachine_0(rctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
@@ -1324,30 +1436,7 @@ func RegisterHeadscaleServiceHandlerServer(ctx context.Context, mux *runtime.Ser
return
}
forward_HeadscaleService_ShareMachine_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_HeadscaleService_UnshareMachine_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/headscale.v1.HeadscaleService/UnshareMachine", runtime.WithHTTPPathPattern("/api/v1/machine/{machine_id}/unshare/{namespace}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_HeadscaleService_UnshareMachine_0(rctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_HeadscaleService_UnshareMachine_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_HeadscaleService_MoveMachine_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
@@ -1707,6 +1796,26 @@ func RegisterHeadscaleServiceHandlerClient(ctx context.Context, mux *runtime.Ser
})
mux.Handle("POST", pattern_HeadscaleService_SetTags_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req, "/headscale.v1.HeadscaleService/SetTags", runtime.WithHTTPPathPattern("/api/v1/machine/{machine_id}/tags"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_HeadscaleService_SetTags_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_HeadscaleService_SetTags_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_HeadscaleService_RegisterMachine_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@@ -1767,6 +1876,26 @@ func RegisterHeadscaleServiceHandlerClient(ctx context.Context, mux *runtime.Ser
})
mux.Handle("POST", pattern_HeadscaleService_RenameMachine_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req, "/headscale.v1.HeadscaleService/RenameMachine", runtime.WithHTTPPathPattern("/api/v1/machine/{machine_id}/rename/{new_name}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_HeadscaleService_RenameMachine_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_HeadscaleService_RenameMachine_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_HeadscaleService_ListMachines_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@@ -1787,43 +1916,23 @@ func RegisterHeadscaleServiceHandlerClient(ctx context.Context, mux *runtime.Ser
})
mux.Handle("POST", pattern_HeadscaleService_ShareMachine_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle("POST", pattern_HeadscaleService_MoveMachine_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req, "/headscale.v1.HeadscaleService/ShareMachine", runtime.WithHTTPPathPattern("/api/v1/machine/{machine_id}/share/{namespace}"))
rctx, err := runtime.AnnotateContext(ctx, mux, req, "/headscale.v1.HeadscaleService/MoveMachine", runtime.WithHTTPPathPattern("/api/v1/machine/{machine_id}/namespace"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_HeadscaleService_ShareMachine_0(rctx, inboundMarshaler, client, req, pathParams)
resp, md, err := request_HeadscaleService_MoveMachine_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_HeadscaleService_ShareMachine_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_HeadscaleService_UnshareMachine_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req, "/headscale.v1.HeadscaleService/UnshareMachine", runtime.WithHTTPPathPattern("/api/v1/machine/{machine_id}/unshare/{namespace}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_HeadscaleService_UnshareMachine_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_HeadscaleService_UnshareMachine_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_HeadscaleService_MoveMachine_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
@@ -1951,17 +2060,19 @@ var (
pattern_HeadscaleService_GetMachine_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "v1", "machine", "machine_id"}, ""))
pattern_HeadscaleService_SetTags_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4}, []string{"api", "v1", "machine", "machine_id", "tags"}, ""))
pattern_HeadscaleService_RegisterMachine_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v1", "machine", "register"}, ""))
pattern_HeadscaleService_DeleteMachine_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "v1", "machine", "machine_id"}, ""))
pattern_HeadscaleService_ExpireMachine_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4}, []string{"api", "v1", "machine", "machine_id", "expire"}, ""))
pattern_HeadscaleService_RenameMachine_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4, 1, 0, 4, 1, 5, 5}, []string{"api", "v1", "machine", "machine_id", "rename", "new_name"}, ""))
pattern_HeadscaleService_ListMachines_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "machine"}, ""))
pattern_HeadscaleService_ShareMachine_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4, 1, 0, 4, 1, 5, 5}, []string{"api", "v1", "machine", "machine_id", "share", "namespace"}, ""))
pattern_HeadscaleService_UnshareMachine_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4, 1, 0, 4, 1, 5, 5}, []string{"api", "v1", "machine", "machine_id", "unshare", "namespace"}, ""))
pattern_HeadscaleService_MoveMachine_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4}, []string{"api", "v1", "machine", "machine_id", "namespace"}, ""))
pattern_HeadscaleService_GetMachineRoute_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4}, []string{"api", "v1", "machine", "machine_id", "routes"}, ""))
@@ -1995,17 +2106,19 @@ var (
forward_HeadscaleService_GetMachine_0 = runtime.ForwardResponseMessage
forward_HeadscaleService_SetTags_0 = runtime.ForwardResponseMessage
forward_HeadscaleService_RegisterMachine_0 = runtime.ForwardResponseMessage
forward_HeadscaleService_DeleteMachine_0 = runtime.ForwardResponseMessage
forward_HeadscaleService_ExpireMachine_0 = runtime.ForwardResponseMessage
forward_HeadscaleService_RenameMachine_0 = runtime.ForwardResponseMessage
forward_HeadscaleService_ListMachines_0 = runtime.ForwardResponseMessage
forward_HeadscaleService_ShareMachine_0 = runtime.ForwardResponseMessage
forward_HeadscaleService_UnshareMachine_0 = runtime.ForwardResponseMessage
forward_HeadscaleService_MoveMachine_0 = runtime.ForwardResponseMessage
forward_HeadscaleService_GetMachineRoute_0 = runtime.ForwardResponseMessage

View File

@@ -4,7 +4,6 @@ package v1
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
@@ -32,12 +31,13 @@ type HeadscaleServiceClient interface {
// --- Machine start ---
DebugCreateMachine(ctx context.Context, in *DebugCreateMachineRequest, opts ...grpc.CallOption) (*DebugCreateMachineResponse, error)
GetMachine(ctx context.Context, in *GetMachineRequest, opts ...grpc.CallOption) (*GetMachineResponse, error)
SetTags(ctx context.Context, in *SetTagsRequest, opts ...grpc.CallOption) (*SetTagsResponse, error)
RegisterMachine(ctx context.Context, in *RegisterMachineRequest, opts ...grpc.CallOption) (*RegisterMachineResponse, error)
DeleteMachine(ctx context.Context, in *DeleteMachineRequest, opts ...grpc.CallOption) (*DeleteMachineResponse, error)
ExpireMachine(ctx context.Context, in *ExpireMachineRequest, opts ...grpc.CallOption) (*ExpireMachineResponse, error)
RenameMachine(ctx context.Context, in *RenameMachineRequest, opts ...grpc.CallOption) (*RenameMachineResponse, error)
ListMachines(ctx context.Context, in *ListMachinesRequest, opts ...grpc.CallOption) (*ListMachinesResponse, error)
ShareMachine(ctx context.Context, in *ShareMachineRequest, opts ...grpc.CallOption) (*ShareMachineResponse, error)
UnshareMachine(ctx context.Context, in *UnshareMachineRequest, opts ...grpc.CallOption) (*UnshareMachineResponse, error)
MoveMachine(ctx context.Context, in *MoveMachineRequest, opts ...grpc.CallOption) (*MoveMachineResponse, error)
// --- Route start ---
GetMachineRoute(ctx context.Context, in *GetMachineRouteRequest, opts ...grpc.CallOption) (*GetMachineRouteResponse, error)
EnableMachineRoutes(ctx context.Context, in *EnableMachineRoutesRequest, opts ...grpc.CallOption) (*EnableMachineRoutesResponse, error)
@@ -145,6 +145,15 @@ func (c *headscaleServiceClient) GetMachine(ctx context.Context, in *GetMachineR
return out, nil
}
func (c *headscaleServiceClient) SetTags(ctx context.Context, in *SetTagsRequest, opts ...grpc.CallOption) (*SetTagsResponse, error) {
out := new(SetTagsResponse)
err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/SetTags", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *headscaleServiceClient) RegisterMachine(ctx context.Context, in *RegisterMachineRequest, opts ...grpc.CallOption) (*RegisterMachineResponse, error) {
out := new(RegisterMachineResponse)
err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/RegisterMachine", in, out, opts...)
@@ -172,6 +181,15 @@ func (c *headscaleServiceClient) ExpireMachine(ctx context.Context, in *ExpireMa
return out, nil
}
func (c *headscaleServiceClient) RenameMachine(ctx context.Context, in *RenameMachineRequest, opts ...grpc.CallOption) (*RenameMachineResponse, error) {
out := new(RenameMachineResponse)
err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/RenameMachine", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *headscaleServiceClient) ListMachines(ctx context.Context, in *ListMachinesRequest, opts ...grpc.CallOption) (*ListMachinesResponse, error) {
out := new(ListMachinesResponse)
err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ListMachines", in, out, opts...)
@@ -181,18 +199,9 @@ func (c *headscaleServiceClient) ListMachines(ctx context.Context, in *ListMachi
return out, nil
}
func (c *headscaleServiceClient) ShareMachine(ctx context.Context, in *ShareMachineRequest, opts ...grpc.CallOption) (*ShareMachineResponse, error) {
out := new(ShareMachineResponse)
err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ShareMachine", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *headscaleServiceClient) UnshareMachine(ctx context.Context, in *UnshareMachineRequest, opts ...grpc.CallOption) (*UnshareMachineResponse, error) {
out := new(UnshareMachineResponse)
err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/UnshareMachine", in, out, opts...)
func (c *headscaleServiceClient) MoveMachine(ctx context.Context, in *MoveMachineRequest, opts ...grpc.CallOption) (*MoveMachineResponse, error) {
out := new(MoveMachineResponse)
err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/MoveMachine", in, out, opts...)
if err != nil {
return nil, err
}
@@ -261,12 +270,13 @@ type HeadscaleServiceServer interface {
// --- Machine start ---
DebugCreateMachine(context.Context, *DebugCreateMachineRequest) (*DebugCreateMachineResponse, error)
GetMachine(context.Context, *GetMachineRequest) (*GetMachineResponse, error)
SetTags(context.Context, *SetTagsRequest) (*SetTagsResponse, error)
RegisterMachine(context.Context, *RegisterMachineRequest) (*RegisterMachineResponse, error)
DeleteMachine(context.Context, *DeleteMachineRequest) (*DeleteMachineResponse, error)
ExpireMachine(context.Context, *ExpireMachineRequest) (*ExpireMachineResponse, error)
RenameMachine(context.Context, *RenameMachineRequest) (*RenameMachineResponse, error)
ListMachines(context.Context, *ListMachinesRequest) (*ListMachinesResponse, error)
ShareMachine(context.Context, *ShareMachineRequest) (*ShareMachineResponse, error)
UnshareMachine(context.Context, *UnshareMachineRequest) (*UnshareMachineResponse, error)
MoveMachine(context.Context, *MoveMachineRequest) (*MoveMachineResponse, error)
// --- Route start ---
GetMachineRoute(context.Context, *GetMachineRouteRequest) (*GetMachineRouteResponse, error)
EnableMachineRoutes(context.Context, *EnableMachineRoutesRequest) (*EnableMachineRoutesResponse, error)
@@ -311,6 +321,9 @@ func (UnimplementedHeadscaleServiceServer) DebugCreateMachine(context.Context, *
func (UnimplementedHeadscaleServiceServer) GetMachine(context.Context, *GetMachineRequest) (*GetMachineResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetMachine not implemented")
}
func (UnimplementedHeadscaleServiceServer) SetTags(context.Context, *SetTagsRequest) (*SetTagsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetTags not implemented")
}
func (UnimplementedHeadscaleServiceServer) RegisterMachine(context.Context, *RegisterMachineRequest) (*RegisterMachineResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method RegisterMachine not implemented")
}
@@ -320,14 +333,14 @@ func (UnimplementedHeadscaleServiceServer) DeleteMachine(context.Context, *Delet
func (UnimplementedHeadscaleServiceServer) ExpireMachine(context.Context, *ExpireMachineRequest) (*ExpireMachineResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ExpireMachine not implemented")
}
func (UnimplementedHeadscaleServiceServer) RenameMachine(context.Context, *RenameMachineRequest) (*RenameMachineResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method RenameMachine not implemented")
}
func (UnimplementedHeadscaleServiceServer) ListMachines(context.Context, *ListMachinesRequest) (*ListMachinesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListMachines not implemented")
}
func (UnimplementedHeadscaleServiceServer) ShareMachine(context.Context, *ShareMachineRequest) (*ShareMachineResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ShareMachine not implemented")
}
func (UnimplementedHeadscaleServiceServer) UnshareMachine(context.Context, *UnshareMachineRequest) (*UnshareMachineResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method UnshareMachine not implemented")
func (UnimplementedHeadscaleServiceServer) MoveMachine(context.Context, *MoveMachineRequest) (*MoveMachineResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method MoveMachine not implemented")
}
func (UnimplementedHeadscaleServiceServer) GetMachineRoute(context.Context, *GetMachineRouteRequest) (*GetMachineRouteResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetMachineRoute not implemented")
@@ -537,6 +550,24 @@ func _HeadscaleService_GetMachine_Handler(srv interface{}, ctx context.Context,
return interceptor(ctx, in, info, handler)
}
func _HeadscaleService_SetTags_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SetTagsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HeadscaleServiceServer).SetTags(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/headscale.v1.HeadscaleService/SetTags",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HeadscaleServiceServer).SetTags(ctx, req.(*SetTagsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _HeadscaleService_RegisterMachine_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RegisterMachineRequest)
if err := dec(in); err != nil {
@@ -591,6 +622,24 @@ func _HeadscaleService_ExpireMachine_Handler(srv interface{}, ctx context.Contex
return interceptor(ctx, in, info, handler)
}
func _HeadscaleService_RenameMachine_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RenameMachineRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HeadscaleServiceServer).RenameMachine(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/headscale.v1.HeadscaleService/RenameMachine",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HeadscaleServiceServer).RenameMachine(ctx, req.(*RenameMachineRequest))
}
return interceptor(ctx, in, info, handler)
}
func _HeadscaleService_ListMachines_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListMachinesRequest)
if err := dec(in); err != nil {
@@ -609,38 +658,20 @@ func _HeadscaleService_ListMachines_Handler(srv interface{}, ctx context.Context
return interceptor(ctx, in, info, handler)
}
func _HeadscaleService_ShareMachine_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ShareMachineRequest)
func _HeadscaleService_MoveMachine_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(MoveMachineRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HeadscaleServiceServer).ShareMachine(ctx, in)
return srv.(HeadscaleServiceServer).MoveMachine(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/headscale.v1.HeadscaleService/ShareMachine",
FullMethod: "/headscale.v1.HeadscaleService/MoveMachine",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HeadscaleServiceServer).ShareMachine(ctx, req.(*ShareMachineRequest))
}
return interceptor(ctx, in, info, handler)
}
func _HeadscaleService_UnshareMachine_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UnshareMachineRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HeadscaleServiceServer).UnshareMachine(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/headscale.v1.HeadscaleService/UnshareMachine",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HeadscaleServiceServer).UnshareMachine(ctx, req.(*UnshareMachineRequest))
return srv.(HeadscaleServiceServer).MoveMachine(ctx, req.(*MoveMachineRequest))
}
return interceptor(ctx, in, info, handler)
}
@@ -782,6 +813,10 @@ var HeadscaleService_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetMachine",
Handler: _HeadscaleService_GetMachine_Handler,
},
{
MethodName: "SetTags",
Handler: _HeadscaleService_SetTags_Handler,
},
{
MethodName: "RegisterMachine",
Handler: _HeadscaleService_RegisterMachine_Handler,
@@ -794,17 +829,17 @@ var HeadscaleService_ServiceDesc = grpc.ServiceDesc{
MethodName: "ExpireMachine",
Handler: _HeadscaleService_ExpireMachine_Handler,
},
{
MethodName: "RenameMachine",
Handler: _HeadscaleService_RenameMachine_Handler,
},
{
MethodName: "ListMachines",
Handler: _HeadscaleService_ListMachines_Handler,
},
{
MethodName: "ShareMachine",
Handler: _HeadscaleService_ShareMachine_Handler,
},
{
MethodName: "UnshareMachine",
Handler: _HeadscaleService_UnshareMachine_Handler,
MethodName: "MoveMachine",
Handler: _HeadscaleService_MoveMachine_Handler,
},
{
MethodName: "GetMachineRoute",

File diff suppressed because it is too large Load Diff

View File

@@ -7,12 +7,11 @@
package v1
import (
reflect "reflect"
sync "sync"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
)
const (

View File

@@ -7,12 +7,11 @@
package v1
import (
reflect "reflect"
sync "sync"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
)
const (

View File

@@ -7,11 +7,10 @@
package v1
import (
reflect "reflect"
sync "sync"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (

View File

@@ -181,6 +181,20 @@
}
}
},
"parameters": [
{
"name": "namespace",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "key",
"in": "query",
"required": false,
"type": "string"
}
],
"tags": [
"HeadscaleService"
]
@@ -277,6 +291,80 @@
]
}
},
"/api/v1/machine/{machineId}/namespace": {
"post": {
"operationId": "HeadscaleService_MoveMachine",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1MoveMachineResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "machineId",
"in": "path",
"required": true,
"type": "string",
"format": "uint64"
},
{
"name": "namespace",
"in": "query",
"required": false,
"type": "string"
}
],
"tags": [
"HeadscaleService"
]
}
},
"/api/v1/machine/{machineId}/rename/{newName}": {
"post": {
"operationId": "HeadscaleService_RenameMachine",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1RenameMachineResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "machineId",
"in": "path",
"required": true,
"type": "string",
"format": "uint64"
},
{
"name": "newName",
"in": "path",
"required": true,
"type": "string"
}
],
"tags": [
"HeadscaleService"
]
}
},
"/api/v1/machine/{machineId}/routes": {
"get": {
"summary": "--- Route start ---",
@@ -331,6 +419,16 @@
"required": true,
"type": "string",
"format": "uint64"
},
{
"name": "routes",
"in": "query",
"required": false,
"type": "array",
"items": {
"type": "string"
},
"collectionFormat": "multi"
}
],
"tags": [
@@ -338,14 +436,14 @@
]
}
},
"/api/v1/machine/{machineId}/share/{namespace}": {
"/api/v1/machine/{machineId}/tags": {
"post": {
"operationId": "HeadscaleService_ShareMachine",
"operationId": "HeadscaleService_SetTags",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1ShareMachineResponse"
"$ref": "#/definitions/v1SetTagsResponse"
}
},
"default": {
@@ -364,47 +462,20 @@
"format": "uint64"
},
{
"name": "namespace",
"in": "path",
"name": "body",
"in": "body",
"required": true,
"type": "string"
}
],
"tags": [
"HeadscaleService"
]
}
},
"/api/v1/machine/{machineId}/unshare/{namespace}": {
"post": {
"operationId": "HeadscaleService_UnshareMachine",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1UnshareMachineResponse"
"type": "object",
"properties": {
"tags": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "machineId",
"in": "path",
"required": true,
"type": "string",
"format": "uint64"
},
{
"name": "namespace",
"in": "path",
"required": true,
"type": "string"
}
],
"tags": [
@@ -935,12 +1006,6 @@
"namespace": {
"$ref": "#/definitions/v1Namespace"
},
"registered": {
"type": "boolean"
},
"registerMethod": {
"$ref": "#/definitions/v1RegisterMethod"
},
"lastSeen": {
"type": "string",
"format": "date-time"
@@ -959,6 +1024,38 @@
"createdAt": {
"type": "string",
"format": "date-time"
},
"registerMethod": {
"$ref": "#/definitions/v1RegisterMethod"
},
"forcedTags": {
"type": "array",
"items": {
"type": "string"
}
},
"invalidTags": {
"type": "array",
"items": {
"type": "string"
}
},
"validTags": {
"type": "array",
"items": {
"type": "string"
}
},
"givenName": {
"type": "string"
}
}
},
"v1MoveMachineResponse": {
"type": "object",
"properties": {
"machine": {
"$ref": "#/definitions/v1Machine"
}
}
},
@@ -1026,6 +1123,14 @@
],
"default": "REGISTER_METHOD_UNSPECIFIED"
},
"v1RenameMachineResponse": {
"type": "object",
"properties": {
"machine": {
"$ref": "#/definitions/v1Machine"
}
}
},
"v1RenameNamespaceResponse": {
"type": "object",
"properties": {
@@ -1051,15 +1156,7 @@
}
}
},
"v1ShareMachineResponse": {
"type": "object",
"properties": {
"machine": {
"$ref": "#/definitions/v1Machine"
}
}
},
"v1UnshareMachineResponse": {
"v1SetTagsResponse": {
"type": "object",
"properties": {
"machine": {

135
go.mod
View File

@@ -1,71 +1,68 @@
module github.com/juanfont/headscale
go 1.17
go 1.18
require (
github.com/AlecAivazis/survey/v2 v2.3.2
github.com/AlecAivazis/survey/v2 v2.3.4
github.com/ccding/go-stun/stun v0.0.0-20200514191101-4dc67bcdb029
github.com/coreos/go-oidc/v3 v3.1.0
github.com/deckarep/golang-set/v2 v2.1.0
github.com/efekarakus/termcolor v1.0.1
github.com/fatih/set v0.2.1
github.com/gin-gonic/gin v1.7.7
github.com/glebarez/sqlite v1.3.5
github.com/glebarez/sqlite v1.4.3
github.com/gofrs/uuid v4.2.0+incompatible
github.com/gorilla/mux v1.8.0
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.3
github.com/infobloxopen/protoc-gen-gorm v1.1.0
github.com/klauspost/compress v1.14.2
github.com/ory/dockertest/v3 v3.8.1
github.com/grpc-ecosystem/grpc-gateway/v2 v2.10.0
github.com/klauspost/compress v1.15.4
github.com/ory/dockertest/v3 v3.9.1
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/philip-bui/grpc-zerolog v1.0.1
github.com/prometheus/client_golang v1.12.1
github.com/pterm/pterm v0.12.36
github.com/prometheus/common v0.32.1
github.com/pterm/pterm v0.12.41
github.com/puzpuzpuz/xsync v1.2.1
github.com/rs/zerolog v1.26.1
github.com/spf13/cobra v1.3.0
github.com/spf13/viper v1.10.1
github.com/stretchr/testify v1.7.0
github.com/tailscale/hujson v0.0.0-20211215203138-ffd971c5f362
github.com/spf13/cobra v1.4.0
github.com/spf13/viper v1.11.0
github.com/stretchr/testify v1.7.1
github.com/tailscale/hujson v0.0.0-20220506202205-92b4b88a9e17
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
github.com/zsais/go-gin-prometheus v0.1.0
golang.org/x/crypto v0.0.0-20220214200702-86341886e292
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
google.golang.org/genproto v0.0.0-20220210181026-6fee9acbd336
google.golang.org/grpc v1.44.0
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0
google.golang.org/protobuf v1.27.1
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29
google.golang.org/genproto v0.0.0-20220422154200-b37d22cd5731
google.golang.org/grpc v1.46.0
google.golang.org/protobuf v1.28.0
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
gopkg.in/yaml.v2 v2.4.0
gorm.io/datatypes v1.0.5
gorm.io/driver/postgres v1.3.1
gorm.io/gorm v1.23.1
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
gorm.io/driver/postgres v1.3.5
gorm.io/gorm v1.23.4
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6
tailscale.com v1.20.4
tailscale.com v1.26.0
)
require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.5.1 // indirect
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/akutz/memconn v0.1.0 // indirect
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect
github.com/atomicgo/cursor v0.0.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.1.2 // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/containerd/continuity v0.2.2 // indirect
github.com/containerd/continuity v0.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/cli v20.10.12+incompatible // indirect
github.com/docker/docker v20.10.12+incompatible // indirect
github.com/docker/cli v20.10.16+incompatible // indirect
github.com/docker/docker v20.10.16+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.14.7 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.10.0 // indirect
github.com/go-sql-driver/mysql v1.6.0 // indirect
github.com/glebarez/go-sqlite v1.16.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/glog v1.0.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.8 // indirect
github.com/google/go-github v17.0.0+incompatible // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
@@ -76,73 +73,67 @@ require (
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.11.0 // indirect
github.com/jackc/pgconn v1.12.0 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.2.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
github.com/jackc/pgtype v1.10.0 // indirect
github.com/jackc/pgx/v4 v4.15.0 // indirect
github.com/jinzhu/gorm v1.9.16 // indirect
github.com/jackc/pgtype v1.11.0 // indirect
github.com/jackc/pgx/v4 v4.16.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.4 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/josharian/native v1.0.0 // indirect
github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/lib/pq v1.10.3 // indirect
github.com/magiconair/properties v1.8.5 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mattn/go-sqlite3 v1.14.11 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mdlayher/netlink v1.6.0 // indirect
github.com/mdlayher/socket v0.2.3 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/opencontainers/runc v1.1.0 // indirect
github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 // indirect
github.com/opencontainers/runc v1.1.2 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/pelletier/go-toml/v2 v2.0.0-beta.8 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.8.1 // indirect
github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/spf13/afero v1.8.1 // indirect
github.com/spf13/afero v1.8.2 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/ugorji/go/codec v1.2.6 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
go4.org/mem v0.0.0-20210711025021-927187094b94 // indirect
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 // indirect
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/net v0.0.0-20220516155154-20f960328961 // indirect
golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a // indirect
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
golang.zx2c4.com/wireguard/windows v0.4.10 // indirect
google.golang.org/appengine v1.6.7 // indirect
gopkg.in/ini.v1 v1.66.4 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
gorm.io/driver/mysql v1.3.2 // indirect
gorm.io/driver/sqlite v1.3.1 // indirect
gorm.io/driver/sqlserver v1.3.1 // indirect
modernc.org/libc v1.14.3 // indirect
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
modernc.org/libc v1.14.12 // indirect
modernc.org/mathutil v1.4.1 // indirect
modernc.org/memory v1.0.5 // indirect
modernc.org/sqlite v1.14.5 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
modernc.org/memory v1.0.7 // indirect
modernc.org/sqlite v1.16.0 // indirect
)

872
go.sum

File diff suppressed because it is too large Load Diff

174
grpcv1.go
View File

@@ -3,12 +3,14 @@ package headscale
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/juanfont/headscale/gen/go/headscale/v1"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/rs/zerolog/log"
"gorm.io/datatypes"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"tailscale.com/tailcfg"
)
@@ -159,9 +161,11 @@ func (api headscaleV1APIServer) RegisterMachine(
Str("namespace", request.GetNamespace()).
Str("machine_key", request.GetKey()).
Msg("Registering machine")
machine, err := api.h.RegisterMachine(
machine, err := api.h.RegisterMachineFromAuthCallback(
request.GetKey(),
request.GetNamespace(),
RegisterMethodCLI,
)
if err != nil {
return nil, err
@@ -182,6 +186,52 @@ func (api headscaleV1APIServer) GetMachine(
return &v1.GetMachineResponse{Machine: machine.toProto()}, nil
}
func (api headscaleV1APIServer) SetTags(
ctx context.Context,
request *v1.SetTagsRequest,
) (*v1.SetTagsResponse, error) {
machine, err := api.h.GetMachineByID(request.GetMachineId())
if err != nil {
return nil, err
}
for _, tag := range request.GetTags() {
err := validateTag(tag)
if err != nil {
return &v1.SetTagsResponse{
Machine: nil,
}, status.Error(codes.InvalidArgument, err.Error())
}
}
err = api.h.SetTags(machine, request.GetTags())
if err != nil {
return &v1.SetTagsResponse{
Machine: nil,
}, status.Error(codes.Internal, err.Error())
}
log.Trace().
Str("machine", machine.Hostname).
Strs("tags", request.GetTags()).
Msg("Changing tags of machine")
return &v1.SetTagsResponse{Machine: machine.toProto()}, nil
}
func validateTag(tag string) error {
if strings.Index(tag, "tag:") != 0 {
return fmt.Errorf("tag must start with the string 'tag:'")
}
if strings.ToLower(tag) != tag {
return fmt.Errorf("tag should be lowercase")
}
if len(strings.Fields(tag)) > 1 {
return fmt.Errorf("tag should not contains space")
}
return nil
}
func (api headscaleV1APIServer) DeleteMachine(
ctx context.Context,
request *v1.DeleteMachineRequest,
@@ -215,13 +265,38 @@ func (api headscaleV1APIServer) ExpireMachine(
)
log.Trace().
Str("machine", machine.Name).
Str("machine", machine.Hostname).
Time("expiry", *machine.Expiry).
Msg("machine expired")
return &v1.ExpireMachineResponse{Machine: machine.toProto()}, nil
}
func (api headscaleV1APIServer) RenameMachine(
ctx context.Context,
request *v1.RenameMachineRequest,
) (*v1.RenameMachineResponse, error) {
machine, err := api.h.GetMachineByID(request.GetMachineId())
if err != nil {
return nil, err
}
err = api.h.RenameMachine(
machine,
request.GetNewName(),
)
if err != nil {
return nil, err
}
log.Trace().
Str("machine", machine.Hostname).
Str("new_name", request.GetNewName()).
Msg("machine renamed")
return &v1.RenameMachineResponse{Machine: machine.toProto()}, nil
}
func (api headscaleV1APIServer) ListMachines(
ctx context.Context,
request *v1.ListMachinesRequest,
@@ -232,15 +307,6 @@ func (api headscaleV1APIServer) ListMachines(
return nil, err
}
sharedMachines, err := api.h.ListSharedMachinesInNamespace(
request.GetNamespace(),
)
if err != nil {
return nil, err
}
machines = append(machines, sharedMachines...)
response := make([]*v1.Machine, len(machines))
for index, machine := range machines {
response[index] = machine.toProto()
@@ -256,54 +322,35 @@ func (api headscaleV1APIServer) ListMachines(
response := make([]*v1.Machine, len(machines))
for index, machine := range machines {
response[index] = machine.toProto()
m := machine.toProto()
validTags, invalidTags := getTags(
api.h.aclPolicy,
machine,
api.h.cfg.OIDC.StripEmaildomain,
)
m.InvalidTags = invalidTags
m.ValidTags = validTags
response[index] = m
}
return &v1.ListMachinesResponse{Machines: response}, nil
}
func (api headscaleV1APIServer) ShareMachine(
func (api headscaleV1APIServer) MoveMachine(
ctx context.Context,
request *v1.ShareMachineRequest,
) (*v1.ShareMachineResponse, error) {
destinationNamespace, err := api.h.GetNamespace(request.GetNamespace())
if err != nil {
return nil, err
}
request *v1.MoveMachineRequest,
) (*v1.MoveMachineResponse, error) {
machine, err := api.h.GetMachineByID(request.GetMachineId())
if err != nil {
return nil, err
}
err = api.h.AddSharedMachineToNamespace(machine, destinationNamespace)
err = api.h.SetMachineNamespace(machine, request.GetNamespace())
if err != nil {
return nil, err
}
return &v1.ShareMachineResponse{Machine: machine.toProto()}, nil
}
func (api headscaleV1APIServer) UnshareMachine(
ctx context.Context,
request *v1.UnshareMachineRequest,
) (*v1.UnshareMachineResponse, error) {
destinationNamespace, err := api.h.GetNamespace(request.GetNamespace())
if err != nil {
return nil, err
}
machine, err := api.h.GetMachineByID(request.GetMachineId())
if err != nil {
return nil, err
}
err = api.h.RemoveSharedMachineFromNamespace(machine, destinationNamespace)
if err != nil {
return nil, err
}
return &v1.UnshareMachineResponse{Machine: machine.toProto()}, nil
return &v1.MoveMachineResponse{Machine: machine.toProto()}, nil
}
func (api headscaleV1APIServer) GetMachineRoute(
@@ -315,13 +362,8 @@ func (api headscaleV1APIServer) GetMachineRoute(
return nil, err
}
routes, err := machine.RoutesToProto()
if err != nil {
return nil, err
}
return &v1.GetMachineRouteResponse{
Routes: routes,
Routes: machine.RoutesToProto(),
}, nil
}
@@ -339,13 +381,8 @@ func (api headscaleV1APIServer) EnableMachineRoutes(
return nil, err
}
routes, err := machine.RoutesToProto()
if err != nil {
return nil, err
}
return &v1.EnableMachineRoutesResponse{
Routes: routes,
Routes: machine.RoutesToProto(),
}, nil
}
@@ -432,30 +469,29 @@ func (api headscaleV1APIServer) DebugCreateMachine(
Hostname: "DebugTestMachine",
}
log.Trace().Caller().Interface("hostinfo", hostinfo).Msg("")
hostinfoJson, err := json.Marshal(hostinfo)
givenName, err := api.h.GenerateGivenName(request.GetName())
if err != nil {
return nil, err
}
newMachine := Machine{
MachineKey: request.GetKey(),
Name: request.GetName(),
Hostname: request.GetName(),
GivenName: givenName,
Namespace: *namespace,
Expiry: &time.Time{},
LastSeen: &time.Time{},
LastSuccessfulUpdate: &time.Time{},
HostInfo: datatypes.JSON(hostinfoJson),
HostInfo: HostInfo(hostinfo),
}
// log.Trace().Caller().Interface("machine", newMachine).Msg("")
if err := api.h.db.Create(&newMachine).Error; err != nil {
return nil, err
}
api.h.registrationCache.Set(
request.GetKey(),
newMachine,
registerCacheExpiration,
)
return &v1.DebugCreateMachineResponse{Machine: newMachine.toProto()}, nil
}

42
grpcv1_test.go Normal file
View File

@@ -0,0 +1,42 @@
package headscale
import "testing"
func Test_validateTag(t *testing.T) {
type args struct {
tag string
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "valid tag",
args: args{tag: "tag:test"},
wantErr: false,
},
{
name: "tag without tag prefix",
args: args{tag: "test"},
wantErr: true,
},
{
name: "uppercase tag",
args: args{tag: "tag:tEST"},
wantErr: true,
},
{
name: "tag that contains space",
args: args{tag: "tag:this is a spaced tag"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := validateTag(tt.args.tag); (err != nil) != tt.wantErr {
t.Errorf("validateTag() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View File

@@ -40,27 +40,27 @@ func (s *IntegrationCLITestSuite) SetupTest() {
if ppool, err := dockertest.NewPool(""); err == nil {
s.pool = *ppool
} 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 {
s.network = *pnetwork
} else {
log.Fatalf("Could not create network: %s", err)
s.FailNow(fmt.Sprintf("Could not create network: %s", err), "")
}
headscaleBuildOptions := &dockertest.BuildOptions{
Dockerfile: "Dockerfile",
Dockerfile: "Dockerfile.tmp-integration",
ContextDir: ".",
}
currentPath, err := os.Getwd()
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{
Name: "headscale",
Name: "headscale-cli",
Mounts: []string{
fmt.Sprintf("%s/integration_test/etc:/etc/headscale", currentPath),
},
@@ -68,11 +68,16 @@ func (s *IntegrationCLITestSuite) SetupTest() {
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")
if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
s.headscale = *pheadscale
} else {
log.Fatalf("Could not start resource: %s", err)
s.FailNow(fmt.Sprintf("Could not start headscale container: %s", err), "")
}
fmt.Println("Created headscale container")
@@ -525,11 +530,140 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommandReusableEphemeral() {
assert.Len(s.T(), listedPreAuthKeys, 2)
}
func (s *IntegrationCLITestSuite) TestNodeTagCommand() {
namespace, err := s.createNamespace("machine-namespace")
assert.Nil(s.T(), err)
machineKeys := []string{
"9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
"6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
}
machines := make([]*v1.Machine, len(machineKeys))
assert.Nil(s.T(), err)
for index, machineKey := range machineKeys {
_, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"debug",
"create-node",
"--name",
fmt.Sprintf("machine-%d", index+1),
"--namespace",
namespace.Name,
"--key",
machineKey,
"--output",
"json",
},
[]string{},
)
assert.Nil(s.T(), err)
machineResult, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"nodes",
"--namespace",
namespace.Name,
"register",
"--key",
machineKey,
"--output",
"json",
},
[]string{},
)
assert.Nil(s.T(), err)
var machine v1.Machine
err = json.Unmarshal([]byte(machineResult), &machine)
assert.Nil(s.T(), err)
machines[index] = &machine
}
assert.Len(s.T(), machines, len(machineKeys))
addTagResult, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"nodes",
"tag",
"-i", "1",
"-t", "tag:test",
"--output", "json",
},
[]string{},
)
assert.Nil(s.T(), err)
var machine v1.Machine
err = json.Unmarshal([]byte(addTagResult), &machine)
assert.Nil(s.T(), err)
assert.Equal(s.T(), []string{"tag:test"}, machine.ForcedTags)
// try to set a wrong tag and retrieve the error
wrongTagResult, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"nodes",
"tag",
"-i", "2",
"-t", "wrong-tag",
"--output", "json",
},
[]string{},
)
assert.Nil(s.T(), err)
type errOutput struct {
Error string `json:"error"`
}
var errorOutput errOutput
err = json.Unmarshal([]byte(wrongTagResult), &errorOutput)
assert.Nil(s.T(), err)
assert.Contains(s.T(), errorOutput.Error, "tag must start with the string 'tag:'")
// Test list all nodes after added seconds
listAllResult, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"nodes",
"list",
"--output", "json",
},
[]string{},
)
resultMachines := make([]*v1.Machine, len(machineKeys))
assert.Nil(s.T(), err)
json.Unmarshal([]byte(listAllResult), &resultMachines)
found := false
for _, machine := range resultMachines {
if machine.ForcedTags != nil {
for _, tag := range machine.ForcedTags {
if tag == "tag:test" {
found = true
}
}
}
}
assert.Equal(
s.T(),
true,
found,
"should find a machine with the tag 'tag:test' in the list of machines",
)
}
func (s *IntegrationCLITestSuite) TestNodeCommand() {
namespace, err := s.createNamespace("machine-namespace")
assert.Nil(s.T(), err)
sharedNamespace, err := s.createNamespace("shared-namespace")
secondNamespace, err := s.createNamespace("other-namespace")
assert.Nil(s.T(), err)
// Randomly generated machine keys
@@ -589,7 +723,7 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
assert.Len(s.T(), machines, len(machineKeys))
// Test list all nodes after added shared
// Test list all nodes after added seconds
listAllResult, err := ExecuteCommand(
&s.headscale,
[]string{
@@ -621,20 +755,14 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
assert.Equal(s.T(), "machine-4", listAll[3].Name)
assert.Equal(s.T(), "machine-5", listAll[4].Name)
assert.True(s.T(), listAll[0].Registered)
assert.True(s.T(), listAll[1].Registered)
assert.True(s.T(), listAll[2].Registered)
assert.True(s.T(), listAll[3].Registered)
assert.True(s.T(), listAll[4].Registered)
sharedMachineKeys := []string{
otherNamespaceMachineKeys := []string{
"b5b444774186d4217adcec407563a1223929465ee2c68a4da13af0d0185b4f8e",
"dc721977ac7415aafa87f7d4574cbe07c6b171834a6d37375782bdc1fb6b3584",
}
sharedMachines := make([]*v1.Machine, len(sharedMachineKeys))
otherNamespaceMachines := make([]*v1.Machine, len(otherNamespaceMachineKeys))
assert.Nil(s.T(), err)
for index, machineKey := range sharedMachineKeys {
for index, machineKey := range otherNamespaceMachineKeys {
_, err := ExecuteCommand(
&s.headscale,
[]string{
@@ -642,9 +770,9 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
"debug",
"create-node",
"--name",
fmt.Sprintf("shared-machine-%d", index+1),
fmt.Sprintf("otherNamespace-machine-%d", index+1),
"--namespace",
sharedNamespace.Name,
secondNamespace.Name,
"--key",
machineKey,
"--output",
@@ -660,7 +788,7 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
"headscale",
"nodes",
"--namespace",
sharedNamespace.Name,
secondNamespace.Name,
"register",
"--key",
machineKey,
@@ -675,13 +803,13 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
err = json.Unmarshal([]byte(machineResult), &machine)
assert.Nil(s.T(), err)
sharedMachines[index] = &machine
otherNamespaceMachines[index] = &machine
}
assert.Len(s.T(), sharedMachines, len(sharedMachineKeys))
assert.Len(s.T(), otherNamespaceMachines, len(otherNamespaceMachineKeys))
// Test list all nodes after added shared
listAllWithSharedResult, err := ExecuteCommand(
// Test list all nodes after added otherNamespace
listAllWithotherNamespaceResult, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
@@ -694,31 +822,31 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
)
assert.Nil(s.T(), err)
var listAllWithShared []v1.Machine
err = json.Unmarshal([]byte(listAllWithSharedResult), &listAllWithShared)
var listAllWithotherNamespace []v1.Machine
err = json.Unmarshal(
[]byte(listAllWithotherNamespaceResult),
&listAllWithotherNamespace,
)
assert.Nil(s.T(), err)
// All nodes, machines + shared
assert.Len(s.T(), listAllWithShared, 7)
// All nodes, machines + otherNamespace
assert.Len(s.T(), listAllWithotherNamespace, 7)
assert.Equal(s.T(), uint64(6), listAllWithShared[5].Id)
assert.Equal(s.T(), uint64(7), listAllWithShared[6].Id)
assert.Equal(s.T(), uint64(6), listAllWithotherNamespace[5].Id)
assert.Equal(s.T(), uint64(7), listAllWithotherNamespace[6].Id)
assert.Equal(s.T(), "shared-machine-1", listAllWithShared[5].Name)
assert.Equal(s.T(), "shared-machine-2", listAllWithShared[6].Name)
assert.Equal(s.T(), "otherNamespace-machine-1", listAllWithotherNamespace[5].Name)
assert.Equal(s.T(), "otherNamespace-machine-2", listAllWithotherNamespace[6].Name)
assert.True(s.T(), listAllWithShared[5].Registered)
assert.True(s.T(), listAllWithShared[6].Registered)
// Test list all nodes after added shared
listOnlySharedMachineNamespaceResult, err := ExecuteCommand(
// Test list all nodes after added otherNamespace
listOnlyotherNamespaceMachineNamespaceResult, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"nodes",
"list",
"--namespace",
sharedNamespace.Name,
secondNamespace.Name,
"--output",
"json",
},
@@ -726,23 +854,28 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
)
assert.Nil(s.T(), err)
var listOnlySharedMachineNamespace []v1.Machine
var listOnlyotherNamespaceMachineNamespace []v1.Machine
err = json.Unmarshal(
[]byte(listOnlySharedMachineNamespaceResult),
&listOnlySharedMachineNamespace,
[]byte(listOnlyotherNamespaceMachineNamespaceResult),
&listOnlyotherNamespaceMachineNamespace,
)
assert.Nil(s.T(), err)
assert.Len(s.T(), listOnlySharedMachineNamespace, 2)
assert.Len(s.T(), listOnlyotherNamespaceMachineNamespace, 2)
assert.Equal(s.T(), uint64(6), listOnlySharedMachineNamespace[0].Id)
assert.Equal(s.T(), uint64(7), listOnlySharedMachineNamespace[1].Id)
assert.Equal(s.T(), uint64(6), listOnlyotherNamespaceMachineNamespace[0].Id)
assert.Equal(s.T(), uint64(7), listOnlyotherNamespaceMachineNamespace[1].Id)
assert.Equal(s.T(), "shared-machine-1", listOnlySharedMachineNamespace[0].Name)
assert.Equal(s.T(), "shared-machine-2", listOnlySharedMachineNamespace[1].Name)
assert.True(s.T(), listOnlySharedMachineNamespace[0].Registered)
assert.True(s.T(), listOnlySharedMachineNamespace[1].Registered)
assert.Equal(
s.T(),
"otherNamespace-machine-1",
listOnlyotherNamespaceMachineNamespace[0].Name,
)
assert.Equal(
s.T(),
"otherNamespace-machine-2",
listOnlyotherNamespaceMachineNamespace[1].Name,
)
// Delete a machines
_, err = ExecuteCommand(
@@ -786,120 +919,6 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
assert.Nil(s.T(), err)
assert.Len(s.T(), listOnlyMachineNamespaceAfterDelete, 4)
// test: share node
shareMachineResult, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"nodes",
"share",
"--namespace",
namespace.Name,
"--identifier",
"7",
"--output",
"json",
},
[]string{},
)
assert.Nil(s.T(), err)
var shareMachine v1.Machine
err = json.Unmarshal([]byte(shareMachineResult), &shareMachine)
assert.Nil(s.T(), err)
assert.Equal(s.T(), uint64(7), shareMachine.Id)
assert.Equal(s.T(), "shared-machine-2", shareMachine.Name)
assert.True(s.T(), shareMachine.Registered)
// Test: list main namespace after machine has been shared
listOnlyMachineNamespaceAfterShareResult, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"nodes",
"list",
"--namespace",
namespace.Name,
"--output",
"json",
},
[]string{},
)
assert.Nil(s.T(), err)
var listOnlyMachineNamespaceAfterShare []v1.Machine
err = json.Unmarshal(
[]byte(listOnlyMachineNamespaceAfterShareResult),
&listOnlyMachineNamespaceAfterShare,
)
assert.Nil(s.T(), err)
assert.Len(s.T(), listOnlyMachineNamespaceAfterShare, 5)
assert.Equal(s.T(), uint64(7), listOnlyMachineNamespaceAfterShare[4].Id)
assert.Equal(s.T(), "shared-machine-2", listOnlyMachineNamespaceAfterShare[4].Name)
assert.True(s.T(), listOnlyMachineNamespaceAfterShare[4].Registered)
// test: unshare node
unshareMachineResult, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"nodes",
"unshare",
"--namespace",
namespace.Name,
"--identifier",
"7",
"--output",
"json",
},
[]string{},
)
assert.Nil(s.T(), err)
var unshareMachine v1.Machine
err = json.Unmarshal([]byte(unshareMachineResult), &unshareMachine)
assert.Nil(s.T(), err)
assert.Equal(s.T(), uint64(7), unshareMachine.Id)
assert.Equal(s.T(), "shared-machine-2", unshareMachine.Name)
assert.True(s.T(), unshareMachine.Registered)
// Test: list main namespace after machine has been shared
listOnlyMachineNamespaceAfterUnshareResult, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"nodes",
"list",
"--namespace",
namespace.Name,
"--output",
"json",
},
[]string{},
)
assert.Nil(s.T(), err)
var listOnlyMachineNamespaceAfterUnshare []v1.Machine
err = json.Unmarshal(
[]byte(listOnlyMachineNamespaceAfterUnshareResult),
&listOnlyMachineNamespaceAfterUnshare,
)
assert.Nil(s.T(), err)
assert.Len(s.T(), listOnlyMachineNamespaceAfterUnshare, 4)
}
func (s *IntegrationCLITestSuite) TestNodeExpireCommand() {
@@ -1029,6 +1048,178 @@ func (s *IntegrationCLITestSuite) TestNodeExpireCommand() {
assert.True(s.T(), listAllAfterExpiry[4].Expiry.AsTime().IsZero())
}
func (s *IntegrationCLITestSuite) TestNodeRenameCommand() {
namespace, err := s.createNamespace("machine-rename-command")
assert.Nil(s.T(), err)
// Randomly generated machine keys
machineKeys := []string{
"cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
"8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1",
"f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
"6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
"9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
}
machines := make([]*v1.Machine, len(machineKeys))
assert.Nil(s.T(), err)
for index, machineKey := range machineKeys {
_, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"debug",
"create-node",
"--name",
fmt.Sprintf("machine-%d", index+1),
"--namespace",
namespace.Name,
"--key",
machineKey,
"--output",
"json",
},
[]string{},
)
assert.Nil(s.T(), err)
machineResult, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"nodes",
"--namespace",
namespace.Name,
"register",
"--key",
machineKey,
"--output",
"json",
},
[]string{},
)
assert.Nil(s.T(), err)
var machine v1.Machine
err = json.Unmarshal([]byte(machineResult), &machine)
assert.Nil(s.T(), err)
machines[index] = &machine
}
assert.Len(s.T(), machines, len(machineKeys))
listAllResult, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"nodes",
"list",
"--output",
"json",
},
[]string{},
)
assert.Nil(s.T(), err)
var listAll []v1.Machine
err = json.Unmarshal([]byte(listAllResult), &listAll)
assert.Nil(s.T(), err)
assert.Len(s.T(), listAll, 5)
assert.Contains(s.T(), listAll[0].GetGivenName(), "machine-1")
assert.Contains(s.T(), listAll[1].GetGivenName(), "machine-2")
assert.Contains(s.T(), listAll[2].GetGivenName(), "machine-3")
assert.Contains(s.T(), listAll[3].GetGivenName(), "machine-4")
assert.Contains(s.T(), listAll[4].GetGivenName(), "machine-5")
for i := 0; i < 3; i++ {
_, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"nodes",
"rename",
"--identifier",
fmt.Sprintf("%d", listAll[i].Id),
fmt.Sprintf("newmachine-%d", i+1),
},
[]string{},
)
assert.Nil(s.T(), err)
}
listAllAfterRenameResult, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"nodes",
"list",
"--output",
"json",
},
[]string{},
)
assert.Nil(s.T(), err)
var listAllAfterRename []v1.Machine
err = json.Unmarshal([]byte(listAllAfterRenameResult), &listAllAfterRename)
assert.Nil(s.T(), err)
assert.Len(s.T(), listAllAfterRename, 5)
assert.Equal(s.T(), "newmachine-1", listAllAfterRename[0].GetGivenName())
assert.Equal(s.T(), "newmachine-2", listAllAfterRename[1].GetGivenName())
assert.Equal(s.T(), "newmachine-3", listAllAfterRename[2].GetGivenName())
assert.Contains(s.T(), listAllAfterRename[3].GetGivenName(), "machine-4")
assert.Contains(s.T(), listAllAfterRename[4].GetGivenName(), "machine-5")
// Test failure for too long names
result, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"nodes",
"rename",
"--identifier",
fmt.Sprintf("%d", listAll[4].Id),
"testmaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaachine12345678901234567890",
},
[]string{},
)
assert.Nil(s.T(), err)
assert.Contains(s.T(), result, "not be over 63 chars")
listAllAfterRenameAttemptResult, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"nodes",
"list",
"--output",
"json",
},
[]string{},
)
assert.Nil(s.T(), err)
var listAllAfterRenameAttempt []v1.Machine
err = json.Unmarshal(
[]byte(listAllAfterRenameAttemptResult),
&listAllAfterRenameAttempt,
)
assert.Nil(s.T(), err)
assert.Len(s.T(), listAllAfterRenameAttempt, 5)
assert.Equal(s.T(), "newmachine-1", listAllAfterRenameAttempt[0].GetGivenName())
assert.Equal(s.T(), "newmachine-2", listAllAfterRenameAttempt[1].GetGivenName())
assert.Equal(s.T(), "newmachine-3", listAllAfterRenameAttempt[2].GetGivenName())
assert.Contains(s.T(), listAllAfterRenameAttempt[3].GetGivenName(), "machine-4")
assert.Contains(s.T(), listAllAfterRenameAttempt[4].GetGivenName(), "machine-5")
}
func (s *IntegrationCLITestSuite) TestRouteCommand() {
namespace, err := s.createNamespace("routes-namespace")
assert.Nil(s.T(), err)
@@ -1082,7 +1273,6 @@ func (s *IntegrationCLITestSuite) TestRouteCommand() {
assert.Equal(s.T(), uint64(1), machine.Id)
assert.Equal(s.T(), "route-machine", machine.Name)
assert.True(s.T(), machine.Registered)
listAllResult, err := ExecuteCommand(
&s.headscale,
@@ -1192,6 +1382,35 @@ func (s *IntegrationCLITestSuite) TestRouteCommand() {
string(failEnableNonAdvertisedRoute),
"route (route-machine) is not available on node",
)
// Enable all routes on host
enableAllRouteResult, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"routes",
"enable",
"--output",
"json",
"--identifier",
"0",
"--all",
},
[]string{},
)
assert.Nil(s.T(), err)
var enableAllRoute v1.Routes
err = json.Unmarshal([]byte(enableAllRouteResult), &enableAllRoute)
assert.Nil(s.T(), err)
assert.Len(s.T(), enableAllRoute.AdvertisedRoutes, 2)
assert.Contains(s.T(), enableAllRoute.AdvertisedRoutes, "10.0.0.0/8")
assert.Contains(s.T(), enableAllRoute.AdvertisedRoutes, "192.168.1.0/24")
assert.Len(s.T(), enableAllRoute.EnabledRoutes, 2)
assert.Contains(s.T(), enableAllRoute.EnabledRoutes, "10.0.0.0/8")
assert.Contains(s.T(), enableAllRoute.EnabledRoutes, "192.168.1.0/24")
}
func (s *IntegrationCLITestSuite) TestApiKeyCommand() {
@@ -1338,3 +1557,212 @@ func (s *IntegrationCLITestSuite) TestApiKeyCommand() {
}
}
}
func (s *IntegrationCLITestSuite) TestNodeMoveCommand() {
oldNamespace, err := s.createNamespace("old-namespace")
assert.Nil(s.T(), err)
newNamespace, err := s.createNamespace("new-namespace")
assert.Nil(s.T(), err)
// Randomly generated machine key
machineKey := "688411b767663479632d44140f08a9fde87383adc7cdeb518f62ce28a17ef0aa"
_, err = ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"debug",
"create-node",
"--name",
"nomad-machine",
"--namespace",
oldNamespace.Name,
"--key",
machineKey,
"--output",
"json",
},
[]string{},
)
assert.Nil(s.T(), err)
machineResult, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"nodes",
"--namespace",
oldNamespace.Name,
"register",
"--key",
machineKey,
"--output",
"json",
},
[]string{},
)
assert.Nil(s.T(), err)
var machine v1.Machine
err = json.Unmarshal([]byte(machineResult), &machine)
assert.Nil(s.T(), err)
assert.Equal(s.T(), uint64(1), machine.Id)
assert.Equal(s.T(), "nomad-machine", machine.Name)
assert.Equal(s.T(), machine.Namespace.Name, oldNamespace.Name)
machineId := fmt.Sprintf("%d", machine.Id)
moveToNewNSResult, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"nodes",
"move",
"--identifier",
machineId,
"--namespace",
newNamespace.Name,
"--output",
"json",
},
[]string{},
)
assert.Nil(s.T(), err)
err = json.Unmarshal([]byte(moveToNewNSResult), &machine)
assert.Nil(s.T(), err)
assert.Equal(s.T(), machine.Namespace, newNamespace)
listAllNodesResult, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"nodes",
"list",
"--output",
"json",
},
[]string{},
)
assert.Nil(s.T(), err)
var allNodes []v1.Machine
err = json.Unmarshal([]byte(listAllNodesResult), &allNodes)
assert.Nil(s.T(), err)
assert.Len(s.T(), allNodes, 1)
assert.Equal(s.T(), allNodes[0].Id, machine.Id)
assert.Equal(s.T(), allNodes[0].Namespace, machine.Namespace)
assert.Equal(s.T(), allNodes[0].Namespace, newNamespace)
moveToNonExistingNSResult, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"nodes",
"move",
"--identifier",
machineId,
"--namespace",
"non-existing-namespace",
"--output",
"json",
},
[]string{},
)
assert.Nil(s.T(), err)
assert.Contains(
s.T(),
string(moveToNonExistingNSResult),
"Namespace not found",
)
assert.Equal(s.T(), machine.Namespace, newNamespace)
moveToOldNSResult, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"nodes",
"move",
"--identifier",
machineId,
"--namespace",
oldNamespace.Name,
"--output",
"json",
},
[]string{},
)
assert.Nil(s.T(), err)
err = json.Unmarshal([]byte(moveToOldNSResult), &machine)
assert.Nil(s.T(), err)
assert.Equal(s.T(), machine.Namespace, oldNamespace)
moveToSameNSResult, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"nodes",
"move",
"--identifier",
machineId,
"--namespace",
oldNamespace.Name,
"--output",
"json",
},
[]string{},
)
assert.Nil(s.T(), err)
err = json.Unmarshal([]byte(moveToSameNSResult), &machine)
assert.Nil(s.T(), err)
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

@@ -5,21 +5,49 @@ package headscale
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"os"
"strconv"
"strings"
"time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker"
"inet.af/netaddr"
)
const DOCKER_EXECUTE_TIMEOUT = 10 * time.Second
const (
DOCKER_EXECUTE_TIMEOUT = 10 * time.Second
)
var (
errEnvVarEmpty = errors.New("getenv: environment variable empty")
IpPrefix4 = netaddr.MustParseIPPrefix("100.64.0.0/10")
IpPrefix6 = netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/48")
tailscaleVersions = []string{
"head",
"unstable",
"1.26.0",
"1.24.2",
"1.22.2",
"1.20.4",
"1.18.2",
"1.16.2",
"1.14.3",
"1.12.3",
}
)
type TestNamespace struct {
count int
tailscales map[string]dockertest.Resource
}
type ExecuteCommandConfig struct {
timeout time.Duration
}
@@ -119,3 +147,168 @@ func DockerAllowNetworkAdministration(config *docker.HostConfig) {
Target: "/dev/net/tun",
})
}
func getDockerBuildOptions(version string) *dockertest.BuildOptions {
var tailscaleBuildOptions *dockertest.BuildOptions
switch version {
case "head":
tailscaleBuildOptions = &dockertest.BuildOptions{
Dockerfile: "Dockerfile.tailscale-HEAD",
ContextDir: ".",
BuildArgs: []docker.BuildArg{},
}
case "unstable":
tailscaleBuildOptions = &dockertest.BuildOptions{
Dockerfile: "Dockerfile.tailscale",
ContextDir: ".",
BuildArgs: []docker.BuildArg{
{
Name: "TAILSCALE_VERSION",
Value: "*", // Installs the latest version https://askubuntu.com/a/824926
},
{
Name: "TAILSCALE_CHANNEL",
Value: "unstable",
},
},
}
default:
tailscaleBuildOptions = &dockertest.BuildOptions{
Dockerfile: "Dockerfile.tailscale",
ContextDir: ".",
BuildArgs: []docker.BuildArg{
{
Name: "TAILSCALE_VERSION",
Value: version,
},
{
Name: "TAILSCALE_CHANNEL",
Value: "stable",
},
},
}
}
return tailscaleBuildOptions
}
func getIPs(
tailscales map[string]dockertest.Resource,
) (map[string][]netaddr.IP, error) {
ips := make(map[string][]netaddr.IP)
for hostname, tailscale := range tailscales {
command := []string{"tailscale", "ip"}
result, err := ExecuteCommand(
&tailscale,
command,
[]string{},
)
if err != nil {
return nil, err
}
for _, address := range strings.Split(result, "\n") {
address = strings.TrimSuffix(address, "\n")
if len(address) < 1 {
continue
}
ip, err := netaddr.ParseIP(address)
if err != nil {
return nil, err
}
ips[hostname] = append(ips[hostname], ip)
}
}
return ips, nil
}
func getDNSNames(
headscale *dockertest.Resource,
) ([]string, error) {
listAllResult, err := ExecuteCommand(
headscale,
[]string{
"headscale",
"nodes",
"list",
"--output",
"json",
},
[]string{},
)
if err != nil {
return nil, err
}
var listAll []v1.Machine
err = json.Unmarshal([]byte(listAllResult), &listAll)
if err != nil {
return nil, err
}
hostnames := make([]string, len(listAll))
for index := range listAll {
hostnames[index] = listAll[index].GetGivenName()
}
return hostnames, nil
}
func getMagicFQDN(
headscale *dockertest.Resource,
) ([]string, error) {
listAllResult, err := ExecuteCommand(
headscale,
[]string{
"headscale",
"nodes",
"list",
"--output",
"json",
},
[]string{},
)
if err != nil {
return nil, err
}
var listAll []v1.Machine
err = json.Unmarshal([]byte(listAllResult), &listAll)
if err != nil {
return nil, err
}
hostnames := make([]string, len(listAll))
for index := range listAll {
hostnames[index] = fmt.Sprintf("%s.%s.headscale.net", listAll[index].GetGivenName(), listAll[index].GetNamespace().GetName())
}
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

@@ -0,0 +1,433 @@
//go:build integration
package headscale
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"path"
"strings"
"sync"
"testing"
"time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"github.com/ccding/go-stun/stun"
)
const (
headscaleHostname = "headscale-derp"
namespaceName = "derpnamespace"
totalContainers = 3
)
type IntegrationDERPTestSuite struct {
suite.Suite
stats *suite.SuiteInformation
pool dockertest.Pool
networks map[int]dockertest.Network // so we keep the containers isolated
headscale dockertest.Resource
saveLogs bool
tailscales map[string]dockertest.Resource
joinWaitGroup sync.WaitGroup
}
func TestDERPIntegrationTestSuite(t *testing.T) {
saveLogs, err := GetEnvBool("HEADSCALE_INTEGRATION_SAVE_LOG")
if err != nil {
saveLogs = false
}
s := new(IntegrationDERPTestSuite)
s.tailscales = make(map[string]dockertest.Resource)
s.networks = make(map[int]dockertest.Network)
s.saveLogs = saveLogs
suite.Run(t, s)
// HandleStats, which allows us to check if we passed and save logs
// is called after TearDown, so we cannot tear down containers before
// we have potentially saved the logs.
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 !s.stats.Passed() {
err := s.saveLog(&s.headscale, "test_output")
if err != nil {
log.Printf("Could not save log: %s\n", err)
}
}
if err := s.pool.Purge(&s.headscale); err != nil {
log.Printf("Could not purge resource: %s\n", err)
}
for _, network := range s.networks {
if err := network.Close(); err != nil {
log.Printf("Could not close network: %s\n", err)
}
}
}
}
func (s *IntegrationDERPTestSuite) SetupSuite() {
if ppool, err := dockertest.NewPool(""); err == nil {
s.pool = *ppool
} else {
s.FailNow(fmt.Sprintf("Could not connect to docker: %s", err), "")
}
for i := 0; i < totalContainers; i++ {
if pnetwork, err := s.pool.CreateNetwork(fmt.Sprintf("headscale-derp-%d", i)); err == nil {
s.networks[i] = *pnetwork
} else {
s.FailNow(fmt.Sprintf("Could not create network: %s", err), "")
}
}
headscaleBuildOptions := &dockertest.BuildOptions{
Dockerfile: "Dockerfile.tmp-integration",
ContextDir: ".",
}
currentPath, err := os.Getwd()
if err != nil {
s.FailNow(fmt.Sprintf("Could not determine current path: %s", err), "")
}
headscaleOptions := &dockertest.RunOptions{
Name: headscaleHostname,
Mounts: []string{
fmt.Sprintf(
"%s/integration_test/etc_embedded_derp:/etc/headscale",
currentPath,
),
},
Cmd: []string{"headscale", "serve"},
ExposedPorts: []string{"8443/tcp", "3478/udp"},
PortBindings: map[docker.Port][]docker.PortBinding{
"8443/tcp": {{HostPort: "8443"}},
"3478/udp": {{HostPort: "3478"}},
},
}
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")
if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
s.headscale = *pheadscale
} else {
s.FailNow(fmt.Sprintf("Could not start headscale container: %s", err), "")
}
log.Println("Created headscale container to test DERP")
log.Println("Creating tailscale containers")
for i := 0; i < totalContainers; i++ {
version := tailscaleVersions[i%len(tailscaleVersions)]
hostname, container := s.tailscaleContainer(
fmt.Sprint(i),
version,
s.networks[i],
)
s.tailscales[hostname] = *container
}
log.Println("Waiting for headscale to be ready")
hostEndpoint := fmt.Sprintf("localhost:%s", s.headscale.GetPort("8443/tcp"))
if err := s.pool.Retry(func() error {
url := fmt.Sprintf("https://%s/health", hostEndpoint)
insecureTransport := http.DefaultTransport.(*http.Transport).Clone()
insecureTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
client := &http.Client{Transport: insecureTransport}
resp, err := client.Get(url)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("status code not OK")
}
return nil
}); err != nil {
// TODO(kradalby): If we cannot access headscale, or any other fatal error during
// test setup, we need to abort and tear down. However, testify does not seem to
// support that at the moment:
// https://github.com/stretchr/testify/issues/849
return // fmt.Errorf("Could not connect to headscale: %s", err)
}
log.Println("headscale container is ready")
log.Printf("Creating headscale namespace: %s\n", namespaceName)
result, err := ExecuteCommand(
&s.headscale,
[]string{"headscale", "namespaces", "create", namespaceName},
[]string{},
)
log.Println("headscale create namespace result: ", result)
assert.Nil(s.T(), err)
log.Printf("Creating pre auth key for %s\n", namespaceName)
preAuthResult, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"--namespace",
namespaceName,
"preauthkeys",
"create",
"--reusable",
"--expiration",
"24h",
"--output",
"json",
},
[]string{"LOG_LEVEL=error"},
)
assert.Nil(s.T(), err)
var preAuthKey v1.PreAuthKey
err = json.Unmarshal([]byte(preAuthResult), &preAuthKey)
assert.Nil(s.T(), err)
assert.True(s.T(), preAuthKey.Reusable)
headscaleEndpoint := fmt.Sprintf(
"https://headscale:%s",
s.headscale.GetPort("8443/tcp"),
)
log.Printf(
"Joining tailscale containers to headscale at %s\n",
headscaleEndpoint,
)
for hostname, tailscale := range s.tailscales {
s.joinWaitGroup.Add(1)
go s.Join(headscaleEndpoint, preAuthKey.Key, hostname, tailscale)
}
s.joinWaitGroup.Wait()
// The nodes need a bit of time to get their updated maps from headscale
// TODO: See if we can have a more deterministic wait here.
time.Sleep(60 * time.Second)
}
func (s *IntegrationDERPTestSuite) Join(
endpoint, key, hostname string,
tailscale dockertest.Resource,
) {
defer s.joinWaitGroup.Done()
command := []string{
"tailscale",
"up",
"-login-server",
endpoint,
"--authkey",
key,
"--hostname",
hostname,
}
log.Println("Join command:", command)
log.Printf("Running join command for %s\n", hostname)
_, err := ExecuteCommand(
&tailscale,
command,
[]string{},
)
assert.Nil(s.T(), err)
log.Printf("%s joined\n", hostname)
}
func (s *IntegrationDERPTestSuite) tailscaleContainer(
identifier, version string,
network dockertest.Network,
) (string, *dockertest.Resource) {
tailscaleBuildOptions := getDockerBuildOptions(version)
hostname := fmt.Sprintf(
"tailscale-%s-%s",
strings.Replace(version, ".", "-", -1),
identifier,
)
tailscaleOptions := &dockertest.RunOptions{
Name: hostname,
Networks: []*dockertest.Network{&network},
Cmd: []string{
"tailscaled", "--tun=tsdev",
},
// expose the host IP address, so we can access it from inside the container
ExtraHosts: []string{
"host.docker.internal:host-gateway",
"headscale:host-gateway",
},
}
pts, err := s.pool.BuildAndRunWithBuildOptions(
tailscaleBuildOptions,
tailscaleOptions,
DockerRestartPolicy,
DockerAllowLocalIPv6,
DockerAllowNetworkAdministration,
)
if err != nil {
log.Fatalf("Could not start tailscale container version %s: %s", version, err)
}
log.Printf("Created %s container\n", hostname)
return hostname, pts
}
func (s *IntegrationDERPTestSuite) TearDownSuite() {
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(
suiteName string,
stats *suite.SuiteInformation,
) {
s.stats = stats
}
func (s *IntegrationDERPTestSuite) saveLog(
resource *dockertest.Resource,
basePath string,
) error {
err := os.MkdirAll(basePath, os.ModePerm)
if err != nil {
return err
}
var stdout bytes.Buffer
var stderr bytes.Buffer
err = s.pool.Client.Logs(
docker.LogsOptions{
Context: context.TODO(),
Container: resource.Container.ID,
OutputStream: &stdout,
ErrorStream: &stderr,
Tail: "all",
RawTerminal: false,
Stdout: true,
Stderr: true,
Follow: false,
Timestamps: false,
},
)
if err != nil {
return err
}
log.Printf("Saving logs for %s to %s\n", resource.Container.Name, basePath)
err = ioutil.WriteFile(
path.Join(basePath, resource.Container.Name+".stdout.log"),
[]byte(stdout.String()),
0o644,
)
if err != nil {
return err
}
err = ioutil.WriteFile(
path.Join(basePath, resource.Container.Name+".stderr.log"),
[]byte(stdout.String()),
0o644,
)
if err != nil {
return err
}
return nil
}
func (s *IntegrationDERPTestSuite) TestPingAllPeersByHostname() {
hostnames, err := getDNSNames(&s.headscale)
assert.Nil(s.T(), err)
log.Printf("Hostnames: %#v\n", hostnames)
for hostname, tailscale := range s.tailscales {
for _, peername := range hostnames {
if strings.Contains(peername, hostname) {
continue
}
s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) {
command := []string{
"tailscale", "ping",
"--timeout=10s",
"--c=5",
"--until-direct=false",
peername,
}
log.Printf(
"Pinging using hostname from %s to %s\n",
hostname,
peername,
)
log.Println(command)
result, err := ExecuteCommand(
&tailscale,
command,
[]string{},
)
assert.Nil(t, err)
log.Printf("Result for %s: %s\n", hostname, result)
assert.Contains(t, result, "via DERP(headscale)")
})
}
}
}
func (s *IntegrationDERPTestSuite) TestDERPSTUN() {
headscaleSTUNAddr := fmt.Sprintf("localhost:%s", s.headscale.GetPort("3478/udp"))
client := stun.NewClient()
client.SetVerbose(true)
client.SetVVerbose(true)
client.SetServerAddr(headscaleSTUNAddr)
_, _, err := client.Discover()
assert.Nil(s.T(), err)
}

View File

@@ -15,6 +15,7 @@ import (
"os"
"path"
"strings"
"sync"
"testing"
"time"
@@ -28,13 +29,6 @@ import (
"tailscale.com/ipn/ipnstate"
)
var tailscaleVersions = []string{"1.20.4", "1.18.2", "1.16.2", "1.14.3", "1.12.3"}
type TestNamespace struct {
count int
tailscales map[string]dockertest.Resource
}
type IntegrationTestSuite struct {
suite.Suite
stats *suite.SuiteInformation
@@ -42,49 +36,60 @@ type IntegrationTestSuite struct {
pool dockertest.Pool
network dockertest.Network
headscale dockertest.Resource
saveLogs bool
namespaces map[string]TestNamespace
joinWaitGroup sync.WaitGroup
}
func TestIntegrationTestSuite(t *testing.T) {
saveLogs, err := GetEnvBool("HEADSCALE_INTEGRATION_SAVE_LOG")
if err != nil {
saveLogs = false
}
s := new(IntegrationTestSuite)
s.namespaces = map[string]TestNamespace{
"main": {
count: 20,
tailscales: make(map[string]dockertest.Resource),
},
"shared": {
"thisspace": {
count: 5,
tailscales: make(map[string]dockertest.Resource),
},
"otherspace": {
count: 2,
tailscales: make(map[string]dockertest.Resource),
},
}
s.saveLogs = saveLogs
suite.Run(t, s)
// HandleStats, which allows us to check if we passed and save logs
// is called after TearDown, so we cannot tear down containers before
// we have potentially saved the logs.
for _, 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 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 !s.stats.Passed() {
err := s.saveLog(&s.headscale, "test_output")
if err != nil {
log.Printf("Could not save log: %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)
}
}
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)
if err := s.network.Close(); err != nil {
log.Printf("Could not close network: %s\n", err)
}
}
}
@@ -118,7 +123,7 @@ func (s *IntegrationTestSuite) saveLog(
return err
}
fmt.Printf("Saving logs for %s to %s\n", resource.Container.Name, basePath)
log.Printf("Saving logs for %s to %s\n", resource.Container.Name, basePath)
err = ioutil.WriteFile(
path.Join(basePath, resource.Container.Name+".stdout.log"),
@@ -141,19 +146,39 @@ func (s *IntegrationTestSuite) saveLog(
return nil
}
func (s *IntegrationTestSuite) Join(
endpoint, key, hostname string,
tailscale dockertest.Resource,
) {
defer s.joinWaitGroup.Done()
command := []string{
"tailscale",
"up",
"-login-server",
endpoint,
"--authkey",
key,
"--hostname",
hostname,
}
log.Println("Join command:", command)
log.Printf("Running join command for %s\n", hostname)
_, err := ExecuteCommand(
&tailscale,
command,
[]string{},
)
assert.Nil(s.T(), err)
log.Printf("%s joined\n", hostname)
}
func (s *IntegrationTestSuite) tailscaleContainer(
namespace, identifier, version string,
) (string, *dockertest.Resource) {
tailscaleBuildOptions := &dockertest.BuildOptions{
Dockerfile: "Dockerfile.tailscale",
ContextDir: ".",
BuildArgs: []docker.BuildArg{
{
Name: "TAILSCALE_VERSION",
Value: version,
},
},
}
tailscaleBuildOptions := getDockerBuildOptions(version)
hostname := fmt.Sprintf(
"%s-tailscale-%s-%s",
namespace,
@@ -176,9 +201,9 @@ func (s *IntegrationTestSuite) tailscaleContainer(
DockerAllowNetworkAdministration,
)
if err != nil {
log.Fatalf("Could not start resource: %s", err)
log.Fatalf("Could not start tailscale container version %s: %s", version, err)
}
fmt.Printf("Created %s container\n", hostname)
log.Printf("Created %s container\n", hostname)
return hostname, pts
}
@@ -193,23 +218,23 @@ func (s *IntegrationTestSuite) SetupSuite() {
if ppool, err := dockertest.NewPool(""); err == nil {
s.pool = *ppool
} 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 {
s.network = *pnetwork
} else {
log.Fatalf("Could not create network: %s", err)
s.FailNow(fmt.Sprintf("Could not create network: %s", err), "")
}
headscaleBuildOptions := &dockertest.BuildOptions{
Dockerfile: "Dockerfile",
Dockerfile: "Dockerfile.tmp-integration",
ContextDir: ".",
}
currentPath, err := os.Getwd()
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{
@@ -221,15 +246,20 @@ func (s *IntegrationTestSuite) SetupSuite() {
Cmd: []string{"headscale", "serve"},
}
fmt.Println("Creating headscale container")
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")
if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
s.headscale = *pheadscale
} else {
log.Fatalf("Could not start resource: %s", err)
s.FailNow(fmt.Sprintf("Could not start headscale container: %s", err), "")
}
fmt.Println("Created headscale container")
log.Println("Created headscale container")
fmt.Println("Creating tailscale containers")
log.Println("Creating tailscale containers")
for namespace, scales := range s.namespaces {
for i := 0; i < scales.count; i++ {
version := tailscaleVersions[i%len(tailscaleVersions)]
@@ -243,7 +273,7 @@ func (s *IntegrationTestSuite) SetupSuite() {
}
}
fmt.Println("Waiting for headscale to be ready")
log.Println("Waiting for headscale to be ready")
hostEndpoint := fmt.Sprintf("localhost:%s", s.headscale.GetPort("8080/tcp"))
if err := s.pool.Retry(func() error {
@@ -266,19 +296,19 @@ func (s *IntegrationTestSuite) SetupSuite() {
// https://github.com/stretchr/testify/issues/849
return // fmt.Errorf("Could not connect to headscale: %s", err)
}
fmt.Println("headscale container is ready")
log.Println("headscale container is ready")
for namespace, scales := range s.namespaces {
fmt.Printf("Creating headscale namespace: %s\n", namespace)
log.Printf("Creating headscale namespace: %s\n", namespace)
result, err := ExecuteCommand(
&s.headscale,
[]string{"headscale", "namespaces", "create", namespace},
[]string{},
)
fmt.Println("headscale create namespace result: ", result)
log.Println("headscale create namespace result: ", result)
assert.Nil(s.T(), err)
fmt.Printf("Creating pre auth key for %s\n", namespace)
log.Printf("Creating pre auth key for %s\n", namespace)
preAuthResult, err := ExecuteCommand(
&s.headscale,
[]string{
@@ -304,33 +334,16 @@ func (s *IntegrationTestSuite) SetupSuite() {
headscaleEndpoint := "http://headscale:8080"
fmt.Printf(
log.Printf(
"Joining tailscale containers to headscale at %s\n",
headscaleEndpoint,
)
for hostname, tailscale := range scales.tailscales {
command := []string{
"tailscale",
"up",
"-login-server",
headscaleEndpoint,
"--authkey",
preAuthKey.Key,
"--hostname",
hostname,
}
fmt.Println("Join command:", command)
fmt.Printf("Running join command for %s\n", hostname)
result, err := ExecuteCommand(
&tailscale,
command,
[]string{},
)
fmt.Println("tailscale result: ", result)
assert.Nil(s.T(), err)
fmt.Printf("%s joined\n", hostname)
s.joinWaitGroup.Add(1)
go s.Join(headscaleEndpoint, preAuthKey.Key, hostname, tailscale)
}
s.joinWaitGroup.Wait()
}
// The nodes need a bit of time to get their updated maps from headscale
@@ -339,6 +352,23 @@ func (s *IntegrationTestSuite) SetupSuite() {
}
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(
@@ -350,7 +380,7 @@ func (s *IntegrationTestSuite) HandleStats(
func (s *IntegrationTestSuite) TestListNodes() {
for namespace, scales := range s.namespaces {
fmt.Println("Listing nodes")
log.Println("Listing nodes")
result, err := ExecuteCommand(
&s.headscale,
[]string{"headscale", "--namespace", namespace, "nodes", "list"},
@@ -358,7 +388,7 @@ func (s *IntegrationTestSuite) TestListNodes() {
)
assert.Nil(s.T(), err)
fmt.Printf("List nodes: \n%s\n", result)
log.Printf("List nodes: \n%s\n", result)
// Chck that the correct count of host is present in node list
lines := strings.Split(result, "\n")
@@ -381,7 +411,7 @@ func (s *IntegrationTestSuite) TestGetIpAddresses() {
s.T().Run(hostname, func(t *testing.T) {
assert.NotNil(t, ip)
fmt.Printf("IP for %s: %s\n", hostname, ip)
log.Printf("IP for %s: %s\n", hostname, ip)
// c.Assert(ip.Valid(), check.IsTrue)
assert.True(t, ip.Is4() || ip.Is6())
@@ -410,7 +440,7 @@ func (s *IntegrationTestSuite) TestGetIpAddresses() {
// s.T().Run(hostname, func(t *testing.T) {
// command := []string{"tailscale", "status", "--json"}
//
// fmt.Printf("Getting status for %s\n", hostname)
// log.Printf("Getting status for %s\n", hostname)
// result, err := ExecuteCommand(
// &tailscale,
// command,
@@ -452,6 +482,7 @@ func getIPsfromIPNstate(status ipnstate.Status) []netaddr.IP {
return ips
}
// TODO: Adopt test for cross communication between namespaces
func (s *IntegrationTestSuite) TestPingAllPeersByAddress() {
for _, scales := range s.namespaces {
ips, err := getIPs(scales.tailscales)
@@ -476,7 +507,7 @@ func (s *IntegrationTestSuite) TestPingAllPeersByAddress() {
ip.String(),
}
fmt.Printf(
log.Printf(
"Pinging from %s to %s (%s)\n",
hostname,
peername,
@@ -488,7 +519,7 @@ func (s *IntegrationTestSuite) TestPingAllPeersByAddress() {
[]string{},
)
assert.Nil(t, err)
fmt.Printf("Result for %s: %s\n", hostname, result)
log.Printf("Result for %s: %s\n", hostname, result)
assert.Contains(t, result, "pong")
})
}
@@ -497,116 +528,10 @@ func (s *IntegrationTestSuite) TestPingAllPeersByAddress() {
}
}
func (s *IntegrationTestSuite) TestSharedNodes() {
main := s.namespaces["main"]
shared := s.namespaces["shared"]
result, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"nodes",
"list",
"--output",
"json",
"--namespace",
"shared",
},
[]string{},
)
assert.Nil(s.T(), err)
var machineList []v1.Machine
err = json.Unmarshal([]byte(result), &machineList)
assert.Nil(s.T(), err)
for _, machine := range machineList {
result, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"nodes",
"share",
"--identifier", fmt.Sprint(machine.Id),
"--namespace", "main",
},
[]string{},
)
assert.Nil(s.T(), err)
fmt.Println("Shared node with result: ", result)
}
result, err = ExecuteCommand(
&s.headscale,
[]string{"headscale", "nodes", "list", "--namespace", "main"},
[]string{},
)
assert.Nil(s.T(), err)
fmt.Println("Nodelist after sharing", result)
// Chck that the correct count of host is present in node list
lines := strings.Split(result, "\n")
assert.Equal(s.T(), len(main.tailscales)+len(shared.tailscales), len(lines)-2)
for hostname := range main.tailscales {
assert.Contains(s.T(), result, hostname)
}
for hostname := range shared.tailscales {
assert.Contains(s.T(), result, hostname)
}
// TODO(juanfont): We have to find out why do we need to wait
time.Sleep(100 * time.Second) // Wait for the nodes to receive updates
sharedIps, err := getIPs(shared.tailscales)
assert.Nil(s.T(), err)
for hostname, tailscale := range main.tailscales {
for peername, peerIPs := range sharedIps {
for i, ip := range peerIPs {
// We currently cant ping ourselves, so skip that.
if peername == hostname {
continue
}
s.T().
Run(fmt.Sprintf("%s-%s-%d", hostname, peername, i), func(t *testing.T) {
// We are only interested in "direct ping" which means what we
// might need a couple of more attempts before reaching the node.
command := []string{
"tailscale", "ping",
"--timeout=15s",
"--c=20",
"--until-direct=true",
ip.String(),
}
fmt.Printf(
"Pinging from %s to %s (%s)\n",
hostname,
peername,
ip,
)
result, err := ExecuteCommand(
&tailscale,
command,
[]string{},
)
assert.Nil(t, err)
fmt.Printf("Result for %s: %s\n", hostname, result)
assert.Contains(t, result, "pong")
})
}
}
}
}
func (s *IntegrationTestSuite) TestTailDrop() {
for _, scales := range s.namespaces {
ips, err := getIPs(scales.tailscales)
assert.Nil(s.T(), err)
assert.Nil(s.T(), err)
retry := func(times int, sleepInverval time.Duration, doWork func() error) (err error) {
for attempts := 0; attempts < times; attempts++ {
@@ -616,6 +541,7 @@ func (s *IntegrationTestSuite) TestTailDrop() {
}
time.Sleep(sleepInverval)
}
return
}
@@ -635,10 +561,10 @@ func (s *IntegrationTestSuite) TestTailDrop() {
command := []string{
"tailscale", "file", "cp",
fmt.Sprintf("/tmp/file_from_%s", hostname),
fmt.Sprintf("%s:", peername),
fmt.Sprintf("%s:", ips[peername][1]),
}
retry(10, 1*time.Second, func() error {
fmt.Printf(
log.Printf(
"Sending file from %s to %s\n",
hostname,
peername,
@@ -677,10 +603,10 @@ func (s *IntegrationTestSuite) TestTailDrop() {
"ls",
fmt.Sprintf("/tmp/file_from_%s", peername),
}
fmt.Printf(
log.Printf(
"Checking file in %s (%s) from %s (%s)\n",
hostname,
ips[hostname],
ips[hostname][1],
peername,
ip,
)
@@ -690,7 +616,7 @@ func (s *IntegrationTestSuite) TestTailDrop() {
[]string{},
)
assert.Nil(t, err)
fmt.Printf("Result for %s: %s\n", peername, result)
log.Printf("Result for %s: %s\n", peername, result)
assert.Equal(
t,
fmt.Sprintf("/tmp/file_from_%s\n", peername),
@@ -703,24 +629,27 @@ func (s *IntegrationTestSuite) TestTailDrop() {
}
func (s *IntegrationTestSuite) TestPingAllPeersByHostname() {
for namespace, scales := range s.namespaces {
ips, err := getIPs(scales.tailscales)
assert.Nil(s.T(), err)
hostnames, err := getMagicFQDN(&s.headscale)
assert.Nil(s.T(), err)
log.Printf("Resolved hostnames: %#v", hostnames)
for _, scales := range s.namespaces {
for hostname, tailscale := range scales.tailscales {
for peername := range ips {
if peername == hostname {
for _, peername := range hostnames {
if strings.Contains(peername, hostname) {
continue
}
s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) {
command := []string{
"tailscale", "ping",
"--timeout=10s",
"--c=20",
"--until-direct=true",
fmt.Sprintf("%s.%s.headscale.net", peername, namespace),
peername,
}
fmt.Printf(
log.Printf(
"Pinging using hostname from %s to %s\n",
hostname,
peername,
@@ -731,7 +660,7 @@ func (s *IntegrationTestSuite) TestPingAllPeersByHostname() {
[]string{},
)
assert.Nil(t, err)
fmt.Printf("Result for %s: %s\n", hostname, result)
log.Printf("Result for %s: %s\n", hostname, result)
assert.Contains(t, result, "pong")
})
}
@@ -740,21 +669,27 @@ func (s *IntegrationTestSuite) TestPingAllPeersByHostname() {
}
func (s *IntegrationTestSuite) TestMagicDNS() {
for namespace, scales := range s.namespaces {
hostnames, err := getMagicFQDN(&s.headscale)
assert.Nil(s.T(), err)
log.Printf("Resolved hostnames: %#v", hostnames)
for _, scales := range s.namespaces {
ips, err := getIPs(scales.tailscales)
assert.Nil(s.T(), err)
for hostname, tailscale := range scales.tailscales {
for peername, ips := range ips {
if peername == hostname {
for _, peername := range hostnames {
if strings.Contains(peername, hostname) {
continue
}
s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) {
command := []string{
"tailscale", "ip",
fmt.Sprintf("%s.%s.headscale.net", peername, namespace),
"tailscale", "ip", peername,
}
fmt.Printf(
log.Printf(
"Resolving name %s from %s\n",
peername,
hostname,
@@ -765,9 +700,11 @@ func (s *IntegrationTestSuite) TestMagicDNS() {
[]string{},
)
assert.Nil(t, err)
fmt.Printf("Result for %s: %s\n", hostname, result)
log.Printf("Result for %s: %s\n", hostname, result)
for _, ip := range ips {
peerBaseName := peername[:len(peername)-MachineGivenNameHashLength-1]
expectedAddresses := ips[peerBaseName]
for _, ip := range expectedAddresses {
assert.Contains(t, result, ip.String())
}
})
@@ -776,38 +713,6 @@ func (s *IntegrationTestSuite) TestMagicDNS() {
}
}
func getIPs(
tailscales map[string]dockertest.Resource,
) (map[string][]netaddr.IP, error) {
ips := make(map[string][]netaddr.IP)
for hostname, tailscale := range tailscales {
command := []string{"tailscale", "ip"}
result, err := ExecuteCommand(
&tailscale,
command,
[]string{},
)
if err != nil {
return nil, err
}
for _, address := range strings.Split(result, "\n") {
address = strings.TrimSuffix(address, "\n")
if len(address) < 1 {
continue
}
ip, err := netaddr.ParseIP(address)
if err != nil {
return nil, err
}
ips[hostname] = append(ips[hostname], ip)
}
}
return ips, nil
}
func getAPIURLs(
tailscales map[string]dockertest.Resource,
) (map[netaddr.IP]string, error) {

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: ""
db_type: sqlite3
ephemeral_node_inactivity_timeout: 30m
node_update_check_interval: 10s
ip_prefixes:
- fd7a:115c:a1e0::/48
- 100.64.0.0/10
@@ -14,6 +15,7 @@ dns_config:
db_path: /tmp/integration_test_db.sqlite3
private_key_path: private.key
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 127.0.0.1:9090
server_url: http://headscale:8080
derp:

View File

@@ -0,0 +1,29 @@
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:8443
server_url: https://headscale:8443
tls_cert_path: "/etc/headscale/tls/server.crt"
tls_key_path: "/etc/headscale/tls/server.key"
tls_client_auth_mode: disabled
derp:
server:
enabled: true
region_id: 999
region_code: "headscale"
region_name: "Headscale Embedded DERP"
stun_listen_addr: "0.0.0.0:3478"

View File

@@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIC8jCCAdqgAwIBAgIULbu+UbSTMG/LtxooLLh7BgSEyqEwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJaGVhZHNjYWxlMCAXDTIyMDMwNTE2NDgwM1oYDzI1MjEx
MTA0MTY0ODAzWjAUMRIwEAYDVQQDDAloZWFkc2NhbGUwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQDqcfpToLZUF0rlNwXkkt3lbyw4Cl4TJdx36o2PKaOK
U+tze/IjRsCWeMwrcR1o9TNZcxsD+c2J48D1WATuQJlMeg+2UJXGaTGRKkkbPMy3
5m7AFf/Q16UEOgm2NYjZaQ8faRGIMYURG/6sXmNeETJvBixpBev9yKJuVXgqHNS4
NpEkNwdOCuAZXrmw0HCbiusawJOay4tFvhH14rav8Uimonl8UTNVXufMzyUOuoaQ
TGflmzYX3hIoswRnTPlIWFoqObvx2Q8H+of3uQJXy0m8I6OrIoXLNxnqYMfFls79
9SYgVc2jPsCbh5fwyRbx2Hof7sIZ1K/mNgxJRG1E3ZiLAgMBAAGjOjA4MBQGA1Ud
EQQNMAuCCWhlYWRzY2FsZTALBgNVHQ8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUH
AwEwDQYJKoZIhvcNAQELBQADggEBANGlVN7NCsJaKz0k0nhlRGK+tcxn2p1PXN/i
Iy+JX8ahixPC4ocRwOhrXgb390ZXLLwq08HrWYRB/Wi1VUzCp5d8dVxvrR43dJ+v
L2EOBiIKgcu2C3pWW1qRR46/EoXUU9kSH2VNBvIhNufi32kEOidoDzxtQf6qVCoF
guUt1JkAqrynv1UvR/2ZRM/WzM/oJ8qfECwrwDxyYhkqU5Z5jCWg0C6kPIBvNdzt
B0eheWS+ZxVwkePTR4e17kIafwknth3lo+orxVrq/xC+OVM1bGrt2ZyD64ZvEqQl
w6kgbzBdLScAQptWOFThwhnJsg0UbYKimZsnYmjVEuN59TJv92M=
-----END CERTIFICATE-----
(Expires on Nov 4 16:48:03 2521 GMT)

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDqcfpToLZUF0rl
NwXkkt3lbyw4Cl4TJdx36o2PKaOKU+tze/IjRsCWeMwrcR1o9TNZcxsD+c2J48D1
WATuQJlMeg+2UJXGaTGRKkkbPMy35m7AFf/Q16UEOgm2NYjZaQ8faRGIMYURG/6s
XmNeETJvBixpBev9yKJuVXgqHNS4NpEkNwdOCuAZXrmw0HCbiusawJOay4tFvhH1
4rav8Uimonl8UTNVXufMzyUOuoaQTGflmzYX3hIoswRnTPlIWFoqObvx2Q8H+of3
uQJXy0m8I6OrIoXLNxnqYMfFls799SYgVc2jPsCbh5fwyRbx2Hof7sIZ1K/mNgxJ
RG1E3ZiLAgMBAAECggEBALu1Ni/u5Qy++YA8ZcN0s6UXNdhItLmv/q0kZuLQ+9et
CT8VZfFInLndTdsaXenDKLHdryunviFA8SV+q7P2lMbek+Xs735EiyMnMBFWxLIZ
FWNGOeQERGL19QCmLEOmEi2b+iWJQHlKaMWpbPXL3w11a+lKjIBNO4ALfoJ5QveZ
cGMKsJdm/mpqBvLeNeh2eAFk3Gp6sT1g80Ge8NkgyzFBNIqnut0eerM15kPTc6Qz
12JLaOXUuV3PrcB4PN4nOwrTDg88GDNOQtc1Pc9r4nOHyLfr8X7QEtj1wXSwmOuK
d6ynMnAmoxVA9wEnupLbil1bzohRzpsTpkmDruYaBEECgYEA/Z09I8D6mt2NVqIE
KyvLjBK39ijSV9r3/lvB2Ple2OOL5YQEd+yTrIFy+3zdUnDgD1zmNnXjmjvHZ9Lc
IFf2o06AF84QLNB5gLPdDQkGNFdDqUxljBrfAfE3oANmPS/B0SijMGOOOiDO2FtO
xl1nfRr78mswuRs9awoUWCdNRKUCgYEA7KaTYKIQW/FEjw9lshp74q5vbn6zoXF5
7N8VkwI+bBVNvRbM9XZ8qhfgRdu9eXs5oL/N4mSYY54I8fA//pJ0Z2vpmureMm1V
mL5WBUmSD9DIbAchoK+sRiQhVmNMBQC6cHMABA7RfXvBeGvWrm9pKCS6ZLgLjkjp
PsmAcaXQcW8CgYEA2inAxljjOwUK6FNGsrxhxIT1qtNC3kCGxE+6WSNq67gSR8Vg
8qiX//T7LEslOB3RIGYRwxd2St7RkgZZRZllmOWWWuPwFhzf6E7RAL2akLvggGov
kG4tGEagSw2hjVDfsUT73ExHtMk0Jfmlsg33UC8+PDLpHtLH6qQpDAwC8+ECgYEA
o+AqOIWhvHmT11l7O915Ip1WzvZwYADbxLsrDnVEUsZh4epTHjvh0kvcY6PqTqCV
ZIrOANNWb811Nkz/k8NJVoD08PFp0xPBbZeIq/qpachTsfMyRzq/mobUiyUR9Hjv
ooUQYr78NOApNsG+lWbTNBhS9wI4BlzZIECbcJe5g4MCgYEAndRoy8S+S0Hx/S8a
O3hzXeDmivmgWqn8NVD4AKOovpkz4PaIVVQbAQkiNfAx8/DavPvjEKAbDezJ4ECV
j7IsOWtDVI7pd6eF9fTcECwisrda8aUoiOap8AQb48153Vx+g2N4Vy3uH0xJs4cz
TDALZPOBg8VlV+HEFDP43sp9Bf0=
-----END PRIVATE KEY-----

View File

@@ -2,7 +2,6 @@ package headscale
import (
"database/sql/driver"
"encoding/json"
"errors"
"fmt"
"sort"
@@ -10,21 +9,29 @@ import (
"strings"
"time"
"github.com/fatih/set"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/rs/zerolog/log"
"google.golang.org/protobuf/types/known/timestamppb"
"gorm.io/datatypes"
"inet.af/netaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
const (
errMachineNotFound = Error("machine not found")
errMachineAlreadyRegistered = Error("machine already registered")
errMachineRouteIsNotAvailable = Error("route is not available on machine")
errMachineAddressesInvalid = Error("failed to parse machine addresses")
errMachineNotFound = Error("machine not found")
errMachineRouteIsNotAvailable = Error("route is not available on machine")
errMachineAddressesInvalid = Error("failed to parse machine addresses")
errMachineNotFoundRegistrationCache = Error(
"machine not found in registration cache",
)
errCouldNotConvertMachineInterface = Error("failed to convert machine interface")
errHostnameTooLong = Error("Hostname too long")
MachineGivenNameHashLength = 8
MachineGivenNameTrimSize = 2
)
const (
maxHostnameLength = 255
)
// Machine is a Headscale client.
@@ -34,22 +41,36 @@ type Machine struct {
NodeKey string
DiscoKey string
IPAddresses MachineAddresses
Name string
// Hostname represents the name given by the Tailscale
// client during registration
Hostname string
// Givenname represents either:
// a DNS normalized version of Hostname
// a valid name set by the User
//
// GivenName is the name used in all DNS related
// parts of headscale.
GivenName string `gorm:"type:varchar(63);unique_index"`
NamespaceID uint
Namespace Namespace `gorm:"foreignKey:NamespaceID"`
Registered bool // temp
RegisterMethod string
AuthKeyID uint
AuthKey *PreAuthKey
ForcedTags StringList
// TODO(kradalby): This seems like irrelevant information?
AuthKeyID uint
AuthKey *PreAuthKey
LastSeen *time.Time
LastSuccessfulUpdate *time.Time
Expiry *time.Time
HostInfo datatypes.JSON
Endpoints datatypes.JSON
EnabledRoutes datatypes.JSON
HostInfo HostInfo
Endpoints StringList
EnabledRoutes IPPrefixes
CreatedAt time.Time
UpdatedAt time.Time
@@ -61,11 +82,6 @@ type (
MachinesP []*Machine
)
// For the time being this method is rather naive.
func (machine Machine) isRegistered() bool {
return machine.Registered
}
type MachineAddresses []netaddr.IP
func (ma MachineAddresses) ToStringSlice() []string {
@@ -112,29 +128,16 @@ func (machine Machine) isExpired() bool {
// If Expiry is not set, the client has not indicated that
// it wants an expiry time, it is therefor considered
// to mean "not expired"
if machine.Expiry.IsZero() {
if machine.Expiry == nil || machine.Expiry.IsZero() {
return false
}
return time.Now().UTC().After(*machine.Expiry)
}
func (h *Headscale) ListAllMachines() ([]Machine, error) {
machines := []Machine{}
if err := h.db.Preload("AuthKey").
Preload("AuthKey.Namespace").
Preload("Namespace").
Where("registered").
Find(&machines).Error; err != nil {
return nil, err
}
return machines, nil
}
func containsAddresses(inputs []string, addrs []string) bool {
for _, addr := range addrs {
if containsString(inputs, addr) {
if contains(inputs, addr) {
return true
}
}
@@ -161,7 +164,7 @@ func getFilteredByACLPeers(
) Machines {
log.Trace().
Caller().
Str("machine", machine.Name).
Str("machine", machine.Hostname).
Msg("Finding peers filtered by ACLs")
peers := make(map[uint64]Machine)
@@ -182,6 +185,12 @@ func getFilteredByACLPeers(
machine.IPAddresses.ToStringSlice(),
peer.IPAddresses.ToStringSlice(),
) || // match source and destination
matchSourceAndDestinationWithRule(
rule.SrcIPs,
dst,
peer.IPAddresses.ToStringSlice(),
machine.IPAddresses.ToStringSlice(),
) || // match return path
matchSourceAndDestinationWithRule(
rule.SrcIPs,
dst,
@@ -191,9 +200,21 @@ func getFilteredByACLPeers(
matchSourceAndDestinationWithRule(
rule.SrcIPs,
dst,
[]string{"*"},
[]string{"*"},
) || // match source and all destination
matchSourceAndDestinationWithRule(
rule.SrcIPs,
dst,
[]string{"*"},
peer.IPAddresses.ToStringSlice(),
) || // match source and all destination
matchSourceAndDestinationWithRule(
rule.SrcIPs,
dst,
[]string{"*"},
machine.IPAddresses.ToStringSlice(),
) { // match return path
) { // match all sources and source
peers[peer.ID] = peer
}
}
@@ -210,21 +231,21 @@ func getFilteredByACLPeers(
log.Trace().
Caller().
Str("machine", machine.Name).
Str("machine", machine.Hostname).
Msgf("Found some machines: %v", machines)
return authorizedPeers
}
func (h *Headscale) getDirectPeers(machine *Machine) (Machines, error) {
func (h *Headscale) ListPeers(machine *Machine) (Machines, error) {
log.Trace().
Caller().
Str("machine", machine.Name).
Str("machine", machine.Hostname).
Msg("Finding direct peers")
machines := Machines{}
if err := h.db.Preload("Namespace").Where("namespace_id = ? AND machine_key <> ? AND registered",
machine.NamespaceID, machine.MachineKey).Find(&machines).Error; err != nil {
if err := h.db.Preload("AuthKey").Preload("AuthKey.Namespace").Preload("Namespace").Where("machine_key <> ?",
machine.MachineKey).Find(&machines).Error; err != nil {
log.Error().Err(err).Msg("Error accessing db")
return Machines{}, err
@@ -234,74 +255,12 @@ func (h *Headscale) getDirectPeers(machine *Machine) (Machines, error) {
log.Trace().
Caller().
Str("machine", machine.Name).
Msgf("Found direct machines: %s", machines.String())
Str("machine", machine.Hostname).
Msgf("Found peers: %s", machines.String())
return machines, nil
}
// getShared fetches machines that are shared to the `Namespace` of the machine we are getting peers for.
func (h *Headscale) getShared(machine *Machine) (Machines, error) {
log.Trace().
Caller().
Str("machine", machine.Name).
Msg("Finding shared peers")
sharedMachines := []SharedMachine{}
if err := h.db.Preload("Namespace").Preload("Machine").Preload("Machine.Namespace").Where("namespace_id = ?",
machine.NamespaceID).Find(&sharedMachines).Error; err != nil {
return Machines{}, err
}
peers := make(Machines, 0)
for _, sharedMachine := range sharedMachines {
peers = append(peers, sharedMachine.Machine)
}
sort.Slice(peers, func(i, j int) bool { return peers[i].ID < peers[j].ID })
log.Trace().
Caller().
Str("machine", machine.Name).
Msgf("Found shared peers: %s", peers.String())
return peers, nil
}
// getSharedTo fetches the machines of the namespaces this machine is shared in.
func (h *Headscale) getSharedTo(machine *Machine) (Machines, error) {
log.Trace().
Caller().
Str("machine", machine.Name).
Msg("Finding peers in namespaces this machine is shared with")
sharedMachines := []SharedMachine{}
if err := h.db.Preload("Namespace").Preload("Machine").Preload("Machine.Namespace").Where("machine_id = ?",
machine.ID).Find(&sharedMachines).Error; err != nil {
return Machines{}, err
}
peers := make(Machines, 0)
for _, sharedMachine := range sharedMachines {
namespaceMachines, err := h.ListMachinesInNamespace(
sharedMachine.Namespace.Name,
)
if err != nil {
return Machines{}, err
}
peers = append(peers, namespaceMachines...)
}
sort.Slice(peers, func(i, j int) bool { return peers[i].ID < peers[j].ID })
log.Trace().
Caller().
Str("machine", machine.Name).
Msgf("Found peers we are shared with: %s", peers.String())
return peers, nil
}
func (h *Headscale) getPeers(machine *Machine) (Machines, error) {
var peers Machines
var err error
@@ -310,7 +269,7 @@ func (h *Headscale) getPeers(machine *Machine) (Machines, error) {
// else use the classic namespace scope
if h.aclPolicy != nil {
var machines []Machine
machines, err = h.ListAllMachines()
machines, err = h.ListMachines()
if err != nil {
log.Error().Err(err).Msg("Error retrieving list of machines")
@@ -318,7 +277,7 @@ func (h *Headscale) getPeers(machine *Machine) (Machines, error) {
}
peers = getFilteredByACLPeers(machines, h.aclRules, machine)
} else {
direct, err := h.getDirectPeers(machine)
peers, err = h.ListPeers(machine)
if err != nil {
log.Error().
Caller().
@@ -327,35 +286,13 @@ func (h *Headscale) getPeers(machine *Machine) (Machines, error) {
return Machines{}, err
}
shared, err := h.getShared(machine)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot fetch peers")
return Machines{}, err
}
sharedTo, err := h.getSharedTo(machine)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot fetch peers")
return Machines{}, err
}
peers = append(direct, shared...)
peers = append(peers, sharedTo...)
}
sort.Slice(peers, func(i, j int) bool { return peers[i].ID < peers[j].ID })
log.Trace().
Caller().
Str("machine", machine.Name).
Str("machine", machine.Hostname).
Msgf("Found total peers: %s", peers.String())
return peers, nil
@@ -370,7 +307,7 @@ func (h *Headscale) getValidPeers(machine *Machine) (Machines, error) {
}
for _, peer := range peers {
if peer.isRegistered() && !peer.isExpired() {
if !peer.isExpired() {
validPeers = append(validPeers, peer)
}
}
@@ -395,7 +332,7 @@ func (h *Headscale) GetMachine(namespace string, name string) (*Machine, error)
}
for _, m := range machines {
if m.Name == name {
if m.Hostname == name {
return &m, nil
}
}
@@ -425,9 +362,9 @@ func (h *Headscale) GetMachineByMachineKey(
return &m, nil
}
// UpdateMachine takes a Machine struct pointer (typically already loaded from database
// UpdateMachineFromDatabase takes a Machine struct pointer (typically already loaded from database
// and updates it with the latest data from the database.
func (h *Headscale) UpdateMachine(machine *Machine) error {
func (h *Headscale) UpdateMachineFromDatabase(machine *Machine) error {
if result := h.db.Find(machine).First(&machine); result.Error != nil {
return result.Error
}
@@ -435,18 +372,70 @@ func (h *Headscale) UpdateMachine(machine *Machine) error {
return nil
}
// SetTags takes a Machine struct pointer and update the forced tags.
func (h *Headscale) SetTags(machine *Machine, tags []string) error {
newTags := []string{}
for _, tag := range tags {
if !contains(newTags, tag) {
newTags = append(newTags, tag)
}
}
machine.ForcedTags = newTags
if err := h.UpdateACLRules(); err != nil && !errors.Is(err, errEmptyPolicy) {
return err
}
h.setLastStateChangeToNow(machine.Namespace.Name)
if err := h.db.Save(machine).Error; err != nil {
return fmt.Errorf("failed to update tags for machine in the database: %w", err)
}
return nil
}
// ExpireMachine takes a Machine struct and sets the expire field to now.
func (h *Headscale) ExpireMachine(machine *Machine) {
func (h *Headscale) ExpireMachine(machine *Machine) error {
now := time.Now()
machine.Expiry = &now
h.setLastStateChangeToNow(machine.Namespace.Name)
h.db.Save(machine)
if err := h.db.Save(machine).Error; err != nil {
return fmt.Errorf("failed to expire machine in the database: %w", err)
}
return nil
}
// RenameMachine takes a Machine struct and a new GivenName for the machines
// and renames it.
func (h *Headscale) RenameMachine(machine *Machine, newName string) error {
err := CheckForFQDNRules(
newName,
)
if err != nil {
log.Error().
Caller().
Str("func", "RenameMachine").
Str("machine", machine.Hostname).
Str("newName", newName).
Err(err)
return err
}
machine.GivenName = newName
h.setLastStateChangeToNow(machine.Namespace.Name)
if err := h.db.Save(machine).Error; err != nil {
return fmt.Errorf("failed to rename machine in the database: %w", err)
}
return nil
}
// RefreshMachine takes a Machine struct and sets the expire field to now.
func (h *Headscale) RefreshMachine(machine *Machine, expiry time.Time) {
func (h *Headscale) RefreshMachine(machine *Machine, expiry time.Time) error {
now := time.Now()
machine.LastSuccessfulUpdate = &now
@@ -454,18 +443,18 @@ func (h *Headscale) RefreshMachine(machine *Machine, expiry time.Time) {
h.setLastStateChangeToNow(machine.Namespace.Name)
h.db.Save(machine)
if err := h.db.Save(machine).Error; err != nil {
return fmt.Errorf(
"failed to refresh machine (update expiration) in the database: %w",
err,
)
}
return nil
}
// DeleteMachine softs deletes a Machine from the database.
func (h *Headscale) DeleteMachine(machine *Machine) error {
err := h.RemoveSharedMachineFromAllNamespaces(machine)
if err != nil && errors.Is(err, errMachineNotShared) {
return err
}
machine.Registered = false
h.db.Save(&machine) // we mark it as unregistered, just in case
if err := h.db.Delete(&machine).Error; err != nil {
return err
}
@@ -483,11 +472,6 @@ func (h *Headscale) TouchMachine(machine *Machine) error {
// HardDeleteMachine hard deletes a Machine from the database.
func (h *Headscale) HardDeleteMachine(machine *Machine) error {
err := h.RemoveSharedMachineFromAllNamespaces(machine)
if err != nil && errors.Is(err, errMachineNotShared) {
return err
}
if err := h.db.Unscoped().Delete(&machine).Error; err != nil {
return err
}
@@ -496,71 +480,46 @@ func (h *Headscale) HardDeleteMachine(machine *Machine) error {
}
// GetHostInfo returns a Hostinfo struct for the machine.
func (machine *Machine) GetHostInfo() (*tailcfg.Hostinfo, error) {
hostinfo := tailcfg.Hostinfo{}
if len(machine.HostInfo) != 0 {
hi, err := machine.HostInfo.MarshalJSON()
if err != nil {
return nil, err
}
err = json.Unmarshal(hi, &hostinfo)
if err != nil {
return nil, err
}
}
return &hostinfo, nil
func (machine *Machine) GetHostInfo() tailcfg.Hostinfo {
return tailcfg.Hostinfo(machine.HostInfo)
}
func (h *Headscale) isOutdated(machine *Machine) bool {
if err := h.UpdateMachine(machine); err != nil {
if err := h.UpdateMachineFromDatabase(machine); err != nil {
// It does not seem meaningful to propagate this error as the end result
// will have to be that the machine has to be considered outdated.
return true
}
sharedMachines, _ := h.getShared(machine)
namespaceSet := set.New(set.ThreadSafe)
namespaceSet.Add(machine.Namespace.Name)
// Check if any of our shared namespaces has updates that we have
// not propagated.
for _, sharedMachine := range sharedMachines {
namespaceSet.Add(sharedMachine.Namespace.Name)
}
namespaces := make([]string, namespaceSet.Size())
for index, namespace := range namespaceSet.List() {
if name, ok := namespace.(string); ok {
namespaces[index] = name
}
}
lastChange := h.getLastStateChange(namespaces...)
// Get the last update from all headscale namespaces to compare with our nodes
// last update.
// TODO(kradalby): Only request updates from namespaces where we can talk to nodes
// This would mostly be for a bit of performance, and can be calculated based on
// ACLs.
lastChange := h.getLastStateChange()
lastUpdate := machine.CreatedAt
if machine.LastSuccessfulUpdate != nil {
lastUpdate = *machine.LastSuccessfulUpdate
}
log.Trace().
Caller().
Str("machine", machine.Name).
Str("machine", machine.Hostname).
Time("last_successful_update", lastChange).
Time("last_state_change", lastUpdate).
Msgf("Checking if %s is missing updates", machine.Name)
Msgf("Checking if %s is missing updates", machine.Hostname)
return lastUpdate.Before(lastChange)
}
func (machine Machine) String() string {
return machine.Name
return machine.Hostname
}
func (machines Machines) String() string {
temp := make([]string, len(machines))
for index, machine := range machines {
temp[index] = machine.Name
temp[index] = machine.Hostname
}
return fmt.Sprintf("[ %s ](%d)", strings.Join(temp, ", "), len(temp))
@@ -571,7 +530,7 @@ func (machines MachinesP) String() string {
temp := make([]string, len(machines))
for index, machine := range machines {
temp[index] = machine.Name
temp[index] = machine.Hostname
}
return fmt.Sprintf("[ %s ](%d)", strings.Join(temp, ", "), len(temp))
@@ -644,55 +603,15 @@ func (machine Machine) toNode(
[]netaddr.IPPrefix{},
addrs...) // we append the node own IP, as it is required by the clients
// TODO(kradalby): Needs investigation, We probably dont need this condition
// now that we dont have shared nodes
if includeRoutes {
routesStr := []string{}
if len(machine.EnabledRoutes) != 0 {
allwIps, err := machine.EnabledRoutes.MarshalJSON()
if err != nil {
return nil, err
}
err = json.Unmarshal(allwIps, &routesStr)
if err != nil {
return nil, err
}
}
for _, routeStr := range routesStr {
ip, err := netaddr.ParseIPPrefix(routeStr)
if err != nil {
return nil, err
}
allowedIPs = append(allowedIPs, ip)
}
}
endpoints := []string{}
if len(machine.Endpoints) != 0 {
be, err := machine.Endpoints.MarshalJSON()
if err != nil {
return nil, err
}
err = json.Unmarshal(be, &endpoints)
if err != nil {
return nil, err
}
}
hostinfo := tailcfg.Hostinfo{}
if len(machine.HostInfo) != 0 {
hi, err := machine.HostInfo.MarshalJSON()
if err != nil {
return nil, err
}
err = json.Unmarshal(hi, &hostinfo)
if err != nil {
return nil, err
}
allowedIPs = append(allowedIPs, machine.EnabledRoutes...)
}
var derp string
if hostinfo.NetInfo != nil {
derp = fmt.Sprintf("127.3.3.40:%d", hostinfo.NetInfo.PreferredDERP)
if machine.HostInfo.NetInfo != nil {
derp = fmt.Sprintf("127.3.3.40:%d", machine.HostInfo.NetInfo.PreferredDERP)
} else {
derp = "127.3.3.40:0" // Zero means disconnected or unknown.
}
@@ -708,18 +627,27 @@ func (machine Machine) toNode(
if dnsConfig != nil && dnsConfig.Proxied { // MagicDNS
hostname = fmt.Sprintf(
"%s.%s.%s",
machine.Name,
strings.ReplaceAll(
machine.Namespace.Name,
"@",
".",
), // Replace @ with . for valid domain for machine
machine.GivenName,
machine.Namespace.Name,
baseDomain,
)
if len(hostname) > maxHostnameLength {
return nil, fmt.Errorf(
"hostname %q is too long it cannot except 255 ASCII chars: %w",
hostname,
errHostnameTooLong,
)
}
} else {
hostname = machine.Name
hostname = machine.GivenName
}
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{
ID: tailcfg.NodeID(machine.ID), // this is the actual ID
StableID: tailcfg.StableNodeID(
@@ -733,15 +661,16 @@ func (machine Machine) toNode(
DiscoKey: discoKey,
Addresses: addrs,
AllowedIPs: allowedIPs,
Endpoints: endpoints,
Endpoints: machine.Endpoints,
DERP: derp,
Hostinfo: hostinfo,
Online: &online,
Hostinfo: hostInfo.View(),
Created: machine.CreatedAt,
LastSeen: machine.LastSeen,
KeepAlive: true,
MachineAuthorized: machine.Registered,
MachineAuthorized: !machine.isExpired(),
Capabilities: []string{tailcfg.CapabilityFileSharing},
}
@@ -756,10 +685,10 @@ func (machine *Machine) toProto() *v1.Machine {
NodeKey: machine.NodeKey,
DiscoKey: machine.DiscoKey,
IpAddresses: machine.IPAddresses.ToStringSlice(),
Name: machine.Name,
Name: machine.Hostname,
GivenName: machine.GivenName,
Namespace: machine.Namespace.toProto(),
Registered: machine.Registered,
ForcedTags: machine.ForcedTags,
// TODO(kradalby): Implement register method enum converter
// RegisterMethod: ,
@@ -788,138 +717,129 @@ func (machine *Machine) toProto() *v1.Machine {
return machineProto
}
// RegisterMachine is executed from the CLI to register a new Machine using its MachineKey.
func (h *Headscale) RegisterMachine(
// getTags will return the tags of the current machine.
// Invalid tags are tags added by a user on a node, and that user doesn't have authority to add this tag.
// Valid tags are tags added by a user that is allowed in the ACL policy to add this tag.
func getTags(
aclPolicy *ACLPolicy,
machine Machine,
stripEmailDomain bool,
) ([]string, []string) {
validTags := make([]string, 0)
invalidTags := make([]string, 0)
if aclPolicy == nil {
return validTags, invalidTags
}
validTagMap := make(map[string]bool)
invalidTagMap := make(map[string]bool)
for _, tag := range machine.HostInfo.RequestTags {
owners, err := expandTagOwners(*aclPolicy, tag, stripEmailDomain)
if errors.Is(err, errInvalidTag) {
invalidTagMap[tag] = true
continue
}
var found bool
for _, owner := range owners {
if machine.Namespace.Name == owner {
found = true
}
}
if found {
validTagMap[tag] = true
} else {
invalidTagMap[tag] = true
}
}
for tag := range invalidTagMap {
invalidTags = append(invalidTags, tag)
}
for tag := range validTagMap {
validTags = append(validTags, tag)
}
return validTags, invalidTags
}
func (h *Headscale) RegisterMachineFromAuthCallback(
machineKeyStr string,
namespaceName string,
registrationMethod string,
) (*Machine, error) {
namespace, err := h.GetNamespace(namespaceName)
if err != nil {
return nil, err
}
if machineInterface, ok := h.registrationCache.Get(machineKeyStr); ok {
if registrationMachine, ok := machineInterface.(Machine); ok {
namespace, err := h.GetNamespace(namespaceName)
if err != nil {
return nil, fmt.Errorf(
"failed to find namespace in register machine from auth callback, %w",
err,
)
}
var machineKey key.MachinePublic
err = machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machineKeyStr)))
if err != nil {
return nil, err
}
registrationMachine.NamespaceID = namespace.ID
registrationMachine.RegisterMethod = registrationMethod
log.Trace().
Caller().
Str("machine_key_str", machineKeyStr).
Str("machine_key", machineKey.String()).
Msg("Registering machine")
machine, err := h.RegisterMachine(
registrationMachine,
)
machine, err := h.GetMachineByMachineKey(machineKey)
if err != nil {
return nil, err
}
// TODO(kradalby): Currently, if it fails to find a requested expiry, non will be set
// This means that if a user is to slow with register a machine, it will possibly not
// have the correct expiry.
requestedTime := time.Time{}
if requestedTimeIf, found := h.requestedExpiryCache.Get(machineKey.String()); found {
log.Trace().
Caller().
Str("machine", machine.Name).
Msg("Expiry time found in cache, assigning to node")
if reqTime, ok := requestedTimeIf.(time.Time); ok {
requestedTime = reqTime
return machine, err
} else {
return nil, errCouldNotConvertMachineInterface
}
}
if machine.isRegistered() {
log.Trace().
Caller().
Str("machine", machine.Name).
Msg("machine already registered, reauthenticating")
return nil, errMachineNotFoundRegistrationCache
}
h.RefreshMachine(machine, requestedTime)
return machine, nil
}
// RegisterMachine is executed from the CLI to register a new Machine using its MachineKey.
func (h *Headscale) RegisterMachine(machine Machine,
) (*Machine, error) {
log.Trace().
Caller().
Str("machine_key", machine.MachineKey).
Msg("Registering machine")
log.Trace().
Caller().
Str("machine", machine.Name).
Str("machine", machine.Hostname).
Msg("Attempting to register machine")
if machine.isRegistered() {
err := errMachineAlreadyRegistered
log.Error().
Caller().
Err(err).
Str("machine", machine.Name).
Msg("Attempting to register machine")
return nil, err
}
h.ipAllocationMutex.Lock()
defer h.ipAllocationMutex.Unlock()
ips, err := h.getAvailableIPs()
if err != nil {
log.Error().
Caller().
Err(err).
Str("machine", machine.Name).
Str("machine", machine.Hostname).
Msg("Could not find IP for the new machine")
return nil, err
}
log.Trace().
Caller().
Str("machine", machine.Name).
Str("ip", strings.Join(ips.ToStringSlice(), ",")).
Msg("Found IP for host")
machine.IPAddresses = ips
machine.NamespaceID = namespace.ID
machine.Registered = true
machine.RegisterMethod = RegisterMethodCLI
machine.Expiry = &requestedTime
h.db.Save(&machine)
if err := h.db.Save(&machine).Error; err != nil {
return nil, fmt.Errorf("failed register(save) machine in the database: %w", err)
}
log.Trace().
Caller().
Str("machine", machine.Name).
Str("machine", machine.Hostname).
Str("ip", strings.Join(ips.ToStringSlice(), ",")).
Msg("Machine registered with the database")
return machine, nil
return &machine, nil
}
func (machine *Machine) GetAdvertisedRoutes() ([]netaddr.IPPrefix, error) {
hostInfo, err := machine.GetHostInfo()
if err != nil {
return nil, err
}
return hostInfo.RoutableIPs, nil
func (machine *Machine) GetAdvertisedRoutes() []netaddr.IPPrefix {
return machine.HostInfo.RoutableIPs
}
func (machine *Machine) GetEnabledRoutes() ([]netaddr.IPPrefix, error) {
data, err := machine.EnabledRoutes.MarshalJSON()
if err != nil {
return nil, err
}
routesStr := []string{}
err = json.Unmarshal(data, &routesStr)
if err != nil {
return nil, err
}
routes := make([]netaddr.IPPrefix, len(routesStr))
for index, routeStr := range routesStr {
route, err := netaddr.ParseIPPrefix(routeStr)
if err != nil {
return nil, err
}
routes[index] = route
}
return routes, nil
func (machine *Machine) GetEnabledRoutes() []netaddr.IPPrefix {
return machine.EnabledRoutes
}
func (machine *Machine) IsRoutesEnabled(routeStr string) bool {
@@ -928,10 +848,7 @@ func (machine *Machine) IsRoutesEnabled(routeStr string) bool {
return false
}
enabledRoutes, err := machine.GetEnabledRoutes()
if err != nil {
return false
}
enabledRoutes := machine.GetEnabledRoutes()
for _, enabledRoute := range enabledRoutes {
if route == enabledRoute {
@@ -955,45 +872,61 @@ func (h *Headscale) EnableRoutes(machine *Machine, routeStrs ...string) error {
newRoutes[index] = route
}
availableRoutes, err := machine.GetAdvertisedRoutes()
if err != nil {
return err
}
for _, newRoute := range newRoutes {
if !containsIPPrefix(availableRoutes, newRoute) {
if !contains(machine.GetAdvertisedRoutes(), newRoute) {
return fmt.Errorf(
"route (%s) is not available on node %s: %w",
machine.Name,
machine.Hostname,
newRoute, errMachineRouteIsNotAvailable,
)
}
}
routes, err := json.Marshal(newRoutes)
if err != nil {
return err
}
machine.EnabledRoutes = newRoutes
machine.EnabledRoutes = datatypes.JSON(routes)
h.db.Save(&machine)
if err := h.db.Save(machine).Error; err != nil {
return fmt.Errorf("failed enable routes for machine in the database: %w", err)
}
return nil
}
func (machine *Machine) RoutesToProto() (*v1.Routes, error) {
availableRoutes, err := machine.GetAdvertisedRoutes()
if err != nil {
return nil, err
}
func (machine *Machine) RoutesToProto() *v1.Routes {
availableRoutes := machine.GetAdvertisedRoutes()
enabledRoutes, err := machine.GetEnabledRoutes()
if err != nil {
return nil, err
}
enabledRoutes := machine.GetEnabledRoutes()
return &v1.Routes{
AdvertisedRoutes: ipPrefixToString(availableRoutes),
EnabledRoutes: ipPrefixToString(enabledRoutes),
}, nil
}
}
func (h *Headscale) GenerateGivenName(suppliedName string) (string, error) {
// If a hostname is or will be longer than 63 chars after adding the hash,
// it needs to be trimmed.
trimmedHostnameLength := labelHostnameLength - MachineGivenNameHashLength - MachineGivenNameTrimSize
normalizedHostname, err := NormalizeToFQDNRules(
suppliedName,
h.cfg.OIDC.StripEmaildomain,
)
if err != nil {
return "", err
}
postfix, err := GenerateRandomStringDNSSafe(MachineGivenNameHashLength)
if err != nil {
return "", err
}
// Verify that that the new unique name is shorter than the maximum allowed
// DNS segment.
if len(normalizedHostname) <= trimmedHostnameLength {
normalizedHostname = fmt.Sprintf("%s-%s", normalizedHostname, postfix)
} else {
normalizedHostname = fmt.Sprintf("%s-%s", normalizedHostname[:trimmedHostnameLength], postfix)
}
return normalizedHostname, nil
}

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"reflect"
"strconv"
"strings"
"testing"
"time"
@@ -27,18 +28,14 @@ func (s *Suite) TestGetMachine(c *check.C) {
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testmachine",
Hostname: "testmachine",
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
}
app.db.Save(machine)
machineFromDB, err := app.GetMachine("test", "testmachine")
c.Assert(err, check.IsNil)
_, err = machineFromDB.GetHostInfo()
_, err = app.GetMachine("test", "testmachine")
c.Assert(err, check.IsNil)
}
@@ -57,18 +54,14 @@ func (s *Suite) TestGetMachineByID(c *check.C) {
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testmachine",
Hostname: "testmachine",
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
}
app.db.Save(&machine)
machineByID, err := app.GetMachineByID(0)
c.Assert(err, check.IsNil)
_, err = machineByID.GetHostInfo()
_, err = app.GetMachineByID(0)
c.Assert(err, check.IsNil)
}
@@ -80,9 +73,8 @@ func (s *Suite) TestDeleteMachine(c *check.C) {
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testmachine",
Hostname: "testmachine",
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(1),
}
@@ -103,9 +95,8 @@ func (s *Suite) TestHardDeleteMachine(c *check.C) {
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testmachine3",
Hostname: "testmachine3",
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(1),
}
@@ -118,7 +109,7 @@ func (s *Suite) TestHardDeleteMachine(c *check.C) {
c.Assert(err, check.NotNil)
}
func (s *Suite) TestGetDirectPeers(c *check.C) {
func (s *Suite) TestListPeers(c *check.C) {
namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil)
@@ -134,9 +125,8 @@ func (s *Suite) TestGetDirectPeers(c *check.C) {
MachineKey: "foo" + strconv.Itoa(index),
NodeKey: "bar" + strconv.Itoa(index),
DiscoKey: "faa" + strconv.Itoa(index),
Name: "testmachine" + strconv.Itoa(index),
Hostname: "testmachine" + strconv.Itoa(index),
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
}
@@ -146,16 +136,13 @@ func (s *Suite) TestGetDirectPeers(c *check.C) {
machine0ByID, err := app.GetMachineByID(0)
c.Assert(err, check.IsNil)
_, err = machine0ByID.GetHostInfo()
c.Assert(err, check.IsNil)
peersOfMachine0, err := app.getDirectPeers(machine0ByID)
peersOfMachine0, err := app.ListPeers(machine0ByID)
c.Assert(err, check.IsNil)
c.Assert(len(peersOfMachine0), check.Equals, 9)
c.Assert(peersOfMachine0[0].Name, check.Equals, "testmachine2")
c.Assert(peersOfMachine0[5].Name, check.Equals, "testmachine7")
c.Assert(peersOfMachine0[8].Name, check.Equals, "testmachine10")
c.Assert(peersOfMachine0[0].Hostname, check.Equals, "testmachine2")
c.Assert(peersOfMachine0[5].Hostname, check.Equals, "testmachine7")
c.Assert(peersOfMachine0[8].Hostname, check.Equals, "testmachine10")
}
func (s *Suite) TestGetACLFilteredPeers(c *check.C) {
@@ -186,9 +173,8 @@ func (s *Suite) TestGetACLFilteredPeers(c *check.C) {
IPAddresses: MachineAddresses{
netaddr.MustParseIP(fmt.Sprintf("100.64.0.%v", strconv.Itoa(index+1))),
},
Name: "testmachine" + strconv.Itoa(index),
Hostname: "testmachine" + strconv.Itoa(index),
NamespaceID: stor[index%2].namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(stor[index%2].key.ID),
}
@@ -202,8 +188,8 @@ func (s *Suite) TestGetACLFilteredPeers(c *check.C) {
Hosts: map[string]netaddr.IPPrefix{},
TagOwners: map[string][]string{},
ACLs: []ACL{
{Action: "accept", Users: []string{"admin"}, Ports: []string{"*:*"}},
{Action: "accept", Users: []string{"test"}, Ports: []string{"test:*"}},
{Action: "accept", Sources: []string{"admin"}, Destinations: []string{"*:*"}},
{Action: "accept", Sources: []string{"test"}, Destinations: []string{"test:*"}},
},
Tests: []ACLTest{},
}
@@ -212,17 +198,14 @@ func (s *Suite) TestGetACLFilteredPeers(c *check.C) {
c.Assert(err, check.IsNil)
adminMachine, err := app.GetMachineByID(1)
c.Logf("Machine(%v), namespace: %v", adminMachine.Name, adminMachine.Namespace)
c.Logf("Machine(%v), namespace: %v", adminMachine.Hostname, adminMachine.Namespace)
c.Assert(err, check.IsNil)
testMachine, err := app.GetMachineByID(2)
c.Logf("Machine(%v), namespace: %v", testMachine.Name, testMachine.Namespace)
c.Logf("Machine(%v), namespace: %v", testMachine.Hostname, testMachine.Namespace)
c.Assert(err, check.IsNil)
_, err = testMachine.GetHostInfo()
c.Assert(err, check.IsNil)
machines, err := app.ListAllMachines()
machines, err := app.ListMachines()
c.Assert(err, check.IsNil)
peersOfTestMachine := getFilteredByACLPeers(machines, app.aclRules, testMachine)
@@ -230,15 +213,15 @@ func (s *Suite) TestGetACLFilteredPeers(c *check.C) {
c.Log(peersOfTestMachine)
c.Assert(len(peersOfTestMachine), check.Equals, 4)
c.Assert(peersOfTestMachine[0].Name, check.Equals, "testmachine4")
c.Assert(peersOfTestMachine[1].Name, check.Equals, "testmachine6")
c.Assert(peersOfTestMachine[3].Name, check.Equals, "testmachine10")
c.Assert(peersOfTestMachine[0].Hostname, check.Equals, "testmachine4")
c.Assert(peersOfTestMachine[1].Hostname, check.Equals, "testmachine6")
c.Assert(peersOfTestMachine[3].Hostname, check.Equals, "testmachine10")
c.Log(peersOfAdminMachine)
c.Assert(len(peersOfAdminMachine), check.Equals, 9)
c.Assert(peersOfAdminMachine[0].Name, check.Equals, "testmachine2")
c.Assert(peersOfAdminMachine[2].Name, check.Equals, "testmachine4")
c.Assert(peersOfAdminMachine[5].Name, check.Equals, "testmachine7")
c.Assert(peersOfAdminMachine[0].Hostname, check.Equals, "testmachine2")
c.Assert(peersOfAdminMachine[2].Hostname, check.Equals, "testmachine4")
c.Assert(peersOfAdminMachine[5].Hostname, check.Equals, "testmachine7")
}
func (s *Suite) TestExpireMachine(c *check.C) {
@@ -256,9 +239,8 @@ func (s *Suite) TestExpireMachine(c *check.C) {
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testmachine",
Hostname: "testmachine",
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
Expiry: &time.Time{},
@@ -267,10 +249,12 @@ func (s *Suite) TestExpireMachine(c *check.C) {
machineFromDB, err := app.GetMachine("test", "testmachine")
c.Assert(err, check.IsNil)
c.Assert(machineFromDB, check.NotNil)
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)
}
@@ -296,6 +280,201 @@ func (s *Suite) TestSerdeAddressStrignSlice(c *check.C) {
}
}
func (s *Suite) TestSetTags(c *check.C) {
namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
c.Assert(err, check.IsNil)
_, err = app.GetMachine("test", "testmachine")
c.Assert(err, check.NotNil)
machine := &Machine{
ID: 0,
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Hostname: "testmachine",
NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
}
app.db.Save(machine)
// assign simple tags
sTags := []string{"tag:test", "tag:foo"}
err = app.SetTags(machine, sTags)
c.Assert(err, check.IsNil)
machine, err = app.GetMachine("test", "testmachine")
c.Assert(err, check.IsNil)
c.Assert(machine.ForcedTags, check.DeepEquals, StringList(sTags))
// assign duplicat tags, expect no errors but no doubles in DB
eTags := []string{"tag:bar", "tag:test", "tag:unknown", "tag:test"}
err = app.SetTags(machine, eTags)
c.Assert(err, check.IsNil)
machine, err = app.GetMachine("test", "testmachine")
c.Assert(err, check.IsNil)
c.Assert(
machine.ForcedTags,
check.DeepEquals,
StringList([]string{"tag:bar", "tag:test", "tag:unknown"}),
)
}
func Test_getTags(t *testing.T) {
type args struct {
aclPolicy *ACLPolicy
machine Machine
stripEmailDomain bool
}
tests := []struct {
name string
args args
wantInvalid []string
wantValid []string
}{
{
name: "valid tag one machine",
args: args{
aclPolicy: &ACLPolicy{
TagOwners: TagOwners{
"tag:valid": []string{"joe"},
},
},
machine: Machine{
Namespace: Namespace{
Name: "joe",
},
HostInfo: HostInfo{
RequestTags: []string{"tag:valid"},
},
},
stripEmailDomain: false,
},
wantValid: []string{"tag:valid"},
wantInvalid: nil,
},
{
name: "invalid tag and valid tag one machine",
args: args{
aclPolicy: &ACLPolicy{
TagOwners: TagOwners{
"tag:valid": []string{"joe"},
},
},
machine: Machine{
Namespace: Namespace{
Name: "joe",
},
HostInfo: HostInfo{
RequestTags: []string{"tag:valid", "tag:invalid"},
},
},
stripEmailDomain: false,
},
wantValid: []string{"tag:valid"},
wantInvalid: []string{"tag:invalid"},
},
{
name: "multiple invalid and identical tags, should return only one invalid tag",
args: args{
aclPolicy: &ACLPolicy{
TagOwners: TagOwners{
"tag:valid": []string{"joe"},
},
},
machine: Machine{
Namespace: Namespace{
Name: "joe",
},
HostInfo: HostInfo{
RequestTags: []string{
"tag:invalid",
"tag:valid",
"tag:invalid",
},
},
},
stripEmailDomain: false,
},
wantValid: []string{"tag:valid"},
wantInvalid: []string{"tag:invalid"},
},
{
name: "only invalid tags",
args: args{
aclPolicy: &ACLPolicy{
TagOwners: TagOwners{
"tag:valid": []string{"joe"},
},
},
machine: Machine{
Namespace: Namespace{
Name: "joe",
},
HostInfo: HostInfo{
RequestTags: []string{"tag:invalid", "very-invalid"},
},
},
stripEmailDomain: false,
},
wantValid: nil,
wantInvalid: []string{"tag:invalid", "very-invalid"},
},
{
name: "empty ACLPolicy should return empty tags and should not panic",
args: args{
aclPolicy: nil,
machine: Machine{
Namespace: Namespace{
Name: "joe",
},
HostInfo: HostInfo{
RequestTags: []string{"tag:invalid", "very-invalid"},
},
},
stripEmailDomain: false,
},
wantValid: nil,
wantInvalid: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
gotValid, gotInvalid := getTags(
test.args.aclPolicy,
test.args.machine,
test.args.stripEmailDomain,
)
for _, valid := range gotValid {
if !contains(test.wantValid, valid) {
t.Errorf(
"valids: getTags() = %v, want %v",
gotValid,
test.wantValid,
)
break
}
}
for _, invalid := range gotInvalid {
if !contains(test.wantInvalid, invalid) {
t.Errorf(
"invalids: getTags() = %v, want %v",
gotInvalid,
test.wantInvalid,
)
break
}
}
})
}
}
// nolint
func Test_getFilteredByACLPeers(t *testing.T) {
type args struct {
machines []Machine
@@ -443,7 +622,7 @@ func Test_getFilteredByACLPeers(t *testing.T) {
},
},
machine: &Machine{ // current machine
ID: 1,
ID: 2,
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
Namespace: Namespace{Name: "marc"},
},
@@ -456,6 +635,208 @@ func Test_getFilteredByACLPeers(t *testing.T) {
},
},
},
{
name: "rules allows all hosts to reach one destination",
args: args{
machines: []Machine{ // list of all machines in the database
{
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
ID: 2,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "marc"},
},
{
ID: 3,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
},
Namespace: Namespace{Name: "mickael"},
},
},
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
{
SrcIPs: []string{"*"},
DstPorts: []tailcfg.NetPortRange{
{IP: "100.64.0.2"},
},
},
},
machine: &Machine{ // current machine
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
},
want: Machines{
{
ID: 2,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "marc"},
},
},
},
{
name: "rules allows all hosts to reach one destination, destination can reach all hosts",
args: args{
machines: []Machine{ // list of all machines in the database
{
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
ID: 2,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "marc"},
},
{
ID: 3,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
},
Namespace: Namespace{Name: "mickael"},
},
},
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
{
SrcIPs: []string{"*"},
DstPorts: []tailcfg.NetPortRange{
{IP: "100.64.0.2"},
},
},
},
machine: &Machine{ // current machine
ID: 2,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "marc"},
},
},
want: Machines{
{
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
ID: 3,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
},
Namespace: Namespace{Name: "mickael"},
},
},
},
{
name: "rule allows all hosts to reach all destinations",
args: args{
machines: []Machine{ // list of all machines in the database
{
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
ID: 2,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "marc"},
},
{
ID: 3,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
},
Namespace: Namespace{Name: "mickael"},
},
},
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
{
SrcIPs: []string{"*"},
DstPorts: []tailcfg.NetPortRange{
{IP: "*"},
},
},
},
machine: &Machine{ // current machine
ID: 2,
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
Namespace: Namespace{Name: "marc"},
},
},
want: Machines{
{
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
ID: 3,
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.3")},
Namespace: Namespace{Name: "mickael"},
},
},
},
{
name: "without rule all communications are forbidden",
args: args{
machines: []Machine{ // list of all machines in the database
{
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
ID: 2,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "marc"},
},
{
ID: 3,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
},
Namespace: Namespace{Name: "mickael"},
},
},
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
},
machine: &Machine{ // current machine
ID: 2,
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
Namespace: Namespace{Name: "marc"},
},
},
want: Machines{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -470,3 +851,137 @@ func Test_getFilteredByACLPeers(t *testing.T) {
})
}
}
func TestHeadscale_GenerateGivenName(t *testing.T) {
type args struct {
suppliedName string
}
tests := []struct {
name string
h *Headscale
args args
want string
wantErr bool
}{
{
name: "simple machine name generation",
h: &Headscale{
cfg: &Config{
OIDC: OIDCConfig{
StripEmaildomain: true,
},
},
},
args: args{
suppliedName: "testmachine",
},
want: "testmachine",
wantErr: false,
},
{
name: "machine name with 53 chars",
h: &Headscale{
cfg: &Config{
OIDC: OIDCConfig{
StripEmaildomain: true,
},
},
},
args: args{
suppliedName: "testmaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaachine",
},
want: "testmaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaachine",
wantErr: false,
},
{
name: "machine name with 60 chars",
h: &Headscale{
cfg: &Config{
OIDC: OIDCConfig{
StripEmaildomain: true,
},
},
},
args: args{
suppliedName: "testmaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaachine1234567",
},
want: "testmaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaachine",
wantErr: false,
},
{
name: "machine name with 63 chars",
h: &Headscale{
cfg: &Config{
OIDC: OIDCConfig{
StripEmaildomain: true,
},
},
},
args: args{
suppliedName: "testmaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaachine1234567890",
},
want: "testmaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
wantErr: false,
},
{
name: "machine name with 64 chars",
h: &Headscale{
cfg: &Config{
OIDC: OIDCConfig{
StripEmaildomain: true,
},
},
},
args: args{
suppliedName: "testmaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaachine1234567891",
},
want: "testmaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
wantErr: false,
},
{
name: "machine name with 73 chars",
h: &Headscale{
cfg: &Config{
OIDC: OIDCConfig{
StripEmaildomain: true,
},
},
},
args: args{
suppliedName: "testmaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaachine12345678901234567890",
},
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.h.GenerateGivenName(tt.args.suppliedName)
if (err != nil) != tt.wantErr {
t.Errorf(
"Headscale.GenerateGivenName() error = %v, wantErr %v",
err,
tt.wantErr,
)
return
}
if tt.want != "" && strings.Contains(tt.want, got) {
t.Errorf(
"Headscale.GenerateGivenName() = %v, is not a substring of %v",
tt.want,
got,
)
}
if len(got) > labelHostnameLength {
t.Errorf(
"Headscale.GenerateGivenName() = %v is larger than allowed DNS segment %d",
got,
labelHostnameLength,
)
}
})
}
}

View File

@@ -2,7 +2,10 @@ package headscale
import (
"errors"
"fmt"
"regexp"
"strconv"
"strings"
"time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
@@ -16,8 +19,16 @@ const (
errNamespaceExists = Error("Namespace already exists")
errNamespaceNotFound = Error("Namespace not found")
errNamespaceNotEmptyOfNodes = Error("Namespace not empty: node(s) found")
errInvalidNamespaceName = Error("Invalid namespace name")
)
const (
// value related to RFC 1123 and 952.
labelHostnameLength = 63
)
var invalidCharsInNamespaceRegex = regexp.MustCompile("[^a-z0-9-.]+")
// Namespace is the way Headscale implements the concept of users in Tailscale
//
// At the end of the day, users in Tailscale are some kind of 'bubbles' or namespaces
@@ -30,6 +41,10 @@ type Namespace struct {
// CreateNamespace creates a new Namespace. Returns error if could not be created
// or another namespace already exists.
func (h *Headscale) CreateNamespace(name string) (*Namespace, error) {
err := CheckForFQDNRules(name)
if err != nil {
return nil, err
}
namespace := Namespace{}
if err := h.db.Where("name = ?", name).First(&namespace).Error; err == nil {
return nil, errNamespaceExists
@@ -84,10 +99,15 @@ func (h *Headscale) DestroyNamespace(name string) error {
// RenameNamespace renames a Namespace. Returns error if the Namespace does
// not exist or if another Namespace exists with the new name.
func (h *Headscale) RenameNamespace(oldName, newName string) error {
var err error
oldNamespace, err := h.GetNamespace(oldName)
if err != nil {
return err
}
err = CheckForFQDNRules(newName)
if err != nil {
return err
}
_, err = h.GetNamespace(newName)
if err == nil {
return errNamespaceExists
@@ -128,8 +148,27 @@ func (h *Headscale) ListNamespaces() ([]Namespace, error) {
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.
func (h *Headscale) ListMachinesInNamespace(name string) ([]Machine, error) {
err := CheckForFQDNRules(name)
if err != nil {
return nil, err
}
namespace, err := h.GetNamespace(name)
if err != nil {
return nil, err
@@ -143,39 +182,20 @@ func (h *Headscale) ListMachinesInNamespace(name string) ([]Machine, error) {
return machines, nil
}
// ListSharedMachinesInNamespace returns all the machines that are shared to the specified namespace.
func (h *Headscale) ListSharedMachinesInNamespace(name string) ([]Machine, error) {
namespace, err := h.GetNamespace(name)
if err != nil {
return nil, err
}
sharedMachines := []SharedMachine{}
if err := h.db.Preload("Namespace").Where(&SharedMachine{NamespaceID: namespace.ID}).Find(&sharedMachines).Error; err != nil {
return nil, err
}
machines := []Machine{}
for _, sharedMachine := range sharedMachines {
machine, err := h.GetMachineByID(
sharedMachine.MachineID,
) // otherwise not everything comes filled
if err != nil {
return nil, err
}
machines = append(machines, *machine)
}
return machines, nil
}
// SetMachineNamespace assigns a Machine to a namespace.
func (h *Headscale) SetMachineNamespace(machine *Machine, namespaceName string) error {
err := CheckForFQDNRules(namespaceName)
if err != nil {
return err
}
namespace, err := h.GetNamespace(namespaceName)
if err != nil {
return err
}
machine.NamespaceID = namespace.ID
h.db.Save(&machine)
machine.Namespace = *namespace
if result := h.db.Save(&machine); result.Error != nil {
return result.Error
}
return nil
}
@@ -233,3 +253,55 @@ func (n *Namespace) toProto() *v1.Namespace {
CreatedAt: timestamppb.New(n.CreatedAt),
}
}
// NormalizeToFQDNRules will replace forbidden chars in namespace
// it can also return an error if the namespace doesn't respect RFC 952 and 1123.
func NormalizeToFQDNRules(name string, stripEmailDomain bool) (string, error) {
name = strings.ToLower(name)
name = strings.ReplaceAll(name, "'", "")
atIdx := strings.Index(name, "@")
if stripEmailDomain && atIdx > 0 {
name = name[:atIdx]
} else {
name = strings.ReplaceAll(name, "@", ".")
}
name = invalidCharsInNamespaceRegex.ReplaceAllString(name, "-")
for _, elt := range strings.Split(name, ".") {
if len(elt) > labelHostnameLength {
return "", fmt.Errorf(
"label %v is more than 63 chars: %w",
elt,
errInvalidNamespaceName,
)
}
}
return name, nil
}
func CheckForFQDNRules(name string) error {
if len(name) > labelHostnameLength {
return fmt.Errorf(
"DNS segment must not be over 63 chars. %v doesn't comply with this rule: %w",
name,
errInvalidNamespaceName,
)
}
if strings.ToLower(name) != name {
return fmt.Errorf(
"DNS segment should be lowercase. %v doesn't comply with this rule: %w",
name,
errInvalidNamespaceName,
)
}
if invalidCharsInNamespaceRegex.MatchString(name) {
return fmt.Errorf(
"DNS segment should only be composed of lowercase ASCII letters numbers, hyphen and dots. %v doesn't comply with theses rules: %w",
name,
errInvalidNamespaceName,
)
}
return nil
}

View File

@@ -1,7 +1,8 @@
package headscale
import (
"github.com/rs/zerolog/log"
"testing"
"gopkg.in/check.v1"
"gorm.io/gorm"
"inet.af/netaddr"
@@ -51,9 +52,8 @@ func (s *Suite) TestDestroyNamespaceErrors(c *check.C) {
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testmachine",
Hostname: "testmachine",
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
}
@@ -72,23 +72,23 @@ func (s *Suite) TestRenameNamespace(c *check.C) {
c.Assert(err, check.IsNil)
c.Assert(len(namespaces), check.Equals, 1)
err = app.RenameNamespace("test", "test_renamed")
err = app.RenameNamespace("test", "test-renamed")
c.Assert(err, check.IsNil)
_, err = app.GetNamespace("test")
c.Assert(err, check.Equals, errNamespaceNotFound)
_, err = app.GetNamespace("test_renamed")
_, err = app.GetNamespace("test-renamed")
c.Assert(err, check.IsNil)
err = app.RenameNamespace("test_does_not_exit", "test")
err = app.RenameNamespace("test-does-not-exit", "test")
c.Assert(err, check.Equals, errNamespaceNotFound)
namespaceTest2, err := app.CreateNamespace("test2")
c.Assert(err, check.IsNil)
c.Assert(namespaceTest2.Name, check.Equals, "test2")
err = app.RenameNamespace("test2", "test_renamed")
err = app.RenameNamespace("test2", "test-renamed")
c.Assert(err, check.Equals, errNamespaceExists)
}
@@ -142,17 +142,16 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
Name: "test_get_shared_nodes_1",
Hostname: "test_get_shared_nodes_1",
NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")},
AuthKeyID: uint(preAuthKeyShared1.ID),
}
app.db.Save(machineInShared1)
_, err = app.GetMachine(namespaceShared1.Name, machineInShared1.Name)
_, err = app.GetMachine(namespaceShared1.Name, machineInShared1.Hostname)
c.Assert(err, check.IsNil)
machineInShared2 := &Machine{
@@ -160,17 +159,16 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Name: "test_get_shared_nodes_2",
Hostname: "test_get_shared_nodes_2",
NamespaceID: namespaceShared2.ID,
Namespace: *namespaceShared2,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.2")},
AuthKeyID: uint(preAuthKeyShared2.ID),
}
app.db.Save(machineInShared2)
_, err = app.GetMachine(namespaceShared2.Name, machineInShared2.Name)
_, err = app.GetMachine(namespaceShared2.Name, machineInShared2.Hostname)
c.Assert(err, check.IsNil)
machineInShared3 := &Machine{
@@ -178,17 +176,16 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Name: "test_get_shared_nodes_3",
Hostname: "test_get_shared_nodes_3",
NamespaceID: namespaceShared3.ID,
Namespace: *namespaceShared3,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.3")},
AuthKeyID: uint(preAuthKeyShared3.ID),
}
app.db.Save(machineInShared3)
_, err = app.GetMachine(namespaceShared3.Name, machineInShared3.Name)
_, err = app.GetMachine(namespaceShared3.Name, machineInShared3.Hostname)
c.Assert(err, check.IsNil)
machine2InShared1 := &Machine{
@@ -196,18 +193,15 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Name: "test_get_shared_nodes_4",
Hostname: "test_get_shared_nodes_4",
NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")},
AuthKeyID: uint(preAuthKey2Shared1.ID),
}
app.db.Save(machine2InShared1)
err = app.AddSharedMachineToNamespace(machineInShared2, namespaceShared1)
c.Assert(err, check.IsNil)
peersOfMachine1InShared1, err := app.getPeers(machineInShared1)
c.Assert(err, check.IsNil)
@@ -216,8 +210,7 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
peersOfMachine1InShared1,
)
log.Trace().Msgf("userProfiles %#v", userProfiles)
c.Assert(len(userProfiles), check.Equals, 2)
c.Assert(len(userProfiles), check.Equals, 3)
found := false
for _, userProfiles := range userProfiles {
@@ -239,3 +232,180 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
}
c.Assert(found, check.Equals, true)
}
func TestNormalizeToFQDNRules(t *testing.T) {
type args struct {
name string
stripEmailDomain bool
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "normalize simple name",
args: args{
name: "normalize-simple.name",
stripEmailDomain: false,
},
want: "normalize-simple.name",
wantErr: false,
},
{
name: "normalize an email",
args: args{
name: "foo.bar@example.com",
stripEmailDomain: false,
},
want: "foo.bar.example.com",
wantErr: false,
},
{
name: "normalize an email domain should be removed",
args: args{
name: "foo.bar@example.com",
stripEmailDomain: true,
},
want: "foo.bar",
wantErr: false,
},
{
name: "strip enabled no email passed as argument",
args: args{
name: "not-email-and-strip-enabled",
stripEmailDomain: true,
},
want: "not-email-and-strip-enabled",
wantErr: false,
},
{
name: "normalize complex email",
args: args{
name: "foo.bar+complex-email@example.com",
stripEmailDomain: false,
},
want: "foo.bar-complex-email.example.com",
wantErr: false,
},
{
name: "namespace name with space",
args: args{
name: "name space",
stripEmailDomain: false,
},
want: "name-space",
wantErr: false,
},
{
name: "namespace with quote",
args: args{
name: "Jamie's iPhone 5",
stripEmailDomain: false,
},
want: "jamies-iphone-5",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := NormalizeToFQDNRules(tt.args.name, tt.args.stripEmailDomain)
if (err != nil) != tt.wantErr {
t.Errorf(
"NormalizeToFQDNRules() error = %v, wantErr %v",
err,
tt.wantErr,
)
return
}
if got != tt.want {
t.Errorf("NormalizeToFQDNRules() = %v, want %v", got, tt.want)
}
})
}
}
func TestCheckForFQDNRules(t *testing.T) {
type args struct {
name string
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "valid: namespace",
args: args{name: "valid-namespace"},
wantErr: false,
},
{
name: "invalid: capitalized namespace",
args: args{name: "Invalid-CapItaLIzed-namespace"},
wantErr: true,
},
{
name: "invalid: email as namespace",
args: args{name: "foo.bar@example.com"},
wantErr: true,
},
{
name: "invalid: chars in namespace name",
args: args{name: "super-namespace+name"},
wantErr: true,
},
{
name: "invalid: too long name for namespace",
args: args{
name: "super-long-namespace-name-that-should-be-a-little-more-than-63-chars",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := CheckForFQDNRules(tt.args.name); (err != nil) != tt.wantErr {
t.Errorf("CheckForFQDNRules() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func (s *Suite) TestSetMachineNamespace(c *check.C) {
oldNamespace, err := app.CreateNamespace("old")
c.Assert(err, check.IsNil)
newNamespace, err := app.CreateNamespace("new")
c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(oldNamespace.Name, false, false, nil)
c.Assert(err, check.IsNil)
machine := Machine{
ID: 0,
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Hostname: "testmachine",
NamespaceID: oldNamespace.ID,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
}
app.db.Save(&machine)
c.Assert(machine.NamespaceID, check.Equals, oldNamespace.ID)
err = app.SetMachineNamespace(&machine, newNamespace.Name)
c.Assert(err, check.IsNil)
c.Assert(machine.NamespaceID, check.Equals, newNamespace.ID)
c.Assert(machine.Namespace.Name, check.Equals, newNamespace.Name)
err = app.SetMachineNamespace(&machine, "non-existing-namespace")
c.Assert(err, check.Equals, errNamespaceNotFound)
err = app.SetMachineNamespace(&machine, newNamespace.Name)
c.Assert(err, check.IsNil)
c.Assert(machine.NamespaceID, check.Equals, newNamespace.ID)
c.Assert(machine.Namespace.Name, check.Equals, newNamespace.Name)
}

458
oidc.go
View File

@@ -9,23 +9,18 @@ import (
"fmt"
"html/template"
"net/http"
"regexp"
"strings"
"time"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/gin-gonic/gin"
"github.com/patrickmn/go-cache"
"github.com/gorilla/mux"
"github.com/rs/zerolog/log"
"golang.org/x/oauth2"
"gorm.io/gorm"
"tailscale.com/types/key"
)
const (
oidcStateCacheExpiration = time.Minute * 5
oidcStateCacheCleanupInterval = time.Minute * 10
randomByteSize = 16
randomByteSize = 16
)
type IDTokenClaims struct {
@@ -58,28 +53,27 @@ func (h *Headscale) initOIDC() error {
"%s/oidc/callback",
strings.TrimSuffix(h.cfg.ServerURL, "/"),
),
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
Scopes: h.cfg.OIDC.Scope,
}
}
// init the state cache if it hasn't been already
if h.oidcStateCache == nil {
h.oidcStateCache = cache.New(
oidcStateCacheExpiration,
oidcStateCacheCleanupInterval,
)
}
return nil
}
// RegisterOIDC redirects to the OIDC provider for authentication
// Puts machine key in cache so the callback can retrieve it using the oidc state param
// Listens in /oidc/register/:mKey.
func (h *Headscale) RegisterOIDC(ctx *gin.Context) {
machineKeyStr := ctx.Param("mkey")
if machineKeyStr == "" {
ctx.String(http.StatusBadRequest, "Wrong params")
func (h *Headscale) RegisterOIDC(
writer http.ResponseWriter,
req *http.Request,
) {
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
}
@@ -94,7 +88,7 @@ func (h *Headscale) RegisterOIDC(ctx *gin.Context) {
log.Error().
Caller().
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
}
@@ -102,12 +96,19 @@ func (h *Headscale) RegisterOIDC(ctx *gin.Context) {
stateStr := hex.EncodeToString(randomBlob)[:32]
// place the machine key into the state cache, so it can be retrieved later
h.oidcStateCache.Set(stateStr, machineKeyStr, oidcStateCacheExpiration)
h.registrationCache.Set(stateStr, machineKeyStr, registerCacheExpiration)
authURL := h.oauth2Config.AuthCodeURL(stateStr)
// Add any extra parameter provided in the configuration to the Authorize Endpoint request
extras := make([]oauth2.AuthCodeOption, 0, len(h.cfg.OIDC.ExtraParams))
for k, v := range h.cfg.OIDC.ExtraParams {
extras = append(extras, oauth2.SetAuthURLParam(k, v))
}
authURL := h.oauth2Config.AuthCodeURL(stateStr, extras...)
log.Debug().Msgf("Redirecting to %s for authentication", authURL)
ctx.Redirect(http.StatusFound, authURL)
http.Redirect(writer, req, authURL, http.StatusFound)
}
type oidcCallbackTemplateConfig struct {
@@ -126,25 +127,47 @@ var oidcCallbackTemplate = template.Must(
</html>`),
)
// TODO: Why is the entire machine registration logic duplicated here?
// OIDCCallback handles the callback from the OIDC endpoint
// Retrieves the mkey from the state cache and adds the machine to the users email namespace
// TODO: A confirmation page for new machines should be added to avoid phishing vulnerabilities
// TODO: Add groups information from OIDC tokens into machine HostInfo
// Listens in /oidc/callback.
func (h *Headscale) OIDCCallback(ctx *gin.Context) {
code := ctx.Query("code")
state := ctx.Query("state")
func (h *Headscale) OIDCCallback(
writer http.ResponseWriter,
req *http.Request,
) {
code := req.URL.Query().Get("code")
state := req.URL.Query().Get("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
}
oauth2Token, err := h.oauth2Config.Exchange(context.Background(), code)
if err != nil {
ctx.String(http.StatusBadRequest, "Could not exchange code for token")
log.Error().
Err(err).
Caller().
Msg("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
}
@@ -157,7 +180,15 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
rawIDToken, rawIDTokenOK := oauth2Token.Extra("id_token").(string)
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
}
@@ -170,7 +201,15 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
Err(err).
Caller().
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
}
@@ -189,74 +228,133 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
Err(err).
Caller().
Msg("Failed to decode id token claims")
ctx.String(
http.StatusBadRequest,
"Failed to decode id token claims",
)
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusBadRequest)
_, err := writer.Write([]byte("Failed to decode id token claims"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return
}
// If AllowedDomains is provided, check that the authenticated principal ends with @<alloweddomain>.
if len(h.cfg.OIDC.AllowedDomains) > 0 {
if at := strings.LastIndex(claims.Email, "@"); at < 0 ||
!IsStringInSlice(h.cfg.OIDC.AllowedDomains, claims.Email[at+1:]) {
log.Error().Msg("authenticated principal does not match any allowed domain")
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusBadRequest)
_, err := writer.Write([]byte("unauthorized principal (domain mismatch)"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return
}
}
// If AllowedUsers is provided, check that the authenticated princial is part of that list.
if len(h.cfg.OIDC.AllowedUsers) > 0 &&
!IsStringInSlice(h.cfg.OIDC.AllowedUsers, claims.Email) {
log.Error().Msg("authenticated principal does not match any allowed user")
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
}
// retrieve machinekey from state cache
machineKeyIf, machineKeyFound := h.oidcStateCache.Get(state)
machineKeyIf, machineKeyFound := h.registrationCache.Get(state)
if !machineKeyFound {
log.Error().
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
}
machineKeyStr, machineKeyOK := machineKeyIf.(string)
machineKeyFromCache, machineKeyOK := machineKeyIf.(string)
var machineKey key.MachinePublic
err = machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machineKeyStr)))
err = machineKey.UnmarshalText(
[]byte(MachinePublicKeyEnsurePrefix(machineKeyFromCache)),
)
if err != nil {
log.Error().
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
}
if !machineKeyOK {
log.Error().Msg("could not get machine key from cache")
ctx.String(
http.StatusInternalServerError,
"could not get machine key from cache",
)
return
}
// TODO(kradalby): Currently, if it fails to find a requested expiry, non will be set
requestedTime := time.Time{}
if requestedTimeIf, found := h.requestedExpiryCache.Get(machineKey.String()); found {
if reqTime, ok := requestedTimeIf.(time.Time); ok {
requestedTime = reqTime
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusInternalServerError)
_, err := writer.Write([]byte("could not get machine key from cache"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
}
// retrieve machine information
machine, err := h.GetMachineByMachineKey(machineKey)
if err != nil {
log.Error().Msg("machine key not found in database")
ctx.String(
http.StatusInternalServerError,
"could not get machine info from database",
)
return
}
if machine.isRegistered() {
// retrieve machine information if it exist
// The error is not important, because if it does not
// exist, then this is a new machine and we will move
// on to registration.
machine, _ := h.GetMachineByMachineKey(machineKey)
if machine != nil {
log.Trace().
Caller().
Str("machine", machine.Name).
Str("machine", machine.Hostname).
Msg("machine already registered, reauthenticating")
h.RefreshMachine(machine, requestedTime)
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
if err := oidcCallbackTemplate.Execute(&content, oidcCallbackTemplateConfig{
@@ -268,123 +366,151 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
Str("type", "reauthenticate").
Err(err).
Msg("Could not render OIDC callback template")
ctx.Data(
http.StatusInternalServerError,
"text/html; charset=utf-8",
[]byte("Could not render OIDC callback template"),
)
}
ctx.Data(http.StatusOK, "text/html; charset=utf-8", content.Bytes())
return
}
now := time.Now().UTC()
if namespaceName, ok := h.getNamespaceFromEmail(claims.Email); ok {
// register the machine if it's new
if !machine.Registered {
log.Debug().Msg("Registering new machine after successful callback")
namespace, err := h.GetNamespace(namespaceName)
if errors.Is(err, gorm.ErrRecordNotFound) {
namespace, err = h.CreateNamespace(namespaceName)
if err != nil {
log.Error().
Err(err).
Caller().
Msgf("could not create new namespace '%s'", namespaceName)
ctx.String(
http.StatusInternalServerError,
"could not create new namespace",
)
return
}
} else if err != nil {
log.Error().
Caller().
Err(err).
Str("namespace", namespaceName).
Msg("could not find or create namespace")
ctx.String(
http.StatusInternalServerError,
"could not find or create namespace",
)
return
}
ips, err := h.getAvailableIPs()
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusInternalServerError)
_, err := writer.Write([]byte("Could not render OIDC callback template"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("could not get an IP from the pool")
ctx.String(
http.StatusInternalServerError,
"could not get an IP from the pool",
)
return
Msg("Failed to write response")
}
machine.IPAddresses = ips
machine.NamespaceID = namespace.ID
machine.Registered = true
machine.RegisterMethod = RegisterMethodOIDC
machine.LastSuccessfulUpdate = &now
machine.Expiry = &requestedTime
h.db.Save(&machine)
return
}
var content bytes.Buffer
if err := oidcCallbackTemplate.Execute(&content, oidcCallbackTemplateConfig{
User: claims.Email,
Verb: "Authenticated",
}); err != nil {
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, err = writer.Write(content.Bytes())
if err != nil {
log.Error().
Str("func", "OIDCCallback").
Str("type", "authenticate").
Caller().
Err(err).
Msg("Could not render OIDC callback template")
ctx.Data(
http.StatusInternalServerError,
"text/html; charset=utf-8",
[]byte("Could not render OIDC callback template"),
)
Msg("Failed to write response")
}
ctx.Data(http.StatusOK, "text/html; charset=utf-8", content.Bytes())
return
}
log.Error().
Caller().
Str("email", claims.Email).
Str("username", claims.Username).
Str("machine", machine.Name).
Msg("Email could not be mapped to a namespace")
ctx.String(
http.StatusBadRequest,
"email from claim could not be mapped to a namespace",
namespaceName, err := NormalizeToFQDNRules(
claims.Email,
h.cfg.OIDC.StripEmaildomain,
)
}
// getNamespaceFromEmail passes the users email through a list of "matchers"
// and iterates through them until it matches and returns a namespace.
// If no match is found, an empty string will be returned.
// TODO(kradalby): golang Maps key order is not stable, so this list is _not_ deterministic. Find a way to make the list of keys stable, preferably in the order presented in a users configuration.
func (h *Headscale) getNamespaceFromEmail(email string) (string, bool) {
for match, namespace := range h.cfg.OIDC.MatchMap {
regex := regexp.MustCompile(match)
if regex.MatchString(email) {
return namespace, true
if err != nil {
log.Error().Err(err).Caller().Msgf("couldn't normalize email")
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusInternalServerError)
_, err := writer.Write([]byte("couldn't normalize email"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return
}
return "", false
// register the machine if it's new
log.Debug().Msg("Registering new machine after successful callback")
namespace, err := h.GetNamespace(namespaceName)
if errors.Is(err, errNamespaceNotFound) {
namespace, err = h.CreateNamespace(namespaceName)
if err != nil {
log.Error().
Err(err).
Caller().
Msgf("could not create new namespace '%s'", namespaceName)
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusInternalServerError)
_, err := writer.Write([]byte("could not create namespace"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return
}
} else if err != nil {
log.Error().
Caller().
Err(err).
Str("namespace", namespaceName).
Msg("could not find or create namespace")
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusInternalServerError)
_, err := writer.Write([]byte("could not find or create namespace"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return
}
machineKeyStr := MachinePublicKeyStripPrefix(machineKey)
_, err = h.RegisterMachineFromAuthCallback(
machineKeyStr,
namespace.Name,
RegisterMethodOIDC,
)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("could not register machine")
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusInternalServerError)
_, err := writer.Write([]byte("could not register machine"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return
}
var content bytes.Buffer
if err := oidcCallbackTemplate.Execute(&content, oidcCallbackTemplateConfig{
User: claims.Email,
Verb: "Authenticated",
}); err != nil {
log.Error().
Str("func", "OIDCCallback").
Str("type", "authenticate").
Err(err).
Msg("Could not render OIDC callback template")
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusInternalServerError)
_, err := writer.Write([]byte("Could not render OIDC callback template"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return
}
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

@@ -1,180 +0,0 @@
package headscale
import (
"sync"
"testing"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/patrickmn/go-cache"
"golang.org/x/oauth2"
"gorm.io/gorm"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
func TestHeadscale_getNamespaceFromEmail(t *testing.T) {
type fields struct {
cfg Config
db *gorm.DB
dbString string
dbType string
dbDebug bool
privateKey *key.MachinePrivate
aclPolicy *ACLPolicy
aclRules []tailcfg.FilterRule
lastStateChange sync.Map
oidcProvider *oidc.Provider
oauth2Config *oauth2.Config
oidcStateCache *cache.Cache
}
type args struct {
email string
}
tests := []struct {
name string
fields fields
args args
want string
want1 bool
}{
{
name: "match all",
fields: fields{
cfg: Config{
OIDC: OIDCConfig{
MatchMap: map[string]string{
".*": "space",
},
},
},
},
args: args{
email: "test@example.no",
},
want: "space",
want1: true,
},
{
name: "match user",
fields: fields{
cfg: Config{
OIDC: OIDCConfig{
MatchMap: map[string]string{
"specific@user\\.no": "user-namespace",
},
},
},
},
args: args{
email: "specific@user.no",
},
want: "user-namespace",
want1: true,
},
{
name: "match domain",
fields: fields{
cfg: Config{
OIDC: OIDCConfig{
MatchMap: map[string]string{
".*@example\\.no": "example",
},
},
},
},
args: args{
email: "test@example.no",
},
want: "example",
want1: true,
},
{
name: "multi match domain",
fields: fields{
cfg: Config{
OIDC: OIDCConfig{
MatchMap: map[string]string{
".*@example\\.no": "exammple",
".*@gmail\\.com": "gmail",
},
},
},
},
args: args{
email: "someuser@gmail.com",
},
want: "gmail",
want1: true,
},
{
name: "no match domain",
fields: fields{
cfg: Config{
OIDC: OIDCConfig{
MatchMap: map[string]string{
".*@dontknow.no": "never",
},
},
},
},
args: args{
email: "test@wedontknow.no",
},
want: "",
want1: false,
},
{
name: "multi no match domain",
fields: fields{
cfg: Config{
OIDC: OIDCConfig{
MatchMap: map[string]string{
".*@dontknow.no": "never",
".*@wedontknow.no": "other",
".*\\.no": "stuffy",
},
},
},
},
args: args{
email: "tasy@nonofthem.com",
},
want: "",
want1: false,
},
}
//nolint
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
app := &Headscale{
cfg: test.fields.cfg,
db: test.fields.db,
dbString: test.fields.dbString,
dbType: test.fields.dbType,
dbDebug: test.fields.dbDebug,
privateKey: test.fields.privateKey,
aclPolicy: test.fields.aclPolicy,
aclRules: test.fields.aclRules,
lastStateChange: test.fields.lastStateChange,
oidcProvider: test.fields.oidcProvider,
oauth2Config: test.fields.oauth2Config,
oidcStateCache: test.fields.oidcStateCache,
}
got, got1 := app.getNamespaceFromEmail(test.args.email)
if got != test.want {
t.Errorf(
"Headscale.getNamespaceFromEmail() got = %v, want %v",
got,
test.want,
)
}
if got1 != test.want1 {
t.Errorf(
"Headscale.getNamespaceFromEmail() got1 = %v, want %v",
got1,
test.want1,
)
}
})
}
}

Some files were not shown because too many files have changed in this diff Show More