Compare commits

...

178 Commits

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

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

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

This means that from this version, all machines in the database is
considered registered.
2022-02-28 18:05:03 +00:00
Kristoffer Dalby
a8649d83c4 Remove all references to Machine.Registered from tests 2022-02-28 17:42:03 +00:00
Kristoffer Dalby
16b21e8158 Remove all references to Machine.Registered 2022-02-28 16:55:57 +00:00
Kristoffer Dalby
35616eb861 Fix oidc error were namespace isnt created #365 2022-02-28 16:41:28 +00:00
Kristoffer Dalby
e7bef56718 Remove reference to registered in integration test 2022-02-28 16:36:29 +00:00
Kristoffer Dalby
c6b87de959 Remove poorly aged test 2022-02-28 16:36:16 +00:00
Kristoffer Dalby
50053e616a Ignore complexity linter 2022-02-28 16:35:08 +00:00
Kristoffer Dalby
54cc3c067f Implement new machine register parameter 2022-02-28 16:34:50 +00:00
Kristoffer Dalby
402a76070f Reuse machine structure for parameters, named parameters 2022-02-28 16:34:28 +00:00
Nico Rey
9a61725e9f Metrics: Disable toggle. Set default port to 9090 2022-02-28 10:40:02 -03:00
Kristoffer Dalby
6126d6d9b5 Merge branch 'main' into metrics-listen 2022-02-28 14:24:25 +01:00
Kristoffer Dalby
469551bc5d Register new machines needing callback in memory
This commit stores temporary registration data in cache, instead of
memory allowing us to only have actually registered machines in the
database.
2022-02-28 08:06:39 +00:00
Kristoffer Dalby
1caa6f5d69 Add todo for JSON datatype 2022-02-27 18:48:25 +01:00
Kristoffer Dalby
ecc26432fd Fix excessive replace 2022-02-27 18:48:12 +01:00
Kristoffer Dalby
caffbd8956 Update cli registration with new method 2022-02-27 18:42:43 +01:00
Kristoffer Dalby
fd1e4a1dcd Generalise registration for openid 2022-02-27 18:42:24 +01:00
Kristoffer Dalby
acb945841c Generalise registration for pre auth keys 2022-02-27 18:42:15 +01:00
Kristoffer Dalby
c58ce6f60c Generalise the registration method to DRY stuff up 2022-02-27 18:40:10 +01:00
Kristoffer Dalby
d6f6939c54 Update changelog 2022-02-27 09:08:29 +01:00
Kristoffer Dalby
e0b9a317f4 Add note to config example 2022-02-27 09:05:08 +01:00
Kristoffer Dalby
c159eb7541 Add basic test of yaml parsing 2022-02-27 09:04:59 +01:00
Kristoffer Dalby
8a3a0b6403 Add YAML support to ACLs 2022-02-27 09:04:48 +01:00
Kristoffer Dalby
67d6c8f946 Remove oversensitive tracing output 2022-02-27 09:04:27 +01:00
Nico Rey
06e6c29a5b metrics: make metrics endpoint toggleable 2022-02-25 18:36:03 -03:00
Nico Rey
a9122c3de3 prometheus: replace default port by a port between the recommended prometheus range 2022-02-25 18:21:20 -03:00
Nico
d55c79e75b Merge branch 'main' into metrics-listen 2022-02-24 10:41:07 -03:00
Kristoffer Dalby
d27f2bc538 Merge branch 'main' into metrics-listen 2022-02-24 11:16:57 +00:00
Nico Rey
e3bcc88880 Linter: make linter happy 2022-02-21 15:22:36 -03:00
Nico Rey
14e49885fb metrics/kustomize: update Kustomize examples 2022-02-21 12:51:25 -03:00
Nico Rey
fbc1843889 metrics/tests: update tests 2022-02-21 12:51:05 -03:00
Nico Rey
45d5ab30ff metrics/cfg: add a new entry for the Prometheus listen address 2022-02-21 12:50:44 -03:00
Nico Rey
d5fd7a5c00 metrics: add a new router and listener for Prometheus' metrics endpoint 2022-02-21 12:50:15 -03:00
Kristoffer Dalby
397b6fc4bf Merge branch 'main' into docs-acl-modifications 2022-02-18 20:13:10 +00:00
Adrien Raffin-Caboisse
55d746d3f5 docs(acls-proposal): wording comment
A hidden thing was implied in this document is that each person should have his own namespace.
Hidden information in spicification isn't good.
Thank's @kradalby for pointing it out.
2022-02-16 09:16:25 +01:00
Adrien Raffin-Caboisse
c364c2a382 chore(acl-proposals): apply prettier 2022-02-15 09:53:22 +01:00
Adrien Raffin-Caboisse
e540679dbd docs(acl-proposals): integrate comments 2022-02-15 09:52:05 +01:00
Adrien Raffin-Caboisse
86b329d8bf chore(docs): create proposals directory 2022-02-15 09:27:33 +01:00
Adrien Raffin-Caboisse
7bdd7748e4 fix(acl): add missing internal namespace communications 2022-02-10 12:03:03 +01:00
Adrien Raffin-Caboisse
0426212348 docs(acls): add example use case 2022-02-10 10:42:26 +01:00
Adrien Raffin-Caboisse
85cf443ac6 docs(acls): Issues with ACL and proposition 2022-02-08 16:59:35 +01:00
64 changed files with 2620 additions and 1078 deletions

View File

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

View File

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

View File

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

View File

@@ -29,6 +29,7 @@ linters:
- wrapcheck
- dupl
- makezero
- maintidx
# We might want to enable this, but it might be a lot of work
- cyclop

View File

