Compare commits

...

395 Commits

Author SHA1 Message Date
Juan Font Alonso
41515532b6 Unpin buf setup to try to fix linting issues 2022-09-06 20:42:06 +02:00
Juan Font
449a135b94 Merge pull request #791 from kradalby/add-logo
Add logo files and add to readme
2022-09-05 15:34:00 +02:00
Kristoffer Dalby
002d484abe use logo in readme correcly 2022-09-05 15:31:03 +02:00
Kristoffer Dalby
9823ef2af5 use logo in readme 2022-09-05 15:30:19 +02:00
Kristoffer Dalby
641c6fd439 Add logo files 2022-09-05 15:29:30 +02:00
Juan Font
af60ffb7fa Merge pull request #788 from juanfont/warn-websockets-requirement
Warn when Headscale is running behind an improperly configured proxy
2022-09-04 16:44:54 +02:00
Juan Font Alonso
c28e559da4 Updated changelog 2022-09-04 16:23:46 +02:00
Juan Font Alonso
5c59255b41 Also warn in DERP server if Websockets are not properly working 2022-09-04 16:13:48 +02:00
Juan Font Alonso
a377ee14b4 Minor message change 2022-09-04 16:13:30 +02:00
Juan Font Alonso
2262188d8a Warn when Headscale is running behind a wrongly configured proxy 2022-09-04 16:05:21 +02:00
Juan Font
7c49c752a9 Merge pull request #786 from juanfont/update-deps-20220904
Update dependencies 20220904
2022-09-04 16:01:52 +02:00
Juan Font Alonso
e29726cc50 Updated nix sum 2022-09-04 15:45:35 +02:00
Juan Font Alonso
3c73cbe92b Merge branch 'main' into update-deps-20220904 2022-09-04 15:37:25 +02:00
Juan Font Alonso
cc357062be Missing go.sum 2022-09-04 15:36:59 +02:00
Juan Font Alonso
17c06f7167 Upgrade direct dependencies 2022-09-04 15:36:00 +02:00
Juan Font
d12e0156c3 Merge pull request #784 from juanfont/fix-go-1.19-lint
Fix linting broken after Go 1.19
2022-09-04 15:31:41 +02:00
Juan Font Alonso
204dedaa49 Only pass the context in pollmap, no req needed 2022-09-04 15:14:12 +02:00
Juan Font Alonso
52073ce7c9 Pass context in OIDC helpers 2022-09-04 15:02:18 +02:00
Juan Font Alonso
434747e007 Use timeout in lets encrypt http server 2022-09-04 11:47:05 +02:00
Juan Font Alonso
7a78314d9d Remove nolint directives 2022-09-04 11:44:24 +02:00
Juan Font Alonso
f23e9dc235 Pass the req context when pinging the DB 2022-09-04 11:43:09 +02:00
Juan Font Alonso
4527801d48 More unused parameters removed in protocol functions 2022-09-04 11:41:31 +02:00
Juan Font Alonso
e0857f0226 Removed unused parameters in protocol functions 2022-09-04 11:40:14 +02:00
Juan Font Alonso
0d074b1da6 setLastStateChangeToNow was always receiving nil 2022-09-04 11:37:49 +02:00
Juan Font Alonso
f2fda4f906 Return error on marshaling issues 2022-09-04 11:36:03 +02:00
Juan Font Alonso
c1c36036ae Add timeouts for the Noise server 2022-09-04 11:35:39 +02:00
Juan Font Alonso
9a1438d2e3 Use inherited context 2022-09-04 11:35:13 +02:00
Juan Font Alonso
582122851d Go do not like underscores in packages 2022-09-04 11:34:23 +02:00
Juan Font Alonso
f4d197485c Use library const for HTTP verbs 2022-09-04 11:33:00 +02:00
Juan Font Alonso
68305df9b2 Applied gofumpt 2022-09-04 11:32:29 +02:00
Juan Font Alonso
ca0be81833 Target the latest version for golint 2022-09-04 11:31:06 +02:00
Juan Font
380fbfe438 Merge pull request #780 from juanfont/bump-ts1.30
Target Tailscale version to v1.30.0 (and replace inet.af/netaddr with net/netip)
2022-09-04 09:24:42 +02:00
Juan Font Alonso
32d68a40d5 Update flake sha 2022-09-04 00:00:23 +02:00
Juan Font Alonso
198e92c08f Remove dependency on netaddr 2022-09-03 23:46:14 +02:00
Juan Font Alonso
38b26f5285 Merge branch 'main' into bump-ts1.30 2022-09-03 23:33:09 +02:00
Juan Font
096a009685 Merge pull request #781 from juanfont/switch-to-nix-unstable
Switch to Nix unstable for Go 1.19
2022-09-03 23:32:51 +02:00
Juan Font Alonso
30c0fdb38d Update changelog 2022-09-03 23:19:07 +02:00
Juan Font Alonso
663dbf7395 Use go 1.19 in Nix 2022-09-03 23:06:21 +02:00
Juan Font Alonso
373db0dc5e Switch to nix unstable 2022-09-03 23:05:34 +02:00
Juan Font Alonso
2733fb30cc Minor change in go.mod 2022-09-03 16:23:36 +02:00
Juan Font Alonso
d29411408b Merge branch 'main' into bump-tailscale-v1.30 2022-09-03 16:20:25 +02:00
Juan Font
24bafdf2bb Merge pull request #778 from juanfont/bump-go-1.19
Target Go 1.19 for Headscale
2022-09-03 13:16:48 +02:00
Juan Font Alonso
2c5bf6982c Updated changelog 2022-09-03 12:24:22 +02:00
Juan Font Alonso
dd3ec84000 Minor doc change 2022-09-03 12:22:03 +02:00
Juan Font Alonso
84044e236d Release using go 1.19 2022-09-03 12:21:54 +02:00
Juan Font Alonso
2ddf7ab515 Use Go 1.19 in Dockerfiles 2022-09-03 12:21:35 +02:00
Juan Font Alonso
f519c513c2 Target go 1.19 in go.mod 2022-09-03 12:21:04 +02:00
Juan Font Alonso
d5cc5b2bc8 Move integration tests to net/netip 2022-09-02 09:22:34 +02:00
Juan Font Alonso
51abf90db6 Use net/netip in derp server 2022-09-02 09:16:19 +02:00
Juan Font Alonso
71410cb6da Port dns to net/netip 2022-09-02 09:15:05 +02:00
Juan Font Alonso
efb12f208c Move db to net/netip 2022-09-02 09:13:50 +02:00
Juan Font Alonso
64ede5dbef Move namespaces unit tests to net/netip 2022-09-02 09:13:07 +02:00
Juan Font Alonso
7af78152a4 Migrate routes to net/netip 2022-09-02 00:06:19 +02:00
Juan Font Alonso
290ec8bb19 Migrate ACLs to net/netip 2022-09-02 00:05:43 +02:00
Juan Font Alonso
cdf48b1216 Migrate utils to net/netip 2022-09-02 00:05:18 +02:00
Juan Font Alonso
a24710a961 Migrate machine to net/netip 2022-09-02 00:04:31 +02:00
Juan Font Alonso
197da8afcb Migrate config.go to net/netip 2022-09-02 00:04:04 +02:00
Juan Font Alonso
12385d4357 Target Tailscale v1.30.0 2022-09-01 20:50:56 +02:00
Juan Font
e7f8bb866f Merge pull request #772 from juanfont/enable-1.30-in-tests
Add Tailscale v1.30.0 to the integration test roaster
2022-09-01 00:03:21 +02:00
Juan Font Alonso
1ad19a3bd8 Add 1.30.0 to the version roaster 2022-08-31 22:17:13 +02:00
Kristoffer Dalby
cc0bec15ef Merge pull request #760 from juanfont/update-contributors 2022-08-23 21:21:50 +02:00
github-actions[bot]
20970b580a docs(README): update contributors 2022-08-22 12:47:42 +00:00
Juan Font
53857d418a Merge pull request #756 from huskyii/env_config
Env config
2022-08-22 14:47:01 +02:00
Jiang Zhu
a81a4d274f Update CHANGELOG.md 2022-08-22 20:20:20 +08:00
Jiang Zhu
ce4a1cf447 1. add noise key to config file
2. lower node check interval
2022-08-22 00:35:08 +08:00
Jiang Zhu
35dd9209b9 update CHANGELOG.md 2022-08-21 23:51:04 +08:00
Jiang Zhu
81f91f03b4 add env var to specify config location 2022-08-21 23:51:04 +08:00
Juan Font
84a5edf345 Merge pull request #738 from juanfont/hs2021-v2
Implement TS2021 protocol in headscale
2022-08-21 16:02:28 +02:00
Juan Font Alonso
4aafe6c9d1 Added line in CHANGELOG 2022-08-21 12:32:01 +02:00
Juan Font
3ab1487641 Merge branch 'main' into hs2021-v2 2022-08-21 11:57:33 +02:00
Kristoffer Dalby
0c7f1eac82 Merge pull request #757 from juanfont/changelog-0.16.4 2022-08-21 11:15:30 +02:00
Juan Font Alonso
6fe895fd22 Updated changelog for 0.16.4 2022-08-21 10:51:58 +02:00
Juan Font Alonso
71d22dc994 Added missing files 2022-08-21 10:47:45 +02:00
Juan Font Alonso
4424a9abc0 Noise private key now a nested field in config 2022-08-21 10:42:23 +02:00
Juan Font Alonso
e20e818a42 Integrate expiration fixes (#754) in TS2021 branch 2022-08-20 11:46:44 +02:00
Juan Font
061e2fe4b4 Merge pull request #754 from Aluxima/expired-machine-registration
Fix cli registration of expired machines
2022-08-20 11:41:15 +02:00
Juan Font Alonso
f0a8a2857b Clarified why we have a different key 2022-08-20 00:23:33 +02:00
Juan Font Alonso
175dfa1ede Update flake.nix sum 2022-08-20 00:15:46 +02:00
Juan Font Alonso
04e4fa785b Updated dependencies 2022-08-20 00:11:07 +02:00
Juan Font Alonso
6aec520889 Merge branch 'hs2021-v2' of https://github.com/juanfont/headscale into hs2021-v2 2022-08-20 00:06:58 +02:00
Juan Font Alonso
e9906b522f Use upstream AcceptHTTP for the Noise upgrade 2022-08-20 00:06:26 +02:00
Juan Font
2f554133c5 Move comment up
Co-authored-by: Kristoffer Dalby <kradalby@kradalby.no>
2022-08-19 23:49:06 +02:00
Juan Font Alonso
922b8b5365 Merge branch 'hs2021-v2' of https://github.com/juanfont/headscale into hs2021-v2 2022-08-19 16:32:18 +02:00
Juan Font Alonso
c894db3dd4 Use common core for noise registration 2022-08-19 16:29:04 +02:00
Laurent Marchaud
e85562268d Switch to using nodeKey instead of machineKey for expired machines registration
Signed-off-by: Laurent Marchaud <laurent@marchaud.com>
2022-08-19 15:48:35 +02:00
Laurent Marchaud
fca33aacbe Fix rebased errors scope in machine.go
Signed-off-by: Laurent Marchaud <laurent@marchaud.com>
2022-08-19 15:07:01 +02:00
Juan Font
e43713a866 Merge branch 'main' into hs2021-v2 2022-08-19 15:02:01 +02:00
Juan Font Alonso
b6e3cd81c6 Fixed minor linting things 2022-08-19 14:27:40 +02:00
Juan Font Alonso
43ad0d4416 Removed unused method 2022-08-19 14:24:43 +02:00
Juan Font Alonso
a33b5a5c00 Merge branch 'hs2021-v2' of https://github.com/juanfont/headscale into hs2021-v2 2022-08-19 14:20:55 +02:00
Juan Font Alonso
e2bffd4f5a Make legacy protocol use common methods for client registration 2022-08-19 14:20:24 +02:00
Juan Font Alonso
a87a9636e3 Expanded response marshal methods to support legacy and Noise 2022-08-19 14:19:29 +02:00
Laurent Marchaud
a31432ee7b Fix changelog
Signed-off-by: Laurent Marchaud <laurent@marchaud.com>
2022-08-19 14:14:30 +02:00
Laurent Marchaud
0c66590108 Update changelog
Signed-off-by: Laurent Marchaud <laurent@marchaud.com>
2022-08-19 14:11:19 +02:00
Laurent Marchaud
c6ea9b4b80 Fix cli registration of expired machines
Signed-off-by: Laurent Marchaud <laurent@marchaud.com>
2022-08-19 14:11:13 +02:00
Juan Font
19455399f4 Merge pull request #752 from juanfont/add-code-of-conduct
Create CODE_OF_CONDUCT.md
2022-08-19 00:38:01 +02:00
Juan Font
43ba1fb176 Prettier 2022-08-18 22:32:53 +00:00
Juan Font
a6f56b4285 Create CODE_OF_CONDUCT.md 2022-08-18 22:08:33 +02:00
Juan Font
9d430d3c72 Update noise.go
Co-authored-by: Kristoffer Dalby <kradalby@kradalby.no>
2022-08-18 21:33:56 +02:00
Juan Font Alonso
f9a2a2b57a Add docker DNS IP to the remaining files 2022-08-18 18:07:15 +02:00
Juan Font Alonso
e4d961cfad Merge branch 'hs2021-v2' of https://github.com/juanfont/headscale into hs2021-v2 2022-08-18 17:57:06 +02:00
Juan Font
67ffebc30a Merge branch 'main' into hs2021-v2 2022-08-18 17:56:56 +02:00
Juan Font Alonso
cf731fafab Catch retry error in taildrop send 2022-08-18 17:56:01 +02:00
Juan Font Alonso
f43a83aad7 Find out IPv4 for taildrop 2022-08-18 17:53:36 +02:00
Juan Font Alonso
7185f8dfea Only use released versions in public integration tests 2022-08-18 17:53:25 +02:00
Juan Font Alonso
8a707de5f1 Add local Docker DNS server (makes resolving http://headscale more reliable) 2022-08-18 17:53:04 +02:00
Juan Font
61bb6292b7 Merge pull request #746 from gozssky/patch-1
Fix charset typo in swagger.go
2022-08-18 12:23:45 +02:00
Juan Font
40e0ae99da Merge branch 'main' into patch-1 2022-08-18 11:49:15 +02:00
Juan Font
2dd615a4ef Merge pull request #745 from 617a7a/main
feat: add support for TLS with Postgres
2022-08-18 11:48:33 +02:00
Azz
7e06abdca2 chore: azz forgot how to write code 2022-08-17 20:12:45 +01:00
Azz
c316f53e23 fix: ci happy now? 2022-08-17 19:32:20 +01:00
Azz
b6d324be69 Merge branch 'main' of https://github.com/juanfont/headscale
# Conflicts:
#	CHANGELOG.md
2022-08-17 19:31:26 +01:00
Juan Font
f7380312d3 Merge pull request #747 from juanfont/fix-oidc
Fix error decoding OIDC claims (#744)
2022-08-17 18:30:50 +02:00
Juan Font
287309b65c Update changelog 2022-08-17 15:08:29 +00:00
Juan Font
cc3de7e723 Fix error decoding claims (#744) 2022-08-17 15:03:10 +00:00
Yujie Xia
e03b3029e3 Fix charset typo in swagger.go 2022-08-17 12:27:58 +08:00
Juan Font Alonso
ba07bac46a Use IPv4 in the tests 2022-08-16 18:42:22 +02:00
Juan Font Alonso
b71a881d0e Retry magicdns tests 2022-08-16 18:19:04 +02:00
Juan Font Alonso
ce53bb0eee Minor changes to HEAD Dockerfile 2022-08-16 17:52:59 +02:00
Juan Font Alonso
c0fe1abf4d Use node_key to find peers 2022-08-16 17:51:43 +02:00
Juan Font Alonso
0db7fc5ab7 Mark all namespaces to lastChange now 2022-08-16 13:39:15 +02:00
azz
701ad3e017 chore: update CHANGELOG.md 2022-08-16 09:09:28 +01:00
azz
0cc14d0aca feat: added db_ssl to config-example.yaml 2022-08-16 09:02:51 +01:00
Azz
3f5ea7998f Merge branch 'main' into main 2022-08-16 08:56:36 +01:00
azz
4c7f54020b feat: add support for TLS with Postgres 2022-08-16 08:50:30 +01:00
Juan Font Alonso
eb461d0713 Enable HEAD and unstable in integration tests 2022-08-16 00:18:02 +02:00
Juan Font Alonso
128ec6717c Merge branch 'hs2021-v2' of https://github.com/juanfont/headscale into hs2021-v2 2022-08-15 23:35:24 +02:00
Juan Font Alonso
b3cf5289f8 Use CapVer to offer Noise only to supported clients 2022-08-15 23:35:06 +02:00
Juan Font
c701f9e817 Merge branch 'main' into hs2021-v2 2022-08-15 22:56:39 +02:00
Juan Font
e1a95e2057 Merge pull request #734 from vtrf/postgres-connection-string
Add ability to connect to PostgreSQL via unix socket
2022-08-15 19:20:01 +02:00
Victor Freire
0a5db52855 Add ability to connect to PostgreSQL via unix socket 2022-08-15 11:55:38 -03:00
Juan Font
7197ade4b4 Merge branch 'main' into postgres-connection-string 2022-08-15 13:37:09 +02:00
Juan Font Alonso
865f1ffb3c Fix issues with DERP integration tests due to tailscale/tailscale#4323 2022-08-15 11:25:47 +02:00
Juan Font Alonso
8db7629edf Fix config file in integration tests for Noise 2022-08-15 10:53:06 +02:00
Juan Font Alonso
b8980b9ed3 More minor logging stuff 2022-08-15 10:44:22 +02:00
Juan Font Alonso
5cf9eedf42 Minor logging corrections 2022-08-15 10:43:39 +02:00
Juan Font
193b4213b3 Merge pull request #739 from juanfont/updated-changelog-0.16.2
Added changelog entries for 0.16.x
2022-08-14 23:27:27 +02:00
Juan Font Alonso
8557bcedae Added changelog entries for 0.16.x 2022-08-14 23:22:41 +02:00
Juan Font Alonso
f599bea216 Fixed issue when not using compression 2022-08-14 23:15:41 +02:00
Juan Font Alonso
704a19b0a5 Removed legacy method to generate MapResponse 2022-08-14 23:13:07 +02:00
Juan Font Alonso
e29b344e0f Move Noise poll to new file, and use common poll 2022-08-14 23:12:18 +02:00
Juan Font Alonso
7cc227d01e Added Noise field to logging 2022-08-14 23:11:33 +02:00
Juan Font Alonso
df8ecdb603 Working on common codebase for poll, starting with legacy 2022-08-14 22:57:03 +02:00
Juan Font Alonso
f4bab6b290 Created common methods for keep and map poll responses 2022-08-14 22:50:39 +02:00
Juan Font Alonso
35f3dee1d0 Move Noise API to new file 2022-08-14 21:19:52 +02:00
Juan Font Alonso
db89fdea23 Added file for legacy protocol 2022-08-14 21:16:29 +02:00
Juan Font Alonso
d0898ecabc Move common parts of the protocol to dedicated file 2022-08-14 21:15:58 +02:00
Juan Font Alonso
e640c6df05 Fixes in Noise poll (clients should work now) 2022-08-14 21:10:08 +02:00
Juan Font Alonso
ab18c721bb Support for Noise machines in getPeers 2022-08-14 21:07:29 +02:00
Juan Font Alonso
aaa33cf093 Minor change in router 2022-08-14 21:07:05 +02:00
Juan Font Alonso
0f09e19e38 Updated go.mod checksum 2022-08-14 17:09:14 +02:00
Juan Font Alonso
b301405f24 Merge branch 'hs2021-v2' of https://github.com/juanfont/headscale into hs2021-v2 2022-08-14 17:06:03 +02:00
Juan Font Alonso
1f3032ad21 Merge branch 'main' into hs2021-v2 2022-08-14 17:05:51 +02:00
Juan Font Alonso
c10142f767 Added noise poll handler 2022-08-14 17:05:04 +02:00
Juan Font Alonso
0d0042b7e6 Added zstd constant for linting 2022-08-14 17:04:07 +02:00
Juan Font Alonso
78a179c971 Minor update in docs 2022-08-14 16:53:54 +02:00
Juan Font Alonso
cab828c9d4 Fixed unit tests to load config 2022-08-14 16:52:57 +02:00
Juan Font Alonso
ff46f3ff49 Move reusable method to common api file 2022-08-14 16:13:17 +02:00
Juan Font
b67cff50f5 Merge branch 'main' into hs2021-v2 2022-08-14 13:44:12 +02:00
Juan Font
e29ac8a4ab Merge pull request #737 from juanfont/fix-machinekey-oidc
Fixed another recurrence of MachineKey
2022-08-14 13:44:01 +02:00
Juan Font Alonso
20d2615081 Check json encoder errors 2022-08-14 12:47:04 +02:00
Juan Font
7fb2f83540 Merge branch 'main' into fix-machinekey-oidc 2022-08-14 12:44:30 +02:00
Juan Font Alonso
eb8d8f142c And more linting stuff 2022-08-14 12:44:07 +02:00
Juan Font Alonso
3bea20850a Some linting fixes 2022-08-14 12:40:22 +02:00
Juan Font Alonso
ade1b73779 Output an error when a user runs headscale without noise_private_key_path defined 2022-08-14 12:35:14 +02:00
Juan Font Alonso
281ae59b5a Update integration tests to work with Noise protocol 2022-08-14 12:18:33 +02:00
Juan Font Alonso
90bb6ea907 Minor formatting changes 2022-08-14 12:10:20 +02:00
Juan Font Alonso
5b14cafddd Fixed another recurrence of MachineKey 2022-08-14 12:04:31 +02:00
Juan Font Alonso
9994fce9d5 Fixed some linting errors 2022-08-14 12:00:43 +02:00
Kristoffer Dalby
c19e1a481e Merge pull request #736 from juanfont/update-contributors 2022-08-14 08:16:58 +02:00
Juan Font Alonso
39b85b02bb Move getMapResponse into reusable function by TS2019 and TS2021 2022-08-14 03:20:53 +02:00
Juan Font Alonso
7a91c82cda Merge branch 'main' into hs2021-v2 2022-08-14 03:07:43 +02:00
Juan Font Alonso
c7cea9ef16 updated paths 2022-08-14 03:07:28 +02:00
github-actions[bot]
d56b409cb9 docs(README): update contributors 2022-08-13 20:44:38 +00:00
Juan Font
ee8f38111e Merge pull request #735 from juanfont/fix-expired-url
Fix expired node registration URL
2022-08-13 22:44:04 +02:00
Juan Font Alonso
8c13f64d3c Changed missing path 2022-08-13 21:55:44 +02:00
Juan Font Alonso
a7efc22045 Fix expired node registration URL 2022-08-13 21:17:05 +02:00
Juan Font Alonso
1880035f6f Add registration handler over Noise protocol 2022-08-13 21:12:19 +02:00
Juan Font Alonso
fdd0c50402 Added helper method to fetch machines by any nodekey + tests 2022-08-13 21:03:02 +02:00
Juan Font Alonso
be24bacb79 Add noise mux and Noise path to base router 2022-08-13 20:55:37 +02:00
Juan Font Alonso
b261d19cfe Added Noise upgrade handler and Noise mux 2022-08-13 20:52:11 +02:00
Victor Freire
ec5acf7be2 Add ability to connect to PostgreSQL via unix socket 2022-08-13 11:34:12 -03:00
Juan Font Alonso
014e7abc68 Make private key errors constants 2022-08-13 14:46:23 +02:00
Juan Font Alonso
3e8f0e9984 Added support for Noise clients in /key handler 2022-08-13 11:24:05 +02:00
Juan Font Alonso
6e8e2bf508 Generate and read the Noise private key 2022-08-13 11:14:38 +02:00
Juan Font
09cd7ba304 Merge pull request #725 from juanfont/switch-to-db-d
Improve registration protocol implementation and switch to NodeKey as main identifier
2022-08-12 09:56:17 +02:00
Juan Font Alonso
77bf1e81ec Added missing dot in comment 2022-08-12 09:36:17 +02:00
Juan Font Alonso
a9b9a2942d Update changelog 2022-08-12 09:31:11 +02:00
Juan Font
a261e27113 Update api.go
Co-authored-by: Kristoffer Dalby <kradalby@kradalby.no>
2022-08-12 09:03:32 +02:00
Juan Font
f01a33491b Update api.go
Co-authored-by: Kristoffer Dalby <kradalby@kradalby.no>
2022-08-12 09:03:11 +02:00
Juan Font
739e11e1ee Update api.go
Co-authored-by: Kristoffer Dalby <kradalby@kradalby.no>
2022-08-12 09:02:58 +02:00
Juan Font
393aae01df Merge branch 'main' into switch-to-db-d 2022-08-11 15:02:08 +02:00
Juan Font
73cd428ed2 Merge pull request #729 from juanfont/fix-reuse-of-ns
Minor fix to linting issue introduced when fixing excludeCorrectlyTaggedNodes (#707)
2022-08-11 15:01:53 +02:00
Juan Font Alonso
1e7b57e513 Minor fix to linting issue introduced in #707 2022-08-11 14:12:45 +02:00
Juan Font Alonso
e1e3feb6a8 Add a sleep to reduce the impact of #727 2022-08-11 13:37:25 +02:00
Juan Font
8e56d8b425 Merge branch 'main' into switch-to-db-d 2022-08-11 13:11:38 +02:00
Juan Font
6c8445988c Merge pull request #707 from restanrm/fix-bug-in-excludecorrectlytaggednodes
Fix bug in excludeCorrectlyTaggedNodes
2022-08-11 13:08:43 +02:00
Adrien Raffin-Caboisse
110b01befa Merge remote-tracking branch 'origin/main' into fix-bug-in-excludecorrectlytaggednodes 2022-08-11 12:49:26 +02:00
Juan Font Alonso
d586b9d285 Added comment clarifying registration API 2022-08-11 12:16:50 +02:00
Juan Font Alonso
804d70386d Switch to nodekey in urls 2022-08-11 12:15:16 +02:00
Juan Font Alonso
fb3b2e6bc8 Improve protocol implementation for client registration (fixes #706) 2022-08-11 12:11:02 +02:00
Juan Font Alonso
030d7264e6 Fixed comment for linting 2022-08-10 16:03:33 +02:00
Juan Font Alonso
e91c378bd4 Replace machine key with node key in preparation for Noise in auth related stuff 2022-08-10 15:35:26 +02:00
Juan Font Alonso
e950b3be29 Add method to fetch by nodekey 2022-08-10 13:15:31 +02:00
Juan Font
dbf0e206b8 Merge pull request #722 from juanfont/bump-versions-20220810
Update dependencies versions
2022-08-10 11:25:03 +02:00
Juan Font Alonso
84f66090fd Updated CHangelog and flake 2022-08-10 11:04:42 +02:00
Juan Font Alonso
f8958d4e22 Update xsync library (helps in #704) 2022-08-10 10:55:45 +02:00
Juan Font Alonso
70807e40f6 Updated base dependencies 2022-08-10 10:54:23 +02:00
Juan Font Alonso
9a01e3d192 Bump tailscale to 1.28.0 2022-08-10 10:47:49 +02:00
Juan Font
a03a99569d Merge pull request #720 from juanfont/replace-ioutil
Replaced legacy ioutil usage
2022-08-10 07:41:50 +02:00
Juan Font Alonso
2d887046de Replaced legacy ioutil usage 2022-08-09 23:21:19 +02:00
Juan Font
3a091896fb Merge pull request #685 from GrigoriyMikhalkin/oidc-refactoring
Decompose OIDCCallback method
2022-08-09 20:52:32 +02:00
Juan Font
8a9fe1da4b Merge branch 'main' into oidc-refactoring 2022-08-09 20:29:02 +02:00
Juan Font
abf478c9e6 Merge pull request #703 from nnsee/android-readme
Update the readme and documentation with details on the Android app
2022-08-09 15:39:45 +02:00
Juan Font
913a94d2ab Merge branch 'main' into android-readme 2022-08-09 15:37:20 +02:00
Juan Font
01e5be3b57 Merge pull request #711 from sophware/typofix
typo fixed from advertised to advertise
2022-08-09 15:11:23 +02:00
Juan Font
e93529e9f3 Merge branch 'main' into typofix 2022-08-09 15:05:53 +02:00
Juan Font
ade4e23e14 Merge pull request #698 from GrigoriyMikhalkin/export-errors
Export API errors
2022-08-09 15:04:51 +02:00
Juan Font
fc65ded2d5 Merge branch 'main' into oidc-refactoring 2022-08-09 14:37:58 +02:00
Juan Font
aa2b92703f Merge branch 'main' into export-errors 2022-08-09 14:33:10 +02:00
Juan Font
2c9dbe158d Merge pull request #713 from juanfont/update-buf-lint
Update buf setup action for proto-lint
2022-08-09 14:32:52 +02:00
Juan Font Alonso
d6fa5c96ae Update setup action for proto lint 2022-08-09 14:21:45 +02:00
Juan Font
0506e68a96 Merge branch 'main' into export-errors 2022-08-09 14:16:24 +02:00
Juan Font
b32f986105 Merge pull request #710 from juanfont/cosmetic-changes-integration
Improvements in integration tests
2022-08-09 14:16:10 +02:00
Juan Font Alonso
577eedef11 Restore the number of containers 2022-08-09 13:53:25 +02:00
Juan Font Alonso
27855880b2 Updated versions for taiscale 2022-08-09 13:53:02 +02:00
Juan Font Alonso
b01d392f9e Run integrtation tests in different steps in Github Actions 2022-08-09 12:26:58 +02:00
Juan Font Alonso
d548f5de3f Splitted integration tests in Makefile 2022-08-09 12:26:29 +02:00
Juan Font Alonso
f8986132d4 Use tags to split the integration tests 2022-08-09 12:26:12 +02:00
Juan Font Alonso
e7148b8080 Temporarily disable unstable branch 2022-08-09 09:58:45 +02:00
Juan Font Alonso
0a29492fc5 Increase swap size in integration tests 2022-08-08 23:20:29 +02:00
Grigoriy Mikhalkin
a1e7e771ce refactor OIDC callback aux functions 2022-08-07 13:57:07 +02:00
Grigoriy Mikhalkin
00d2a447f4 decompose OIDCCallback method 2022-08-07 13:27:29 +02:00
Steve Malloy
2254ac2102 typo fixed from advertised to advertise 2022-08-05 15:44:11 -04:00
Juan Font Alonso
21ae31e77d Reduce number of containers in integration tests (for testing) 2022-08-05 18:57:08 +02:00
Juan Font Alonso
a6113066ff Improved logs in integration tests 2022-08-05 17:35:28 +02:00
Adrien Raffin-Caboisse
0bb205d31f Merge remote-tracking branch 'origin/main' into fix-bug-in-excludecorrectlytaggednodes 2022-08-05 11:56:33 +02:00
Juan Font
d7e8db7adc Merge branch 'main' into export-errors 2022-08-05 10:14:35 +02:00
Juan Font
0eb3b23f16 Merge pull request #708 from juanfont/revert-680-topic/speedup-build
Revert BuildKit (docker buildx) support
2022-08-05 10:14:19 +02:00
Juan Font
54e381cecb Revert "Topic/speedup build" 2022-08-05 00:31:39 +02:00
Grigoriy Mikhalkin
cc1343d31d fixed typo in ErrCannotDecryptResponse name 2022-08-05 00:00:36 +02:00
Adrien Raffin-Caboisse
bce59345e4 docs: add entry in changelog 2022-08-04 10:51:06 +02:00
Adrien Raffin-Caboisse
79688e6187 chore(all): apply formater 2022-08-04 10:47:00 +02:00
Adrien Raffin-Caboisse
babf9470c2 fix(acl): fix issue with groups in excludeCorretlyTaggedNodes
This commit fix issue #563
2022-08-04 10:42:47 +02:00
Rasmus Moorats
10d566c946 add details on how to use the android app 2022-08-02 09:49:28 +03:00
Grigoriy Mikhalkin
911e6ba6de exported API errors 2022-07-29 17:35:21 +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
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
3f7749c6d4 Merge branch 'main' into feature/configure-randomize-port 2022-06-11 10:55:05 +01:00
Anton Schubert
34be10840c add ability to set randomizeClientPort 2022-06-09 21:26:40 +02:00
80 changed files with 5090 additions and 2060 deletions

View File

@@ -26,7 +26,7 @@ jobs:
if: steps.changed-files.outputs.any_changed == 'true'
uses: golangci/golangci-lint-action@v2
with:
version: v1.46.1
version: v1.49.0
# Only block PRs on new problems.
# If this is not enabled, we will end up having PRs
@@ -70,7 +70,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: bufbuild/buf-setup-action@v0.7.0
- uses: bufbuild/buf-setup-action@v1
- uses: bufbuild/buf-lint-action@v1
with:
input: "proto"

View File

@@ -18,7 +18,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.18.0
go-version: 1.19.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

@@ -11,6 +11,11 @@ jobs:
with:
fetch-depth: 2
- name: Set Swap Space
uses: pierotofy/set-swap-space@master
with:
swap-size-gb: 10
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v14.1
@@ -25,6 +30,29 @@ jobs:
- uses: cachix/install-nix-action@v16
if: steps.changed-files.outputs.any_changed == 'true'
- name: Run Integration tests
- name: Run CLI integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: nix develop --command -- make test_integration
uses: nick-fields/retry@v2
with:
timeout_minutes: 240
max_attempts: 5
retry_on: error
command: nix develop --command -- make test_integration_cli
- name: Run Embedded DERP server integration tests
if: steps.changed-files.outputs.any_changed == 'true'
uses: nick-fields/retry@v2
with:
timeout_minutes: 240
max_attempts: 5
retry_on: error
command: nix develop --command -- make test_integration_derp
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
uses: nick-fields/retry@v2
with:
timeout_minutes: 240
max_attempts: 5
retry_on: error
command: nix develop --command -- make test_integration_general

2
.gitignore vendored
View File

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

View File

@@ -1,7 +1,7 @@
---
before:
hooks:
- go mod tidy -compat=1.18
- go mod tidy -compat=1.19
release:
prerelease: auto

View File

@@ -1,6 +1,44 @@
# CHANGELOG
## 0.16.0 (2022-xx-xx)
## 0.17.0 (2022-XX-XX)
- Added support for Tailscale TS2021 protocol [#738](https://github.com/juanfont/headscale/pull/738)
- Add ability to specify config location via env var `HEADSCALE_CONFIG` [#674](https://github.com/juanfont/headscale/issues/674)
- Target Go 1.19 for Headscale [#778](https://github.com/juanfont/headscale/pull/778)
- Target Tailscale v1.30.0 to build Headscale [#780](https://github.com/juanfont/headscale/pull/780)
- Give a warning when running Headscale with reverse proxy improperly configured for WebSockets [#788](https://github.com/juanfont/headscale/pull/788)
## 0.16.4 (2022-08-21)
### Changes
- Add ability to connect to PostgreSQL over TLS/SSL [#745](https://github.com/juanfont/headscale/pull/745)
- Fix CLI registration of expired machines [#754](https://github.com/juanfont/headscale/pull/754)
## 0.16.3 (2022-08-17)
### Changes
- Fix issue with OIDC authentication [#747](https://github.com/juanfont/headscale/pull/747)
## 0.16.2 (2022-08-14)
### Changes
- Fixed bugs in the client registration process after migration to NodeKey [#735](https://github.com/juanfont/headscale/pull/735)
## 0.16.1 (2022-08-12)
### Changes
- Updated dependencies (including the library that lacked armhf support) [#722](https://github.com/juanfont/headscale/pull/722)
- Fix missing group expansion in function `excludeCorretlyTaggedNodes` [#563](https://github.com/juanfont/headscale/issues/563)
- Improve registration protocol implementation and switch to NodeKey as main identifier [#725](https://github.com/juanfont/headscale/pull/725)
- Add ability to connect to PostgreSQL via unix socket [#734](https://github.com/juanfont/headscale/pull/734)
## 0.16.0 (2022-07-25)
**Note:** Take a backup of your database before upgrading.
### BREAKING
@@ -28,6 +66,14 @@
- 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)
@@ -98,7 +144,7 @@ This is a part of aligning `headscale`'s behaviour with Tailscale's upstream beh
- 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))
- `strip_email_domain` option has been added (see [config-example.yaml](./config-example.yaml))
### Changes

134
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,134 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation
in our community a harassment-free experience for everyone, regardless
of age, body size, visible or invisible disability, ethnicity, sex
characteristics, gender identity and expression, level of experience,
education, socio-economic status, nationality, personal appearance,
race, religion, or sexual identity and orientation.
We pledge to act and interact in ways that contribute to an open,
welcoming, diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for
our community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our
mistakes, and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
- Trolling, insulting or derogatory comments, and personal or
political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in
a professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our
standards of acceptable behavior and will take appropriate and fair
corrective action in response to any behavior that they deem
inappropriate, threatening, offensive, or harmful.
Community leaders have the right and responsibility to remove, edit,
or reject comments, commits, code, wiki edits, issues, and other
contributions that are not aligned to this Code of Conduct, and will
communicate reasons for moderation decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also
applies when an individual is officially representing the community in
public spaces. Examples of representing our community include using an
official e-mail address, posting via an official social media account,
or acting as an appointed representative at an online or offline
event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior
may be reported to the community leaders responsible for enforcement
at our Discord channel. All complaints
will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and
security of the reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in
determining the consequences for any action they deem in violation of
this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior
deemed unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders,
providing clarity around the nature of the violation and an
explanation of why the behavior was inappropriate. A public apology
may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued
behavior. No interaction with the people involved, including
unsolicited interaction with those enforcing the Code of Conduct, for
a specified period of time. This includes avoiding interactions in
community spaces as well as external channels like social
media. Violating these terms may lead to a temporary or permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards,
including sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or
public communication with the community for a specified period of
time. No public or private interaction with the people involved,
including unsolicited interaction with those enforcing the Code of
Conduct, is allowed during this period. Violating these terms may lead
to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of
community standards, including sustained inappropriate behavior,
harassment of an individual, or aggression toward or disparagement of
classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction
within the community.
## Attribution
This Code of Conduct is adapted from the [Contributor
Covenant][homepage], version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of
conduct enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the
FAQ at https://www.contributor-covenant.org/faq. Translations are
available at https://www.contributor-covenant.org/translations.

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,9 @@ RUN apt-get update \
RUN git clone https://github.com/tailscale/tailscale.git
WORKDIR tailscale
WORKDIR /go/tailscale
RUN git checkout main
RUN sh build_dist.sh tailscale.com/cmd/tailscale
RUN sh build_dist.sh tailscale.com/cmd/tailscaled

View File

@@ -1,5 +1,5 @@
# Calculate version
version = $(git describe --always --tags --dirty)
version ?= $(shell git describe --always --tags --dirty)
rwildcard=$(foreach d,$(wildcard $1*),$(call rwildcard,$d/,$2) $(filter $(subst *,%,$2),$d))
@@ -24,14 +24,16 @@ dev: lint test build
test:
@go test -coverprofile=coverage.out ./...
test_integration:
go test -failfast -tags integration -timeout 30m -count=1 ./...
test_integration: test_integration_cli test_integration_derp test_integration_general
test_integration_cli:
go test -tags integration -v integration_cli_test.go integration_common_test.go
go test -failfast -tags integration_cli,integration -timeout 30m -count=1 ./...
test_integration_derp:
go test -tags integration -v integration_embedded_derp_test.go integration_common_test.go
go test -failfast -tags integration_derp,integration -timeout 30m -count=1 ./...
test_integration_general:
go test -failfast -tags integration_general,integration -timeout 30m -count=1 ./...
coverprofile_func:
go tool cover -func=coverage.out

161
README.md
View File

@@ -1,4 +1,4 @@
# headscale
![headscale logo](./docs/logo/headscale3_header_stacked_left.png)
![ci](https://github.com/juanfont/headscale/actions/workflows/test.yml/badge.svg)
@@ -67,15 +67,15 @@ one of the maintainers.
## Client OS support
| OS | Supports headscale |
| ------- | ----------------------------------------------------------------------------------------------------------------- |
| Linux | Yes |
| OpenBSD | Yes |
| FreeBSD | Yes |
| macOS | Yes (see `/apple` on your headscale for more information) |
| Windows | Yes [docs](./docs/windows-client.md) |
| Android | [You need to compile the client yourself](https://github.com/juanfont/headscale/issues/58#issuecomment-885255270) |
| iOS | Not yet |
| OS | Supports headscale |
| ------- | --------------------------------------------------------- |
| Linux | Yes |
| OpenBSD | Yes |
| FreeBSD | Yes |
| macOS | Yes (see `/apple` on your headscale for more information) |
| Windows | Yes [docs](./docs/windows-client.md) |
| Android | Yes [docs](./docs/android-client.md) |
| iOS | Not yet |
## Running headscale
@@ -188,6 +188,13 @@ make build
<sub style="font-size:14px"><b>Ward Vandewege</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/huskyii>
<img src=https://avatars.githubusercontent.com/u/5499746?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jiang Zhu/>
<br />
<sub style="font-size:14px"><b>Jiang Zhu</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/reynico>
<img src=https://avatars.githubusercontent.com/u/715768?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Nico/>
@@ -195,6 +202,8 @@ make build
<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/>
@@ -202,8 +211,6 @@ make build
<sub style="font-size:14px"><b>e-zk</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/arch4ngel>
<img src=https://avatars.githubusercontent.com/u/11574161?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Justin Angel/>
@@ -225,6 +232,13 @@ 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/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>
<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/>
@@ -232,11 +246,13 @@ make build
<sub style="font-size:14px"><b>Moritz Poldrack</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/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/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>ohdearaugustin</b></sub>
<sub style="font-size:14px"><b>GrigoriyMikhalkin</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
@@ -246,8 +262,6 @@ make build
<sub style="font-size:14px"><b>Niek van der Maas</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/negbie>
<img src=https://avatars.githubusercontent.com/u/20154956?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Eugen Biegler/>
@@ -255,6 +269,20 @@ make build
<sub style="font-size:14px"><b>Eugen Biegler</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/617a7a>
<img src=https://avatars.githubusercontent.com/u/67651251?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Azz/>
<br />
<sub style="font-size:14px"><b>Azz</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/qbit>
<img src=https://avatars.githubusercontent.com/u/68368?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Aaron Bieber/>
@@ -262,6 +290,15 @@ make build
<sub style="font-size:14px"><b>Aaron Bieber</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/Aluxima>
<img src=https://avatars.githubusercontent.com/u/16262531?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Laurent Marchaud/>
<br />
<sub style="font-size:14px"><b>Laurent Marchaud</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/>
@@ -290,6 +327,13 @@ make build
<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>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
@@ -313,6 +357,20 @@ make build
<sub style="font-size:14px"><b>Paul Tötterman</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/samson4649>
<img src=https://avatars.githubusercontent.com/u/12725953?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Samuel Lock/>
<br />
<sub style="font-size:14px"><b>Samuel Lock</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/majst01>
<img src=https://avatars.githubusercontent.com/u/410110?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Stefan Majer/>
<br />
<sub style="font-size:14px"><b>Stefan Majer</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<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/>
@@ -320,6 +378,8 @@ make build
<sub style="font-size:14px"><b>Artem Klevtsov</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/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/>
@@ -334,8 +394,6 @@ make build
<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/>
@@ -344,10 +402,10 @@ make build
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/majst01>
<img src=https://avatars.githubusercontent.com/u/410110?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Stefan Majer/>
<a href=https://github.com/vtrf>
<img src=https://avatars.githubusercontent.com/u/25647735?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Victor Freire/>
<br />
<sub style="font-size:14px"><b>Stefan Majer</b></sub>
<sub style="font-size:14px"><b>Victor Freire</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
@@ -364,6 +422,8 @@ 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/>
@@ -378,15 +438,6 @@ make build
<sub style="font-size:14px"><b>Antoine POPINEAU</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/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/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/>
@@ -415,6 +466,8 @@ make build
<sub style="font-size:14px"><b> Carson Yang</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/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/>
@@ -422,8 +475,6 @@ make build
<sub style="font-size:14px"><b>kundel</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/fkr>
<img src=https://avatars.githubusercontent.com/u/51063?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Felix Kronlage-Dammers/>
@@ -452,13 +503,6 @@ make build
<sub style="font-size:14px"><b>Jamie Greeff</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/huskyii>
<img src=https://avatars.githubusercontent.com/u/5499746?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jiang Zhu/>
<br />
<sub style="font-size:14px"><b>Jiang Zhu</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/jimt>
<img src=https://avatars.githubusercontent.com/u/180326?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jim Tittsler/>
@@ -475,6 +519,13 @@ make build
<sub style="font-size:14px"><b>Pierre Carru</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/nnsee>
<img src=https://avatars.githubusercontent.com/u/36747857?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Rasmus Moorats/>
<br />
<sub style="font-size:14px"><b>Rasmus Moorats</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/rcursaru>
<img src=https://avatars.githubusercontent.com/u/16259641?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=rcursaru/>
@@ -503,6 +554,15 @@ make build
<sub style="font-size:14px"><b>Shaanan Cohney</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/sophware>
<img src=https://avatars.githubusercontent.com/u/41669?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=sophware/>
<br />
<sub style="font-size:14px"><b>sophware</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/m-tanner-dev0>
<img src=https://avatars.githubusercontent.com/u/97977342?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tanner/>
@@ -510,8 +570,6 @@ make build
<sub style="font-size:14px"><b>Tanner</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/Teteros>
<img src=https://avatars.githubusercontent.com/u/5067989?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Teteros/>
@@ -540,6 +598,8 @@ 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/>
@@ -547,6 +607,13 @@ make build
<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/gozssky>
<img src=https://avatars.githubusercontent.com/u/17199941?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Yujie Xia/>
<br />
<sub style="font-size:14px"><b>Yujie Xia</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/>
@@ -554,13 +621,11 @@ make build
<sub style="font-size:14px"><b>Zakhar Bessarab</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/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">
@@ -577,6 +642,8 @@ make build
<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/>
@@ -598,8 +665,6 @@ make build
<sub style="font-size:14px"><b>pernila</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/Wakeful-Cloud>
<img src=https://avatars.githubusercontent.com/u/38930607?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Wakeful-Cloud/>

33
acls.go
View File

@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"io"
"net/netip"
"os"
"path/filepath"
"strconv"
@@ -13,7 +14,6 @@ import (
"github.com/rs/zerolog/log"
"github.com/tailscale/hujson"
"gopkg.in/yaml.v3"
"inet.af/netaddr"
"tailscale.com/tailcfg"
)
@@ -37,7 +37,7 @@ const (
expectedTokenItems = 2
)
// For some reason golang.org/x/net/internal/iana is an internal package
// For some reason golang.org/x/net/internal/iana is an internal package.
const (
protocolICMP = 1 // Internet Control Message
protocolIGMP = 2 // Internet Group Management
@@ -162,7 +162,12 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
destPorts := []tailcfg.NetPortRange{}
for innerIndex, dest := range acl.Destinations {
dests, err := h.generateACLPolicyDest(machines, *h.aclPolicy, dest, needsWildcard)
dests, err := h.generateACLPolicyDest(
machines,
*h.aclPolicy,
dest,
needsWildcard,
)
if err != nil {
log.Error().
Msgf("Error parsing ACL %d, Destination %d", index, innerIndex)
@@ -255,7 +260,12 @@ func (h *Headscale) generateACLPolicyDest(
func parseProtocol(protocol string) ([]int, bool, error) {
switch protocol {
case "":
return []int{protocolICMP, protocolIPv6ICMP, protocolTCP, protocolUDP}, false, nil
return []int{
protocolICMP,
protocolIPv6ICMP,
protocolTCP,
protocolUDP,
}, false, nil
case "igmp":
return []int{protocolIGMP}, true, nil
case "ipv4", "ip-in-ip":
@@ -284,7 +294,9 @@ func parseProtocol(protocol string) ([]int, bool, error) {
if err != nil {
return nil, false, err
}
needsWildcard := protocolNumber != protocolTCP && protocolNumber != protocolUDP && protocolNumber != protocolSCTP
needsWildcard := protocolNumber != protocolTCP &&
protocolNumber != protocolUDP &&
protocolNumber != protocolSCTP
return []int{protocolNumber}, needsWildcard, nil
}
@@ -367,7 +379,7 @@ func expandAlias(
// if alias is a namespace
nodes := filterMachinesByNamespace(machines, alias)
nodes = excludeCorrectlyTaggedNodes(aclPolicy, nodes, alias)
nodes = excludeCorrectlyTaggedNodes(aclPolicy, nodes, alias, stripEmailDomain)
for _, n := range nodes {
ips = append(ips, n.IPAddresses.ToStringSlice()...)
@@ -382,13 +394,13 @@ func expandAlias(
}
// if alias is an IP
ip, err := netaddr.ParseIP(alias)
ip, err := netip.ParseAddr(alias)
if err == nil {
return []string{ip.String()}, nil
}
// if alias is an CIDR
cidr, err := netaddr.ParseIPPrefix(alias)
cidr, err := netip.ParsePrefix(alias)
if err == nil {
return []string{cidr.String()}, nil
}
@@ -405,10 +417,13 @@ func excludeCorrectlyTaggedNodes(
aclPolicy ACLPolicy,
nodes []Machine,
namespace string,
stripEmailDomain bool,
) []Machine {
out := []Machine{}
tags := []string{}
for tag, ns := range aclPolicy.TagOwners {
for tag := range aclPolicy.TagOwners {
owners, _ := expandTagOwners(aclPolicy, namespace, stripEmailDomain)
ns := append(owners, namespace)
if contains(ns, namespace) {
tags = append(tags, tag)
}

View File

@@ -2,11 +2,11 @@ package headscale
import (
"errors"
"net/netip"
"reflect"
"testing"
"gopkg.in/check.v1"
"inet.af/netaddr"
"tailscale.com/tailcfg"
)
@@ -62,7 +62,11 @@ func (s *Suite) TestBasicRule(c *check.C) {
func (s *Suite) TestInvalidAction(c *check.C) {
app.aclPolicy = &ACLPolicy{
ACLs: []ACL{
{Action: "invalidAction", Sources: []string{"*"}, Destinations: []string{"*:*"}},
{
Action: "invalidAction",
Sources: []string{"*"},
Destinations: []string{"*:*"},
},
},
}
err := app.UpdateACLRules()
@@ -77,7 +81,11 @@ func (s *Suite) TestInvalidGroupInGroup(c *check.C) {
"group:error": []string{"foo", "group:test"},
},
ACLs: []ACL{
{Action: "accept", Sources: []string{"group:error"}, Destinations: []string{"*:*"}},
{
Action: "accept",
Sources: []string{"group:error"},
Destinations: []string{"*:*"},
},
},
}
err := app.UpdateACLRules()
@@ -88,7 +96,11 @@ 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", Sources: []string{"tag:foo"}, Destinations: []string{"*:*"}},
{
Action: "accept",
Sources: []string{"tag:foo"},
Destinations: []string{"*:*"},
},
},
}
err := app.UpdateACLRules()
@@ -119,7 +131,7 @@ func (s *Suite) TestValidExpandTagOwnersInSources(c *check.C) {
NodeKey: "bar",
DiscoKey: "faa",
Hostname: "testmachine",
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.1")},
NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
@@ -131,7 +143,11 @@ func (s *Suite) TestValidExpandTagOwnersInSources(c *check.C) {
Groups: Groups{"group:test": []string{"user1", "user2"}},
TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}},
ACLs: []ACL{
{Action: "accept", Sources: []string{"tag:test"}, Destinations: []string{"*:*"}},
{
Action: "accept",
Sources: []string{"tag:test"},
Destinations: []string{"*:*"},
},
},
}
err = app.UpdateACLRules()
@@ -165,7 +181,7 @@ func (s *Suite) TestValidExpandTagOwnersInDestinations(c *check.C) {
NodeKey: "bar",
DiscoKey: "faa",
Hostname: "testmachine",
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.1")},
NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
@@ -177,7 +193,11 @@ func (s *Suite) TestValidExpandTagOwnersInDestinations(c *check.C) {
Groups: Groups{"group:test": []string{"user1", "user2"}},
TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}},
ACLs: []ACL{
{Action: "accept", Sources: []string{"*"}, Destinations: []string{"tag:test:*"}},
{
Action: "accept",
Sources: []string{"*"},
Destinations: []string{"tag:test:*"},
},
},
}
err = app.UpdateACLRules()
@@ -211,7 +231,7 @@ func (s *Suite) TestInvalidTagValidNamespace(c *check.C) {
NodeKey: "bar",
DiscoKey: "faa",
Hostname: "testmachine",
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.1")},
NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
@@ -222,7 +242,11 @@ func (s *Suite) TestInvalidTagValidNamespace(c *check.C) {
app.aclPolicy = &ACLPolicy{
TagOwners: TagOwners{"tag:test": []string{"user1"}},
ACLs: []ACL{
{Action: "accept", Sources: []string{"user1"}, Destinations: []string{"*:*"}},
{
Action: "accept",
Sources: []string{"user1"},
Destinations: []string{"*:*"},
},
},
}
err = app.UpdateACLRules()
@@ -256,7 +280,7 @@ func (s *Suite) TestValidTagInvalidNamespace(c *check.C) {
NodeKey: "bar",
DiscoKey: "faa",
Hostname: "webserver",
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.1")},
NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
@@ -275,7 +299,7 @@ func (s *Suite) TestValidTagInvalidNamespace(c *check.C) {
NodeKey: "bar2",
DiscoKey: "faab",
Hostname: "user",
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.2")},
NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
@@ -801,7 +825,6 @@ func Test_listMachinesInNamespace(t *testing.T) {
}
}
// nolint
func Test_expandAlias(t *testing.T) {
type args struct {
machines []Machine
@@ -820,10 +843,10 @@ func Test_expandAlias(t *testing.T) {
args: args{
alias: "*",
machines: []Machine{
{IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")}},
{IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.1")}},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.78.84.227"),
netip.MustParseAddr("100.78.84.227"),
},
},
},
@@ -840,25 +863,25 @@ func Test_expandAlias(t *testing.T) {
machines: []Machine{
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
netip.MustParseAddr("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
netip.MustParseAddr("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
netip.MustParseAddr("100.64.0.3"),
},
Namespace: Namespace{Name: "marc"},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.4"),
netip.MustParseAddr("100.64.0.4"),
},
Namespace: Namespace{Name: "mickael"},
},
@@ -878,25 +901,25 @@ func Test_expandAlias(t *testing.T) {
machines: []Machine{
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
netip.MustParseAddr("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
netip.MustParseAddr("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
netip.MustParseAddr("100.64.0.3"),
},
Namespace: Namespace{Name: "marc"},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.4"),
netip.MustParseAddr("100.64.0.4"),
},
Namespace: Namespace{Name: "mickael"},
},
@@ -927,7 +950,7 @@ func Test_expandAlias(t *testing.T) {
machines: []Machine{},
aclPolicy: ACLPolicy{
Hosts: Hosts{
"homeNetwork": netaddr.MustParseIPPrefix("192.168.1.0/24"),
"homeNetwork": netip.MustParsePrefix("192.168.1.0/24"),
},
},
stripEmailDomain: true,
@@ -964,7 +987,7 @@ func Test_expandAlias(t *testing.T) {
machines: []Machine{
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
netip.MustParseAddr("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{
@@ -975,7 +998,7 @@ func Test_expandAlias(t *testing.T) {
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
netip.MustParseAddr("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{
@@ -986,13 +1009,13 @@ func Test_expandAlias(t *testing.T) {
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
netip.MustParseAddr("100.64.0.3"),
},
Namespace: Namespace{Name: "marc"},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.4"),
netip.MustParseAddr("100.64.0.4"),
},
Namespace: Namespace{Name: "joe"},
},
@@ -1012,25 +1035,25 @@ func Test_expandAlias(t *testing.T) {
machines: []Machine{
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
netip.MustParseAddr("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
netip.MustParseAddr("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
netip.MustParseAddr("100.64.0.3"),
},
Namespace: Namespace{Name: "marc"},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.4"),
netip.MustParseAddr("100.64.0.4"),
},
Namespace: Namespace{Name: "mickael"},
},
@@ -1053,27 +1076,27 @@ func Test_expandAlias(t *testing.T) {
machines: []Machine{
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
netip.MustParseAddr("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
ForcedTags: []string{"tag:hr-webserver"},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
netip.MustParseAddr("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
ForcedTags: []string{"tag:hr-webserver"},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
netip.MustParseAddr("100.64.0.3"),
},
Namespace: Namespace{Name: "marc"},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.4"),
netip.MustParseAddr("100.64.0.4"),
},
Namespace: Namespace{Name: "mickael"},
},
@@ -1091,14 +1114,14 @@ func Test_expandAlias(t *testing.T) {
machines: []Machine{
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
netip.MustParseAddr("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
ForcedTags: []string{"tag:hr-webserver"},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
netip.MustParseAddr("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{
@@ -1109,13 +1132,13 @@ func Test_expandAlias(t *testing.T) {
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
netip.MustParseAddr("100.64.0.3"),
},
Namespace: Namespace{Name: "marc"},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.4"),
netip.MustParseAddr("100.64.0.4"),
},
Namespace: Namespace{Name: "mickael"},
},
@@ -1137,7 +1160,7 @@ func Test_expandAlias(t *testing.T) {
machines: []Machine{
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
netip.MustParseAddr("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{
@@ -1148,7 +1171,7 @@ func Test_expandAlias(t *testing.T) {
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
netip.MustParseAddr("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{
@@ -1159,13 +1182,13 @@ func Test_expandAlias(t *testing.T) {
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
netip.MustParseAddr("100.64.0.3"),
},
Namespace: Namespace{Name: "marc"},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.4"),
netip.MustParseAddr("100.64.0.4"),
},
Namespace: Namespace{Name: "joe"},
},
@@ -1201,9 +1224,10 @@ func Test_expandAlias(t *testing.T) {
func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
type args struct {
aclPolicy ACLPolicy
nodes []Machine
namespace string
aclPolicy ACLPolicy
nodes []Machine
namespace string
stripEmailDomain bool
}
tests := []struct {
name string
@@ -1220,7 +1244,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
nodes: []Machine{
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
netip.MustParseAddr("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{
@@ -1231,7 +1255,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
netip.MustParseAddr("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{
@@ -1242,16 +1266,68 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.4"),
netip.MustParseAddr("100.64.0.4"),
},
Namespace: Namespace{Name: "joe"},
},
},
namespace: "joe",
namespace: "joe",
stripEmailDomain: true,
},
want: []Machine{
{
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.4")},
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.4")},
Namespace: Namespace{Name: "joe"},
},
},
},
{
name: "exclude nodes with valid tags, and owner is in a group",
args: args{
aclPolicy: ACLPolicy{
Groups: Groups{
"group:accountant": []string{"joe", "bar"},
},
TagOwners: TagOwners{
"tag:accountant-webserver": []string{"group:accountant"},
},
},
nodes: []Machine{
{
IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:accountant-webserver"},
},
},
{
IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:accountant-webserver"},
},
},
{
IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.4"),
},
Namespace: Namespace{Name: "joe"},
},
},
namespace: "joe",
stripEmailDomain: true,
},
want: []Machine{
{
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.4")},
Namespace: Namespace{Name: "joe"},
},
},
@@ -1265,7 +1341,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
nodes: []Machine{
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
netip.MustParseAddr("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{
@@ -1276,23 +1352,24 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
netip.MustParseAddr("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
ForcedTags: []string{"tag:accountant-webserver"},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.4"),
netip.MustParseAddr("100.64.0.4"),
},
Namespace: Namespace{Name: "joe"},
},
},
namespace: "joe",
namespace: "joe",
stripEmailDomain: true,
},
want: []Machine{
{
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.4")},
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.4")},
Namespace: Namespace{Name: "joe"},
},
},
@@ -1306,7 +1383,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
nodes: []Machine{
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
netip.MustParseAddr("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{
@@ -1317,7 +1394,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
netip.MustParseAddr("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{
@@ -1328,17 +1405,18 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.4"),
netip.MustParseAddr("100.64.0.4"),
},
Namespace: Namespace{Name: "joe"},
},
},
namespace: "joe",
namespace: "joe",
stripEmailDomain: true,
},
want: []Machine{
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
netip.MustParseAddr("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{
@@ -1349,7 +1427,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
netip.MustParseAddr("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{
@@ -1360,7 +1438,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.4"),
netip.MustParseAddr("100.64.0.4"),
},
Namespace: Namespace{Name: "joe"},
},
@@ -1373,6 +1451,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
test.args.aclPolicy,
test.args.nodes,
test.args.namespace,
test.args.stripEmailDomain,
)
if !reflect.DeepEqual(got, test.want) {
t.Errorf("excludeCorrectlyTaggedNodes() = %v, want %v", got, test.want)

View File

@@ -2,11 +2,11 @@ package headscale
import (
"encoding/json"
"net/netip"
"strings"
"github.com/tailscale/hujson"
"gopkg.in/yaml.v3"
"inet.af/netaddr"
)
// ACLPolicy represents a Tailscale ACL Policy.
@@ -21,28 +21,28 @@ type ACLPolicy struct {
// ACL is a basic rule for the ACL Policy.
type ACL struct {
Action string `json:"action" yaml:"action"`
Protocol string `json:"proto" yaml:"proto"`
Sources []string `json:"src" yaml:"src"`
Destinations []string `json:"dst" yaml:"dst"`
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.
type Groups map[string][]string
// Hosts are alias for IP addresses or subnets.
type Hosts map[string]netaddr.IPPrefix
type Hosts map[string]netip.Prefix
// TagOwners specify what users (namespaces?) are allow to use certain tags.
type TagOwners map[string][]string
// ACLTest is not implemented, but should be use to check if a certain rule is allowed.
type ACLTest struct {
Source string `json:"src" yaml:"src"`
Accept []string `json:"accept" yaml:"accept"`
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.
// UnmarshalJSON allows to parse the Hosts directly into netip objects.
func (hosts *Hosts) UnmarshalJSON(data []byte) error {
newHosts := Hosts{}
hostIPPrefixMap := make(map[string]string)
@@ -60,7 +60,7 @@ func (hosts *Hosts) UnmarshalJSON(data []byte) error {
if !strings.Contains(prefixStr, "/") {
prefixStr += "/32"
}
prefix, err := netaddr.ParseIPPrefix(prefixStr)
prefix, err := netip.ParsePrefix(prefixStr)
if err != nil {
return err
}
@@ -71,7 +71,7 @@ func (hosts *Hosts) UnmarshalJSON(data []byte) error {
return nil
}
// UnmarshalYAML allows to parse the Hosts directly into netaddr objects.
// UnmarshalYAML allows to parse the Hosts directly into netip objects.
func (hosts *Hosts) UnmarshalYAML(data []byte) error {
newHosts := Hosts{}
hostIPPrefixMap := make(map[string]string)
@@ -81,7 +81,7 @@ func (hosts *Hosts) UnmarshalYAML(data []byte) error {
return err
}
for host, prefixStr := range hostIPPrefixMap {
prefix, err := netaddr.ParseIPPrefix(prefixStr)
prefix, err := netip.ParsePrefix(prefixStr)
if err != nil {
return err
}

693
api.go
View File

@@ -2,25 +2,18 @@ package headscale
import (
"bytes"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"html/template"
"io"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/klauspost/compress/zstd"
"github.com/gorilla/mux"
"github.com/rs/zerolog/log"
"gorm.io/gorm"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
const (
// TODO(juan): remove this once https://github.com/juanfont/headscale/issues/727 is fixed.
registrationHoldoff = time.Second * 5
reservedResponseHeaderSize = 4
RegisterMethodAuthKey = "authkey"
RegisterMethodOIDC = "oidc"
@@ -30,14 +23,42 @@ const (
)
)
// 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) 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(req.Context()); err != nil {
respond(err)
return
}
respond(nil)
}
type registerWebAPITemplateConfig struct {
@@ -62,624 +83,58 @@ var registerWebAPITemplate = template.Must(
`))
// 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")
if machineKeyStr == "" {
ctx.String(http.StatusBadRequest, "Wrong params")
// Listens in /register/:nkey.
//
// This is not part of the Tailscale control API, as we could send whatever URL
// in the RegisterResponse.AuthURL field.
func (h *Headscale) RegisterWebAPI(
writer http.ResponseWriter,
req *http.Request,
) {
vars := mux.Vars(req)
nodeKeyStr, ok := vars["nkey"]
if !ok || nodeKeyStr == "" {
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
}
var content bytes.Buffer
if err := registerWebAPITemplate.Execute(&content, registerWebAPITemplateConfig{
Key: machineKeyStr,
Key: nodeKeyStr,
}); err != nil {
log.Error().
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"),
)
}
ctx.Data(http.StatusOK, "text/html; charset=utf-8", content.Bytes())
}
// 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")
var machineKey key.MachinePublic
err := machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machineKeyStr)))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot parse machine key")
machineRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc()
ctx.String(http.StatusInternalServerError, "Sad!")
return
}
req := tailcfg.RegisterRequest{}
err = decode(body, &req, &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!")
return
}
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")
machineKeyStr := MachinePublicKeyStripPrefix(machineKey)
// If the machine has AuthKey set, handle registration via PreAuthKeys
if req.Auth.AuthKey != "" {
h.handleAuthKey(ctx, machineKey, req)
return
}
givenName, err := h.GenerateGivenName(req.Hostinfo.Hostname)
if err != nil {
log.Error().
Caller().
Str("func", "RegistrationHandler").
Str("hostinfo.name", req.Hostinfo.Hostname).
Err(err)
return
}
// The machine did not have a key to authenticate, which means
// that we rely on a method that calls back some how (OpenID or CLI)
// We create the machine and then keep it around until a callback
// happens
newMachine := Machine{
MachineKey: machineKeyStr,
Hostname: req.Hostinfo.Hostname,
GivenName: givenName,
NodeKey: NodePublicKeyStripPrefix(req.NodeKey),
LastSeen: &now,
Expiry: &time.Time{},
}
if !req.Expiry.IsZero() {
log.Trace().
Caller().
Str("machine", req.Hostinfo.Hostname).
Time("expiry", req.Expiry).
Msg("Non-zero expiry time requested")
newMachine.Expiry = &req.Expiry
}
h.registrationCache.Set(
machineKeyStr,
newMachine,
registerCacheExpiration,
)
h.handleMachineRegistrationNew(ctx, machineKey, req)
return
}
// The machine is already registered, so we need to pass through reauth or key update.
if machine != nil {
// If the NodeKey stored in headscale is the same as the key presented in a registration
// request, then we have a node that is either:
// - Trying to log out (sending a expiry in the past)
// - A valid, registered machine, looking for the node map
// - Expired machine wanting to reauthenticate
if machine.NodeKey == NodePublicKeyStripPrefix(req.NodeKey) {
// The client sends an Expiry in the past if the client is requesting to expire the key (aka logout)
// https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L648
if !req.Expiry.IsZero() && req.Expiry.UTC().Before(now) {
h.handleMachineLogOut(ctx, machineKey, *machine)
return
}
// If machine is not expired, and is register, we have a already accepted this machine,
// let it proceed with a valid registration
if !machine.isExpired() {
h.handleMachineValidRegistration(ctx, 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) &&
!machine.isExpired() {
h.handleMachineRefreshKey(ctx, machineKey, req, *machine)
return
}
// The machine has expired
h.handleMachineExpired(ctx, machineKey, req, *machine)
return
}
}
func (h *Headscale) getMapResponse(
machineKey key.MachinePublic,
req tailcfg.MapRequest,
machine *Machine,
) ([]byte, error) {
log.Trace().
Str("func", "getMapResponse").
Str("machine", req.Hostinfo.Hostname).
Msg("Creating Map response")
node, err := machine.toNode(h.cfg.BaseDomain, h.cfg.DNSConfig, true)
if err != nil {
log.Error().
Caller().
Str("func", "getMapResponse").
Err(err).
Msg("Cannot convert to node")
return nil, err
}
peers, err := h.getValidPeers(machine)
if err != nil {
log.Error().
Caller().
Str("func", "getMapResponse").
Err(err).
Msg("Cannot fetch peers")
return nil, err
}
profiles := getMapResponseUserProfiles(*machine, peers)
nodePeers, err := peers.toNodes(h.cfg.BaseDomain, h.cfg.DNSConfig, true)
if err != nil {
log.Error().
Caller().
Str("func", "getMapResponse").
Err(err).
Msg("Failed to convert peers to Tailscale nodes")
return nil, err
}
dnsConfig := getMapResponseDNSConfig(
h.cfg.DNSConfig,
h.cfg.BaseDomain,
*machine,
peers,
)
resp := tailcfg.MapResponse{
KeepAlive: false,
Node: node,
Peers: nodePeers,
DNSConfig: dnsConfig,
Domain: h.cfg.BaseDomain,
PacketFilter: h.aclRules,
DERPMap: h.DERPMap,
UserProfiles: profiles,
Debug: &tailcfg.Debug{
DisableLogTail: !h.cfg.LogTail.Enabled,
},
}
log.Trace().
Str("func", "getMapResponse").
Str("machine", req.Hostinfo.Hostname).
// Interface("payload", resp).
Msgf("Generated map response: %s", tailMapResponseToString(resp))
var respBody []byte
if req.Compress == "zstd" {
src, err := json.Marshal(resp)
if err != nil {
log.Error().
Caller().
Str("func", "getMapResponse").
Err(err).
Msg("Failed to marshal response for the client")
return nil, err
}
encoder, _ := zstd.NewWriter(nil)
srcCompressed := encoder.EncodeAll(src, nil)
respBody = h.privateKey.SealTo(machineKey, srcCompressed)
} else {
respBody, err = encode(resp, &machineKey, h.privateKey)
if err != nil {
return nil, err
}
}
// declare the incoming size on the first 4 bytes
data := make([]byte, reservedResponseHeaderSize)
binary.LittleEndian.PutUint32(data, uint32(len(respBody)))
data = append(data, respBody...)
return data, nil
}
func (h *Headscale) getMapKeepAliveResponse(
machineKey key.MachinePublic,
mapRequest tailcfg.MapRequest,
) ([]byte, error) {
mapResponse := tailcfg.MapResponse{
KeepAlive: true,
}
var respBody []byte
var err error
if mapRequest.Compress == "zstd" {
src, err := json.Marshal(mapResponse)
if err != nil {
log.Error().
Caller().
Str("func", "getMapKeepAliveResponse").
Err(err).
Msg("Failed to marshal keepalive response for the client")
return nil, err
}
encoder, _ := zstd.NewWriter(nil)
srcCompressed := encoder.EncodeAll(src, nil)
respBody = h.privateKey.SealTo(machineKey, srcCompressed)
} else {
respBody, err = encode(mapResponse, &machineKey, h.privateKey)
if err != nil {
return nil, err
}
}
data := make([]byte, reservedResponseHeaderSize)
binary.LittleEndian.PutUint32(data, uint32(len(respBody)))
data = append(data, respBody...)
return data, nil
}
func (h *Headscale) handleMachineLogOut(
ctx *gin.Context,
machineKey key.MachinePublic,
machine Machine,
) {
resp := tailcfg.RegisterResponse{}
log.Info().
Str("machine", machine.Hostname).
Msg("Client requested logout")
h.ExpireMachine(&machine)
resp.AuthURL = ""
resp.MachineAuthorized = false
resp.User = *machine.Namespace.toUser()
respBody, err := encode(resp, &machineKey, h.privateKey)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot encode message")
ctx.String(http.StatusInternalServerError, "")
return
}
ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
}
func (h *Headscale) handleMachineValidRegistration(
ctx *gin.Context,
machineKey key.MachinePublic,
machine Machine,
) {
resp := tailcfg.RegisterResponse{}
// The machine registration is valid, respond with redirect to /map
log.Debug().
Str("machine", machine.Hostname).
Msg("Client is registered and we have the current NodeKey. All clear to /map")
resp.AuthURL = ""
resp.MachineAuthorized = true
resp.User = *machine.Namespace.toUser()
resp.Login = *machine.Namespace.toLogin()
respBody, err := encode(resp, &machineKey, h.privateKey)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot encode message")
machineRegistrations.WithLabelValues("update", "web", "error", machine.Namespace.Name).
Inc()
ctx.String(http.StatusInternalServerError, "")
return
}
machineRegistrations.WithLabelValues("update", "web", "success", machine.Namespace.Name).
Inc()
ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
}
func (h *Headscale) handleMachineExpired(
ctx *gin.Context,
machineKey key.MachinePublic,
registerRequest tailcfg.RegisterRequest,
machine Machine,
) {
resp := tailcfg.RegisterResponse{}
// The client has registered before, but has expired
log.Debug().
Str("machine", machine.Hostname).
Msg("Machine registration has expired. Sending a authurl to register")
if registerRequest.Auth.AuthKey != "" {
h.handleAuthKey(ctx, machineKey, registerRequest)
return
}
if h.cfg.OIDC.Issuer != "" {
resp.AuthURL = fmt.Sprintf("%s/oidc/register/%s",
strings.TrimSuffix(h.cfg.ServerURL, "/"), machineKey.String())
} else {
resp.AuthURL = fmt.Sprintf("%s/register?key=%s",
strings.TrimSuffix(h.cfg.ServerURL, "/"), machineKey.String())
}
respBody, err := encode(resp, &machineKey, h.privateKey)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot encode message")
machineRegistrations.WithLabelValues("reauth", "web", "error", machine.Namespace.Name).
Inc()
ctx.String(http.StatusInternalServerError, "")
return
}
machineRegistrations.WithLabelValues("reauth", "web", "success", machine.Namespace.Name).
Inc()
ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
}
func (h *Headscale) handleMachineRefreshKey(
ctx *gin.Context,
machineKey key.MachinePublic,
registerRequest tailcfg.RegisterRequest,
machine Machine,
) {
resp := tailcfg.RegisterResponse{}
log.Debug().
Str("machine", machine.Hostname).
Msg("We have the OldNodeKey in the database. This is a key refresh")
machine.NodeKey = NodePublicKeyStripPrefix(registerRequest.NodeKey)
if err := h.db.Save(&machine).Error; err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to update machine key in the database")
ctx.String(http.StatusInternalServerError, "Internal server error")
return
}
resp.AuthURL = ""
resp.User = *machine.Namespace.toUser()
respBody, err := encode(resp, &machineKey, h.privateKey)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot encode message")
ctx.String(http.StatusInternalServerError, "Internal server error")
return
}
ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
}
func (h *Headscale) handleMachineRegistrationNew(
ctx *gin.Context,
machineKey key.MachinePublic,
registerRequest tailcfg.RegisterRequest,
) {
resp := tailcfg.RegisterResponse{}
// The machine registration is new, redirect the client to the registration URL
log.Debug().
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(
"%s/oidc/register/%s",
strings.TrimSuffix(h.cfg.ServerURL, "/"),
machineKey.String(),
)
} else {
resp.AuthURL = fmt.Sprintf("%s/register?key=%s",
strings.TrimSuffix(h.cfg.ServerURL, "/"), MachinePublicKeyStripPrefix(machineKey))
}
respBody, err := encode(resp, &machineKey, h.privateKey)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot encode message")
ctx.String(http.StatusInternalServerError, "")
return
}
ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
}
// TODO: check if any locks are needed around IP allocation.
func (h *Headscale) handleAuthKey(
ctx *gin.Context,
machineKey key.MachinePublic,
registerRequest tailcfg.RegisterRequest,
) {
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", registerRequest.Hostinfo.Hostname).
Err(err).
Msg("Failed authentication via AuthKey")
resp.MachineAuthorized = false
respBody, err := encode(resp, &machineKey, h.privateKey)
if err != nil {
log.Error().
Caller().
Str("func", "handleAuthKey").
Str("machine", registerRequest.Hostinfo.Hostname).
Err(err).
Msg("Cannot encode message")
ctx.String(http.StatusInternalServerError, "")
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
Inc()
return
}
ctx.Data(http.StatusUnauthorized, "application/json; charset=utf-8", respBody)
log.Error().
Caller().
Str("func", "handleAuthKey").
Str("machine", registerRequest.Hostinfo.Hostname).
Msg("Failed authentication via AuthKey")
if pak != nil {
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
Inc()
} else {
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", "unknown").Inc()
}
return
}
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.Hostname).
Msg("machine already registered, refreshing with new auth key")
machine.NodeKey = nodeKey
machine.AuthKeyID = uint(pak.ID)
h.RefreshMachine(machine, registerRequest.Expiry)
} else {
now := time.Now().UTC()
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
}
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,
)
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("could not register machine")
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
Inc()
ctx.String(
http.StatusInternalServerError,
"could not register machine",
)
return
Msg("Failed to write response")
}
}
h.UsePreAuthKey(pak)
resp.MachineAuthorized = true
resp.User = *pak.Namespace.toUser()
respBody, err := encode(resp, &machineKey, h.privateKey)
if err != nil {
log.Error().
Caller().
Str("func", "handleAuthKey").
Str("machine", registerRequest.Hostinfo.Hostname).
Err(err).
Msg("Cannot encode message")
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
Inc()
ctx.String(http.StatusInternalServerError, "Extremely sad!")
return
}
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "success", pak.Namespace.Name).
Inc()
ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
log.Info().
Str("func", "handleAuthKey").
Str("machine", registerRequest.Hostinfo.Hostname).
Str("ips", strings.Join(machine.IPAddresses.ToStringSlice(), ", ")).
Msg("Successfully authenticated via AuthKey")
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")
}
}

80
api_common.go Normal file
View File

@@ -0,0 +1,80 @@
package headscale
import (
"github.com/rs/zerolog/log"
"tailscale.com/tailcfg"
)
func (h *Headscale) generateMapResponse(
mapRequest tailcfg.MapRequest,
machine *Machine,
) (*tailcfg.MapResponse, error) {
log.Trace().
Str("func", "generateMapResponse").
Str("machine", mapRequest.Hostinfo.Hostname).
Msg("Creating Map response")
node, err := machine.toNode(h.cfg.BaseDomain, h.cfg.DNSConfig, true)
if err != nil {
log.Error().
Caller().
Str("func", "generateMapResponse").
Err(err).
Msg("Cannot convert to node")
return nil, err
}
peers, err := h.getValidPeers(machine)
if err != nil {
log.Error().
Caller().
Str("func", "generateMapResponse").
Err(err).
Msg("Cannot fetch peers")
return nil, err
}
profiles := getMapResponseUserProfiles(*machine, peers)
nodePeers, err := peers.toNodes(h.cfg.BaseDomain, h.cfg.DNSConfig, true)
if err != nil {
log.Error().
Caller().
Str("func", "generateMapResponse").
Err(err).
Msg("Failed to convert peers to Tailscale nodes")
return nil, err
}
dnsConfig := getMapResponseDNSConfig(
h.cfg.DNSConfig,
h.cfg.BaseDomain,
*machine,
peers,
)
resp := tailcfg.MapResponse{
KeepAlive: false,
Node: node,
Peers: nodePeers,
DNSConfig: dnsConfig,
Domain: h.cfg.BaseDomain,
PacketFilter: h.aclRules,
DERPMap: h.DERPMap,
UserProfiles: profiles,
Debug: &tailcfg.Debug{
DisableLogTail: !h.cfg.LogTail.Enabled,
RandomizeClientPort: h.cfg.RandomizeClientPort,
},
}
log.Trace().
Str("func", "generateMapResponse").
Str("machine", mapRequest.Hostinfo.Hostname).
// Interface("payload", resp).
Msgf("Generated map response: %s", tailMapResponseToString(resp))
return &resp, nil
}

View File

@@ -14,7 +14,7 @@ const (
apiPrefixLength = 7
apiKeyLength = 32
errAPIKeyFailedToParse = Error("Failed to parse ApiKey")
ErrAPIKeyFailedToParse = Error("Failed to parse ApiKey")
)
// APIKey describes the datamodel for API keys used to remotely authenticate with
@@ -116,7 +116,7 @@ func (h *Headscale) ExpireAPIKey(key *APIKey) error {
func (h *Headscale) ValidateAPIKey(keyStr string) (bool, error) {
prefix, hash, found := strings.Cut(keyStr, ".")
if !found {
return false, errAPIKeyFailedToParse
return false, ErrAPIKeyFailedToParse
}
key, err := h.GetAPIKey(prefix)

357
app.go
View File

@@ -17,16 +17,16 @@ import (
"time"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/gin-gonic/gin"
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
"github.com/gorilla/mux"
grpcMiddleware "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"
@@ -51,15 +51,20 @@ const (
errUnsupportedLetsEncryptChallengeType = Error(
"unknown value for Lets Encrypt challenge type",
)
ErrFailedPrivateKey = Error("failed to read or create private key")
ErrFailedNoisePrivateKey = Error("failed to read or create Noise protocol private key")
ErrSamePrivateKeys = Error("private key and noise private key are the same")
)
const (
AuthPrefix = "Bearer "
Postgres = "postgres"
Sqlite = "sqlite3"
updateInterval = 5000
HTTPReadTimeout = 30 * time.Second
privateKeyFileMode = 0o600
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
@@ -71,12 +76,15 @@ const (
// Headscale represents the base app of the service.
type Headscale struct {
cfg *Config
db *gorm.DB
dbString string
dbType string
dbDebug bool
privateKey *key.MachinePrivate
cfg *Config
db *gorm.DB
dbString string
dbType string
dbDebug bool
privateKey *key.MachinePrivate
noisePrivateKey *key.MachinePrivate
noiseMux *mux.Router
DERPMap *tailcfg.DERPMap
DERPServer *DERPServer
@@ -92,6 +100,9 @@ type Headscale struct {
registrationCache *cache.Cache
ipAllocationMutex sync.Mutex
shutdownChan chan struct{}
pollNetMapStreamWG sync.WaitGroup
}
// Look up the TLS constant relative to user-supplied TLS client
@@ -116,22 +127,42 @@ func LookupTLSClientAuthMode(mode string) (tls.ClientAuthType, bool) {
}
func NewHeadscale(cfg *Config) (*Headscale, error) {
privKey, err := readOrCreatePrivateKey(cfg.PrivateKeyPath)
privateKey, err := readOrCreatePrivateKey(cfg.PrivateKeyPath)
if err != nil {
return nil, fmt.Errorf("failed to read or create private key: %w", err)
return nil, ErrFailedPrivateKey
}
// TS2021 requires to have a different key from the legacy protocol.
noisePrivateKey, err := readOrCreatePrivateKey(cfg.NoisePrivateKeyPath)
if err != nil {
return nil, ErrFailedNoisePrivateKey
}
if privateKey.Equal(*noisePrivateKey) {
return nil, ErrSamePrivateKeys
}
var dbString string
switch cfg.DBtype {
case Postgres:
dbString = fmt.Sprintf(
"host=%s port=%d dbname=%s user=%s password=%s sslmode=disable",
"host=%s dbname=%s user=%s",
cfg.DBhost,
cfg.DBport,
cfg.DBname,
cfg.DBuser,
cfg.DBpass,
)
if !cfg.DBssl {
dbString += " sslmode=disable"
}
if cfg.DBport != 0 {
dbString += fmt.Sprintf(" port=%d", cfg.DBport)
}
if cfg.DBpass != "" {
dbString += fmt.Sprintf(" password=%s", cfg.DBpass)
}
case Sqlite:
dbString = cfg.DBpath
default:
@@ -144,12 +175,14 @@ func NewHeadscale(cfg *Config) (*Headscale, error) {
)
app := Headscale{
cfg: cfg,
dbType: cfg.DBtype,
dbString: dbString,
privateKey: privKey,
aclRules: tailcfg.FilterAllowAll, // default allowall
registrationCache: registrationCache,
cfg: cfg,
dbType: cfg.DBtype,
dbString: dbString,
privateKey: privateKey,
noisePrivateKey: noisePrivateKey,
aclRules: tailcfg.FilterAllowAll, // default allowall
registrationCache: registrationCache,
pollNetMapStreamWG: sync.WaitGroup{},
}
err = app.initDB()
@@ -168,7 +201,7 @@ 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
@@ -242,7 +275,7 @@ func (h *Headscale) expireEphemeralNodesWorker() {
}
if expiredFound {
h.setLastStateChangeToNow(namespace.Name)
h.setLastStateChangeToNow()
}
}
}
@@ -326,48 +359,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")
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")
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")
}
ctx.AbortWithStatus(http.StatusInternalServerError)
return
}
return
}
valid, err := h.ValidateAPIKey(strings.TrimPrefix(authHeader, AuthPrefix))
if err != nil {
log.Error().
Caller().
Err(err).
Str("client_address", req.RemoteAddr).
Msg("failed to validate token")
if !valid {
log.Info().
Str("client_address", ctx.ClientIP()).
Msg("invalid token")
writer.WriteHeader(http.StatusInternalServerError)
_, err := writer.Write([]byte("Unauthorized"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
ctx.AbortWithStatus(http.StatusUnauthorized)
return
}
return
}
if !valid {
log.Info().
Str("client_address", req.RemoteAddr).
Msg("invalid token")
ctx.Next()
writer.WriteHeader(http.StatusUnauthorized)
_, err := writer.Write([]byte("Unauthorized"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return
}
next.ServeHTTP(writer, req)
})
}
// ensureUnixSocketIsAbsent will check if the given path for headscales unix socket is clear
@@ -381,48 +440,45 @@ func (h *Headscale) ensureUnixSocketIsAbsent() error {
return os.Remove(h.cfg.UnixSocket)
}
func (h *Headscale) createPrometheusRouter() *gin.Engine {
promRouter := gin.Default()
func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *mux.Router {
router := mux.NewRouter()
prometheus := ginprometheus.NewPrometheus("gin")
prometheus.Use(promRouter)
router.HandleFunc(ts2021UpgradePath, h.NoiseUpgradeHandler).Methods(http.MethodPost)
return promRouter
}
func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *gin.Engine {
router := gin.Default()
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.AppleConfigMessage)
router.GET("/apple/:platform", h.ApplePlatformConfig)
router.GET("/windows", h.WindowsConfigMessage)
router.GET("/windows/tailscale.reg", h.WindowsRegConfig)
router.GET("/swagger", SwaggerUI)
router.GET("/swagger/v1/openapiv2.json", SwaggerAPIv1)
router.HandleFunc("/health", h.HealthHandler).Methods(http.MethodGet)
router.HandleFunc("/key", h.KeyHandler).Methods(http.MethodGet)
router.HandleFunc("/register/{nkey}", 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/{nkey}", h.RegisterOIDC).Methods(http.MethodGet)
router.HandleFunc("/oidc/callback", h.OIDCCallback).Methods(http.MethodGet)
router.HandleFunc("/apple", h.AppleConfigMessage).Methods(http.MethodGet)
router.HandleFunc("/apple/{platform}", h.ApplePlatformConfig).Methods(http.MethodGet)
router.HandleFunc("/windows", h.WindowsConfigMessage).Methods(http.MethodGet)
router.HandleFunc("/windows/tailscale.reg", h.WindowsRegConfig).Methods(http.MethodGet)
router.HandleFunc("/swagger", SwaggerUI).Methods(http.MethodGet)
router.HandleFunc("/swagger/v1/openapiv2.json", SwaggerAPIv1).Methods(http.MethodGet)
if h.cfg.DERP.ServerEnabled {
router.Any("/derp", h.DERPHandler)
router.Any("/derp/probe", h.DERPProbeHandler)
router.Any("/bootstrap-dns", h.DERPBootstrapDNSHandler)
router.HandleFunc("/derp", h.DERPHandler)
router.HandleFunc("/derp/probe", h.DERPProbeHandler)
router.HandleFunc("/bootstrap-dns", h.DERPBootstrapDNSHandler)
}
api := router.Group("/api")
api.Use(h.httpAuthenticationMiddleware)
{
api.Any("/v1/*any", gin.WrapF(grpcMux.ServeHTTP))
}
apiRouter := router.PathPrefix("/api").Subrouter()
apiRouter.Use(h.httpAuthenticationMiddleware)
apiRouter.PathPrefix("/v1/").HandlerFunc(grpcMux.ServeHTTP)
router.NoRoute(stdoutHandler)
router.PathPrefix("/").HandlerFunc(stdoutHandler)
return router
}
func (h *Headscale) createNoiseMux() *mux.Router {
router := mux.NewRouter()
router.HandleFunc("/machine/register", h.NoiseRegistrationHandler).Methods(http.MethodPost)
router.HandleFunc("/machine/map", h.NoisePollNetMapHandler)
return router
}
@@ -538,12 +594,14 @@ 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)
grpcOptions := []grpc.ServerOption{
grpc.UnaryInterceptor(
grpc_middleware.ChainUnaryServer(
grpcMiddleware.ChainUnaryServer(
h.grpcAuthenticationInterceptor,
zerolog.NewUnaryServerInterceptor(),
),
@@ -558,12 +616,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)
}
@@ -578,9 +636,16 @@ func (h *Headscale) Serve() error {
//
// HTTP setup
//
// This is the regular router that we expose
// over our main Addr. It also serves the legacy Tailcale API
router := h.createRouter(grpcGatewayMux)
// This router is served only over the Noise connection, and exposes only the new API.
//
// The HTTP2 server that exposes this router is created for
// a single hijacked connection from /ts2021, using netutil.NewOneConnListener
h.noiseMux = h.createNoiseMux()
httpServer := &http.Server{
Addr: h.cfg.Addr,
Handler: router,
@@ -608,11 +673,12 @@ func (h *Headscale) Serve() error {
log.Info().
Msgf("listening and serving HTTP on: %s", h.cfg.Addr)
promRouter := h.createPrometheusRouter()
promMux := http.NewServeMux()
promMux.Handle("/metrics", promhttp.Handler())
promHTTPServer := &http.Server{
Addr: h.cfg.MetricsAddr,
Handler: promRouter,
Handler: promMux,
ReadTimeout: HTTPReadTimeout,
WriteTimeout: 0,
}
@@ -630,6 +696,7 @@ func (h *Headscale) Serve() error {
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,
@@ -637,7 +704,7 @@ func (h *Headscale) Serve() error {
syscall.SIGTERM,
syscall.SIGQUIT,
syscall.SIGHUP)
go func(c chan os.Signal) {
sigFunc := func(c chan os.Signal) {
// Wait for a SIGINT or SIGKILL:
for {
sig := <-c
@@ -647,7 +714,7 @@ func (h *Headscale) Serve() error {
Str("signal", sig.String()).
Msg("Received SIGHUP, reloading ACL and Config")
// TODO(kradalby): Reload config on SIGHUP
// TODO(kradalby): Reload config on SIGHUP
if h.cfg.ACL.PolicyPath != "" {
aclPath := AbsolutePathFromConfigPath(h.cfg.ACL.PolicyPath)
@@ -667,11 +734,27 @@ func (h *Headscale) Serve() error {
Str("signal", sig.String()).
Msg("Received signal to stop, shutting down gracefully")
close(h.shutdownChan)
h.pollNetMapStreamWG.Wait()
// Gracefully shut down servers
promHTTPServer.Shutdown(ctx)
httpServer.Shutdown(ctx)
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()
@@ -680,11 +763,30 @@ func (h *Headscale) Serve() error {
// 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)
}
}
}(sigc)
}
errorGroup.Go(func() error {
sigFunc(sigc)
return nil
})
return errorGroup.Wait()
}
@@ -708,20 +810,29 @@ func (h *Headscale) getTLSSettings() (*tls.Config, error) {
}
switch h.cfg.TLS.LetsEncrypt.ChallengeType {
case "TLS-ALPN-01":
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.
server := &http.Server{
Addr: h.cfg.TLS.LetsEncrypt.Listen,
Handler: certManager.HTTPHandler(http.HandlerFunc(h.redirect)),
ReadTimeout: HTTPReadTimeout,
}
err := server.ListenAndServe()
go func() {
log.Fatal().
Caller().
Err(http.ListenAndServe(h.cfg.TLS.LetsEncrypt.Listen, certManager.HTTPHandler(http.HandlerFunc(h.redirect)))).
Err(err).
Msg("failed to set up a HTTP server")
}()
@@ -758,16 +869,17 @@ func (h *Headscale) getTLSSettings() (*tls.Config, error) {
}
}
func (h *Headscale) setLastStateChangeToNow(namespaces ...string) {
func (h *Headscale) setLastStateChangeToNow() {
var err error
now := time.Now().UTC()
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.")
}
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 {
@@ -811,13 +923,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

@@ -1,12 +1,11 @@
package headscale
import (
"io/ioutil"
"net/netip"
"os"
"testing"
"gopkg.in/check.v1"
"inet.af/netaddr"
)
func Test(t *testing.T) {
@@ -35,13 +34,13 @@ func (s *Suite) ResetDB(c *check.C) {
os.RemoveAll(tmpDir)
}
var err error
tmpDir, err = ioutil.TempDir("", "autoygg-client-test")
tmpDir, err = os.MkdirTemp("", "autoygg-client-test")
if err != nil {
c.Fatal(err)
}
cfg := Config{
IPPrefixes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("10.27.0.0/23"),
IPPrefixes: []netip.Prefix{
netip.MustParsePrefix("10.27.0.0/23"),
},
}

View File

@@ -134,7 +134,9 @@ If you loose a key, create a new one and revoke (expire) the old one.`,
expiration := time.Now().UTC().Add(time.Duration(duration))
log.Trace().Dur("expiration", time.Duration(duration)).Msg("expiration has been set")
log.Trace().
Dur("expiration", time.Duration(duration)).
Msg("expiration has been set")
request.Expiration = timestamppb.New(expiration)

View File

@@ -3,6 +3,7 @@ package cli
import (
"fmt"
"log"
"net/netip"
"strconv"
"strings"
"time"
@@ -13,7 +14,6 @@ import (
"github.com/pterm/pterm"
"github.com/spf13/cobra"
"google.golang.org/grpc/status"
"inet.af/netaddr"
"tailscale.com/types/key"
)
@@ -108,7 +108,7 @@ var registerNodeCmd = &cobra.Command{
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error getting machine key from flag: %s", err),
fmt.Sprintf("Error getting node key from flag: %s", err),
output,
)
@@ -465,6 +465,7 @@ func nodesToPtables(
) (pterm.TableData, error) {
tableHeader := []string{
"ID",
"Hostname",
"Name",
"NodeKey",
"Namespace",
@@ -556,7 +557,7 @@ func nodesToPtables(
var IPV4Address string
var IPV6Address string
for _, addr := range machine.IpAddresses {
if netaddr.MustParseIP(addr).Is4() {
if netip.MustParseAddr(addr).Is4() {
IPV4Address = addr
} else {
IPV6Address = addr
@@ -566,6 +567,7 @@ func nodesToPtables(
nodeData := []string{
strconv.FormatUint(machine.Id, headscale.Base10),
machine.Name,
machine.GetGivenName(),
nodeKey.ShortString(),
namespace,
strings.Join([]string{IPV4Address, IPV6Address}, ", "),

View File

@@ -164,7 +164,9 @@ var createPreAuthKeyCmd = &cobra.Command{
expiration := time.Now().UTC().Add(time.Duration(duration))
log.Trace().Dur("expiration", time.Duration(duration)).Msg("expiration has been set")
log.Trace().
Dur("expiration", time.Duration(duration)).
Msg("expiration has been set")
request.Expiration = timestamppb.New(expiration)

View File

@@ -25,15 +25,18 @@ func init() {
}
func initConfig() {
if cfgFile == "" {
cfgFile = os.Getenv("HEADSCALE_CONFIG")
}
if cfgFile != "" {
err := headscale.LoadConfig(cfgFile, true)
if err != nil {
log.Fatal().Caller().Err(err)
log.Fatal().Caller().Err(err).Msgf("Error loading config file %s", cfgFile)
}
} else {
err := headscale.LoadConfig("", false)
if err != nil {
log.Fatal().Caller().Err(err)
log.Fatal().Caller().Err(err).Msgf("Error loading config")
}
}

View File

@@ -7,12 +7,10 @@ import (
"fmt"
"os"
"reflect"
"time"
"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"
@@ -26,22 +24,10 @@ const (
func getHeadscaleApp() (*headscale.Headscale, error) {
cfg, err := headscale.GetHeadscaleConfig()
if err != nil {
return nil, fmt.Errorf("failed to load configuration while creating headscale instance: %w", err)
}
// Minimum inactivity time out is keepalive timeout (60s) plus a few seconds
// to avoid races
minInactivityTimeout, _ := time.ParseDuration("65s")
if viper.GetDuration("ephemeral_node_inactivity_timeout") <= minInactivityTimeout {
// TODO: Find a better way to return this text
//nolint
err := fmt.Errorf(
"ephemeral_node_inactivity_timeout (%s) is set too low, must be more than %s",
viper.GetString("ephemeral_node_inactivity_timeout"),
minInactivityTimeout,
return nil, fmt.Errorf(
"failed to load configuration while creating headscale instance: %w",
err,
)
return nil, err
}
app, err := headscale.NewHeadscale(cfg)
@@ -72,6 +58,7 @@ func getHeadscaleCLIClient() (context.Context, v1.HeadscaleServiceClient, *grpc.
Err(err).
Caller().
Msgf("Failed to load configuration")
os.Exit(-1) // we get here if logging is suppressed (i.e., json output)
}
log.Debug().
@@ -133,6 +120,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)

View File

@@ -2,7 +2,6 @@ package main
import (
"io/fs"
"io/ioutil"
"os"
"path/filepath"
"strings"
@@ -28,7 +27,7 @@ func (s *Suite) TearDownSuite(c *check.C) {
}
func (*Suite) TestConfigFileLoading(c *check.C) {
tmpDir, err := ioutil.TempDir("", "headscale")
tmpDir, err := os.MkdirTemp("", "headscale")
if err != nil {
c.Fatal(err)
}
@@ -73,7 +72,7 @@ func (*Suite) TestConfigFileLoading(c *check.C) {
}
func (*Suite) TestConfigLoading(c *check.C) {
tmpDir, err := ioutil.TempDir("", "headscale")
tmpDir, err := os.MkdirTemp("", "headscale")
if err != nil {
c.Fatal(err)
}
@@ -113,10 +112,11 @@ func (*Suite) TestConfigLoading(c *check.C) {
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) {
tmpDir, err := ioutil.TempDir("", "headscale")
tmpDir, err := os.MkdirTemp("", "headscale")
if err != nil {
c.Fatal(err)
}
@@ -151,22 +151,24 @@ func (*Suite) TestDNSConfigLoading(c *check.C) {
func writeConfig(c *check.C, tmpDir string, configYaml []byte) {
// Populate a custom config file
configFile := filepath.Join(tmpDir, "config.yaml")
err := ioutil.WriteFile(configFile, configYaml, 0o600)
err := os.WriteFile(configFile, configYaml, 0o600)
if err != nil {
c.Fatalf("Couldn't write file %s", configFile)
}
}
func (*Suite) TestTLSConfigValidation(c *check.C) {
tmpDir, err := ioutil.TempDir("", "headscale")
tmpDir, err := os.MkdirTemp("", "headscale")
if err != nil {
c.Fatal(err)
}
// defer os.RemoveAll(tmpDir)
configYaml := []byte(
"---\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"\"\ntls_cert_path: \"abc.pem\"",
)
configYaml := []byte(`---
tls_letsencrypt_hostname: example.com
tls_letsencrypt_challenge_type: ""
tls_cert_path: abc.pem
noise:
private_key_path: noise_private.key`)
writeConfig(c, tmpDir, configYaml)
// Check configuration validation errors (1)
@@ -191,9 +193,13 @@ func (*Suite) TestTLSConfigValidation(c *check.C) {
)
// Check configuration validation errors (2)
configYaml = []byte(
"---\nserver_url: \"http://127.0.0.1:8080\"\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"TLS-ALPN-01\"",
)
configYaml = []byte(`---
noise:
private_key_path: noise_private.key
server_url: http://127.0.0.1:8080
tls_letsencrypt_hostname: example.com
tls_letsencrypt_challenge_type: TLS-ALPN-01
`)
writeConfig(c, tmpDir, configYaml)
err = headscale.LoadConfig(tmpDir, false)
c.Assert(err, check.IsNil)

View File

@@ -41,6 +41,15 @@ grpc_allow_insecure: false
# autogenerated if it's missing
private_key_path: /var/lib/headscale/private.key
# The Noise section includes specific configuration for the
# TS2021 Noise procotol
noise:
# The Noise private key is used to encrypt the
# traffic between headscale and Tailscale clients when
# using the new Noise-based protocol. It must be different
# from the legacy private key.
private_key_path: /var/lib/headscale/noise_private.key
# List of IP prefixes to allocate tailaddresses from.
# Each prefix consists of either an IPv4 or IPv6 address,
# and the associated prefix length, delimited by a slash.
@@ -103,17 +112,25 @@ 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
# # Postgres config
# If using a Unix socket to connect to Postgres, set the socket path in the 'host' field and leave 'port' blank.
# db_type: postgres
# db_host: localhost
# db_port: 5432
# db_name: headscale
# db_user: foo
# db_pass: bar
# db_ssl: false
### TLS configuration
#
@@ -244,3 +261,8 @@ logtail:
# 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

View File

@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"io/fs"
"net/netip"
"net/url"
"strings"
"time"
@@ -13,11 +14,16 @@ import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
"inet.af/netaddr"
"go4.org/netipx"
"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
@@ -26,8 +32,10 @@ type Config struct {
GRPCAddr string
GRPCAllowInsecure bool
EphemeralNodeInactivityTimeout time.Duration
IPPrefixes []netaddr.IPPrefix
NodeUpdateCheckInterval time.Duration
IPPrefixes []netip.Prefix
PrivateKeyPath string
NoisePrivateKeyPath string
BaseDomain string
LogLevel zerolog.Level
DisableUpdateCheck bool
@@ -41,6 +49,7 @@ type Config struct {
DBname string
DBuser string
DBpass string
DBssl bool
TLS TLSConfig
@@ -54,7 +63,8 @@ type Config struct {
OIDC OIDCConfig
LogTail LogTailConfig
LogTail LogTailConfig
RandomizeClientPort bool
CLI CLIConfig
@@ -134,7 +144,7 @@ func LoadConfig(path string, isFile bool) error {
viper.AutomaticEnv()
viper.SetDefault("tls_letsencrypt_cache_dir", "/var/www/.cache")
viper.SetDefault("tls_letsencrypt_challenge_type", "HTTP-01")
viper.SetDefault("tls_letsencrypt_challenge_type", http01ChallengeType)
viper.SetDefault("tls_client_auth_mode", "relaxed")
viper.SetDefault("log_level", "info")
@@ -157,8 +167,15 @@ func LoadConfig(path string, isFile bool) error {
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)
}
@@ -169,16 +186,20 @@ func LoadConfig(path string, isFile bool) error {
errorText += "Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both\n"
}
if !viper.IsSet("noise") || viper.GetString("noise.private_key_path") == "" {
errorText += "Fatal config error: headscale now requires a new `noise.private_key_path` field in the config file for the Tailscale v2 protocol\n"
}
if (viper.GetString("tls_letsencrypt_hostname") != "") &&
(viper.GetString("tls_letsencrypt_challenge_type") == "TLS-ALPN-01") &&
(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") != "HTTP-01") &&
(viper.GetString("tls_letsencrypt_challenge_type") != "TLS-ALPN-01") {
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"
}
@@ -200,6 +221,26 @@ func LoadConfig(path string, isFile bool) error {
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"))
@@ -300,11 +341,11 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) {
if viper.IsSet("dns_config.nameservers") {
nameserversStr := viper.GetStringSlice("dns_config.nameservers")
nameservers := make([]netaddr.IP, len(nameserversStr))
resolvers := make([]dnstype.Resolver, len(nameserversStr))
nameservers := make([]netip.Addr, len(nameserversStr))
resolvers := make([]*dnstype.Resolver, len(nameserversStr))
for index, nameserverStr := range nameserversStr {
nameserver, err := netaddr.ParseIP(nameserverStr)
nameserver, err := netip.ParseAddr(nameserverStr)
if err != nil {
log.Error().
Str("func", "getDNSConfig").
@@ -313,7 +354,7 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) {
}
nameservers[index] = nameserver
resolvers[index] = dnstype.Resolver{
resolvers[index] = &dnstype.Resolver{
Addr: nameserver.String(),
}
}
@@ -324,24 +365,24 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) {
if viper.IsSet("dns_config.restricted_nameservers") {
if len(dnsConfig.Nameservers) > 0 {
dnsConfig.Routes = make(map[string][]dnstype.Resolver)
dnsConfig.Routes = make(map[string][]*dnstype.Resolver)
restrictedDNS := viper.GetStringMapStringSlice(
"dns_config.restricted_nameservers",
)
for domain, restrictedNameservers := range restrictedDNS {
restrictedResolvers := make(
[]dnstype.Resolver,
[]*dnstype.Resolver,
len(restrictedNameservers),
)
for index, nameserverStr := range restrictedNameservers {
nameserver, err := netaddr.ParseIP(nameserverStr)
nameserver, err := netip.ParseAddr(nameserverStr)
if err != nil {
log.Error().
Str("func", "getDNSConfig").
Err(err).
Msgf("Could not parse restricted nameserver IP: %s", nameserverStr)
}
restrictedResolvers[index] = dnstype.Resolver{
restrictedResolvers[index] = &dnstype.Resolver{
Addr: nameserver.String(),
}
}
@@ -384,9 +425,10 @@ 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)
parsedPrefixes := make([]netip.Prefix, 0, len(configuredPrefixes)+1)
logLevelStr := viper.GetString("log_level")
logLevel, err := zerolog.ParseLevel(logLevelStr)
@@ -403,7 +445,7 @@ func GetHeadscaleConfig() (*Config, error) {
"use of 'ip_prefix' for configuration is deprecated",
"please see 'ip_prefixes' in the shipped example.",
)
legacyPrefix, err := netaddr.ParseIPPrefix(legacyPrefixField)
legacyPrefix, err := netip.ParsePrefix(legacyPrefixField)
if err != nil {
panic(fmt.Errorf("failed to parse ip_prefix: %w", err))
}
@@ -411,19 +453,19 @@ func GetHeadscaleConfig() (*Config, error) {
}
for i, prefixInConfig := range configuredPrefixes {
prefix, err := netaddr.ParseIPPrefix(prefixInConfig)
prefix, err := netip.ParsePrefix(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))
prefixes := make([]netip.Prefix, 0, len(parsedPrefixes))
{
// dedup
normalizedPrefixes := make(map[string]int, len(parsedPrefixes))
for i, p := range parsedPrefixes {
normalized, _ := p.Range().Prefix()
normalized, _ := netipx.RangeOfPrefix(p).Prefix()
normalizedPrefixes[normalized.String()] = i
}
@@ -434,7 +476,7 @@ func GetHeadscaleConfig() (*Config, error) {
}
if len(prefixes) < 1 {
prefixes = append(prefixes, netaddr.MustParseIPPrefix("100.64.0.0/10"))
prefixes = append(prefixes, netip.MustParsePrefix("100.64.0.0/10"))
log.Warn().
Msgf("'ip_prefixes' not configured, falling back to default: %v", prefixes)
}
@@ -452,6 +494,9 @@ func GetHeadscaleConfig() (*Config, error) {
PrivateKeyPath: AbsolutePathFromConfigPath(
viper.GetString("private_key_path"),
),
NoisePrivateKeyPath: AbsolutePathFromConfigPath(
viper.GetString("noise.private_key_path"),
),
BaseDomain: baseDomain,
DERP: derpConfig,
@@ -460,6 +505,10 @@ func GetHeadscaleConfig() (*Config, error) {
"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"),
@@ -467,6 +516,7 @@ func GetHeadscaleConfig() (*Config, error) {
DBname: viper.GetString("db_name"),
DBuser: viper.GetString("db_user"),
DBpass: viper.GetString("db_pass"),
DBssl: viper.GetBool("db_ssl"),
TLS: GetTLSConfig(),
@@ -489,7 +539,8 @@ func GetHeadscaleConfig() (*Config, error) {
StripEmaildomain: viper.GetBool("oidc.strip_email_domain"),
},
LogTail: logConfig,
LogTail: logConfig,
RandomizeClientPort: randomizeClientPort,
CLI: CLIConfig{
Address: viper.GetString("cli.address"),

27
db.go
View File

@@ -1,10 +1,12 @@
package headscale
import (
"context"
"database/sql/driver"
"encoding/json"
"errors"
"fmt"
"net/netip"
"time"
"github.com/glebarez/sqlite"
@@ -12,7 +14,6 @@ import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"inet.af/netaddr"
"tailscale.com/tailcfg"
)
@@ -89,7 +90,7 @@ func (h *Headscale) initDB() error {
log.Error().Err(err).Msg("Error accessing db")
}
for _, machine := range machines {
for item, machine := range machines {
if machine.GivenName == "" {
normalizedHostname, err := NormalizeToFQDNRules(
machine.Hostname,
@@ -103,7 +104,7 @@ func (h *Headscale) initDB() error {
Msg("Failed to normalize machine hostname in DB migration")
}
err = h.RenameMachine(&machine, normalizedHostname)
err = h.RenameMachine(&machines[item], normalizedHostname)
if err != nil {
log.Error().
Caller().
@@ -111,7 +112,6 @@ func (h *Headscale) initDB() error {
Err(err).
Msg("Failed to save normalized machine name in DB migration")
}
}
}
}
@@ -221,6 +221,17 @@ func (h *Headscale) setValue(key string, value string) error {
return nil
}
func (h *Headscale) pingDB(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, 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
@@ -237,7 +248,7 @@ func (hi *HostInfo) Scan(destination interface{}) error {
return json.Unmarshal([]byte(value), hi)
default:
return fmt.Errorf("%w: unexpected data type %T", errMachineAddressesInvalid, destination)
return fmt.Errorf("%w: unexpected data type %T", ErrMachineAddressesInvalid, destination)
}
}
@@ -248,7 +259,7 @@ func (hi HostInfo) Value() (driver.Value, error) {
return string(bytes), err
}
type IPPrefixes []netaddr.IPPrefix
type IPPrefixes []netip.Prefix
func (i *IPPrefixes) Scan(destination interface{}) error {
switch value := destination.(type) {
@@ -259,7 +270,7 @@ func (i *IPPrefixes) Scan(destination interface{}) error {
return json.Unmarshal([]byte(value), i)
default:
return fmt.Errorf("%w: unexpected data type %T", errMachineAddressesInvalid, destination)
return fmt.Errorf("%w: unexpected data type %T", ErrMachineAddressesInvalid, destination)
}
}
@@ -281,7 +292,7 @@ func (i *StringList) Scan(destination interface{}) error {
return json.Unmarshal([]byte(value), i)
default:
return fmt.Errorf("%w: unexpected data type %T", errMachineAddressesInvalid, destination)
return fmt.Errorf("%w: unexpected data type %T", ErrMachineAddressesInvalid, destination)
}
}

16
derp.go
View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
@@ -35,7 +34,7 @@ func loadDERPMapFromURL(addr url.URL) (*tailcfg.DERPMap, error) {
ctx, cancel := context.WithTimeout(context.Background(), HTTPReadTimeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", addr.String(), nil)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, addr.String(), nil)
if err != nil {
return nil, err
}
@@ -50,7 +49,7 @@ func loadDERPMapFromURL(addr url.URL) (*tailcfg.DERPMap, error) {
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
@@ -152,16 +151,7 @@ func (h *Headscale) scheduledDERPMapUpdateWorker(cancelChan <-chan struct{}) {
h.DERPMap.Regions[h.DERPServer.region.RegionID] = &h.DERPServer.region
}
namespaces, err := h.ListNamespaces()
if err != nil {
log.Error().
Err(err).
Msg("Failed to fetch namespaces")
}
for _, namespace := range namespaces {
h.setLastStateChangeToNow(namespace.Name)
}
h.setLastStateChangeToNow()
}
}
}

View File

@@ -2,15 +2,16 @@ package headscale
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"net/netip"
"net/url"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"tailscale.com/derp"
"tailscale.com/net/stun"
@@ -30,6 +31,7 @@ type DERPServer struct {
}
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 {
@@ -87,30 +89,51 @@ func (h *Headscale) generateRegionLocalDERP() (tailcfg.DERPRegion, error) {
}
localDERPregion.Nodes[0].STUNPort = portSTUN
log.Info().Caller().Msgf("DERP region: %+v", localDERPregion)
return localDERPregion, nil
}
func (h *Headscale) DERPHandler(ctx *gin.Context) {
log.Trace().Caller().Msgf("/derp request from %v", ctx.ClientIP())
up := strings.ToLower(ctx.Request.Header.Get("Upgrade"))
if up != "websocket" && up != "derp" {
if up != "" {
log.Warn().Caller().Msgf("Weird websockets connection upgrade: %q", up)
func (h *Headscale) DERPHandler(
writer http.ResponseWriter,
req *http.Request,
) {
log.Trace().Caller().Msgf("/derp request from %v", req.RemoteAddr)
upgrade := strings.ToLower(req.Header.Get("Upgrade"))
if upgrade != "websocket" && upgrade != "derp" {
if upgrade != "" {
log.Warn().
Caller().
Msg("No Upgrade header in DERP server request. If headscale is behind a reverse proxy, make sure it is configured to pass WebSockets through.")
}
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")
}
ctx.String(http.StatusUpgradeRequired, "DERP requires connection upgrade")
return
}
fastStart := ctx.Request.Header.Get(fastStartHeader) == "1"
fastStart := req.Header.Get(fastStartHeader) == "1"
hijacker, ok := ctx.Writer.(http.Hijacker)
hijacker, ok := writer.(http.Hijacker)
if !ok {
log.Error().Caller().Msg("DERP requires Hijacker interface from Gin")
ctx.String(
http.StatusInternalServerError,
"HTTP does not support general TCP support",
)
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
}
@@ -118,17 +141,23 @@ func (h *Headscale) DERPHandler(ctx *gin.Context) {
netConn, conn, err := hijacker.Hijack()
if err != nil {
log.Error().Caller().Err(err).Msgf("Hijack failed")
ctx.String(
http.StatusInternalServerError,
"HTTP does not support general TCP support",
)
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
pubKeyStr := pubKey.UntypedHexString() //nolint
fmt.Fprintf(conn, "HTTP/1.1 101 Switching Protocols\r\n"+
"Upgrade: DERP\r\n"+
"Connection: Upgrade\r\n"+
@@ -138,17 +167,28 @@ func (h *Headscale) DERPHandler(ctx *gin.Context) {
pubKeyStr)
}
h.DERPServer.tailscaleDERP.Accept(netConn, conn, netConn.RemoteAddr().String())
h.DERPServer.tailscaleDERP.Accept(req.Context(), netConn, conn, netConn.RemoteAddr().String())
}
// DERPProbeHandler is the endpoint that js/wasm clients hit to measure
// DERP latency, since they can't do UDP STUN queries.
func (h *Headscale) DERPProbeHandler(ctx *gin.Context) {
switch ctx.Request.Method {
case "HEAD", "GET":
ctx.Writer.Header().Set("Access-Control-Allow-Origin", "*")
func (h *Headscale) DERPProbeHandler(
writer http.ResponseWriter,
req *http.Request,
) {
switch req.Method {
case http.MethodHead, http.MethodGet:
writer.Header().Set("Access-Control-Allow-Origin", "*")
writer.WriteHeader(http.StatusOK)
default:
ctx.String(http.StatusMethodNotAllowed, "bogus probe method")
writer.WriteHeader(http.StatusMethodNotAllowed)
_, err := writer.Write([]byte("bogus probe method"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
}
}
@@ -159,15 +199,18 @@ func (h *Headscale) DERPProbeHandler(ctx *gin.Context) {
// The initial implementation is here https://github.com/tailscale/tailscale/pull/1406
// They have a cache, but not clear if that is really necessary at Headscale, uh, scale.
// An example implementation is found here https://derp.tailscale.com/bootstrap-dns
func (h *Headscale) DERPBootstrapDNSHandler(ctx *gin.Context) {
func (h *Headscale) DERPBootstrapDNSHandler(
writer http.ResponseWriter,
req *http.Request,
) {
dnsEntries := make(map[string][]net.IP)
resolvCtx, cancel := context.WithTimeout(context.Background(), time.Minute)
resolvCtx, cancel := context.WithTimeout(req.Context(), time.Minute)
defer cancel()
var r net.Resolver
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 := r.LookupIP(resolvCtx, "ip", node.HostName)
addrs, err := resolver.LookupIP(resolvCtx, "ip", node.HostName)
if err != nil {
log.Trace().
Caller().
@@ -179,7 +222,15 @@ func (h *Headscale) DERPBootstrapDNSHandler(ctx *gin.Context) {
dnsEntries[node.HostName] = addrs
}
}
ctx.JSON(http.StatusOK, dnsEntries)
writer.Header().Set("Content-Type", "application/json")
writer.WriteHeader(http.StatusOK)
err := json.NewEncoder(writer).Encode(dnsEntries)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
}
// ServeSTUN starts a STUN server on the configured addr.
@@ -229,7 +280,8 @@ func serverSTUNListener(ctx context.Context, packetConn *net.UDPConn) {
continue
}
res := stun.Response(txid, udpAddr.IP, uint16(udpAddr.Port))
addr, _ := netip.AddrFromSlice(udpAddr.IP)
res := stun.Response(txid, netip.AddrPortFrom(addr, uint16(udpAddr.Port)))
_, err = packetConn.WriteTo(res, udpAddr)
if err != nil {
log.Trace().Caller().Err(err).Msgf("Issue writing to UDP")

21
dns.go
View File

@@ -2,10 +2,11 @@ package headscale
import (
"fmt"
"net/netip"
"strings"
mapset "github.com/deckarep/golang-set/v2"
"inet.af/netaddr"
"go4.org/netipx"
"tailscale.com/tailcfg"
"tailscale.com/util/dnsname"
)
@@ -39,11 +40,11 @@ const (
// From the netmask we can find out the wildcard bits (the bits that are not set in the netmask).
// This allows us to then calculate the subnets included in the subsequent class block and generate the entries.
func generateMagicDNSRootDomains(ipPrefixes []netaddr.IPPrefix) []dnsname.FQDN {
func generateMagicDNSRootDomains(ipPrefixes []netip.Prefix) []dnsname.FQDN {
fqdns := make([]dnsname.FQDN, 0, len(ipPrefixes))
for _, ipPrefix := range ipPrefixes {
var generateDNSRoot func(netaddr.IPPrefix) []dnsname.FQDN
switch ipPrefix.IP().BitLen() {
var generateDNSRoot func(netip.Prefix) []dnsname.FQDN
switch ipPrefix.Addr().BitLen() {
case ipv4AddressLength:
generateDNSRoot = generateIPv4DNSRootDomain
@@ -54,7 +55,7 @@ func generateMagicDNSRootDomains(ipPrefixes []netaddr.IPPrefix) []dnsname.FQDN {
panic(
fmt.Sprintf(
"unsupported IP version with address length %d",
ipPrefix.IP().BitLen(),
ipPrefix.Addr().BitLen(),
),
)
}
@@ -65,9 +66,9 @@ func generateMagicDNSRootDomains(ipPrefixes []netaddr.IPPrefix) []dnsname.FQDN {
return fqdns
}
func generateIPv4DNSRootDomain(ipPrefix netaddr.IPPrefix) []dnsname.FQDN {
func generateIPv4DNSRootDomain(ipPrefix netip.Prefix) []dnsname.FQDN {
// Conversion to the std lib net.IPnet, a bit easier to operate
netRange := ipPrefix.IPNet()
netRange := netipx.PrefixIPNet(ipPrefix)
maskBits, _ := netRange.Mask.Size()
// lastOctet is the last IP byte covered by the mask
@@ -101,11 +102,11 @@ func generateIPv4DNSRootDomain(ipPrefix netaddr.IPPrefix) []dnsname.FQDN {
return fqdns
}
func generateIPv6DNSRootDomain(ipPrefix netaddr.IPPrefix) []dnsname.FQDN {
func generateIPv6DNSRootDomain(ipPrefix netip.Prefix) []dnsname.FQDN {
const nibbleLen = 4
maskBits, _ := ipPrefix.IPNet().Mask.Size()
expanded := ipPrefix.IP().StringExpanded()
maskBits, _ := netipx.PrefixIPNet(ipPrefix).Mask.Size()
expanded := ipPrefix.Addr().StringExpanded()
nibbleStr := strings.Map(func(r rune) rune {
if r == ':' {
return -1

View File

@@ -2,16 +2,16 @@ package headscale
import (
"fmt"
"net/netip"
"gopkg.in/check.v1"
"inet.af/netaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/dnstype"
)
func (s *Suite) TestMagicDNSRootDomains100(c *check.C) {
prefixes := []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("100.64.0.0/10"),
prefixes := []netip.Prefix{
netip.MustParsePrefix("100.64.0.0/10"),
}
domains := generateMagicDNSRootDomains(prefixes)
@@ -47,8 +47,8 @@ func (s *Suite) TestMagicDNSRootDomains100(c *check.C) {
}
func (s *Suite) TestMagicDNSRootDomains172(c *check.C) {
prefixes := []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("172.16.0.0/16"),
prefixes := []netip.Prefix{
netip.MustParsePrefix("172.16.0.0/16"),
}
domains := generateMagicDNSRootDomains(prefixes)
@@ -75,8 +75,8 @@ func (s *Suite) TestMagicDNSRootDomains172(c *check.C) {
// Happens when netmask is a multiple of 4 bits (sounds likely).
func (s *Suite) TestMagicDNSRootDomainsIPv6Single(c *check.C) {
prefixes := []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/48"),
prefixes := []netip.Prefix{
netip.MustParsePrefix("fd7a:115c:a1e0::/48"),
}
domains := generateMagicDNSRootDomains(prefixes)
@@ -89,8 +89,8 @@ func (s *Suite) TestMagicDNSRootDomainsIPv6Single(c *check.C) {
}
func (s *Suite) TestMagicDNSRootDomainsIPv6SingleMultiple(c *check.C) {
prefixes := []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/50"),
prefixes := []netip.Prefix{
netip.MustParsePrefix("fd7a:115c:a1e0::/50"),
}
domains := generateMagicDNSRootDomains(prefixes)
@@ -161,11 +161,11 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
Hostname: "test_get_shared_nodes_1",
Hostname: "test_get_shared_nodes_1",
NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")},
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")},
AuthKeyID: uint(preAuthKeyInShared1.ID),
}
app.db.Save(machineInShared1)
@@ -178,11 +178,11 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Hostname: "test_get_shared_nodes_2",
Hostname: "test_get_shared_nodes_2",
NamespaceID: namespaceShared2.ID,
Namespace: *namespaceShared2,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.2")},
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.2")},
AuthKeyID: uint(preAuthKeyInShared2.ID),
}
app.db.Save(machineInShared2)
@@ -195,11 +195,11 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Hostname: "test_get_shared_nodes_3",
Hostname: "test_get_shared_nodes_3",
NamespaceID: namespaceShared3.ID,
Namespace: *namespaceShared3,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.3")},
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.3")},
AuthKeyID: uint(preAuthKeyInShared3.ID),
}
app.db.Save(machineInShared3)
@@ -212,18 +212,18 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Hostname: "test_get_shared_nodes_4",
Hostname: "test_get_shared_nodes_4",
NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")},
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.4")},
AuthKeyID: uint(PreAuthKey2InShared1.ID),
}
app.db.Save(machine2InShared1)
baseDomain := "foobar.headscale.net"
dnsConfigOrig := tailcfg.DNSConfig{
Routes: make(map[string][]dnstype.Resolver),
Routes: make(map[string][]*dnstype.Resolver),
Domains: []string{baseDomain},
Proxied: true,
}
@@ -304,11 +304,11 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
Hostname: "test_get_shared_nodes_1",
Hostname: "test_get_shared_nodes_1",
NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")},
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")},
AuthKeyID: uint(preAuthKeyInShared1.ID),
}
app.db.Save(machineInShared1)
@@ -321,11 +321,11 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Hostname: "test_get_shared_nodes_2",
Hostname: "test_get_shared_nodes_2",
NamespaceID: namespaceShared2.ID,
Namespace: *namespaceShared2,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.2")},
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.2")},
AuthKeyID: uint(preAuthKeyInShared2.ID),
}
app.db.Save(machineInShared2)
@@ -338,11 +338,11 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Hostname: "test_get_shared_nodes_3",
Hostname: "test_get_shared_nodes_3",
NamespaceID: namespaceShared3.ID,
Namespace: *namespaceShared3,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.3")},
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.3")},
AuthKeyID: uint(preAuthKeyInShared3.ID),
}
app.db.Save(machineInShared3)
@@ -355,18 +355,18 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Hostname: "test_get_shared_nodes_4",
Hostname: "test_get_shared_nodes_4",
NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")},
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.4")},
AuthKeyID: uint(preAuthKey2InShared1.ID),
}
app.db.Save(machine2InShared1)
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

@@ -36,7 +36,7 @@ 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
`--advertise-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

19
docs/android-client.md Normal file
View File

@@ -0,0 +1,19 @@
# Connecting an Android client
## Goal
This documentation has the goal of showing how a user can use the official Android [Tailscale](https://tailscale.com) client with `headscale`.
## Installation
Install the official Tailscale Android client from the [Google Play Store](https://play.google.com/store/apps/details?id=com.tailscale.ipn) or [F-Droid](https://f-droid.org/packages/com.tailscale.ipn/).
Ensure that the installed version is at least 1.30.0, as that is the first release to support custom URLs.
## Configuring the headscale URL
After opening the app, the kebab menu icon (three dots) on the top bar on the right must be repeatedly opened and closed until the _Change server_ option appears in the menu. This is where you can enter your headscale URL.
A screen recording of this process can be seen in the `tailscale-android` PR which implemented this functionality: <https://github.com/tailscale/tailscale-android/pull/55>
After saving and restarting the app, selecting the regular _Sign in_ option (non-SSO) should open up the headscale authentication page.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -53,6 +53,9 @@ server_url: http://your-host-name:8080 # Change to your hostname or host IP
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
noise:
private_key_path: /var/lib/headscale/noise_private.key
# The default /var/lib/headscale path is not writable in the container
db_path: /etc/headscale/db.sqlite
```

View File

@@ -17,7 +17,7 @@ describing how to make `headscale` run properly in a server environment.
```shell
# Install prerequistes
# 1. go v1.18+: headscale newer than 0.15 needs go 1.18+ to compile
# 1. go v1.19+: headscale newer than 0.17 needs go 1.19+ to compile
# 2. gmake: Makefile in the headscale repo is written in GNU make syntax
pkg_add -D snap go
pkg_add gmake
@@ -46,7 +46,7 @@ cp headscale /usr/local/sbin
```shell
# Install prerequistes
# 1. go v1.18+: headscale newer than 0.15 needs go 1.18+ to compile
# 1. go v1.19+: headscale newer than 0.17 needs go 1.19+ to compile
# 2. gmake: Makefile in the headscale repo is written in GNU make syntax
git clone https://github.com/juanfont/headscale.git

14
flake.lock generated
View File

@@ -2,11 +2,11 @@
"nodes": {
"flake-utils": {
"locked": {
"lastModified": 1644229661,
"narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=",
"lastModified": 1659877975,
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "3cecb5b042f7f209c56ffd8371b2711a290ec797",
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
"type": "github"
},
"original": {
@@ -17,16 +17,16 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1653733789,
"narHash": "sha256-VIYazYCWNvcFNns2XQkHx/mVmCZ3oebZv8W2LS1gLQE=",
"lastModified": 1662019588,
"narHash": "sha256-oPEjHKGGVbBXqwwL+UjsveJzghWiWV0n9ogo1X6l4cw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d1086907f56c5a6c33c0c2e8dc9f42ef6988294f",
"rev": "2da64a81275b68fdad38af669afeda43d401e94b",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-22.05",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}

View File

@@ -2,7 +2,7 @@
description = "headscale - Open Source Tailscale Control server";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-22.05";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
@@ -17,14 +17,14 @@
in
rec {
headscale =
pkgs.buildGo118Module rec {
pkgs.buildGo119Module 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-b6qPOO/NmcXsAsSRWZlYXZKyRAF++DsL4TEZzRhQhME=";
vendorSha256 = "sha256-kc8EU+TkwRlsKM2+ljm/88aWe5h2QMgd/ZGPSgdd9QQ=";
ldflags = [ "-s" "-w" "-X github.com/juanfont/headscale/cmd/headscale/cli.Version=v${version}" ];
};
@@ -95,7 +95,7 @@
overlays = [ self.overlay ];
inherit system;
};
buildDeps = with pkgs; [ git go_1_18 gnumake ];
buildDeps = with pkgs; [ git go_1_19 gnumake ];
devDeps = with pkgs;
buildDeps ++ [
golangci-lint

131
go.mod
View File

@@ -1,71 +1,76 @@
module github.com/juanfont/headscale
go 1.18
go 1.19
require (
github.com/AlecAivazis/survey/v2 v2.3.4
github.com/AlecAivazis/survey/v2 v2.3.5
github.com/ccding/go-stun/stun v0.0.0-20200514191101-4dc67bcdb029
github.com/coreos/go-oidc/v3 v3.1.0
github.com/coreos/go-oidc/v3 v3.3.0
github.com/deckarep/golang-set/v2 v2.1.0
github.com/efekarakus/termcolor v1.0.1
github.com/gin-gonic/gin v1.7.7
github.com/glebarez/sqlite v1.4.3
github.com/glebarez/sqlite v1.4.6
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.10.0
github.com/klauspost/compress v1.15.1
github.com/ory/dockertest/v3 v3.8.1
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3
github.com/klauspost/compress v1.15.9
github.com/oauth2-proxy/mockoidc v0.0.0-20220308204021-b9169deeb282
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.41
github.com/rs/zerolog v1.26.1
github.com/spf13/cobra v1.4.0
github.com/spf13/viper v1.11.0
github.com/stretchr/testify v1.7.1
github.com/tailscale/hujson v0.0.0-20220421170326-6583d0610064
github.com/prometheus/client_golang v1.13.0
github.com/prometheus/common v0.37.0
github.com/pterm/pterm v0.12.45
github.com/puzpuzpuz/xsync v1.4.3
github.com/rs/zerolog v1.28.0
github.com/spf13/cobra v1.5.0
github.com/spf13/viper v1.12.0
github.com/stretchr/testify v1.8.0
github.com/tailscale/hujson v0.0.0-20220630195928-54599719472f
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-20220411220226-7b82a4e95df4
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
google.golang.org/genproto v0.0.0-20220422154200-b37d22cd5731
google.golang.org/grpc v1.46.0
google.golang.org/protobuf v1.28.0
go4.org/netipx v0.0.0-20220812043211-3cc044ffd68d
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b
golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde
google.golang.org/genproto v0.0.0-20220902135211-223410557253
google.golang.org/grpc v1.49.0
google.golang.org/protobuf v1.28.1
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
gorm.io/driver/postgres v1.3.5
gorm.io/gorm v1.23.4
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6
tailscale.com v1.24.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/postgres v1.3.9
gorm.io/gorm v1.23.8
tailscale.com v1.30.0
)
require (
atomicgo.dev/cursor v0.1.1 // indirect
atomicgo.dev/keyboard v0.2.8 // indirect
filippo.io/edwards25519 v1.0.0-rc.1 // indirect
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
github.com/Microsoft/go-winio v0.5.1 // indirect
github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/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.0.0-20190827140505-75bee3e2ccb6 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/containerd/continuity v0.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/cli v20.10.11+incompatible // indirect
github.com/docker/docker v20.10.7+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.16.0 // indirect
github.com/go-playground/locales v0.13.0 // indirect
github.com/go-playground/universal-translator v0.17.0 // indirect
github.com/go-playground/validator/v10 v10.4.1 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
github.com/glebarez/go-sqlite v1.17.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.7 // indirect
github.com/google/go-cmp v0.5.8 // indirect
github.com/google/go-github v17.0.0+incompatible // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
@@ -73,25 +78,25 @@ require (
github.com/gookit/color v1.5.0 // indirect
github.com/hashicorp/go-version v1.4.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hdevalence/ed25519consensus v0.0.0-20220222234857-c00d1f31bab3 // indirect
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.12.0 // indirect
github.com/jackc/pgconn v1.12.1 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.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.11.0 // indirect
github.com/jackc/pgx/v4 v4.16.0 // indirect
github.com/jackc/pgx/v4 v4.16.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.4 // indirect
github.com/josharian/native v1.0.0 // indirect
github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.2.0 // indirect
github.com/lithammer/fuzzysearch v1.1.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
@@ -101,49 +106,43 @@ require (
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/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/opencontainers/runc v1.0.2 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/pelletier/go-toml/v2 v2.0.0-beta.8 // indirect
github.com/opencontainers/go-digest v1.0.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.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.1 // 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/puzpuzpuz/xsync v1.2.1 // indirect
github.com/prometheus/procfs v0.8.0 // 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-0.20211023094830-115ce09fd6b4 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/spf13/afero v1.8.2 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/cast v1.5.0 // 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.1.7 // indirect
github.com/subosito/gotenv v1.3.0 // indirect
github.com/x448/float16 v0.8.4 // 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-20220412020605-290c469a71a5 // indirect
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // 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.5.1 // indirect
modernc.org/libc v1.14.12 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
modernc.org/libc v1.16.8 // indirect
modernc.org/mathutil v1.4.1 // indirect
modernc.org/memory v1.0.7 // indirect
modernc.org/sqlite v1.16.0 // indirect
modernc.org/memory v1.1.1 // indirect
modernc.org/sqlite v1.17.3 // indirect
nhooyr.io/websocket v1.8.7 // indirect
)

1202
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@ package headscale
import (
"context"
"fmt"
"strings"
"time"
@@ -158,7 +159,7 @@ func (api headscaleV1APIServer) RegisterMachine(
) (*v1.RegisterMachineResponse, error) {
log.Trace().
Str("namespace", request.GetNamespace()).
Str("machine_key", request.GetKey()).
Str("node_key", request.GetKey()).
Msg("Registering machine")
machine, err := api.h.RegisterMachineFromAuthCallback(
@@ -195,13 +196,11 @@ func (api headscaleV1APIServer) SetTags(
}
for _, tag := range request.GetTags() {
if strings.Index(tag, "tag:") != 0 {
err := validateTag(tag)
if err != nil {
return &v1.SetTagsResponse{
Machine: nil,
}, status.Error(
codes.InvalidArgument,
"Invalid tag detected. Each tag must start with the string 'tag:'",
)
Machine: nil,
}, status.Error(codes.InvalidArgument, err.Error())
}
}
@@ -220,6 +219,19 @@ func (api headscaleV1APIServer) SetTags(
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,

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

@@ -1,5 +1,4 @@
//go:build integration
// +build integration
//go:build integration_cli
package headscale
@@ -40,13 +39,13 @@ 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{
@@ -56,7 +55,7 @@ func (s *IntegrationCLITestSuite) SetupTest() {
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{
@@ -68,21 +67,33 @@ func (s *IntegrationCLITestSuite) SetupTest() {
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,
),
"",
)
}
fmt.Println("Creating headscale container for CLI tests")
if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
s.headscale = *pheadscale
} else {
log.Fatalf("Could not start headscale container: %s", err)
s.FailNow(fmt.Sprintf("Could not start headscale container: %s", err), "")
}
fmt.Println("Created headscale container")
fmt.Println("Created headscale container for CLI tests")
fmt.Println("Waiting for headscale to be ready")
fmt.Println("Waiting for headscale to be ready for CLI tests")
hostEndpoint := fmt.Sprintf("localhost:%s", s.headscale.GetPort("8080/tcp"))
if err := s.pool.Retry(func() error {
url := fmt.Sprintf("http://%s/health", hostEndpoint)
resp, err := http.Get(url)
if err != nil {
fmt.Printf("headscale for CLI test is not ready: %s\n", err)
return err
}
if resp.StatusCode != http.StatusOK {
@@ -97,7 +108,7 @@ func (s *IntegrationCLITestSuite) SetupTest() {
// https://github.com/stretchr/testify/issues/849
return // fmt.Errorf("Could not connect to headscale: %s", err)
}
fmt.Println("headscale container is ready")
fmt.Println("headscale container is ready for CLI tests")
}
func (s *IntegrationCLITestSuite) TearDownTest() {
@@ -620,7 +631,7 @@ func (s *IntegrationCLITestSuite) TestNodeTagCommand() {
var errorOutput errOutput
err = json.Unmarshal([]byte(wrongTagResult), &errorOutput)
assert.Nil(s.T(), err)
assert.Contains(s.T(), errorOutput.Error, "Invalid tag detected")
assert.Contains(s.T(), errorOutput.Error, "tag must start with the string 'tag:'")
// Test list all nodes after added seconds
listAllResult, err := ExecuteCommand(
@@ -1728,6 +1739,8 @@ func (s *IntegrationCLITestSuite) TestLoadConfigFromCommand() {
assert.Nil(s.T(), err)
altConfig, err := os.ReadFile("integration_test/etc/alt-config.dump.gold.yaml")
assert.Nil(s.T(), err)
altEnvConfig, err := os.ReadFile("integration_test/etc/alt-env-config.dump.gold.yaml")
assert.Nil(s.T(), err)
_, err = ExecuteCommand(
&s.headscale,
@@ -1760,4 +1773,40 @@ func (s *IntegrationCLITestSuite) TestLoadConfigFromCommand() {
assert.Nil(s.T(), err)
assert.YAMLEq(s.T(), string(altConfig), string(altDumpConfig))
_, err = ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"dumpConfig",
},
[]string{
"HEADSCALE_CONFIG=/etc/headscale/alt-env-config.yaml",
},
)
assert.Nil(s.T(), err)
altEnvDumpConfig, err := os.ReadFile("integration_test/etc/config.dump.yaml")
assert.Nil(s.T(), err)
assert.YAMLEq(s.T(), string(altEnvConfig), string(altEnvDumpConfig))
_, err = ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"-c",
"/etc/headscale/alt-config.yaml",
"dumpConfig",
},
[]string{
"HEADSCALE_CONFIG=/etc/headscale/alt-env-config.yaml",
},
)
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

@@ -1,31 +1,41 @@
//go:build integration
// +build integration
package headscale
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/netip"
"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 (
headscaleHostname = "headscale-derp"
DOCKER_EXECUTE_TIMEOUT = 10 * time.Second
)
var (
IpPrefix4 = netaddr.MustParseIPPrefix("100.64.0.0/10")
IpPrefix6 = netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/48")
errEnvVarEmpty = errors.New("getenv: environment variable empty")
IpPrefix4 = netip.MustParsePrefix("100.64.0.0/10")
IpPrefix6 = netip.MustParsePrefix("fd7a:115c:a1e0::/48")
tailscaleVersions = []string{
"head",
"unstable",
"1.24.0",
// "head",
// "unstable",
"1.30.0",
"1.28.0",
"1.26.2",
"1.24.2",
"1.22.2",
"1.20.4",
"1.18.2",
@@ -185,8 +195,8 @@ func getDockerBuildOptions(version string) *dockertest.BuildOptions {
func getIPs(
tailscales map[string]dockertest.Resource,
) (map[string][]netaddr.IP, error) {
ips := make(map[string][]netaddr.IP)
) (map[string][]netip.Addr, error) {
ips := make(map[string][]netip.Addr)
for hostname, tailscale := range tailscales {
command := []string{"tailscale", "ip"}
@@ -204,7 +214,7 @@ func getIPs(
if len(address) < 1 {
continue
}
ip, err := netaddr.ParseIP(address)
ip, err := netip.ParseAddr(address)
if err != nil {
return nil, err
}
@@ -218,7 +228,6 @@ func getIPs(
func getDNSNames(
headscale *dockertest.Resource,
) ([]string, error) {
listAllResult, err := ExecuteCommand(
headscale,
[]string{
@@ -252,7 +261,6 @@ func getDNSNames(
func getMagicFQDN(
headscale *dockertest.Resource,
) ([]string, error) {
listAllResult, err := ExecuteCommand(
headscale,
[]string{
@@ -277,8 +285,34 @@ func getMagicFQDN(
hostnames := make([]string, len(listAll))
for index := range listAll {
hostnames[index] = fmt.Sprintf("%s.%s.headscale.net", listAll[index].GetGivenName(), listAll[index].GetNamespace().GetName())
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

@@ -1,4 +1,4 @@
//go:build integration
//go:build integration_derp
package headscale
@@ -8,7 +8,6 @@ import (
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
@@ -28,9 +27,8 @@ import (
)
const (
headscaleHostname = "headscale-derp"
namespaceName = "derpnamespace"
totalContainers = 3
namespaceName = "derpnamespace"
totalContainers = 3
)
type IntegrationDERPTestSuite struct {
@@ -40,41 +38,50 @@ type IntegrationDERPTestSuite struct {
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.
for _, tailscale := range s.tailscales {
if err := s.pool.Purge(&tailscale); err != nil {
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)
}
}
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)
for _, network := range s.networks {
if err := network.Close(); err != nil {
log.Printf("Could not close network: %s\n", err)
}
}
}
}
@@ -83,14 +90,14 @@ func (s *IntegrationDERPTestSuite) 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), "")
}
for i := 0; i < totalContainers; i++ {
if pnetwork, err := s.pool.CreateNetwork(fmt.Sprintf("headscale-derp-%d", i)); err == nil {
s.networks[i] = *pnetwork
} else {
log.Fatalf("Could not create network: %s", err)
s.FailNow(fmt.Sprintf("Could not create network: %s", err), "")
}
}
@@ -101,7 +108,7 @@ func (s *IntegrationDERPTestSuite) SetupSuite() {
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{
@@ -120,15 +127,26 @@ func (s *IntegrationDERPTestSuite) SetupSuite() {
},
}
log.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 for DERP integration tests")
if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
s.headscale = *pheadscale
} else {
log.Fatalf("Could not start headscale container: %s", err)
s.FailNow(fmt.Sprintf("Could not start headscale container: %s", err), "")
}
log.Println("Created headscale container to test DERP")
log.Println("Created headscale container for embedded DERP tests")
log.Println("Creating tailscale containers")
log.Println("Creating tailscale containers for embedded DERP tests")
for i := 0; i < totalContainers; i++ {
version := tailscaleVersions[i%len(tailscaleVersions)]
@@ -140,7 +158,7 @@ func (s *IntegrationDERPTestSuite) SetupSuite() {
s.tailscales[hostname] = *container
}
log.Println("Waiting for headscale to be ready")
log.Println("Waiting for headscale to be ready for embedded DERP tests")
hostEndpoint := fmt.Sprintf("localhost:%s", s.headscale.GetPort("8443/tcp"))
if err := s.pool.Retry(func() error {
@@ -150,6 +168,7 @@ func (s *IntegrationDERPTestSuite) SetupSuite() {
client := &http.Client{Transport: insecureTransport}
resp, err := client.Get(url)
if err != nil {
fmt.Printf("headscale for embedded DERP tests is not ready: %s\n", err)
return err
}
@@ -165,7 +184,7 @@ func (s *IntegrationDERPTestSuite) SetupSuite() {
// https://github.com/stretchr/testify/issues/849
return // fmt.Errorf("Could not connect to headscale: %s", err)
}
log.Println("headscale container is ready")
log.Println("headscale container is ready for embedded DERP tests")
log.Printf("Creating headscale namespace: %s\n", namespaceName)
result, err := ExecuteCommand(
@@ -290,6 +309,23 @@ func (s *IntegrationDERPTestSuite) tailscaleContainer(
}
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(
@@ -331,7 +367,7 @@ func (s *IntegrationDERPTestSuite) saveLog(
log.Printf("Saving logs for %s to %s\n", resource.Container.Name, basePath)
err = ioutil.WriteFile(
err = os.WriteFile(
path.Join(basePath, resource.Container.Name+".stdout.log"),
[]byte(stdout.String()),
0o644,
@@ -340,7 +376,7 @@ func (s *IntegrationDERPTestSuite) saveLog(
return err
}
err = ioutil.WriteFile(
err = os.WriteFile(
path.Join(basePath, resource.Container.Name+".stderr.log"),
[]byte(stdout.String()),
0o644,

View File

@@ -1,5 +1,4 @@
//go:build integration
// +build integration
//go:build integration_general
package headscale
@@ -9,9 +8,9 @@ import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/netip"
"os"
"path"
"strings"
@@ -24,7 +23,6 @@ import (
"github.com/ory/dockertest/v3/docker"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"inet.af/netaddr"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/ipn/ipnstate"
)
@@ -36,6 +34,7 @@ type IntegrationTestSuite struct {
pool dockertest.Pool
network dockertest.Network
headscale dockertest.Resource
saveLogs bool
namespaces map[string]TestNamespace
@@ -43,6 +42,11 @@ type IntegrationTestSuite struct {
}
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{
@@ -55,32 +59,35 @@ func TestIntegrationTestSuite(t *testing.T) {
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)
}
}
}
@@ -116,7 +123,7 @@ func (s *IntegrationTestSuite) saveLog(
log.Printf("Saving logs for %s to %s\n", resource.Container.Name, basePath)
err = ioutil.WriteFile(
err = os.WriteFile(
path.Join(basePath, resource.Container.Name+".stdout.log"),
[]byte(stdout.String()),
0o644,
@@ -125,7 +132,7 @@ func (s *IntegrationTestSuite) saveLog(
return err
}
err = ioutil.WriteFile(
err = os.WriteFile(
path.Join(basePath, resource.Container.Name+".stderr.log"),
[]byte(stdout.String()),
0o644,
@@ -209,13 +216,13 @@ 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{
@@ -225,7 +232,7 @@ func (s *IntegrationTestSuite) SetupSuite() {
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{
@@ -237,15 +244,26 @@ func (s *IntegrationTestSuite) SetupSuite() {
Cmd: []string{"headscale", "serve"},
}
log.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 for core integration tests")
if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
s.headscale = *pheadscale
} else {
log.Fatalf("Could not start headscale container: %s", err)
s.FailNow(fmt.Sprintf("Could not start headscale container for core integration tests: %s", err), "")
}
log.Println("Created headscale container")
log.Println("Created headscale container for core integration tests")
log.Println("Creating tailscale containers")
log.Println("Creating tailscale containers for core integration tests")
for namespace, scales := range s.namespaces {
for i := 0; i < scales.count; i++ {
version := tailscaleVersions[i%len(tailscaleVersions)]
@@ -259,7 +277,7 @@ func (s *IntegrationTestSuite) SetupSuite() {
}
}
log.Println("Waiting for headscale to be ready")
log.Println("Waiting for headscale to be ready for core integration tests")
hostEndpoint := fmt.Sprintf("localhost:%s", s.headscale.GetPort("8080/tcp"))
if err := s.pool.Retry(func() error {
@@ -267,6 +285,7 @@ func (s *IntegrationTestSuite) SetupSuite() {
resp, err := http.Get(url)
if err != nil {
fmt.Printf("headscale for core integration test is not ready: %s\n", err)
return err
}
@@ -282,7 +301,7 @@ func (s *IntegrationTestSuite) SetupSuite() {
// https://github.com/stretchr/testify/issues/849
return // fmt.Errorf("Could not connect to headscale: %s", err)
}
log.Println("headscale container is ready")
log.Println("headscale container is ready for core integration tests")
for namespace, scales := range s.namespaces {
log.Printf("Creating headscale namespace: %s\n", namespace)
@@ -338,6 +357,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(
@@ -441,8 +477,8 @@ func (s *IntegrationTestSuite) TestGetIpAddresses() {
// }
// }
func getIPsfromIPNstate(status ipnstate.Status) []netaddr.IP {
ips := make([]netaddr.IP, 0)
func getIPsfromIPNstate(status ipnstate.Status) []netip.Addr {
ips := make([]netip.Addr, 0)
for _, peer := range status.Peer {
ips = append(ips, peer.TailscaleIPs...)
@@ -526,13 +562,25 @@ func (s *IntegrationTestSuite) TestTailDrop() {
if peername == hostname {
continue
}
var ip4 netip.Addr
for _, ip := range ips[peername] {
if ip.Is4() {
ip4 = ip
break
}
}
if ip4.IsUnspecified() {
panic("no ipv4 address found")
}
s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) {
command := []string{
"tailscale", "file", "cp",
fmt.Sprintf("/tmp/file_from_%s", hostname),
fmt.Sprintf("%s:", ips[peername][1]),
fmt.Sprintf("%s:", ip4),
}
retry(10, 1*time.Second, func() error {
err := retry(10, 1*time.Second, func() error {
log.Printf(
"Sending file from %s to %s\n",
hostname,
@@ -546,6 +594,7 @@ func (s *IntegrationTestSuite) TestTailDrop() {
)
return err
})
assert.Nil(t, err)
})
}
@@ -647,6 +696,18 @@ func (s *IntegrationTestSuite) TestMagicDNS() {
ips, err := getIPs(scales.tailscales)
assert.Nil(s.T(), err)
retry := func(times int, sleepInverval time.Duration, doWork func() (string, error)) (result string, err error) {
for attempts := 0; attempts < times; attempts++ {
result, err = doWork()
if err == nil {
return
}
time.Sleep(sleepInverval)
}
return
}
for hostname, tailscale := range scales.tailscales {
for _, peername := range hostnames {
if strings.Contains(peername, hostname) {
@@ -657,17 +718,20 @@ func (s *IntegrationTestSuite) TestMagicDNS() {
command := []string{
"tailscale", "ip", peername,
}
result, err := retry(10, 1*time.Second, func() (string, error) {
log.Printf(
"Resolving name %s from %s\n",
peername,
hostname,
)
result, err := ExecuteCommand(
&tailscale,
command,
[]string{},
)
return result, err
})
log.Printf(
"Resolving name %s from %s\n",
peername,
hostname,
)
result, err := ExecuteCommand(
&tailscale,
command,
[]string{},
)
assert.Nil(t, err)
log.Printf("Result for %s: %s\n", hostname, result)
@@ -684,8 +748,8 @@ func (s *IntegrationTestSuite) TestMagicDNS() {
func getAPIURLs(
tailscales map[string]dockertest.Resource,
) (map[netaddr.IP]string, error) {
fts := make(map[netaddr.IP]string)
) (map[netip.Addr]string, error) {
fts := make(map[netip.Addr]string)
for _, tailscale := range tailscales {
command := []string{
"curl",
@@ -709,11 +773,11 @@ func getAPIURLs(
for _, ft := range pft {
n := ft.Node
for _, a := range n.Addresses { // just add all the addresses
if _, ok := fts[a.IP()]; !ok {
if _, ok := fts[a.Addr()]; !ok {
if ft.PeerAPIURL == "" {
return nil, errors.New("api url is empty")
}
fts[a.IP()] = ft.PeerAPIURL
fts[a.Addr()] = ft.PeerAPIURL
}
}
}

View File

@@ -18,8 +18,10 @@ dns_config:
domains: []
magic_dns: true
nameservers:
- 127.0.0.11
- 1.1.1.1
ephemeral_node_inactivity_timeout: 30m
node_update_check_interval: 10s
grpc_allow_insecure: false
grpc_listen_addr: :50443
ip_prefixes:
@@ -37,10 +39,12 @@ oidc:
- email
strip_email_domain: true
private_key_path: private.key
noise:
private_key_path: noise_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

@@ -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
@@ -10,9 +11,12 @@ dns_config:
magic_dns: true
domains: []
nameservers:
- 127.0.0.11
- 1.1.1.1
db_path: /tmp/integration_test_db.sqlite3
private_key_path: private.key
noise:
private_key_path: noise_private.key
listen_addr: 0.0.0.0:18080
metrics_listen_addr: 127.0.0.1:19090
server_url: http://headscale:18080

View File

@@ -0,0 +1,49 @@
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: 30s
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
noise:
private_key_path: noise_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,27 @@
log_level: trace
acl_policy_path: ""
db_type: sqlite3
ephemeral_node_inactivity_timeout: 30m
node_update_check_interval: 30s
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
noise:
private_key_path: noise_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

@@ -18,8 +18,10 @@ dns_config:
domains: []
magic_dns: true
nameservers:
- 127.0.0.11
- 1.1.1.1
ephemeral_node_inactivity_timeout: 30m
node_update_check_interval: 10s
grpc_allow_insecure: false
grpc_listen_addr: :50443
ip_prefixes:
@@ -37,10 +39,12 @@ oidc:
- email
strip_email_domain: true
private_key_path: private.key
noise:
private_key_path: noise_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
@@ -10,9 +11,12 @@ dns_config:
magic_dns: true
domains: []
nameservers:
- 127.0.0.11
- 1.1.1.1
db_path: /tmp/integration_test_db.sqlite3
private_key_path: private.key
noise:
private_key_path: noise_private.key
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 127.0.0.1:9090
server_url: http://headscale:8080

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
@@ -13,8 +14,10 @@ dns_config:
- 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
noise:
private_key_path: noise_private.key
listen_addr: 0.0.0.0:443
server_url: https://headscale:443
tls_cert_path: "/etc/headscale/tls/server.crt"
tls_key_path: "/etc/headscale/tls/server.key"
tls_client_auth_mode: disabled

View File

@@ -4,6 +4,7 @@ import (
"database/sql/driver"
"errors"
"fmt"
"net/netip"
"sort"
"strconv"
"strings"
@@ -12,21 +13,22 @@ import (
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/rs/zerolog/log"
"google.golang.org/protobuf/types/known/timestamppb"
"inet.af/netaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
const (
errMachineNotFound = Error("machine not found")
errMachineRouteIsNotAvailable = Error("route is not available on machine")
errMachineAddressesInvalid = Error("failed to parse machine addresses")
errMachineNotFoundRegistrationCache = Error(
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")
ErrCouldNotConvertMachineInterface = Error("failed to convert machine interface")
ErrHostnameTooLong = Error("Hostname too long")
ErrDifferentRegisteredNamespace = Error("machine was previously registered with a different namespace")
MachineGivenNameHashLength = 8
MachineGivenNameTrimSize = 2
)
const (
@@ -81,7 +83,7 @@ type (
MachinesP []*Machine
)
type MachineAddresses []netaddr.IP
type MachineAddresses []netip.Addr
func (ma MachineAddresses) ToStringSlice() []string {
strSlice := make([]string, 0, len(ma))
@@ -101,7 +103,7 @@ func (ma *MachineAddresses) Scan(destination interface{}) error {
if len(addr) < 1 {
continue
}
parsed, err := netaddr.ParseIP(addr)
parsed, err := netip.ParseAddr(addr)
if err != nil {
return err
}
@@ -111,7 +113,7 @@ func (ma *MachineAddresses) Scan(destination interface{}) error {
return nil
default:
return fmt.Errorf("%w: unexpected data type %T", errMachineAddressesInvalid, destination)
return fmt.Errorf("%w: unexpected data type %T", ErrMachineAddressesInvalid, destination)
}
}
@@ -243,8 +245,8 @@ func (h *Headscale) ListPeers(machine *Machine) (Machines, error) {
Msg("Finding direct peers")
machines := Machines{}
if err := h.db.Preload("AuthKey").Preload("AuthKey.Namespace").Preload("Namespace").Where("machine_key <> ?",
machine.MachineKey).Find(&machines).Error; err != nil {
if err := h.db.Preload("AuthKey").Preload("AuthKey.Namespace").Preload("Namespace").Where("node_key <> ?",
machine.NodeKey).Find(&machines).Error; err != nil {
log.Error().Err(err).Msg("Error accessing db")
return Machines{}, err
@@ -336,7 +338,7 @@ func (h *Headscale) GetMachine(namespace string, name string) (*Machine, error)
}
}
return nil, errMachineNotFound
return nil, ErrMachineNotFound
}
// GetMachineByID finds a Machine by ID and returns the Machine struct.
@@ -349,7 +351,7 @@ func (h *Headscale) GetMachineByID(id uint64) (*Machine, error) {
return &m, nil
}
// GetMachineByMachineKey finds a Machine by ID and returns the Machine struct.
// GetMachineByMachineKey finds a Machine by its MachineKey and returns the Machine struct.
func (h *Headscale) GetMachineByMachineKey(
machineKey key.MachinePublic,
) (*Machine, error) {
@@ -361,6 +363,32 @@ func (h *Headscale) GetMachineByMachineKey(
return &m, nil
}
// GetMachineByNodeKey finds a Machine by its current NodeKey.
func (h *Headscale) GetMachineByNodeKey(
nodeKey key.NodePublic,
) (*Machine, error) {
machine := Machine{}
if result := h.db.Preload("Namespace").First(&machine, "node_key = ?",
NodePublicKeyStripPrefix(nodeKey)); result.Error != nil {
return nil, result.Error
}
return &machine, nil
}
// GetMachineByAnyNodeKey finds a Machine by its current NodeKey or the old one, and returns the Machine struct.
func (h *Headscale) GetMachineByAnyNodeKey(
nodeKey key.NodePublic, oldNodeKey key.NodePublic,
) (*Machine, error) {
machine := Machine{}
if result := h.db.Preload("Namespace").First(&machine, "node_key = ? OR node_key = ?",
NodePublicKeyStripPrefix(nodeKey), NodePublicKeyStripPrefix(oldNodeKey)); result.Error != nil {
return nil, result.Error
}
return &machine, nil
}
// UpdateMachineFromDatabase takes a Machine struct pointer (typically already loaded from database
// and updates it with the latest data from the database.
func (h *Headscale) UpdateMachineFromDatabase(machine *Machine) error {
@@ -373,11 +401,17 @@ func (h *Headscale) UpdateMachineFromDatabase(machine *Machine) error {
// SetTags takes a Machine struct pointer and update the forced tags.
func (h *Headscale) SetTags(machine *Machine, tags []string) error {
machine.ForcedTags = tags
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)
h.setLastStateChangeToNow()
if err := h.db.Save(machine).Error; err != nil {
return fmt.Errorf("failed to update tags for machine in the database: %w", err)
@@ -391,7 +425,7 @@ func (h *Headscale) ExpireMachine(machine *Machine) error {
now := time.Now()
machine.Expiry = &now
h.setLastStateChangeToNow(machine.Namespace.Name)
h.setLastStateChangeToNow()
if err := h.db.Save(machine).Error; err != nil {
return fmt.Errorf("failed to expire machine in the database: %w", err)
@@ -418,7 +452,7 @@ func (h *Headscale) RenameMachine(machine *Machine, newName string) error {
}
machine.GivenName = newName
h.setLastStateChangeToNow(machine.Namespace.Name)
h.setLastStateChangeToNow()
if err := h.db.Save(machine).Error; err != nil {
return fmt.Errorf("failed to rename machine in the database: %w", err)
@@ -434,7 +468,7 @@ func (h *Headscale) RefreshMachine(machine *Machine, expiry time.Time) error {
machine.LastSuccessfulUpdate = &now
machine.Expiry = &expiry
h.setLastStateChangeToNow(machine.Namespace.Name)
h.setLastStateChangeToNow()
if err := h.db.Save(machine).Error; err != nil {
return fmt.Errorf(
@@ -567,11 +601,14 @@ func (machine Machine) toNode(
}
var machineKey key.MachinePublic
err = machineKey.UnmarshalText(
[]byte(MachinePublicKeyEnsurePrefix(machine.MachineKey)),
)
if err != nil {
return nil, fmt.Errorf("failed to parse machine public key: %w", err)
// MachineKey is only used in the legacy protocol
if machine.MachineKey != "" {
err = machineKey.UnmarshalText(
[]byte(MachinePublicKeyEnsurePrefix(machine.MachineKey)),
)
if err != nil {
return nil, fmt.Errorf("failed to parse machine public key: %w", err)
}
}
var discoKey key.DiscoPublic
@@ -586,14 +623,14 @@ func (machine Machine) toNode(
discoKey = key.DiscoPublic{}
}
addrs := []netaddr.IPPrefix{}
addrs := []netip.Prefix{}
for _, machineAddress := range machine.IPAddresses {
ip := netaddr.IPPrefixFrom(machineAddress, machineAddress.BitLen())
ip := netip.PrefixFrom(machineAddress, machineAddress.BitLen())
addrs = append(addrs, ip)
}
allowedIPs := append(
[]netaddr.IPPrefix{},
[]netip.Prefix{},
addrs...) // we append the node own IP, as it is required by the clients
// TODO(kradalby): Needs investigation, We probably dont need this condition
@@ -628,7 +665,7 @@ func (machine Machine) toNode(
return nil, fmt.Errorf(
"hostname %q is too long it cannot except 255 ASCII chars: %w",
hostname,
errHostnameTooLong,
ErrHostnameTooLong,
)
}
} else {
@@ -637,6 +674,10 @@ func (machine Machine) toNode(
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(
@@ -653,6 +694,7 @@ func (machine Machine) toNode(
Endpoints: machine.Endpoints,
DERP: derp,
Online: &online,
Hostinfo: hostInfo.View(),
Created: machine.CreatedAt,
LastSeen: machine.LastSeen,
@@ -750,11 +792,11 @@ func getTags(
}
func (h *Headscale) RegisterMachineFromAuthCallback(
machineKeyStr string,
nodeKeyStr string,
namespaceName string,
registrationMethod string,
) (*Machine, error) {
if machineInterface, ok := h.registrationCache.Get(machineKeyStr); ok {
if machineInterface, ok := h.registrationCache.Get(nodeKeyStr); ok {
if registrationMachine, ok := machineInterface.(Machine); ok {
namespace, err := h.GetNamespace(namespaceName)
if err != nil {
@@ -764,6 +806,11 @@ func (h *Headscale) RegisterMachineFromAuthCallback(
)
}
// Registration of expired machine with different namespace
if registrationMachine.ID != 0 && registrationMachine.NamespaceID != namespace.ID {
return nil, ErrDifferentRegisteredNamespace
}
registrationMachine.NamespaceID = namespace.ID
registrationMachine.RegisterMethod = registrationMethod
@@ -771,13 +818,17 @@ func (h *Headscale) RegisterMachineFromAuthCallback(
registrationMachine,
)
if err == nil {
h.registrationCache.Delete(nodeKeyStr)
}
return machine, err
} else {
return nil, errCouldNotConvertMachineInterface
return nil, ErrCouldNotConvertMachineInterface
}
}
return nil, errMachineNotFoundRegistrationCache
return nil, ErrMachineNotFoundRegistrationCache
}
// RegisterMachine is executed from the CLI to register a new Machine using its MachineKey.
@@ -822,16 +873,16 @@ func (h *Headscale) RegisterMachine(machine Machine,
return &machine, nil
}
func (machine *Machine) GetAdvertisedRoutes() []netaddr.IPPrefix {
func (machine *Machine) GetAdvertisedRoutes() []netip.Prefix {
return machine.HostInfo.RoutableIPs
}
func (machine *Machine) GetEnabledRoutes() []netaddr.IPPrefix {
func (machine *Machine) GetEnabledRoutes() []netip.Prefix {
return machine.EnabledRoutes
}
func (machine *Machine) IsRoutesEnabled(routeStr string) bool {
route, err := netaddr.ParseIPPrefix(routeStr)
route, err := netip.ParsePrefix(routeStr)
if err != nil {
return false
}
@@ -850,9 +901,9 @@ func (machine *Machine) IsRoutesEnabled(routeStr string) bool {
// EnableNodeRoute enables new routes based on a list of new routes. It will _replace_ the
// previous list of routes.
func (h *Headscale) EnableRoutes(machine *Machine, routeStrs ...string) error {
newRoutes := make([]netaddr.IPPrefix, len(routeStrs))
newRoutes := make([]netip.Prefix, len(routeStrs))
for index, routeStr := range routeStrs {
route, err := netaddr.ParseIPPrefix(routeStr)
route, err := netip.ParsePrefix(routeStr)
if err != nil {
return err
}
@@ -865,7 +916,7 @@ func (h *Headscale) EnableRoutes(machine *Machine, routeStrs ...string) error {
return fmt.Errorf(
"route (%s) is not available on node %s: %w",
machine.Hostname,
newRoute, errMachineRouteIsNotAvailable,
newRoute, ErrMachineRouteIsNotAvailable,
)
}
}
@@ -893,7 +944,7 @@ func (machine *Machine) RoutesToProto() *v1.Routes {
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 - 2
trimmedHostnameLength := labelHostnameLength - MachineGivenNameHashLength - MachineGivenNameTrimSize
normalizedHostname, err := NormalizeToFQDNRules(
suppliedName,

View File

@@ -2,6 +2,7 @@ package headscale
import (
"fmt"
"net/netip"
"reflect"
"strconv"
"strings"
@@ -9,8 +10,8 @@ import (
"time"
"gopkg.in/check.v1"
"inet.af/netaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
func (s *Suite) TestGetMachine(c *check.C) {
@@ -65,6 +66,63 @@ func (s *Suite) TestGetMachineByID(c *check.C) {
c.Assert(err, check.IsNil)
}
func (s *Suite) TestGetMachineByNodeKey(c *check.C) {
namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
c.Assert(err, check.IsNil)
_, err = app.GetMachineByID(0)
c.Assert(err, check.NotNil)
nodeKey := key.NewNode()
machine := Machine{
ID: 0,
MachineKey: "foo",
NodeKey: NodePublicKeyStripPrefix(nodeKey.Public()),
DiscoKey: "faa",
Hostname: "testmachine",
NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
}
app.db.Save(&machine)
_, err = app.GetMachineByNodeKey(nodeKey.Public())
c.Assert(err, check.IsNil)
}
func (s *Suite) TestGetMachineByAnyNodeKey(c *check.C) {
namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
c.Assert(err, check.IsNil)
_, err = app.GetMachineByID(0)
c.Assert(err, check.NotNil)
nodeKey := key.NewNode()
oldNodeKey := key.NewNode()
machine := Machine{
ID: 0,
MachineKey: "foo",
NodeKey: NodePublicKeyStripPrefix(nodeKey.Public()),
DiscoKey: "faa",
Hostname: "testmachine",
NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
}
app.db.Save(&machine)
_, err = app.GetMachineByAnyNodeKey(nodeKey.Public(), oldNodeKey.Public())
c.Assert(err, check.IsNil)
}
func (s *Suite) TestDeleteMachine(c *check.C) {
namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil)
@@ -171,7 +229,7 @@ func (s *Suite) TestGetACLFilteredPeers(c *check.C) {
NodeKey: "bar" + strconv.Itoa(index),
DiscoKey: "faa" + strconv.Itoa(index),
IPAddresses: MachineAddresses{
netaddr.MustParseIP(fmt.Sprintf("100.64.0.%v", strconv.Itoa(index+1))),
netip.MustParseAddr(fmt.Sprintf("100.64.0.%v", strconv.Itoa(index+1))),
},
Hostname: "testmachine" + strconv.Itoa(index),
NamespaceID: stor[index%2].namespace.ID,
@@ -185,11 +243,19 @@ func (s *Suite) TestGetACLFilteredPeers(c *check.C) {
Groups: map[string][]string{
"group:test": {"admin"},
},
Hosts: map[string]netaddr.IPPrefix{},
Hosts: map[string]netip.Prefix{},
TagOwners: map[string][]string{},
ACLs: []ACL{
{Action: "accept", Sources: []string{"admin"}, Destinations: []string{"*:*"}},
{Action: "accept", Sources: []string{"test"}, Destinations: []string{"test:*"}},
{
Action: "accept",
Sources: []string{"admin"},
Destinations: []string{"*:*"},
},
{
Action: "accept",
Sources: []string{"test"},
Destinations: []string{"test:*"},
},
},
Tests: []ACLTest{},
}
@@ -249,18 +315,20 @@ 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)
}
func (s *Suite) TestSerdeAddressStrignSlice(c *check.C) {
input := MachineAddresses([]netaddr.IP{
netaddr.MustParseIP("192.0.2.1"),
netaddr.MustParseIP("2001:db8::1"),
input := MachineAddresses([]netip.Addr{
netip.MustParseAddr("192.0.2.1"),
netip.MustParseAddr("2001:db8::1"),
})
serialized, err := input.Value()
c.Assert(err, check.IsNil)
@@ -278,6 +346,49 @@ 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
@@ -429,7 +540,6 @@ func Test_getTags(t *testing.T) {
}
}
// nolint
func Test_getFilteredByACLPeers(t *testing.T) {
type args struct {
machines []Machine
@@ -448,21 +558,21 @@ func Test_getFilteredByACLPeers(t *testing.T) {
{
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
netip.MustParseAddr("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
ID: 2,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
netip.MustParseAddr("100.64.0.2"),
},
Namespace: Namespace{Name: "marc"},
},
{
ID: 3,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
netip.MustParseAddr("100.64.0.3"),
},
Namespace: Namespace{Name: "mickael"},
},
@@ -477,19 +587,19 @@ func Test_getFilteredByACLPeers(t *testing.T) {
},
machine: &Machine{ // current machine
ID: 1,
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.1")},
Namespace: Namespace{Name: "joe"},
},
},
want: Machines{
{
ID: 2,
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.2")},
Namespace: Namespace{Name: "marc"},
},
{
ID: 3,
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.3")},
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.3")},
Namespace: Namespace{Name: "mickael"},
},
},
@@ -501,21 +611,21 @@ func Test_getFilteredByACLPeers(t *testing.T) {
{
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
netip.MustParseAddr("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
ID: 2,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
netip.MustParseAddr("100.64.0.2"),
},
Namespace: Namespace{Name: "marc"},
},
{
ID: 3,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
netip.MustParseAddr("100.64.0.3"),
},
Namespace: Namespace{Name: "mickael"},
},
@@ -530,14 +640,14 @@ func Test_getFilteredByACLPeers(t *testing.T) {
},
machine: &Machine{ // current machine
ID: 1,
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.1")},
Namespace: Namespace{Name: "joe"},
},
},
want: Machines{
{
ID: 2,
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.2")},
Namespace: Namespace{Name: "marc"},
},
},
@@ -549,21 +659,21 @@ func Test_getFilteredByACLPeers(t *testing.T) {
{
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
netip.MustParseAddr("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
ID: 2,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
netip.MustParseAddr("100.64.0.2"),
},
Namespace: Namespace{Name: "marc"},
},
{
ID: 3,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
netip.MustParseAddr("100.64.0.3"),
},
Namespace: Namespace{Name: "mickael"},
},
@@ -578,14 +688,14 @@ func Test_getFilteredByACLPeers(t *testing.T) {
},
machine: &Machine{ // current machine
ID: 2,
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.2")},
Namespace: Namespace{Name: "marc"},
},
},
want: Machines{
{
ID: 3,
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.3")},
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.3")},
Namespace: Namespace{Name: "mickael"},
},
},
@@ -597,21 +707,21 @@ func Test_getFilteredByACLPeers(t *testing.T) {
{
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
netip.MustParseAddr("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
ID: 2,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
netip.MustParseAddr("100.64.0.2"),
},
Namespace: Namespace{Name: "marc"},
},
{
ID: 3,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
netip.MustParseAddr("100.64.0.3"),
},
Namespace: Namespace{Name: "mickael"},
},
@@ -627,7 +737,7 @@ func Test_getFilteredByACLPeers(t *testing.T) {
machine: &Machine{ // current machine
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
netip.MustParseAddr("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
@@ -636,7 +746,7 @@ func Test_getFilteredByACLPeers(t *testing.T) {
{
ID: 2,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
netip.MustParseAddr("100.64.0.2"),
},
Namespace: Namespace{Name: "marc"},
},
@@ -649,21 +759,21 @@ func Test_getFilteredByACLPeers(t *testing.T) {
{
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
netip.MustParseAddr("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
ID: 2,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
netip.MustParseAddr("100.64.0.2"),
},
Namespace: Namespace{Name: "marc"},
},
{
ID: 3,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
netip.MustParseAddr("100.64.0.3"),
},
Namespace: Namespace{Name: "mickael"},
},
@@ -679,7 +789,7 @@ func Test_getFilteredByACLPeers(t *testing.T) {
machine: &Machine{ // current machine
ID: 2,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
netip.MustParseAddr("100.64.0.2"),
},
Namespace: Namespace{Name: "marc"},
},
@@ -688,14 +798,14 @@ func Test_getFilteredByACLPeers(t *testing.T) {
{
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
netip.MustParseAddr("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
ID: 3,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
netip.MustParseAddr("100.64.0.3"),
},
Namespace: Namespace{Name: "mickael"},
},
@@ -708,21 +818,21 @@ func Test_getFilteredByACLPeers(t *testing.T) {
{
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
netip.MustParseAddr("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
ID: 2,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
netip.MustParseAddr("100.64.0.2"),
},
Namespace: Namespace{Name: "marc"},
},
{
ID: 3,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
netip.MustParseAddr("100.64.0.3"),
},
Namespace: Namespace{Name: "mickael"},
},
@@ -737,7 +847,7 @@ func Test_getFilteredByACLPeers(t *testing.T) {
},
machine: &Machine{ // current machine
ID: 2,
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.2")},
Namespace: Namespace{Name: "marc"},
},
},
@@ -745,13 +855,13 @@ func Test_getFilteredByACLPeers(t *testing.T) {
{
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
netip.MustParseAddr("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
ID: 3,
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.3")},
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.3")},
Namespace: Namespace{Name: "mickael"},
},
},
@@ -763,21 +873,21 @@ func Test_getFilteredByACLPeers(t *testing.T) {
{
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
netip.MustParseAddr("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
ID: 2,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
netip.MustParseAddr("100.64.0.2"),
},
Namespace: Namespace{Name: "marc"},
},
{
ID: 3,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
netip.MustParseAddr("100.64.0.3"),
},
Namespace: Namespace{Name: "mickael"},
},
@@ -786,7 +896,7 @@ func Test_getFilteredByACLPeers(t *testing.T) {
},
machine: &Machine{ // current machine
ID: 2,
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.2")},
Namespace: Namespace{Name: "marc"},
},
},
@@ -918,6 +1028,7 @@ func TestHeadscale_GenerateGivenName(t *testing.T) {
err,
tt.wantErr,
)
return
}

View File

@@ -16,10 +16,10 @@ import (
)
const (
errNamespaceExists = Error("Namespace already exists")
errNamespaceNotFound = Error("Namespace not found")
errNamespaceNotEmptyOfNodes = Error("Namespace not empty: node(s) found")
errInvalidNamespaceName = Error("Invalid namespace name")
ErrNamespaceExists = Error("Namespace already exists")
ErrNamespaceNotFound = Error("Namespace not found")
ErrNamespaceNotEmptyOfNodes = Error("Namespace not empty: node(s) found")
ErrInvalidNamespaceName = Error("Invalid namespace name")
)
const (
@@ -47,7 +47,7 @@ func (h *Headscale) CreateNamespace(name string) (*Namespace, error) {
}
namespace := Namespace{}
if err := h.db.Where("name = ?", name).First(&namespace).Error; err == nil {
return nil, errNamespaceExists
return nil, ErrNamespaceExists
}
namespace.Name = name
if err := h.db.Create(&namespace).Error; err != nil {
@@ -67,7 +67,7 @@ func (h *Headscale) CreateNamespace(name string) (*Namespace, error) {
func (h *Headscale) DestroyNamespace(name string) error {
namespace, err := h.GetNamespace(name)
if err != nil {
return errNamespaceNotFound
return ErrNamespaceNotFound
}
machines, err := h.ListMachinesInNamespace(name)
@@ -75,7 +75,7 @@ func (h *Headscale) DestroyNamespace(name string) error {
return err
}
if len(machines) > 0 {
return errNamespaceNotEmptyOfNodes
return ErrNamespaceNotEmptyOfNodes
}
keys, err := h.ListPreAuthKeys(name)
@@ -110,9 +110,9 @@ func (h *Headscale) RenameNamespace(oldName, newName string) error {
}
_, err = h.GetNamespace(newName)
if err == nil {
return errNamespaceExists
return ErrNamespaceExists
}
if !errors.Is(err, errNamespaceNotFound) {
if !errors.Is(err, ErrNamespaceNotFound) {
return err
}
@@ -132,7 +132,7 @@ func (h *Headscale) GetNamespace(name string) (*Namespace, error) {
result.Error,
gorm.ErrRecordNotFound,
) {
return nil, errNamespaceNotFound
return nil, ErrNamespaceNotFound
}
return &namespace, nil
@@ -272,7 +272,7 @@ func NormalizeToFQDNRules(name string, stripEmailDomain bool) (string, error) {
return "", fmt.Errorf(
"label %v is more than 63 chars: %w",
elt,
errInvalidNamespaceName,
ErrInvalidNamespaceName,
)
}
}
@@ -285,21 +285,21 @@ func CheckForFQDNRules(name string) error {
return fmt.Errorf(
"DNS segment must not be over 63 chars. %v doesn't comply with this rule: %w",
name,
errInvalidNamespaceName,
ErrInvalidNamespaceName,
)
}
if strings.ToLower(name) != name {
return fmt.Errorf(
"DNS segment should be lowercase. %v doesn't comply with this rule: %w",
name,
errInvalidNamespaceName,
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,
ErrInvalidNamespaceName,
)
}

View File

@@ -1,11 +1,11 @@
package headscale
import (
"net/netip"
"testing"
"gopkg.in/check.v1"
"gorm.io/gorm"
"inet.af/netaddr"
)
func (s *Suite) TestCreateAndDestroyNamespace(c *check.C) {
@@ -26,7 +26,7 @@ func (s *Suite) TestCreateAndDestroyNamespace(c *check.C) {
func (s *Suite) TestDestroyNamespaceErrors(c *check.C) {
err := app.DestroyNamespace("test")
c.Assert(err, check.Equals, errNamespaceNotFound)
c.Assert(err, check.Equals, ErrNamespaceNotFound)
namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil)
@@ -60,7 +60,7 @@ func (s *Suite) TestDestroyNamespaceErrors(c *check.C) {
app.db.Save(&machine)
err = app.DestroyNamespace("test")
c.Assert(err, check.Equals, errNamespaceNotEmptyOfNodes)
c.Assert(err, check.Equals, ErrNamespaceNotEmptyOfNodes)
}
func (s *Suite) TestRenameNamespace(c *check.C) {
@@ -76,20 +76,20 @@ func (s *Suite) TestRenameNamespace(c *check.C) {
c.Assert(err, check.IsNil)
_, err = app.GetNamespace("test")
c.Assert(err, check.Equals, errNamespaceNotFound)
c.Assert(err, check.Equals, ErrNamespaceNotFound)
_, err = app.GetNamespace("test-renamed")
c.Assert(err, check.IsNil)
err = app.RenameNamespace("test-does-not-exit", "test")
c.Assert(err, check.Equals, errNamespaceNotFound)
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")
c.Assert(err, check.Equals, errNamespaceExists)
c.Assert(err, check.Equals, ErrNamespaceExists)
}
func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
@@ -146,7 +146,7 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")},
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")},
AuthKeyID: uint(preAuthKeyShared1.ID),
}
app.db.Save(machineInShared1)
@@ -163,7 +163,7 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
NamespaceID: namespaceShared2.ID,
Namespace: *namespaceShared2,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.2")},
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.2")},
AuthKeyID: uint(preAuthKeyShared2.ID),
}
app.db.Save(machineInShared2)
@@ -180,7 +180,7 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
NamespaceID: namespaceShared3.ID,
Namespace: *namespaceShared3,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.3")},
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.3")},
AuthKeyID: uint(preAuthKeyShared3.ID),
}
app.db.Save(machineInShared3)
@@ -197,7 +197,7 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")},
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.4")},
AuthKeyID: uint(preAuthKey2Shared1.ID),
}
app.db.Save(machine2InShared1)
@@ -402,7 +402,7 @@ func (s *Suite) TestSetMachineNamespace(c *check.C) {
c.Assert(machine.Namespace.Name, check.Equals, newNamespace.Name)
err = app.SetMachineNamespace(&machine, "non-existing-namespace")
c.Assert(err, check.Equals, errNamespaceNotFound)
c.Assert(err, check.Equals, ErrNamespaceNotFound)
err = app.SetMachineNamespace(&machine, newNamespace.Name)
c.Assert(err, check.IsNil)

55
noise.go Normal file
View File

@@ -0,0 +1,55 @@
package headscale
import (
"net/http"
"github.com/rs/zerolog/log"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
"tailscale.com/control/controlhttp"
"tailscale.com/net/netutil"
)
const (
// ts2021UpgradePath is the path that the server listens on for the WebSockets upgrade.
ts2021UpgradePath = "/ts2021"
)
// NoiseUpgradeHandler is to upgrade the connection and hijack the net.Conn
// in order to use the Noise-based TS2021 protocol. Listens in /ts2021.
func (h *Headscale) NoiseUpgradeHandler(
writer http.ResponseWriter,
req *http.Request,
) {
log.Trace().Caller().Msgf("Noise upgrade handler for client %s", req.RemoteAddr)
upgrade := req.Header.Get("Upgrade")
if upgrade == "" {
// This probably means that the user is running Headscale behind an
// improperly configured reverse proxy. TS2021 requires WebSockets to
// be passed to Headscale. Let's give them a hint.
log.Warn().
Caller().
Msg("No Upgrade header in TS2021 request. If headscale is behind a reverse proxy, make sure it is configured to pass WebSockets through.")
http.Error(writer, "Internal error", http.StatusInternalServerError)
return
}
noiseConn, err := controlhttp.AcceptHTTP(req.Context(), writer, req, *h.noisePrivateKey)
if err != nil {
log.Error().Err(err).Msg("noise upgrade failed")
http.Error(writer, err.Error(), http.StatusInternalServerError)
return
}
server := http.Server{
ReadTimeout: HTTPReadTimeout,
}
server.Handler = h2c.NewHandler(h.noiseMux, &http2.Server{})
err = server.Serve(netutil.NewOneConnListener(noiseConn, nil))
if err != nil {
log.Info().Err(err).Msg("The HTTP2 server was closed")
}
}

559
oidc.go
View File

@@ -13,7 +13,7 @@ import (
"time"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/gin-gonic/gin"
"github.com/gorilla/mux"
"github.com/rs/zerolog/log"
"golang.org/x/oauth2"
"tailscale.com/types/key"
@@ -21,6 +21,13 @@ import (
const (
randomByteSize = 16
errEmptyOIDCCallbackParams = Error("empty OIDC callback params")
errNoOIDCIDToken = Error("could not extract ID Token for OIDC callback")
errOIDCAllowedDomains = Error("authenticated principal does not match any allowed domain")
errOIDCAllowedUsers = Error("authenticated principal does not match any allowed user")
errOIDCInvalidMachineState = Error("requested machine state key expired before authorisation completed")
errOIDCNodeKeyMissing = Error("could not get node key from cache")
)
type IDTokenClaims struct {
@@ -61,19 +68,26 @@ func (h *Headscale) initOIDC() error {
}
// 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")
// Puts NodeKey in cache so the callback can retrieve it using the oidc state param
// Listens in /oidc/register/:nKey.
func (h *Headscale) RegisterOIDC(
writer http.ResponseWriter,
req *http.Request,
) {
vars := mux.Vars(req)
nodeKeyStr, ok := vars["nkey"]
if !ok || nodeKeyStr == "" {
log.Error().
Caller().
Msg("Missing node key in URL")
http.Error(writer, "Missing node key in URL", http.StatusBadRequest)
return
}
log.Trace().
Caller().
Str("machine_key", machineKeyStr).
Str("node_key", nodeKeyStr).
Msg("Received oidc register call")
randomBlob := make([]byte, randomByteSize)
@@ -81,15 +95,15 @@ 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
}
stateStr := hex.EncodeToString(randomBlob)[:32]
// place the machine key into the state cache, so it can be retrieved later
h.registrationCache.Set(stateStr, machineKeyStr, registerCacheExpiration)
// place the node key into the state cache, so it can be retrieved later
h.registrationCache.Set(stateStr, nodeKeyStr, registerCacheExpiration)
// Add any extra parameter provided in the configuration to the Authorize Endpoint request
extras := make([]oauth2.AuthCodeOption, 0, len(h.cfg.OIDC.ExtraParams))
@@ -101,7 +115,7 @@ func (h *Headscale) RegisterOIDC(ctx *gin.Context) {
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 {
@@ -121,54 +135,26 @@ var oidcCallbackTemplate = template.Must(
)
// OIDCCallback handles the callback from the OIDC endpoint
// Retrieves the mkey from the state cache and adds the machine to the users email namespace
// Retrieves the nkey from the state cache and adds the machine to the users email namespace
// TODO: A confirmation page for new machines should be added to avoid phishing vulnerabilities
// TODO: 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")
if code == "" || state == "" {
ctx.String(http.StatusBadRequest, "Wrong params")
return
}
oauth2Token, err := h.oauth2Config.Exchange(context.Background(), code)
func (h *Headscale) OIDCCallback(
writer http.ResponseWriter,
req *http.Request,
) {
code, state, err := validateOIDCCallbackParams(writer, req)
if err != nil {
log.Error().
Err(err).
Caller().
Msg("Could not exchange code for token")
ctx.String(http.StatusBadRequest, "Could not exchange code for token")
return
}
log.Trace().
Caller().
Str("code", code).
Str("state", state).
Msg("Got oidc callback")
rawIDToken, rawIDTokenOK := oauth2Token.Extra("id_token").(string)
if !rawIDTokenOK {
ctx.String(http.StatusBadRequest, "Could not extract ID Token")
return
}
verifier := h.oidcProvider.Verifier(&oidc.Config{ClientID: h.cfg.OIDC.ClientID})
idToken, err := verifier.Verify(context.Background(), rawIDToken)
rawIDToken, err := h.getIDTokenForOIDCCallback(req.Context(), writer, code, state)
if err != nil {
log.Error().
Err(err).
Caller().
Msg("failed to verify id token")
ctx.String(http.StatusBadRequest, "Failed to verify id token")
return
}
idToken, err := h.verifyIDTokenForOIDCCallback(req.Context(), writer, rawIDToken)
if err != nil {
return
}
@@ -179,84 +165,304 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
// return
// }
// Extract custom claims
claims, err := extractIDTokenClaims(writer, idToken)
if err != nil {
return
}
if err := validateOIDCAllowedDomains(writer, h.cfg.OIDC.AllowedDomains, claims); err != nil {
return
}
if err := validateOIDCAllowedUsers(writer, h.cfg.OIDC.AllowedUsers, claims); err != nil {
return
}
nodeKey, machineExists, err := h.validateMachineForOIDCCallback(writer, state, claims)
if err != nil || machineExists {
return
}
namespaceName, err := getNamespaceName(writer, claims, h.cfg.OIDC.StripEmaildomain)
if err != nil {
return
}
// register the machine if it's new
log.Debug().Msg("Registering new machine after successful callback")
namespace, err := h.findOrCreateNewNamespaceForOIDCCallback(writer, namespaceName)
if err != nil {
return
}
if err := h.registerMachineForOIDCCallback(writer, namespace, nodeKey); err != nil {
return
}
content, err := renderOIDCCallbackTemplate(writer, claims)
if err != nil {
return
}
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
writer.WriteHeader(http.StatusOK)
if _, err := writer.Write(content.Bytes()); err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
}
func validateOIDCCallbackParams(
writer http.ResponseWriter,
req *http.Request,
) (string, string, error) {
code := req.URL.Query().Get("code")
state := req.URL.Query().Get("state")
if code == "" || state == "" {
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 "", "", errEmptyOIDCCallbackParams
}
return code, state, nil
}
func (h *Headscale) getIDTokenForOIDCCallback(
ctx context.Context,
writer http.ResponseWriter,
code, state string,
) (string, error) {
oauth2Token, err := h.oauth2Config.Exchange(ctx, code)
if err != nil {
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)
_, werr := writer.Write([]byte("Could not exchange code for token"))
if werr != nil {
log.Error().
Caller().
Err(werr).
Msg("Failed to write response")
}
return "", err
}
log.Trace().
Caller().
Str("code", code).
Str("state", state).
Msg("Got oidc callback")
rawIDToken, rawIDTokenOK := oauth2Token.Extra("id_token").(string)
if !rawIDTokenOK {
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 "", errNoOIDCIDToken
}
return rawIDToken, nil
}
func (h *Headscale) verifyIDTokenForOIDCCallback(
ctx context.Context,
writer http.ResponseWriter,
rawIDToken string,
) (*oidc.IDToken, error) {
verifier := h.oidcProvider.Verifier(&oidc.Config{ClientID: h.cfg.OIDC.ClientID})
idToken, err := verifier.Verify(ctx, rawIDToken)
if err != nil {
log.Error().
Err(err).
Caller().
Msg("failed to verify id token")
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusBadRequest)
_, werr := writer.Write([]byte("Failed to verify id token"))
if werr != nil {
log.Error().
Caller().
Err(werr).
Msg("Failed to write response")
}
return nil, err
}
return idToken, nil
}
func extractIDTokenClaims(
writer http.ResponseWriter,
idToken *oidc.IDToken,
) (*IDTokenClaims, error) {
var claims IDTokenClaims
if err = idToken.Claims(&claims); err != nil {
if err := idToken.Claims(&claims); err != nil {
log.Error().
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)
_, werr := writer.Write([]byte("Failed to decode id token claims"))
if werr != nil {
log.Error().
Caller().
Err(werr).
Msg("Failed to write response")
}
return
return nil, err
}
// 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")
ctx.String(
http.StatusBadRequest,
"unauthorized principal (domain mismatch)",
)
return &claims, nil
}
return
// validateOIDCAllowedDomains checks that if AllowedDomains is provided,
// that the authenticated principal ends with @<alloweddomain>.
func validateOIDCAllowedDomains(
writer http.ResponseWriter,
allowedDomains []string,
claims *IDTokenClaims,
) error {
if len(allowedDomains) > 0 {
if at := strings.LastIndex(claims.Email, "@"); at < 0 ||
!IsStringInSlice(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 errOIDCAllowedDomains
}
}
// 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")
ctx.String(http.StatusBadRequest, "unauthorized principal (user mismatch)")
return nil
}
return
// validateOIDCAllowedUsers checks that if AllowedUsers is provided,
// that the authenticated principal is part of that list.
func validateOIDCAllowedUsers(
writer http.ResponseWriter,
allowedUsers []string,
claims *IDTokenClaims,
) error {
if len(allowedUsers) > 0 &&
!IsStringInSlice(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 errOIDCAllowedUsers
}
return nil
}
// validateMachine retrieves 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.
func (h *Headscale) validateMachineForOIDCCallback(
writer http.ResponseWriter,
state string,
claims *IDTokenClaims,
) (*key.NodePublic, bool, error) {
// retrieve machinekey from state cache
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
return nil, false, errOIDCInvalidMachineState
}
machineKeyFromCache, machineKeyOK := machineKeyIf.(string)
var machineKey key.MachinePublic
err = machineKey.UnmarshalText(
[]byte(MachinePublicKeyEnsurePrefix(machineKeyFromCache)),
var nodeKey key.NodePublic
nodeKeyFromCache, nodeKeyOK := machineKeyIf.(string)
err := nodeKey.UnmarshalText(
[]byte(NodePublicKeyEnsurePrefix(nodeKeyFromCache)),
)
if err != nil {
log.Error().
Msg("could not parse machine public key")
ctx.String(http.StatusBadRequest, "could not parse public key")
Msg("could not parse node public key")
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusBadRequest)
_, werr := writer.Write([]byte("could not parse public key"))
if werr != nil {
log.Error().
Caller().
Err(werr).
Msg("Failed to write response")
}
return
return nil, false, err
}
if !machineKeyOK {
log.Error().Msg("could not get machine key from cache")
ctx.String(
http.StatusInternalServerError,
"could not get machine key from cache",
)
if !nodeKeyOK {
log.Error().Msg("could not get node key from cache")
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusInternalServerError)
_, err := writer.Write([]byte("could not get node key from cache"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return
return nil, false, errOIDCNodeKeyMissing
}
// 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)
machine, _ := h.GetMachineByNodeKey(nodeKey)
if machine != nil {
log.Trace().
@@ -264,7 +470,20 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
Str("machine", machine.Hostname).
Msg("machine already registered, reauthenticating")
h.RefreshMachine(machine, time.Time{})
err := h.RefreshMachine(machine, time.Time{})
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to refresh machine")
http.Error(
writer,
"Failed to refresh machine",
http.StatusInternalServerError,
)
return nil, true, err
}
var content bytes.Buffer
if err := oidcCallbackTemplate.Execute(&content, oidcCallbackTemplateConfig{
@@ -276,37 +495,69 @@ 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"),
)
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusInternalServerError)
_, werr := writer.Write([]byte("Could not render OIDC callback template"))
if werr != nil {
log.Error().
Caller().
Err(werr).
Msg("Failed to write response")
}
return nil, true, err
}
ctx.Data(http.StatusOK, "text/html; charset=utf-8", content.Bytes())
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, err = writer.Write(content.Bytes())
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return
return nil, true, nil
}
return &nodeKey, false, nil
}
func getNamespaceName(
writer http.ResponseWriter,
claims *IDTokenClaims,
stripEmaildomain bool,
) (string, error) {
namespaceName, err := NormalizeToFQDNRules(
claims.Email,
h.cfg.OIDC.StripEmaildomain,
stripEmaildomain,
)
if err != nil {
log.Error().Err(err).Caller().Msgf("couldn't normalize email")
ctx.String(
http.StatusInternalServerError,
"couldn't normalize email",
)
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusInternalServerError)
_, werr := writer.Write([]byte("couldn't normalize email"))
if werr != nil {
log.Error().
Caller().
Err(werr).
Msg("Failed to write response")
}
return
return "", err
}
// register the machine if it's new
log.Debug().Msg("Registering new machine after successful callback")
return namespaceName, nil
}
func (h *Headscale) findOrCreateNewNamespaceForOIDCCallback(
writer http.ResponseWriter,
namespaceName string,
) (*Namespace, error) {
namespace, err := h.GetNamespace(namespaceName)
if errors.Is(err, errNamespaceNotFound) {
if errors.Is(err, ErrNamespaceNotFound) {
namespace, err = h.CreateNamespace(namespaceName)
if err != nil {
@@ -314,12 +565,17 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
Err(err).
Caller().
Msgf("could not create new namespace '%s'", namespaceName)
ctx.String(
http.StatusInternalServerError,
"could not create new namespace",
)
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusInternalServerError)
_, werr := writer.Write([]byte("could not create namespace"))
if werr != nil {
log.Error().
Caller().
Err(werr).
Msg("Failed to write response")
}
return
return nil, err
}
} else if err != nil {
log.Error().
@@ -327,34 +583,58 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
Err(err).
Str("namespace", namespaceName).
Msg("could not find or create namespace")
ctx.String(
http.StatusInternalServerError,
"could not find or create namespace",
)
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusInternalServerError)
_, werr := writer.Write([]byte("could not find or create namespace"))
if werr != nil {
log.Error().
Caller().
Err(werr).
Msg("Failed to write response")
}
return
return nil, err
}
machineKeyStr := MachinePublicKeyStripPrefix(machineKey)
return namespace, nil
}
_, err = h.RegisterMachineFromAuthCallback(
machineKeyStr,
func (h *Headscale) registerMachineForOIDCCallback(
writer http.ResponseWriter,
namespace *Namespace,
nodeKey *key.NodePublic,
) error {
nodeKeyStr := NodePublicKeyStripPrefix(*nodeKey)
if _, err := h.RegisterMachineFromAuthCallback(
nodeKeyStr,
namespace.Name,
RegisterMethodOIDC,
)
if err != nil {
); err != nil {
log.Error().
Caller().
Err(err).
Msg("could not register machine")
ctx.String(
http.StatusInternalServerError,
"could not register machine",
)
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusInternalServerError)
_, werr := writer.Write([]byte("could not register machine"))
if werr != nil {
log.Error().
Caller().
Err(werr).
Msg("Failed to write response")
}
return
return err
}
return nil
}
func renderOIDCCallbackTemplate(
writer http.ResponseWriter,
claims *IDTokenClaims,
) (*bytes.Buffer, error) {
var content bytes.Buffer
if err := oidcCallbackTemplate.Execute(&content, oidcCallbackTemplateConfig{
User: claims.Email,
@@ -365,12 +645,19 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
Str("type", "authenticate").
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"),
)
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusInternalServerError)
_, werr := writer.Write([]byte("Could not render OIDC callback template"))
if werr != nil {
log.Error().
Caller().
Err(werr).
Msg("Failed to write response")
}
return nil, err
}
ctx.Data(http.StatusOK, "text/html; charset=utf-8", content.Bytes())
return &content, nil
}

View File

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

View File

@@ -14,10 +14,10 @@ import (
)
const (
errPreAuthKeyNotFound = Error("AuthKey not found")
errPreAuthKeyExpired = Error("AuthKey expired")
errSingleUseAuthKeyHasBeenUsed = Error("AuthKey has already been used")
errNamespaceMismatch = Error("namespace mismatch")
ErrPreAuthKeyNotFound = Error("AuthKey not found")
ErrPreAuthKeyExpired = Error("AuthKey expired")
ErrSingleUseAuthKeyHasBeenUsed = Error("AuthKey has already been used")
ErrNamespaceMismatch = Error("namespace mismatch")
)
// PreAuthKey describes a pre-authorization key usable in a particular namespace.
@@ -92,7 +92,7 @@ func (h *Headscale) GetPreAuthKey(namespace string, key string) (*PreAuthKey, er
}
if pak.Namespace.Name != namespace {
return nil, errNamespaceMismatch
return nil, ErrNamespaceMismatch
}
return pak, nil
@@ -135,11 +135,11 @@ func (h *Headscale) checkKeyValidity(k string) (*PreAuthKey, error) {
result.Error,
gorm.ErrRecordNotFound,
) {
return nil, errPreAuthKeyNotFound
return nil, ErrPreAuthKeyNotFound
}
if pak.Expiration != nil && pak.Expiration.Before(time.Now()) {
return nil, errPreAuthKeyExpired
return nil, ErrPreAuthKeyExpired
}
if pak.Reusable || pak.Ephemeral { // we don't need to check if has been used before
@@ -152,7 +152,7 @@ func (h *Headscale) checkKeyValidity(k string) (*PreAuthKey, error) {
}
if len(machines) != 0 || pak.Used {
return nil, errSingleUseAuthKeyHasBeenUsed
return nil, ErrSingleUseAuthKeyHasBeenUsed
}
return &pak, nil

View File

@@ -44,13 +44,13 @@ func (*Suite) TestExpiredPreAuthKey(c *check.C) {
c.Assert(err, check.IsNil)
key, err := app.checkKeyValidity(pak.Key)
c.Assert(err, check.Equals, errPreAuthKeyExpired)
c.Assert(err, check.Equals, ErrPreAuthKeyExpired)
c.Assert(key, check.IsNil)
}
func (*Suite) TestPreAuthKeyDoesNotExist(c *check.C) {
key, err := app.checkKeyValidity("potatoKey")
c.Assert(err, check.Equals, errPreAuthKeyNotFound)
c.Assert(err, check.Equals, ErrPreAuthKeyNotFound)
c.Assert(key, check.IsNil)
}
@@ -86,7 +86,7 @@ func (*Suite) TestAlreadyUsedKey(c *check.C) {
app.db.Save(&machine)
key, err := app.checkKeyValidity(pak.Key)
c.Assert(err, check.Equals, errSingleUseAuthKeyHasBeenUsed)
c.Assert(err, check.Equals, ErrSingleUseAuthKeyHasBeenUsed)
c.Assert(key, check.IsNil)
}
@@ -174,7 +174,7 @@ func (*Suite) TestExpirePreauthKey(c *check.C) {
c.Assert(pak.Expiration, check.NotNil)
key, err := app.checkKeyValidity(pak.Key)
c.Assert(err, check.Equals, errPreAuthKeyExpired)
c.Assert(err, check.Equals, ErrPreAuthKeyExpired)
c.Assert(key, check.IsNil)
}
@@ -188,5 +188,5 @@ func (*Suite) TestNotReusableMarkedAsUsed(c *check.C) {
app.db.Save(&pak)
_, err = app.checkKeyValidity(pak.Key)
c.Assert(err, check.Equals, errSingleUseAuthKeyHasBeenUsed)
c.Assert(err, check.Equals, ErrSingleUseAuthKeyHasBeenUsed)
}

742
protocol_common.go Normal file
View File

@@ -0,0 +1,742 @@
package headscale
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/rs/zerolog/log"
"gorm.io/gorm"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
const (
// The CapabilityVersion is used by Tailscale clients to indicate
// their codebase version. Tailscale clients can communicate over TS2021
// from CapabilityVersion 28, but we only have good support for it
// since https://github.com/tailscale/tailscale/pull/4323 (Noise in any HTTPS port).
//
// Related to this change, there is https://github.com/tailscale/tailscale/pull/5379,
// where CapabilityVersion 39 is introduced to indicate #4323 was merged.
//
// See also https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go
NoiseCapabilityVersion = 39
)
// KeyHandler provides the Headscale pub key
// Listens in /key.
func (h *Headscale) KeyHandler(
writer http.ResponseWriter,
req *http.Request,
) {
// New Tailscale clients send a 'v' parameter to indicate the CurrentCapabilityVersion
clientCapabilityStr := req.URL.Query().Get("v")
if clientCapabilityStr != "" {
log.Debug().
Str("handler", "/key").
Str("v", clientCapabilityStr).
Msg("New noise client")
clientCapabilityVersion, err := strconv.Atoi(clientCapabilityStr)
if err != nil {
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
}
// TS2021 (Tailscale v2 protocol) requires to have a different key
if clientCapabilityVersion >= NoiseCapabilityVersion {
resp := tailcfg.OverTLSPublicKeyResponse{
LegacyPublicKey: h.privateKey.Public(),
PublicKey: h.noisePrivateKey.Public(),
}
writer.Header().Set("Content-Type", "application/json")
writer.WriteHeader(http.StatusOK)
err = json.NewEncoder(writer).Encode(resp)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return
}
}
log.Debug().
Str("handler", "/key").
Msg("New legacy client")
// Old clients don't send a 'v' parameter, so we send the legacy public key
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")
}
}
// handleRegisterCommon is the common logic for registering a client in the legacy and Noise protocols
//
// When using Noise, the machineKey is Zero.
func (h *Headscale) handleRegisterCommon(
writer http.ResponseWriter,
req *http.Request,
registerRequest tailcfg.RegisterRequest,
machineKey key.MachinePublic,
) {
now := time.Now().UTC()
machine, err := h.GetMachineByAnyNodeKey(registerRequest.NodeKey, registerRequest.OldNodeKey)
if errors.Is(err, gorm.ErrRecordNotFound) {
// If the machine has AuthKey set, handle registration via PreAuthKeys
if registerRequest.Auth.AuthKey != "" {
h.handleAuthKeyCommon(writer, registerRequest, machineKey)
return
}
// Check if the node is waiting for interactive login.
//
// TODO(juan): We could use this field to improve our protocol implementation,
// and hold the request until the client closes it, or the interactive
// login is completed (i.e., the user registers the machine).
// This is not implemented yet, as it is no strictly required. The only side-effect
// is that the client will hammer headscale with requests until it gets a
// successful RegisterResponse.
if registerRequest.Followup != "" {
if _, ok := h.registrationCache.Get(NodePublicKeyStripPrefix(registerRequest.NodeKey)); ok {
log.Debug().
Caller().
Str("machine", registerRequest.Hostinfo.Hostname).
Str("node_key", registerRequest.NodeKey.ShortString()).
Str("node_key_old", registerRequest.OldNodeKey.ShortString()).
Str("follow_up", registerRequest.Followup).
Bool("noise", machineKey.IsZero()).
Msg("Machine is waiting for interactive login")
ticker := time.NewTicker(registrationHoldoff)
select {
case <-req.Context().Done():
return
case <-ticker.C:
h.handleNewMachineCommon(writer, registerRequest, machineKey)
return
}
}
}
log.Info().
Caller().
Str("machine", registerRequest.Hostinfo.Hostname).
Str("node_key", registerRequest.NodeKey.ShortString()).
Str("node_key_old", registerRequest.OldNodeKey.ShortString()).
Str("follow_up", registerRequest.Followup).
Bool("noise", machineKey.IsZero()).
Msg("New machine not yet in the database")
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: MachinePublicKeyStripPrefix(machineKey),
Hostname: registerRequest.Hostinfo.Hostname,
GivenName: givenName,
NodeKey: NodePublicKeyStripPrefix(registerRequest.NodeKey),
LastSeen: &now,
Expiry: &time.Time{},
}
if !registerRequest.Expiry.IsZero() {
log.Trace().
Caller().
Bool("noise", machineKey.IsZero()).
Str("machine", registerRequest.Hostinfo.Hostname).
Time("expiry", registerRequest.Expiry).
Msg("Non-zero expiry time requested")
newMachine.Expiry = &registerRequest.Expiry
}
h.registrationCache.Set(
newMachine.NodeKey,
newMachine,
registerCacheExpiration,
)
h.handleNewMachineCommon(writer, registerRequest, machineKey)
return
}
// The machine is already registered, so we need to pass through reauth or key update.
if machine != nil {
// If the NodeKey stored in headscale is the same as the key presented in a registration
// request, then we have a node that is either:
// - Trying to log out (sending a expiry in the past)
// - A valid, registered machine, looking for the node map
// - Expired machine wanting to reauthenticate
if machine.NodeKey == NodePublicKeyStripPrefix(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 !registerRequest.Expiry.IsZero() &&
registerRequest.Expiry.UTC().Before(now) {
h.handleMachineLogOutCommon(writer, *machine, machineKey)
return
}
// If machine is not expired, and is register, we have a already accepted this machine,
// let it proceed with a valid registration
if !machine.isExpired() {
h.handleMachineValidRegistrationCommon(writer, *machine, machineKey)
return
}
}
// The NodeKey we have matches OldNodeKey, which means this is a refresh after a key expiration
if machine.NodeKey == NodePublicKeyStripPrefix(registerRequest.OldNodeKey) &&
!machine.isExpired() {
h.handleMachineRefreshKeyCommon(
writer,
registerRequest,
*machine,
machineKey,
)
return
}
// The machine has expired
h.handleMachineExpiredCommon(writer, registerRequest, *machine, machineKey)
machine.Expiry = &time.Time{}
h.registrationCache.Set(
NodePublicKeyStripPrefix(registerRequest.NodeKey),
*machine,
registerCacheExpiration,
)
return
}
}
// handleAuthKeyCommon contains the logic to manage auth key client registration
// It is used both by the legacy and the new Noise protocol.
// When using Noise, the machineKey is Zero.
//
// TODO: check if any locks are needed around IP allocation.
func (h *Headscale) handleAuthKeyCommon(
writer http.ResponseWriter,
registerRequest tailcfg.RegisterRequest,
machineKey key.MachinePublic,
) {
log.Debug().
Str("func", "handleAuthKeyCommon").
Str("machine", registerRequest.Hostinfo.Hostname).
Bool("noise", machineKey.IsZero()).
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", "handleAuthKeyCommon").
Bool("noise", machineKey.IsZero()).
Str("machine", registerRequest.Hostinfo.Hostname).
Err(err).
Msg("Failed authentication via AuthKey")
resp.MachineAuthorized = false
respBody, err := h.marshalResponse(resp, machineKey)
if err != nil {
log.Error().
Caller().
Str("func", "handleAuthKeyCommon").
Bool("noise", machineKey.IsZero()).
Str("machine", registerRequest.Hostinfo.Hostname).
Err(err).
Msg("Cannot encode message")
http.Error(writer, "Internal server error", http.StatusInternalServerError)
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
Inc()
return
}
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusUnauthorized)
_, err = writer.Write(respBody)
if err != nil {
log.Error().
Caller().
Bool("noise", machineKey.IsZero()).
Err(err).
Msg("Failed to write response")
}
log.Error().
Caller().
Str("func", "handleAuthKeyCommon").
Bool("noise", machineKey.IsZero()).
Str("machine", registerRequest.Hostinfo.Hostname).
Msg("Failed authentication via AuthKey")
if pak != nil {
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
Inc()
} else {
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", "unknown").Inc()
}
return
}
log.Debug().
Str("func", "handleAuthKeyCommon").
Bool("noise", machineKey.IsZero()).
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.GetMachineByAnyNodeKey(registerRequest.NodeKey, registerRequest.OldNodeKey)
if machine != nil {
log.Trace().
Caller().
Bool("noise", machineKey.IsZero()).
Str("machine", machine.Hostname).
Msg("machine was already registered before, refreshing with new auth key")
machine.NodeKey = nodeKey
machine.AuthKeyID = uint(pak.ID)
err := h.RefreshMachine(machine, registerRequest.Expiry)
if err != nil {
log.Error().
Caller().
Bool("noise", machineKey.IsZero()).
Str("machine", machine.Hostname).
Err(err).
Msg("Failed to refresh machine")
return
}
} else {
now := time.Now().UTC()
givenName, err := h.GenerateGivenName(registerRequest.Hostinfo.Hostname)
if err != nil {
log.Error().
Caller().
Bool("noise", machineKey.IsZero()).
Str("func", "RegistrationHandler").
Str("hostinfo.name", registerRequest.Hostinfo.Hostname).
Err(err)
return
}
machineToRegister := Machine{
Hostname: registerRequest.Hostinfo.Hostname,
GivenName: givenName,
NamespaceID: pak.Namespace.ID,
MachineKey: MachinePublicKeyStripPrefix(machineKey),
RegisterMethod: RegisterMethodAuthKey,
Expiry: &registerRequest.Expiry,
NodeKey: nodeKey,
LastSeen: &now,
AuthKeyID: uint(pak.ID),
}
machine, err = h.RegisterMachine(
machineToRegister,
)
if err != nil {
log.Error().
Caller().
Bool("noise", machineKey.IsZero()).
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
}
}
err = h.UsePreAuthKey(pak)
if err != nil {
log.Error().
Caller().
Bool("noise", machineKey.IsZero()).
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()
respBody, err := h.marshalResponse(resp, machineKey)
if err != nil {
log.Error().
Caller().
Bool("noise", machineKey.IsZero()).
Str("func", "handleAuthKeyCommon").
Str("machine", registerRequest.Hostinfo.Hostname).
Err(err).
Msg("Cannot encode message")
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
Inc()
http.Error(writer, "Internal server error", http.StatusInternalServerError)
return
}
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "success", pak.Namespace.Name).
Inc()
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, err = writer.Write(respBody)
if err != nil {
log.Error().
Caller().
Bool("noise", machineKey.IsZero()).
Err(err).
Msg("Failed to write response")
}
log.Info().
Str("func", "handleAuthKeyCommon").
Bool("noise", machineKey.IsZero()).
Str("machine", registerRequest.Hostinfo.Hostname).
Str("ips", strings.Join(machine.IPAddresses.ToStringSlice(), ", ")).
Msg("Successfully authenticated via AuthKey")
}
// handleNewMachineCommon exposes for both legacy and Noise the functionality to get a URL
// for authorizing the machine. This url is then showed to the user by the local Tailscale client.
func (h *Headscale) handleNewMachineCommon(
writer http.ResponseWriter,
registerRequest tailcfg.RegisterRequest,
machineKey key.MachinePublic,
) {
resp := tailcfg.RegisterResponse{}
// The machine registration is new, redirect the client to the registration URL
log.Debug().
Caller().
Bool("noise", machineKey.IsZero()).
Str("machine", registerRequest.Hostinfo.Hostname).
Msg("The node seems to be new, sending auth url")
if h.cfg.OIDC.Issuer != "" {
resp.AuthURL = fmt.Sprintf(
"%s/oidc/register/%s",
strings.TrimSuffix(h.cfg.ServerURL, "/"),
NodePublicKeyStripPrefix(registerRequest.NodeKey),
)
} else {
resp.AuthURL = fmt.Sprintf("%s/register/%s",
strings.TrimSuffix(h.cfg.ServerURL, "/"),
NodePublicKeyStripPrefix(registerRequest.NodeKey))
}
respBody, err := h.marshalResponse(resp, machineKey)
if err != nil {
log.Error().
Caller().
Bool("noise", machineKey.IsZero()).
Err(err).
Msg("Cannot encode message")
http.Error(writer, "Internal server error", http.StatusInternalServerError)
return
}
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, err = writer.Write(respBody)
if err != nil {
log.Error().
Bool("noise", machineKey.IsZero()).
Caller().
Err(err).
Msg("Failed to write response")
}
log.Info().
Caller().
Bool("noise", machineKey.IsZero()).
Str("machine", registerRequest.Hostinfo.Hostname).
Msg("Successfully sent auth url")
}
func (h *Headscale) handleMachineLogOutCommon(
writer http.ResponseWriter,
machine Machine,
machineKey key.MachinePublic,
) {
resp := tailcfg.RegisterResponse{}
log.Info().
Bool("noise", machineKey.IsZero()).
Str("machine", machine.Hostname).
Msg("Client requested logout")
err := h.ExpireMachine(&machine)
if err != nil {
log.Error().
Caller().
Bool("noise", machineKey.IsZero()).
Str("func", "handleMachineLogOutCommon").
Err(err).
Msg("Failed to expire machine")
http.Error(writer, "Internal server error", http.StatusInternalServerError)
return
}
resp.AuthURL = ""
resp.MachineAuthorized = false
resp.User = *machine.Namespace.toUser()
respBody, err := h.marshalResponse(resp, machineKey)
if err != nil {
log.Error().
Caller().
Bool("noise", machineKey.IsZero()).
Err(err).
Msg("Cannot encode message")
http.Error(writer, "Internal server error", http.StatusInternalServerError)
return
}
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, err = writer.Write(respBody)
if err != nil {
log.Error().
Bool("noise", machineKey.IsZero()).
Caller().
Err(err).
Msg("Failed to write response")
}
log.Info().
Caller().
Bool("noise", machineKey.IsZero()).
Str("machine", machine.Hostname).
Msg("Successfully logged out")
}
func (h *Headscale) handleMachineValidRegistrationCommon(
writer http.ResponseWriter,
machine Machine,
machineKey key.MachinePublic,
) {
resp := tailcfg.RegisterResponse{}
// The machine registration is valid, respond with redirect to /map
log.Debug().
Caller().
Bool("noise", machineKey.IsZero()).
Str("machine", machine.Hostname).
Msg("Client is registered and we have the current NodeKey. All clear to /map")
resp.AuthURL = ""
resp.MachineAuthorized = true
resp.User = *machine.Namespace.toUser()
resp.Login = *machine.Namespace.toLogin()
respBody, err := h.marshalResponse(resp, machineKey)
if err != nil {
log.Error().
Caller().
Bool("noise", machineKey.IsZero()).
Err(err).
Msg("Cannot encode message")
machineRegistrations.WithLabelValues("update", "web", "error", machine.Namespace.Name).
Inc()
http.Error(writer, "Internal server error", http.StatusInternalServerError)
return
}
machineRegistrations.WithLabelValues("update", "web", "success", machine.Namespace.Name).
Inc()
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, err = writer.Write(respBody)
if err != nil {
log.Error().
Caller().
Bool("noise", machineKey.IsZero()).
Err(err).
Msg("Failed to write response")
}
log.Info().
Caller().
Bool("noise", machineKey.IsZero()).
Str("machine", machine.Hostname).
Msg("Machine successfully authorized")
}
func (h *Headscale) handleMachineRefreshKeyCommon(
writer http.ResponseWriter,
registerRequest tailcfg.RegisterRequest,
machine Machine,
machineKey key.MachinePublic,
) {
resp := tailcfg.RegisterResponse{}
log.Debug().
Caller().
Bool("noise", machineKey.IsZero()).
Str("machine", machine.Hostname).
Msg("We have the OldNodeKey in the database. This is a key refresh")
machine.NodeKey = NodePublicKeyStripPrefix(registerRequest.NodeKey)
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()
respBody, err := h.marshalResponse(resp, machineKey)
if err != nil {
log.Error().
Caller().
Bool("noise", machineKey.IsZero()).
Err(err).
Msg("Cannot encode message")
http.Error(writer, "Internal server error", http.StatusInternalServerError)
return
}
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, err = writer.Write(respBody)
if err != nil {
log.Error().
Caller().
Bool("noise", machineKey.IsZero()).
Err(err).
Msg("Failed to write response")
}
log.Info().
Caller().
Bool("noise", machineKey.IsZero()).
Str("node_key", registerRequest.NodeKey.ShortString()).
Str("old_node_key", registerRequest.OldNodeKey.ShortString()).
Str("machine", machine.Hostname).
Msg("Machine successfully refreshed")
}
func (h *Headscale) handleMachineExpiredCommon(
writer http.ResponseWriter,
registerRequest tailcfg.RegisterRequest,
machine Machine,
machineKey key.MachinePublic,
) {
resp := tailcfg.RegisterResponse{}
// The client has registered before, but has expired
log.Debug().
Caller().
Bool("noise", machineKey.IsZero()).
Str("machine", machine.Hostname).
Msg("Machine registration has expired. Sending a authurl to register")
if registerRequest.Auth.AuthKey != "" {
h.handleAuthKeyCommon(writer, registerRequest, machineKey)
return
}
if h.cfg.OIDC.Issuer != "" {
resp.AuthURL = fmt.Sprintf("%s/oidc/register/%s",
strings.TrimSuffix(h.cfg.ServerURL, "/"),
NodePublicKeyStripPrefix(registerRequest.NodeKey))
} else {
resp.AuthURL = fmt.Sprintf("%s/register/%s",
strings.TrimSuffix(h.cfg.ServerURL, "/"),
NodePublicKeyStripPrefix(registerRequest.NodeKey))
}
respBody, err := h.marshalResponse(resp, machineKey)
if err != nil {
log.Error().
Caller().
Bool("noise", machineKey.IsZero()).
Err(err).
Msg("Cannot encode message")
machineRegistrations.WithLabelValues("reauth", "web", "error", machine.Namespace.Name).
Inc()
http.Error(writer, "Internal server error", http.StatusInternalServerError)
return
}
machineRegistrations.WithLabelValues("reauth", "web", "success", machine.Namespace.Name).
Inc()
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, err = writer.Write(respBody)
if err != nil {
log.Error().
Caller().
Bool("noise", machineKey.IsZero()).
Err(err).
Msg("Failed to write response")
}
log.Info().
Caller().
Bool("noise", machineKey.IsZero()).
Str("machine", machine.Hostname).
Msg("Auth URL for reauthenticate successfully sent")
}

View File

@@ -2,103 +2,43 @@ package headscale
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"gorm.io/gorm"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
const (
keepAliveInterval = 60 * time.Second
updateCheckInterval = 10 * time.Second
keepAliveInterval = 60 * time.Second
)
type contextKey string
const machineNameContextKey = contextKey("machineName")
// PollNetMapHandler takes care of /machine/:id/map
//
// This is the busiest endpoint, as it keeps the HTTP long poll that updates
// the clients when something in the network changes.
//
// The clients POST stuff like HostInfo and their Endpoints here, but
// only after their first request (marked with the ReadOnly field).
//
// At this moment the updates are sent in a quite horrendous way, but they kinda work.
func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
log.Trace().
Str("handler", "PollNetMap").
Str("id", ctx.Param("id")).
Msg("PollNetMapHandler called")
body, _ := io.ReadAll(ctx.Request.Body)
machineKeyStr := ctx.Param("id")
var machineKey key.MachinePublic
err := machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machineKeyStr)))
if err != nil {
log.Error().
Str("handler", "PollNetMap").
Err(err).
Msg("Cannot parse client key")
ctx.String(http.StatusBadRequest, "")
return
}
req := tailcfg.MapRequest{}
err = decode(body, &req, &machineKey, h.privateKey)
if err != nil {
log.Error().
Str("handler", "PollNetMap").
Err(err).
Msg("Cannot decode message")
ctx.String(http.StatusBadRequest, "")
return
}
machine, err := h.GetMachineByMachineKey(machineKey)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
log.Warn().
Str("handler", "PollNetMap").
Msgf("Ignoring request, cannot find machine with key %s", machineKey.String())
ctx.String(http.StatusUnauthorized, "")
return
}
log.Error().
Str("handler", "PollNetMap").
Msgf("Failed to fetch machine from the database with Machine key: %s", machineKey.String())
ctx.String(http.StatusInternalServerError, "")
return
}
log.Trace().
Str("handler", "PollNetMap").
Str("id", ctx.Param("id")).
Str("machine", machine.Hostname).
Msg("Found machine in database")
machine.Hostname = req.Hostinfo.Hostname
machine.HostInfo = HostInfo(*req.Hostinfo)
machine.DiscoKey = DiscoPublicKeyStripPrefix(req.DiscoKey)
// handlePollCommon is the common code for the legacy and Noise protocols to
// managed the poll loop.
func (h *Headscale) handlePollCommon(
writer http.ResponseWriter,
ctx context.Context,
machine *Machine,
mapRequest tailcfg.MapRequest,
isNoise bool,
) {
machine.Hostname = mapRequest.Hostinfo.Hostname
machine.HostInfo = HostInfo(*mapRequest.Hostinfo)
machine.DiscoKey = DiscoPublicKeyStripPrefix(mapRequest.DiscoKey)
now := time.Now().UTC()
// update ACLRules with peer informations (to update server tags if necessary)
if h.aclPolicy != nil {
err = h.UpdateACLRules()
err := h.UpdateACLRules()
if err != nil {
log.Error().
Caller().
Str("func", "handleAuthKey").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Err(err)
}
@@ -111,8 +51,8 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
//
// The intended use is for clients to discover the DERP map at start-up
// before their first real endpoint update.
if !req.ReadOnly {
machine.Endpoints = req.Endpoints
if !mapRequest.ReadOnly {
machine.Endpoints = mapRequest.Endpoints
machine.LastSeen = &now
}
@@ -120,25 +60,27 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
if err != nil {
log.Error().
Str("handler", "PollNetMap").
Str("id", ctx.Param("id")).
Bool("noise", isNoise).
Str("node_key", machine.NodeKey).
Str("machine", machine.Hostname).
Err(err).
Msg("Failed to persist/update machine in the database")
ctx.String(http.StatusInternalServerError, ":(")
http.Error(writer, "", http.StatusInternalServerError)
return
}
}
data, err := h.getMapResponse(machineKey, req, machine)
mapResp, err := h.getMapResponseData(mapRequest, machine, isNoise)
if err != nil {
log.Error().
Str("handler", "PollNetMap").
Str("id", ctx.Param("id")).
Bool("noise", isNoise).
Str("node_key", machine.NodeKey).
Str("machine", machine.Hostname).
Err(err).
Msg("Failed to get Map response")
ctx.String(http.StatusInternalServerError, ":(")
http.Error(writer, "", http.StatusInternalServerError)
return
}
@@ -150,34 +92,48 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
// Details on the protocol can be found in https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L696
log.Debug().
Str("handler", "PollNetMap").
Str("id", ctx.Param("id")).
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Bool("readOnly", req.ReadOnly).
Bool("omitPeers", req.OmitPeers).
Bool("stream", req.Stream).
Bool("readOnly", mapRequest.ReadOnly).
Bool("omitPeers", mapRequest.OmitPeers).
Bool("stream", mapRequest.Stream).
Msg("Client map request processed")
if req.ReadOnly {
if mapRequest.ReadOnly {
log.Info().
Str("handler", "PollNetMap").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Msg("Client is starting up. Probably interested in a DERP map")
ctx.Data(http.StatusOK, "application/json; charset=utf-8", data)
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, err := writer.Write(mapResp)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
if f, ok := writer.(http.Flusher); ok {
f.Flush()
}
return
}
// There has been an update to _any_ of the nodes that the other nodes would
// need to know about
h.setLastStateChangeToNow(machine.Namespace.Name)
h.setLastStateChangeToNow()
// The request is not ReadOnly, so we need to set up channels for updating
// peers via longpoll
// Only create update channel if it has not been created
log.Trace().
Str("handler", "PollNetMap").
Str("id", ctx.Param("id")).
Caller().
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Msg("Loading or creating update channel")
@@ -189,13 +145,21 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
keepAliveChan := make(chan []byte)
if req.OmitPeers && !req.Stream {
if mapRequest.OmitPeers && !mapRequest.Stream {
log.Info().
Str("handler", "PollNetMap").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Msg("Client sent endpoint update and is ok with a response without peer list")
ctx.Data(http.StatusOK, "application/json; charset=utf-8", data)
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, err := writer.Write(mapResp)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
// It sounds like we should update the nodes when we have received a endpoint update
// even tho the comments in the tailscale code dont explicitly say so.
updateRequestsFromNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "endpoint-update").
@@ -203,111 +167,103 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
updateChan <- struct{}{}
return
} else if req.OmitPeers && req.Stream {
} else if mapRequest.OmitPeers && mapRequest.Stream {
log.Warn().
Str("handler", "PollNetMap").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Msg("Ignoring request, don't know how to handle it")
ctx.String(http.StatusBadRequest, "")
http.Error(writer, "", http.StatusBadRequest)
return
}
log.Info().
Str("handler", "PollNetMap").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Msg("Client is ready to access the tailnet")
log.Info().
Str("handler", "PollNetMap").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Msg("Sending initial map")
pollDataChan <- data
pollDataChan <- mapResp
log.Info().
Str("handler", "PollNetMap").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Msg("Notifying peers")
updateRequestsFromNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "full-update").
Inc()
updateChan <- struct{}{}
h.PollNetMapStream(
h.pollNetMapStream(
writer,
ctx,
machine,
req,
machineKey,
mapRequest,
pollDataChan,
keepAliveChan,
updateChan,
isNoise,
)
log.Trace().
Str("handler", "PollNetMap").
Str("id", ctx.Param("id")).
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Msg("Finished stream, closing PollNetMap session")
}
// PollNetMapStream takes care of /machine/:id/map
// stream logic, ensuring we communicate updates and data
// to the connected clients.
func (h *Headscale) PollNetMapStream(
ctx *gin.Context,
// pollNetMapStream stream logic for /machine/map,
// ensuring we communicate updates and data to the connected clients.
func (h *Headscale) pollNetMapStream(
writer http.ResponseWriter,
ctxReq context.Context,
machine *Machine,
mapRequest tailcfg.MapRequest,
machineKey key.MachinePublic,
pollDataChan chan []byte,
keepAliveChan chan []byte,
updateChan chan struct{},
isNoise bool,
) {
{
machine, err := h.GetMachineByMachineKey(machineKey)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
log.Warn().
Str("handler", "PollNetMap").
Msgf("Ignoring request, cannot find machine with key %s", machineKey.String())
ctx.String(http.StatusUnauthorized, "")
h.pollNetMapStreamWG.Add(1)
defer h.pollNetMapStreamWG.Done()
return
}
log.Error().
Str("handler", "PollNetMap").
Msgf("Failed to fetch machine from the database with Machine key: %s", machineKey.String())
ctx.String(http.StatusInternalServerError, "")
ctx := context.WithValue(ctxReq, machineNameContextKey, machine.Hostname)
return
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
ctx := context.WithValue(ctx.Request.Context(), machineNameContextKey, machine.Hostname)
go h.scheduledPollWorker(
ctx,
updateChan,
keepAliveChan,
mapRequest,
machine,
isNoise,
)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
log.Trace().
Str("handler", "pollNetMapStream").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Msg("Waiting for data to stream...")
go h.scheduledPollWorker(
ctx,
updateChan,
keepAliveChan,
machineKey,
mapRequest,
machine,
)
}
ctx.Stream(func(writer io.Writer) bool {
log.Trace().
Str("handler", "PollNetMapStream").
Str("machine", machine.Hostname).
Msg("Waiting for data to stream...")
log.Trace().
Str("handler", "PollNetMapStream").
Str("machine", machine.Hostname).
Msgf("pollData is %#v, keepAliveChan is %#v, updateChan is %#v", pollDataChan, keepAliveChan, updateChan)
log.Trace().
Str("handler", "pollNetMapStream").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Msgf("pollData is %#v, keepAliveChan is %#v, updateChan is %#v", pollDataChan, keepAliveChan, updateChan)
for {
select {
case data := <-pollDataChan:
log.Trace().
Str("handler", "PollNetMapStream").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Str("channel", "pollData").
Int("bytes", len(data)).
@@ -316,15 +272,31 @@ func (h *Headscale) PollNetMapStream(
if err != nil {
log.Error().
Str("handler", "PollNetMapStream").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Str("channel", "pollData").
Err(err).
Msg("Cannot write data")
return false
return
}
flusher, ok := writer.(http.Flusher)
if !ok {
log.Error().
Caller().
Str("handler", "PollNetMapStream").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Str("channel", "pollData").
Msg("Cannot cast writer to http.Flusher")
} else {
flusher.Flush()
}
log.Trace().
Str("handler", "PollNetMapStream").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Str("channel", "pollData").
Int("bytes", len(data)).
@@ -336,6 +308,7 @@ func (h *Headscale) PollNetMapStream(
if err != nil {
log.Error().
Str("handler", "PollNetMapStream").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Str("channel", "pollData").
Err(err).
@@ -343,7 +316,7 @@ func (h *Headscale) PollNetMapStream(
// client has been removed from database
// since the stream opened, terminate connection.
return false
return
}
now := time.Now().UTC()
machine.LastSeen = &now
@@ -356,20 +329,22 @@ func (h *Headscale) PollNetMapStream(
if err != nil {
log.Error().
Str("handler", "PollNetMapStream").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Str("channel", "pollData").
Err(err).
Msg("Cannot update machine LastSuccessfulUpdate")
} else {
log.Trace().
Str("handler", "PollNetMapStream").
Str("machine", machine.Hostname).
Str("channel", "pollData").
Int("bytes", len(data)).
Msg("Machine entry in database updated successfully after sending pollData")
return
}
return true
log.Trace().
Str("handler", "PollNetMapStream").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Str("channel", "pollData").
Int("bytes", len(data)).
Msg("Machine entry in database updated successfully after sending data")
case data := <-keepAliveChan:
log.Trace().
@@ -382,15 +357,30 @@ func (h *Headscale) PollNetMapStream(
if err != nil {
log.Error().
Str("handler", "PollNetMapStream").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Str("channel", "keepAlive").
Err(err).
Msg("Cannot write keep alive message")
return false
return
}
flusher, ok := writer.(http.Flusher)
if !ok {
log.Error().
Caller().
Str("handler", "PollNetMapStream").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Str("channel", "keepAlive").
Msg("Cannot cast writer to http.Flusher")
} else {
flusher.Flush()
}
log.Trace().
Str("handler", "PollNetMapStream").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Str("channel", "keepAlive").
Int("bytes", len(data)).
@@ -402,6 +392,7 @@ func (h *Headscale) PollNetMapStream(
if err != nil {
log.Error().
Str("handler", "PollNetMapStream").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Str("channel", "keepAlive").
Err(err).
@@ -409,7 +400,7 @@ func (h *Headscale) PollNetMapStream(
// client has been removed from database
// since the stream opened, terminate connection.
return false
return
}
now := time.Now().UTC()
machine.LastSeen = &now
@@ -417,29 +408,33 @@ func (h *Headscale) PollNetMapStream(
if err != nil {
log.Error().
Str("handler", "PollNetMapStream").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Str("channel", "keepAlive").
Err(err).
Msg("Cannot update machine LastSeen")
} else {
log.Trace().
Str("handler", "PollNetMapStream").
Str("machine", machine.Hostname).
Str("channel", "keepAlive").
Int("bytes", len(data)).
Msg("Machine updated successfully after sending keep alive")
return
}
return true
log.Trace().
Str("handler", "PollNetMapStream").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Str("channel", "keepAlive").
Int("bytes", len(data)).
Msg("Machine updated successfully after sending keep alive")
case <-updateChan:
log.Trace().
Str("handler", "PollNetMapStream").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Str("channel", "update").
Msg("Received a request for update")
updateRequestsReceivedOnChannel.WithLabelValues(machine.Namespace.Name, machine.Hostname).
Inc()
if h.isOutdated(machine) {
var lastUpdate time.Time
if machine.LastSuccessfulUpdate != nil {
@@ -447,23 +442,28 @@ func (h *Headscale) PollNetMapStream(
}
log.Debug().
Str("handler", "PollNetMapStream").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Time("last_successful_update", lastUpdate).
Time("last_state_change", h.getLastStateChange(machine.Namespace.Name)).
Msgf("There has been updates since the last successful update to %s", machine.Hostname)
data, err := h.getMapResponse(machineKey, mapRequest, machine)
data, err := h.getMapResponseData(mapRequest, machine, false)
if err != nil {
log.Error().
Str("handler", "PollNetMapStream").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Str("channel", "update").
Err(err).
Msg("Could not get the map update")
return
}
_, err = writer.Write(data)
if err != nil {
log.Error().
Str("handler", "PollNetMapStream").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Str("channel", "update").
Err(err).
@@ -471,10 +471,25 @@ func (h *Headscale) PollNetMapStream(
updateRequestsSentToNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "failed").
Inc()
return false
return
}
flusher, ok := writer.(http.Flusher)
if !ok {
log.Error().
Caller().
Str("handler", "PollNetMapStream").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Str("channel", "update").
Msg("Cannot cast writer to http.Flusher")
} else {
flusher.Flush()
}
log.Trace().
Str("handler", "PollNetMapStream").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Str("channel", "update").
Msg("Updated Map has been sent")
@@ -492,6 +507,7 @@ func (h *Headscale) PollNetMapStream(
if err != nil {
log.Error().
Str("handler", "PollNetMapStream").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Str("channel", "update").
Err(err).
@@ -499,7 +515,7 @@ func (h *Headscale) PollNetMapStream(
// client has been removed from database
// since the stream opened, terminate connection.
return false
return
}
now := time.Now().UTC()
@@ -511,10 +527,13 @@ func (h *Headscale) PollNetMapStream(
if err != nil {
log.Error().
Str("handler", "PollNetMapStream").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Str("channel", "update").
Err(err).
Msg("Cannot update machine LastSuccessfulUpdate")
return
}
} else {
var lastUpdate time.Time
@@ -523,15 +542,14 @@ func (h *Headscale) PollNetMapStream(
}
log.Trace().
Str("handler", "PollNetMapStream").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Time("last_successful_update", lastUpdate).
Time("last_state_change", h.getLastStateChange(machine.Namespace.Name)).
Msgf("%s is up to date", machine.Hostname)
}
return true
case <-ctx.Request.Context().Done():
case <-ctx.Done():
log.Info().
Str("handler", "PollNetMapStream").
Str("machine", machine.Hostname).
@@ -543,6 +561,7 @@ func (h *Headscale) PollNetMapStream(
if err != nil {
log.Error().
Str("handler", "PollNetMapStream").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Str("channel", "Done").
Err(err).
@@ -550,7 +569,7 @@ func (h *Headscale) PollNetMapStream(
// client has been removed from database
// since the stream opened, terminate connection.
return false
return
}
now := time.Now().UTC()
machine.LastSeen = &now
@@ -558,27 +577,38 @@ func (h *Headscale) PollNetMapStream(
if err != nil {
log.Error().
Str("handler", "PollNetMapStream").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Str("channel", "Done").
Err(err).
Msg("Cannot update machine LastSeen")
}
return false
// The connection has been closed, so we can stop polling.
return
case <-h.shutdownChan:
log.Info().
Str("handler", "PollNetMapStream").
Bool("noise", isNoise).
Str("machine", machine.Hostname).
Msg("The long-poll handler is shutting down")
return
}
})
}
}
func (h *Headscale) scheduledPollWorker(
ctx context.Context,
updateChan chan struct{},
keepAliveChan chan []byte,
machineKey key.MachinePublic,
mapRequest tailcfg.MapRequest,
machine *Machine,
isNoise bool,
) {
keepAliveTicker := time.NewTicker(keepAliveInterval)
updateCheckerTicker := time.NewTicker(updateCheckInterval)
updateCheckerTicker := time.NewTicker(h.cfg.NodeUpdateCheckInterval)
defer closeChanWithLog(
updateChan,
@@ -597,10 +627,11 @@ func (h *Headscale) scheduledPollWorker(
return
case <-keepAliveTicker.C:
data, err := h.getMapKeepAliveResponse(machineKey, mapRequest)
data, err := h.getMapKeepAliveResponseData(mapRequest, machine, isNoise)
if err != nil {
log.Error().
Str("func", "keepAlive").
Bool("noise", isNoise).
Err(err).
Msg("Error generating the keep alive msg")
@@ -610,6 +641,7 @@ func (h *Headscale) scheduledPollWorker(
log.Debug().
Str("func", "keepAlive").
Str("machine", machine.Hostname).
Bool("noise", isNoise).
Msg("Sending keepalive")
keepAliveChan <- data
@@ -617,6 +649,7 @@ func (h *Headscale) scheduledPollWorker(
log.Debug().
Str("func", "scheduledPollWorker").
Str("machine", machine.Hostname).
Bool("noise", isNoise).
Msg("Sending update request")
updateRequestsFromNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "scheduled-update").
Inc()

122
protocol_common_utils.go Normal file
View File

@@ -0,0 +1,122 @@
package headscale
import (
"encoding/binary"
"encoding/json"
"github.com/klauspost/compress/zstd"
"github.com/rs/zerolog/log"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
func (h *Headscale) getMapResponseData(
mapRequest tailcfg.MapRequest,
machine *Machine,
isNoise bool,
) ([]byte, error) {
mapResponse, err := h.generateMapResponse(mapRequest, machine)
if err != nil {
return nil, err
}
if isNoise {
return h.marshalMapResponse(mapResponse, key.MachinePublic{}, mapRequest.Compress)
}
var machineKey key.MachinePublic
err = machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machine.MachineKey)))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot parse client key")
return nil, err
}
return h.marshalMapResponse(mapResponse, machineKey, mapRequest.Compress)
}
func (h *Headscale) getMapKeepAliveResponseData(
mapRequest tailcfg.MapRequest,
machine *Machine,
isNoise bool,
) ([]byte, error) {
keepAliveResponse := tailcfg.MapResponse{
KeepAlive: true,
}
if isNoise {
return h.marshalMapResponse(keepAliveResponse, key.MachinePublic{}, mapRequest.Compress)
}
var machineKey key.MachinePublic
err := machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machine.MachineKey)))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot parse client key")
return nil, err
}
return h.marshalMapResponse(keepAliveResponse, machineKey, mapRequest.Compress)
}
func (h *Headscale) marshalResponse(
resp interface{},
machineKey key.MachinePublic,
) ([]byte, error) {
jsonBody, err := json.Marshal(resp)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot marshal response")
return nil, err
}
if machineKey.IsZero() { // if Noise
return jsonBody, nil
}
return h.privateKey.SealTo(machineKey, jsonBody), nil
}
func (h *Headscale) marshalMapResponse(
resp interface{},
machineKey key.MachinePublic,
compression string,
) ([]byte, error) {
jsonBody, err := json.Marshal(resp)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot marshal map response")
}
var respBody []byte
if compression == ZstdCompression {
encoder, _ := zstd.NewWriter(nil)
respBody = encoder.EncodeAll(jsonBody, nil)
if !machineKey.IsZero() { // if legacy protocol
respBody = h.privateKey.SealTo(machineKey, respBody)
}
} else {
if !machineKey.IsZero() { // if legacy protocol
respBody = h.privateKey.SealTo(machineKey, jsonBody)
} else {
respBody = jsonBody
}
}
data := make([]byte, reservedResponseHeaderSize)
binary.LittleEndian.PutUint32(data, uint32(len(respBody)))
data = append(data, respBody...)
return data, nil
}

58
protocol_legacy.go Normal file
View File

@@ -0,0 +1,58 @@
package headscale
import (
"io"
"net/http"
"github.com/gorilla/mux"
"github.com/rs/zerolog/log"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
// RegistrationHandler handles the actual registration process of a machine
// 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)))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot parse machine key")
machineRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc()
http.Error(writer, "Cannot parse machine key", http.StatusBadRequest)
return
}
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()
http.Error(writer, "Cannot decode message", http.StatusBadRequest)
return
}
h.handleRegisterCommon(writer, req, registerRequest, machineKey)
}

94
protocol_legacy_poll.go Normal file
View File

@@ -0,0 +1,94 @@
package headscale
import (
"errors"
"io"
"net/http"
"github.com/gorilla/mux"
"github.com/rs/zerolog/log"
"gorm.io/gorm"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
// PollNetMapHandler takes care of /machine/:id/map
//
// This is the busiest endpoint, as it keeps the HTTP long poll that updates
// the clients when something in the network changes.
//
// The clients POST stuff like HostInfo and their Endpoints here, but
// only after their first request (marked with the ReadOnly field).
//
// At this moment the updates are sent in a quite horrendous way, but they kinda work.
func (h *Headscale) PollNetMapHandler(
writer http.ResponseWriter,
req *http.Request,
) {
vars := mux.Vars(req)
machineKeyStr, ok := vars["mkey"]
if !ok || machineKeyStr == "" {
log.Error().
Str("handler", "PollNetMap").
Msg("No machine key in request")
http.Error(writer, "No machine key in request", http.StatusBadRequest)
return
}
log.Trace().
Str("handler", "PollNetMap").
Str("id", machineKeyStr).
Msg("PollNetMapHandler called")
body, _ := io.ReadAll(req.Body)
var machineKey key.MachinePublic
err := machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machineKeyStr)))
if err != nil {
log.Error().
Str("handler", "PollNetMap").
Err(err).
Msg("Cannot parse client key")
http.Error(writer, "Cannot parse client key", http.StatusBadRequest)
return
}
mapRequest := tailcfg.MapRequest{}
err = decode(body, &mapRequest, &machineKey, h.privateKey)
if err != nil {
log.Error().
Str("handler", "PollNetMap").
Err(err).
Msg("Cannot decode message")
http.Error(writer, "Cannot decode message", http.StatusBadRequest)
return
}
machine, err := h.GetMachineByMachineKey(machineKey)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
log.Warn().
Str("handler", "PollNetMap").
Msgf("Ignoring request, cannot find machine with key %s", machineKey.String())
http.Error(writer, "", http.StatusUnauthorized)
return
}
log.Error().
Str("handler", "PollNetMap").
Msgf("Failed to fetch machine from the database with Machine key: %s", machineKey.String())
http.Error(writer, "", http.StatusInternalServerError)
return
}
log.Trace().
Str("handler", "PollNetMap").
Str("id", machineKeyStr).
Str("machine", machine.Hostname).
Msg("A machine is entering polling via the legacy protocol")
h.handlePollCommon(writer, req.Context(), machine, mapRequest, false)
}

38
protocol_noise.go Normal file
View File

@@ -0,0 +1,38 @@
package headscale
import (
"encoding/json"
"io"
"net/http"
"github.com/rs/zerolog/log"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
// // NoiseRegistrationHandler handles the actual registration process of a machine.
func (h *Headscale) NoiseRegistrationHandler(
writer http.ResponseWriter,
req *http.Request,
) {
log.Trace().Caller().Msgf("Noise registration handler for client %s", req.RemoteAddr)
if req.Method != http.MethodPost {
http.Error(writer, "Wrong method", http.StatusMethodNotAllowed)
return
}
body, _ := io.ReadAll(req.Body)
registerRequest := tailcfg.RegisterRequest{}
if err := json.Unmarshal(body, &registerRequest); err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot parse RegisterRequest")
machineRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc()
http.Error(writer, "Internal error", http.StatusInternalServerError)
return
}
h.handleRegisterCommon(writer, req, registerRequest, key.MachinePublic{})
}

67
protocol_noise_poll.go Normal file
View File

@@ -0,0 +1,67 @@
package headscale
import (
"encoding/json"
"errors"
"io"
"net/http"
"github.com/rs/zerolog/log"
"gorm.io/gorm"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
// NoisePollNetMapHandler takes care of /machine/:id/map using the Noise protocol
//
// This is the busiest endpoint, as it keeps the HTTP long poll that updates
// the clients when something in the network changes.
//
// The clients POST stuff like HostInfo and their Endpoints here, but
// only after their first request (marked with the ReadOnly field).
//
// At this moment the updates are sent in a quite horrendous way, but they kinda work.
func (h *Headscale) NoisePollNetMapHandler(
writer http.ResponseWriter,
req *http.Request,
) {
log.Trace().
Str("handler", "NoisePollNetMap").
Msg("PollNetMapHandler called")
body, _ := io.ReadAll(req.Body)
mapRequest := tailcfg.MapRequest{}
if err := json.Unmarshal(body, &mapRequest); err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot parse MapRequest")
http.Error(writer, "Internal error", http.StatusInternalServerError)
return
}
machine, err := h.GetMachineByAnyNodeKey(mapRequest.NodeKey, key.NodePublic{})
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
log.Warn().
Str("handler", "NoisePollNetMap").
Msgf("Ignoring request, cannot find machine with key %s", mapRequest.NodeKey.String())
http.Error(writer, "Internal error", http.StatusNotFound)
return
}
log.Error().
Str("handler", "NoisePollNetMap").
Msgf("Failed to fetch machine from the database with node key: %s", mapRequest.NodeKey.String())
http.Error(writer, "Internal error", http.StatusInternalServerError)
return
}
log.Debug().
Str("handler", "NoisePollNetMap").
Str("machine", machine.Hostname).
Msg("A machine is entering polling via the Noise protocol")
h.handlePollCommon(writer, req.Context(), machine, mapRequest, true)
}

View File

@@ -2,12 +2,11 @@ package headscale
import (
"fmt"
"inet.af/netaddr"
"net/netip"
)
const (
errRouteIsNotAvailable = Error("route is not available")
ErrRouteIsNotAvailable = Error("route is not available")
)
// Deprecated: use machine function instead
@@ -16,7 +15,7 @@ const (
func (h *Headscale) GetAdvertisedNodeRoutes(
namespace string,
nodeName string,
) (*[]netaddr.IPPrefix, error) {
) (*[]netip.Prefix, error) {
machine, err := h.GetMachine(namespace, nodeName)
if err != nil {
return nil, err
@@ -31,7 +30,7 @@ func (h *Headscale) GetAdvertisedNodeRoutes(
func (h *Headscale) GetEnabledNodeRoutes(
namespace string,
nodeName string,
) ([]netaddr.IPPrefix, error) {
) ([]netip.Prefix, error) {
machine, err := h.GetMachine(namespace, nodeName)
if err != nil {
return nil, err
@@ -47,7 +46,7 @@ func (h *Headscale) IsNodeRouteEnabled(
nodeName string,
routeStr string,
) bool {
route, err := netaddr.ParseIPPrefix(routeStr)
route, err := netip.ParsePrefix(routeStr)
if err != nil {
return false
}
@@ -79,7 +78,7 @@ func (h *Headscale) EnableNodeRoute(
return err
}
route, err := netaddr.ParseIPPrefix(routeStr)
route, err := netip.ParsePrefix(routeStr)
if err != nil {
return err
}
@@ -106,7 +105,7 @@ func (h *Headscale) EnableNodeRoute(
}
if !available {
return errRouteIsNotAvailable
return ErrRouteIsNotAvailable
}
machine.EnabledRoutes = enabledRoutes

View File

@@ -1,8 +1,9 @@
package headscale
import (
"net/netip"
"gopkg.in/check.v1"
"inet.af/netaddr"
"tailscale.com/tailcfg"
)
@@ -16,11 +17,11 @@ func (s *Suite) TestGetRoutes(c *check.C) {
_, err = app.GetMachine("test", "test_get_route_machine")
c.Assert(err, check.NotNil)
route, err := netaddr.ParseIPPrefix("10.0.0.0/24")
route, err := netip.ParsePrefix("10.0.0.0/24")
c.Assert(err, check.IsNil)
hostInfo := tailcfg.Hostinfo{
RoutableIPs: []netaddr.IPPrefix{route},
RoutableIPs: []netip.Prefix{route},
}
machine := Machine{
@@ -28,7 +29,7 @@ func (s *Suite) TestGetRoutes(c *check.C) {
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Hostname: "test_get_route_machine",
Hostname: "test_get_route_machine",
NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
@@ -60,18 +61,18 @@ func (s *Suite) TestGetEnableRoutes(c *check.C) {
_, err = app.GetMachine("test", "test_enable_route_machine")
c.Assert(err, check.NotNil)
route, err := netaddr.ParseIPPrefix(
route, err := netip.ParsePrefix(
"10.0.0.0/24",
)
c.Assert(err, check.IsNil)
route2, err := netaddr.ParseIPPrefix(
route2, err := netip.ParsePrefix(
"150.0.10.0/25",
)
c.Assert(err, check.IsNil)
hostInfo := tailcfg.Hostinfo{
RoutableIPs: []netaddr.IPPrefix{route, route2},
RoutableIPs: []netip.Prefix{route, route2},
}
machine := Machine{
@@ -79,7 +80,7 @@ func (s *Suite) TestGetEnableRoutes(c *check.C) {
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Hostname: "test_enable_route_machine",
Hostname: "test_enable_route_machine",
NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),

View File

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

View File

@@ -13,6 +13,7 @@ import (
"fmt"
"io/fs"
"net"
"net/netip"
"os"
"path/filepath"
"reflect"
@@ -21,14 +22,14 @@ import (
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
"inet.af/netaddr"
"go4.org/netipx"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
const (
errCannotDecryptReponse = Error("cannot decrypt response")
errCouldNotAllocateIP = Error("could not find any suitable IP")
ErrCannotDecryptResponse = Error("cannot decrypt response")
ErrCouldNotAllocateIP = Error("could not find any suitable IP")
// These constants are copied from the upstream tailscale.com/types/key
// library, because they are not exported.
@@ -59,6 +60,8 @@ const (
privateHexPrefix = "privkey:"
PermissionFallback = 0o700
ZstdCompression = "zstd"
)
func MachinePublicKeyStripPrefix(machineKey key.MachinePublic) string {
@@ -116,11 +119,14 @@ func decode(
pubKey *key.MachinePublic,
privKey *key.MachinePrivate,
) error {
log.Trace().Int("length", len(msg)).Msg("Trying to decrypt")
log.Trace().
Str("pubkey", pubKey.ShortString()).
Int("length", len(msg)).
Msg("Trying to decrypt")
decrypted, ok := privKey.OpenFrom(*pubKey, msg)
if !ok {
return errCannotDecryptReponse
return ErrCannotDecryptResponse
}
if err := json.Unmarshal(decrypted, output); err != nil {
@@ -130,25 +136,12 @@ func decode(
return nil
}
func encode(
v interface{},
pubKey *key.MachinePublic,
privKey *key.MachinePrivate,
) ([]byte, error) {
b, err := json.Marshal(v)
if err != nil {
return nil, err
}
return privKey.SealTo(*pubKey, b), nil
}
func (h *Headscale) getAvailableIPs() (MachineAddresses, error) {
var ips MachineAddresses
var err error
ipPrefixes := h.cfg.IPPrefixes
for _, ipPrefix := range ipPrefixes {
var ip *netaddr.IP
var ip *netip.Addr
ip, err = h.getAvailableIP(ipPrefix)
if err != nil {
return ips, err
@@ -159,16 +152,16 @@ func (h *Headscale) getAvailableIPs() (MachineAddresses, error) {
return ips, err
}
func GetIPPrefixEndpoints(na netaddr.IPPrefix) (netaddr.IP, netaddr.IP) {
var network, broadcast netaddr.IP
ipRange := na.Range()
func GetIPPrefixEndpoints(na netip.Prefix) (netip.Addr, netip.Addr) {
var network, broadcast netip.Addr
ipRange := netipx.RangeOfPrefix(na)
network = ipRange.From()
broadcast = ipRange.To()
return network, broadcast
}
func (h *Headscale) getAvailableIP(ipPrefix netaddr.IPPrefix) (*netaddr.IP, error) {
func (h *Headscale) getAvailableIP(ipPrefix netip.Prefix) (*netip.Addr, error) {
usedIps, err := h.getUsedIPs()
if err != nil {
return nil, err
@@ -181,7 +174,7 @@ func (h *Headscale) getAvailableIP(ipPrefix netaddr.IPPrefix) (*netaddr.IP, erro
for {
if !ipPrefix.Contains(ip) {
return nil, errCouldNotAllocateIP
return nil, ErrCouldNotAllocateIP
}
switch {
@@ -189,7 +182,7 @@ func (h *Headscale) getAvailableIP(ipPrefix netaddr.IPPrefix) (*netaddr.IP, erro
fallthrough
case usedIps.Contains(ip):
fallthrough
case ip.IsZero() || ip.IsLoopback():
case ip == netip.Addr{} || ip.IsLoopback():
ip = ip.Next()
continue
@@ -200,19 +193,19 @@ func (h *Headscale) getAvailableIP(ipPrefix netaddr.IPPrefix) (*netaddr.IP, erro
}
}
func (h *Headscale) getUsedIPs() (*netaddr.IPSet, error) {
func (h *Headscale) getUsedIPs() (*netipx.IPSet, error) {
// FIXME: This really deserves a better data model,
// but this was quick to get running and it should be enough
// to begin experimenting with a dual stack tailnet.
var addressesSlices []string
h.db.Model(&Machine{}).Pluck("ip_addresses", &addressesSlices)
var ips netaddr.IPSetBuilder
var ips netipx.IPSetBuilder
for _, slice := range addressesSlices {
var machineAddresses MachineAddresses
err := machineAddresses.Scan(slice)
if err != nil {
return &netaddr.IPSet{}, fmt.Errorf(
return &netipx.IPSet{}, fmt.Errorf(
"failed to read ip from database: %w",
err,
)
@@ -225,7 +218,7 @@ func (h *Headscale) getUsedIPs() (*netaddr.IPSet, error) {
ipSet, err := ips.IPSet()
if err != nil {
return &netaddr.IPSet{}, fmt.Errorf(
return &netipx.IPSet{}, fmt.Errorf(
"failed to build IP Set: %w",
err,
)
@@ -258,7 +251,7 @@ func GrpcSocketDialer(ctx context.Context, addr string) (net.Conn, error) {
return d.DialContext(ctx, "unix", addr)
}
func ipPrefixToString(prefixes []netaddr.IPPrefix) []string {
func ipPrefixToString(prefixes []netip.Prefix) []string {
result := make([]string, len(prefixes))
for index, prefix := range prefixes {
@@ -268,13 +261,13 @@ func ipPrefixToString(prefixes []netaddr.IPPrefix) []string {
return result
}
func stringToIPPrefix(prefixes []string) ([]netaddr.IPPrefix, error) {
result := make([]netaddr.IPPrefix, len(prefixes))
func stringToIPPrefix(prefixes []string) ([]netip.Prefix, error) {
result := make([]netip.Prefix, len(prefixes))
for index, prefixStr := range prefixes {
prefix, err := netaddr.ParseIPPrefix(prefixStr)
prefix, err := netip.ParsePrefix(prefixStr)
if err != nil {
return []netaddr.IPPrefix{}, err
return []netip.Prefix{}, err
}
result[index] = prefix
@@ -283,7 +276,7 @@ func stringToIPPrefix(prefixes []string) ([]netaddr.IPPrefix, error) {
return result, nil
}
func contains[T string | netaddr.IPPrefix](ts []T, t T) bool {
func contains[T string | netip.Prefix](ts []T, t T) bool {
for _, v := range ts {
if reflect.DeepEqual(v, t) {
return true
@@ -324,12 +317,18 @@ func GenerateRandomStringURLSafe(n int) (string, error) {
// It will return an error if the system's secure random
// number generator fails to function correctly, in which
// case the caller should not continue.
func GenerateRandomStringDNSSafe(n int) (string, error) {
str, err := GenerateRandomStringURLSafe(n)
func GenerateRandomStringDNSSafe(size int) (string, error) {
var str string
var err error
for len(str) < size {
str, err = GenerateRandomStringURLSafe(size)
if err != nil {
return "", err
}
str = strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(str, "_", ""), "-", ""))
}
str = strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(str, "_", ""), "-", ""))
return str[:n], err
return str[:size], nil
}
func IsStringInSlice(slice []string, str string) bool {

View File

@@ -1,8 +1,10 @@
package headscale
import (
"net/netip"
"go4.org/netipx"
"gopkg.in/check.v1"
"inet.af/netaddr"
)
func (s *Suite) TestGetAvailableIp(c *check.C) {
@@ -10,7 +12,7 @@ func (s *Suite) TestGetAvailableIp(c *check.C) {
c.Assert(err, check.IsNil)
expected := netaddr.MustParseIP("10.27.0.1")
expected := netip.MustParseAddr("10.27.0.1")
c.Assert(len(ips), check.Equals, 1)
c.Assert(ips[0].String(), check.Equals, expected.String())
@@ -34,7 +36,7 @@ func (s *Suite) TestGetUsedIps(c *check.C) {
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Hostname: "testmachine",
Hostname: "testmachine",
NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
@@ -46,8 +48,8 @@ func (s *Suite) TestGetUsedIps(c *check.C) {
c.Assert(err, check.IsNil)
expected := netaddr.MustParseIP("10.27.0.1")
expectedIPSetBuilder := netaddr.IPSetBuilder{}
expected := netip.MustParseAddr("10.27.0.1")
expectedIPSetBuilder := netipx.IPSetBuilder{}
expectedIPSetBuilder.Add(expected)
expectedIPSet, _ := expectedIPSetBuilder.IPSet()
@@ -82,7 +84,7 @@ func (s *Suite) TestGetMultiIp(c *check.C) {
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Hostname: "testmachine",
Hostname: "testmachine",
NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
@@ -96,11 +98,11 @@ func (s *Suite) TestGetMultiIp(c *check.C) {
usedIps, err := app.getUsedIPs()
c.Assert(err, check.IsNil)
expected0 := netaddr.MustParseIP("10.27.0.1")
expected9 := netaddr.MustParseIP("10.27.0.10")
expected300 := netaddr.MustParseIP("10.27.0.45")
expected0 := netip.MustParseAddr("10.27.0.1")
expected9 := netip.MustParseAddr("10.27.0.10")
expected300 := netip.MustParseAddr("10.27.0.45")
notExpectedIPSetBuilder := netaddr.IPSetBuilder{}
notExpectedIPSetBuilder := netipx.IPSetBuilder{}
notExpectedIPSetBuilder.Add(expected0)
notExpectedIPSetBuilder.Add(expected9)
notExpectedIPSetBuilder.Add(expected300)
@@ -121,7 +123,7 @@ func (s *Suite) TestGetMultiIp(c *check.C) {
c.Assert(
machine1.IPAddresses[0],
check.Equals,
netaddr.MustParseIP("10.27.0.1"),
netip.MustParseAddr("10.27.0.1"),
)
machine50, err := app.GetMachineByID(50)
@@ -130,10 +132,10 @@ func (s *Suite) TestGetMultiIp(c *check.C) {
c.Assert(
machine50.IPAddresses[0],
check.Equals,
netaddr.MustParseIP("10.27.0.50"),
netip.MustParseAddr("10.27.0.50"),
)
expectedNextIP := netaddr.MustParseIP("10.27.1.95")
expectedNextIP := netip.MustParseAddr("10.27.1.95")
nextIP, err := app.getAvailableIPs()
c.Assert(err, check.IsNil)
@@ -153,7 +155,7 @@ func (s *Suite) TestGetAvailableIpMachineWithoutIP(c *check.C) {
ips, err := app.getAvailableIPs()
c.Assert(err, check.IsNil)
expected := netaddr.MustParseIP("10.27.0.1")
expected := netip.MustParseAddr("10.27.0.1")
c.Assert(len(ips), check.Equals, 1)
c.Assert(ips[0].String(), check.Equals, expected.String())
@@ -172,7 +174,7 @@ func (s *Suite) TestGetAvailableIpMachineWithoutIP(c *check.C) {
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Hostname: "testmachine",
Hostname: "testmachine",
NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
@@ -185,3 +187,15 @@ func (s *Suite) TestGetAvailableIpMachineWithoutIP(c *check.C) {
c.Assert(len(ips2), check.Equals, 1)
c.Assert(ips2[0].String(), check.Equals, expected.String())
}
func (s *Suite) TestGenerateRandomStringDNSSafe(c *check.C) {
for i := 0; i < 100000; i++ {
str, err := GenerateRandomStringDNSSafe(8)
if err != nil {
c.Error(err)
}
if len(str) != 8 {
c.Error("invalid length", len(str), str)
}
}
}