@@ -1,27 +1,46 @@
# CHANGELOG
**0.15.0 (2022-xx-xx):**
## 0.15.0 (2022-xx-xx)
**BREAKING**:
**Note:** Take a backup of your database before upgrading.
### BREAKING
- Boundaries between Namespaces has been removed and all nodes can communicate by default [#357](https://github.com/juanfont/headscale/pull/357)
- To limit access between nodes, use [ACLs](./docs/acls.md).
- `/metrics` is now a configurable host:port endpoint: [#344](https://github.com/juanfont/headscale/pull/344). You must update your `config.yaml` file to include:
```yaml
metrics_listen_addr: 127.0.0.1:9090
```
**Changes**:
### Features
- Add support for writing ACL files with YAML [#359](https://github.com/juanfont/headscale/pull/359)
- Users can now use emails in ACL's groups [#372](https://github.com/juanfont/headscale/issues/372)
- Add shorthand aliases for commands and subcommands [#376](https://github.com/juanfont/headscale/pull/376)
- Add `/windows` endpoint for Windows configuration instructions + registry file download [#392](https://github.com/juanfont/headscale/pull/392)
- Added embedded DERP server into Headscale [#388](https://github.com/juanfont/headscale/pull/388)
### Changes
- Fix a bug were the same IP could be assigned to multiple hosts if joined in quick succession [#346](https://github.com/juanfont/headscale/pull/346)
- Simplify the code behind registration of machines [#366](https://github.com/juanfont/headscale/pull/366)
- Nodes are now only written to database if they are registrated successfully
- Fix a limitation in the ACLs that prevented users to write rules with `*` as source [#374](https://github.com/juanfont/headscale/issues/374)
- Reduce the overhead of marshal/unmarshal for Hostinfo, routes and endpoints by using specific types in Machine [#371](https://github.com/juanfont/headscale/pull/371)
- Apply normalization function to FQDN on hostnames when hosts registers and retrieve informations [#363](https://github.com/juanfont/headscale/issues/363)
**0.14.0 (2022-02-24):**
## 0.14.0 (2022-02-24)
**UPCOMING BREAKING**:
From the **next** version (`0.15.0`), all machines will be able to communicate regardless of
**UPCOMING ### BREAKING
From the **next\*\* version (`0.15.0`), all machines will be able to communicate regardless of
if they are in the same namespace. This means that the behaviour currently limited to ACLs
will become default. From version `0.15.0`, all limitation of communications must be done
with ACLs.
This is a part of aligning `headscale`'s behaviour with Tailscale's upstream behaviour.
**BREAKING**:
### BREAKING
- ACLs have been rewritten to align with the bevaviour Tailscale Control Panel provides. **NOTE:** This is only active if you use ACLs
- Namespaces are now treated as Users
@@ -29,17 +48,17 @@ This is a part of aligning `headscale`'s behaviour with Tailscale's upstream beh
- Tags should now work correctly and adding a host to Headscale should now reload the rules.
- The documentation have a [fictional example](docs/acls.md) that should cover some use cases of the ACLs features
**Features**:
### Features
- Add support for configurable mTLS [docs](docs/tls.md#configuring-mutual-tls-authentication-mtls) [#297](https://github.com/juanfont/headscale/pull/297)
**Changes**:
### Changes
- Remove dependency on CGO (switch from CGO SQLite to pure Go) [#346](https://github.com/juanfont/headscale/pull/346)
**0.13.0 (2022-02-18):**
**Features**:
### Features
- Add IPv6 support to the prefix assigned to namespaces
- Add API Key support
@@ -50,7 +69,7 @@ This is a part of aligning `headscale`'s behaviour with Tailscale's upstream beh
- `oidc.domain_map` option has been removed
- `strip_email_domain` option has been added (see [config-example.yaml](./config_example.yaml))
**Changes**:
### Changes
- `ip_prefix` is now superseded by `ip_prefixes` in the configuration [#208](https://github.com/juanfont/headscale/pull/208)
- Upgrade `tailscale` (1.20.4) and other dependencies to latest [#314](https://github.com/juanfont/headscale/pull/314)
@@ -59,35 +78,35 @@ This is a part of aligning `headscale`'s behaviour with Tailscale's upstream beh
**0.12.4 (2022-01-29):**
**Changes**:
### Changes
- Make gRPC Unix Socket permissions configurable [#292](https://github.com/juanfont/headscale/pull/292)
- Trim whitespace before reading Private Key from file [#289](https://github.com/juanfont/headscale/pull/289)
- Add new command to generate a private key for `headscale` [#290](https://github.com/juanfont/headscale/pull/290)
- Fixed issue where hosts deleted from control server may be written back to the database, as long as they are connected to the control server [#278](https://github.com/juanfont/headscale/pull/278)
**0.12.3 (2022-01-13):**
## 0.12.3 (2022-01-13)
**Changes**:
### Changes
- Added Alpine container [#270](https://github.com/juanfont/headscale/pull/270)
- Minor updates in dependencies [#271](https://github.com/juanfont/headscale/pull/271)
**0.12.2 (2022-01-11):**
## 0.12.2 (2022-01-11)
Happy New Year!
**Changes**:
### Changes
- Fix Docker release [#258](https://github.com/juanfont/headscale/pull/258)
- Rewrite main docs [#262](https://github.com/juanfont/headscale/pull/262)
- Improve Docker docs [#263](https://github.com/juanfont/headscale/pull/263)
**0.12.1 (2021-12-24):**
## 0.12.1 (2021-12-24)
(We are skipping 0.12.0 to correct a mishap done weeks ago with the version tagging)
**BREAKING**:
### BREAKING
- Upgrade to Tailscale 1.18 [#229](https://github.com/juanfont/headscale/pull/229)
- This change requires a new format for private key, private keys are now generated automatically:
@@ -95,19 +114,19 @@ Happy New Year!
2. Restart `headscale`, a new key will be generated.
3. Restart all Tailscale clients to fetch the new key
**Changes**:
### Changes
- Unify configuration example [#197](https://github.com/juanfont/headscale/pull/197)
- Add stricter linting and formatting [#223](https://github.com/juanfont/headscale/pull/223)
**Features**:
### Features
- Add gRPC and HTTP API (HTTP API is currently disabled) [#204](https://github.com/juanfont/headscale/pull/204)
- Use gRPC between the CLI and the server [#206](https://github.com/juanfont/headscale/pull/206), [#212](https://github.com/juanfont/headscale/pull/212)
- Beta OpenID Connect support [#126](https://github.com/juanfont/headscale/pull/126), [#227](https://github.com/juanfont/headscale/pull/227)
**0.11.0 (2021-10-25):**
## 0.11.0 (2021-10-25)
**BREAKING**:
### BREAKING
- Make headscale fetch DERP map from URL and file [#196](https://github.com/juanfont/headscale/pull/196)

View File

@@ -1,5 +1,5 @@
# Builder image
FROM docker.io/golang:1.17.7-bullseye AS build
FROM docker.io/golang:1.17.8-bullseye AS build
ENV GOPATH /go
WORKDIR /go/src/headscale

View File

@@ -1,5 +1,5 @@
# Builder image
FROM docker.io/golang:1.17.7-alpine AS build
FROM docker.io/golang:1.17.8-alpine AS build
ENV GOPATH /go
WORKDIR /go/src/headscale

View File

@@ -1,5 +1,5 @@
# Builder image
FROM docker.io/golang:1.17.7-bullseye AS build
FROM docker.io/golang:1.17.8-bullseye AS build
ENV GOPATH /go
WORKDIR /go/src/headscale

View File

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

View File

@@ -23,6 +23,9 @@ test_integration:
test_integration_cli:
go test -tags integration -v integration_cli_test.go integration_common_test.go
test_integration_derp:
go test -tags integration -v integration_embedded_derp_test.go integration_common_test.go
coverprofile_func:
go tool cover -func=coverage.out

View File

@@ -63,6 +63,7 @@ one of the maintainers.
- Dual stack (IPv4 and IPv6)
- Routing advertising (including exit nodes)
- Ephemeral nodes
- Embedded [DERP server](https://tailscale.com/blog/how-tailscale-works/#encrypted-tcp-relays-derp)
## Client OS support
@@ -161,6 +162,13 @@ make build
<sub style="font-size:14px"><b>Juan Font</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/restanrm>
<img src=https://avatars.githubusercontent.com/u/4344371?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Adrien Raffin-Caboisse/>
<br />
<sub style="font-size:14px"><b>Adrien Raffin-Caboisse</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/cure>
<img src=https://avatars.githubusercontent.com/u/149135?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Ward Vandewege/>
@@ -176,10 +184,19 @@ make build
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/restanrm>
<img src=https://avatars.githubusercontent.com/u/4344371?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Adrien Raffin-Caboisse/>
<a href=https://github.com/e-zk>
<img src=https://avatars.githubusercontent.com/u/58356365?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=e-zk/>
<br />
<sub style="font-size:14px"><b>Adrien Raffin-Caboisse</b></sub>
<sub style="font-size:14px"><b>e-zk</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/arch4ngel>
<img src=https://avatars.githubusercontent.com/u/11574161?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Justin Angel/>
<br />
<sub style="font-size:14px"><b>Justin Angel</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
@@ -189,8 +206,6 @@ make build
<sub style="font-size:14px"><b>Alessandro (Ale) Segala</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/unreality>
<img src=https://avatars.githubusercontent.com/u/352522?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=unreality/>
@@ -198,6 +213,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/reynico>
<img src=https://avatars.githubusercontent.com/u/715768?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Nico/>
<br />
<sub style="font-size:14px"><b>Nico</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/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/>
@@ -212,6 +234,8 @@ 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/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/>
@@ -226,6 +250,13 @@ make build
<sub style="font-size:14px"><b>Hoàng Đức Hiếu</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/mevansam>
<img src=https://avatars.githubusercontent.com/u/403630?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Mevan Samaratunga/>
<br />
<sub style="font-size:14px"><b>Mevan Samaratunga</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/dragetd>
<img src=https://avatars.githubusercontent.com/u/3639577?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Michael G./>
@@ -233,8 +264,6 @@ make build
<sub style="font-size:14px"><b>Michael G.</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/ptman>
<img src=https://avatars.githubusercontent.com/u/24669?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Paul Tötterman/>
@@ -249,6 +278,8 @@ make build
<sub style="font-size:14px"><b>Casey Marshall</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/>
@@ -264,10 +295,10 @@ make build
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/lachy-2849>
<img src=https://avatars.githubusercontent.com/u/98844035?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=lachy-2849/>
<a href=https://github.com/lachy2849>
<img src=https://avatars.githubusercontent.com/u/98844035?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=lachy2849/>
<br />
<sub style="font-size:14px"><b>lachy-2849</b></sub>
<sub style="font-size:14px"><b>lachy2849</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
@@ -277,8 +308,6 @@ make build
<sub style="font-size:14px"><b>thomas</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/aberoham>
<img src=https://avatars.githubusercontent.com/u/586805?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Abraham Ingersoll/>
@@ -293,6 +322,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/awoimbee>
<img src=https://avatars.githubusercontent.com/u/22431493?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Arthur Woimbée/>
@@ -321,8 +352,6 @@ make build
<sub style="font-size:14px"><b>Felix Yan</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/JJGadgets>
<img src=https://avatars.githubusercontent.com/u/5709019?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=JJGadgets/>
@@ -337,6 +366,8 @@ make build
<sub style="font-size:14px"><b>Jamie Greeff</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/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/>
@@ -358,6 +389,13 @@ make build
<sub style="font-size:14px"><b>rcursaru</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/renovate-bot>
<img src=https://avatars.githubusercontent.com/u/25180681?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=WhiteSource Renovate/>
<br />
<sub style="font-size:14px"><b>WhiteSource Renovate</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/ryanfowler>
<img src=https://avatars.githubusercontent.com/u/2668821?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Ryan Fowler/>
@@ -365,8 +403,6 @@ make build
<sub style="font-size:14px"><b>Ryan Fowler</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/shaananc>
<img src=https://avatars.githubusercontent.com/u/2287839?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Shaanan Cohney/>
@@ -374,6 +410,8 @@ 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/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/>
@@ -409,8 +447,6 @@ 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/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/>
@@ -418,6 +454,8 @@ 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/>
@@ -432,13 +470,6 @@ make build
<sub style="font-size:14px"><b>derelm</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/e-zk>
<img src=https://avatars.githubusercontent.com/u/58356365?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=e-zk/>
<br />
<sub style="font-size:14px"><b>e-zk</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/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/>
@@ -453,8 +484,6 @@ make build
<sub style="font-size:14px"><b>lion24</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/pernila>
<img src=https://avatars.githubusercontent.com/u/12460060?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=pernila/>
@@ -469,6 +498,8 @@ make build
<sub style="font-size:14px"><b>Wakeful-Cloud</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/xpzouying>
<img src=https://avatars.githubusercontent.com/u/3946563?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=zy/>

132
acls.go
View File

@@ -5,22 +5,23 @@ import (
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/rs/zerolog/log"
"github.com/tailscale/hujson"
"gopkg.in/yaml.v3"
"inet.af/netaddr"
"tailscale.com/tailcfg"
)
const (
errEmptyPolicy = Error("empty policy")
errInvalidAction = Error("invalid action")
errInvalidUserSection = Error("invalid user section")
errInvalidGroup = Error("invalid group")
errInvalidTag = Error("invalid tag")
errInvalidPortFormat = Error("invalid port format")
errEmptyPolicy = Error("empty policy")
errInvalidAction = Error("invalid action")
errInvalidGroup = Error("invalid group")
errInvalidTag = Error("invalid tag")
errInvalidPortFormat = Error("invalid port format")
)
const (
@@ -53,16 +54,36 @@ func (h *Headscale) LoadACLPolicy(path string) error {
return err
}
ast, err := hujson.Parse(policyBytes)
if err != nil {
return err
}
ast.Standardize()
policyBytes = ast.Pack()
err = json.Unmarshal(policyBytes, &policy)
if err != nil {
return err
switch filepath.Ext(path) {
case ".yml", ".yaml":
log.Debug().
Str("path", path).
Bytes("file", policyBytes).
Msg("Loading ACLs from YAML")
err := yaml.Unmarshal(policyBytes, &policy)
if err != nil {
return err
}
log.Trace().
Interface("policy", policy).
Msg("Loaded policy from YAML")
default:
ast, err := hujson.Parse(policyBytes)
if err != nil {
return err
}
ast.Standardize()
policyBytes = ast.Pack()
err = json.Unmarshal(policyBytes, &policy)
if err != nil {
return err
}
}
if policy.IsZero() {
return errEmptyPolicy
}
@@ -138,7 +159,7 @@ func (h *Headscale) generateACLPolicySrcIP(
aclPolicy ACLPolicy,
u string,
) ([]string, error) {
return expandAlias(machines, aclPolicy, u)
return expandAlias(machines, aclPolicy, u, h.cfg.OIDC.StripEmaildomain)
}
func (h *Headscale) generateACLPolicyDestPorts(
@@ -164,7 +185,12 @@ func (h *Headscale) generateACLPolicyDestPorts(
alias = fmt.Sprintf("%s:%s", tokens[0], tokens[1])
}
expanded, err := expandAlias(machines, aclPolicy, alias)
expanded, err := expandAlias(
machines,
aclPolicy,
alias,
h.cfg.OIDC.StripEmaildomain,
)
if err != nil {
return nil, err
}
@@ -196,14 +222,19 @@ func expandAlias(
machines []Machine,
aclPolicy ACLPolicy,
alias string,
stripEmailDomain bool,
) ([]string, error) {
ips := []string{}
if alias == "*" {
return []string{"*"}, nil
}
log.Debug().
Str("alias", alias).
Msg("Expanding")
if strings.HasPrefix(alias, "group:") {
namespaces, err := expandGroup(aclPolicy, alias)
namespaces, err := expandGroup(aclPolicy, alias, stripEmailDomain)
if err != nil {
return ips, err
}
@@ -218,20 +249,14 @@ func expandAlias(
}
if strings.HasPrefix(alias, "tag:") {
owners, err := expandTagOwners(aclPolicy, alias)
owners, err := expandTagOwners(aclPolicy, alias, stripEmailDomain)
if err != nil {
return ips, err
}
for _, namespace := range owners {
machines := filterMachinesByNamespace(machines, namespace)
for _, machine := range machines {
if len(machine.HostInfo) == 0 {
continue
}
hi, err := machine.GetHostInfo()
if err != nil {
return ips, err
}
hi := machine.GetHostInfo()
for _, t := range hi.RequestTags {
if alias == t {
ips = append(ips, machine.IPAddresses.ToStringSlice()...)
@@ -245,10 +270,8 @@ func expandAlias(
// if alias is a namespace
nodes := filterMachinesByNamespace(machines, alias)
nodes, err := excludeCorrectlyTaggedNodes(aclPolicy, nodes, alias)
if err != nil {
return ips, err
}
nodes = excludeCorrectlyTaggedNodes(aclPolicy, nodes, alias)
for _, n := range nodes {
ips = append(ips, n.IPAddresses.ToStringSlice()...)
}
@@ -273,7 +296,9 @@ func expandAlias(
return []string{cidr.String()}, nil
}
return ips, errInvalidUserSection
log.Warn().Msgf("No IPs found with the alias %v", alias)
return ips, nil
}
// excludeCorrectlyTaggedNodes will remove from the list of input nodes the ones
@@ -283,7 +308,7 @@ func excludeCorrectlyTaggedNodes(
aclPolicy ACLPolicy,
nodes []Machine,
namespace string,
) ([]Machine, error) {
) []Machine {
out := []Machine{}
tags := []string{}
for tag, ns := range aclPolicy.TagOwners {
@@ -293,15 +318,8 @@ func excludeCorrectlyTaggedNodes(
}
// for each machine if tag is in tags list, don't append it.
for _, machine := range nodes {
if len(machine.HostInfo) == 0 {
out = append(out, machine)
hi := machine.GetHostInfo()
continue
}
hi, err := machine.GetHostInfo()
if err != nil {
return out, err
}
found := false
for _, t := range hi.RequestTags {
if containsString(tags, t) {
@@ -315,7 +333,7 @@ func excludeCorrectlyTaggedNodes(
}
}
return out, nil
return out
}
func expandPorts(portsStr string) (*[]tailcfg.PortRange, error) {
@@ -374,7 +392,11 @@ func filterMachinesByNamespace(machines []Machine, namespace string) []Machine {
// expandTagOwners will return a list of namespace. An owner can be either a namespace or a group
// a group cannot be composed of groups.
func expandTagOwners(aclPolicy ACLPolicy, tag string) ([]string, error) {
func expandTagOwners(
aclPolicy ACLPolicy,
tag string,
stripEmailDomain bool,
) ([]string, error) {
var owners []string
ows, ok := aclPolicy.TagOwners[tag]
if !ok {
@@ -386,7 +408,7 @@ func expandTagOwners(aclPolicy ACLPolicy, tag string) ([]string, error) {
}
for _, owner := range ows {
if strings.HasPrefix(owner, "group:") {
gs, err := expandGroup(aclPolicy, owner)
gs, err := expandGroup(aclPolicy, owner, stripEmailDomain)
if err != nil {
return []string{}, err
}
@@ -401,8 +423,13 @@ func expandTagOwners(aclPolicy ACLPolicy, tag string) ([]string, error) {
// expandGroup will return the list of namespace inside the group
// after some validation.
func expandGroup(aclPolicy ACLPolicy, group string) ([]string, error) {
groups, ok := aclPolicy.Groups[group]
func expandGroup(
aclPolicy ACLPolicy,
group string,
stripEmailDomain bool,
) ([]string, error) {
outGroups := []string{}
aclGroups, ok := aclPolicy.Groups[group]
if !ok {
return []string{}, fmt.Errorf(
"group %v isn't registered. %w",
@@ -410,14 +437,23 @@ func expandGroup(aclPolicy ACLPolicy, group string) ([]string, error) {
errInvalidGroup,
)
}
for _, g := range groups {
if strings.HasPrefix(g, "group:") {
for _, group := range aclGroups {
if strings.HasPrefix(group, "group:") {
return []string{}, fmt.Errorf(
"%w. A group cannot be composed of groups. https://tailscale.com/kb/1018/acls/#groups",
errInvalidGroup,
)
}
grp, err := NormalizeToFQDNRules(group, stripEmailDomain)
if err != nil {
return []string{}, fmt.Errorf(
"failed to normalize group %q, err: %w",
group,
errInvalidGroup,
)
}
outGroups = append(outGroups, grp)
}
return groups, nil
return outGroups, nil
}

View File

@@ -6,7 +6,6 @@ import (
"testing"
"gopkg.in/check.v1"
"gorm.io/datatypes"
"inet.af/netaddr"
"tailscale.com/tailcfg"
)
@@ -108,9 +107,12 @@ func (s *Suite) TestValidExpandTagOwnersInUsers(c *check.C) {
_, err = app.GetMachine("user1", "testmachine")
c.Assert(err, check.NotNil)
hostInfo := []byte(
"{\"OS\":\"centos\",\"Hostname\":\"testmachine\",\"RequestTags\":[\"tag:test\"]}",
)
hostInfo := tailcfg.Hostinfo{
OS: "centos",
Hostname: "testmachine",
RequestTags: []string{"tag:test"},
}
machine := Machine{
ID: 0,
MachineKey: "foo",
@@ -119,10 +121,9 @@ func (s *Suite) TestValidExpandTagOwnersInUsers(c *check.C) {
Name: "testmachine",
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
HostInfo: datatypes.JSON(hostInfo),
HostInfo: HostInfo(hostInfo),
}
app.db.Save(&machine)
@@ -152,9 +153,12 @@ func (s *Suite) TestValidExpandTagOwnersInPorts(c *check.C) {
_, err = app.GetMachine("user1", "testmachine")
c.Assert(err, check.NotNil)
hostInfo := []byte(
"{\"OS\":\"centos\",\"Hostname\":\"testmachine\",\"RequestTags\":[\"tag:test\"]}",
)
hostInfo := tailcfg.Hostinfo{
OS: "centos",
Hostname: "testmachine",
RequestTags: []string{"tag:test"},
}
machine := Machine{
ID: 1,
MachineKey: "12345",
@@ -163,10 +167,9 @@ func (s *Suite) TestValidExpandTagOwnersInPorts(c *check.C) {
Name: "testmachine",
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
HostInfo: datatypes.JSON(hostInfo),
HostInfo: HostInfo(hostInfo),
}
app.db.Save(&machine)
@@ -196,9 +199,12 @@ func (s *Suite) TestInvalidTagValidNamespace(c *check.C) {
_, err = app.GetMachine("user1", "testmachine")
c.Assert(err, check.NotNil)
hostInfo := []byte(
"{\"OS\":\"centos\",\"Hostname\":\"testmachine\",\"RequestTags\":[\"tag:foo\"]}",
)
hostInfo := tailcfg.Hostinfo{
OS: "centos",
Hostname: "testmachine",
RequestTags: []string{"tag:foo"},
}
machine := Machine{
ID: 1,
MachineKey: "12345",
@@ -207,10 +213,9 @@ func (s *Suite) TestInvalidTagValidNamespace(c *check.C) {
Name: "testmachine",
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
HostInfo: datatypes.JSON(hostInfo),
HostInfo: HostInfo(hostInfo),
}
app.db.Save(&machine)
@@ -239,9 +244,12 @@ func (s *Suite) TestValidTagInvalidNamespace(c *check.C) {
_, err = app.GetMachine("user1", "webserver")
c.Assert(err, check.NotNil)
hostInfo := []byte(
"{\"OS\":\"centos\",\"Hostname\":\"webserver\",\"RequestTags\":[\"tag:webapp\"]}",
)
hostInfo := tailcfg.Hostinfo{
OS: "centos",
Hostname: "webserver",
RequestTags: []string{"tag:webapp"},
}
machine := Machine{
ID: 1,
MachineKey: "12345",
@@ -250,14 +258,16 @@ func (s *Suite) TestValidTagInvalidNamespace(c *check.C) {
Name: "webserver",
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
HostInfo: datatypes.JSON(hostInfo),
HostInfo: HostInfo(hostInfo),
}
app.db.Save(&machine)
_, err = app.GetMachine("user1", "user")
hostInfo = []byte("{\"OS\":\"debian\",\"Hostname\":\"user\"}")
hostInfo2 := tailcfg.Hostinfo{
OS: "debian",
Hostname: "Hostname",
}
c.Assert(err, check.NotNil)
machine = Machine{
ID: 2,
@@ -267,10 +277,9 @@ func (s *Suite) TestValidTagInvalidNamespace(c *check.C) {
Name: "user",
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
HostInfo: datatypes.JSON(hostInfo),
HostInfo: HostInfo(hostInfo2),
}
app.db.Save(&machine)
@@ -328,6 +337,22 @@ func (s *Suite) TestPortWildcard(c *check.C) {
c.Assert(rules[0].SrcIPs[0], check.Equals, "*")
}
func (s *Suite) TestPortWildcardYAML(c *check.C) {
err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_wildcards.yaml")
c.Assert(err, check.IsNil)
rules, err := app.generateACLRules()
c.Assert(err, check.IsNil)
c.Assert(rules, check.NotNil)
c.Assert(rules, check.HasLen, 1)
c.Assert(rules[0].DstPorts, check.HasLen, 1)
c.Assert(rules[0].DstPorts[0].Ports.First, check.Equals, uint16(0))
c.Assert(rules[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535))
c.Assert(rules[0].SrcIPs, check.HasLen, 1)
c.Assert(rules[0].SrcIPs[0], check.Equals, "*")
}
func (s *Suite) TestPortNamespace(c *check.C) {
namespace, err := app.CreateNamespace("testnamespace")
c.Assert(err, check.IsNil)
@@ -345,7 +370,6 @@ func (s *Suite) TestPortNamespace(c *check.C) {
DiscoKey: "faa",
Name: "testmachine",
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: ips,
AuthKeyID: uint(pak.ID),
@@ -388,7 +412,6 @@ func (s *Suite) TestPortGroup(c *check.C) {
DiscoKey: "faa",
Name: "testmachine",
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: ips,
AuthKeyID: uint(pak.ID),
@@ -414,8 +437,9 @@ func (s *Suite) TestPortGroup(c *check.C) {
func Test_expandGroup(t *testing.T) {
type args struct {
aclPolicy ACLPolicy
group string
aclPolicy ACLPolicy
group string
stripEmailDomain bool
}
tests := []struct {
name string
@@ -432,7 +456,8 @@ func Test_expandGroup(t *testing.T) {
"group:foo": []string{"user2", "user3"},
},
},
group: "group:test",
group: "group:test",
stripEmailDomain: true,
},
want: []string{"user1", "user2", "user3"},
wantErr: false,
@@ -446,15 +471,54 @@ func Test_expandGroup(t *testing.T) {
"group:foo": []string{"user2", "user3"},
},
},
group: "group:undefined",
group: "group:undefined",
stripEmailDomain: true,
},
want: []string{},
wantErr: true,
},
{
name: "Expand emails in group",
args: args{
aclPolicy: ACLPolicy{
Groups: Groups{
"group:admin": []string{
"joe.bar@gmail.com",
"john.doe@yahoo.fr",
},
},
},
group: "group:admin",
stripEmailDomain: true,
},
want: []string{"joe.bar", "john.doe"},
wantErr: false,
},
{
name: "Expand emails in group",
args: args{
aclPolicy: ACLPolicy{
Groups: Groups{
"group:admin": []string{
"joe.bar@gmail.com",
"john.doe@yahoo.fr",
},
},
},
group: "group:admin",
stripEmailDomain: false,
},
want: []string{"joe.bar.gmail.com", "john.doe.yahoo.fr"},
wantErr: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := expandGroup(test.args.aclPolicy, test.args.group)
got, err := expandGroup(
test.args.aclPolicy,
test.args.group,
test.args.stripEmailDomain,
)
if (err != nil) != test.wantErr {
t.Errorf("expandGroup() error = %v, wantErr %v", err, test.wantErr)
@@ -469,8 +533,9 @@ func Test_expandGroup(t *testing.T) {
func Test_expandTagOwners(t *testing.T) {
type args struct {
aclPolicy ACLPolicy
tag string
aclPolicy ACLPolicy
tag string
stripEmailDomain bool
}
tests := []struct {
name string
@@ -484,7 +549,8 @@ func Test_expandTagOwners(t *testing.T) {
aclPolicy: ACLPolicy{
TagOwners: TagOwners{"tag:test": []string{"user1"}},
},
tag: "tag:test",
tag: "tag:test",
stripEmailDomain: true,
},
want: []string{"user1"},
wantErr: false,
@@ -496,7 +562,8 @@ func Test_expandTagOwners(t *testing.T) {
Groups: Groups{"group:foo": []string{"user1", "user2"}},
TagOwners: TagOwners{"tag:test": []string{"group:foo"}},
},
tag: "tag:test",
tag: "tag:test",
stripEmailDomain: true,
},
want: []string{"user1", "user2"},
wantErr: false,
@@ -508,7 +575,8 @@ func Test_expandTagOwners(t *testing.T) {
Groups: Groups{"group:foo": []string{"user1", "user2"}},
TagOwners: TagOwners{"tag:test": []string{"group:foo", "user3"}},
},
tag: "tag:test",
tag: "tag:test",
stripEmailDomain: true,
},
want: []string{"user1", "user2", "user3"},
wantErr: false,
@@ -519,7 +587,8 @@ func Test_expandTagOwners(t *testing.T) {
aclPolicy: ACLPolicy{
TagOwners: TagOwners{"tag:foo": []string{"group:foo", "user1"}},
},
tag: "tag:test",
tag: "tag:test",
stripEmailDomain: true,
},
want: []string{},
wantErr: true,
@@ -531,7 +600,8 @@ func Test_expandTagOwners(t *testing.T) {
Groups: Groups{"group:bar": []string{"user1", "user2"}},
TagOwners: TagOwners{"tag:test": []string{"group:foo", "user2"}},
},
tag: "tag:test",
tag: "tag:test",
stripEmailDomain: true,
},
want: []string{},
wantErr: true,
@@ -539,7 +609,11 @@ func Test_expandTagOwners(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := expandTagOwners(test.args.aclPolicy, test.args.tag)
got, err := expandTagOwners(
test.args.aclPolicy,
test.args.tag,
test.args.stripEmailDomain,
)
if (err != nil) != test.wantErr {
t.Errorf("expandTagOwners() error = %v, wantErr %v", err, test.wantErr)
@@ -701,9 +775,10 @@ func Test_listMachinesInNamespace(t *testing.T) {
// nolint
func Test_expandAlias(t *testing.T) {
type args struct {
machines []Machine
aclPolicy ACLPolicy
alias string
machines []Machine
aclPolicy ACLPolicy
alias string
stripEmailDomain bool
}
tests := []struct {
name string
@@ -723,7 +798,8 @@ func Test_expandAlias(t *testing.T) {
},
},
},
aclPolicy: ACLPolicy{},
aclPolicy: ACLPolicy{},
stripEmailDomain: true,
},
want: []string{"*"},
wantErr: false,
@@ -761,6 +837,7 @@ func Test_expandAlias(t *testing.T) {
aclPolicy: ACLPolicy{
Groups: Groups{"group:accountant": []string{"joe", "marc"}},
},
stripEmailDomain: true,
},
want: []string{"100.64.0.1", "100.64.0.2", "100.64.0.3"},
wantErr: false,
@@ -798,6 +875,7 @@ func Test_expandAlias(t *testing.T) {
aclPolicy: ACLPolicy{
Groups: Groups{"group:accountant": []string{"joe", "marc"}},
},
stripEmailDomain: true,
},
want: []string{},
wantErr: true,
@@ -805,9 +883,10 @@ func Test_expandAlias(t *testing.T) {
{
name: "simple ipaddress",
args: args{
alias: "10.0.0.3",
machines: []Machine{},
aclPolicy: ACLPolicy{},
alias: "10.0.0.3",
machines: []Machine{},
aclPolicy: ACLPolicy{},
stripEmailDomain: true,
},
want: []string{"10.0.0.3"},
wantErr: false,
@@ -822,6 +901,7 @@ func Test_expandAlias(t *testing.T) {
"homeNetwork": netaddr.MustParseIPPrefix("192.168.1.0/24"),
},
},
stripEmailDomain: true,
},
want: []string{"192.168.1.0/24"},
wantErr: false,
@@ -829,9 +909,10 @@ func Test_expandAlias(t *testing.T) {
{
name: "simple host",
args: args{
alias: "10.0.0.1",
machines: []Machine{},
aclPolicy: ACLPolicy{},
alias: "10.0.0.1",
machines: []Machine{},
aclPolicy: ACLPolicy{},
stripEmailDomain: true,
},
want: []string{"10.0.0.1"},
wantErr: false,
@@ -839,9 +920,10 @@ func Test_expandAlias(t *testing.T) {
{
name: "simple CIDR",
args: args{
alias: "10.0.0.0/16",
machines: []Machine{},
aclPolicy: ACLPolicy{},
alias: "10.0.0.0/16",
machines: []Machine{},
aclPolicy: ACLPolicy{},
stripEmailDomain: true,
},
want: []string{"10.0.0.0/16"},
wantErr: false,
@@ -856,18 +938,22 @@ func Test_expandAlias(t *testing.T) {
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: []byte(
"{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:hr-webserver\"]}",
),
HostInfo: HostInfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:hr-webserver"},
},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: []byte(
"{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:hr-webserver\"]}",
),
HostInfo: HostInfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:hr-webserver"},
},
},
{
IPAddresses: MachineAddresses{
@@ -885,6 +971,7 @@ func Test_expandAlias(t *testing.T) {
aclPolicy: ACLPolicy{
TagOwners: TagOwners{"tag:hr-webserver": []string{"joe"}},
},
stripEmailDomain: true,
},
want: []string{"100.64.0.1", "100.64.0.2"},
wantErr: false,
@@ -925,6 +1012,7 @@ func Test_expandAlias(t *testing.T) {
"tag:accountant-webserver": []string{"group:accountant"},
},
},
stripEmailDomain: true,
},
want: []string{},
wantErr: true,
@@ -939,18 +1027,22 @@ func Test_expandAlias(t *testing.T) {
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: []byte(
"{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:accountant-webserver\"]}",
),
HostInfo: HostInfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:accountant-webserver"},
},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: []byte(
"{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:accountant-webserver\"]}",
),
HostInfo: HostInfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:accountant-webserver"},
},
},
{
IPAddresses: MachineAddresses{
@@ -968,6 +1060,7 @@ func Test_expandAlias(t *testing.T) {
aclPolicy: ACLPolicy{
TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}},
},
stripEmailDomain: true,
},
want: []string{"100.64.0.4"},
wantErr: false,
@@ -979,6 +1072,7 @@ func Test_expandAlias(t *testing.T) {
test.args.machines,
test.args.aclPolicy,
test.args.alias,
test.args.stripEmailDomain,
)
if (err != nil) != test.wantErr {
t.Errorf("expandAlias() error = %v, wantErr %v", err, test.wantErr)
@@ -1016,18 +1110,22 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: []byte(
"{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:accountant-webserver\"]}",
),
HostInfo: HostInfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:accountant-webserver"},
},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: []byte(
"{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:accountant-webserver\"]}",
),
HostInfo: HostInfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:accountant-webserver"},
},
},
{
IPAddresses: MachineAddresses{
@@ -1044,7 +1142,6 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
Namespace: Namespace{Name: "joe"},
},
},
wantErr: false,
},
{
name: "all nodes have invalid tags, don't exclude them",
@@ -1058,18 +1155,22 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: []byte(
"{\"OS\":\"centos\",\"Hostname\":\"hr-web1\",\"RequestTags\":[\"tag:hr-webserver\"]}",
),
HostInfo: HostInfo{
OS: "centos",
Hostname: "hr-web1",
RequestTags: []string{"tag:hr-webserver"},
},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: []byte(
"{\"OS\":\"centos\",\"Hostname\":\"hr-web2\",\"RequestTags\":[\"tag:hr-webserver\"]}",
),
HostInfo: HostInfo{
OS: "centos",
Hostname: "hr-web2",
RequestTags: []string{"tag:hr-webserver"},
},
},
{
IPAddresses: MachineAddresses{
@@ -1086,18 +1187,22 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: []byte(
"{\"OS\":\"centos\",\"Hostname\":\"hr-web1\",\"RequestTags\":[\"tag:hr-webserver\"]}",
),
HostInfo: HostInfo{
OS: "centos",
Hostname: "hr-web1",
RequestTags: []string{"tag:hr-webserver"},
},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: []byte(
"{\"OS\":\"centos\",\"Hostname\":\"hr-web2\",\"RequestTags\":[\"tag:hr-webserver\"]}",
),
HostInfo: HostInfo{
OS: "centos",
Hostname: "hr-web2",
RequestTags: []string{"tag:hr-webserver"},
},
},
{
IPAddresses: MachineAddresses{
@@ -1106,25 +1211,15 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
Namespace: Namespace{Name: "joe"},
},
},
wantErr: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := excludeCorrectlyTaggedNodes(
got := excludeCorrectlyTaggedNodes(
test.args.aclPolicy,
test.args.nodes,
test.args.namespace,
)
if (err != nil) != test.wantErr {
t.Errorf(
"excludeCorrectlyTaggedNodes() error = %v, wantErr %v",
err,
test.wantErr,
)
return
}
if !reflect.DeepEqual(got, test.want) {
t.Errorf("excludeCorrectlyTaggedNodes() = %v, want %v", got, test.want)
}

View File

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

230
api.go
View File

@@ -22,7 +22,7 @@ import (
const (
reservedResponseHeaderSize = 4
RegisterMethodAuthKey = "authKey"
RegisterMethodAuthKey = "authkey"
RegisterMethodOIDC = "oidc"
RegisterMethodCLI = "cli"
ErrRegisterMethodCLIDoesNotSupportExpire = Error(
@@ -45,22 +45,21 @@ type registerWebAPITemplateConfig struct {
}
var registerWebAPITemplate = template.Must(
template.New("registerweb").Parse(`<html>
template.New("registerweb").Parse(`
<html>
<head>
<title>Registration - Headscale</title>
</head>
<body>
<h1>headscale</h1>
<p>
Run the command below in the headscale server to add this machine to your network:
</p>
<p>
<code>
<b>headscale -n NAMESPACE nodes register --key {{.Key}}</b>
</code>
</p>
<h1>headscale</h1>
<h2>Machine registration</h2>
<p>
Run the command below in the headscale server to add this machine to your network:
</p>
<pre><code>headscale -n NAMESPACE nodes register --key {{.Key}}</code></pre>
</body>
</html>`),
)
</html>
`))
// RegisterWebAPI shows a simple message in the browser to point to the CLI
// Listens in /register.
@@ -125,25 +124,63 @@ func (h *Headscale) RegistrationHandler(ctx *gin.Context) {
machine, err := h.GetMachineByMachineKey(machineKey)
if errors.Is(err, gorm.ErrRecordNotFound) {
log.Info().Str("machine", req.Hostinfo.Hostname).Msg("New machine")
newMachine := Machine{
Expiry: &time.Time{},
MachineKey: MachinePublicKeyStripPrefix(machineKey),
Name: req.Hostinfo.Hostname,
}
if err := h.db.Create(&newMachine).Error; err != nil {
log.Error().
Caller().
Err(err).
Msg("Could not create row")
machineRegistrations.WithLabelValues("unknown", "web", "error", machine.Namespace.Name).
Inc()
machineKeyStr := MachinePublicKeyStripPrefix(machineKey)
// If the machine has AuthKey set, handle registration via PreAuthKeys
if req.Auth.AuthKey != "" {
h.handleAuthKey(ctx, machineKey, req)
return
}
machine = &newMachine
hname, err := NormalizeToFQDNRules(
req.Hostinfo.Hostname,
h.cfg.OIDC.StripEmaildomain,
)
if err != nil {
log.Error().
Caller().
Str("func", "RegistrationHandler").
Str("hostinfo.name", req.Hostinfo.Hostname).
Err(err)
return
}
// The machine did not have a key to authenticate, which means
// that we rely on a method that calls back some how (OpenID or CLI)
// We create the machine and then keep it around until a callback
// happens
newMachine := Machine{
MachineKey: machineKeyStr,
Name: hname,
NodeKey: NodePublicKeyStripPrefix(req.NodeKey),
LastSeen: &now,
Expiry: &time.Time{},
}
if !req.Expiry.IsZero() {
log.Trace().
Caller().
Str("machine", req.Hostinfo.Hostname).
Time("expiry", req.Expiry).
Msg("Non-zero expiry time requested")
newMachine.Expiry = &req.Expiry
}
h.registrationCache.Set(
machineKeyStr,
newMachine,
registerCacheExpiration,
)
h.handleMachineRegistrationNew(ctx, machineKey, req)
return
}
if machine.Registered {
// The machine is already registered, so we need to pass through reauth or key update.
if machine != nil {
// If the NodeKey stored in headscale is the same as the key presented in a registration
// request, then we have a node that is either:
// - Trying to log out (sending a expiry in the past)
@@ -180,15 +217,6 @@ func (h *Headscale) RegistrationHandler(ctx *gin.Context) {
return
}
// If the machine has AuthKey set, handle registration via PreAuthKeys
if req.Auth.AuthKey != "" {
h.handleAuthKey(ctx, machineKey, req, *machine)
return
}
h.handleMachineRegistrationNew(ctx, machineKey, req, *machine)
}
func (h *Headscale) getMapResponse(
@@ -402,7 +430,7 @@ func (h *Headscale) handleMachineExpired(
Msg("Machine registration has expired. Sending a authurl to register")
if registerRequest.Auth.AuthKey != "" {
h.handleAuthKey(ctx, machineKey, registerRequest, machine)
h.handleAuthKey(ctx, machineKey, registerRequest)
return
}
@@ -465,13 +493,12 @@ func (h *Headscale) handleMachineRegistrationNew(
ctx *gin.Context,
machineKey key.MachinePublic,
registerRequest tailcfg.RegisterRequest,
machine Machine,
) {
resp := tailcfg.RegisterResponse{}
// The machine registration is new, redirect the client to the registration URL
log.Debug().
Str("machine", machine.Name).
Str("machine", registerRequest.Hostinfo.Hostname).
Msg("The node is sending us a new NodeKey, sending auth url")
if h.cfg.OIDC.Issuer != "" {
resp.AuthURL = fmt.Sprintf(
@@ -484,24 +511,6 @@ func (h *Headscale) handleMachineRegistrationNew(
strings.TrimSuffix(h.cfg.ServerURL, "/"), MachinePublicKeyStripPrefix(machineKey))
}
if !registerRequest.Expiry.IsZero() {
log.Trace().
Caller().
Str("machine", machine.Name).
Time("expiry", registerRequest.Expiry).
Msg("Non-zero expiry time requested, adding to cache")
h.requestedExpiryCache.Set(
machineKey.String(),
registerRequest.Expiry,
requestedExpiryCacheExpiration,
)
}
machine.NodeKey = NodePublicKeyStripPrefix(registerRequest.NodeKey)
// save the NodeKey
h.db.Save(&machine)
respBody, err := encode(resp, &machineKey, h.privateKey)
if err != nil {
log.Error().
@@ -520,19 +529,21 @@ func (h *Headscale) handleAuthKey(
ctx *gin.Context,
machineKey key.MachinePublic,
registerRequest tailcfg.RegisterRequest,
machine Machine,
) {
machineKeyStr := MachinePublicKeyStripPrefix(machineKey)
log.Debug().
Str("func", "handleAuthKey").
Str("machine", registerRequest.Hostinfo.Hostname).
Msgf("Processing auth key for %s", registerRequest.Hostinfo.Hostname)
resp := tailcfg.RegisterResponse{}
pak, err := h.checkKeyValidity(registerRequest.Auth.AuthKey)
if err != nil {
log.Error().
Caller().
Str("func", "handleAuthKey").
Str("machine", machine.Name).
Str("machine", registerRequest.Hostinfo.Hostname).
Err(err).
Msg("Failed authentication via AuthKey")
resp.MachineAuthorized = false
@@ -541,76 +552,87 @@ func (h *Headscale) handleAuthKey(
log.Error().
Caller().
Str("func", "handleAuthKey").
Str("machine", machine.Name).
Str("machine", registerRequest.Hostinfo.Hostname).
Err(err).
Msg("Cannot encode message")
ctx.String(http.StatusInternalServerError, "")
machineRegistrations.WithLabelValues("new", "authkey", "error", machine.Namespace.Name).
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", machine.Name).
Str("machine", registerRequest.Hostinfo.Hostname).
Msg("Failed authentication via AuthKey")
machineRegistrations.WithLabelValues("new", "authkey", "error", machine.Namespace.Name).
Inc()
if pak != nil {
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
Inc()
} else {
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error").Inc()
}
return
}
if machine.isRegistered() {
log.Debug().
Str("func", "handleAuthKey").
Str("machine", registerRequest.Hostinfo.Hostname).
Msg("Authentication key was valid, proceeding to acquire IP addresses")
nodeKey := NodePublicKeyStripPrefix(registerRequest.NodeKey)
// retrieve machine information if it exist
// The error is not important, because if it does not
// exist, then this is a new machine and we will move
// on to registration.
machine, _ := h.GetMachineByMachineKey(machineKey)
if machine != nil {
log.Trace().
Caller().
Str("machine", machine.Name).
Msg("machine already registered, reauthenticating")
Msg("machine already registered, refreshing with new auth key")
h.RefreshMachine(&machine, registerRequest.Expiry)
machine.NodeKey = nodeKey
machine.AuthKeyID = uint(pak.ID)
h.RefreshMachine(machine, registerRequest.Expiry)
} else {
log.Debug().
Str("func", "handleAuthKey").
Str("machine", machine.Name).
Msg("Authentication key was valid, proceeding to acquire IP addresses")
now := time.Now().UTC()
machineToRegister := Machine{
Name: registerRequest.Hostinfo.Hostname,
NamespaceID: pak.Namespace.ID,
MachineKey: machineKeyStr,
RegisterMethod: RegisterMethodAuthKey,
Expiry: &registerRequest.Expiry,
NodeKey: nodeKey,
LastSeen: &now,
AuthKeyID: uint(pak.ID),
}
h.ipAllocationMutex.Lock()
ips, err := h.getAvailableIPs()
machine, err = h.RegisterMachine(
machineToRegister,
)
if err != nil {
log.Error().
Caller().
Str("func", "handleAuthKey").
Str("machine", machine.Name).
Msg("Failed to find an available IP address")
machineRegistrations.WithLabelValues("new", "authkey", "error", machine.Namespace.Name).
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
}
log.Info().
Str("func", "handleAuthKey").
Str("machine", machine.Name).
Str("ips", strings.Join(ips.ToStringSlice(), ",")).
Msgf("Assigning %s to %s", strings.Join(ips.ToStringSlice(), ","), machine.Name)
machine.Expiry = &registerRequest.Expiry
machine.AuthKeyID = uint(pak.ID)
machine.IPAddresses = ips
machine.NamespaceID = pak.NamespaceID
machine.NodeKey = NodePublicKeyStripPrefix(registerRequest.NodeKey)
// we update it just in case
machine.Registered = true
machine.RegisterMethod = RegisterMethodAuthKey
h.db.Save(&machine)
h.ipAllocationMutex.Unlock()
}
pak.Used = true
h.db.Save(&pak)
h.UsePreAuthKey(pak)
resp.MachineAuthorized = true
resp.User = *pak.Namespace.toUser()
@@ -619,21 +641,21 @@ func (h *Headscale) handleAuthKey(
log.Error().
Caller().
Str("func", "handleAuthKey").
Str("machine", machine.Name).
Str("machine", registerRequest.Hostinfo.Hostname).
Err(err).
Msg("Cannot encode message")
machineRegistrations.WithLabelValues("new", "authkey", "error", machine.Namespace.Name).
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
Inc()
ctx.String(http.StatusInternalServerError, "Extremely sad!")
return
}
machineRegistrations.WithLabelValues("new", "authkey", "success", machine.Namespace.Name).
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "success", pak.Namespace.Name).
Inc()
ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
log.Info().
Str("func", "handleAuthKey").
Str("machine", machine.Name).
Str("machine", registerRequest.Hostinfo.Hostname).
Str("ips", strings.Join(machine.IPAddresses.ToStringSlice(), ", ")).
Msg("Successfully authenticated via AuthKey")
}

108
app.go
View File

@@ -55,8 +55,8 @@ const (
HTTPReadTimeout = 30 * time.Second
privateKeyFileMode = 0o600
requestedExpiryCacheExpiration = time.Minute * 5
requestedExpiryCacheCleanupInterval = time.Minute * 10
registerCacheExpiration = time.Minute * 15
registerCacheCleanup = time.Minute * 20
errUnsupportedDatabase = Error("unsupported DB")
errUnsupportedLetsEncryptChallengeType = Error(
@@ -72,6 +72,7 @@ const (
type Config struct {
ServerURL string
Addr string
MetricsAddr string
GRPCAddr string
GRPCAllowInsecure bool
EphemeralNodeInactivityTimeout time.Duration
@@ -119,10 +120,16 @@ type OIDCConfig struct {
}
type DERPConfig struct {
URLs []url.URL
Paths []string
AutoUpdate bool
UpdateFrequency time.Duration
ServerEnabled bool
ServerRegionID int
ServerRegionCode string
ServerRegionName string
STUNEnabled bool
STUNAddr string
URLs []url.URL
Paths []string
AutoUpdate bool
UpdateFrequency time.Duration
}
type CLIConfig struct {
@@ -141,18 +148,18 @@ type Headscale struct {
dbDebug bool
privateKey *key.MachinePrivate
DERPMap *tailcfg.DERPMap
DERPMap *tailcfg.DERPMap
DERPServer *DERPServer
aclPolicy *ACLPolicy
aclRules []tailcfg.FilterRule
lastStateChange sync.Map
oidcProvider *oidc.Provider
oauth2Config *oauth2.Config
oidcStateCache *cache.Cache
oidcProvider *oidc.Provider
oauth2Config *oauth2.Config
requestedExpiryCache *cache.Cache
registrationCache *cache.Cache
ipAllocationMutex sync.Mutex
}
@@ -178,7 +185,6 @@ func LookupTLSClientAuthMode(mode string) (tls.ClientAuthType, bool) {
}
}
// NewHeadscale returns the Headscale app.
func NewHeadscale(cfg Config) (*Headscale, error) {
privKey, err := readOrCreatePrivateKey(cfg.PrivateKeyPath)
if err != nil {
@@ -202,18 +208,18 @@ func NewHeadscale(cfg Config) (*Headscale, error) {
return nil, errUnsupportedDatabase
}
requestedExpiryCache := cache.New(
requestedExpiryCacheExpiration,
requestedExpiryCacheCleanupInterval,
registrationCache := cache.New(
registerCacheExpiration,
registerCacheCleanup,
)
app := Headscale{
cfg: cfg,
dbType: cfg.DBtype,
dbString: dbString,
privateKey: privKey,
aclRules: tailcfg.FilterAllowAll, // default allowall
requestedExpiryCache: requestedExpiryCache,
cfg: cfg,
dbType: cfg.DBtype,
dbString: dbString,
privateKey: privKey,
aclRules: tailcfg.FilterAllowAll, // default allowall
registrationCache: registrationCache,
}
err = app.initDB()
@@ -239,6 +245,14 @@ func NewHeadscale(cfg Config) (*Headscale, error) {
}
}
if cfg.DERP.ServerEnabled {
embeddedDERPServer, err := app.NewDERPServer()
if err != nil {
return nil, err
}
app.DERPServer = embeddedDERPServer
}
return &app, nil
}
@@ -395,8 +409,6 @@ func (h *Headscale) httpAuthenticationMiddleware(ctx *gin.Context) {
return
}
ctx.AbortWithStatus(http.StatusUnauthorized)
valid, err := h.ValidateAPIKey(strings.TrimPrefix(authHeader, AuthPrefix))
if err != nil {
log.Error().
@@ -434,11 +446,17 @@ func (h *Headscale) ensureUnixSocketIsAbsent() error {
return os.Remove(h.cfg.UnixSocket)
}
func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *gin.Engine {
router := gin.Default()
func (h *Headscale) createPrometheusRouter() *gin.Engine {
promRouter := gin.Default()
prometheus := ginprometheus.NewPrometheus("gin")
prometheus.Use(router)
prometheus.Use(promRouter)
return promRouter
}
func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *gin.Engine {
router := gin.Default()
router.GET(
"/health",
@@ -450,11 +468,19 @@ func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *gin.Engine {
router.POST("/machine/:id", h.RegistrationHandler)
router.GET("/oidc/register/:mkey", h.RegisterOIDC)
router.GET("/oidc/callback", h.OIDCCallback)
router.GET("/apple", h.AppleMobileConfig)
router.GET("/apple", h.AppleConfigMessage)
router.GET("/apple/:platform", h.ApplePlatformConfig)
router.GET("/windows", h.WindowsConfigMessage)
router.GET("/windows/tailscale.reg", h.WindowsRegConfig)
router.GET("/swagger", SwaggerUI)
router.GET("/swagger/v1/openapiv2.json", SwaggerAPIv1)
if h.cfg.DERP.ServerEnabled {
router.Any("/derp", h.DERPHandler)
router.Any("/derp/probe", h.DERPProbeHandler)
router.Any("/bootstrap-dns", h.DERPBootstrapDNSHandler)
}
api := router.Group("/api")
api.Use(h.httpAuthenticationMiddleware)
{
@@ -473,6 +499,13 @@ func (h *Headscale) Serve() error {
// Fetch an initial DERP Map before we start serving
h.DERPMap = GetDERPMap(h.cfg.DERP)
if h.cfg.DERP.ServerEnabled {
h.DERPMap.Regions[h.DERPServer.region.RegionID] = &h.DERPServer.region
if h.cfg.DERP.STUNEnabled {
go h.ServeSTUN()
}
}
if h.cfg.DERP.AutoUpdate {
derpMapCancelChannel := make(chan struct{})
defer func() { derpMapCancelChannel <- struct{}{} }()
@@ -650,6 +683,27 @@ func (h *Headscale) Serve() error {
log.Info().
Msgf("listening and serving HTTP on: %s", h.cfg.Addr)
promRouter := h.createPrometheusRouter()
promHTTPServer := &http.Server{
Addr: h.cfg.MetricsAddr,
Handler: promRouter,
ReadTimeout: HTTPReadTimeout,
WriteTimeout: 0,
}
var promHTTPListener net.Listener
promHTTPListener, err = net.Listen("tcp", h.cfg.MetricsAddr)
if err != nil {
return fmt.Errorf("failed to bind to TCP address: %w", err)
}
errorGroup.Go(func() error { return promHTTPServer.Serve(promHTTPListener) })
log.Info().
Msgf("listening and serving metrics on: %s", h.cfg.MetricsAddr)
return errorGroup.Wait()
}

View File

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

View File

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

View File

@@ -36,13 +36,15 @@ func init() {
}
var apiKeysCmd = &cobra.Command{
Use: "apikeys",
Short: "Handle the Api keys in Headscale",
Use: "apikeys",
Short: "Handle the Api keys in Headscale",
Aliases: []string{"apikey", "api"},
}
var listAPIKeys = &cobra.Command{
Use: "list",
Short: "List the Api keys for headscale",
Use: "list",
Short: "List the Api keys for headscale",
Aliases: []string{"ls", "show"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
@@ -107,6 +109,7 @@ var createAPIKeyCmd = &cobra.Command{
Creates a new Api key, the Api key is only visible on creation
and cannot be retrieved again.
If you loose a key, create a new one and revoke (expire) the old one.`,
Aliases: []string{"c", "new"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
@@ -144,7 +147,7 @@ If you loose a key, create a new one and revoke (expire) the old one.`,
var expireAPIKeyCmd = &cobra.Command{
Use: "expire",
Short: "Expire an ApiKey",
Aliases: []string{"revoke"},
Aliases: []string{"revoke", "exp", "e"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")

View File

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

View File

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

View File

@@ -49,8 +49,9 @@ func init() {
}
var nodeCmd = &cobra.Command{
Use: "nodes",
Short: "Manage the nodes of Headscale",
Use: "nodes",
Short: "Manage the nodes of Headscale",
Aliases: []string{"node", "machine", "machines"},
}
var registerNodeCmd = &cobra.Command{
@@ -104,8 +105,9 @@ var registerNodeCmd = &cobra.Command{
}
var listNodesCmd = &cobra.Command{
Use: "list",
Short: "List nodes",
Use: "list",
Short: "List nodes",
Aliases: []string{"ls", "show"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
namespace, err := cmd.Flags().GetString("namespace")
@@ -164,7 +166,7 @@ var expireNodeCmd = &cobra.Command{
Use: "expire",
Short: "Expire (log out) a machine in your network",
Long: "Expiring a node will keep the node in the database and force it to reauthenticate.",
Aliases: []string{"logout"},
Aliases: []string{"logout", "exp", "e"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
@@ -206,8 +208,9 @@ var expireNodeCmd = &cobra.Command{
}
var deleteNodeCmd = &cobra.Command{
Use: "delete",
Short: "Delete a node",
Use: "delete",
Short: "Delete a node",
Aliases: []string{"del"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")

View File

@@ -35,13 +35,15 @@ func init() {
}
var preauthkeysCmd = &cobra.Command{
Use: "preauthkeys",
Short: "Handle the preauthkeys in Headscale",
Use: "preauthkeys",
Short: "Handle the preauthkeys in Headscale",
Aliases: []string{"preauthkey", "authkey", "pre"},
}
var listPreAuthKeys = &cobra.Command{
Use: "list",
Short: "List the preauthkeys for this namespace",
Use: "list",
Short: "List the preauthkeys for this namespace",
Aliases: []string{"ls", "show"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
@@ -118,8 +120,9 @@ var listPreAuthKeys = &cobra.Command{
}
var createPreAuthKeyCmd = &cobra.Command{
Use: "create",
Short: "Creates a new preauthkey in the specified namespace",
Use: "create",
Short: "Creates a new preauthkey in the specified namespace",
Aliases: []string{"c", "new"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
@@ -172,8 +175,9 @@ var createPreAuthKeyCmd = &cobra.Command{
}
var expirePreAuthKeyCmd = &cobra.Command{
Use: "expire KEY",
Short: "Expire a preauthkey",
Use: "expire KEY",
Short: "Expire a preauthkey",
Aliases: []string{"revoke", "exp", "e"},
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return errMissingParameter

View File

@@ -35,13 +35,15 @@ func init() {
}
var routesCmd = &cobra.Command{
Use: "routes",
Short: "Manage the routes of Headscale",
Use: "routes",
Short: "Manage the routes of Headscale",
Aliases: []string{"r", "route"},
}
var listRoutesCmd = &cobra.Command{
Use: "list",
Short: "List routes advertised and enabled by a given node",
Use: "list",
Short: "List routes advertised and enabled by a given node",
Aliases: []string{"ls", "show"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")

View File

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

View File

@@ -117,6 +117,13 @@ func LoadConfig(path string) error {
}
func GetDERPConfig() headscale.DERPConfig {
serverEnabled := viper.GetBool("derp.server.enabled")
serverRegionID := viper.GetInt("derp.server.region_id")
serverRegionCode := viper.GetString("derp.server.region_code")
serverRegionName := viper.GetString("derp.server.region_name")
stunEnabled := viper.GetBool("derp.server.stun.enabled")
stunAddr := viper.GetString("derp.server.stun.listen_addr")
urlStrs := viper.GetStringSlice("derp.urls")
urls := make([]url.URL, len(urlStrs))
@@ -138,10 +145,16 @@ func GetDERPConfig() headscale.DERPConfig {
updateFrequency := viper.GetDuration("derp.update_frequency")
return headscale.DERPConfig{
URLs: urls,
Paths: paths,
AutoUpdate: autoUpdate,
UpdateFrequency: updateFrequency,
ServerEnabled: serverEnabled,
ServerRegionID: serverRegionID,
ServerRegionCode: serverRegionCode,
ServerRegionName: serverRegionName,
STUNEnabled: stunEnabled,
STUNAddr: stunAddr,
URLs: urls,
Paths: paths,
AutoUpdate: autoUpdate,
UpdateFrequency: updateFrequency,
}
}
@@ -304,6 +317,7 @@ func getHeadscaleConfig() headscale.Config {
return headscale.Config{
ServerURL: viper.GetString("server_url"),
Addr: viper.GetString("listen_addr"),
MetricsAddr: viper.GetString("metrics_listen_addr"),
GRPCAddr: viper.GetString("grpc_listen_addr"),
GRPCAllowInsecure: viper.GetBool("grpc_allow_insecure"),

View File

@@ -55,6 +55,7 @@ func (*Suite) TestConfigLoading(c *check.C) {
// Test that config file was interpreted correctly
c.Assert(viper.GetString("server_url"), check.Equals, "http://127.0.0.1:8080")
c.Assert(viper.GetString("listen_addr"), check.Equals, "0.0.0.0:8080")
c.Assert(viper.GetString("metrics_listen_addr"), check.Equals, "127.0.0.1:9090")
c.Assert(viper.GetString("db_type"), check.Equals, "sqlite3")
c.Assert(viper.GetString("db_path"), check.Equals, "/var/lib/headscale/db.sqlite")
c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "")

View File

@@ -16,6 +16,12 @@ server_url: http://127.0.0.1:8080
#
listen_addr: 0.0.0.0:8080
# Address to listen to /metrics, you may want
# to keep this endpoint private to your internal
# network
#
metrics_listen_addr: 127.0.0.1:9090
# Address to listen for gRPC.
# gRPC is used for controlling a headscale server
# remotely with the CLI
@@ -49,6 +55,26 @@ ip_prefixes:
# headscale needs a list of DERP servers that can be presented
# to the clients.
derp:
server:
# If enabled, runs the embedded DERP server and merges it into the rest of the DERP config
# The Headscale server_url defined above MUST be using https, DERP requires TLS to be in place
enabled: false
# Region ID to use for the embedded DERP server.
# The local DERP prevails if the region ID collides with other region ID coming from
# the regular DERP config.
region_id: 999
# Region code and name are displayed in the Tailscale UI to identify a DERP region
region_code: "headscale"
region_name: "Headscale Embedded DERP"
# If enabled, also listens in UDP at the configured address for STUN connections to help on NAT traversal
# For more details on how this works, check this great article: https://tailscale.com/blog/how-tailscale-works/
stun:
enabled: false
listen_addr: "0.0.0.0:3478"
# List of externally available DERP maps encoded in JSON
urls:
- https://controlplane.tailscale.com/derpmap/default
@@ -117,7 +143,7 @@ tls_client_auth_mode: relaxed
tls_letsencrypt_cache_dir: /var/lib/headscale/cache
# Type of ACME challenge to use, currently supported types:
# HTTP-01 or TLS_ALPN-01
# HTTP-01 or TLS-ALPN-01
# See [docs/tls.md](docs/tls.md) for more information
tls_letsencrypt_challenge_type: HTTP-01
# When HTTP-01 challenge is chosen, letsencrypt must set up a
@@ -132,7 +158,8 @@ tls_key_path: ""
log_level: info
# Path to a file containg ACL policies.
# Recommended path: /etc/headscale/acl.hujson
# ACLs can be defined as YAML or HUJSON.
# https://tailscale.com/kb/1018/acls/
acl_policy_path: ""
## DNS

109
db.go
View File

@@ -1,13 +1,19 @@
package headscale
import (
"database/sql/driver"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/glebarez/sqlite"
"github.com/rs/zerolog/log"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"inet.af/netaddr"
"tailscale.com/tailcfg"
)
const (
@@ -34,6 +40,38 @@ func (h *Headscale) initDB() error {
_ = db.Migrator().RenameColumn(&Machine{}, "ip_address", "ip_addresses")
// If the Machine table has a column for registered,
// find all occourences of "false" and drop them. Then
// remove the column.
if db.Migrator().HasColumn(&Machine{}, "registered") {
log.Info().
Msg(`Database has legacy "registered" column in machine, removing...`)
machines := Machines{}
if err := h.db.Not("registered").Find(&machines).Error; err != nil {
log.Error().Err(err).Msg("Error accessing db")
}
for _, machine := range machines {
log.Info().
Str("machine", machine.Name).
Str("machine_key", machine.MachineKey).
Msg("Deleting unregistered machine")
if err := h.db.Delete(&Machine{}, machine.ID).Error; err != nil {
log.Error().
Err(err).
Str("machine", machine.Name).
Str("machine_key", machine.MachineKey).
Msg("Error deleting unregistered machine")
}
}
err := db.Migrator().DropColumn(&Machine{}, "registered")
if err != nil {
log.Error().Err(err).Msg("Error dropping registered column")
}
}
err = db.AutoMigrate(&Machine{})
if err != nil {
return err
@@ -141,3 +179,74 @@ func (h *Headscale) setValue(key string, value string) error {
return nil
}
// This is a "wrapper" type around tailscales
// Hostinfo to allow us to add database "serialization"
// methods. This allows us to use a typed values throughout
// the code and not have to marshal/unmarshal and error
// check all over the code.
type HostInfo tailcfg.Hostinfo
func (hi *HostInfo) Scan(destination interface{}) error {
switch value := destination.(type) {
case []byte:
return json.Unmarshal(value, hi)
case string:
return json.Unmarshal([]byte(value), hi)
default:
return fmt.Errorf("%w: unexpected data type %T", errMachineAddressesInvalid, destination)
}
}
// Value return json value, implement driver.Valuer interface.
func (hi HostInfo) Value() (driver.Value, error) {
bytes, err := json.Marshal(hi)
return string(bytes), err
}
type IPPrefixes []netaddr.IPPrefix
func (i *IPPrefixes) Scan(destination interface{}) error {
switch value := destination.(type) {
case []byte:
return json.Unmarshal(value, i)
case string:
return json.Unmarshal([]byte(value), i)
default:
return fmt.Errorf("%w: unexpected data type %T", errMachineAddressesInvalid, destination)
}
}
// Value return json value, implement driver.Valuer interface.
func (i IPPrefixes) Value() (driver.Value, error) {
bytes, err := json.Marshal(i)
return string(bytes), err
}
type StringList []string
func (i *StringList) Scan(destination interface{}) error {
switch value := destination.(type) {
case []byte:
return json.Unmarshal(value, i)
case string:
return json.Unmarshal([]byte(value), i)
default:
return fmt.Errorf("%w: unexpected data type %T", errMachineAddressesInvalid, destination)
}
}
// Value return json value, implement driver.Valuer interface.
func (i StringList) Value() (driver.Value, error) {
bytes, err := json.Marshal(i)
return string(bytes), err
}

View File

@@ -148,6 +148,7 @@ func (h *Headscale) scheduledDERPMapUpdateWorker(cancelChan <-chan struct{}) {
case <-ticker.C:
log.Info().Msg("Fetching DERPMap updates")
h.DERPMap = GetDERPMap(h.cfg.DERP)
h.DERPMap.Regions[h.DERPServer.region.RegionID] = &h.DERPServer.region
namespaces, err := h.ListNamespaces()
if err != nil {

233
derp_server.go Normal file
View File

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

View File

@@ -164,7 +164,6 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
Name: "test_get_shared_nodes_1",
NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")},
AuthKeyID: uint(preAuthKeyInShared1.ID),
@@ -182,7 +181,6 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
Name: "test_get_shared_nodes_2",
NamespaceID: namespaceShared2.ID,
Namespace: *namespaceShared2,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.2")},
AuthKeyID: uint(preAuthKeyInShared2.ID),
@@ -200,7 +198,6 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
Name: "test_get_shared_nodes_3",
NamespaceID: namespaceShared3.ID,
Namespace: *namespaceShared3,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.3")},
AuthKeyID: uint(preAuthKeyInShared3.ID),
@@ -218,7 +215,6 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
Name: "test_get_shared_nodes_4",
NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")},
AuthKeyID: uint(PreAuthKey2InShared1.ID),
@@ -311,7 +307,6 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
Name: "test_get_shared_nodes_1",
NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")},
AuthKeyID: uint(preAuthKeyInShared1.ID),
@@ -329,7 +324,6 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
Name: "test_get_shared_nodes_2",
NamespaceID: namespaceShared2.ID,
Namespace: *namespaceShared2,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.2")},
AuthKeyID: uint(preAuthKeyInShared2.ID),
@@ -347,7 +341,6 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
Name: "test_get_shared_nodes_3",
NamespaceID: namespaceShared3.ID,
Namespace: *namespaceShared3,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.3")},
AuthKeyID: uint(preAuthKeyInShared3.ID),
@@ -365,7 +358,6 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
Name: "test_get_shared_nodes_4",
NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")},
AuthKeyID: uint(preAuthKey2InShared1.ID),

View File

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

View File

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

View File

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

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

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

View File

@@ -85,13 +85,12 @@ type Machine struct {
IpAddresses []string `protobuf:"bytes,5,rep,name=ip_addresses,json=ipAddresses,proto3" json:"ip_addresses,omitempty"`
Name string `protobuf:"bytes,6,opt,name=name,proto3" json:"name,omitempty"`
Namespace *Namespace `protobuf:"bytes,7,opt,name=namespace,proto3" json:"namespace,omitempty"`
Registered bool `protobuf:"varint,8,opt,name=registered,proto3" json:"registered,omitempty"`
RegisterMethod RegisterMethod `protobuf:"varint,9,opt,name=register_method,json=registerMethod,proto3,enum=headscale.v1.RegisterMethod" json:"register_method,omitempty"`
LastSeen *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=last_seen,json=lastSeen,proto3" json:"last_seen,omitempty"`
LastSuccessfulUpdate *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=last_successful_update,json=lastSuccessfulUpdate,proto3" json:"last_successful_update,omitempty"`
Expiry *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=expiry,proto3" json:"expiry,omitempty"`
PreAuthKey *PreAuthKey `protobuf:"bytes,13,opt,name=pre_auth_key,json=preAuthKey,proto3" json:"pre_auth_key,omitempty"`
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,14,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
LastSeen *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=last_seen,json=lastSeen,proto3" json:"last_seen,omitempty"`
LastSuccessfulUpdate *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=last_successful_update,json=lastSuccessfulUpdate,proto3" json:"last_successful_update,omitempty"`
Expiry *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=expiry,proto3" json:"expiry,omitempty"`
PreAuthKey *PreAuthKey `protobuf:"bytes,11,opt,name=pre_auth_key,json=preAuthKey,proto3" json:"pre_auth_key,omitempty"`
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
RegisterMethod RegisterMethod `protobuf:"varint,13,opt,name=register_method,json=registerMethod,proto3,enum=headscale.v1.RegisterMethod" json:"register_method,omitempty"`
}
func (x *Machine) Reset() {
@@ -175,20 +174,6 @@ func (x *Machine) GetNamespace() *Namespace {
return nil
}
func (x *Machine) GetRegistered() bool {
if x != nil {
return x.Registered
}
return false
}
func (x *Machine) GetRegisterMethod() RegisterMethod {
if x != nil {
return x.RegisterMethod
}
return RegisterMethod_REGISTER_METHOD_UNSPECIFIED
}
func (x *Machine) GetLastSeen() *timestamppb.Timestamp {
if x != nil {
return x.LastSeen
@@ -224,6 +209,13 @@ func (x *Machine) GetCreatedAt() *timestamppb.Timestamp {
return nil
}
func (x *Machine) GetRegisterMethod() RegisterMethod {
if x != nil {
return x.RegisterMethod
}
return RegisterMethod_REGISTER_METHOD_UNSPECIFIED
}
type RegisterMachineRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -822,7 +814,7 @@ var file_headscale_v1_machine_proto_rawDesc = []byte{
0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70,
0x61, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1d, 0x68, 0x65, 0x61, 0x64, 0x73,
0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x65, 0x61, 0x75, 0x74, 0x68, 0x6b,
0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xfd, 0x04, 0x0a, 0x07, 0x4d, 0x61, 0x63,
0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xdd, 0x04, 0x0a, 0x07, 0x4d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04,
0x52, 0x02, 0x69, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f,
0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69,
@@ -836,33 +828,31 @@ var file_headscale_v1_machine_proto_rawDesc = []byte{
0x6e, 0x61, 0x6d, 0x65, 0x12, 0x35, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63,
0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63,
0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65,
0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x72,
0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52,
0x0a, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x12, 0x45, 0x0a, 0x0f, 0x72,
0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x09,
0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65,
0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x68,
0x6f, 0x64, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x68,
0x6f, 0x64, 0x12, 0x37, 0x0a, 0x09, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x18,
0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
0x70, 0x52, 0x08, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x12, 0x50, 0x0a, 0x16, 0x6c,
0x61, 0x73, 0x74, 0x5f, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x66, 0x75, 0x6c, 0x5f, 0x75,
0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f,
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69,
0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x14, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x75, 0x63,
0x63, 0x65, 0x73, 0x73, 0x66, 0x75, 0x6c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x32, 0x0a,
0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72,
0x79, 0x12, 0x3a, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6b, 0x65,
0x79, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63,
0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65,
0x79, 0x52, 0x0a, 0x70, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x12, 0x39, 0x0a,
0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28,
0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63,
0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0x48, 0x0a, 0x16, 0x52, 0x65, 0x67, 0x69,
0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x37, 0x0a, 0x09, 0x6c,
0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a,
0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x08, 0x6c, 0x61, 0x73, 0x74,
0x53, 0x65, 0x65, 0x6e, 0x12, 0x50, 0x0a, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x75, 0x63,
0x63, 0x65, 0x73, 0x73, 0x66, 0x75, 0x6c, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x09,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70,
0x52, 0x14, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x66, 0x75, 0x6c,
0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x32, 0x0a, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79,
0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,
0x6d, 0x70, 0x52, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, 0x3a, 0x0a, 0x0c, 0x70, 0x72,
0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x18, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e,
0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x0a, 0x70, 0x72, 0x65, 0x41,
0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65,
0x64, 0x5f, 0x61, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d,
0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41,
0x74, 0x12, 0x45, 0x0a, 0x0f, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x5f, 0x6d, 0x65,
0x74, 0x68, 0x6f, 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x68, 0x65, 0x61,
0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74,
0x65, 0x72, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74,
0x65, 0x72, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x22, 0x48, 0x0a, 0x16, 0x52, 0x65, 0x67, 0x69,
0x73, 0x74, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65,
@@ -962,12 +952,12 @@ var file_headscale_v1_machine_proto_goTypes = []interface{}{
}
var file_headscale_v1_machine_proto_depIdxs = []int32{
14, // 0: headscale.v1.Machine.namespace:type_name -> headscale.v1.Namespace
0, // 1: headscale.v1.Machine.register_method:type_name -> headscale.v1.RegisterMethod
15, // 2: headscale.v1.Machine.last_seen:type_name -> google.protobuf.Timestamp
15, // 3: headscale.v1.Machine.last_successful_update:type_name -> google.protobuf.Timestamp
15, // 4: headscale.v1.Machine.expiry:type_name -> google.protobuf.Timestamp
16, // 5: headscale.v1.Machine.pre_auth_key:type_name -> headscale.v1.PreAuthKey
15, // 6: headscale.v1.Machine.created_at:type_name -> google.protobuf.Timestamp
15, // 1: headscale.v1.Machine.last_seen:type_name -> google.protobuf.Timestamp
15, // 2: headscale.v1.Machine.last_successful_update:type_name -> google.protobuf.Timestamp
15, // 3: headscale.v1.Machine.expiry:type_name -> google.protobuf.Timestamp
16, // 4: headscale.v1.Machine.pre_auth_key:type_name -> headscale.v1.PreAuthKey
15, // 5: headscale.v1.Machine.created_at:type_name -> google.protobuf.Timestamp
0, // 6: headscale.v1.Machine.register_method:type_name -> headscale.v1.RegisterMethod
1, // 7: headscale.v1.RegisterMachineResponse.machine:type_name -> headscale.v1.Machine
1, // 8: headscale.v1.GetMachineResponse.machine:type_name -> headscale.v1.Machine
1, // 9: headscale.v1.ExpireMachineResponse.machine:type_name -> headscale.v1.Machine

View File

@@ -885,12 +885,6 @@
"namespace": {
"$ref": "#/definitions/v1Namespace"
},
"registered": {
"type": "boolean"
},
"registerMethod": {
"$ref": "#/definitions/v1RegisterMethod"
},
"lastSeen": {
"type": "string",
"format": "date-time"
@@ -909,6 +903,9 @@
"createdAt": {
"type": "string",
"format": "date-time"
},
"registerMethod": {
"$ref": "#/definitions/v1RegisterMethod"
}
}
},

37
go.mod
View File

@@ -13,12 +13,12 @@ require (
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.3
github.com/infobloxopen/protoc-gen-gorm v1.1.0
github.com/klauspost/compress v1.14.2
github.com/klauspost/compress v1.14.4
github.com/ory/dockertest/v3 v3.8.1
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/philip-bui/grpc-zerolog v1.0.1
github.com/prometheus/client_golang v1.12.1
github.com/pterm/pterm v0.12.36
github.com/pterm/pterm v0.12.37
github.com/rs/zerolog v1.26.1
github.com/spf13/cobra v1.3.0
github.com/spf13/viper v1.10.1
@@ -27,38 +27,41 @@ require (
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
github.com/zsais/go-gin-prometheus v0.1.0
golang.org/x/crypto v0.0.0-20220214200702-86341886e292
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
google.golang.org/genproto v0.0.0-20220210181026-6fee9acbd336
google.golang.org/genproto v0.0.0-20220228195345-15d65a4533f7
google.golang.org/grpc v1.44.0
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0
google.golang.org/protobuf v1.27.1
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
gopkg.in/yaml.v2 v2.4.0
gorm.io/datatypes v1.0.5
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
gorm.io/driver/postgres v1.3.1
gorm.io/gorm v1.23.1
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6
tailscale.com v1.20.4
tailscale.com v1.22.0
)
require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // 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/atomicgo/cursor v0.0.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/ccding/go-stun/stun v0.0.0-20200514191101-4dc67bcdb029 // indirect
github.com/cenkalti/backoff/v4 v4.1.2 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/containerd/continuity v0.2.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/denisenkom/go-mssqldb v0.12.0 // indirect
github.com/docker/cli v20.10.12+incompatible // indirect
github.com/docker/docker v20.10.12+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.14.7 // indirect
github.com/glebarez/go-sqlite v1.14.8 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.10.0 // indirect
@@ -92,13 +95,14 @@ require (
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/lib/pq v1.10.3 // indirect
github.com/magiconair/properties v1.8.5 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mattn/go-sqlite3 v1.14.11 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
@@ -121,7 +125,7 @@ require (
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/ugorji/go/codec v1.2.6 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
@@ -129,20 +133,17 @@ require (
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
go4.org/mem v0.0.0-20210711025021-927187094b94 // indirect
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 // indirect
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect
golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
google.golang.org/appengine v1.6.7 // indirect
gopkg.in/ini.v1 v1.66.4 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
gorm.io/driver/mysql v1.3.2 // indirect
gorm.io/driver/sqlite v1.3.1 // indirect
gorm.io/driver/sqlserver v1.3.1 // indirect
modernc.org/libc v1.14.3 // indirect
modernc.org/libc v1.14.5 // indirect
modernc.org/mathutil v1.4.1 // indirect
modernc.org/memory v1.0.5 // indirect
modernc.org/sqlite v1.14.5 // indirect
modernc.org/sqlite v1.14.7 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)

85
go.sum
View File

@@ -73,14 +73,17 @@ github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzX
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY=
github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw=
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -105,6 +108,8 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/bufbuild/buf v0.37.0/go.mod h1:lQ1m2HkIaGOFba6w/aC3KYBHhKEOESP3gaAEpS3dAFM=
github.com/ccding/go-stun/stun v0.0.0-20200514191101-4dc67bcdb029 h1:POmUHfxXdeyM8Aomg4tKDcwATCFuW+cYLkj6pwsw9pc=
github.com/ccding/go-stun/stun v0.0.0-20200514191101-4dc67bcdb029/go.mod h1:Rpr5n9cGHYdM3S3IK8ROSUUUYjQOu+MSUCZDcJbYWi8=
github.com/cenkalti/backoff/v4 v4.1.2 h1:6Yo7N8UP2K6LWZnW94DLVSSrbobcWdVzAYOisuDPIFo=
github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -204,6 +209,7 @@ github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYF
github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA=
github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI=
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
@@ -212,8 +218,9 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
github.com/glebarez/go-sqlite v1.14.7 h1:eXrKp59O5eWBfxv2Xfq5d7uex4+clKrOtWfMzzGSkoM=
github.com/glebarez/go-sqlite v1.14.7/go.mod h1:TKAw5tjyB/ocvVht7Xv4772qRAun5CG/xLCEbkDwNUc=
github.com/glebarez/go-sqlite v1.14.8 h1:30RsIS/olgfOMr7SxiCaYhpq50BTteA/CUKaWVOOHYg=
github.com/glebarez/go-sqlite v1.14.8/go.mod h1:gf9QVsKCYMcu+7nd+ZbDqvXnEXEb22qLcqRUQ9XEI34=
github.com/glebarez/sqlite v1.3.5 h1:R9op5nxb9Z10t4VXQSdAVyqRalLhWdLrlaT/iuvOGHI=
github.com/glebarez/sqlite v1.3.5/go.mod h1:ZffEtp/afVhV+jvIzQi8wlYEIkuGAYshr9OPKM/NmQc=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
@@ -412,7 +419,6 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt
github.com/infobloxopen/atlas-app-toolkit v0.24.1-0.20210416193901-4c7518b07e08/go.mod h1:9BTHnpff654rY1J8KxSUOLJ+ZUDn2Vi3mmk26gQDo1M=
github.com/infobloxopen/protoc-gen-gorm v1.1.0 h1:l6JKEkqMTFbtoGIfQmh/aOy7KfljgX4ql772LtG4kas=
github.com/infobloxopen/protoc-gen-gorm v1.1.0/go.mod h1:ohzLmmFMWQztw2RBHunfjKSCjTPUW4JvbgU1Mdazwxg=
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
@@ -434,7 +440,6 @@ github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5W
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
@@ -450,7 +455,6 @@ github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01C
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
github.com/jackc/pgtype v1.9.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgtype v1.9.1/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgtype v1.10.0 h1:ILnBWrRMSXGczYvmkYD6PsYyVFUNLTnIUJHHDLmqk38=
github.com/jackc/pgtype v1.10.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
@@ -458,7 +462,6 @@ github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
github.com/jackc/pgx/v4 v4.14.0/go.mod h1:jT3ibf/A0ZVCp89rtCIN0zCJxcE74ypROmHEZYsG/j8=
github.com/jackc/pgx/v4 v4.14.1/go.mod h1:RgDuE4Z34o7XE92RpLsvFiOEfrAUT0Xt2KxvX73W06M=
github.com/jackc/pgx/v4 v4.15.0 h1:B7dTkXsdILD3MF987WGGCcg+tvLW6bZJdEcqVFeU//w=
github.com/jackc/pgx/v4 v4.15.0/go.mod h1:D/zyOyXiaM1TmVWnOM18p0xdDtdakRBa0RsVGI3U3bw=
@@ -474,8 +477,6 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.3/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
@@ -497,8 +498,8 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.14.2 h1:S0OHlFk/Gbon/yauFJ4FfJJF5V0fc5HbBTJazi28pRw=
github.com/klauspost/compress v1.14.2/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.14.4 h1:eijASRJcobkVtSt81Olfh7JX43osYLwy5krOJo6YEu4=
github.com/klauspost/compress v1.14.4/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
@@ -535,8 +536,9 @@ github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc8
github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
@@ -558,7 +560,6 @@ github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.10/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.11 h1:gt+cp9c0XGqe9S/wAHTL3n/7MqY+siPWgWJgqdsFrzQ=
github.com/mattn/go-sqlite3 v1.14.11/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
@@ -574,6 +575,8 @@ github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceT
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
@@ -671,8 +674,8 @@ github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY
github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE=
github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU=
github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE=
github.com/pterm/pterm v0.12.36 h1:Ui5zZj7xA8lXR0CxWXlKGCQMW1cZVUMOS8jEXs6ur/g=
github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8=
github.com/pterm/pterm v0.12.37 h1:QGOyuaDUmY3yTbP0k6i0uPNqNHA9YofEBQDy0tIyKTA=
github.com/pterm/pterm v0.12.37/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
@@ -762,10 +765,10 @@ github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9
github.com/twitchtv/twirp v7.1.0+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.2.6 h1:7kbGefxLoDBuYXOms4yD7223OpNMMPNPZxXk5TvFcyQ=
github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
@@ -845,8 +848,6 @@ golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20220210151621-f4118a5b28e2 h1:XdAboW3BNMv9ocSCOk/u1MFioZGzCNkiJZ19v9Oe3Ig=
golang.org/x/crypto v0.0.0-20220210151621-f4118a5b28e2/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -940,8 +941,9 @@ golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -959,8 +961,9 @@ golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b h1:clP8eMhB30EHdc0bd2Twtq6kgU7yl5ub2cQLSdrv1Dg=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -1063,9 +1066,10 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 h1:nhht2DYV/Sn3qOayu8lM+cU1ii9sTLUeBQwQQfUHtrs=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -1086,6 +1090,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M=
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -1274,8 +1280,8 @@ google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ6
google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220210181026-6fee9acbd336 h1:RK2ysGpQApbI6U7xn+ROT2rrm08lE/t8AcGqG8XI1CY=
google.golang.org/genproto v0.0.0-20220210181026-6fee9acbd336/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220228195345-15d65a4533f7 h1:ntPPoHzFW6Xp09ueznmahONZufyoSakK/piXnr2BU3I=
google.golang.org/genproto v0.0.0-20220228195345-15d65a4533f7/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -1359,23 +1365,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/datatypes v1.0.5 h1:3vHCfg4Bz8SDx83zE+ASskF+g/j0kWrcKrY9jFUyAl0=
gorm.io/datatypes v1.0.5/go.mod h1:acG/OHGwod+1KrbwPL1t+aavb7jOBOETeyl5M8K5VQs=
gorm.io/driver/mysql v1.2.2/go.mod h1:qsiz+XcAyMrS6QY+X3M9R6b/lKM1imKmcuK9kac5LTo=
gorm.io/driver/mysql v1.2.3 h1:cZqzlOfg5Kf1VIdLC1D9hT6Cy9BgxhExLj/2tIgUe7Y=
gorm.io/driver/mysql v1.2.3/go.mod h1:qsiz+XcAyMrS6QY+X3M9R6b/lKM1imKmcuK9kac5LTo=
gorm.io/driver/mysql v1.3.2 h1:QJryWiqQ91EvZ0jZL48NOpdlPdMjdip1hQ8bTgo4H7I=
gorm.io/driver/mysql v1.3.2/go.mod h1:ChK6AHbHgDCFZyJp0F+BmVGb06PSIoh9uVYKAlRbb2U=
gorm.io/driver/postgres v1.2.3 h1:f4t0TmNMy9gh3TU2PX+EppoA6YsgFnyq8Ojtddb42To=
gorm.io/driver/postgres v1.2.3/go.mod h1:pJV6RgYQPG47aM1f0QeOzFH9HxQc8JcmAgjRCgS0wjs=
gorm.io/driver/postgres v1.3.1 h1:Pyv+gg1Gq1IgsLYytj/S2k7ebII3CzEdpqQkPOdH24g=
gorm.io/driver/postgres v1.3.1/go.mod h1:WwvWOuR9unCLpGWCL6Y3JOeBWvbKi6JLhayiVclSZZU=
gorm.io/driver/sqlite v1.3.1 h1:bwfE+zTEWklBYoEodIOIBwuWHpnx52Z9zJFW5F33WLk=
gorm.io/driver/sqlite v1.3.1/go.mod h1:wJx0hJspfycZ6myN38x1O/AqLtNS6c5o9TndewFbELg=
gorm.io/driver/sqlserver v1.3.1 h1:F5t6ScMzOgy1zukRTIZgLZwKahgt3q1woAILVolKpOI=
gorm.io/driver/sqlserver v1.3.1/go.mod h1:w25Vrx2BG+CJNUu/xKbFhaKlGxT/nzRkhWCCoptX8tQ=
gorm.io/gorm v1.22.3/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
gorm.io/gorm v1.22.4/go.mod h1:1aeVC+pe9ZmvKZban/gW4QPra7PRoTEssyc922qCAkk=
gorm.io/gorm v1.22.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.23.1 h1:aj5IlhDzEPsoIyOPtTRVI+SyaN1u6k613sbt4pwbxG0=
gorm.io/gorm v1.23.1/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
@@ -1450,7 +1441,10 @@ modernc.org/ccgo/v3 v3.14.0/go.mod h1:hBrkiBlUwvr5vV/ZH9YzXIp982jKE8Ek8tR1ytoAL6
modernc.org/ccgo/v3 v3.15.1/go.mod h1:md59wBwDT2LznX/OTCPoVS6KIsdRgY8xqQwBV+hkTH0=
modernc.org/ccgo/v3 v3.15.9/go.mod h1:md59wBwDT2LznX/OTCPoVS6KIsdRgY8xqQwBV+hkTH0=
modernc.org/ccgo/v3 v3.15.10/go.mod h1:wQKxoFn0ynxMuCLfFD09c8XPUCc8obfchoVR9Cn0fI8=
modernc.org/ccgo/v3 v3.15.12/go.mod h1:VFePOWoCd8uDGRJpq/zfJ29D0EVzMSyID8LCMWYbX6I=
modernc.org/ccgo/v3 v3.15.13/go.mod h1:QHtvdpeODlXjdK3tsbpyK+7U9JV4PQsrPGIbtmc0KfY=
modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
modernc.org/ccorpus v1.11.4/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q=
@@ -1494,8 +1488,9 @@ modernc.org/libc v1.13.1/go.mod h1:npFeGWjmZTjFeWALQLrvklVmAxv4m80jnG3+xI8FdJk=
modernc.org/libc v1.13.2/go.mod h1:npFeGWjmZTjFeWALQLrvklVmAxv4m80jnG3+xI8FdJk=
modernc.org/libc v1.14.1/go.mod h1:npFeGWjmZTjFeWALQLrvklVmAxv4m80jnG3+xI8FdJk=
modernc.org/libc v1.14.2/go.mod h1:MX1GBLnRLNdvmK9azU9LCxZ5lMyhrbEMK8rG3X/Fe34=
modernc.org/libc v1.14.3 h1:ruQJ8VDhnWkUR/otUG/Ksw+sWHUw9cPAq6mjDaY/Y7c=
modernc.org/libc v1.14.3/go.mod h1:GPIvQVOVPizzlqyRX3l756/3ppsAgg1QgPxjr5Q4agQ=
modernc.org/libc v1.14.5 h1:DAHvwGoVRDZs5iJXnX9RJrgXSsorupCWmJ2ac964Owk=
modernc.org/libc v1.14.5/go.mod h1:2PJHINagVxO4QW/5OQdRrvMYo+bm5ClpUFfyXCYl9ak=
modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
@@ -1505,10 +1500,12 @@ modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc=
modernc.org/memory v1.0.5 h1:XRch8trV7GgvTec2i7jc33YlUI0RKVDBvZ5eZ5m8y14=
modernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM=
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.14.5 h1:bYrrjwH9Y7QUGk1MbchZDhRfmpGuEAs/D45sVjNbfvs=
modernc.org/sqlite v1.14.5/go.mod h1:YyX5Rx0WbXokitdWl2GJIDy4BrPxBP0PwwhpXOHCDLE=
modernc.org/sqlite v1.14.7 h1:A+6rGjtRQbt9SORXfV+hUyXOP3mDf7J5uz+EES/CNPE=
modernc.org/sqlite v1.14.7/go.mod h1:yiCvMv3HblGmzENNIaNtFhfaNIwcla4u2JQEwJPzfEc=
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
modernc.org/tcl v1.10.0/go.mod h1:WzWapmP/7dHVhFoyPpEaNSVTL8xtewhouN/cqSJ5A2s=
modernc.org/tcl v1.11.0/go.mod h1:zsTUpbQ+NxQEjOjCUlImDLPv1sG8Ww0qp66ZvyOxCgw=
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.2.21/go.mod h1:uXrObx4pGqXWIMliC5MiKuwAyMrltzwpteOFUP1PWCc=
modernc.org/z v1.3.0/go.mod h1:+mvgLH814oDjtATDdT3rs84JnUIpkvAF5B8AVkNlE2g=
@@ -1517,5 +1514,5 @@ rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
tailscale.com v1.20.4 h1:7cl/Q2Sbo2Jb2dX7zA+Exbbl7DT5UGZ4iGhQ2xj23X0=
tailscale.com v1.20.4/go.mod h1:kjVy3ji2OH5lZhPLIIRacoY3CN4Bo3Yyb2mtoM8nfJ4=
tailscale.com v1.22.0 h1:/a1f6eKEl9vL/wGFP8mkhe7O1zDRGtWa9Ft2rGp5N80=
tailscale.com v1.22.0/go.mod h1:D2zuDnjHT7v4aCt71c4+ytQUUAGpnypW+DoubYLaHjg=

View File

@@ -3,12 +3,10 @@ package headscale
import (
"context"
"encoding/json"
"time"
"github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/rs/zerolog/log"
"gorm.io/datatypes"
"tailscale.com/tailcfg"
)
@@ -159,9 +157,11 @@ func (api headscaleV1APIServer) RegisterMachine(
Str("namespace", request.GetNamespace()).
Str("machine_key", request.GetKey()).
Msg("Registering machine")
machine, err := api.h.RegisterMachine(
machine, err := api.h.RegisterMachineFromAuthCallback(
request.GetKey(),
request.GetNamespace(),
RegisterMethodCLI,
)
if err != nil {
return nil, err
@@ -262,13 +262,8 @@ func (api headscaleV1APIServer) GetMachineRoute(
return nil, err
}
routes, err := machine.RoutesToProto()
if err != nil {
return nil, err
}
return &v1.GetMachineRouteResponse{
Routes: routes,
Routes: machine.RoutesToProto(),
}, nil
}
@@ -286,13 +281,8 @@ func (api headscaleV1APIServer) EnableMachineRoutes(
return nil, err
}
routes, err := machine.RoutesToProto()
if err != nil {
return nil, err
}
return &v1.EnableMachineRoutesResponse{
Routes: routes,
Routes: machine.RoutesToProto(),
}, nil
}
@@ -379,13 +369,6 @@ func (api headscaleV1APIServer) DebugCreateMachine(
Hostname: "DebugTestMachine",
}
log.Trace().Caller().Interface("hostinfo", hostinfo).Msg("")
hostinfoJson, err := json.Marshal(hostinfo)
if err != nil {
return nil, err
}
newMachine := Machine{
MachineKey: request.GetKey(),
Name: request.GetName(),
@@ -395,14 +378,14 @@ func (api headscaleV1APIServer) DebugCreateMachine(
LastSeen: &time.Time{},
LastSuccessfulUpdate: &time.Time{},
HostInfo: datatypes.JSON(hostinfoJson),
HostInfo: HostInfo(hostinfo),
}
// log.Trace().Caller().Interface("machine", newMachine).Msg("")
if err := api.h.db.Create(&newMachine).Error; err != nil {
return nil, err
}
api.h.registrationCache.Set(
request.GetKey(),
newMachine,
registerCacheExpiration,
)
return &v1.DebugCreateMachineResponse{Machine: newMachine.toProto()}, nil
}

View File

@@ -621,12 +621,6 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
assert.Equal(s.T(), "machine-4", listAll[3].Name)
assert.Equal(s.T(), "machine-5", listAll[4].Name)
assert.True(s.T(), listAll[0].Registered)
assert.True(s.T(), listAll[1].Registered)
assert.True(s.T(), listAll[2].Registered)
assert.True(s.T(), listAll[3].Registered)
assert.True(s.T(), listAll[4].Registered)
otherNamespaceMachineKeys := []string{
"b5b444774186d4217adcec407563a1223929465ee2c68a4da13af0d0185b4f8e",
"dc721977ac7415aafa87f7d4574cbe07c6b171834a6d37375782bdc1fb6b3584",
@@ -710,9 +704,6 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
assert.Equal(s.T(), "otherNamespace-machine-1", listAllWithotherNamespace[5].Name)
assert.Equal(s.T(), "otherNamespace-machine-2", listAllWithotherNamespace[6].Name)
assert.True(s.T(), listAllWithotherNamespace[5].Registered)
assert.True(s.T(), listAllWithotherNamespace[6].Registered)
// Test list all nodes after added otherNamespace
listOnlyotherNamespaceMachineNamespaceResult, err := ExecuteCommand(
&s.headscale,
@@ -752,9 +743,6 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
listOnlyotherNamespaceMachineNamespace[1].Name,
)
assert.True(s.T(), listOnlyotherNamespaceMachineNamespace[0].Registered)
assert.True(s.T(), listOnlyotherNamespaceMachineNamespace[1].Registered)
// Delete a machines
_, err = ExecuteCommand(
&s.headscale,
@@ -979,7 +967,6 @@ func (s *IntegrationCLITestSuite) TestRouteCommand() {
assert.Equal(s.T(), uint64(1), machine.Id)
assert.Equal(s.T(), "route-machine", machine.Name)
assert.True(s.T(), machine.Registered)
listAllResult, err := ExecuteCommand(
&s.headscale,

View File

@@ -6,6 +6,7 @@ package headscale
import (
"bytes"
"fmt"
"strings"
"time"
"github.com/ory/dockertest/v3"
@@ -18,8 +19,15 @@ const DOCKER_EXECUTE_TIMEOUT = 10 * time.Second
var (
IpPrefix4 = netaddr.MustParseIPPrefix("100.64.0.0/10")
IpPrefix6 = netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/48")
tailscaleVersions = []string{"1.22.0", "1.20.4", "1.18.2", "1.16.2", "1.14.3", "1.12.3"}
)
type TestNamespace struct {
count int
tailscales map[string]dockertest.Resource
}
type ExecuteCommandConfig struct {
timeout time.Duration
}
@@ -119,3 +127,35 @@ func DockerAllowNetworkAdministration(config *docker.HostConfig) {
Target: "/dev/net/tun",
})
}
func getIPs(
tailscales map[string]dockertest.Resource,
) (map[string][]netaddr.IP, error) {
ips := make(map[string][]netaddr.IP)
for hostname, tailscale := range tailscales {
command := []string{"tailscale", "ip"}
result, err := ExecuteCommand(
&tailscale,
command,
[]string{},
)
if err != nil {
return nil, err
}
for _, address := range strings.Split(result, "\n") {
address = strings.TrimSuffix(address, "\n")
if len(address) < 1 {
continue
}
ip, err := netaddr.ParseIP(address)
if err != nil {
return nil, err
}
ips[hostname] = append(ips[hostname], ip)
}
}
return ips, nil
}

View File

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

View File

@@ -29,13 +29,6 @@ import (
"tailscale.com/ipn/ipnstate"
)
var tailscaleVersions = []string{"1.20.4", "1.18.2", "1.16.2", "1.14.3", "1.12.3"}
type TestNamespace struct {
count int
tailscales map[string]dockertest.Resource
}
type IntegrationTestSuite struct {
suite.Suite
stats *suite.SuiteInformation
@@ -687,38 +680,6 @@ func (s *IntegrationTestSuite) TestMagicDNS() {
}
}
func getIPs(
tailscales map[string]dockertest.Resource,
) (map[string][]netaddr.IP, error) {
ips := make(map[string][]netaddr.IP)
for hostname, tailscale := range tailscales {
command := []string{"tailscale", "ip"}
result, err := ExecuteCommand(
&tailscale,
command,
[]string{},
)
if err != nil {
return nil, err
}
for _, address := range strings.Split(result, "\n") {
address = strings.TrimSuffix(address, "\n")
if len(address) < 1 {
continue
}
ip, err := netaddr.ParseIP(address)
if err != nil {
return nil, err
}
ips[hostname] = append(ips[hostname], ip)
}
}
return ips, nil
}
func getAPIURLs(
tailscales map[string]dockertest.Resource,
) (map[netaddr.IP]string, error) {

View File

@@ -14,6 +14,7 @@ dns_config:
db_path: /tmp/integration_test_db.sqlite3
private_key_path: private.key
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 127.0.0.1:9090
server_url: http://headscale:8080
derp:

View File

@@ -0,0 +1,29 @@
log_level: trace
acl_policy_path: ""
db_type: sqlite3
ephemeral_node_inactivity_timeout: 30m
ip_prefixes:
- fd7a:115c:a1e0::/48
- 100.64.0.0/10
dns_config:
base_domain: headscale.net
magic_dns: true
domains: []
nameservers:
- 1.1.1.1
db_path: /tmp/integration_test_db.sqlite3
private_key_path: private.key
listen_addr: 0.0.0.0:8443
server_url: https://headscale:8443
tls_cert_path: "/etc/headscale/tls/server.crt"
tls_key_path: "/etc/headscale/tls/server.key"
tls_client_auth_mode: disabled
derp:
server:
enabled: true
region_id: 999
region_code: "headscale"
region_name: "Headscale Embedded DERP"
stun:
enabled: true
listen_addr: "0.0.0.0:3478"

View File

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

View File

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

View File

@@ -2,7 +2,6 @@ package headscale
import (
"database/sql/driver"
"encoding/json"
"fmt"
"sort"
"strconv"
@@ -13,18 +12,20 @@ import (
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/rs/zerolog/log"
"google.golang.org/protobuf/types/known/timestamppb"
"gorm.io/datatypes"
"inet.af/netaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
const (
errMachineNotFound = Error("machine not found")
errMachineAlreadyRegistered = Error("machine already registered")
errMachineRouteIsNotAvailable = Error("route is not available on machine")
errMachineAddressesInvalid = Error("failed to parse machine addresses")
errHostnameTooLong = Error("Hostname too long")
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")
)
const (
@@ -42,18 +43,19 @@ type Machine struct {
NamespaceID uint
Namespace Namespace `gorm:"foreignKey:NamespaceID"`
Registered bool // temp
RegisterMethod string
AuthKeyID uint
AuthKey *PreAuthKey
// TODO(kradalby): This seems like irrelevant information?
AuthKeyID uint
AuthKey *PreAuthKey
LastSeen *time.Time
LastSuccessfulUpdate *time.Time
Expiry *time.Time
HostInfo datatypes.JSON
Endpoints datatypes.JSON
EnabledRoutes datatypes.JSON
HostInfo HostInfo
Endpoints StringList
EnabledRoutes IPPrefixes
CreatedAt time.Time
UpdatedAt time.Time
@@ -65,11 +67,6 @@ type (
MachinesP []*Machine
)
// For the time being this method is rather naive.
func (machine Machine) isRegistered() bool {
return machine.Registered
}
type MachineAddresses []netaddr.IP
func (ma MachineAddresses) ToStringSlice() []string {
@@ -116,7 +113,7 @@ func (machine Machine) isExpired() bool {
// If Expiry is not set, the client has not indicated that
// it wants an expiry time, it is therefor considered
// to mean "not expired"
if machine.Expiry.IsZero() {
if machine.Expiry == nil || machine.Expiry.IsZero() {
return false
}
@@ -173,6 +170,12 @@ func getFilteredByACLPeers(
machine.IPAddresses.ToStringSlice(),
peer.IPAddresses.ToStringSlice(),
) || // match source and destination
matchSourceAndDestinationWithRule(
rule.SrcIPs,
dst,
peer.IPAddresses.ToStringSlice(),
machine.IPAddresses.ToStringSlice(),
) || // match return path
matchSourceAndDestinationWithRule(
rule.SrcIPs,
dst,
@@ -182,9 +185,21 @@ func getFilteredByACLPeers(
matchSourceAndDestinationWithRule(
rule.SrcIPs,
dst,
[]string{"*"},
[]string{"*"},
) || // match source and all destination
matchSourceAndDestinationWithRule(
rule.SrcIPs,
dst,
[]string{"*"},
peer.IPAddresses.ToStringSlice(),
) || // match source and all destination
matchSourceAndDestinationWithRule(
rule.SrcIPs,
dst,
[]string{"*"},
machine.IPAddresses.ToStringSlice(),
) { // match return path
) { // match all sources and source
peers[peer.ID] = peer
}
}
@@ -214,7 +229,7 @@ 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 <> ? AND registered",
if err := h.db.Preload("AuthKey").Preload("AuthKey.Namespace").Preload("Namespace").Where("machine_key <> ?",
machine.MachineKey).Find(&machines).Error; err != nil {
log.Error().Err(err).Msg("Error accessing db")
@@ -277,7 +292,7 @@ func (h *Headscale) getValidPeers(machine *Machine) (Machines, error) {
}
for _, peer := range peers {
if peer.isRegistered() && !peer.isExpired() {
if !peer.isExpired() {
validPeers = append(validPeers, peer)
}
}
@@ -366,8 +381,6 @@ func (h *Headscale) RefreshMachine(machine *Machine, expiry time.Time) {
// DeleteMachine softs deletes a Machine from the database.
func (h *Headscale) DeleteMachine(machine *Machine) error {
machine.Registered = false
h.db.Save(&machine) // we mark it as unregistered, just in case
if err := h.db.Delete(&machine).Error; err != nil {
return err
}
@@ -393,20 +406,8 @@ func (h *Headscale) HardDeleteMachine(machine *Machine) error {
}
// GetHostInfo returns a Hostinfo struct for the machine.
func (machine *Machine) GetHostInfo() (*tailcfg.Hostinfo, error) {
hostinfo := tailcfg.Hostinfo{}
if len(machine.HostInfo) != 0 {
hi, err := machine.HostInfo.MarshalJSON()
if err != nil {
return nil, err
}
err = json.Unmarshal(hi, &hostinfo)
if err != nil {
return nil, err
}
}
return &hostinfo, nil
func (machine *Machine) GetHostInfo() tailcfg.Hostinfo {
return tailcfg.Hostinfo(machine.HostInfo)
}
func (h *Headscale) isOutdated(machine *Machine) bool {
@@ -536,54 +537,12 @@ func (machine Machine) toNode(
// TODO(kradalby): Needs investigation, We probably dont need this condition
// now that we dont have shared nodes
if includeRoutes {
routesStr := []string{}
if len(machine.EnabledRoutes) != 0 {
allwIps, err := machine.EnabledRoutes.MarshalJSON()
if err != nil {
return nil, err
}
err = json.Unmarshal(allwIps, &routesStr)
if err != nil {
return nil, err
}
}
for _, routeStr := range routesStr {
ip, err := netaddr.ParseIPPrefix(routeStr)
if err != nil {
return nil, err
}
allowedIPs = append(allowedIPs, ip)
}
}
endpoints := []string{}
if len(machine.Endpoints) != 0 {
be, err := machine.Endpoints.MarshalJSON()
if err != nil {
return nil, err
}
err = json.Unmarshal(be, &endpoints)
if err != nil {
return nil, err
}
}
hostinfo := tailcfg.Hostinfo{}
if len(machine.HostInfo) != 0 {
hi, err := machine.HostInfo.MarshalJSON()
if err != nil {
return nil, err
}
err = json.Unmarshal(hi, &hostinfo)
if err != nil {
return nil, err
}
allowedIPs = append(allowedIPs, machine.EnabledRoutes...)
}
var derp string
if hostinfo.NetInfo != nil {
derp = fmt.Sprintf("127.3.3.40:%d", hostinfo.NetInfo.PreferredDERP)
if machine.HostInfo.NetInfo != nil {
derp = fmt.Sprintf("127.3.3.40:%d", machine.HostInfo.NetInfo.PreferredDERP)
} else {
derp = "127.3.3.40:0" // Zero means disconnected or unknown.
}
@@ -614,6 +573,8 @@ func (machine Machine) toNode(
hostname = machine.Name
}
hostInfo := machine.GetHostInfo()
node := tailcfg.Node{
ID: tailcfg.NodeID(machine.ID), // this is the actual ID
StableID: tailcfg.StableNodeID(
@@ -627,15 +588,15 @@ func (machine Machine) toNode(
DiscoKey: discoKey,
Addresses: addrs,
AllowedIPs: allowedIPs,
Endpoints: endpoints,
Endpoints: machine.Endpoints,
DERP: derp,
Hostinfo: hostinfo,
Hostinfo: hostInfo.View(),
Created: machine.CreatedAt,
LastSeen: machine.LastSeen,
KeepAlive: true,
MachineAuthorized: machine.Registered,
MachineAuthorized: !machine.isExpired(),
Capabilities: []string{tailcfg.CapabilityFileSharing},
}
@@ -653,8 +614,6 @@ func (machine *Machine) toProto() *v1.Machine {
Name: machine.Name,
Namespace: machine.Namespace.toProto(),
Registered: machine.Registered,
// TODO(kradalby): Implement register method enum converter
// RegisterMethod: ,
@@ -682,74 +641,50 @@ func (machine *Machine) toProto() *v1.Machine {
return machineProto
}
// RegisterMachine is executed from the CLI to register a new Machine using its MachineKey.
func (h *Headscale) RegisterMachine(
func (h *Headscale) RegisterMachineFromAuthCallback(
machineKeyStr string,
namespaceName string,
registrationMethod string,
) (*Machine, error) {
namespace, err := h.GetNamespace(namespaceName)
if err != nil {
return nil, err
}
if machineInterface, ok := h.registrationCache.Get(machineKeyStr); ok {
if registrationMachine, ok := machineInterface.(Machine); ok {
namespace, err := h.GetNamespace(namespaceName)
if err != nil {
return nil, fmt.Errorf(
"failed to find namespace in register machine from auth callback, %w",
err,
)
}
var machineKey key.MachinePublic
err = machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machineKeyStr)))
if err != nil {
return nil, err
}
registrationMachine.NamespaceID = namespace.ID
registrationMachine.RegisterMethod = registrationMethod
log.Trace().
Caller().
Str("machine_key_str", machineKeyStr).
Str("machine_key", machineKey.String()).
Msg("Registering machine")
machine, err := h.RegisterMachine(
registrationMachine,
)
machine, err := h.GetMachineByMachineKey(machineKey)
if err != nil {
return nil, err
}
// TODO(kradalby): Currently, if it fails to find a requested expiry, non will be set
// This means that if a user is to slow with register a machine, it will possibly not
// have the correct expiry.
requestedTime := time.Time{}
if requestedTimeIf, found := h.requestedExpiryCache.Get(machineKey.String()); found {
log.Trace().
Caller().
Str("machine", machine.Name).
Msg("Expiry time found in cache, assigning to node")
if reqTime, ok := requestedTimeIf.(time.Time); ok {
requestedTime = reqTime
return machine, err
} else {
return nil, errCouldNotConvertMachineInterface
}
}
if machine.isRegistered() {
log.Trace().
Caller().
Str("machine", machine.Name).
Msg("machine already registered, reauthenticating")
return nil, errMachineNotFoundRegistrationCache
}
h.RefreshMachine(machine, requestedTime)
return machine, nil
}
// RegisterMachine is executed from the CLI to register a new Machine using its MachineKey.
func (h *Headscale) RegisterMachine(machine Machine,
) (*Machine, error) {
log.Trace().
Caller().
Str("machine_key", machine.MachineKey).
Msg("Registering machine")
log.Trace().
Caller().
Str("machine", machine.Name).
Msg("Attempting to register machine")
if machine.isRegistered() {
err := errMachineAlreadyRegistered
log.Error().
Caller().
Err(err).
Str("machine", machine.Name).
Msg("Attempting to register machine")
return nil, err
}
h.ipAllocationMutex.Lock()
defer h.ipAllocationMutex.Unlock()
@@ -764,17 +699,8 @@ func (h *Headscale) RegisterMachine(
return nil, err
}
log.Trace().
Caller().
Str("machine", machine.Name).
Str("ip", strings.Join(ips.ToStringSlice(), ",")).
Msg("Found IP for host")
machine.IPAddresses = ips
machine.NamespaceID = namespace.ID
machine.Registered = true
machine.RegisterMethod = RegisterMethodCLI
machine.Expiry = &requestedTime
h.db.Save(&machine)
log.Trace().
@@ -783,40 +709,15 @@ func (h *Headscale) RegisterMachine(
Str("ip", strings.Join(ips.ToStringSlice(), ",")).
Msg("Machine registered with the database")
return machine, nil
return &machine, nil
}
func (machine *Machine) GetAdvertisedRoutes() ([]netaddr.IPPrefix, error) {
hostInfo, err := machine.GetHostInfo()
if err != nil {
return nil, err
}
return hostInfo.RoutableIPs, nil
func (machine *Machine) GetAdvertisedRoutes() []netaddr.IPPrefix {
return machine.HostInfo.RoutableIPs
}
func (machine *Machine) GetEnabledRoutes() ([]netaddr.IPPrefix, error) {
data, err := machine.EnabledRoutes.MarshalJSON()
if err != nil {
return nil, err
}
routesStr := []string{}
err = json.Unmarshal(data, &routesStr)
if err != nil {
return nil, err
}
routes := make([]netaddr.IPPrefix, len(routesStr))
for index, routeStr := range routesStr {
route, err := netaddr.ParseIPPrefix(routeStr)
if err != nil {
return nil, err
}
routes[index] = route
}
return routes, nil
func (machine *Machine) GetEnabledRoutes() []netaddr.IPPrefix {
return machine.EnabledRoutes
}
func (machine *Machine) IsRoutesEnabled(routeStr string) bool {
@@ -825,10 +726,7 @@ func (machine *Machine) IsRoutesEnabled(routeStr string) bool {
return false
}
enabledRoutes, err := machine.GetEnabledRoutes()
if err != nil {
return false
}
enabledRoutes := machine.GetEnabledRoutes()
for _, enabledRoute := range enabledRoutes {
if route == enabledRoute {
@@ -852,13 +750,8 @@ func (h *Headscale) EnableRoutes(machine *Machine, routeStrs ...string) error {
newRoutes[index] = route
}
availableRoutes, err := machine.GetAdvertisedRoutes()
if err != nil {
return err
}
for _, newRoute := range newRoutes {
if !containsIPPrefix(availableRoutes, newRoute) {
if !containsIPPrefix(machine.GetAdvertisedRoutes(), newRoute) {
return fmt.Errorf(
"route (%s) is not available on node %s: %w",
machine.Name,
@@ -867,30 +760,19 @@ func (h *Headscale) EnableRoutes(machine *Machine, routeStrs ...string) error {
}
}
routes, err := json.Marshal(newRoutes)
if err != nil {
return err
}
machine.EnabledRoutes = datatypes.JSON(routes)
machine.EnabledRoutes = newRoutes
h.db.Save(&machine)
return nil
}
func (machine *Machine) RoutesToProto() (*v1.Routes, error) {
availableRoutes, err := machine.GetAdvertisedRoutes()
if err != nil {
return nil, err
}
func (machine *Machine) RoutesToProto() *v1.Routes {
availableRoutes := machine.GetAdvertisedRoutes()
enabledRoutes, err := machine.GetEnabledRoutes()
if err != nil {
return nil, err
}
enabledRoutes := machine.GetEnabledRoutes()
return &v1.Routes{
AdvertisedRoutes: ipPrefixToString(availableRoutes),
EnabledRoutes: ipPrefixToString(enabledRoutes),
}, nil
}
}

View File

@@ -29,16 +29,12 @@ func (s *Suite) TestGetMachine(c *check.C) {
DiscoKey: "faa",
Name: "testmachine",
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
}
app.db.Save(machine)
machineFromDB, err := app.GetMachine("test", "testmachine")
c.Assert(err, check.IsNil)
_, err = machineFromDB.GetHostInfo()
_, err = app.GetMachine("test", "testmachine")
c.Assert(err, check.IsNil)
}
@@ -59,16 +55,12 @@ func (s *Suite) TestGetMachineByID(c *check.C) {
DiscoKey: "faa",
Name: "testmachine",
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
}
app.db.Save(&machine)
machineByID, err := app.GetMachineByID(0)
c.Assert(err, check.IsNil)
_, err = machineByID.GetHostInfo()
_, err = app.GetMachineByID(0)
c.Assert(err, check.IsNil)
}
@@ -82,7 +74,6 @@ func (s *Suite) TestDeleteMachine(c *check.C) {
DiscoKey: "faa",
Name: "testmachine",
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(1),
}
@@ -105,7 +96,6 @@ func (s *Suite) TestHardDeleteMachine(c *check.C) {
DiscoKey: "faa",
Name: "testmachine3",
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(1),
}
@@ -136,7 +126,6 @@ func (s *Suite) TestListPeers(c *check.C) {
DiscoKey: "faa" + strconv.Itoa(index),
Name: "testmachine" + strconv.Itoa(index),
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
}
@@ -146,9 +135,6 @@ func (s *Suite) TestListPeers(c *check.C) {
machine0ByID, err := app.GetMachineByID(0)
c.Assert(err, check.IsNil)
_, err = machine0ByID.GetHostInfo()
c.Assert(err, check.IsNil)
peersOfMachine0, err := app.ListPeers(machine0ByID)
c.Assert(err, check.IsNil)
@@ -188,7 +174,6 @@ func (s *Suite) TestGetACLFilteredPeers(c *check.C) {
},
Name: "testmachine" + strconv.Itoa(index),
NamespaceID: stor[index%2].namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(stor[index%2].key.ID),
}
@@ -219,9 +204,6 @@ func (s *Suite) TestGetACLFilteredPeers(c *check.C) {
c.Logf("Machine(%v), namespace: %v", testMachine.Name, testMachine.Namespace)
c.Assert(err, check.IsNil)
_, err = testMachine.GetHostInfo()
c.Assert(err, check.IsNil)
machines, err := app.ListMachines()
c.Assert(err, check.IsNil)
@@ -258,7 +240,6 @@ func (s *Suite) TestExpireMachine(c *check.C) {
DiscoKey: "faa",
Name: "testmachine",
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
Expiry: &time.Time{},
@@ -296,6 +277,7 @@ func (s *Suite) TestSerdeAddressStrignSlice(c *check.C) {
}
}
// nolint
func Test_getFilteredByACLPeers(t *testing.T) {
type args struct {
machines []Machine
@@ -443,7 +425,7 @@ func Test_getFilteredByACLPeers(t *testing.T) {
},
},
machine: &Machine{ // current machine
ID: 1,
ID: 2,
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
Namespace: Namespace{Name: "marc"},
},
@@ -456,6 +438,208 @@ func Test_getFilteredByACLPeers(t *testing.T) {
},
},
},
{
name: "rules allows all hosts to reach one destination",
args: args{
machines: []Machine{ // list of all machines in the database
{
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
ID: 2,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "marc"},
},
{
ID: 3,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
},
Namespace: Namespace{Name: "mickael"},
},
},
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
{
SrcIPs: []string{"*"},
DstPorts: []tailcfg.NetPortRange{
{IP: "100.64.0.2"},
},
},
},
machine: &Machine{ // current machine
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
},
want: Machines{
{
ID: 2,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "marc"},
},
},
},
{
name: "rules allows all hosts to reach one destination, destination can reach all hosts",
args: args{
machines: []Machine{ // list of all machines in the database
{
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
ID: 2,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "marc"},
},
{
ID: 3,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
},
Namespace: Namespace{Name: "mickael"},
},
},
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
{
SrcIPs: []string{"*"},
DstPorts: []tailcfg.NetPortRange{
{IP: "100.64.0.2"},
},
},
},
machine: &Machine{ // current machine
ID: 2,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "marc"},
},
},
want: Machines{
{
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
ID: 3,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
},
Namespace: Namespace{Name: "mickael"},
},
},
},
{
name: "rule allows all hosts to reach all destinations",
args: args{
machines: []Machine{ // list of all machines in the database
{
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
ID: 2,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "marc"},
},
{
ID: 3,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
},
Namespace: Namespace{Name: "mickael"},
},
},
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
{
SrcIPs: []string{"*"},
DstPorts: []tailcfg.NetPortRange{
{IP: "*"},
},
},
},
machine: &Machine{ // current machine
ID: 2,
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
Namespace: Namespace{Name: "marc"},
},
},
want: Machines{
{
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
ID: 3,
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.3")},
Namespace: Namespace{Name: "mickael"},
},
},
},
{
name: "without rule all communications are forbidden",
args: args{
machines: []Machine{ // list of all machines in the database
{
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
ID: 2,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "marc"},
},
{
ID: 3,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
},
Namespace: Namespace{Name: "mickael"},
},
},
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
},
machine: &Machine{ // current machine
ID: 2,
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
Namespace: Namespace{Name: "marc"},
},
},
want: Machines{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@@ -41,7 +41,7 @@ type Namespace struct {
// CreateNamespace creates a new Namespace. Returns error if could not be created
// or another namespace already exists.
func (h *Headscale) CreateNamespace(name string) (*Namespace, error) {
err := CheckNamespaceName(name)
err := CheckForFQDNRules(name)
if err != nil {
return nil, err
}
@@ -104,7 +104,7 @@ func (h *Headscale) RenameNamespace(oldName, newName string) error {
if err != nil {
return err
}
err = CheckNamespaceName(newName)
err = CheckForFQDNRules(newName)
if err != nil {
return err
}
@@ -150,7 +150,7 @@ func (h *Headscale) ListNamespaces() ([]Namespace, error) {
// ListMachinesInNamespace gets all the nodes in a given namespace.
func (h *Headscale) ListMachinesInNamespace(name string) ([]Machine, error) {
err := CheckNamespaceName(name)
err := CheckForFQDNRules(name)
if err != nil {
return nil, err
}
@@ -169,7 +169,7 @@ func (h *Headscale) ListMachinesInNamespace(name string) ([]Machine, error) {
// SetMachineNamespace assigns a Machine to a namespace.
func (h *Headscale) SetMachineNamespace(machine *Machine, namespaceName string) error {
err := CheckNamespaceName(namespaceName)
err := CheckForFQDNRules(namespaceName)
if err != nil {
return err
}
@@ -237,9 +237,9 @@ func (n *Namespace) toProto() *v1.Namespace {
}
}
// NormalizeNamespaceName will replace forbidden chars in namespace
// NormalizeToFQDNRules will replace forbidden chars in namespace
// it can also return an error if the namespace doesn't respect RFC 952 and 1123.
func NormalizeNamespaceName(name string, stripEmailDomain bool) (string, error) {
func NormalizeToFQDNRules(name string, stripEmailDomain bool) (string, error) {
name = strings.ToLower(name)
name = strings.ReplaceAll(name, "'", "")
atIdx := strings.Index(name, "@")
@@ -263,7 +263,7 @@ func NormalizeNamespaceName(name string, stripEmailDomain bool) (string, error)
return name, nil
}
func CheckNamespaceName(name string) error {
func CheckForFQDNRules(name string) error {
if len(name) > labelHostnameLength {
return fmt.Errorf(
"Namespace must not be over 63 chars. %v doesn't comply with this rule: %w",

View File

@@ -54,7 +54,6 @@ func (s *Suite) TestDestroyNamespaceErrors(c *check.C) {
DiscoKey: "faa",
Name: "testmachine",
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
}
@@ -146,7 +145,6 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
Name: "test_get_shared_nodes_1",
NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")},
AuthKeyID: uint(preAuthKeyShared1.ID),
@@ -164,7 +162,6 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
Name: "test_get_shared_nodes_2",
NamespaceID: namespaceShared2.ID,
Namespace: *namespaceShared2,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.2")},
AuthKeyID: uint(preAuthKeyShared2.ID),
@@ -182,7 +179,6 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
Name: "test_get_shared_nodes_3",
NamespaceID: namespaceShared3.ID,
Namespace: *namespaceShared3,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.3")},
AuthKeyID: uint(preAuthKeyShared3.ID),
@@ -200,7 +196,6 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
Name: "test_get_shared_nodes_4",
NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")},
AuthKeyID: uint(preAuthKey2Shared1.ID),
@@ -238,7 +233,7 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
c.Assert(found, check.Equals, true)
}
func TestNormalizeNamespaceName(t *testing.T) {
func TestNormalizeToFQDNRules(t *testing.T) {
type args struct {
name string
stripEmailDomain bool
@@ -315,10 +310,10 @@ func TestNormalizeNamespaceName(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := NormalizeNamespaceName(tt.args.name, tt.args.stripEmailDomain)
got, err := NormalizeToFQDNRules(tt.args.name, tt.args.stripEmailDomain)
if (err != nil) != tt.wantErr {
t.Errorf(
"NormalizeNamespaceName() error = %v, wantErr %v",
"NormalizeToFQDNRules() error = %v, wantErr %v",
err,
tt.wantErr,
)
@@ -326,13 +321,13 @@ func TestNormalizeNamespaceName(t *testing.T) {
return
}
if got != tt.want {
t.Errorf("NormalizeNamespaceName() = %v, want %v", got, tt.want)
t.Errorf("NormalizeToFQDNRules() = %v, want %v", got, tt.want)
}
})
}
}
func TestCheckNamespaceName(t *testing.T) {
func TestCheckForFQDNRules(t *testing.T) {
type args struct {
name string
}
@@ -371,8 +366,8 @@ func TestCheckNamespaceName(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := CheckNamespaceName(tt.args.name); (err != nil) != tt.wantErr {
t.Errorf("CheckNamespaceName() error = %v, wantErr %v", err, tt.wantErr)
if err := CheckForFQDNRules(tt.args.name); (err != nil) != tt.wantErr {
t.Errorf("CheckForFQDNRules() error = %v, wantErr %v", err, tt.wantErr)
}
})
}

139
oidc.go
View File

@@ -10,21 +10,16 @@ import (
"html/template"
"net/http"
"strings"
"time"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/gin-gonic/gin"
"github.com/patrickmn/go-cache"
"github.com/rs/zerolog/log"
"golang.org/x/oauth2"
"gorm.io/gorm"
"tailscale.com/types/key"
)
const (
oidcStateCacheExpiration = time.Minute * 5
oidcStateCacheCleanupInterval = time.Minute * 10
randomByteSize = 16
randomByteSize = 16
)
type IDTokenClaims struct {
@@ -61,14 +56,6 @@ func (h *Headscale) initOIDC() error {
}
}
// init the state cache if it hasn't been already
if h.oidcStateCache == nil {
h.oidcStateCache = cache.New(
oidcStateCacheExpiration,
oidcStateCacheCleanupInterval,
)
}
return nil
}
@@ -101,7 +88,7 @@ func (h *Headscale) RegisterOIDC(ctx *gin.Context) {
stateStr := hex.EncodeToString(randomBlob)[:32]
// place the machine key into the state cache, so it can be retrieved later
h.oidcStateCache.Set(stateStr, machineKeyStr, oidcStateCacheExpiration)
h.registrationCache.Set(stateStr, machineKeyStr, registerCacheExpiration)
authURL := h.oauth2Config.AuthCodeURL(stateStr)
log.Debug().Msgf("Redirecting to %s for authentication", authURL)
@@ -125,7 +112,6 @@ var oidcCallbackTemplate = template.Must(
</html>`),
)
// TODO: Why is the entire machine registration logic duplicated here?
// OIDCCallback handles the callback from the OIDC endpoint
// Retrieves the mkey from the state cache and adds the machine to the users email namespace
// TODO: A confirmation page for new machines should be added to avoid phishing vulnerabilities
@@ -197,7 +183,7 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
}
// retrieve machinekey from state cache
machineKeyIf, machineKeyFound := h.oidcStateCache.Get(state)
machineKeyIf, machineKeyFound := h.registrationCache.Get(state)
if !machineKeyFound {
log.Error().
@@ -207,10 +193,12 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
return
}
machineKeyStr, machineKeyOK := machineKeyIf.(string)
machineKeyFromCache, machineKeyOK := machineKeyIf.(string)
var machineKey key.MachinePublic
err = machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machineKeyStr)))
err = machineKey.UnmarshalText(
[]byte(MachinePublicKeyEnsurePrefix(machineKeyFromCache)),
)
if err != nil {
log.Error().
Msg("could not parse machine public key")
@@ -229,33 +217,19 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
return
}
// TODO(kradalby): Currently, if it fails to find a requested expiry, non will be set
requestedTime := time.Time{}
if requestedTimeIf, found := h.requestedExpiryCache.Get(machineKey.String()); found {
if reqTime, ok := requestedTimeIf.(time.Time); ok {
requestedTime = reqTime
}
}
// 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)
// retrieve machine information
machine, err := h.GetMachineByMachineKey(machineKey)
if err != nil {
log.Error().Msg("machine key not found in database")
ctx.String(
http.StatusInternalServerError,
"could not get machine info from database",
)
return
}
if machine.isRegistered() {
if machine != nil {
log.Trace().
Caller().
Str("machine", machine.Name).
Msg("machine already registered, reauthenticating")
h.RefreshMachine(machine, requestedTime)
h.RefreshMachine(machine, *machine.Expiry)
var content bytes.Buffer
if err := oidcCallbackTemplate.Execute(&content, oidcCallbackTemplateConfig{
@@ -279,9 +253,7 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
return
}
now := time.Now().UTC()
namespaceName, err := NormalizeNamespaceName(
namespaceName, err := NormalizeToFQDNRules(
claims.Email,
h.cfg.OIDC.StripEmaildomain,
)
@@ -294,61 +266,58 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
return
}
// register the machine if it's new
if !machine.Registered {
log.Debug().Msg("Registering new machine after successful callback")
log.Debug().Msg("Registering new machine after successful callback")
namespace, err := h.GetNamespace(namespaceName)
if errors.Is(err, gorm.ErrRecordNotFound) {
namespace, err = h.CreateNamespace(namespaceName)
namespace, err := h.GetNamespace(namespaceName)
if errors.Is(err, errNamespaceNotFound) {
namespace, err = h.CreateNamespace(namespaceName)
if err != nil {
log.Error().
Err(err).
Caller().
Msgf("could not create new namespace '%s'", namespaceName)
ctx.String(
http.StatusInternalServerError,
"could not create new namespace",
)
return
}
} else if err != nil {
log.Error().
Caller().
Err(err).
Str("namespace", namespaceName).
Msg("could not find or create namespace")
ctx.String(
http.StatusInternalServerError,
"could not find or create namespace",
)
return
}
ips, err := h.getAvailableIPs()
if err != nil {
log.Error().
Caller().
Err(err).
Msg("could not get an IP from the pool")
Caller().
Msgf("could not create new namespace '%s'", namespaceName)
ctx.String(
http.StatusInternalServerError,
"could not get an IP from the pool",
"could not create new namespace",
)
return
}
} else if err != nil {
log.Error().
Caller().
Err(err).
Str("namespace", namespaceName).
Msg("could not find or create namespace")
ctx.String(
http.StatusInternalServerError,
"could not find or create namespace",
)
machine.IPAddresses = ips
machine.NamespaceID = namespace.ID
machine.Registered = true
machine.RegisterMethod = RegisterMethodOIDC
machine.LastSuccessfulUpdate = &now
machine.Expiry = &requestedTime
h.db.Save(&machine)
return
}
machineKeyStr := MachinePublicKeyStripPrefix(machineKey)
_, err = h.RegisterMachineFromAuthCallback(
machineKeyStr,
namespace.Name,
RegisterMethodOIDC,
)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("could not register machine")
ctx.String(
http.StatusInternalServerError,
"could not register machine",
)
return
}
var content bytes.Buffer

View File

@@ -4,33 +4,126 @@ import (
"bytes"
"html/template"
"net/http"
textTemplate "text/template"
"github.com/gin-gonic/gin"
"github.com/gofrs/uuid"
"github.com/rs/zerolog/log"
)
// AppleMobileConfig shows a simple message in the browser to point to the CLI
// Listens in /register.
func (h *Headscale) AppleMobileConfig(ctx *gin.Context) {
// WindowsConfigMessage shows a simple message in the browser for how to configure the Windows Tailscale client.
func (h *Headscale) WindowsConfigMessage(ctx *gin.Context) {
winTemplate := template.Must(template.New("windows").Parse(`
<html>
<body>
<h1>headscale</h1>
<h2>Windows registry configuration</h2>
<p>
This page provides Windows registry information for the official Windows Tailscale client.
<p>
<p>
The registry file will configure Tailscale to use <code>{{.URL}}</code> as its control server.
<p>
<h3>Caution</h3>
<p>You should always download and inspect the registry file before installing it:</p>
<pre><code>curl {{.URL}}/windows/tailscale.reg</code></pre>
<h2>Installation</h2>
<p>Headscale can be set to the default server by running the registry file:</p>
<p>
<a href="/windows/tailscale.reg" download="tailscale.reg">Windows registry file</a>
</p>
<ol>
<li>Download the registry file, then run it</li>
<li>Follow the prompts</li>
<li>Install and run the official windows Tailscale client</li>
<li>When the installation has finished, start Tailscale, and log in by clicking the icon in the system tray</li>
</ol>
<p>Or</p>
<p>Open command prompt with Administrator rights. Issue the following commands to add the required registry entries:</p>
<pre>
<code>REG ADD "HKLM\Software\Tailscale IPN" /v UnattendedMode /t REG_SZ /d always
REG ADD "HKLM\Software\Tailscale IPN" /v LoginURL /t REG_SZ /d "{{.URL}}"</code></pre>
<p>
Restart Tailscale and log in.
<p>
</body>
</html>
`))
config := map[string]interface{}{
"URL": h.cfg.ServerURL,
}
var payload bytes.Buffer
if err := winTemplate.Execute(&payload, config); err != nil {
log.Error().
Str("handler", "WindowsRegConfig").
Err(err).
Msg("Could not render Windows index template")
ctx.Data(
http.StatusInternalServerError,
"text/html; charset=utf-8",
[]byte("Could not render Windows index template"),
)
return
}
ctx.Data(http.StatusOK, "text/html; charset=utf-8", payload.Bytes())
}
// WindowsRegConfig generates and serves a .reg file configured with the Headscale server address.
func (h *Headscale) WindowsRegConfig(ctx *gin.Context) {
config := WindowsRegistryConfig{
URL: h.cfg.ServerURL,
}
var content bytes.Buffer
if err := windowsRegTemplate.Execute(&content, config); err != nil {
log.Error().
Str("handler", "WindowsRegConfig").
Err(err).
Msg("Could not render Apple macOS template")
ctx.Data(
http.StatusInternalServerError,
"text/html; charset=utf-8",
[]byte("Could not render Windows registry template"),
)
return
}
ctx.Data(
http.StatusOK,
"text/x-ms-regedit; charset=utf-8",
content.Bytes(),
)
}
// AppleConfigMessage shows a simple message in the browser to point the user to the iOS/MacOS profile and instructions for how to install it.
func (h *Headscale) AppleConfigMessage(ctx *gin.Context) {
appleTemplate := template.Must(template.New("apple").Parse(`
<html>
<body>
<h1>Apple configuration profiles</h1>
<h1>headscale</h1>
<h2>Apple configuration profiles</h2>
<p>
This page provides <a href="https://support.apple.com/guide/mdm/mdm-overview-mdmbf9e668/web">configuration profiles</a> for the official Tailscale clients for <a href="https://apps.apple.com/us/app/tailscale/id1470499037?ls=1">iOS</a> and <a href="https://apps.apple.com/ca/app/tailscale/id1475387142?mt=12">macOS</a>.
</p>
<p>
The profiles will configure Tailscale.app to use {{.Url}} as its control server.
The profiles will configure Tailscale.app to use <code>{{.URL}}</code> as its control server.
</p>
<h3>Caution</h3>
<p>You should always inspect the profile before installing it:</p>
<p>You should always download and inspect the profile before installing it:</p>
<!--
<p><code>curl {{.Url}}/apple/ios</code></p>
<pre><code>curl {{.URL}}/apple/ios</code></pre>
-->
<p><code>curl {{.Url}}/apple/macos</code></p>
<pre><code>curl {{.URL}}/apple/macos</code></pre>
<h2>Profiles</h2>
<!--
@@ -39,7 +132,7 @@ func (h *Headscale) AppleMobileConfig(ctx *gin.Context) {
<a href="/apple/ios" download="headscale_ios.mobileconfig">iOS profile</a>
</p>
-->
<h3>macOS</h3>
<p>Headscale can be set to the default server by installing a Headscale configuration profile:</p>
<p>
@@ -58,7 +151,7 @@ func (h *Headscale) AppleMobileConfig(ctx *gin.Context) {
<code>defaults write io.tailscale.ipn.macos ControlURL {{.URL}}</code>
<p>Restart Tailscale.app and log in.</p>
</body>
</html>`))
@@ -191,6 +284,10 @@ func (h *Headscale) ApplePlatformConfig(ctx *gin.Context) {
)
}
type WindowsRegistryConfig struct {
URL string
}
type AppleMobileConfig struct {
UUID uuid.UUID
URL string
@@ -202,8 +299,16 @@ type AppleMobilePlatformConfig struct {
URL string
}
var commonTemplate = template.Must(
template.New("mobileconfig").Parse(`<?xml version="1.0" encoding="UTF-8"?>
var windowsRegTemplate = textTemplate.Must(
textTemplate.New("windowsconfig").Parse(`Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Tailscale IPN]
"UnattendedMode"="always"
"LoginURL"="{{.URL}}"
`))
var commonTemplate = textTemplate.Must(
textTemplate.New("mobileconfig").Parse(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
@@ -229,7 +334,7 @@ var commonTemplate = template.Must(
</plist>`),
)
var iosTemplate = template.Must(template.New("iosTemplate").Parse(`
var iosTemplate = textTemplate.Must(textTemplate.New("iosTemplate").Parse(`
<dict>
<key>PayloadType</key>
<string>io.tailscale.ipn.ios</string>

30
poll.go
View File

@@ -2,7 +2,6 @@ package headscale
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
@@ -11,7 +10,6 @@ import (
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"gorm.io/datatypes"
"gorm.io/gorm"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
@@ -85,12 +83,19 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
Str("machine", machine.Name).
Msg("Found machine in database")
hostinfo, err := json.Marshal(req.Hostinfo)
hname, err := NormalizeToFQDNRules(
req.Hostinfo.Hostname,
h.cfg.OIDC.StripEmaildomain,
)
if err != nil {
return
log.Error().
Caller().
Str("func", "handleAuthKey").
Str("hostinfo.name", req.Hostinfo.Hostname).
Err(err)
}
machine.Name = req.Hostinfo.Hostname
machine.HostInfo = datatypes.JSON(hostinfo)
machine.Name = hname
machine.HostInfo = HostInfo(*req.Hostinfo)
machine.DiscoKey = DiscoPublicKeyStripPrefix(req.DiscoKey)
now := time.Now().UTC()
@@ -114,18 +119,7 @@ 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 {
endpoints, err := json.Marshal(req.Endpoints)
if err != nil {
log.Error().
Caller().
Str("func", "PollNetMapHandler").
Err(err).
Msg("Failed to mashal requested endpoints for the client")
ctx.String(http.StatusInternalServerError, ":(")
return
}
machine.Endpoints = datatypes.JSON(endpoints)
machine.Endpoints = req.Endpoints
machine.LastSeen = &now
}
h.db.Updates(machine)

View File

@@ -113,6 +113,12 @@ func (h *Headscale) ExpirePreAuthKey(k *PreAuthKey) error {
return nil
}
// UsePreAuthKey marks a PreAuthKey as used.
func (h *Headscale) UsePreAuthKey(k *PreAuthKey) {
k.Used = true
h.db.Save(k)
}
// checkKeyValidity does the heavy lifting for validation of the PreAuthKey coming from a node
// If returns no error and a PreAuthKey, it can be used.
func (h *Headscale) checkKeyValidity(k string) (*PreAuthKey, error) {

View File

@@ -80,7 +80,6 @@ func (*Suite) TestAlreadyUsedKey(c *check.C) {
DiscoKey: "faa",
Name: "testest",
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
}
@@ -105,7 +104,6 @@ func (*Suite) TestReusableBeingUsedKey(c *check.C) {
DiscoKey: "faa",
Name: "testest",
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
}
@@ -143,7 +141,6 @@ func (*Suite) TestEphemeralKey(c *check.C) {
DiscoKey: "faa",
Name: "testest",
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
LastSeen: &now,
AuthKeyID: uint(pak.ID),

View File

@@ -22,16 +22,16 @@ message Machine {
string name = 6;
Namespace namespace = 7;
bool registered = 8;
RegisterMethod register_method = 9;
google.protobuf.Timestamp last_seen = 10;
google.protobuf.Timestamp last_successful_update = 11;
google.protobuf.Timestamp expiry = 12;
google.protobuf.Timestamp last_seen = 8;
google.protobuf.Timestamp last_successful_update = 9;
google.protobuf.Timestamp expiry = 10;
PreAuthKey pre_auth_key = 13;
PreAuthKey pre_auth_key = 11;
google.protobuf.Timestamp created_at = 14;
google.protobuf.Timestamp created_at = 12;
RegisterMethod register_method = 13;
// google.protobuf.Timestamp updated_at = 14;
// google.protobuf.Timestamp deleted_at = 15;

View File

@@ -1,9 +1,6 @@
package headscale
import (
"encoding/json"
"gorm.io/datatypes"
"inet.af/netaddr"
)
@@ -23,12 +20,7 @@ func (h *Headscale) GetAdvertisedNodeRoutes(
return nil, err
}
hostInfo, err := machine.GetHostInfo()
if err != nil {
return nil, err
}
return &hostInfo.RoutableIPs, nil
return &machine.HostInfo.RoutableIPs, nil
}
// Deprecated: use machine function instead
@@ -43,27 +35,7 @@ func (h *Headscale) GetEnabledNodeRoutes(
return nil, err
}
data, err := machine.EnabledRoutes.MarshalJSON()
if err != nil {
return nil, err
}
routesStr := []string{}
err = json.Unmarshal(data, &routesStr)
if err != nil {
return nil, err
}
routes := make([]netaddr.IPPrefix, len(routesStr))
for index, routeStr := range routesStr {
route, err := netaddr.ParseIPPrefix(routeStr)
if err != nil {
return nil, err
}
routes[index] = route
}
return routes, nil
return machine.EnabledRoutes, nil
}
// Deprecated: use machine function instead
@@ -135,12 +107,7 @@ func (h *Headscale) EnableNodeRoute(
return errRouteIsNotAvailable
}
routes, err := json.Marshal(enabledRoutes)
if err != nil {
return err
}
machine.EnabledRoutes = datatypes.JSON(routes)
machine.EnabledRoutes = enabledRoutes
h.db.Save(&machine)
return nil

View File

@@ -1,10 +1,7 @@
package headscale
import (
"encoding/json"
"gopkg.in/check.v1"
"gorm.io/datatypes"
"inet.af/netaddr"
"tailscale.com/tailcfg"
)
@@ -25,8 +22,6 @@ func (s *Suite) TestGetRoutes(c *check.C) {
hostInfo := tailcfg.Hostinfo{
RoutableIPs: []netaddr.IPPrefix{route},
}
hostinfo, err := json.Marshal(hostInfo)
c.Assert(err, check.IsNil)
machine := Machine{
ID: 0,
@@ -35,10 +30,9 @@ func (s *Suite) TestGetRoutes(c *check.C) {
DiscoKey: "faa",
Name: "test_get_route_machine",
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
HostInfo: datatypes.JSON(hostinfo),
HostInfo: HostInfo(hostInfo),
}
app.db.Save(&machine)
@@ -79,8 +73,6 @@ func (s *Suite) TestGetEnableRoutes(c *check.C) {
hostInfo := tailcfg.Hostinfo{
RoutableIPs: []netaddr.IPPrefix{route, route2},
}
hostinfo, err := json.Marshal(hostInfo)
c.Assert(err, check.IsNil)
machine := Machine{
ID: 0,
@@ -89,10 +81,9 @@ func (s *Suite) TestGetEnableRoutes(c *check.C) {
DiscoKey: "faa",
Name: "test_enable_route_machine",
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
HostInfo: datatypes.JSON(hostinfo),
HostInfo: HostInfo(hostInfo),
}
app.db.Save(&machine)

View File

@@ -0,0 +1,10 @@
---
Hosts:
host-1: 100.100.100.100/32
subnet-1: 100.100.101.100/24
ACLs:
- Action: accept
Users:
- "*"
Ports:
- host-1:*

View File

@@ -196,10 +196,6 @@ func (h *Headscale) getUsedIPs() (*netaddr.IPSet, error) {
var addressesSlices []string
h.db.Model(&Machine{}).Pluck("ip_addresses", &addressesSlices)
log.Trace().
Strs("addresses", addressesSlices).
Msg("Got allocated ip addresses from databases")
var ips netaddr.IPSetBuilder
for _, slice := range addressesSlices {
var machineAddresses MachineAddresses
@@ -216,10 +212,6 @@ func (h *Headscale) getUsedIPs() (*netaddr.IPSet, error) {
}
}
log.Trace().
Interface("addresses", ips).
Msg("Parsed ip addresses that has been allocated from databases")
ipSet, err := ips.IPSet()
if err != nil {
return &netaddr.IPSet{}, fmt.Errorf(

View File

@@ -36,7 +36,6 @@ func (s *Suite) TestGetUsedIps(c *check.C) {
DiscoKey: "faa",
Name: "testmachine",
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
IPAddresses: ips,
@@ -85,7 +84,6 @@ func (s *Suite) TestGetMultiIp(c *check.C) {
DiscoKey: "faa",
Name: "testmachine",
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
IPAddresses: ips,
@@ -176,7 +174,6 @@ func (s *Suite) TestGetAvailableIpMachineWithoutIP(c *check.C) {
DiscoKey: "faa",
Name: "testmachine",
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
}