Compare commits

...

168 Commits

Author SHA1 Message Date
Juan Font
f90ebb8567 Update config.go 2023-05-10 12:16:07 +02:00
Juan Font
7564c4548c Fix socket location in config.go 2023-05-10 12:16:07 +02:00
Juan Font
5f80d1b155 Revert "Revert unix_socket to default value"
This reverts commit ca54fb9f56.
2023-05-10 12:16:07 +02:00
Juan Font
9478c288f6 Added missing file 2023-05-10 10:26:21 +02:00
Juan Font
6043ec87cf Update mkdocs.yml 2023-05-10 09:49:13 +02:00
Juan Font
dcf2439c61 Improved website
More docs
2023-05-10 09:49:13 +02:00
Kristoffer Dalby
ba45d7dbd3 update readme and templates to clarify scope (#1437)
Co-authored-by: Juan Font <juanfontalonso@gmail.com>
2023-05-10 08:03:13 +01:00
Juan Font
bab4e14828 Further clarification on unsupported ranges in config example 2023-05-08 12:47:08 +02:00
Juan Font
526e568e1e Update changelog 2023-05-07 15:27:30 +02:00
Juan Font
02ab0df2de Disable and Delete route must affect both exit routes (IPv4 and IPv6)
Fixed linting
2023-05-07 15:27:30 +02:00
Juan Font
7338775de7 Give a warning when users have set an unsupported prefix
Fix minor log issue

Removed debug meessage
2023-05-07 13:14:32 +02:00
Sebastian Muszytowski
00c514608e Add IP forwarding requirement to documentation
I propose to add the information, that IP forwarding needs to be enabled in order to use a node as an exit-node.
2023-05-06 21:48:59 +02:00
Maja Bojarska
6c5723a463 Update CHANGELOG.md
Co-authored-by: Juan Font <juanfontalonso@gmail.com>
2023-05-04 22:54:32 +02:00
Maja Bojarska
57fd5cf310 Update CHANGELOG.md 2023-05-04 22:54:32 +02:00
Maja Bojarska
f113cc7846 Add missing GH releases page link 2023-05-04 22:54:32 +02:00
ohdearaugustin
ca54fb9f56 Revert unix_socket to default value 2023-05-03 20:16:04 +02:00
Kristoffer Dalby
735b185e7f use IPSet in acls instead of string slice
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-03 18:43:57 +02:00
Kristoffer Dalby
1a7ae11697 Add basic testcases for Machine.canAccess
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-03 18:43:57 +02:00
Kristoffer Dalby
644be822d5 move matcher to separate file
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-03 18:43:57 +02:00
Kristoffer Dalby
56b63c6e10 use netipx.IPSet for matcher
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-03 18:43:57 +02:00
Kristoffer Dalby
ccedf276ab add a filter case with really large destination set #1372
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-03 18:43:57 +02:00
Kristoffer Dalby
10320a5f1f lint and nolint tailscale borrowed func
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-03 18:43:57 +02:00
Kristoffer Dalby
ecd62fb785 remove terrible filter code
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-03 18:43:57 +02:00
Kristoffer Dalby
0d24e878d0 update flake hash
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-03 18:43:57 +02:00
Kristoffer Dalby
889d5a1b29 testing without that horrible filtercode
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-03 18:43:57 +02:00
Kristoffer Dalby
1700a747f6 outline tests for full filter generate
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-03 18:43:57 +02:00
Kristoffer Dalby
200e3b88cc make generateFilterRule a pol struct func
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-03 18:43:57 +02:00
Kristoffer Dalby
5bbbe437df clear up the acl function naming
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-03 18:43:57 +02:00
Kristoffer Dalby
6de53e2f8d simplify expandAlias function, move seperate logic out
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-03 18:43:57 +02:00
Kristoffer Dalby
b23a9153df trim dockerfiles, script to rebuild test images (#1403) 2023-05-02 10:51:30 +01:00
Juan Font
80772033ee Improvements on Noise implementation (#1379) 2023-05-02 08:15:33 +02:00
Juan Font
a2b760834f Fix extra space 2023-04-30 23:28:16 +02:00
loprima-l
493bcfcf18 Update mkdocs.yml
Co-authored-by: Juan Font <juanfontalonso@gmail.com>
2023-04-30 23:28:16 +02:00
loprima-l
df72508089 Fix : Change master branch to main
This fix should change the edit branch to main in the documentation
2023-04-30 23:28:16 +02:00
loprima-l
0f8d8fc2d8 Fix : Updating the doc path
Updating the doc path to be the doc website url as it's a better documentation tool
2023-04-30 22:56:38 +02:00
Jonathan Wright
744e5a11b6 Update CHANGELOG.md
Co-authored-by: Juan Font <juanfontalonso@gmail.com>
2023-04-30 18:25:43 +02:00
Jonathan Wright
3ea1750ea0 Update CHANGELOG.md 2023-04-30 18:25:43 +02:00
Jonathan Wright
a45777d22e Put systemd service file in proper location 2023-04-30 18:25:43 +02:00
Kristoffer Dalby
56dd734300 Add go profiling flag, and enable on integration tests (#1382) 2023-04-27 16:57:11 +02:00
Philipp Krivanec
d0113732fe optimize generateACLPeerCacheMap (#1377) 2023-04-26 06:02:54 +02:00
Kristoffer Dalby
6215eb6471 update flake hash (#1376) 2023-04-24 15:52:15 +02:00
Juan Font
1d2b4bca8a Remove legacy DERP tests 2023-04-24 12:35:29 +02:00
Juan Font
96f9680afd Reuse Ping function for DERP ping 2023-04-24 12:17:24 +02:00
Juan Font
b465592c07 Do not use host networking in embedded DERP tests
fixed linting
2023-04-24 12:17:24 +02:00
Juan Font
991ff25362 Added workflow for embedded derp 2023-04-24 12:17:24 +02:00
Juan Font
eacd687dbf Added DERP integration tests
Linting fixes

Set listen addr to :8443
2023-04-24 12:17:24 +02:00
Juan Font
549f5a164d Expand surface of hsic for better TLS support 2023-04-24 12:17:24 +02:00
Juan Font
bb07aec82c Expand tsic to offer PingViaDerp 2023-04-24 12:17:24 +02:00
Kristoffer Dalby
a5afe4bd06 Add more capabilities for systemd
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-04-20 15:53:19 +02:00
Kristoffer Dalby
a71cc81fe7 fix
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-04-20 12:05:57 +02:00
Kristoffer Dalby
679305c3e4 Add version to binary release
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-04-20 12:05:57 +02:00
Kristoffer Dalby
c0680f34f1 fix issue where binaries are not released
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-04-20 11:10:27 +02:00
Kristoffer Dalby
64ebe6b0c8 change date in changelog
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-04-20 08:13:38 +02:00
Kristoffer Dalby
e6b26499f7 release source code with vendored dependencies
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-04-20 08:13:38 +02:00
Kristoffer Dalby
977eb1dee3 Update flakes, add some quality of life improvements (#1346) 2023-04-20 07:56:53 +02:00
Kristoffer Dalby
b2e2b02210 set release date
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-04-19 20:47:31 +02:00
Kristoffer Dalby
2abff4bb08 update changelog for #1339
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-04-19 20:45:27 +02:00
Kristoffer Dalby
54c00645d1 update changelog
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-04-19 20:04:58 +02:00
Kristoffer Dalby
cad5ce0ebd lint fix
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-04-19 20:04:58 +02:00
Kristoffer Dalby
b12a167fa2 remove rpm, might add back later
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-04-19 20:04:58 +02:00
Kristoffer Dalby
667295e15e add new documentation on how to install on debian/ubuntu
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-04-19 20:04:58 +02:00
Kristoffer Dalby
bea52678e3 move current linux documentation into "manual"
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-04-19 20:04:58 +02:00
Kristoffer Dalby
307cfc3304 add systemd enable to postinstall script
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-04-19 20:04:58 +02:00
Kristoffer Dalby
5e74ca9414 Fix IPv6 in ACLs (#1339) 2023-04-16 12:26:35 +02:00
Juan Font
9836b097a4 Make sure all clients of a user are ready (#1335) 2023-04-12 09:25:51 +02:00
Juan Font
d0b3b1bfc4 Fix binary releases 2023-04-08 09:21:27 +02:00
Juan Font
6eea96eabc Added 1.38.4 in the new tests 2023-04-07 19:45:46 +02:00
github-actions[bot]
d08fee78c3 docs(README): update contributors (#1325) 2023-04-07 17:31:29 +02:00
Andriy Kushnir (Orhideous)
bb5f0d456c Change primary color for light mode to white 2023-04-07 15:24:13 +02:00
Andriy Kushnir (Orhideous)
c186c49e25 Removed custom accents, going with defaults 2023-04-07 15:24:13 +02:00
Andriy Kushnir (Orhideous)
4ec6894773 Build with strict mode 2023-04-07 15:24:13 +02:00
Andriy Kushnir (Orhideous)
dd9b4b1cb7 Move examples out of docs/ directory 2023-04-07 15:24:13 +02:00
Andriy Kushnir (Orhideous)
a43bb9c958 Replace placeholder link with actual one 2023-04-07 15:24:13 +02:00
Andriy Kushnir (Orhideous)
ba905ff6fc Add GHA CI to build and deploy docs 2023-04-07 15:24:13 +02:00
Andriy Kushnir (Orhideous)
99bd09f688 Add new index page 2023-04-07 15:24:13 +02:00
Andriy Kushnir (Orhideous)
a6bc792a61 Move admonitions to relevant sections 2023-04-07 15:24:13 +02:00
Andriy Kushnir (Orhideous)
6381d3660a Add admonitions marking community-provided docs 2023-04-07 15:24:13 +02:00
Andriy Kushnir (Orhideous)
66c5f74d78 Add admonitions marking community-provided docs 2023-04-07 15:24:13 +02:00
Andriy Kushnir (Orhideous)
1723a6bf40 Configure MkDocs Material scaffold 2023-04-07 15:24:13 +02:00
Juan Font
353f191e4f Update changelog 2023-04-07 13:25:34 +02:00
Juan Font
8d865bb61b Target Go 1.20 in flake.nix 2023-04-07 13:25:34 +02:00
Juan Font
c6815c5334 Target Go 1.20 and Tailscale 1.38 2023-04-07 13:25:34 +02:00
Kristoffer Dalby
b684ac0668 Simplify goreleaser, package deb and rpm
This commit simplifies the goreleaser configuration and then adds nfpm
support which allows us to build .deb and .rpm for each of the ARCH we
support.

The deb and rpm packages adds systemd services and users, creates
directories etc and should in general give the user a working
environment. We should be able to remove a lot of the complicated,
PEBCAK inducing documentation after this.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-04-07 11:06:42 +02:00
Juan Font
dfc5d861c7 Fix CIDR calculation in expandACLPeerAddr 2023-04-05 09:44:46 +02:00
Juan Font
50b706eeed Remove deprecated linters + one casuing issues with imports 2023-04-04 22:37:27 +02:00
Sean Reifschneider
036ff1cbb9 Adding Powershell commands to Windows instructions (#1299)
Co-authored-by: Kristoffer Dalby <kristoffer@dalby.cc>
2023-04-04 08:58:32 +02:00
Kristoffer Dalby
ceeef40cdf Add tests to verify "Hosts" aliases in ACL (#1304) 2023-04-03 10:08:48 +02:00
Julien Zweverink
681c86cc95 ACL Doc's (#1288) 2023-03-28 18:41:23 +02:00
Kristoffer Dalby
c7b459b615 Fix issue where ACL * would filter out returning connections (#1279) 2023-03-27 19:19:32 +02:00
Gabe Cook
56a7b1e349 Add SVG logos (#1286) 2023-03-27 15:33:25 +02:00
Antonio Fernandez
f1eee841cb updated to ACL doc (#1278) 2023-03-27 11:25:55 +02:00
Stefan Majer
45fbd34480 Do not use yaml.v2 and yaml.v3 as direct dependency (#1281) 2023-03-27 10:48:39 +02:00
Juan Font
248abcf353 Add missing entry to changelog and prepare for 0.22
Add missing entry to changelog
2023-03-20 13:48:56 +01:00
Kurnia D Win
2560c32378 adding some sleep on re-registration after machine expired (#1256) 2023-03-20 11:14:34 +01:00
Kristoffer Dalby
e38efd3cfa Add ACL test for limiting a single port. (#1258) 2023-03-20 08:52:52 +01:00
Moritz Poldrack
d12f247490 document running exit nodes
Currently the only kind-of documentation is #210 which is outdated. To remedy
this, add a document describing the process.
2023-03-19 11:02:01 +01:00
nicholas-yap
003036a779 Update iOS compatibility and added iOS docs (#1264) 2023-03-17 15:56:15 +01:00
github-actions[bot]
ed79f977a7 docs(README): update contributors (#1262) 2023-03-17 10:07:39 +01:00
Kristoffer Dalby
8012e1cbd2 Add instructions on how to login to iOS (#1261) 2023-03-15 11:31:38 +00:00
Kristoffer Dalby
a5562850a7 MapResponse optimalisations, peer list integration tests (#1254)
Co-authored-by: Allen <979347228@qq.com>
2023-03-06 17:50:26 +01:00
Stefan Majer
bb786ac8e4 github.com/gofrs/uuid/v5 is now go modules compatible, use it (#1224) 2023-03-06 09:54:24 +01:00
Juan Font
ea82035222 Allow to delete routes (#1244) 2023-03-06 09:05:40 +01:00
Albert Copeland
c9ecdd6ef1 Add Graphical Control Panels section to README (#1226) 2023-03-03 19:05:12 +01:00
Juan Font
54f5c249f1 Fix various linting issues + golang-lint upgrade (#1245) 2023-03-03 18:22:47 +01:00
dnaq
a82a603db6 Return 404 on unmatched routes (#1201) 2023-03-03 17:14:30 +01:00
Sean Reifschneider
f49930c514 Add "configtest" CLI command. (#1230)
Co-authored-by: Kristoffer Dalby <kristoffer@dalby.cc>
Fixes https://github.com/juanfont/headscale/issues/1229
2023-03-03 14:55:29 +01:00
Kristoffer Dalby
2baeb79aa0 changelog: prep for 0.21 (#1246) 2023-03-03 13:42:45 +01:00
Kristoffer Dalby
b3f78a209a Post PR comment when nix vendor sum breaks
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-03-02 18:17:59 +01:00
Josh Taylor
5e6868a858 Run prettier 2023-02-27 10:28:49 +01:00
Josh Taylor
5caf848f94 Add steps for Google OAuth for OIDC 2023-02-27 10:28:49 +01:00
Juan Font
3e097123bf Target ts 1.36 in integration tests 2023-02-26 15:35:27 +01:00
Juan Font
74447b02e8 Target Tailscale 1.36 when building 2023-02-26 15:35:27 +01:00
Juan Font
20e96de963 Update dependencies 20230226 2023-02-26 14:39:37 +01:00
Juan Font
7c765fb3dc Update prettier action
Update prettier action
2023-02-26 13:54:30 +01:00
Michael Savage
dcc246c869 Fix OpenBSD build docs
- OpenBSD 7.2 installs go 1.19 by default
- Doubt you can run nix so skip the makefile/nix build and just go build directly
2023-02-26 12:54:47 +01:00
Àlex Torregrosa
cf7767d8f9 Add css to the /windows and /apple templates (#1211)
Co-authored-by: Kristoffer Dalby <kristoffer@dalby.cc>
2023-02-09 08:49:05 -05:00
Maxim Gajdaj
61c578f82b Update running-headscale-linux.md
Option WorkingDirectory for headscale.service added
2023-02-09 08:17:28 -05:00
Kristoffer Dalby
6950ff7841 Add list of talks to the readme
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-07 17:04:14 +01:00
Kristoffer Dalby
e65ce17f7b Add documentation to integration test framework
so tsic, hsic and scenario

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-03 16:25:58 +01:00
Kristoffer Dalby
b190ec8edc Add section about running locally
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-03 16:25:58 +01:00
Kristoffer Dalby
c39085911f Add node expiry test
This commits adds a test to verify that nodes get updated if a node in
their network expires.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-03 09:26:22 +01:00
Kristoffer Dalby
3c20d2a178 Update changelog
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-03 09:26:22 +01:00
Kristoffer Dalby
9187e4287c Remove unused components from old integration tests
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-03 09:26:22 +01:00
Kristoffer Dalby
2b7bcb77a5 Stop using deprecated string function
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-03 09:26:22 +01:00
Kristoffer Dalby
97a909866d Use pingAll helper for all integration pinging
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-03 09:26:22 +01:00
Kristoffer Dalby
feeb5d334b Populate the tags field on node
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-03 09:26:22 +01:00
Kristoffer Dalby
a840a2e6ee Sort tailcfg.Node creation as upstream
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-03 09:26:22 +01:00
Kristoffer Dalby
4183345020 Do not collect services, we dont support it
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-03 09:26:22 +01:00
Kristoffer Dalby
50fb7ad6ce Add TODOs for only sending patch updates
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-03 09:26:22 +01:00
Kristoffer Dalby
88a9f4b44c Send control time in map response
This gives all the nodes the same constant time to work from

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-03 09:26:22 +01:00
Kristoffer Dalby
00fbd8dd93 Remove all tests before generating new ones
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-02 17:55:19 +01:00
Kristoffer Dalby
ce587d2421 Update test workflows
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-01 10:58:37 +01:00
Kristoffer Dalby
e1eb30084d Remove new line at start of test template
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-01 10:58:37 +01:00
Kristoffer Dalby
673638afe7 Use ripgrep to find list of tests
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-01 10:58:37 +01:00
Kristoffer Dalby
da48cf64b3 Set OpenID Connect Expiry
This commit adds a default OpenID Connect expiry to 180d to align with
Tailscale SaaS (previously infinite or based on token expiry).

In addition, it adds an option use the expiry time from the Token sent
by the OpenID provider. This will typically cause really short expiry
and you should only turn on this option if you know what you are
desiring.

This fixes #1176.

Co-authored-by: Even Holthe <even.holthe@bekk.no>
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-31 18:55:16 +01:00
Dominic Bevacqua
385fd93e73 Update changelog 2023-01-31 00:15:48 +01:00
Dominic Bevacqua
26edf24477 Allow split DNS configuration without requiring global nameservers
Align behaviour of dns_config.restricted_nameservers to tailscale.

Tailscale allows split DNS configuration without requiring global nameservers.

In addition, as per [the docs](https://tailscale.com/kb/1054/dns/#using-dns-settings-in-the-admin-console):

> These nameservers also configure search domains for your devices

This commit aligns headscale to tailscale by:

 * honouring dns_config.restricted_nameservers regardless of whether any global resolvers are configured
 * adding a search domain for each restricted_nameserver
2023-01-31 00:15:48 +01:00
Kristoffer Dalby
83a538cc95 Rename IP specific function, add missing test case
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-30 15:56:38 +01:00
Kristoffer Dalby
cffa040474 Cancel old builds if new commits appear
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-30 14:57:10 +01:00
Kristoffer Dalby
727d95b477 Improve generated integration tests
- Save logs from control(headscale) on every run to tmp
- Upgrade nix-actions
- Cancel builds if new commit is pushed
- Fix a sorting bug in user command test

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-30 14:43:03 +01:00
Juan Font
640bb94119 Do not show IsPrimary field as false in exit nodes 2023-01-29 14:54:09 +01:00
Juan Font
0f65918a25 Update tests
Fixed linting
2023-01-29 12:25:37 +01:00
Juan Font
3ac2e0b253 Enable both exit node routes (IPv4 and IPv6) at the same time.
As indicated by bradfitz in https://github.com/juanfont/headscale/issues/804#issuecomment-1399314002,
both routes for the exit node must be enabled at the same time. If a user tries to enable one of the exit node routes,
the other gets activated too.

This commit also reduces the API surface, making private a method that didnt need to be exposed.
2023-01-29 12:25:37 +01:00
Juan Font
b322cdf251 Updated changelog for v0.20.0 2023-01-29 11:46:37 +01:00
Johan Siebens
e128796b59 use smallzstd and sync pool 2023-01-27 12:03:24 +01:00
Kristoffer Dalby
6d669c6b9c Migrate namespace_id to user_id column in machine and pak
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-26 11:07:26 +01:00
Kristoffer Dalby
8dadb045cf Mark -n and --namespace as deprecated
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-26 10:22:38 +01:00
Christian Heusel
9f6e546522 modify the test to reflect the changes on the webinterface
related to 2d44a1c99c17

Signed-off-by: Christian Heusel <christian@heusel.eu>
2023-01-26 08:33:44 +01:00
Juan Font
9714900db9 Target Tailscale 1.36.0 2023-01-26 07:50:03 +01:00
Jan Hartkopf
cb25f0d650 Add hint for reverse proxying with Apache 2023-01-23 15:51:20 +01:00
caelansar
9c2e580ab5 put Where before Find 2023-01-20 10:50:29 +01:00
Christian Heusel
0ffff2c994 Update the node join instruction to reference "username"
related to https://github.com/juanfont/headscale/pull/1144

Signed-off-by: Christian Heusel <christian@heusel.eu>
2023-01-20 09:50:49 +01:00
Christian Heusel
c720af66d6 permalink in the limitations section to tailscale
The relative link was broken after one commit to the file

Signed-off-by: Christian Heusel <christian@heusel.eu>
2023-01-20 09:19:26 +01:00
Kristoffer Dalby
86a7129027 Update changelog, more explicit backup note
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-19 12:54:34 +01:00
Kristoffer Dalby
9eaa8dd049 Migrate DB: rename table is plural, order matters
The calls to AutoMigrate to other classes that refer to users will
create the table and it will break, it needs to be done before
everything else.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-19 12:54:34 +01:00
Kristoffer Dalby
81441afe70 update changelog
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-18 15:40:04 +01:00
Kristoffer Dalby
f19e8aa7f0 Fix failing tests
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-18 15:40:04 +01:00
Kristoffer Dalby
90287a6735 gofumpt
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-18 15:40:04 +01:00
Kristoffer Dalby
fb3e2dcf10 Rename namespace to user in docs
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-18 15:40:04 +01:00
Kristoffer Dalby
bf0b85f382 Rename acl test file
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-18 15:40:04 +01:00
Kristoffer Dalby
5da0963aac Migrate DB: rename namespace, automigrate user
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-18 15:40:04 +01:00
Kristoffer Dalby
da5c051d73 Lint fix
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-18 15:40:04 +01:00
Kristoffer Dalby
b98bf199dd Regenerate go from proto
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-18 15:40:04 +01:00
Kristoffer Dalby
428d7c86ce Rename namespace in protobuf files
While this truly breaks the point of the backwards compatible stuff with
protobuf, it does not seem worth it to attempt to glue together a
compatible API.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-18 15:40:04 +01:00
Kristoffer Dalby
af1ec5a593 Rename .go namespace files
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-18 15:40:04 +01:00
Kristoffer Dalby
e3a2593344 Rename [Nn]amespace -> [Uu]ser in go code
Use gopls, ag and perl to rename all occurances of Namespace

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-18 15:40:04 +01:00
Motiejus Jakštys
bafb6791d3 oidc: allow reading the client secret from a file
Currently the most "secret" way to specify the oidc client secret is via
an environment variable `OIDC_CLIENT_SECRET`, which is problematic[1].
Lets allow reading oidc client secret from a file. For extra convenience
the path to the secret will resolve the environment variables.

[1]: https://systemd.io/CREDENTIALS/
2023-01-14 17:03:57 +01:00
Motiejus Jakštys
6edac4863a Makefile: remove a missing target
test_integration_oidc was removed in 0525bea593
2023-01-14 13:42:48 +01:00
196 changed files with 11075 additions and 5914 deletions

View File

@@ -6,19 +6,24 @@ labels: ["bug"]
assignees: "" assignees: ""
--- ---
<!-- Headscale is a multinational community across the globe. Our common language is English. Please consider raising the bug report in this language. --> <!--
Before posting a bug report, discuss the behaviour you are expecting with the Discord community
to make sure that it is truly a bug.
The issue tracker is not the place to ask for support or how to set up Headscale.
**Bug description** Bug reports without the sufficient information will be closed.
Headscale is a multinational community across the globe. Our language is English.
All bug reports needs to be in English.
-->
## Bug description
<!-- A clear and concise description of what the bug is. Describe the expected bahavior <!-- A clear and concise description of what the bug is. Describe the expected bahavior
and how it is currently different. If you are unsure if it is a bug, consider discussing and how it is currently different. If you are unsure if it is a bug, consider discussing
it on our Discord server first. --> it on our Discord server first. -->
**To Reproduce** ## Environment
<!-- Steps to reproduce the behavior. -->
**Context info**
<!-- Please add relevant information about your system. For example: <!-- Please add relevant information about your system. For example:
- Version of headscale used - Version of headscale used
@@ -28,3 +33,20 @@ assignees: ""
- The relevant config parameters you used - The relevant config parameters you used
- Log output - Log output
--> -->
- OS:
- Headscale version:
- Tailscale version:
<!--
We do not support running Headscale in a container nor behind a (reverse) proxy.
If either of these are true for your environment, ask the community in Discord
instead of filing a bug report.
-->
- [ ] Headscale is behind a (reverse) proxy
- [ ] Headscale runs in a container
## To Reproduce
<!-- Steps to reproduce the behavior. -->

View File

@@ -6,12 +6,21 @@ labels: ["enhancement"]
assignees: "" assignees: ""
--- ---
<!-- Headscale is a multinational community across the globe. Our common language is English. Please consider raising the feature request in this language. --> <!--
We typically have a clear roadmap for what we want to improve and reserve the right
to close feature requests that does not fit in the roadmap, or fit with the scope
of the project, or we actually want to implement ourselves.
**Feature request** Headscale is a multinational community across the globe. Our language is English.
All bug reports needs to be in English.
-->
<!-- A clear and precise description of what new or changed feature you want. --> ## Why
<!-- Please include the reason, why you would need the feature. E.g. what problem <!-- Include the reason, why you would need the feature. E.g. what problem
does it solve? Or which workflow is currently frustrating and will be improved by does it solve? Or which workflow is currently frustrating and will be improved by
this? --> this? -->
## Description
<!-- A clear and precise description of what new or changed feature you want. -->

View File

@@ -1,30 +0,0 @@
---
name: "Other issue"
about: "Report a different issue"
title: ""
labels: ["bug"]
assignees: ""
---
<!-- Headscale is a multinational community across the globe. Our common language is English. Please consider raising the issue in this language. -->
<!-- If you have a question, please consider using our Discord for asking questions -->
**Issue description**
<!-- Please add your issue description. -->
**To Reproduce**
<!-- Steps to reproduce the behavior. -->
**Context info**
<!-- Please add relevant information about your system. For example:
- Version of headscale used
- Version of tailscale client
- OS (e.g. Linux, Mac, Cygwin, WSL, etc.) and version
- Kernel version
- The relevant config parameters you used
- Log output
-->

View File

@@ -1,3 +1,15 @@
<!--
Headscale is "Open Source, acknowledged contribution", this means that any
contribution will have to be discussed with the Maintainers before being submitted.
This model has been chosen to reduce the risk of burnout by limiting the
maintenance overhead of reviewing and validating third-party code.
Headscale is open to code contributions for bug fixes without discussion.
If you find mistakes in the documentation, please submit a fix to the documentation.
-->
<!-- Please tick if the following things apply. You… --> <!-- Please tick if the following things apply. You… -->
- [ ] read the [CONTRIBUTING guidelines](README.md#contributing) - [ ] read the [CONTRIBUTING guidelines](README.md#contributing)

26
.github/renovate.json vendored
View File

@@ -6,31 +6,27 @@
"onboarding": false, "onboarding": false,
"extends": ["config:base", ":rebaseStalePrs"], "extends": ["config:base", ":rebaseStalePrs"],
"ignorePresets": [":prHourlyLimit2"], "ignorePresets": [":prHourlyLimit2"],
"enabledManagers": ["dockerfile", "gomod", "github-actions","regex" ], "enabledManagers": ["dockerfile", "gomod", "github-actions", "regex"],
"includeForks": true, "includeForks": true,
"repositories": ["juanfont/headscale"], "repositories": ["juanfont/headscale"],
"platform": "github", "platform": "github",
"packageRules": [ "packageRules": [
{ {
"matchDatasources": ["go"], "matchDatasources": ["go"],
"groupName": "Go modules", "groupName": "Go modules",
"groupSlug": "gomod", "groupSlug": "gomod",
"separateMajorMinor": false "separateMajorMinor": false
}, },
{ {
"matchDatasources": ["docker"], "matchDatasources": ["docker"],
"groupName": "Dockerfiles", "groupName": "Dockerfiles",
"groupSlug": "dockerfiles" "groupSlug": "dockerfiles"
} }
], ],
"regexManagers": [ "regexManagers": [
{ {
"fileMatch": [ "fileMatch": [".github/workflows/.*.yml$"],
".github/workflows/.*.yml$" "matchStrings": ["\\s*go-version:\\s*\"?(?<currentValue>.*?)\"?\\n"],
],
"matchStrings": [
"\\s*go-version:\\s*\"?(?<currentValue>.*?)\"?\\n"
],
"datasourceTemplate": "golang-version", "datasourceTemplate": "golang-version",
"depNameTemplate": "actions/go-version" "depNameTemplate": "actions/go-version"
} }

View File

@@ -8,9 +8,14 @@ on:
branches: branches:
- main - main
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: write-all
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@@ -32,10 +37,34 @@ jobs:
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
- name: Run build - name: Run build
id: build
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
run: nix build run: |
nix build |& tee build-result
BUILD_STATUS="${PIPESTATUS[0]}"
- uses: actions/upload-artifact@v2 OLD_HASH=$(cat build-result | grep specified: | awk -F ':' '{print $2}' | sed 's/ //g')
NEW_HASH=$(cat build-result | grep got: | awk -F ':' '{print $2}' | sed 's/ //g')
echo "OLD_HASH=$OLD_HASH" >> $GITHUB_OUTPUT
echo "NEW_HASH=$NEW_HASH" >> $GITHUB_OUTPUT
exit $BUILD_STATUS
- name: Nix gosum diverging
uses: actions/github-script@v6
if: failure() && steps.build.outcome == 'failure'
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
github.rest.pulls.createReviewComment({
pull_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Nix build failed with wrong gosum, please update "vendorSha256" (${{ steps.build.outputs.OLD_HASH }}) for the "headscale" package in flake.nix with the new SHA: ${{ steps.build.outputs.NEW_HASH }}'
})
- uses: actions/upload-artifact@v3
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
with: with:
name: headscale-linux name: headscale-linux

45
.github/workflows/docs.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: Build documentation
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Install python
uses: actions/setup-python@v4
with:
python-version: 3.x
- name: Setup cache
uses: actions/cache@v2
with:
key: ${{ github.ref }}
path: .cache
- name: Setup dependencies
run: pip install mkdocs-material pillow cairosvg mkdocs-minify-plugin
- name: Build docs
run: mkdocs build --strict
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
with:
path: ./site
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1

View File

@@ -3,6 +3,10 @@ name: Lint
on: [push, pull_request] on: [push, pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs: jobs:
golangci-lint: golangci-lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -26,7 +30,7 @@ jobs:
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
uses: golangci/golangci-lint-action@v2 uses: golangci/golangci-lint-action@v2
with: with:
version: v1.49.0 version: v1.51.2
# Only block PRs on new problems. # Only block PRs on new problems.
# If this is not enabled, we will end up having PRs # If this is not enabled, we will end up having PRs
@@ -59,7 +63,7 @@ jobs:
- name: Prettify code - name: Prettify code
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
uses: creyD/prettier_action@v4.0 uses: creyD/prettier_action@v4.3
with: with:
prettier_options: >- prettier_options: >-
--check **/*.{ts,js,md,yaml,yml,sass,css,scss,html} --check **/*.{ts,js,md,yaml,yml,sass,css,scss,html}

138
.github/workflows/release-docker.yml vendored Normal file
View File

@@ -0,0 +1,138 @@
---
name: Release Docker
on:
push:
tags:
- "*" # triggers only if push new tag version
workflow_dispatch:
jobs:
docker-release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Set up QEMU for multiple platforms
uses: docker/setup-qemu-action@master
with:
platforms: arm64,amd64
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
# list of Docker images to use as base name for tags
images: |
${{ secrets.DOCKERHUB_USERNAME }}/headscale
ghcr.io/${{ github.repository_owner }}/headscale
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
type=raw,value=develop
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
push: true
context: .
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new
build-args: |
VERSION=${{ steps.meta.outputs.version }}
- name: Prepare cache for next build
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
docker-debug-release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Set up QEMU for multiple platforms
uses: docker/setup-qemu-action@master
with:
platforms: arm64,amd64
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache-debug
key: ${{ runner.os }}-buildx-debug-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-debug-
- name: Docker meta
id: meta-debug
uses: docker/metadata-action@v3
with:
# list of Docker images to use as base name for tags
images: |
${{ secrets.DOCKERHUB_USERNAME }}/headscale
ghcr.io/${{ github.repository_owner }}/headscale
flavor: |
suffix=-debug,onlatest=true
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
type=raw,value=develop
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
push: true
context: .
file: Dockerfile.debug
tags: ${{ steps.meta-debug.outputs.tags }}
labels: ${{ steps.meta-debug.outputs.labels }}
platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache-debug
cache-to: type=local,dest=/tmp/.buildx-cache-debug-new
build-args: |
VERSION=${{ steps.meta-debug.outputs.version }}
- name: Prepare cache for next build
run: |
rm -rf /tmp/.buildx-cache-debug
mv /tmp/.buildx-cache-debug-new /tmp/.buildx-cache-debug

View File

@@ -19,135 +19,6 @@ jobs:
- uses: cachix/install-nix-action@v16 - uses: cachix/install-nix-action@v16
- name: Run goreleaser - name: Run goreleaser
run: nix develop --command -- goreleaser release --rm-dist run: nix develop --command -- goreleaser release --clean
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
docker-release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Set up QEMU for multiple platforms
uses: docker/setup-qemu-action@master
with:
platforms: arm64,amd64
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
# list of Docker images to use as base name for tags
images: |
${{ secrets.DOCKERHUB_USERNAME }}/headscale
ghcr.io/${{ github.repository_owner }}/headscale
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
type=raw,value=develop
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
push: true
context: .
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new
build-args: |
VERSION=${{ steps.meta.outputs.version }}
- name: Prepare cache for next build
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
docker-debug-release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Set up QEMU for multiple platforms
uses: docker/setup-qemu-action@master
with:
platforms: arm64,amd64
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache-debug
key: ${{ runner.os }}-buildx-debug-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-debug-
- name: Docker meta
id: meta-debug
uses: docker/metadata-action@v3
with:
# list of Docker images to use as base name for tags
images: |
${{ secrets.DOCKERHUB_USERNAME }}/headscale
ghcr.io/${{ github.repository_owner }}/headscale
flavor: |
suffix=-debug,onlatest=true
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
type=raw,value=develop
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
push: true
context: .
file: Dockerfile.debug
tags: ${{ steps.meta-debug.outputs.tags }}
labels: ${{ steps.meta-debug.outputs.labels }}
platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache-debug
cache-to: type=local,dest=/tmp/.buildx-cache-debug-new
build-args: |
VERSION=${{ steps.meta-debug.outputs.version }}
- name: Prepare cache for next build
run: |
rm -rf /tmp/.buildx-cache-debug
mv /tmp/.buildx-cache-debug-new /tmp/.buildx-cache-debug

View File

@@ -1,35 +0,0 @@
name: Integration Test DERP
on: [pull_request]
jobs:
integration-test-derp:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Set Swap Space
uses: pierotofy/set-swap-space@master
with:
swap-size-gb: 10
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v16
if: steps.changed-files.outputs.any_changed == 'true'
- name: Run Embedded DERP server integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: nix develop --command -- make test_integration_derp

View File

@@ -1,11 +1,14 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go # DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/ # To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestOIDCExpireNodes name: Integration Test v2 - TestACLAllowStarDst
on: [pull_request] on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -26,8 +29,8 @@ jobs:
integration_test/ integration_test/
config-example.yaml config-example.yaml
- uses: cachix/install-nix-action@v16 - uses: cachix/install-nix-action@v18
if: steps.changed-files.outputs.any_changed == 'true' if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests - name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
@@ -38,10 +41,23 @@ jobs:
--name headscale-test-suite \ --name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \ --volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \ --volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \ golang:1 \
go test ./... \ go test ./... \
-tags ts2019 \ -tags ts2019 \
-failfast \ -failfast \
-timeout 120m \ -timeout 120m \
-parallel 1 \ -parallel 1 \
-run "^TestOIDCExpireNodes$" -run "^TestACLAllowStarDst$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"

View File

@@ -0,0 +1,63 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestACLAllowUser80Dst
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestACLAllowUser80Dst$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"

View File

@@ -0,0 +1,63 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestACLAllowUserDst
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestACLAllowUserDst$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"

View File

@@ -0,0 +1,63 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestACLDenyAllPort80
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestACLDenyAllPort80$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"

View File

@@ -0,0 +1,63 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestACLDevice1CanAccessDevice2
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestACLDevice1CanAccessDevice2$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"

View File

@@ -0,0 +1,63 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestACLHostsInNetMapTable
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestACLHostsInNetMapTable$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"

View File

@@ -0,0 +1,63 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestACLNamedHostsCanReach
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestACLNamedHostsCanReach$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"

View File

@@ -0,0 +1,63 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestACLNamedHostsCanReachBySubnet
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestACLNamedHostsCanReachBySubnet$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"

View File

@@ -1,4 +1,3 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go # DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/ # To regenerate, run "go generate" in cmd/gh-action-integration-generator/
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestAuthKeyLogoutAndRelogin
on: [pull_request] on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -26,8 +29,8 @@ jobs:
integration_test/ integration_test/
config-example.yaml config-example.yaml
- uses: cachix/install-nix-action@v16 - uses: cachix/install-nix-action@v18
if: steps.changed-files.outputs.any_changed == 'true' if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests - name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
@@ -38,6 +41,7 @@ jobs:
--name headscale-test-suite \ --name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \ --volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \ --volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \ golang:1 \
go test ./... \ go test ./... \
-tags ts2019 \ -tags ts2019 \
@@ -45,3 +49,15 @@ jobs:
-timeout 120m \ -timeout 120m \
-parallel 1 \ -parallel 1 \
-run "^TestAuthKeyLogoutAndRelogin$" -run "^TestAuthKeyLogoutAndRelogin$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"

View File

@@ -1,4 +1,3 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go # DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/ # To regenerate, run "go generate" in cmd/gh-action-integration-generator/
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestAuthWebFlowAuthenticationPingAll
on: [pull_request] on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -26,8 +29,8 @@ jobs:
integration_test/ integration_test/
config-example.yaml config-example.yaml
- uses: cachix/install-nix-action@v16 - uses: cachix/install-nix-action@v18
if: steps.changed-files.outputs.any_changed == 'true' if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests - name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
@@ -38,6 +41,7 @@ jobs:
--name headscale-test-suite \ --name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \ --volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \ --volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \ golang:1 \
go test ./... \ go test ./... \
-tags ts2019 \ -tags ts2019 \
@@ -45,3 +49,15 @@ jobs:
-timeout 120m \ -timeout 120m \
-parallel 1 \ -parallel 1 \
-run "^TestAuthWebFlowAuthenticationPingAll$" -run "^TestAuthWebFlowAuthenticationPingAll$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"

View File

@@ -1,4 +1,3 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go # DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/ # To regenerate, run "go generate" in cmd/gh-action-integration-generator/
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestAuthWebFlowLogoutAndRelogin
on: [pull_request] on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -26,8 +29,8 @@ jobs:
integration_test/ integration_test/
config-example.yaml config-example.yaml
- uses: cachix/install-nix-action@v16 - uses: cachix/install-nix-action@v18
if: steps.changed-files.outputs.any_changed == 'true' if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests - name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
@@ -38,6 +41,7 @@ jobs:
--name headscale-test-suite \ --name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \ --volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \ --volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \ golang:1 \
go test ./... \ go test ./... \
-tags ts2019 \ -tags ts2019 \
@@ -45,3 +49,15 @@ jobs:
-timeout 120m \ -timeout 120m \
-parallel 1 \ -parallel 1 \
-run "^TestAuthWebFlowLogoutAndRelogin$" -run "^TestAuthWebFlowLogoutAndRelogin$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"

View File

@@ -1,4 +1,3 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go # DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/ # To regenerate, run "go generate" in cmd/gh-action-integration-generator/
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestCreateTailscale
on: [pull_request] on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -26,8 +29,8 @@ jobs:
integration_test/ integration_test/
config-example.yaml config-example.yaml
- uses: cachix/install-nix-action@v16 - uses: cachix/install-nix-action@v18
if: steps.changed-files.outputs.any_changed == 'true' if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests - name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
@@ -38,6 +41,7 @@ jobs:
--name headscale-test-suite \ --name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \ --volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \ --volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \ golang:1 \
go test ./... \ go test ./... \
-tags ts2019 \ -tags ts2019 \
@@ -45,3 +49,15 @@ jobs:
-timeout 120m \ -timeout 120m \
-parallel 1 \ -parallel 1 \
-run "^TestCreateTailscale$" -run "^TestCreateTailscale$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"

View File

@@ -0,0 +1,63 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestDERPServerScenario
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestDERPServerScenario$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"

View File

@@ -1,4 +1,3 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go # DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/ # To regenerate, run "go generate" in cmd/gh-action-integration-generator/
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestEnablingRoutes
on: [pull_request] on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -26,8 +29,8 @@ jobs:
integration_test/ integration_test/
config-example.yaml config-example.yaml
- uses: cachix/install-nix-action@v16 - uses: cachix/install-nix-action@v18
if: steps.changed-files.outputs.any_changed == 'true' if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests - name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
@@ -38,6 +41,7 @@ jobs:
--name headscale-test-suite \ --name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \ --volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \ --volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \ golang:1 \
go test ./... \ go test ./... \
-tags ts2019 \ -tags ts2019 \
@@ -45,3 +49,15 @@ jobs:
-timeout 120m \ -timeout 120m \
-parallel 1 \ -parallel 1 \
-run "^TestEnablingRoutes$" -run "^TestEnablingRoutes$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"

View File

@@ -1,11 +1,14 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go # DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/ # To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestSSHMultipleNamespacesAllToAll name: Integration Test v2 - TestEphemeral
on: [pull_request] on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -26,8 +29,8 @@ jobs:
integration_test/ integration_test/
config-example.yaml config-example.yaml
- uses: cachix/install-nix-action@v16 - uses: cachix/install-nix-action@v18
if: steps.changed-files.outputs.any_changed == 'true' if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests - name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
@@ -38,10 +41,23 @@ jobs:
--name headscale-test-suite \ --name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \ --volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \ --volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \ golang:1 \
go test ./... \ go test ./... \
-tags ts2019 \ -tags ts2019 \
-failfast \ -failfast \
-timeout 120m \ -timeout 120m \
-parallel 1 \ -parallel 1 \
-run "^TestSSHMultipleNamespacesAllToAll$" -run "^TestEphemeral$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"

View File

@@ -1,11 +1,14 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go # DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/ # To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestSSHOneNamespaceAllToAll name: Integration Test v2 - TestExpireNode
on: [pull_request] on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -26,8 +29,8 @@ jobs:
integration_test/ integration_test/
config-example.yaml config-example.yaml
- uses: cachix/install-nix-action@v16 - uses: cachix/install-nix-action@v18
if: steps.changed-files.outputs.any_changed == 'true' if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests - name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
@@ -38,10 +41,23 @@ jobs:
--name headscale-test-suite \ --name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \ --volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \ --volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \ golang:1 \
go test ./... \ go test ./... \
-tags ts2019 \ -tags ts2019 \
-failfast \ -failfast \
-timeout 120m \ -timeout 120m \
-parallel 1 \ -parallel 1 \
-run "^TestSSHOneNamespaceAllToAll$" -run "^TestExpireNode$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"

View File

@@ -1,4 +1,3 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go # DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/ # To regenerate, run "go generate" in cmd/gh-action-integration-generator/
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestHeadscale
on: [pull_request] on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -26,8 +29,8 @@ jobs:
integration_test/ integration_test/
config-example.yaml config-example.yaml
- uses: cachix/install-nix-action@v16 - uses: cachix/install-nix-action@v18
if: steps.changed-files.outputs.any_changed == 'true' if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests - name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
@@ -38,6 +41,7 @@ jobs:
--name headscale-test-suite \ --name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \ --volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \ --volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \ golang:1 \
go test ./... \ go test ./... \
-tags ts2019 \ -tags ts2019 \
@@ -45,3 +49,15 @@ jobs:
-timeout 120m \ -timeout 120m \
-parallel 1 \ -parallel 1 \
-run "^TestHeadscale$" -run "^TestHeadscale$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"

View File

@@ -1,4 +1,3 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go # DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/ # To regenerate, run "go generate" in cmd/gh-action-integration-generator/
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestOIDCAuthenticationPingAll
on: [pull_request] on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -26,8 +29,8 @@ jobs:
integration_test/ integration_test/
config-example.yaml config-example.yaml
- uses: cachix/install-nix-action@v16 - uses: cachix/install-nix-action@v18
if: steps.changed-files.outputs.any_changed == 'true' if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests - name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
@@ -38,6 +41,7 @@ jobs:
--name headscale-test-suite \ --name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \ --volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \ --volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \ golang:1 \
go test ./... \ go test ./... \
-tags ts2019 \ -tags ts2019 \
@@ -45,3 +49,15 @@ jobs:
-timeout 120m \ -timeout 120m \
-parallel 1 \ -parallel 1 \
-run "^TestOIDCAuthenticationPingAll$" -run "^TestOIDCAuthenticationPingAll$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"

View File

@@ -0,0 +1,63 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestOIDCExpireNodesBasedOnTokenExpiry
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestOIDCExpireNodesBasedOnTokenExpiry$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"

View File

@@ -1,4 +1,3 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go # DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/ # To regenerate, run "go generate" in cmd/gh-action-integration-generator/
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestPingAllByHostname
on: [pull_request] on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -26,8 +29,8 @@ jobs:
integration_test/ integration_test/
config-example.yaml config-example.yaml
- uses: cachix/install-nix-action@v16 - uses: cachix/install-nix-action@v18
if: steps.changed-files.outputs.any_changed == 'true' if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests - name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
@@ -38,6 +41,7 @@ jobs:
--name headscale-test-suite \ --name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \ --volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \ --volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \ golang:1 \
go test ./... \ go test ./... \
-tags ts2019 \ -tags ts2019 \
@@ -45,3 +49,15 @@ jobs:
-timeout 120m \ -timeout 120m \
-parallel 1 \ -parallel 1 \
-run "^TestPingAllByHostname$" -run "^TestPingAllByHostname$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"

View File

@@ -1,4 +1,3 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go # DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/ # To regenerate, run "go generate" in cmd/gh-action-integration-generator/
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestPingAllByIP
on: [pull_request] on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -26,8 +29,8 @@ jobs:
integration_test/ integration_test/
config-example.yaml config-example.yaml
- uses: cachix/install-nix-action@v16 - uses: cachix/install-nix-action@v18
if: steps.changed-files.outputs.any_changed == 'true' if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests - name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
@@ -38,6 +41,7 @@ jobs:
--name headscale-test-suite \ --name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \ --volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \ --volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \ golang:1 \
go test ./... \ go test ./... \
-tags ts2019 \ -tags ts2019 \
@@ -45,3 +49,15 @@ jobs:
-timeout 120m \ -timeout 120m \
-parallel 1 \ -parallel 1 \
-run "^TestPingAllByIP$" -run "^TestPingAllByIP$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"

View File

@@ -1,4 +1,3 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go # DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/ # To regenerate, run "go generate" in cmd/gh-action-integration-generator/
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestPreAuthKeyCommand
on: [pull_request] on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -26,8 +29,8 @@ jobs:
integration_test/ integration_test/
config-example.yaml config-example.yaml
- uses: cachix/install-nix-action@v16 - uses: cachix/install-nix-action@v18
if: steps.changed-files.outputs.any_changed == 'true' if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests - name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
@@ -38,6 +41,7 @@ jobs:
--name headscale-test-suite \ --name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \ --volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \ --volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \ golang:1 \
go test ./... \ go test ./... \
-tags ts2019 \ -tags ts2019 \
@@ -45,3 +49,15 @@ jobs:
-timeout 120m \ -timeout 120m \
-parallel 1 \ -parallel 1 \
-run "^TestPreAuthKeyCommand$" -run "^TestPreAuthKeyCommand$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"

View File

@@ -1,4 +1,3 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go # DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/ # To regenerate, run "go generate" in cmd/gh-action-integration-generator/
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestPreAuthKeyCommandReusableEphemeral
on: [pull_request] on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -26,8 +29,8 @@ jobs:
integration_test/ integration_test/
config-example.yaml config-example.yaml
- uses: cachix/install-nix-action@v16 - uses: cachix/install-nix-action@v18
if: steps.changed-files.outputs.any_changed == 'true' if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests - name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
@@ -38,6 +41,7 @@ jobs:
--name headscale-test-suite \ --name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \ --volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \ --volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \ golang:1 \
go test ./... \ go test ./... \
-tags ts2019 \ -tags ts2019 \
@@ -45,3 +49,15 @@ jobs:
-timeout 120m \ -timeout 120m \
-parallel 1 \ -parallel 1 \
-run "^TestPreAuthKeyCommandReusableEphemeral$" -run "^TestPreAuthKeyCommandReusableEphemeral$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"

View File

@@ -1,4 +1,3 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go # DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/ # To regenerate, run "go generate" in cmd/gh-action-integration-generator/
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestPreAuthKeyCommandWithoutExpiry
on: [pull_request] on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -26,8 +29,8 @@ jobs:
integration_test/ integration_test/
config-example.yaml config-example.yaml
- uses: cachix/install-nix-action@v16 - uses: cachix/install-nix-action@v18
if: steps.changed-files.outputs.any_changed == 'true' if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests - name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
@@ -38,6 +41,7 @@ jobs:
--name headscale-test-suite \ --name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \ --volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \ --volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \ golang:1 \
go test ./... \ go test ./... \
-tags ts2019 \ -tags ts2019 \
@@ -45,3 +49,15 @@ jobs:
-timeout 120m \ -timeout 120m \
-parallel 1 \ -parallel 1 \
-run "^TestPreAuthKeyCommandWithoutExpiry$" -run "^TestPreAuthKeyCommandWithoutExpiry$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"

View File

@@ -1,4 +1,3 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go # DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/ # To regenerate, run "go generate" in cmd/gh-action-integration-generator/
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestResolveMagicDNS
on: [pull_request] on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -26,8 +29,8 @@ jobs:
integration_test/ integration_test/
config-example.yaml config-example.yaml
- uses: cachix/install-nix-action@v16 - uses: cachix/install-nix-action@v18
if: steps.changed-files.outputs.any_changed == 'true' if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests - name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
@@ -38,6 +41,7 @@ jobs:
--name headscale-test-suite \ --name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \ --volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \ --volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \ golang:1 \
go test ./... \ go test ./... \
-tags ts2019 \ -tags ts2019 \
@@ -45,3 +49,15 @@ jobs:
-timeout 120m \ -timeout 120m \
-parallel 1 \ -parallel 1 \
-run "^TestResolveMagicDNS$" -run "^TestResolveMagicDNS$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"

View File

@@ -1,4 +1,3 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go # DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/ # To regenerate, run "go generate" in cmd/gh-action-integration-generator/
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestSSHIsBlockedInACL
on: [pull_request] on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -26,8 +29,8 @@ jobs:
integration_test/ integration_test/
config-example.yaml config-example.yaml
- uses: cachix/install-nix-action@v16 - uses: cachix/install-nix-action@v18
if: steps.changed-files.outputs.any_changed == 'true' if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests - name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
@@ -38,6 +41,7 @@ jobs:
--name headscale-test-suite \ --name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \ --volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \ --volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \ golang:1 \
go test ./... \ go test ./... \
-tags ts2019 \ -tags ts2019 \
@@ -45,3 +49,15 @@ jobs:
-timeout 120m \ -timeout 120m \
-parallel 1 \ -parallel 1 \
-run "^TestSSHIsBlockedInACL$" -run "^TestSSHIsBlockedInACL$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"

View File

@@ -0,0 +1,63 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestSSHMultipleUsersAllToAll
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestSSHMultipleUsersAllToAll$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"

View File

@@ -1,4 +1,3 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go # DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/ # To regenerate, run "go generate" in cmd/gh-action-integration-generator/
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestSSHNoSSHConfigured
on: [pull_request] on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -26,8 +29,8 @@ jobs:
integration_test/ integration_test/
config-example.yaml config-example.yaml
- uses: cachix/install-nix-action@v16 - uses: cachix/install-nix-action@v18
if: steps.changed-files.outputs.any_changed == 'true' if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests - name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
@@ -38,6 +41,7 @@ jobs:
--name headscale-test-suite \ --name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \ --volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \ --volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \ golang:1 \
go test ./... \ go test ./... \
-tags ts2019 \ -tags ts2019 \
@@ -45,3 +49,15 @@ jobs:
-timeout 120m \ -timeout 120m \
-parallel 1 \ -parallel 1 \
-run "^TestSSHNoSSHConfigured$" -run "^TestSSHNoSSHConfigured$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"

View File

@@ -0,0 +1,63 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestSSHOneUserAllToAll
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestSSHOneUserAllToAll$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"

View File

@@ -1,47 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestSSNamespaceOnlyIsolation
on: [pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v16
if: steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestSSNamespaceOnlyIsolation$"

View File

@@ -0,0 +1,63 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestSSUserOnlyIsolation
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestSSUserOnlyIsolation$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"

View File

@@ -1,4 +1,3 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go # DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/ # To regenerate, run "go generate" in cmd/gh-action-integration-generator/
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestTaildrop
on: [pull_request] on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -26,8 +29,8 @@ jobs:
integration_test/ integration_test/
config-example.yaml config-example.yaml
- uses: cachix/install-nix-action@v16 - uses: cachix/install-nix-action@v18
if: steps.changed-files.outputs.any_changed == 'true' if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests - name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
@@ -38,6 +41,7 @@ jobs:
--name headscale-test-suite \ --name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \ --volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \ --volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \ golang:1 \
go test ./... \ go test ./... \
-tags ts2019 \ -tags ts2019 \
@@ -45,3 +49,15 @@ jobs:
-timeout 120m \ -timeout 120m \
-parallel 1 \ -parallel 1 \
-run "^TestTaildrop$" -run "^TestTaildrop$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"

View File

@@ -1,4 +1,3 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go # DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/ # To regenerate, run "go generate" in cmd/gh-action-integration-generator/
@@ -6,6 +5,10 @@ name: Integration Test v2 - TestTailscaleNodesJoiningHeadcale
on: [pull_request] on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -26,8 +29,8 @@ jobs:
integration_test/ integration_test/
config-example.yaml config-example.yaml
- uses: cachix/install-nix-action@v16 - uses: cachix/install-nix-action@v18
if: steps.changed-files.outputs.any_changed == 'true' if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests - name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
@@ -38,6 +41,7 @@ jobs:
--name headscale-test-suite \ --name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \ --volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \ --volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \ golang:1 \
go test ./... \ go test ./... \
-tags ts2019 \ -tags ts2019 \
@@ -45,3 +49,15 @@ jobs:
-timeout 120m \ -timeout 120m \
-parallel 1 \ -parallel 1 \
-run "^TestTailscaleNodesJoiningHeadcale$" -run "^TestTailscaleNodesJoiningHeadcale$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"

View File

@@ -1,11 +1,14 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go # DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/ # To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestNamespaceCommand name: Integration Test v2 - TestUserCommand
on: [pull_request] on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -26,8 +29,8 @@ jobs:
integration_test/ integration_test/
config-example.yaml config-example.yaml
- uses: cachix/install-nix-action@v16 - uses: cachix/install-nix-action@v18
if: steps.changed-files.outputs.any_changed == 'true' if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests - name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
@@ -38,10 +41,23 @@ jobs:
--name headscale-test-suite \ --name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \ --volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \ --volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \ golang:1 \
go test ./... \ go test ./... \
-tags ts2019 \ -tags ts2019 \
-failfast \ -failfast \
-timeout 120m \ -timeout 120m \
-parallel 1 \ -parallel 1 \
-run "^TestNamespaceCommand$" -run "^TestUserCommand$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"

View File

@@ -2,6 +2,10 @@ name: Tests
on: [push, pull_request] on: [push, pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest

12
.gitignore vendored
View File

@@ -1,3 +1,5 @@
ignored/
# Binaries for programs and plugins # Binaries for programs and plugins
*.exe *.exe
*.exe~ *.exe~
@@ -12,8 +14,9 @@
*.out *.out
# Dependency directories (remove the comment below to include it) # Dependency directories (remove the comment below to include it)
# vendor/ vendor/
dist/
/headscale /headscale
config.json config.json
config.yaml config.yaml
@@ -26,10 +29,15 @@ derp.yaml
# Exclude Jetbrains Editors # Exclude Jetbrains Editors
.idea .idea
test_output/ test_output/
control_logs/
# Nix build output # Nix build output
result result
.direnv/ .direnv/
integration_test/etc/config.dump.yaml integration_test/etc/config.dump.yaml
# MkDocs
.cache
/site

View File

@@ -29,6 +29,14 @@ linters:
- execinquery - execinquery
- exhaustruct - exhaustruct
- nolintlint - nolintlint
- musttag # causes issues with imported libs
# deprecated
- structcheck # replaced by unused
- ifshort # deprecated by the owner
- varcheck # replaced by unused
- nosnakecase # replaced by revive
- deadcode # replaced by unused
# We should strive to enable these: # We should strive to enable these:
- wrapcheck - wrapcheck

View File

@@ -1,21 +1,28 @@
--- ---
before: before:
hooks: hooks:
- go mod tidy -compat=1.19 - go mod tidy -compat=1.20
- go mod vendor
release: release:
prerelease: auto prerelease: auto
builds: builds:
- id: darwin-amd64 - id: headscale
main: ./cmd/headscale/headscale.go main: ./cmd/headscale/headscale.go
mod_timestamp: "{{ .CommitTimestamp }}" mod_timestamp: "{{ .CommitTimestamp }}"
env: env:
- CGO_ENABLED=0 - CGO_ENABLED=0
goos: targets:
- darwin - darwin_amd64
goarch: - darwin_arm64
- amd64 - freebsd_amd64
- linux_386
- linux_amd64
- linux_arm64
- linux_arm_5
- linux_arm_6
- linux_arm_7
flags: flags:
- -mod=readonly - -mod=readonly
ldflags: ldflags:
@@ -23,60 +30,56 @@ builds:
tags: tags:
- ts2019 - ts2019
- id: darwin-arm64
main: ./cmd/headscale/headscale.go
mod_timestamp: "{{ .CommitTimestamp }}"
env:
- CGO_ENABLED=0
goos:
- darwin
goarch:
- arm64
flags:
- -mod=readonly
ldflags:
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
tags:
- ts2019
- id: linux-amd64
mod_timestamp: "{{ .CommitTimestamp }}"
env:
- CGO_ENABLED=0
goos:
- linux
goarch:
- amd64
main: ./cmd/headscale/headscale.go
ldflags:
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
tags:
- ts2019
- id: linux-arm64
mod_timestamp: "{{ .CommitTimestamp }}"
env:
- CGO_ENABLED=0
goos:
- linux
goarch:
- arm64
main: ./cmd/headscale/headscale.go
ldflags:
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
tags:
- ts2019
archives: archives:
- id: golang-cross - id: golang-cross
builds: name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
- darwin-amd64
- darwin-arm64
- linux-amd64
- linux-arm64
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
format: binary format: binary
source:
enabled: true
name_template: "{{ .ProjectName }}_{{ .Version }}"
format: tar.gz
files:
- "vendor/"
nfpms:
# Configure nFPM for .deb and .rpm releases
#
# See https://nfpm.goreleaser.com/configuration/
# and https://goreleaser.com/customization/nfpm/
#
# Useful tools for debugging .debs:
# List file contents: dpkg -c dist/headscale...deb
# Package metadata: dpkg --info dist/headscale....deb
#
- builds:
- headscale
package_name: headscale
priority: optional
vendor: headscale
maintainer: Kristoffer Dalby <kristoffer@dalby.cc>
homepage: https://github.com/juanfont/headscale
license: BSD
bindir: /usr/bin
formats:
- deb
# - rpm
contents:
- src: ./config-example.yaml
dst: /etc/headscale/config.yaml
type: config|noreplace
file_info:
mode: 0644
- src: ./docs/packaging/headscale.systemd.service
dst: /usr/lib/systemd/system/headscale.service
- dst: /var/lib/headscale
type: dir
- dst: /var/run/headscale
type: dir
scripts:
postinstall: ./docs/packaging/postinstall.sh
postremove: ./docs/packaging/postremove.sh
checksum: checksum:
name_template: "checksums.txt" name_template: "checksums.txt"
snapshot: snapshot:

View File

@@ -1,6 +1,63 @@
# CHANGELOG # CHANGELOG
## 0.18.x (2022-xx-xx) ## 0.23.0 (2023-XX-XX)
### Changes
- Add environment flags to enable pprof (profiling) [#1382](https://github.com/juanfont/headscale/pull/1382)
- Profiles are continously generated in our integration tests.
- Fix systemd service file location in `.deb` packages [#1391](https://github.com/juanfont/headscale/pull/1391)
- Improvements on Noise implementation [#1379](https://github.com/juanfont/headscale/pull/1379)
- Replace node filter logic, ensuring nodes with access can see eachother [#1381](https://github.com/juanfont/headscale/pull/1381)
## 0.22.1 (2023-04-20)
### Changes
- Fix issue where systemd could not bind to port 80 [#1365](https://github.com/juanfont/headscale/pull/1365)
- Disable (or delete) both exit routes at the same time [#1428](https://github.com/juanfont/headscale/pull/1428)
## 0.22.0 (2023-04-20)
### Changes
- Add `.deb` packages to release process [#1297](https://github.com/juanfont/headscale/pull/1297)
- Update and simplify the documentation to use new `.deb` packages [#1349](https://github.com/juanfont/headscale/pull/1349)
- Add 32-bit Arm platforms to release process [#1297](https://github.com/juanfont/headscale/pull/1297)
- Fix longstanding bug that would prevent "\*" from working properly in ACLs (issue [#699](https://github.com/juanfont/headscale/issues/699)) [#1279](https://github.com/juanfont/headscale/pull/1279)
- Fix issue where IPv6 could not be used in, or while using ACLs (part of [#809](https://github.com/juanfont/headscale/issues/809)) [#1339](https://github.com/juanfont/headscale/pull/1339)
- Target Go 1.20 and Tailscale 1.38 for Headscale [#1323](https://github.com/juanfont/headscale/pull/1323)
## 0.21.0 (2023-03-20)
### Changes
- Adding "configtest" CLI command. [#1230](https://github.com/juanfont/headscale/pull/1230)
- Add documentation on connecting with iOS to `/apple` [#1261](https://github.com/juanfont/headscale/pull/1261)
- Update iOS compatibility and added documentation for iOS [#1264](https://github.com/juanfont/headscale/pull/1264)
- Allow to delete routes [#1244](https://github.com/juanfont/headscale/pull/1244)
## 0.20.0 (2023-02-03)
### Changes
- Fix wrong behaviour in exit nodes [#1159](https://github.com/juanfont/headscale/pull/1159)
- Align behaviour of `dns_config.restricted_nameservers` to tailscale [#1162](https://github.com/juanfont/headscale/pull/1162)
- Make OpenID Connect authenticated client expiry time configurable [#1191](https://github.com/juanfont/headscale/pull/1191)
- defaults to 180 days like Tailscale SaaS
- adds option to use the expiry time from the OpenID token for the node (see config-example.yaml)
- Set ControlTime in Map info sent to nodes [#1195](https://github.com/juanfont/headscale/pull/1195)
- Populate Tags field on Node updates sent [#1195](https://github.com/juanfont/headscale/pull/1195)
## 0.19.0 (2023-01-29)
### BREAKING
- Rename Namespace to User [#1144](https://github.com/juanfont/headscale/pull/1144)
- **BACKUP your database before upgrading**
- Command line flags previously taking `--namespace` or `-n` will now require `--user` or `-u`
## 0.18.0 (2023-01-14)
### Changes ### Changes
@@ -13,6 +70,7 @@
- Expire nodes based on OIDC token expiry [#1067](https://github.com/juanfont/headscale/pull/1067) - Expire nodes based on OIDC token expiry [#1067](https://github.com/juanfont/headscale/pull/1067)
- Remove ephemeral nodes on logout [#1098](https://github.com/juanfont/headscale/pull/1098) - Remove ephemeral nodes on logout [#1098](https://github.com/juanfont/headscale/pull/1098)
- Performance improvements in ACLs [#1129](https://github.com/juanfont/headscale/pull/1129) - Performance improvements in ACLs [#1129](https://github.com/juanfont/headscale/pull/1129)
- OIDC client secret can be passed via a file [#1127](https://github.com/juanfont/headscale/pull/1127)
## 0.17.1 (2022-12-05) ## 0.17.1 (2022-12-05)

View File

@@ -1,5 +1,5 @@
# Builder image # Builder image
FROM docker.io/golang:1.19-bullseye AS build FROM docker.io/golang:1.20-bullseye AS build
ARG VERSION=dev ARG VERSION=dev
ENV GOPATH /go ENV GOPATH /go
WORKDIR /go/src/headscale WORKDIR /go/src/headscale

View File

@@ -1,5 +1,5 @@
# Builder image # Builder image
FROM docker.io/golang:1.19-bullseye AS build FROM docker.io/golang:1.20-bullseye AS build
ARG VERSION=dev ARG VERSION=dev
ENV GOPATH /go ENV GOPATH /go
WORKDIR /go/src/headscale WORKDIR /go/src/headscale
@@ -13,7 +13,7 @@ RUN CGO_ENABLED=0 GOOS=linux go install -tags ts2019 -ldflags="-s -w -X github.c
RUN test -e /go/bin/headscale RUN test -e /go/bin/headscale
# Debug image # Debug image
FROM docker.io/golang:1.19.0-bullseye FROM docker.io/golang:1.20.0-bullseye
COPY --from=build /go/bin/headscale /bin/headscale COPY --from=build /go/bin/headscale /bin/headscale
ENV TZ UTC ENV TZ UTC

View File

@@ -1,19 +1,16 @@
FROM ubuntu:latest FROM ubuntu:22.04
ARG TAILSCALE_VERSION=* ARG TAILSCALE_VERSION=*
ARG TAILSCALE_CHANNEL=stable ARG TAILSCALE_CHANNEL=stable
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y gnupg curl ssh \ && apt-get install -y gnupg curl ssh dnsutils ca-certificates \
&& curl -fsSL https://pkgs.tailscale.com/${TAILSCALE_CHANNEL}/ubuntu/focal.gpg | apt-key add - \ && adduser --shell=/bin/bash ssh-it-user
# Tailscale is deliberately split into a second stage so we can cash utils as a seperate layer.
RUN curl -fsSL https://pkgs.tailscale.com/${TAILSCALE_CHANNEL}/ubuntu/focal.gpg | apt-key add - \
&& curl -fsSL https://pkgs.tailscale.com/${TAILSCALE_CHANNEL}/ubuntu/focal.list | tee /etc/apt/sources.list.d/tailscale.list \ && curl -fsSL https://pkgs.tailscale.com/${TAILSCALE_CHANNEL}/ubuntu/focal.list | tee /etc/apt/sources.list.d/tailscale.list \
&& apt-get update \ && apt-get update \
&& apt-get install -y ca-certificates tailscale=${TAILSCALE_VERSION} dnsutils \ && apt-get install -y tailscale=${TAILSCALE_VERSION} \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
RUN adduser --shell=/bin/bash ssh-it-user
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

@@ -1,7 +1,7 @@
FROM golang:latest FROM golang:latest
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y ca-certificates dnsutils git iptables ssh \ && apt-get install -y dnsutils git iptables ssh ca-certificates \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
RUN useradd --shell=/bin/bash --create-home ssh-it-user RUN useradd --shell=/bin/bash --create-home ssh-it-user
@@ -10,15 +10,8 @@ RUN git clone https://github.com/tailscale/tailscale.git
WORKDIR /go/tailscale WORKDIR /go/tailscale
RUN git checkout main RUN git checkout main \
&& sh build_dist.sh tailscale.com/cmd/tailscale \
RUN sh build_dist.sh tailscale.com/cmd/tailscale && sh build_dist.sh tailscale.com/cmd/tailscaled \
RUN sh build_dist.sh tailscale.com/cmd/tailscaled && cp tailscale /usr/local/bin/ \
&& cp tailscaled /usr/local/bin/
RUN cp tailscale /usr/local/bin/
RUN cp tailscaled /usr/local/bin/
ADD integration_test/etc_embedded_derp/tls/server.crt /usr/local/share/ca-certificates/
RUN chmod 644 /usr/local/share/ca-certificates/server.crt
RUN update-ca-certificates

View File

@@ -26,7 +26,7 @@ dev: lint test build
test: test:
@go test $(TAGS) -short -coverprofile=coverage.out ./... @go test $(TAGS) -short -coverprofile=coverage.out ./...
test_integration: test_integration_cli test_integration_derp test_integration_oidc test_integration_v2_general test_integration: test_integration_cli test_integration_derp test_integration_v2_general
test_integration_cli: test_integration_cli:
docker network rm $$(docker network ls --filter name=headscale --quiet) || true docker network rm $$(docker network ls --filter name=headscale --quiet) || true
@@ -36,17 +36,7 @@ test_integration_cli:
-v ~/.cache/hs-integration-go:/go \ -v ~/.cache/hs-integration-go:/go \
-v $$PWD:$$PWD -w $$PWD \ -v $$PWD:$$PWD -w $$PWD \
-v /var/run/docker.sock:/var/run/docker.sock golang:1 \ -v /var/run/docker.sock:/var/run/docker.sock golang:1 \
go test $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationCLI ./... go run gotest.tools/gotestsum@latest -- $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationCLI ./...
test_integration_derp:
docker network rm $$(docker network ls --filter name=headscale --quiet) || true
docker network create headscale-test || true
docker run -t --rm \
--network headscale-test \
-v ~/.cache/hs-integration-go:/go \
-v $$PWD:$$PWD -w $$PWD \
-v /var/run/docker.sock:/var/run/docker.sock golang:1 \
go test $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationDERP ./...
test_integration_v2_general: test_integration_v2_general:
docker run \ docker run \
@@ -56,13 +46,7 @@ test_integration_v2_general:
-v $$PWD:$$PWD -w $$PWD/integration \ -v $$PWD:$$PWD -w $$PWD/integration \
-v /var/run/docker.sock:/var/run/docker.sock \ -v /var/run/docker.sock:/var/run/docker.sock \
golang:1 \ golang:1 \
go test $(TAGS) -failfast ./... -timeout 120m -parallel 8 go run gotest.tools/gotestsum@latest -- $(TAGS) -failfast ./... -timeout 120m -parallel 8
coverprofile_func:
go tool cover -func=coverage.out
coverprofile_html:
go tool cover -html=coverage.out
lint: lint:
golangci-lint run --fix --timeout 10m golangci-lint run --fix --timeout 10m
@@ -80,11 +64,4 @@ compress: build
generate: generate:
rm -rf gen rm -rf gen
go run github.com/bufbuild/buf/cmd/buf generate proto buf generate proto
install-protobuf-plugins:
go install \
github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway \
github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2 \
google.golang.org/protobuf/cmd/protoc-gen-go \
google.golang.org/grpc/cmd/protoc-gen-go-grpc

377
README.md
View File

@@ -32,22 +32,18 @@ organisation.
## Design goal ## Design goal
`headscale` aims to implement a self-hosted, open source alternative to the Tailscale Headscale aims to implement a self-hosted, open source alternative to the Tailscale
control server. `headscale` has a narrower scope and an instance of `headscale` control server.
implements a _single_ Tailnet, which is typically what a single organisation, or Headscale's goal is to provide self-hosters and hobbyists with an open-source
home/personal setup would use. server they can use for their projects and labs.
It implements a narrow scope, a single Tailnet, suitable for a personal use, or a small
open-source organisation.
`headscale` uses terms that maps to Tailscale's control server, consult the ## Supporting Headscale
[glossary](./docs/glossary.md) for explainations.
## Support
If you like `headscale` and find it useful, there is a sponsorship and donation If you like `headscale` and find it useful, there is a sponsorship and donation
buttons available in the repo. buttons available in the repo.
If you would like to sponsor features, bugs or prioritisation, reach out to
one of the maintainers.
## Features ## Features
- Full "base" support of Tailscale's features - Full "base" support of Tailscale's features
@@ -75,19 +71,48 @@ one of the maintainers.
| macOS | Yes (see `/apple` on your headscale for more information) | | macOS | Yes (see `/apple` on your headscale for more information) |
| Windows | Yes [docs](./docs/windows-client.md) | | Windows | Yes [docs](./docs/windows-client.md) |
| Android | Yes [docs](./docs/android-client.md) | | Android | Yes [docs](./docs/android-client.md) |
| iOS | Not yet | | iOS | Yes [docs](./docs/iOS-client.md) |
## Running headscale ## Running headscale
Please have a look at the documentation under [`docs/`](docs/). **Please note that we do not support nor encourage the use of reverse proxies
and container to run Headscale.**
Please have a look at the [`documentation`](https://headscale.net/).
## Graphical Control Panels
Headscale provides an API for complete management of your Tailnet.
These are community projects not directly affiliated with the Headscale project.
| Name | Repository Link | Description | Status |
| --------------- | ---------------------------------------------------- | ------------------------------------------------------ | ------ |
| headscale-webui | [Github](https://github.com/ifargle/headscale-webui) | A simple Headscale web UI for small-scale deployments. | Alpha |
## Talks
- Fosdem 2023 (video): [Headscale: How we are using integration testing to reimplement Tailscale](https://fosdem.org/2023/schedule/event/goheadscale/)
- presented by Juan Font Alonso and Kristoffer Dalby
## Disclaimer ## Disclaimer
1. We have nothing to do with Tailscale, or Tailscale Inc. 1. This project is not associated with Tailscale Inc.
2. The purpose of Headscale is maintaining a working, self-hosted Tailscale control panel. 2. The purpose of Headscale is maintaining a working, self-hosted Tailscale control panel.
## Contributing ## Contributing
Headscale is "Open Source, acknowledged contribution", this means that any
contribution will have to be discussed with the Maintainers before being submitted.
This model has been chosen to reduce the risk of burnout by limiting the
maintenance overhead of reviewing and validating third-party code.
Headscale is open to code contributions for bug fixes without discussion.
If you find mistakes in the documentation, please submit a fix to the documentation.
### Requirements
To contribute to headscale you would need the lastest version of [Go](https://golang.org) To contribute to headscale you would need the lastest version of [Go](https://golang.org)
and [Buf](https://buf.build)(Protobuf generator). and [Buf](https://buf.build)(Protobuf generator).
@@ -95,8 +120,6 @@ We recommend using [Nix](https://nixos.org/) to setup a development environment.
be done with `nix develop`, which will install the tools and give you a shell. be done with `nix develop`, which will install the tools and give you a shell.
This guarantees that you will have the same dev env as `headscale` maintainers. This guarantees that you will have the same dev env as `headscale` maintainers.
PRs and suggestions are welcome.
### Code style ### Code style
To ensure we have some consistency with a growing number of contributions, To ensure we have some consistency with a growing number of contributions,
@@ -174,13 +197,6 @@ make build
<sub style="font-size:14px"><b>Juan Font</b></sub> <sub style="font-size:14px"><b>Juan Font</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/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"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/cure> <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/> <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/>
@@ -202,8 +218,6 @@ make build
<sub style="font-size:14px"><b>Benjamin Roberts</b></sub> <sub style="font-size:14px"><b>Benjamin Roberts</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/reynico> <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/> <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/>
@@ -211,6 +225,15 @@ make build
<sub style="font-size:14px"><b>Nico</b></sub> <sub style="font-size:14px"><b>Nico</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/evenh>
<img src=https://avatars.githubusercontent.com/u/2701536?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Even Holthe/>
<br />
<sub style="font-size:14px"><b>Even Holthe</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/e-zk> <a href=https://github.com/e-zk>
<img src=https://avatars.githubusercontent.com/u/58356365?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=e-zk/> <img src=https://avatars.githubusercontent.com/u/58356365?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=e-zk/>
@@ -239,6 +262,15 @@ make build
<sub style="font-size:14px"><b>unreality</b></sub> <sub style="font-size:14px"><b>unreality</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/mpldr>
<img src=https://avatars.githubusercontent.com/u/33086936?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Moritz Poldrack/>
<br />
<sub style="font-size:14px"><b>Moritz Poldrack</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/ohdearaugustin> <a href=https://github.com/ohdearaugustin>
<img src=https://avatars.githubusercontent.com/u/14001491?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ohdearaugustin/> <img src=https://avatars.githubusercontent.com/u/14001491?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ohdearaugustin/>
@@ -246,13 +278,11 @@ make build
<sub style="font-size:14px"><b>ohdearaugustin</b></sub> <sub style="font-size:14px"><b>ohdearaugustin</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/mpldr> <a href=https://github.com/restanrm>
<img src=https://avatars.githubusercontent.com/u/33086936?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Moritz Poldrack/> <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 /> <br />
<sub style="font-size:14px"><b>Moritz Poldrack</b></sub> <sub style="font-size:14px"><b>Adrien Raffin-Caboisse</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
@@ -262,6 +292,13 @@ make build
<sub style="font-size:14px"><b>GrigoriyMikhalkin</b></sub> <sub style="font-size:14px"><b>GrigoriyMikhalkin</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/christian-heusel>
<img src=https://avatars.githubusercontent.com/u/26827864?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Christian Heusel/>
<br />
<sub style="font-size:14px"><b>Christian Heusel</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/mike-lloyd03> <a href=https://github.com/mike-lloyd03>
<img src=https://avatars.githubusercontent.com/u/49411532?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Mike Lloyd/> <img src=https://avatars.githubusercontent.com/u/49411532?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Mike Lloyd/>
@@ -276,6 +313,8 @@ make build
<sub style="font-size:14px"><b>Anton Schubert</b></sub> <sub style="font-size:14px"><b>Anton Schubert</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/Niek> <a href=https://github.com/Niek>
<img src=https://avatars.githubusercontent.com/u/213140?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Niek van der Maas/> <img src=https://avatars.githubusercontent.com/u/213140?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Niek van der Maas/>
@@ -290,8 +329,6 @@ make build
<sub style="font-size:14px"><b>Eugen Biegler</b></sub> <sub style="font-size:14px"><b>Eugen Biegler</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/617a7a> <a href=https://github.com/617a7a>
<img src=https://avatars.githubusercontent.com/u/67651251?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Azz/> <img src=https://avatars.githubusercontent.com/u/67651251?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Azz/>
@@ -299,13 +336,6 @@ make build
<sub style="font-size:14px"><b>Azz</b></sub> <sub style="font-size:14px"><b>Azz</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/evenh>
<img src=https://avatars.githubusercontent.com/u/2701536?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Even Holthe/>
<br />
<sub style="font-size:14px"><b>Even Holthe</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/qbit> <a href=https://github.com/qbit>
<img src=https://avatars.githubusercontent.com/u/68368?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Aaron Bieber/> <img src=https://avatars.githubusercontent.com/u/68368?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Aaron Bieber/>
@@ -327,6 +357,15 @@ make build
<sub style="font-size:14px"><b>Laurent Marchaud</b></sub> <sub style="font-size:14px"><b>Laurent Marchaud</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/majst01>
<img src=https://avatars.githubusercontent.com/u/410110?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Stefan Majer/>
<br />
<sub style="font-size:14px"><b>Stefan Majer</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/fdelucchijr> <a href=https://github.com/fdelucchijr>
<img src=https://avatars.githubusercontent.com/u/69133647?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Fernando De Lucchi/> <img src=https://avatars.githubusercontent.com/u/69133647?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Fernando De Lucchi/>
@@ -334,8 +373,6 @@ make build
<sub style="font-size:14px"><b>Fernando De Lucchi</b></sub> <sub style="font-size:14px"><b>Fernando De Lucchi</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/OrvilleQ> <a href=https://github.com/OrvilleQ>
<img src=https://avatars.githubusercontent.com/u/21377465?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Orville Q. Song/> <img src=https://avatars.githubusercontent.com/u/21377465?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Orville Q. Song/>
@@ -357,6 +394,15 @@ make build
<sub style="font-size:14px"><b>bravechamp</b></sub> <sub style="font-size:14px"><b>bravechamp</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/bravechamp>
<img src=https://avatars.githubusercontent.com/u/48980452?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=bravechamp/>
<br />
<sub style="font-size:14px"><b>bravechamp</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/deonthomasgy> <a href=https://github.com/deonthomasgy>
<img src=https://avatars.githubusercontent.com/u/150036?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Deon Thomas/> <img src=https://avatars.githubusercontent.com/u/150036?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Deon Thomas/>
@@ -378,8 +424,6 @@ make build
<sub style="font-size:14px"><b>ChibangLW</b></sub> <sub style="font-size:14px"><b>ChibangLW</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/mevansam> <a href=https://github.com/mevansam>
<img src=https://avatars.githubusercontent.com/u/403630?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Mevan Samaratunga/> <img src=https://avatars.githubusercontent.com/u/403630?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Mevan Samaratunga/>
@@ -401,6 +445,8 @@ make build
<sub style="font-size:14px"><b>Paul Tötterman</b></sub> <sub style="font-size:14px"><b>Paul Tötterman</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/samson4649> <a href=https://github.com/samson4649>
<img src=https://avatars.githubusercontent.com/u/12725953?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Samuel Lock/> <img src=https://avatars.githubusercontent.com/u/12725953?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Samuel Lock/>
@@ -408,13 +454,6 @@ make build
<sub style="font-size:14px"><b>Samuel Lock</b></sub> <sub style="font-size:14px"><b>Samuel Lock</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/majst01>
<img src=https://avatars.githubusercontent.com/u/410110?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Stefan Majer/>
<br />
<sub style="font-size:14px"><b>Stefan Majer</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/kevin1sMe> <a href=https://github.com/kevin1sMe>
<img src=https://avatars.githubusercontent.com/u/6886076?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=kevinlin/> <img src=https://avatars.githubusercontent.com/u/6886076?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=kevinlin/>
@@ -422,8 +461,13 @@ make build
<sub style="font-size:14px"><b>kevinlin</b></sub> <sub style="font-size:14px"><b>kevinlin</b></sub>
</a> </a>
</td> </td>
</tr> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<tr> <a href=https://github.com/QZAiXH>
<img src=https://avatars.githubusercontent.com/u/23068780?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Snack/>
<br />
<sub style="font-size:14px"><b>Snack</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/artemklevtsov> <a href=https://github.com/artemklevtsov>
<img src=https://avatars.githubusercontent.com/u/603798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Artem Klevtsov/> <img src=https://avatars.githubusercontent.com/u/603798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Artem Klevtsov/>
@@ -438,6 +482,22 @@ make build
<sub style="font-size:14px"><b>Casey Marshall</b></sub> <sub style="font-size:14px"><b>Casey Marshall</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/dbevacqua>
<img src=https://avatars.githubusercontent.com/u/6534306?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=dbevacqua/>
<br />
<sub style="font-size:14px"><b>dbevacqua</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/joshuataylor>
<img src=https://avatars.githubusercontent.com/u/225131?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Josh Taylor/>
<br />
<sub style="font-size:14px"><b>Josh Taylor</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/CNLHC> <a href=https://github.com/CNLHC>
<img src=https://avatars.githubusercontent.com/u/21005146?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=LiuHanCheng/> <img src=https://avatars.githubusercontent.com/u/21005146?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=LiuHanCheng/>
@@ -445,6 +505,13 @@ make build
<sub style="font-size:14px"><b>LiuHanCheng</b></sub> <sub style="font-size:14px"><b>LiuHanCheng</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/motiejus>
<img src=https://avatars.githubusercontent.com/u/107720?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Motiejus Jakštys/>
<br />
<sub style="font-size:14px"><b>Motiejus Jakštys</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/pvinis> <a href=https://github.com/pvinis>
<img src=https://avatars.githubusercontent.com/u/100233?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Pavlos Vinieratos/> <img src=https://avatars.githubusercontent.com/u/100233?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Pavlos Vinieratos/>
@@ -475,13 +542,6 @@ make build
<sub style="font-size:14px"><b>Victor Freire</b></sub> <sub style="font-size:14px"><b>Victor Freire</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/lachy2849>
<img src=https://avatars.githubusercontent.com/u/98844035?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=lachy2849/>
<br />
<sub style="font-size:14px"><b>lachy2849</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/t56k> <a href=https://github.com/t56k>
<img src=https://avatars.githubusercontent.com/u/12165422?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=thomas/> <img src=https://avatars.githubusercontent.com/u/12165422?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=thomas/>
@@ -489,6 +549,13 @@ make build
<sub style="font-size:14px"><b>thomas</b></sub> <sub style="font-size:14px"><b>thomas</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/linsomniac>
<img src=https://avatars.githubusercontent.com/u/466380?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Sean Reifschneider/>
<br />
<sub style="font-size:14px"><b>Sean Reifschneider</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/aberoham> <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/> <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/>
@@ -496,6 +563,13 @@ make build
<sub style="font-size:14px"><b>Abraham Ingersoll</b></sub> <sub style="font-size:14px"><b>Abraham Ingersoll</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/iFargle>
<img src=https://avatars.githubusercontent.com/u/124551390?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Albert Copeland/>
<br />
<sub style="font-size:14px"><b>Albert Copeland</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/puzpuzpuz> <a href=https://github.com/puzpuzpuz>
<img src=https://avatars.githubusercontent.com/u/37772591?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Andrei Pechkurov/> <img src=https://avatars.githubusercontent.com/u/37772591?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Andrei Pechkurov/>
@@ -503,6 +577,15 @@ make build
<sub style="font-size:14px"><b>Andrei Pechkurov</b></sub> <sub style="font-size:14px"><b>Andrei Pechkurov</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/theryecatcher>
<img src=https://avatars.githubusercontent.com/u/16442416?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Anoop Sundaresh/>
<br />
<sub style="font-size:14px"><b>Anoop Sundaresh</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/apognu> <a href=https://github.com/apognu>
<img src=https://avatars.githubusercontent.com/u/3017182?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Antoine POPINEAU/> <img src=https://avatars.githubusercontent.com/u/3017182?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Antoine POPINEAU/>
@@ -510,8 +593,13 @@ make build
<sub style="font-size:14px"><b>Antoine POPINEAU</b></sub> <sub style="font-size:14px"><b>Antoine POPINEAU</b></sub>
</a> </a>
</td> </td>
</tr> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<tr> <a href=https://github.com/tony1661>
<img src=https://avatars.githubusercontent.com/u/5287266?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Antonio Fernandez/>
<br />
<sub style="font-size:14px"><b>Antonio Fernandez</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/aofei> <a href=https://github.com/aofei>
<img src=https://avatars.githubusercontent.com/u/5037285?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Aofei Sheng/> <img src=https://avatars.githubusercontent.com/u/5037285?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Aofei Sheng/>
@@ -527,12 +615,14 @@ make build
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/awoimbee> <a href=https://github.com/avirut>
<img src=https://avatars.githubusercontent.com/u/22431493?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Arthur Woimbée/> <img src=https://avatars.githubusercontent.com/u/27095602?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Avirut Mehta/>
<br /> <br />
<sub style="font-size:14px"><b>Arthur Woimbée</b></sub> <sub style="font-size:14px"><b>Avirut Mehta</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/stensonb> <a href=https://github.com/stensonb>
<img src=https://avatars.githubusercontent.com/u/933389?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Bryan Stenson/> <img src=https://avatars.githubusercontent.com/u/933389?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Bryan Stenson/>
@@ -554,8 +644,13 @@ make build
<sub style="font-size:14px"><b>kundel</b></sub> <sub style="font-size:14px"><b>kundel</b></sub>
</a> </a>
</td> </td>
</tr> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<tr> <a href=https://github.com/fatih-acar>
<img src=https://avatars.githubusercontent.com/u/15028881?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=fatih-acar/>
<br />
<sub style="font-size:14px"><b>fatih-acar</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/fkr> <a href=https://github.com/fkr>
<img src=https://avatars.githubusercontent.com/u/51063?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Felix Kronlage-Dammers/> <img src=https://avatars.githubusercontent.com/u/51063?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Felix Kronlage-Dammers/>
@@ -570,6 +665,15 @@ make build
<sub style="font-size:14px"><b>Felix Yan</b></sub> <sub style="font-size:14px"><b>Felix Yan</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/gabe565>
<img src=https://avatars.githubusercontent.com/u/7717888?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Gabe Cook/>
<br />
<sub style="font-size:14px"><b>Gabe Cook</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/JJGadgets> <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/> <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/>
@@ -577,6 +681,13 @@ make build
<sub style="font-size:14px"><b>JJGadgets</b></sub> <sub style="font-size:14px"><b>JJGadgets</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/hrtkpf>
<img src=https://avatars.githubusercontent.com/u/42646788?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=hrtkpf/>
<br />
<sub style="font-size:14px"><b>hrtkpf</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/jimt> <a href=https://github.com/jimt>
<img src=https://avatars.githubusercontent.com/u/180326?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jim Tittsler/> <img src=https://avatars.githubusercontent.com/u/180326?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jim Tittsler/>
@@ -584,6 +695,22 @@ make build
<sub style="font-size:14px"><b>Jim Tittsler</b></sub> <sub style="font-size:14px"><b>Jim Tittsler</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/jsiebens>
<img src=https://avatars.githubusercontent.com/u/499769?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Johan Siebens/>
<br />
<sub style="font-size:14px"><b>Johan Siebens</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/johnae>
<img src=https://avatars.githubusercontent.com/u/28332?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=John Axel Eriksson/>
<br />
<sub style="font-size:14px"><b>John Axel Eriksson</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/ShadowJonathan> <a href=https://github.com/ShadowJonathan>
<img src=https://avatars.githubusercontent.com/u/22740616?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jonathan de Jong/> <img src=https://avatars.githubusercontent.com/u/22740616?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jonathan de Jong/>
@@ -591,6 +718,43 @@ make build
<sub style="font-size:14px"><b>Jonathan de Jong</b></sub> <sub style="font-size:14px"><b>Jonathan de Jong</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/JulienFloris>
<img src=https://avatars.githubusercontent.com/u/20380255?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Julien Zweverink/>
<br />
<sub style="font-size:14px"><b>Julien Zweverink</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/win-t>
<img src=https://avatars.githubusercontent.com/u/1589120?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Kurnia D Win/>
<br />
<sub style="font-size:14px"><b>Kurnia D Win</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/foxtrot>
<img src=https://avatars.githubusercontent.com/u/4153572?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Marc/>
<br />
<sub style="font-size:14px"><b>Marc</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/magf>
<img src=https://avatars.githubusercontent.com/u/11992737?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Maxim Gajdaj/>
<br />
<sub style="font-size:14px"><b>Maxim Gajdaj</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/mikejsavage>
<img src=https://avatars.githubusercontent.com/u/579299?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Michael Savage/>
<br />
<sub style="font-size:14px"><b>Michael Savage</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/piec> <a href=https://github.com/piec>
<img src=https://avatars.githubusercontent.com/u/781471?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Pierre Carru/> <img src=https://avatars.githubusercontent.com/u/781471?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Pierre Carru/>
@@ -598,8 +762,6 @@ make build
<sub style="font-size:14px"><b>Pierre Carru</b></sub> <sub style="font-size:14px"><b>Pierre Carru</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/Donran> <a href=https://github.com/Donran>
<img src=https://avatars.githubusercontent.com/u/4838348?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Pontus N/> <img src=https://avatars.githubusercontent.com/u/4838348?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Pontus N/>
@@ -635,6 +797,8 @@ make build
<sub style="font-size:14px"><b>Ryan Fowler</b></sub> <sub style="font-size:14px"><b>Ryan Fowler</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/shaananc> <a href=https://github.com/shaananc>
<img src=https://avatars.githubusercontent.com/u/2287839?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Shaanan Cohney/> <img src=https://avatars.githubusercontent.com/u/2287839?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Shaanan Cohney/>
@@ -642,8 +806,6 @@ make build
<sub style="font-size:14px"><b>Shaanan Cohney</b></sub> <sub style="font-size:14px"><b>Shaanan Cohney</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/stefanvanburen> <a href=https://github.com/stefanvanburen>
<img src=https://avatars.githubusercontent.com/u/622527?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Stefan VanBuren/> <img src=https://avatars.githubusercontent.com/u/622527?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Stefan VanBuren/>
@@ -672,6 +834,15 @@ make build
<sub style="font-size:14px"><b>Teteros</b></sub> <sub style="font-size:14px"><b>Teteros</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/Teteros>
<img src=https://avatars.githubusercontent.com/u/5067989?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Teteros/>
<br />
<sub style="font-size:14px"><b>Teteros</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/gitter-badger> <a href=https://github.com/gitter-badger>
<img src=https://avatars.githubusercontent.com/u/8518239?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=The Gitter Badger/> <img src=https://avatars.githubusercontent.com/u/8518239?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=The Gitter Badger/>
@@ -686,8 +857,6 @@ make build
<sub style="font-size:14px"><b>Tianon Gravi</b></sub> <sub style="font-size:14px"><b>Tianon Gravi</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/thetillhoff> <a href=https://github.com/thetillhoff>
<img src=https://avatars.githubusercontent.com/u/25052289?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Till Hoffmann/> <img src=https://avatars.githubusercontent.com/u/25052289?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Till Hoffmann/>
@@ -716,11 +885,13 @@ make build
<sub style="font-size:14px"><b>Yujie Xia</b></sub> <sub style="font-size:14px"><b>Yujie Xia</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/newellz2> <a href=https://github.com/newellz2>
<img src=https://avatars.githubusercontent.com/u/52436542?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Zachary N./> <img src=https://avatars.githubusercontent.com/u/52436542?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Zachary Newell/>
<br /> <br />
<sub style="font-size:14px"><b>Zachary N.</b></sub> <sub style="font-size:14px"><b>Zachary Newell</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
@@ -730,8 +901,6 @@ make build
<sub style="font-size:14px"><b>Zakhar Bessarab</b></sub> <sub style="font-size:14px"><b>Zakhar Bessarab</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/zhzy0077> <a href=https://github.com/zhzy0077>
<img src=https://avatars.githubusercontent.com/u/8717471?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Zhiyuan Zheng/> <img src=https://avatars.githubusercontent.com/u/8717471?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Zhiyuan Zheng/>
@@ -746,6 +915,13 @@ make build
<sub style="font-size:14px"><b>Ziyuan Han</b></sub> <sub style="font-size:14px"><b>Ziyuan Han</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/caelansar>
<img src=https://avatars.githubusercontent.com/u/31852257?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=caelansar/>
<br />
<sub style="font-size:14px"><b>caelansar</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/derelm> <a href=https://github.com/derelm>
<img src=https://avatars.githubusercontent.com/u/465155?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=derelm/> <img src=https://avatars.githubusercontent.com/u/465155?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=derelm/>
@@ -753,6 +929,15 @@ make build
<sub style="font-size:14px"><b>derelm</b></sub> <sub style="font-size:14px"><b>derelm</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/dnaq>
<img src=https://avatars.githubusercontent.com/u/1299717?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=dnaq/>
<br />
<sub style="font-size:14px"><b>dnaq</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/nning> <a href=https://github.com/nning>
<img src=https://avatars.githubusercontent.com/u/557430?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=henning mueller/> <img src=https://avatars.githubusercontent.com/u/557430?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=henning mueller/>
@@ -767,6 +952,13 @@ make build
<sub style="font-size:14px"><b>ignoramous</b></sub> <sub style="font-size:14px"><b>ignoramous</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/jimyag>
<img src=https://avatars.githubusercontent.com/u/69233189?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=jimyag/>
<br />
<sub style="font-size:14px"><b>jimyag</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/magichuihui> <a href=https://github.com/magichuihui>
<img src=https://avatars.githubusercontent.com/u/10866198?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=suhelen/> <img src=https://avatars.githubusercontent.com/u/10866198?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=suhelen/>
@@ -774,8 +966,6 @@ make build
<sub style="font-size:14px"><b>suhelen</b></sub> <sub style="font-size:14px"><b>suhelen</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/lion24> <a href=https://github.com/lion24>
<img src=https://avatars.githubusercontent.com/u/1382102?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=sharkonet/> <img src=https://avatars.githubusercontent.com/u/1382102?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=sharkonet/>
@@ -783,6 +973,15 @@ make build
<sub style="font-size:14px"><b>sharkonet</b></sub> <sub style="font-size:14px"><b>sharkonet</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/ma6174>
<img src=https://avatars.githubusercontent.com/u/1449133?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ma6174/>
<br />
<sub style="font-size:14px"><b>ma6174</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/manju-rn> <a href=https://github.com/manju-rn>
<img src=https://avatars.githubusercontent.com/u/26291847?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=manju-rn/> <img src=https://avatars.githubusercontent.com/u/26291847?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=manju-rn/>
@@ -791,10 +990,17 @@ make build
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/pernila> <a href=https://github.com/nicholas-yap>
<img src=https://avatars.githubusercontent.com/u/12460060?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=pernila/> <img src=https://avatars.githubusercontent.com/u/38109533?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=nicholas-yap/>
<br /> <br />
<sub style="font-size:14px"><b>pernila</b></sub> <sub style="font-size:14px"><b>nicholas-yap</b></sub>
</a>
</td>
<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=Tommi Pernila/>
<br />
<sub style="font-size:14px"><b>Tommi Pernila</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
@@ -811,6 +1017,8 @@ make build
<sub style="font-size:14px"><b>Wakeful-Cloud</b></sub> <sub style="font-size:14px"><b>Wakeful-Cloud</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/xpzouying> <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/> <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/>
@@ -818,5 +1026,12 @@ make build
<sub style="font-size:14px"><b>zy</b></sub> <sub style="font-size:14px"><b>zy</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/atorregrosa-smd>
<img src=https://avatars.githubusercontent.com/u/78434679?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Àlex Torregrosa/>
<br />
<sub style="font-size:14px"><b>Àlex Torregrosa</b></sub>
</a>
</td>
</tr> </tr>
</table> </table>

458
acls.go
View File

@@ -14,6 +14,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/tailscale/hujson" "github.com/tailscale/hujson"
"go4.org/netipx"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"tailscale.com/envknob" "tailscale.com/envknob"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
@@ -126,10 +127,11 @@ func (h *Headscale) UpdateACLRules() error {
return errEmptyPolicy return errEmptyPolicy
} }
rules, err := generateACLRules(machines, *h.aclPolicy, h.cfg.OIDC.StripEmaildomain) rules, err := h.aclPolicy.generateFilterRules(machines, h.cfg.OIDC.StripEmaildomain)
if err != nil { if err != nil {
return err return err
} }
log.Trace().Interface("ACL", rules).Msg("ACL rules generated") log.Trace().Interface("ACL", rules).Msg("ACL rules generated")
h.aclRules = rules h.aclRules = rules
@@ -150,20 +152,28 @@ func (h *Headscale) UpdateACLRules() error {
return nil return nil
} }
func generateACLRules(machines []Machine, aclPolicy ACLPolicy, stripEmaildomain bool) ([]tailcfg.FilterRule, error) { // generateFilterRules takes a set of machines and an ACLPolicy and generates a
// set of Tailscale compatible FilterRules used to allow traffic on clients.
func (pol *ACLPolicy) generateFilterRules(
machines []Machine,
stripEmailDomain bool,
) ([]tailcfg.FilterRule, error) {
rules := []tailcfg.FilterRule{} rules := []tailcfg.FilterRule{}
for index, acl := range aclPolicy.ACLs { for index, acl := range pol.ACLs {
if acl.Action != "accept" { if acl.Action != "accept" {
return nil, errInvalidAction return nil, errInvalidAction
} }
srcIPs := []string{} srcIPs := []string{}
for innerIndex, src := range acl.Sources { for srcIndex, src := range acl.Sources {
srcs, err := generateACLPolicySrcIP(machines, aclPolicy, src, stripEmaildomain) srcs, err := pol.getIPsFromSource(src, machines, stripEmailDomain)
if err != nil { if err != nil {
log.Error(). log.Error().
Msgf("Error parsing ACL %d, Source %d", index, innerIndex) Interface("src", src).
Int("ACL index", index).
Int("Src index", srcIndex).
Msgf("Error parsing ACL")
return nil, err return nil, err
} }
@@ -179,17 +189,19 @@ func generateACLRules(machines []Machine, aclPolicy ACLPolicy, stripEmaildomain
} }
destPorts := []tailcfg.NetPortRange{} destPorts := []tailcfg.NetPortRange{}
for innerIndex, dest := range acl.Destinations { for destIndex, dest := range acl.Destinations {
dests, err := generateACLPolicyDest( dests, err := pol.getNetPortRangeFromDestination(
machines,
aclPolicy,
dest, dest,
machines,
needsWildcard, needsWildcard,
stripEmaildomain, stripEmailDomain,
) )
if err != nil { if err != nil {
log.Error(). log.Error().
Msgf("Error parsing ACL %d, Destination %d", index, innerIndex) Interface("dest", dest).
Int("ACL index", index).
Int("dest index", destIndex).
Msgf("Error parsing ACL")
return nil, err return nil, err
} }
@@ -260,22 +272,41 @@ func (h *Headscale) generateSSHRules() ([]*tailcfg.SSHRule, error) {
principals := make([]*tailcfg.SSHPrincipal, 0, len(sshACL.Sources)) principals := make([]*tailcfg.SSHPrincipal, 0, len(sshACL.Sources))
for innerIndex, rawSrc := range sshACL.Sources { for innerIndex, rawSrc := range sshACL.Sources {
expandedSrcs, err := expandAlias( if isWildcard(rawSrc) {
machines,
*h.aclPolicy,
rawSrc,
h.cfg.OIDC.StripEmaildomain,
)
if err != nil {
log.Error().
Msgf("Error parsing SSH %d, Source %d", index, innerIndex)
return nil, err
}
for _, expandedSrc := range expandedSrcs {
principals = append(principals, &tailcfg.SSHPrincipal{ principals = append(principals, &tailcfg.SSHPrincipal{
NodeIP: expandedSrc, Any: true,
}) })
} else if isGroup(rawSrc) {
users, err := h.aclPolicy.getUsersInGroup(rawSrc, h.cfg.OIDC.StripEmaildomain)
if err != nil {
log.Error().
Msgf("Error parsing SSH %d, Source %d", index, innerIndex)
return nil, err
}
for _, user := range users {
principals = append(principals, &tailcfg.SSHPrincipal{
UserLogin: user,
})
}
} else {
expandedSrcs, err := h.aclPolicy.expandAlias(
machines,
rawSrc,
h.cfg.OIDC.StripEmaildomain,
)
if err != nil {
log.Error().
Msgf("Error parsing SSH %d, Source %d", index, innerIndex)
return nil, err
}
for _, expandedSrc := range expandedSrcs.Prefixes() {
principals = append(principals, &tailcfg.SSHPrincipal{
NodeIP: expandedSrc.Addr().String(),
})
}
} }
} }
@@ -284,10 +315,9 @@ func (h *Headscale) generateSSHRules() ([]*tailcfg.SSHRule, error) {
userMap[user] = "=" userMap[user] = "="
} }
rules = append(rules, &tailcfg.SSHRule{ rules = append(rules, &tailcfg.SSHRule{
RuleExpires: nil, Principals: principals,
Principals: principals, SSHUsers: userMap,
SSHUsers: userMap, Action: &action,
Action: &action,
}) })
} }
@@ -311,31 +341,69 @@ func sshCheckAction(duration string) (*tailcfg.SSHAction, error) {
}, nil }, nil
} }
func generateACLPolicySrcIP( // getIPsFromSource returns a set of Source IPs that would be associated
machines []Machine, // with the given src alias.
aclPolicy ACLPolicy, func (pol *ACLPolicy) getIPsFromSource(
src string, src string,
machines []Machine,
stripEmaildomain bool, stripEmaildomain bool,
) ([]string, error) { ) ([]string, error) {
return expandAlias(machines, aclPolicy, src, stripEmaildomain) ipSet, err := pol.expandAlias(machines, src, stripEmaildomain)
if err != nil {
return []string{}, err
}
prefixes := []string{}
for _, prefix := range ipSet.Prefixes() {
prefixes = append(prefixes, prefix.String())
}
return prefixes, nil
} }
func generateACLPolicyDest( // getNetPortRangeFromDestination returns a set of tailcfg.NetPortRange
machines []Machine, // which are associated with the dest alias.
aclPolicy ACLPolicy, func (pol *ACLPolicy) getNetPortRangeFromDestination(
dest string, dest string,
machines []Machine,
needsWildcard bool, needsWildcard bool,
stripEmaildomain bool, stripEmaildomain bool,
) ([]tailcfg.NetPortRange, error) { ) ([]tailcfg.NetPortRange, error) {
tokens := strings.Split(dest, ":") var tokens []string
log.Trace().Str("destination", dest).Msg("generating policy destination")
// Check if there is a IPv4/6:Port combination, IPv6 has more than
// three ":".
tokens = strings.Split(dest, ":")
if len(tokens) < expectedTokenItems || len(tokens) > 3 { if len(tokens) < expectedTokenItems || len(tokens) > 3 {
return nil, errInvalidPortFormat port := tokens[len(tokens)-1]
maybeIPv6Str := strings.TrimSuffix(dest, ":"+port)
log.Trace().Str("maybeIPv6Str", maybeIPv6Str).Msg("")
if maybeIPv6, err := netip.ParseAddr(maybeIPv6Str); err != nil && !maybeIPv6.Is6() {
log.Trace().Err(err).Msg("trying to parse as IPv6")
return nil, fmt.Errorf(
"failed to parse destination, tokens %v: %w",
tokens,
errInvalidPortFormat,
)
} else {
tokens = []string{maybeIPv6Str, port}
}
} }
log.Trace().Strs("tokens", tokens).Msg("generating policy destination")
var alias string var alias string
// We can have here stuff like: // We can have here stuff like:
// git-server:* // git-server:*
// 192.168.1.0/24:22 // 192.168.1.0/24:22
// fd7a:115c:a1e0::2:22
// fd7a:115c:a1e0::2/128:22
// tag:montreal-webserver:80,443 // tag:montreal-webserver:80,443
// tag:api-server:443 // tag:api-server:443
// example-host-1:* // example-host-1:*
@@ -345,9 +413,8 @@ func generateACLPolicyDest(
alias = fmt.Sprintf("%s:%s", tokens[0], tokens[1]) alias = fmt.Sprintf("%s:%s", tokens[0], tokens[1])
} }
expanded, err := expandAlias( expanded, err := pol.expandAlias(
machines, machines,
aclPolicy,
alias, alias,
stripEmaildomain, stripEmaildomain,
) )
@@ -360,11 +427,11 @@ func generateACLPolicyDest(
} }
dests := []tailcfg.NetPortRange{} dests := []tailcfg.NetPortRange{}
for _, d := range expanded { for _, dest := range expanded.Prefixes() {
for _, p := range *ports { for _, port := range *ports {
pr := tailcfg.NetPortRange{ pr := tailcfg.NetPortRange{
IP: d, IP: dest.String(),
Ports: p, Ports: port,
} }
dests = append(dests, pr) dests = append(dests, pr)
} }
@@ -424,128 +491,81 @@ func parseProtocol(protocol string) ([]int, bool, error) {
} }
// expandalias has an input of either // expandalias has an input of either
// - a namespace // - a user
// - a group // - a group
// - a tag // - a tag
// - a host
// - an ip
// - a cidr
// and transform these in IPAddresses. // and transform these in IPAddresses.
func expandAlias( func (pol *ACLPolicy) expandAlias(
machines []Machine, machines Machines,
aclPolicy ACLPolicy,
alias string, alias string,
stripEmailDomain bool, stripEmailDomain bool,
) ([]string, error) { ) (*netipx.IPSet, error) {
ips := []string{} if isWildcard(alias) {
if alias == "*" { return parseIPSet("*", nil)
return []string{"*"}, nil
} }
build := netipx.IPSetBuilder{}
log.Debug(). log.Debug().
Str("alias", alias). Str("alias", alias).
Msg("Expanding") Msg("Expanding")
if strings.HasPrefix(alias, "group:") { // if alias is a group
namespaces, err := expandGroup(aclPolicy, alias, stripEmailDomain) if isGroup(alias) {
if err != nil { return pol.getIPsFromGroup(alias, machines, stripEmailDomain)
return ips, err
}
for _, n := range namespaces {
nodes := filterMachinesByNamespace(machines, n)
for _, node := range nodes {
ips = append(ips, node.IPAddresses.ToStringSlice()...)
}
}
return ips, nil
} }
if strings.HasPrefix(alias, "tag:") { // if alias is a tag
// check for forced tags if isTag(alias) {
for _, machine := range machines { return pol.getIPsFromTag(alias, machines, stripEmailDomain)
if contains(machine.ForcedTags, alias) {
ips = append(ips, machine.IPAddresses.ToStringSlice()...)
}
}
// find tag owners
owners, err := expandTagOwners(aclPolicy, alias, stripEmailDomain)
if err != nil {
if errors.Is(err, errInvalidTag) {
if len(ips) == 0 {
return ips, fmt.Errorf(
"%w. %v isn't owned by a TagOwner and no forced tags are defined",
errInvalidTag,
alias,
)
}
return ips, nil
} else {
return ips, err
}
}
// filter out machines per tag owner
for _, namespace := range owners {
machines := filterMachinesByNamespace(machines, namespace)
for _, machine := range machines {
hi := machine.GetHostInfo()
if contains(hi.RequestTags, alias) {
ips = append(ips, machine.IPAddresses.ToStringSlice()...)
}
}
}
return ips, nil
} }
// if alias is a namespace // if alias is a user
nodes := filterMachinesByNamespace(machines, alias) if ips, err := pol.getIPsForUser(alias, machines, stripEmailDomain); ips != nil {
nodes = excludeCorrectlyTaggedNodes(aclPolicy, nodes, alias, stripEmailDomain) return ips, err
for _, n := range nodes {
ips = append(ips, n.IPAddresses.ToStringSlice()...)
}
if len(ips) > 0 {
return ips, nil
} }
// if alias is an host // if alias is an host
if h, ok := aclPolicy.Hosts[alias]; ok { // Note, this is recursive.
return []string{h.String()}, nil if h, ok := pol.Hosts[alias]; ok {
log.Trace().Str("host", h.String()).Msg("expandAlias got hosts entry")
return pol.expandAlias(machines, h.String(), stripEmailDomain)
} }
// if alias is an IP // if alias is an IP
ip, err := netip.ParseAddr(alias) if ip, err := netip.ParseAddr(alias); err == nil {
if err == nil { return pol.getIPsFromSingleIP(ip, machines)
return []string{ip.String()}, nil
} }
// if alias is an CIDR // if alias is an IP Prefix (CIDR)
cidr, err := netip.ParsePrefix(alias) if prefix, err := netip.ParsePrefix(alias); err == nil {
if err == nil { return pol.getIPsFromIPPrefix(prefix, machines)
return []string{cidr.String()}, nil
} }
log.Warn().Msgf("No IPs found with the alias %v", alias) log.Warn().Msgf("No IPs found with the alias %v", alias)
return ips, nil return build.IPSet()
} }
// excludeCorrectlyTaggedNodes will remove from the list of input nodes the ones // excludeCorrectlyTaggedNodes will remove from the list of input nodes the ones
// that are correctly tagged since they should not be listed as being in the namespace // that are correctly tagged since they should not be listed as being in the user
// we assume in this function that we only have nodes from 1 namespace. // we assume in this function that we only have nodes from 1 user.
func excludeCorrectlyTaggedNodes( func excludeCorrectlyTaggedNodes(
aclPolicy ACLPolicy, aclPolicy *ACLPolicy,
nodes []Machine, nodes []Machine,
namespace string, user string,
stripEmailDomain bool, stripEmailDomain bool,
) []Machine { ) []Machine {
out := []Machine{} out := []Machine{}
tags := []string{} tags := []string{}
for tag := range aclPolicy.TagOwners { for tag := range aclPolicy.TagOwners {
owners, _ := expandTagOwners(aclPolicy, namespace, stripEmailDomain) owners, _ := getTagOwners(aclPolicy, user, stripEmailDomain)
ns := append(owners, namespace) ns := append(owners, user)
if contains(ns, namespace) { if contains(ns, user) {
tags = append(tags, tag) tags = append(tags, tag)
} }
} }
@@ -573,7 +593,7 @@ func excludeCorrectlyTaggedNodes(
} }
func expandPorts(portsStr string, needsWildcard bool) (*[]tailcfg.PortRange, error) { func expandPorts(portsStr string, needsWildcard bool) (*[]tailcfg.PortRange, error) {
if portsStr == "*" { if isWildcard(portsStr) {
return &[]tailcfg.PortRange{ return &[]tailcfg.PortRange{
{First: portRangeBegin, Last: portRangeEnd}, {First: portRangeBegin, Last: portRangeEnd},
}, nil }, nil
@@ -585,6 +605,7 @@ func expandPorts(portsStr string, needsWildcard bool) (*[]tailcfg.PortRange, err
ports := []tailcfg.PortRange{} ports := []tailcfg.PortRange{}
for _, portStr := range strings.Split(portsStr, ",") { for _, portStr := range strings.Split(portsStr, ",") {
log.Trace().Msgf("parsing portstring: %s", portStr)
rang := strings.Split(portStr, "-") rang := strings.Split(portStr, "-")
switch len(rang) { switch len(rang) {
case 1: case 1:
@@ -619,10 +640,10 @@ func expandPorts(portsStr string, needsWildcard bool) (*[]tailcfg.PortRange, err
return &ports, nil return &ports, nil
} }
func filterMachinesByNamespace(machines []Machine, namespace string) []Machine { func filterMachinesByUser(machines []Machine, user string) []Machine {
out := []Machine{} out := []Machine{}
for _, machine := range machines { for _, machine := range machines {
if machine.Namespace.Name == namespace { if machine.User.Name == user {
out = append(out, machine) out = append(out, machine)
} }
} }
@@ -630,15 +651,15 @@ func filterMachinesByNamespace(machines []Machine, namespace string) []Machine {
return out return out
} }
// expandTagOwners will return a list of namespace. An owner can be either a namespace or a group // getTagOwners will return a list of user. An owner can be either a user or a group
// a group cannot be composed of groups. // a group cannot be composed of groups.
func expandTagOwners( func getTagOwners(
aclPolicy ACLPolicy, pol *ACLPolicy,
tag string, tag string,
stripEmailDomain bool, stripEmailDomain bool,
) ([]string, error) { ) ([]string, error) {
var owners []string var owners []string
ows, ok := aclPolicy.TagOwners[tag] ows, ok := pol.TagOwners[tag]
if !ok { if !ok {
return []string{}, fmt.Errorf( return []string{}, fmt.Errorf(
"%w. %v isn't owned by a TagOwner. Please add one first. https://tailscale.com/kb/1018/acls/#tag-owners", "%w. %v isn't owned by a TagOwner. Please add one first. https://tailscale.com/kb/1018/acls/#tag-owners",
@@ -647,8 +668,8 @@ func expandTagOwners(
) )
} }
for _, owner := range ows { for _, owner := range ows {
if strings.HasPrefix(owner, "group:") { if isGroup(owner) {
gs, err := expandGroup(aclPolicy, owner, stripEmailDomain) gs, err := pol.getUsersInGroup(owner, stripEmailDomain)
if err != nil { if err != nil {
return []string{}, err return []string{}, err
} }
@@ -661,15 +682,15 @@ func expandTagOwners(
return owners, nil return owners, nil
} }
// expandGroup will return the list of namespace inside the group // getUsersInGroup will return the list of user inside the group
// after some validation. // after some validation.
func expandGroup( func (pol *ACLPolicy) getUsersInGroup(
aclPolicy ACLPolicy,
group string, group string,
stripEmailDomain bool, stripEmailDomain bool,
) ([]string, error) { ) ([]string, error) {
outGroups := []string{} users := []string{}
aclGroups, ok := aclPolicy.Groups[group] log.Trace().Caller().Interface("pol", pol).Msg("test")
aclGroups, ok := pol.Groups[group]
if !ok { if !ok {
return []string{}, fmt.Errorf( return []string{}, fmt.Errorf(
"group %v isn't registered. %w", "group %v isn't registered. %w",
@@ -678,7 +699,7 @@ func expandGroup(
) )
} }
for _, group := range aclGroups { for _, group := range aclGroups {
if strings.HasPrefix(group, "group:") { if isGroup(group) {
return []string{}, fmt.Errorf( return []string{}, fmt.Errorf(
"%w. A group cannot be composed of groups. https://tailscale.com/kb/1018/acls/#groups", "%w. A group cannot be composed of groups. https://tailscale.com/kb/1018/acls/#groups",
errInvalidGroup, errInvalidGroup,
@@ -692,8 +713,151 @@ func expandGroup(
errInvalidGroup, errInvalidGroup,
) )
} }
outGroups = append(outGroups, grp) users = append(users, grp)
} }
return outGroups, nil return users, nil
}
func (pol *ACLPolicy) getIPsFromGroup(
group string,
machines Machines,
stripEmailDomain bool,
) (*netipx.IPSet, error) {
build := netipx.IPSetBuilder{}
users, err := pol.getUsersInGroup(group, stripEmailDomain)
if err != nil {
return &netipx.IPSet{}, err
}
for _, user := range users {
filteredMachines := filterMachinesByUser(machines, user)
for _, machine := range filteredMachines {
machine.IPAddresses.AppendToIPSet(&build)
}
}
return build.IPSet()
}
func (pol *ACLPolicy) getIPsFromTag(
alias string,
machines Machines,
stripEmailDomain bool,
) (*netipx.IPSet, error) {
build := netipx.IPSetBuilder{}
// check for forced tags
for _, machine := range machines {
if contains(machine.ForcedTags, alias) {
machine.IPAddresses.AppendToIPSet(&build)
}
}
// find tag owners
owners, err := getTagOwners(pol, alias, stripEmailDomain)
if err != nil {
if errors.Is(err, errInvalidTag) {
ipSet, _ := build.IPSet()
if len(ipSet.Prefixes()) == 0 {
return ipSet, fmt.Errorf(
"%w. %v isn't owned by a TagOwner and no forced tags are defined",
errInvalidTag,
alias,
)
}
return build.IPSet()
} else {
return nil, err
}
}
// filter out machines per tag owner
for _, user := range owners {
machines := filterMachinesByUser(machines, user)
for _, machine := range machines {
hi := machine.GetHostInfo()
if contains(hi.RequestTags, alias) {
machine.IPAddresses.AppendToIPSet(&build)
}
}
}
return build.IPSet()
}
func (pol *ACLPolicy) getIPsForUser(
user string,
machines Machines,
stripEmailDomain bool,
) (*netipx.IPSet, error) {
build := netipx.IPSetBuilder{}
filteredMachines := filterMachinesByUser(machines, user)
filteredMachines = excludeCorrectlyTaggedNodes(pol, filteredMachines, user, stripEmailDomain)
// shortcurcuit if we have no machines to get ips from.
if len(filteredMachines) == 0 {
return nil, nil //nolint
}
for _, machine := range filteredMachines {
machine.IPAddresses.AppendToIPSet(&build)
}
return build.IPSet()
}
func (pol *ACLPolicy) getIPsFromSingleIP(
ip netip.Addr,
machines Machines,
) (*netipx.IPSet, error) {
log.Trace().Str("ip", ip.String()).Msg("expandAlias got ip")
matches := machines.FilterByIP(ip)
build := netipx.IPSetBuilder{}
build.Add(ip)
for _, machine := range matches {
machine.IPAddresses.AppendToIPSet(&build)
}
return build.IPSet()
}
func (pol *ACLPolicy) getIPsFromIPPrefix(
prefix netip.Prefix,
machines Machines,
) (*netipx.IPSet, error) {
log.Trace().Str("prefix", prefix.String()).Msg("expandAlias got prefix")
build := netipx.IPSetBuilder{}
build.AddPrefix(prefix)
// This is suboptimal and quite expensive, but if we only add the prefix, we will miss all the relevant IPv6
// addresses for the hosts that belong to tailscale. This doesnt really affect stuff like subnet routers.
for _, machine := range machines {
for _, ip := range machine.IPAddresses {
// log.Trace().
// Msgf("checking if machine ip (%s) is part of prefix (%s): %v, is single ip prefix (%v), addr: %s", ip.String(), prefix.String(), prefix.Contains(ip), prefix.IsSingleIP(), prefix.Addr().String())
if prefix.Contains(ip) {
machine.IPAddresses.AppendToIPSet(&build)
}
}
}
return build.IPSet()
}
func isWildcard(str string) bool {
return str == "*"
}
func isGroup(str string) bool {
return strings.HasPrefix(str, "group:")
}
func isTag(str string) bool {
return strings.HasPrefix(str, "tag:")
} }

File diff suppressed because it is too large Load Diff

View File

@@ -34,7 +34,7 @@ type Groups map[string][]string
// Hosts are alias for IP addresses or subnets. // Hosts are alias for IP addresses or subnets.
type Hosts map[string]netip.Prefix type Hosts map[string]netip.Prefix
// TagOwners specify what users (namespaces?) are allow to use certain tags. // TagOwners specify what users (users?) are allow to use certain tags.
type TagOwners map[string][]string type TagOwners map[string][]string
// ACLTest is not implemented, but should be use to check if a certain rule is allowed. // ACLTest is not implemented, but should be use to check if a certain rule is allowed.
@@ -44,7 +44,7 @@ type ACLTest struct {
Deny []string `json:"deny,omitempty" yaml:"deny,omitempty"` Deny []string `json:"deny,omitempty" yaml:"deny,omitempty"`
} }
// AutoApprovers specify which users (namespaces?), groups or tags have their advertised routes // AutoApprovers specify which users (users?), groups or tags have their advertised routes
// or exit node status automatically enabled. // or exit node status automatically enabled.
type AutoApprovers struct { type AutoApprovers struct {
Routes map[string][]string `json:"routes" yaml:"routes"` Routes map[string][]string `json:"routes" yaml:"routes"`
@@ -111,15 +111,15 @@ func (hosts *Hosts) UnmarshalYAML(data []byte) error {
} }
// IsZero is perhaps a bit naive here. // IsZero is perhaps a bit naive here.
func (policy ACLPolicy) IsZero() bool { func (pol ACLPolicy) IsZero() bool {
if len(policy.Groups) == 0 && len(policy.Hosts) == 0 && len(policy.ACLs) == 0 { if len(pol.Groups) == 0 && len(pol.Hosts) == 0 && len(pol.ACLs) == 0 {
return true return true
} }
return false return false
} }
// Returns the list of autoApproving namespaces, groups or tags for a given IPPrefix. // Returns the list of autoApproving users, groups or tags for a given IPPrefix.
func (autoApprovers *AutoApprovers) GetRouteApprovers( func (autoApprovers *AutoApprovers) GetRouteApprovers(
prefix netip.Prefix, prefix netip.Prefix,
) ([]string, error) { ) ([]string, error) {

2
api.go
View File

@@ -78,7 +78,7 @@ var registerWebAPITemplate = template.Must(
<p> <p>
Run the command below in the headscale server to add this machine to your network: Run the command below in the headscale server to add this machine to your network:
</p> </p>
<pre><code>headscale -n NAMESPACE nodes register --key {{.Key}}</code></pre> <pre><code>headscale nodes register --user USERNAME --key {{.Key}}</code></pre>
</body> </body>
</html> </html>
`)) `))

View File

@@ -1,6 +1,8 @@
package headscale package headscale
import ( import (
"time"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
) )
@@ -55,16 +57,46 @@ func (h *Headscale) generateMapResponse(
peers, peers,
) )
now := time.Now()
resp := tailcfg.MapResponse{ resp := tailcfg.MapResponse{
KeepAlive: false, KeepAlive: false,
Node: node, Node: node,
Peers: nodePeers,
DNSConfig: dnsConfig, // TODO: Only send if updated
Domain: h.cfg.BaseDomain, DERPMap: h.DERPMap,
// TODO: Only send if updated
Peers: nodePeers,
// TODO(kradalby): Implement:
// https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L1351-L1374
// PeersChanged
// PeersRemoved
// PeersChangedPatch
// PeerSeenChange
// OnlineChange
// TODO: Only send if updated
DNSConfig: dnsConfig,
// TODO: Only send if updated
Domain: h.cfg.BaseDomain,
// Do not instruct clients to collect services, we do not
// support or do anything with them
CollectServices: "false",
// TODO: Only send if updated
PacketFilter: h.aclRules, PacketFilter: h.aclRules,
SSHPolicy: h.sshPolicy,
DERPMap: h.DERPMap,
UserProfiles: profiles, UserProfiles: profiles,
// TODO: Only send if updated
SSHPolicy: h.sshPolicy,
ControlTime: &now,
Debug: &tailcfg.Debug{ Debug: &tailcfg.Debug{
DisableLogTail: !h.cfg.LogTail.Enabled, DisableLogTail: !h.cfg.LogTail.Enabled,
RandomizeClientPort: h.cfg.RandomizeClientPort, RandomizeClientPort: h.cfg.RandomizeClientPort,

View File

@@ -29,7 +29,7 @@ type APIKey struct {
LastSeen *time.Time LastSeen *time.Time
} }
// CreateAPIKey creates a new ApiKey in a namespace, and returns it. // CreateAPIKey creates a new ApiKey in a user, and returns it.
func (h *Headscale) CreateAPIKey( func (h *Headscale) CreateAPIKey(
expiration *time.Time, expiration *time.Time,
) (string, *APIKey, error) { ) (string, *APIKey, error) {
@@ -64,7 +64,7 @@ func (h *Headscale) CreateAPIKey(
return keyStr, &key, nil return keyStr, &key, nil
} }
// ListAPIKeys returns the list of ApiKeys for a namespace. // ListAPIKeys returns the list of ApiKeys for a user.
func (h *Headscale) ListAPIKeys() ([]APIKey, error) { func (h *Headscale) ListAPIKeys() ([]APIKey, error) {
keys := []APIKey{} keys := []APIKey{}
if err := h.db.Find(&keys).Error; err != nil { if err := h.db.Find(&keys).Error; err != nil {

54
app.go
View File

@@ -238,20 +238,20 @@ func (h *Headscale) failoverSubnetRoutes(milliSeconds int64) {
} }
func (h *Headscale) expireEphemeralNodesWorker() { func (h *Headscale) expireEphemeralNodesWorker() {
namespaces, err := h.ListNamespaces() users, err := h.ListUsers()
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error listing namespaces") log.Error().Err(err).Msg("Error listing users")
return return
} }
for _, namespace := range namespaces { for _, user := range users {
machines, err := h.ListMachinesInNamespace(namespace.Name) machines, err := h.ListMachinesByUser(user.Name)
if err != nil { if err != nil {
log.Error(). log.Error().
Err(err). Err(err).
Str("namespace", namespace.Name). Str("user", user.Name).
Msg("Error listing machines in namespace") Msg("Error listing machines in user")
return return
} }
@@ -283,20 +283,20 @@ func (h *Headscale) expireEphemeralNodesWorker() {
} }
func (h *Headscale) expireExpiredMachinesWorker() { func (h *Headscale) expireExpiredMachinesWorker() {
namespaces, err := h.ListNamespaces() users, err := h.ListUsers()
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error listing namespaces") log.Error().Err(err).Msg("Error listing users")
return return
} }
for _, namespace := range namespaces { for _, user := range users {
machines, err := h.ListMachinesInNamespace(namespace.Name) machines, err := h.ListMachinesByUser(user.Name)
if err != nil { if err != nil {
log.Error(). log.Error().
Err(err). Err(err).
Str("namespace", namespace.Name). Str("user", user.Name).
Msg("Error listing machines in namespace") Msg("Error listing machines in user")
return return
} }
@@ -304,7 +304,7 @@ func (h *Headscale) expireExpiredMachinesWorker() {
expiredFound := false expiredFound := false
for index, machine := range machines { for index, machine := range machines {
if machine.isExpired() && if machine.isExpired() &&
machine.Expiry.After(h.getLastStateChange(namespace)) { machine.Expiry.After(h.getLastStateChange(user)) {
expiredFound = true expiredFound = true
err := h.ExpireMachine(&machines[index]) err := h.ExpireMachine(&machines[index])
@@ -521,7 +521,7 @@ func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *mux.Router {
apiRouter.Use(h.httpAuthenticationMiddleware) apiRouter.Use(h.httpAuthenticationMiddleware)
apiRouter.PathPrefix("/v1/").HandlerFunc(grpcMux.ServeHTTP) apiRouter.PathPrefix("/v1/").HandlerFunc(grpcMux.ServeHTTP)
router.PathPrefix("/").HandlerFunc(stdoutHandler) router.PathPrefix("/").HandlerFunc(notFoundHandler)
return router return router
} }
@@ -818,7 +818,6 @@ func (h *Headscale) Serve() error {
// And we're done: // And we're done:
cancel() cancel()
os.Exit(0)
} }
} }
} }
@@ -908,31 +907,31 @@ func (h *Headscale) setLastStateChangeToNow() {
now := time.Now().UTC() now := time.Now().UTC()
namespaces, err := h.ListNamespaces() users, err := h.ListUsers()
if err != nil { if err != nil {
log.Error(). log.Error().
Caller(). Caller().
Err(err). Err(err).
Msg("failed to fetch all namespaces, failing to update last changed state.") Msg("failed to fetch all users, failing to update last changed state.")
} }
for _, namespace := range namespaces { for _, user := range users {
lastStateUpdate.WithLabelValues(namespace.Name, "headscale").Set(float64(now.Unix())) lastStateUpdate.WithLabelValues(user.Name, "headscale").Set(float64(now.Unix()))
if h.lastStateChange == nil { if h.lastStateChange == nil {
h.lastStateChange = xsync.NewMapOf[time.Time]() h.lastStateChange = xsync.NewMapOf[time.Time]()
} }
h.lastStateChange.Store(namespace.Name, now) h.lastStateChange.Store(user.Name, now)
} }
} }
func (h *Headscale) getLastStateChange(namespaces ...Namespace) time.Time { func (h *Headscale) getLastStateChange(users ...User) time.Time {
times := []time.Time{} times := []time.Time{}
// getLastStateChange takes a list of namespaces as a "filter", if no namespaces // getLastStateChange takes a list of users as a "filter", if no users
// are past, then use the entier list of namespaces and look for the last update // are past, then use the entier list of users and look for the last update
if len(namespaces) > 0 { if len(users) > 0 {
for _, namespace := range namespaces { for _, user := range users {
if lastChange, ok := h.lastStateChange.Load(namespace.Name); ok { if lastChange, ok := h.lastStateChange.Load(user.Name); ok {
times = append(times, lastChange) times = append(times, lastChange)
} }
} }
@@ -957,7 +956,7 @@ func (h *Headscale) getLastStateChange(namespaces ...Namespace) time.Time {
} }
} }
func stdoutHandler( func notFoundHandler(
writer http.ResponseWriter, writer http.ResponseWriter,
req *http.Request, req *http.Request,
) { ) {
@@ -969,6 +968,7 @@ func stdoutHandler(
Interface("url", req.URL). Interface("url", req.URL).
Bytes("body", body). Bytes("body", body).
Msg("Request did not match") Msg("Request did not match")
writer.WriteHeader(http.StatusNotFound)
} }
func readOrCreatePrivateKey(path string) (*key.MachinePrivate, error) { func readOrCreatePrivateKey(path string) (*key.MachinePrivate, error) {

View File

@@ -0,0 +1,47 @@
package main
import (
"log"
"github.com/juanfont/headscale/integration"
"github.com/juanfont/headscale/integration/tsic"
"github.com/ory/dockertest/v3"
)
func main() {
log.Printf("creating docker pool")
pool, err := dockertest.NewPool("")
if err != nil {
log.Fatalf("could not connect to docker: %s", err)
}
log.Printf("creating docker network")
network, err := pool.CreateNetwork("docker-integration-net")
if err != nil {
log.Fatalf("failed to create or get network: %s", err)
}
for _, version := range integration.TailscaleVersions {
log.Printf("creating container image for Tailscale (%s)", version)
tsClient, err := tsic.New(
pool,
version,
network,
)
if err != nil {
log.Fatalf("failed to create tailscale node: %s", err)
}
err = tsClient.Shutdown()
if err != nil {
log.Fatalf("failed to shut down container: %s", err)
}
}
network.Close()
err = pool.RemoveNetwork(network)
if err != nil {
log.Fatalf("failed to remove network: %s", err)
}
}

View File

@@ -7,19 +7,29 @@ import (
"fmt" "fmt"
"log" "log"
"os" "os"
"os/exec"
"path"
"path/filepath"
"strings"
"text/template" "text/template"
) )
var ( var (
githubWorkflowPath = "../../.github/workflows/"
jobFileNameTemplate = `test-integration-v2-%s.yaml` jobFileNameTemplate = `test-integration-v2-%s.yaml`
jobTemplate = template.Must(template.New("jobTemplate").Parse(` jobTemplate = template.Must(
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go template.New("jobTemplate").
Parse(`# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/ # To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - {{.Name}} name: Integration Test v2 - {{.Name}}
on: [pull_request] on: [pull_request]
concurrency:
group: {{ "${{ github.workflow }}-$${{ github.head_ref || github.run_id }}" }}
cancel-in-progress: true
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -40,8 +50,8 @@ jobs:
integration_test/ integration_test/
config-example.yaml config-example.yaml
- uses: cachix/install-nix-action@v16 - uses: cachix/install-nix-action@v18
if: steps.changed-files.outputs.any_changed == 'true' if: {{ "${{ env.ACT }}" }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests - name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
@@ -52,6 +62,7 @@ jobs:
--name headscale-test-suite \ --name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \ --volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \ --volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \ golang:1 \
go test ./... \ go test ./... \
-tags ts2019 \ -tags ts2019 \
@@ -59,43 +70,87 @@ jobs:
-timeout 120m \ -timeout 120m \
-parallel 1 \ -parallel 1 \
-run "^{{.Name}}$" -run "^{{.Name}}$"
`))
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: pprof
path: "control_logs/*.pprof.tar"
`),
)
) )
const workflowFilePerm = 0o600 const workflowFilePerm = 0o600
func removeTests() {
glob := fmt.Sprintf(jobFileNameTemplate, "*")
files, err := filepath.Glob(filepath.Join(githubWorkflowPath, glob))
if err != nil {
log.Fatalf("failed to find test files")
}
for _, file := range files {
err := os.Remove(file)
if err != nil {
log.Printf("failed to remove: %s", err)
}
}
}
func findTests() []string {
rgBin, err := exec.LookPath("rg")
if err != nil {
log.Fatalf("failed to find rg (ripgrep) binary")
}
args := []string{
"--regexp", "func (Test.+)\\(.*",
"../../integration/",
"--replace", "$1",
"--sort", "path",
"--no-line-number",
"--no-filename",
"--no-heading",
}
log.Printf("executing: %s %s", rgBin, strings.Join(args, " "))
ripgrep := exec.Command(
rgBin,
args...,
)
result, err := ripgrep.CombinedOutput()
if err != nil {
log.Printf("out: %s", result)
log.Fatalf("failed to run ripgrep: %s", err)
}
tests := strings.Split(string(result), "\n")
tests = tests[:len(tests)-1]
return tests
}
func main() { func main() {
type testConfig struct { type testConfig struct {
Name string Name string
} }
// TODO(kradalby): automatic fetch tests at runtime tests := findTests()
tests := []string{
"TestAuthKeyLogoutAndRelogin", removeTests()
"TestAuthWebFlowAuthenticationPingAll",
"TestAuthWebFlowLogoutAndRelogin",
"TestCreateTailscale",
"TestEnablingRoutes",
"TestHeadscale",
"TestNamespaceCommand",
"TestOIDCAuthenticationPingAll",
"TestOIDCExpireNodes",
"TestPingAllByHostname",
"TestPingAllByIP",
"TestPreAuthKeyCommand",
"TestPreAuthKeyCommandReusableEphemeral",
"TestPreAuthKeyCommandWithoutExpiry",
"TestResolveMagicDNS",
"TestSSHIsBlockedInACL",
"TestSSHMultipleNamespacesAllToAll",
"TestSSHNoSSHConfigured",
"TestSSHOneNamespaceAllToAll",
"TestSSNamespaceOnlyIsolation",
"TestTaildrop",
"TestTailscaleNodesJoiningHeadcale",
}
for _, test := range tests { for _, test := range tests {
log.Printf("generating workflow for %s", test)
var content bytes.Buffer var content bytes.Buffer
if err := jobTemplate.Execute(&content, testConfig{ if err := jobTemplate.Execute(&content, testConfig{
@@ -104,9 +159,9 @@ func main() {
log.Fatalf("failed to render template: %s", err) log.Fatalf("failed to render template: %s", err)
} }
path := "../../.github/workflows/" + fmt.Sprintf(jobFileNameTemplate, test) testPath := path.Join(githubWorkflowPath, fmt.Sprintf(jobFileNameTemplate, test))
err := os.WriteFile(path, content.Bytes(), workflowFilePerm) err := os.WriteFile(testPath, content.Bytes(), workflowFilePerm)
if err != nil { if err != nil {
log.Fatalf("failed to write github job: %s", err) log.Fatalf("failed to write github job: %s", err)
} }

View File

@@ -0,0 +1,22 @@
package cli
import (
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(configTestCmd)
}
var configTestCmd = &cobra.Command{
Use: "configtest",
Short: "Test the configuration.",
Long: "Run a test of the configuration and exit.",
Run: func(cmd *cobra.Command, args []string) {
_, err := getHeadscaleApp()
if err != nil {
log.Fatal().Caller().Err(err).Msg("Error initializing")
}
},
}

View File

@@ -27,8 +27,14 @@ func init() {
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("") log.Fatal().Err(err).Msg("")
} }
createNodeCmd.Flags().StringP("namespace", "n", "", "Namespace") createNodeCmd.Flags().StringP("user", "u", "", "User")
err = createNodeCmd.MarkFlagRequired("namespace")
createNodeCmd.Flags().StringP("namespace", "n", "", "User")
createNodeNamespaceFlag := createNodeCmd.Flags().Lookup("namespace")
createNodeNamespaceFlag.Deprecated = deprecateNamespaceMessage
createNodeNamespaceFlag.Hidden = true
err = createNodeCmd.MarkFlagRequired("user")
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("") log.Fatal().Err(err).Msg("")
} }
@@ -55,9 +61,9 @@ var createNodeCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output") output, _ := cmd.Flags().GetString("output")
namespace, err := cmd.Flags().GetString("namespace") user, err := cmd.Flags().GetString("user")
if err != nil { if err != nil {
ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output) ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output)
return return
} }
@@ -110,10 +116,10 @@ var createNodeCmd = &cobra.Command{
} }
request := &v1.DebugCreateMachineRequest{ request := &v1.DebugCreateMachineRequest{
Key: machineKey, Key: machineKey,
Name: name, Name: name,
Namespace: namespace, User: user,
Routes: routes, Routes: routes,
} }
response, err := client.DebugCreateMachine(ctx, request) response, err := client.DebugCreateMachine(ctx, request)

View File

@@ -19,12 +19,24 @@ import (
func init() { func init() {
rootCmd.AddCommand(nodeCmd) rootCmd.AddCommand(nodeCmd)
listNodesCmd.Flags().StringP("namespace", "n", "", "Filter by namespace") listNodesCmd.Flags().StringP("user", "u", "", "Filter by user")
listNodesCmd.Flags().BoolP("tags", "t", false, "Show tags") listNodesCmd.Flags().BoolP("tags", "t", false, "Show tags")
listNodesCmd.Flags().StringP("namespace", "n", "", "User")
listNodesNamespaceFlag := listNodesCmd.Flags().Lookup("namespace")
listNodesNamespaceFlag.Deprecated = deprecateNamespaceMessage
listNodesNamespaceFlag.Hidden = true
nodeCmd.AddCommand(listNodesCmd) nodeCmd.AddCommand(listNodesCmd)
registerNodeCmd.Flags().StringP("namespace", "n", "", "Namespace") registerNodeCmd.Flags().StringP("user", "u", "", "User")
err := registerNodeCmd.MarkFlagRequired("namespace")
registerNodeCmd.Flags().StringP("namespace", "n", "", "User")
registerNodeNamespaceFlag := registerNodeCmd.Flags().Lookup("namespace")
registerNodeNamespaceFlag.Deprecated = deprecateNamespaceMessage
registerNodeNamespaceFlag.Hidden = true
err := registerNodeCmd.MarkFlagRequired("user")
if err != nil { if err != nil {
log.Fatalf(err.Error()) log.Fatalf(err.Error())
} }
@@ -63,9 +75,14 @@ func init() {
log.Fatalf(err.Error()) log.Fatalf(err.Error())
} }
moveNodeCmd.Flags().StringP("namespace", "n", "", "New namespace") moveNodeCmd.Flags().StringP("user", "u", "", "New user")
err = moveNodeCmd.MarkFlagRequired("namespace") moveNodeCmd.Flags().StringP("namespace", "n", "", "User")
moveNodeNamespaceFlag := moveNodeCmd.Flags().Lookup("namespace")
moveNodeNamespaceFlag.Deprecated = deprecateNamespaceMessage
moveNodeNamespaceFlag.Hidden = true
err = moveNodeCmd.MarkFlagRequired("user")
if err != nil { if err != nil {
log.Fatalf(err.Error()) log.Fatalf(err.Error())
} }
@@ -93,9 +110,9 @@ var registerNodeCmd = &cobra.Command{
Short: "Registers a machine to your network", Short: "Registers a machine to your network",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output") output, _ := cmd.Flags().GetString("output")
namespace, err := cmd.Flags().GetString("namespace") user, err := cmd.Flags().GetString("user")
if err != nil { if err != nil {
ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output) ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output)
return return
} }
@@ -116,8 +133,8 @@ var registerNodeCmd = &cobra.Command{
} }
request := &v1.RegisterMachineRequest{ request := &v1.RegisterMachineRequest{
Key: machineKey, Key: machineKey,
Namespace: namespace, User: user,
} }
response, err := client.RegisterMachine(ctx, request) response, err := client.RegisterMachine(ctx, request)
@@ -146,9 +163,9 @@ var listNodesCmd = &cobra.Command{
Aliases: []string{"ls", "show"}, Aliases: []string{"ls", "show"},
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output") output, _ := cmd.Flags().GetString("output")
namespace, err := cmd.Flags().GetString("namespace") user, err := cmd.Flags().GetString("user")
if err != nil { if err != nil {
ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output) ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output)
return return
} }
@@ -164,7 +181,7 @@ var listNodesCmd = &cobra.Command{
defer conn.Close() defer conn.Close()
request := &v1.ListMachinesRequest{ request := &v1.ListMachinesRequest{
Namespace: namespace, User: user,
} }
response, err := client.ListMachines(ctx, request) response, err := client.ListMachines(ctx, request)
@@ -184,7 +201,7 @@ var listNodesCmd = &cobra.Command{
return return
} }
tableData, err := nodesToPtables(namespace, showTags, response.Machines) tableData, err := nodesToPtables(user, showTags, response.Machines)
if err != nil { if err != nil {
ErrorOutput(err, fmt.Sprintf("Error converting to table: %s", err), output) ErrorOutput(err, fmt.Sprintf("Error converting to table: %s", err), output)
@@ -388,7 +405,7 @@ var deleteNodeCmd = &cobra.Command{
var moveNodeCmd = &cobra.Command{ var moveNodeCmd = &cobra.Command{
Use: "move", Use: "move",
Short: "Move node to another namespace", Short: "Move node to another user",
Aliases: []string{"mv"}, Aliases: []string{"mv"},
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output") output, _ := cmd.Flags().GetString("output")
@@ -404,11 +421,11 @@ var moveNodeCmd = &cobra.Command{
return return
} }
namespace, err := cmd.Flags().GetString("namespace") user, err := cmd.Flags().GetString("user")
if err != nil { if err != nil {
ErrorOutput( ErrorOutput(
err, err,
fmt.Sprintf("Error getting namespace: %s", err), fmt.Sprintf("Error getting user: %s", err),
output, output,
) )
@@ -439,7 +456,7 @@ var moveNodeCmd = &cobra.Command{
moveRequest := &v1.MoveMachineRequest{ moveRequest := &v1.MoveMachineRequest{
MachineId: identifier, MachineId: identifier,
Namespace: namespace, User: user,
} }
moveResponse, err := client.MoveMachine(ctx, moveRequest) moveResponse, err := client.MoveMachine(ctx, moveRequest)
@@ -456,12 +473,12 @@ var moveNodeCmd = &cobra.Command{
return return
} }
SuccessOutput(moveResponse.Machine, "Node moved to another namespace", output) SuccessOutput(moveResponse.Machine, "Node moved to another user", output)
}, },
} }
func nodesToPtables( func nodesToPtables(
currentNamespace string, currentUser string,
showTags bool, showTags bool,
machines []*v1.Machine, machines []*v1.Machine,
) (pterm.TableData, error) { ) (pterm.TableData, error) {
@@ -471,7 +488,7 @@ func nodesToPtables(
"Name", "Name",
"MachineKey", "MachineKey",
"NodeKey", "NodeKey",
"Namespace", "User",
"IP addresses", "IP addresses",
"Ephemeral", "Ephemeral",
"Last seen", "Last seen",
@@ -560,12 +577,12 @@ func nodesToPtables(
} }
validTags = strings.TrimLeft(validTags, ",") validTags = strings.TrimLeft(validTags, ",")
var namespace string var user string
if currentNamespace == "" || (currentNamespace == machine.Namespace.Name) { if currentUser == "" || (currentUser == machine.User.Name) {
namespace = pterm.LightMagenta(machine.Namespace.Name) user = pterm.LightMagenta(machine.User.Name)
} else { } else {
// Shared into this namespace // Shared into this user
namespace = pterm.LightYellow(machine.Namespace.Name) user = pterm.LightYellow(machine.User.Name)
} }
var IPV4Address string var IPV4Address string
@@ -584,7 +601,7 @@ func nodesToPtables(
machine.GetGivenName(), machine.GetGivenName(),
machineKey.ShortString(), machineKey.ShortString(),
nodeKey.ShortString(), nodeKey.ShortString(),
namespace, user,
strings.Join([]string{IPV4Address, IPV6Address}, ", "), strings.Join([]string{IPV4Address, IPV6Address}, ", "),
strconv.FormatBool(ephemeral), strconv.FormatBool(ephemeral),
lastSeenTime, lastSeenTime,

View File

@@ -20,8 +20,14 @@ const (
func init() { func init() {
rootCmd.AddCommand(preauthkeysCmd) rootCmd.AddCommand(preauthkeysCmd)
preauthkeysCmd.PersistentFlags().StringP("namespace", "n", "", "Namespace") preauthkeysCmd.PersistentFlags().StringP("user", "u", "", "User")
err := preauthkeysCmd.MarkPersistentFlagRequired("namespace")
preauthkeysCmd.PersistentFlags().StringP("namespace", "n", "", "User")
pakNamespaceFlag := preauthkeysCmd.PersistentFlags().Lookup("namespace")
pakNamespaceFlag.Deprecated = deprecateNamespaceMessage
pakNamespaceFlag.Hidden = true
err := preauthkeysCmd.MarkPersistentFlagRequired("user")
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("") log.Fatal().Err(err).Msg("")
} }
@@ -46,14 +52,14 @@ var preauthkeysCmd = &cobra.Command{
var listPreAuthKeys = &cobra.Command{ var listPreAuthKeys = &cobra.Command{
Use: "list", Use: "list",
Short: "List the preauthkeys for this namespace", Short: "List the preauthkeys for this user",
Aliases: []string{"ls", "show"}, Aliases: []string{"ls", "show"},
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output") output, _ := cmd.Flags().GetString("output")
namespace, err := cmd.Flags().GetString("namespace") user, err := cmd.Flags().GetString("user")
if err != nil { if err != nil {
ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output) ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output)
return return
} }
@@ -63,7 +69,7 @@ var listPreAuthKeys = &cobra.Command{
defer conn.Close() defer conn.Close()
request := &v1.ListPreAuthKeysRequest{ request := &v1.ListPreAuthKeysRequest{
Namespace: namespace, User: user,
} }
response, err := client.ListPreAuthKeys(ctx, request) response, err := client.ListPreAuthKeys(ctx, request)
@@ -143,14 +149,14 @@ var listPreAuthKeys = &cobra.Command{
var createPreAuthKeyCmd = &cobra.Command{ var createPreAuthKeyCmd = &cobra.Command{
Use: "create", Use: "create",
Short: "Creates a new preauthkey in the specified namespace", Short: "Creates a new preauthkey in the specified user",
Aliases: []string{"c", "new"}, Aliases: []string{"c", "new"},
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output") output, _ := cmd.Flags().GetString("output")
namespace, err := cmd.Flags().GetString("namespace") user, err := cmd.Flags().GetString("user")
if err != nil { if err != nil {
ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output) ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output)
return return
} }
@@ -162,11 +168,11 @@ var createPreAuthKeyCmd = &cobra.Command{
log.Trace(). log.Trace().
Bool("reusable", reusable). Bool("reusable", reusable).
Bool("ephemeral", ephemeral). Bool("ephemeral", ephemeral).
Str("namespace", namespace). Str("user", user).
Msg("Preparing to create preauthkey") Msg("Preparing to create preauthkey")
request := &v1.CreatePreAuthKeyRequest{ request := &v1.CreatePreAuthKeyRequest{
Namespace: namespace, User: user,
Reusable: reusable, Reusable: reusable,
Ephemeral: ephemeral, Ephemeral: ephemeral,
AclTags: tags, AclTags: tags,
@@ -225,9 +231,9 @@ var expirePreAuthKeyCmd = &cobra.Command{
}, },
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output") output, _ := cmd.Flags().GetString("output")
namespace, err := cmd.Flags().GetString("namespace") user, err := cmd.Flags().GetString("user")
if err != nil { if err != nil {
ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output) ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output)
return return
} }
@@ -237,8 +243,8 @@ var expirePreAuthKeyCmd = &cobra.Command{
defer conn.Close() defer conn.Close()
request := &v1.ExpirePreAuthKeyRequest{ request := &v1.ExpirePreAuthKeyRequest{
Namespace: namespace, User: user,
Key: args[0], Key: args[0],
} }
response, err := client.ExpirePreAuthKey(ctx, request) response, err := client.ExpirePreAuthKey(ctx, request)

View File

@@ -12,10 +12,15 @@ import (
"github.com/tcnksm/go-latest" "github.com/tcnksm/go-latest"
) )
const (
deprecateNamespaceMessage = "use --user"
)
var cfgFile string = "" var cfgFile string = ""
func init() { func init() {
if len(os.Args) > 1 && (os.Args[1] == "version" || os.Args[1] == "mockoidc" || os.Args[1] == "completion") { if len(os.Args) > 1 &&
(os.Args[1] == "version" || os.Args[1] == "mockoidc" || os.Args[1] == "completion") {
return return
} }

View File

@@ -3,8 +3,10 @@ package cli
import ( import (
"fmt" "fmt"
"log" "log"
"net/netip"
"strconv" "strconv"
"github.com/juanfont/headscale"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1" v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/pterm/pterm" "github.com/pterm/pterm"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -33,6 +35,13 @@ func init() {
log.Fatalf(err.Error()) log.Fatalf(err.Error())
} }
routesCmd.AddCommand(disableRouteCmd) routesCmd.AddCommand(disableRouteCmd)
deleteRouteCmd.Flags().Uint64P("route", "r", 0, "Route identifier (ID)")
err = deleteRouteCmd.MarkFlagRequired("route")
if err != nil {
log.Fatalf(err.Error())
}
routesCmd.AddCommand(deleteRouteCmd)
} }
var routesCmd = &cobra.Command{ var routesCmd = &cobra.Command{
@@ -198,7 +207,50 @@ var disableRouteCmd = &cobra.Command{
if err != nil { if err != nil {
ErrorOutput( ErrorOutput(
err, err,
fmt.Sprintf("Cannot enable route %d: %s", routeID, status.Convert(err).Message()), fmt.Sprintf("Cannot disable route %d: %s", routeID, status.Convert(err).Message()),
output,
)
return
}
if output != "" {
SuccessOutput(response, "", output)
return
}
},
}
var deleteRouteCmd = &cobra.Command{
Use: "delete",
Short: "Delete a given route",
Long: `This command will delete a given route.`,
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
routeID, err := cmd.Flags().GetUint64("route")
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error getting machine id from flag: %s", err),
output,
)
return
}
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
response, err := client.DeleteRoute(ctx, &v1.DeleteRouteRequest{
RouteId: routeID,
})
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Cannot delete route %d: %s", routeID, status.Convert(err).Message()),
output, output,
) )
@@ -218,6 +270,19 @@ func routesToPtables(routes []*v1.Route) pterm.TableData {
tableData := pterm.TableData{{"ID", "Machine", "Prefix", "Advertised", "Enabled", "Primary"}} tableData := pterm.TableData{{"ID", "Machine", "Prefix", "Advertised", "Enabled", "Primary"}}
for _, route := range routes { for _, route := range routes {
var isPrimaryStr string
prefix, err := netip.ParsePrefix(route.Prefix)
if err != nil {
log.Printf("Error parsing prefix %s: %s", route.Prefix, err)
continue
}
if prefix == headscale.ExitRouteV4 || prefix == headscale.ExitRouteV6 {
isPrimaryStr = "-"
} else {
isPrimaryStr = strconv.FormatBool(route.IsPrimary)
}
tableData = append(tableData, tableData = append(tableData,
[]string{ []string{
strconv.FormatUint(route.Id, Base10), strconv.FormatUint(route.Id, Base10),
@@ -225,7 +290,7 @@ func routesToPtables(routes []*v1.Route) pterm.TableData {
route.Prefix, route.Prefix,
strconv.FormatBool(route.Advertised), strconv.FormatBool(route.Advertised),
strconv.FormatBool(route.Enabled), strconv.FormatBool(route.Enabled),
strconv.FormatBool(route.IsPrimary), isPrimaryStr,
}) })
} }

View File

@@ -13,26 +13,26 @@ import (
) )
func init() { func init() {
rootCmd.AddCommand(namespaceCmd) rootCmd.AddCommand(userCmd)
namespaceCmd.AddCommand(createNamespaceCmd) userCmd.AddCommand(createUserCmd)
namespaceCmd.AddCommand(listNamespacesCmd) userCmd.AddCommand(listUsersCmd)
namespaceCmd.AddCommand(destroyNamespaceCmd) userCmd.AddCommand(destroyUserCmd)
namespaceCmd.AddCommand(renameNamespaceCmd) userCmd.AddCommand(renameUserCmd)
} }
const ( const (
errMissingParameter = headscale.Error("missing parameters") errMissingParameter = headscale.Error("missing parameters")
) )
var namespaceCmd = &cobra.Command{ var userCmd = &cobra.Command{
Use: "namespaces", Use: "users",
Short: "Manage the namespaces of Headscale", Short: "Manage the users of Headscale",
Aliases: []string{"namespace", "ns", "user", "users"}, Aliases: []string{"user", "namespace", "namespaces", "ns"},
} }
var createNamespaceCmd = &cobra.Command{ var createUserCmd = &cobra.Command{
Use: "create NAME", Use: "create NAME",
Short: "Creates a new namespace", Short: "Creates a new user",
Aliases: []string{"c", "new"}, Aliases: []string{"c", "new"},
Args: func(cmd *cobra.Command, args []string) error { Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 { if len(args) < 1 {
@@ -44,7 +44,7 @@ var createNamespaceCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output") output, _ := cmd.Flags().GetString("output")
namespaceName := args[0] userName := args[0]
ctx, client, conn, cancel := getHeadscaleCLIClient() ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel() defer cancel()
@@ -52,15 +52,15 @@ var createNamespaceCmd = &cobra.Command{
log.Trace().Interface("client", client).Msg("Obtained gRPC client") log.Trace().Interface("client", client).Msg("Obtained gRPC client")
request := &v1.CreateNamespaceRequest{Name: namespaceName} request := &v1.CreateUserRequest{Name: userName}
log.Trace().Interface("request", request).Msg("Sending CreateNamespace request") log.Trace().Interface("request", request).Msg("Sending CreateUser request")
response, err := client.CreateNamespace(ctx, request) response, err := client.CreateUser(ctx, request)
if err != nil { if err != nil {
ErrorOutput( ErrorOutput(
err, err,
fmt.Sprintf( fmt.Sprintf(
"Cannot create namespace: %s", "Cannot create user: %s",
status.Convert(err).Message(), status.Convert(err).Message(),
), ),
output, output,
@@ -69,13 +69,13 @@ var createNamespaceCmd = &cobra.Command{
return return
} }
SuccessOutput(response.Namespace, "Namespace created", output) SuccessOutput(response.User, "User created", output)
}, },
} }
var destroyNamespaceCmd = &cobra.Command{ var destroyUserCmd = &cobra.Command{
Use: "destroy NAME", Use: "destroy NAME",
Short: "Destroys a namespace", Short: "Destroys a user",
Aliases: []string{"delete"}, Aliases: []string{"delete"},
Args: func(cmd *cobra.Command, args []string) error { Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 { if len(args) < 1 {
@@ -87,17 +87,17 @@ var destroyNamespaceCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output") output, _ := cmd.Flags().GetString("output")
namespaceName := args[0] userName := args[0]
request := &v1.GetNamespaceRequest{ request := &v1.GetUserRequest{
Name: namespaceName, Name: userName,
} }
ctx, client, conn, cancel := getHeadscaleCLIClient() ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel() defer cancel()
defer conn.Close() defer conn.Close()
_, err := client.GetNamespace(ctx, request) _, err := client.GetUser(ctx, request)
if err != nil { if err != nil {
ErrorOutput( ErrorOutput(
err, err,
@@ -113,8 +113,8 @@ var destroyNamespaceCmd = &cobra.Command{
if !force { if !force {
prompt := &survey.Confirm{ prompt := &survey.Confirm{
Message: fmt.Sprintf( Message: fmt.Sprintf(
"Do you want to remove the namespace '%s' and any associated preauthkeys?", "Do you want to remove the user '%s' and any associated preauthkeys?",
namespaceName, userName,
), ),
} }
err := survey.AskOne(prompt, &confirm) err := survey.AskOne(prompt, &confirm)
@@ -124,14 +124,14 @@ var destroyNamespaceCmd = &cobra.Command{
} }
if confirm || force { if confirm || force {
request := &v1.DeleteNamespaceRequest{Name: namespaceName} request := &v1.DeleteUserRequest{Name: userName}
response, err := client.DeleteNamespace(ctx, request) response, err := client.DeleteUser(ctx, request)
if err != nil { if err != nil {
ErrorOutput( ErrorOutput(
err, err,
fmt.Sprintf( fmt.Sprintf(
"Cannot destroy namespace: %s", "Cannot destroy user: %s",
status.Convert(err).Message(), status.Convert(err).Message(),
), ),
output, output,
@@ -139,16 +139,16 @@ var destroyNamespaceCmd = &cobra.Command{
return return
} }
SuccessOutput(response, "Namespace destroyed", output) SuccessOutput(response, "User destroyed", output)
} else { } else {
SuccessOutput(map[string]string{"Result": "Namespace not destroyed"}, "Namespace not destroyed", output) SuccessOutput(map[string]string{"Result": "User not destroyed"}, "User not destroyed", output)
} }
}, },
} }
var listNamespacesCmd = &cobra.Command{ var listUsersCmd = &cobra.Command{
Use: "list", Use: "list",
Short: "List all the namespaces", Short: "List all the users",
Aliases: []string{"ls", "show"}, Aliases: []string{"ls", "show"},
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output") output, _ := cmd.Flags().GetString("output")
@@ -157,13 +157,13 @@ var listNamespacesCmd = &cobra.Command{
defer cancel() defer cancel()
defer conn.Close() defer conn.Close()
request := &v1.ListNamespacesRequest{} request := &v1.ListUsersRequest{}
response, err := client.ListNamespaces(ctx, request) response, err := client.ListUsers(ctx, request)
if err != nil { if err != nil {
ErrorOutput( ErrorOutput(
err, err,
fmt.Sprintf("Cannot get namespaces: %s", status.Convert(err).Message()), fmt.Sprintf("Cannot get users: %s", status.Convert(err).Message()),
output, output,
) )
@@ -171,19 +171,19 @@ var listNamespacesCmd = &cobra.Command{
} }
if output != "" { if output != "" {
SuccessOutput(response.Namespaces, "", output) SuccessOutput(response.Users, "", output)
return return
} }
tableData := pterm.TableData{{"ID", "Name", "Created"}} tableData := pterm.TableData{{"ID", "Name", "Created"}}
for _, namespace := range response.GetNamespaces() { for _, user := range response.GetUsers() {
tableData = append( tableData = append(
tableData, tableData,
[]string{ []string{
namespace.GetId(), user.GetId(),
namespace.GetName(), user.GetName(),
namespace.GetCreatedAt().AsTime().Format("2006-01-02 15:04:05"), user.GetCreatedAt().AsTime().Format("2006-01-02 15:04:05"),
}, },
) )
} }
@@ -200,9 +200,9 @@ var listNamespacesCmd = &cobra.Command{
}, },
} }
var renameNamespaceCmd = &cobra.Command{ var renameUserCmd = &cobra.Command{
Use: "rename OLD_NAME NEW_NAME", Use: "rename OLD_NAME NEW_NAME",
Short: "Renames a namespace", Short: "Renames a user",
Aliases: []string{"mv"}, Aliases: []string{"mv"},
Args: func(cmd *cobra.Command, args []string) error { Args: func(cmd *cobra.Command, args []string) error {
expectedArguments := 2 expectedArguments := 2
@@ -219,17 +219,17 @@ var renameNamespaceCmd = &cobra.Command{
defer cancel() defer cancel()
defer conn.Close() defer conn.Close()
request := &v1.RenameNamespaceRequest{ request := &v1.RenameUserRequest{
OldName: args[0], OldName: args[0],
NewName: args[1], NewName: args[1],
} }
response, err := client.RenameNamespace(ctx, request) response, err := client.RenameUser(ctx, request)
if err != nil { if err != nil {
ErrorOutput( ErrorOutput(
err, err,
fmt.Sprintf( fmt.Sprintf(
"Cannot rename namespace: %s", "Cannot rename user: %s",
status.Convert(err).Message(), status.Convert(err).Message(),
), ),
output, output,
@@ -238,6 +238,6 @@ var renameNamespaceCmd = &cobra.Command{
return return
} }
SuccessOutput(response.Namespace, "Namespace renamed", output) SuccessOutput(response.User, "User renamed", output)
}, },
} }

View File

@@ -14,7 +14,7 @@ import (
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/credentials/insecure"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v3"
) )
const ( const (

View File

@@ -6,11 +6,25 @@ import (
"github.com/efekarakus/termcolor" "github.com/efekarakus/termcolor"
"github.com/juanfont/headscale/cmd/headscale/cli" "github.com/juanfont/headscale/cmd/headscale/cli"
"github.com/pkg/profile"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
func main() { func main() {
if _, enableProfile := os.LookupEnv("HEADSCALE_PROFILING_ENABLED"); enableProfile {
if profilePath, ok := os.LookupEnv("HEADSCALE_PROFILING_PATH"); ok {
err := os.MkdirAll(profilePath, os.ModePerm)
if err != nil {
log.Fatal().Err(err).Msg("failed to create profiling directory")
}
defer profile.Start(profile.ProfilePath(profilePath)).Stop()
} else {
defer profile.Start().Stop()
}
}
var colors bool var colors bool
switch l := termcolor.SupportLevel(os.Stderr); l { switch l := termcolor.SupportLevel(os.Stderr); l {
case termcolor.Level16M: case termcolor.Level16M:

View File

@@ -58,7 +58,7 @@ func (*Suite) TestConfigFileLoading(c *check.C) {
c.Assert(viper.GetString("listen_addr"), check.Equals, "127.0.0.1:8080") c.Assert(viper.GetString("listen_addr"), check.Equals, "127.0.0.1:8080")
c.Assert(viper.GetString("metrics_listen_addr"), check.Equals, "127.0.0.1:9090") 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_type"), check.Equals, "sqlite3")
c.Assert(viper.GetString("db_path"), check.Equals, "./db.sqlite") c.Assert(viper.GetString("db_path"), check.Equals, "/var/lib/headscale/db.sqlite")
c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "") c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "")
c.Assert(viper.GetString("tls_letsencrypt_listen"), check.Equals, ":http") c.Assert(viper.GetString("tls_letsencrypt_listen"), check.Equals, ":http")
c.Assert(viper.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01") c.Assert(viper.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01")
@@ -101,7 +101,7 @@ func (*Suite) TestConfigLoading(c *check.C) {
c.Assert(viper.GetString("listen_addr"), check.Equals, "127.0.0.1:8080") c.Assert(viper.GetString("listen_addr"), check.Equals, "127.0.0.1:8080")
c.Assert(viper.GetString("metrics_listen_addr"), check.Equals, "127.0.0.1:9090") 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_type"), check.Equals, "sqlite3")
c.Assert(viper.GetString("db_path"), check.Equals, "./db.sqlite") c.Assert(viper.GetString("db_path"), check.Equals, "/var/lib/headscale/db.sqlite")
c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "") c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "")
c.Assert(viper.GetString("tls_letsencrypt_listen"), check.Equals, ":http") c.Assert(viper.GetString("tls_letsencrypt_listen"), check.Equals, ":http")
c.Assert(viper.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01") c.Assert(viper.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01")

View File

@@ -44,9 +44,7 @@ grpc_allow_insecure: false
# and Tailscale clients. # and Tailscale clients.
# The private key file will be autogenerated if it's missing. # The private key file will be autogenerated if it's missing.
# #
# For production: private_key_path: /var/lib/headscale/private.key
# /var/lib/headscale/private.key
private_key_path: ./private.key
# The Noise section includes specific configuration for the # The Noise section includes specific configuration for the
# TS2021 Noise protocol # TS2021 Noise protocol
@@ -55,19 +53,17 @@ noise:
# traffic between headscale and Tailscale clients when # traffic between headscale and Tailscale clients when
# using the new Noise-based protocol. It must be different # using the new Noise-based protocol. It must be different
# from the legacy private key. # from the legacy private key.
# private_key_path: /var/lib/headscale/noise_private.key
# For production:
# private_key_path: /var/lib/headscale/noise_private.key
private_key_path: ./noise_private.key
# List of IP prefixes to allocate tailaddresses from. # List of IP prefixes to allocate tailaddresses from.
# Each prefix consists of either an IPv4 or IPv6 address, # Each prefix consists of either an IPv4 or IPv6 address,
# and the associated prefix length, delimited by a slash. # and the associated prefix length, delimited by a slash.
# While this looks like it can take arbitrary values, it # It must be within IP ranges supported by the Tailscale
# needs to be within IP ranges supported by the Tailscale # client - i.e., subnets of 100.64.0.0/10 and fd7a:115c:a1e0::/48.
# client. # See below:
# IPv6: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#LL81C52-L81C71 # IPv6: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#LL81C52-L81C71
# IPv4: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#L33 # IPv4: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#L33
# Any other range is NOT supported, and it will cause unexpected issues.
ip_prefixes: ip_prefixes:
- fd7a:115c:a1e0::/48 - fd7a:115c:a1e0::/48
- 100.64.0.0/10 - 100.64.0.0/10
@@ -137,8 +133,7 @@ node_update_check_interval: 10s
db_type: sqlite3 db_type: sqlite3
# For production: # For production:
# db_path: /var/lib/headscale/db.sqlite db_path: /var/lib/headscale/db.sqlite
db_path: ./db.sqlite
# # Postgres config # # Postgres config
# If using a Unix socket to connect to Postgres, set the socket path in the 'host' field and leave 'port' blank. # If using a Unix socket to connect to Postgres, set the socket path in the 'host' field and leave 'port' blank.
@@ -172,8 +167,7 @@ tls_letsencrypt_hostname: ""
# Path to store certificates and metadata needed by # Path to store certificates and metadata needed by
# letsencrypt # letsencrypt
# For production: # For production:
# tls_letsencrypt_cache_dir: /var/lib/headscale/cache tls_letsencrypt_cache_dir: /var/lib/headscale/cache
tls_letsencrypt_cache_dir: ./cache
# Type of ACME challenge to use, currently supported types: # Type of ACME challenge to use, currently supported types:
# HTTP-01 or TLS-ALPN-01 # HTTP-01 or TLS-ALPN-01
@@ -258,13 +252,12 @@ dns_config:
# Defines the base domain to create the hostnames for MagicDNS. # Defines the base domain to create the hostnames for MagicDNS.
# `base_domain` must be a FQDNs, without the trailing dot. # `base_domain` must be a FQDNs, without the trailing dot.
# The FQDN of the hosts will be # The FQDN of the hosts will be
# `hostname.namespace.base_domain` (e.g., _myhost.mynamespace.example.com_). # `hostname.user.base_domain` (e.g., _myhost.myuser.example.com_).
base_domain: example.com base_domain: example.com
# Unix socket used for the CLI to connect without authentication # Unix socket used for the CLI to connect without authentication
# Note: for production you will want to set this to something like: # Note: for production you will want to set this to something like:
# unix_socket: /var/run/headscale.sock unix_socket: /var/run/headscale/headscale.sock
unix_socket: ./headscale.sock
unix_socket_permission: "0770" unix_socket_permission: "0770"
# #
# headscale supports experimental OpenID connect support, # headscale supports experimental OpenID connect support,
@@ -276,29 +269,45 @@ unix_socket_permission: "0770"
# issuer: "https://your-oidc.issuer.com/path" # issuer: "https://your-oidc.issuer.com/path"
# client_id: "your-oidc-client-id" # client_id: "your-oidc-client-id"
# client_secret: "your-oidc-client-secret" # client_secret: "your-oidc-client-secret"
# # Alternatively, set `client_secret_path` to read the secret from the file.
# # It resolves environment variables, making integration to systemd's
# # `LoadCredential` straightforward:
# client_secret_path: "${CREDENTIALS_DIRECTORY}/oidc_client_secret"
# # client_secret and client_secret_path are mutually exclusive.
# #
# Customize the scopes used in the OIDC flow, defaults to "openid", "profile" and "email" and add custom query # # The amount of time from a node is authenticated with OpenID until it
# parameters to the Authorize Endpoint request. Scopes default to "openid", "profile" and "email". # # expires and needs to reauthenticate.
# # Setting the value to "0" will mean no expiry.
# expiry: 180d
#
# # Use the expiry from the token received from OpenID when the user logged
# # in, this will typically lead to frequent need to reauthenticate and should
# # only been enabled if you know what you are doing.
# # Note: enabling this will cause `oidc.expiry` to be ignored.
# use_expiry_from_token: false
#
# # Customize the scopes used in the OIDC flow, defaults to "openid", "profile" and "email" and add custom query
# # parameters to the Authorize Endpoint request. Scopes default to "openid", "profile" and "email".
# #
# scope: ["openid", "profile", "email", "custom"] # scope: ["openid", "profile", "email", "custom"]
# extra_params: # extra_params:
# domain_hint: example.com # domain_hint: example.com
# #
# List allowed principal domains and/or users. If an authenticated user's domain is not in this list, the # # List allowed principal domains and/or users. If an authenticated user's domain is not in this list, the
# authentication request will be rejected. # # authentication request will be rejected.
# #
# allowed_domains: # allowed_domains:
# - example.com # - example.com
# Groups from keycloak have a leading '/' # # Note: Groups from keycloak have a leading '/'
# allowed_groups: # allowed_groups:
# - /headscale # - /headscale
# allowed_users: # allowed_users:
# - alice@example.com # - alice@example.com
# #
# If `strip_email_domain` is set to `true`, the domain part of the username email address will be removed. # # If `strip_email_domain` is set to `true`, the domain part of the username email address will be removed.
# This will transform `first-name.last-name@example.com` to the namespace `first-name.last-name` # # This will transform `first-name.last-name@example.com` to the user `first-name.last-name`
# If `strip_email_domain` is set to `false` the domain part will NOT be removed resulting to the following # # If `strip_email_domain` is set to `false` the domain part will NOT be removed resulting to the following
# namespace: `first-name.last-name.example.com` # user: `first-name.last-name.example.com`
# #
# strip_email_domain: true # strip_email_domain: true

114
config.go
View File

@@ -6,14 +6,17 @@ import (
"io/fs" "io/fs"
"net/netip" "net/netip"
"net/url" "net/url"
"os"
"strings" "strings"
"time" "time"
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
"github.com/prometheus/common/model"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/spf13/viper" "github.com/spf13/viper"
"go4.org/netipx" "go4.org/netipx"
"tailscale.com/net/tsaddr"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/dnstype" "tailscale.com/types/dnstype"
) )
@@ -24,6 +27,13 @@ const (
JSONLogFormat = "json" JSONLogFormat = "json"
TextLogFormat = "text" TextLogFormat = "text"
defaultOIDCExpiryTime = 180 * 24 * time.Hour // 180 Days
maxDuration time.Duration = 1<<63 - 1
)
var errOidcMutuallyExclusive = errors.New(
"oidc_client_secret and oidc_client_secret_path are mutually exclusive",
) )
// Config contains the initial Headscale configuration. // Config contains the initial Headscale configuration.
@@ -98,6 +108,8 @@ type OIDCConfig struct {
AllowedUsers []string AllowedUsers []string
AllowedGroups []string AllowedGroups []string
StripEmaildomain bool StripEmaildomain bool
Expiry time.Duration
UseExpiryFromToken bool
} }
type DERPConfig struct { type DERPConfig struct {
@@ -177,6 +189,8 @@ func LoadConfig(path string, isFile bool) error {
viper.SetDefault("oidc.scope", []string{oidc.ScopeOpenID, "profile", "email"}) viper.SetDefault("oidc.scope", []string{oidc.ScopeOpenID, "profile", "email"})
viper.SetDefault("oidc.strip_email_domain", true) viper.SetDefault("oidc.strip_email_domain", true)
viper.SetDefault("oidc.only_start_if_oidc_is_available", true) viper.SetDefault("oidc.only_start_if_oidc_is_available", true)
viper.SetDefault("oidc.expiry", "180d")
viper.SetDefault("oidc.use_expiry_from_token", false)
viper.SetDefault("logtail.enabled", false) viper.SetDefault("logtail.enabled", false)
viper.SetDefault("randomize_client_port", false) viper.SetDefault("randomize_client_port", false)
@@ -408,34 +422,32 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) {
} }
if viper.IsSet("dns_config.restricted_nameservers") { if viper.IsSet("dns_config.restricted_nameservers") {
if len(dnsConfig.Resolvers) > 0 { dnsConfig.Routes = make(map[string][]*dnstype.Resolver)
dnsConfig.Routes = make(map[string][]*dnstype.Resolver) domains := []string{}
restrictedDNS := viper.GetStringMapStringSlice( restrictedDNS := viper.GetStringMapStringSlice(
"dns_config.restricted_nameservers", "dns_config.restricted_nameservers",
)
for domain, restrictedNameservers := range restrictedDNS {
restrictedResolvers := make(
[]*dnstype.Resolver,
len(restrictedNameservers),
) )
for domain, restrictedNameservers := range restrictedDNS { for index, nameserverStr := range restrictedNameservers {
restrictedResolvers := make( nameserver, err := netip.ParseAddr(nameserverStr)
[]*dnstype.Resolver, if err != nil {
len(restrictedNameservers), log.Error().
) Str("func", "getDNSConfig").
for index, nameserverStr := range restrictedNameservers { Err(err).
nameserver, err := netip.ParseAddr(nameserverStr) Msgf("Could not parse restricted nameserver IP: %s", nameserverStr)
if err != nil { }
log.Error(). restrictedResolvers[index] = &dnstype.Resolver{
Str("func", "getDNSConfig"). Addr: nameserver.String(),
Err(err).
Msgf("Could not parse restricted nameserver IP: %s", nameserverStr)
}
restrictedResolvers[index] = &dnstype.Resolver{
Addr: nameserver.String(),
}
} }
dnsConfig.Routes[domain] = restrictedResolvers
} }
} else { dnsConfig.Routes[domain] = restrictedResolvers
log.Warn(). domains = append(domains, domain)
Msg("Warning: dns_config.restricted_nameservers is set, but no nameservers are configured. Ignoring restricted_nameservers.")
} }
dnsConfig.Domains = domains
} }
if viper.IsSet("dns_config.domains") { if viper.IsSet("dns_config.domains") {
@@ -504,6 +516,29 @@ func GetHeadscaleConfig() (*Config, error) {
if err != nil { if err != nil {
panic(fmt.Errorf("failed to parse ip_prefixes[%d]: %w", i, err)) panic(fmt.Errorf("failed to parse ip_prefixes[%d]: %w", i, err))
} }
if prefix.Addr().Is4() {
builder := netipx.IPSetBuilder{}
builder.AddPrefix(tsaddr.CGNATRange())
ipSet, _ := builder.IPSet()
if !ipSet.ContainsPrefix(prefix) {
log.Warn().
Msgf("Prefix %s is not in the %s range. This is an unsupported configuration.",
prefixInConfig, tsaddr.CGNATRange())
}
}
if prefix.Addr().Is6() {
builder := netipx.IPSetBuilder{}
builder.AddPrefix(tsaddr.TailscaleULARange())
ipSet, _ := builder.IPSet()
if !ipSet.ContainsPrefix(prefix) {
log.Warn().
Msgf("Prefix %s is not in the %s range. This is an unsupported configuration.",
prefixInConfig, tsaddr.TailscaleULARange())
}
}
parsedPrefixes = append(parsedPrefixes, prefix) parsedPrefixes = append(parsedPrefixes, prefix)
} }
@@ -528,6 +563,19 @@ func GetHeadscaleConfig() (*Config, error) {
Msgf("'ip_prefixes' not configured, falling back to default: %v", prefixes) Msgf("'ip_prefixes' not configured, falling back to default: %v", prefixes)
} }
oidcClientSecret := viper.GetString("oidc.client_secret")
oidcClientSecretPath := viper.GetString("oidc.client_secret_path")
if oidcClientSecretPath != "" && oidcClientSecret != "" {
return nil, errOidcMutuallyExclusive
}
if oidcClientSecretPath != "" {
secretBytes, err := os.ReadFile(os.ExpandEnv(oidcClientSecretPath))
if err != nil {
return nil, err
}
oidcClientSecret = string(secretBytes)
}
return &Config{ return &Config{
ServerURL: viper.GetString("server_url"), ServerURL: viper.GetString("server_url"),
Addr: viper.GetString("listen_addr"), Addr: viper.GetString("listen_addr"),
@@ -580,13 +628,29 @@ func GetHeadscaleConfig() (*Config, error) {
), ),
Issuer: viper.GetString("oidc.issuer"), Issuer: viper.GetString("oidc.issuer"),
ClientID: viper.GetString("oidc.client_id"), ClientID: viper.GetString("oidc.client_id"),
ClientSecret: viper.GetString("oidc.client_secret"), ClientSecret: oidcClientSecret,
Scope: viper.GetStringSlice("oidc.scope"), Scope: viper.GetStringSlice("oidc.scope"),
ExtraParams: viper.GetStringMapString("oidc.extra_params"), ExtraParams: viper.GetStringMapString("oidc.extra_params"),
AllowedDomains: viper.GetStringSlice("oidc.allowed_domains"), AllowedDomains: viper.GetStringSlice("oidc.allowed_domains"),
AllowedUsers: viper.GetStringSlice("oidc.allowed_users"), AllowedUsers: viper.GetStringSlice("oidc.allowed_users"),
AllowedGroups: viper.GetStringSlice("oidc.allowed_groups"), AllowedGroups: viper.GetStringSlice("oidc.allowed_groups"),
StripEmaildomain: viper.GetBool("oidc.strip_email_domain"), StripEmaildomain: viper.GetBool("oidc.strip_email_domain"),
Expiry: func() time.Duration {
// if set to 0, we assume no expiry
if value := viper.GetString("oidc.expiry"); value == "0" {
return maxDuration
} else {
expiry, err := model.ParseDuration(value)
if err != nil {
log.Warn().Msg("failed to parse oidc.expiry, defaulting back to 180 days")
return defaultOIDCExpiryTime
}
return time.Duration(expiry)
}
}(),
UseExpiryFromToken: viper.GetBool("oidc.use_expiry_from_token"),
}, },
LogTail: logConfig, LogTail: logConfig,

20
db.go
View File

@@ -41,6 +41,16 @@ func (h *Headscale) initDB() error {
db.Exec(`create extension if not exists "uuid-ossp";`) db.Exec(`create extension if not exists "uuid-ossp";`)
} }
_ = db.Migrator().RenameTable("namespaces", "users")
err = db.AutoMigrate(&User{})
if err != nil {
return err
}
_ = db.Migrator().RenameColumn(&Machine{}, "namespace_id", "user_id")
_ = db.Migrator().RenameColumn(&PreAuthKey{}, "namespace_id", "user_id")
_ = db.Migrator().RenameColumn(&Machine{}, "ip_address", "ip_addresses") _ = db.Migrator().RenameColumn(&Machine{}, "ip_address", "ip_addresses")
_ = db.Migrator().RenameColumn(&Machine{}, "name", "hostname") _ = db.Migrator().RenameColumn(&Machine{}, "name", "hostname")
@@ -110,7 +120,10 @@ func (h *Headscale) initDB() error {
continue continue
} }
err = db.Preload("Machine").Where("machine_id = ? AND prefix = ?", machine.ID, IPPrefix(prefix)).First(&Route{}).Error err = db.Preload("Machine").
Where("machine_id = ? AND prefix = ?", machine.ID, IPPrefix(prefix)).
First(&Route{}).
Error
if err == nil { if err == nil {
log.Info(). log.Info().
Str("enabled_route", prefix.String()). Str("enabled_route", prefix.String()).
@@ -184,11 +197,6 @@ func (h *Headscale) initDB() error {
return err return err
} }
err = db.AutoMigrate(&Namespace{})
if err != nil {
return err
}
err = db.AutoMigrate(&PreAuthKey{}) err = db.AutoMigrate(&PreAuthKey{})
if err != nil { if err != nil {
return err return err

View File

@@ -10,7 +10,7 @@ import (
"time" "time"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v3"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
) )

View File

@@ -157,14 +157,14 @@ func (h *Headscale) DERPHandler(
if !fastStart { if !fastStart {
pubKey := h.privateKey.Public() pubKey := h.privateKey.Public()
pubKeyStr := pubKey.UntypedHexString() //nolint pubKeyStr, _ := pubKey.MarshalText() //nolint
fmt.Fprintf(conn, "HTTP/1.1 101 Switching Protocols\r\n"+ fmt.Fprintf(conn, "HTTP/1.1 101 Switching Protocols\r\n"+
"Upgrade: DERP\r\n"+ "Upgrade: DERP\r\n"+
"Connection: Upgrade\r\n"+ "Connection: Upgrade\r\n"+
"Derp-Version: %v\r\n"+ "Derp-Version: %v\r\n"+
"Derp-Public-Key: %s\r\n\r\n", "Derp-Public-Key: %s\r\n\r\n",
derp.ProtocolVersion, derp.ProtocolVersion,
pubKeyStr) string(pubKeyStr))
} }
h.DERPServer.tailscaleDERP.Accept(req.Context(), netConn, conn, netConn.RemoteAddr().String()) h.DERPServer.tailscaleDERP.Accept(req.Context(), netConn, conn, netConn.RemoteAddr().String())

14
dns.go
View File

@@ -190,23 +190,23 @@ func getMapResponseDNSConfig(
) *tailcfg.DNSConfig { ) *tailcfg.DNSConfig {
var dnsConfig *tailcfg.DNSConfig = dnsConfigOrig.Clone() var dnsConfig *tailcfg.DNSConfig = dnsConfigOrig.Clone()
if dnsConfigOrig != nil && dnsConfigOrig.Proxied { // if MagicDNS is enabled if dnsConfigOrig != nil && dnsConfigOrig.Proxied { // if MagicDNS is enabled
// Only inject the Search Domain of the current namespace - shared nodes should use their full FQDN // Only inject the Search Domain of the current user - shared nodes should use their full FQDN
dnsConfig.Domains = append( dnsConfig.Domains = append(
dnsConfig.Domains, dnsConfig.Domains,
fmt.Sprintf( fmt.Sprintf(
"%s.%s", "%s.%s",
machine.Namespace.Name, machine.User.Name,
baseDomain, baseDomain,
), ),
) )
namespaceSet := mapset.NewSet[Namespace]() userSet := mapset.NewSet[User]()
namespaceSet.Add(machine.Namespace) userSet.Add(machine.User)
for _, p := range peers { for _, p := range peers {
namespaceSet.Add(p.Namespace) userSet.Add(p.User)
} }
for _, namespace := range namespaceSet.ToSlice() { for _, user := range userSet.ToSlice() {
dnsRoute := fmt.Sprintf("%v.%v", namespace.Name, baseDomain) dnsRoute := fmt.Sprintf("%v.%v", user.Name, baseDomain)
dnsConfig.Routes[dnsRoute] = nil dnsConfig.Routes[dnsRoute] = nil
} }
} else { } else {

View File

@@ -112,17 +112,17 @@ func (s *Suite) TestMagicDNSRootDomainsIPv6SingleMultiple(c *check.C) {
} }
func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) { func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
namespaceShared1, err := app.CreateNamespace("shared1") userShared1, err := app.CreateUser("shared1")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
namespaceShared2, err := app.CreateNamespace("shared2") userShared2, err := app.CreateUser("shared2")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
namespaceShared3, err := app.CreateNamespace("shared3") userShared3, err := app.CreateUser("shared3")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
preAuthKeyInShared1, err := app.CreatePreAuthKey( preAuthKeyInShared1, err := app.CreatePreAuthKey(
namespaceShared1.Name, userShared1.Name,
false, false,
false, false,
nil, nil,
@@ -131,7 +131,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
preAuthKeyInShared2, err := app.CreatePreAuthKey( preAuthKeyInShared2, err := app.CreatePreAuthKey(
namespaceShared2.Name, userShared2.Name,
false, false,
false, false,
nil, nil,
@@ -140,7 +140,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
preAuthKeyInShared3, err := app.CreatePreAuthKey( preAuthKeyInShared3, err := app.CreatePreAuthKey(
namespaceShared3.Name, userShared3.Name,
false, false,
false, false,
nil, nil,
@@ -149,7 +149,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
PreAuthKey2InShared1, err := app.CreatePreAuthKey( PreAuthKey2InShared1, err := app.CreatePreAuthKey(
namespaceShared1.Name, userShared1.Name,
false, false,
false, false,
nil, nil,
@@ -157,7 +157,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
) )
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachine(namespaceShared1.Name, "test_get_shared_nodes_1") _, err = app.GetMachine(userShared1.Name, "test_get_shared_nodes_1")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
machineInShared1 := &Machine{ machineInShared1 := &Machine{
@@ -166,15 +166,15 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
Hostname: "test_get_shared_nodes_1", Hostname: "test_get_shared_nodes_1",
NamespaceID: namespaceShared1.ID, UserID: userShared1.ID,
Namespace: *namespaceShared1, User: *userShared1,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")}, IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")},
AuthKeyID: uint(preAuthKeyInShared1.ID), AuthKeyID: uint(preAuthKeyInShared1.ID),
} }
app.db.Save(machineInShared1) app.db.Save(machineInShared1)
_, err = app.GetMachine(namespaceShared1.Name, machineInShared1.Hostname) _, err = app.GetMachine(userShared1.Name, machineInShared1.Hostname)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
machineInShared2 := &Machine{ machineInShared2 := &Machine{
@@ -183,15 +183,15 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Hostname: "test_get_shared_nodes_2", Hostname: "test_get_shared_nodes_2",
NamespaceID: namespaceShared2.ID, UserID: userShared2.ID,
Namespace: *namespaceShared2, User: *userShared2,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.2")}, IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.2")},
AuthKeyID: uint(preAuthKeyInShared2.ID), AuthKeyID: uint(preAuthKeyInShared2.ID),
} }
app.db.Save(machineInShared2) app.db.Save(machineInShared2)
_, err = app.GetMachine(namespaceShared2.Name, machineInShared2.Hostname) _, err = app.GetMachine(userShared2.Name, machineInShared2.Hostname)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
machineInShared3 := &Machine{ machineInShared3 := &Machine{
@@ -200,15 +200,15 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Hostname: "test_get_shared_nodes_3", Hostname: "test_get_shared_nodes_3",
NamespaceID: namespaceShared3.ID, UserID: userShared3.ID,
Namespace: *namespaceShared3, User: *userShared3,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.3")}, IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.3")},
AuthKeyID: uint(preAuthKeyInShared3.ID), AuthKeyID: uint(preAuthKeyInShared3.ID),
} }
app.db.Save(machineInShared3) app.db.Save(machineInShared3)
_, err = app.GetMachine(namespaceShared3.Name, machineInShared3.Hostname) _, err = app.GetMachine(userShared3.Name, machineInShared3.Hostname)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
machine2InShared1 := &Machine{ machine2InShared1 := &Machine{
@@ -217,8 +217,8 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Hostname: "test_get_shared_nodes_4", Hostname: "test_get_shared_nodes_4",
NamespaceID: namespaceShared1.ID, UserID: userShared1.ID,
Namespace: *namespaceShared1, User: *userShared1,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.4")}, IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.4")},
AuthKeyID: uint(PreAuthKey2InShared1.ID), AuthKeyID: uint(PreAuthKey2InShared1.ID),
@@ -245,31 +245,31 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
c.Assert(len(dnsConfig.Routes), check.Equals, 3) c.Assert(len(dnsConfig.Routes), check.Equals, 3)
domainRouteShared1 := fmt.Sprintf("%s.%s", namespaceShared1.Name, baseDomain) domainRouteShared1 := fmt.Sprintf("%s.%s", userShared1.Name, baseDomain)
_, ok := dnsConfig.Routes[domainRouteShared1] _, ok := dnsConfig.Routes[domainRouteShared1]
c.Assert(ok, check.Equals, true) c.Assert(ok, check.Equals, true)
domainRouteShared2 := fmt.Sprintf("%s.%s", namespaceShared2.Name, baseDomain) domainRouteShared2 := fmt.Sprintf("%s.%s", userShared2.Name, baseDomain)
_, ok = dnsConfig.Routes[domainRouteShared2] _, ok = dnsConfig.Routes[domainRouteShared2]
c.Assert(ok, check.Equals, true) c.Assert(ok, check.Equals, true)
domainRouteShared3 := fmt.Sprintf("%s.%s", namespaceShared3.Name, baseDomain) domainRouteShared3 := fmt.Sprintf("%s.%s", userShared3.Name, baseDomain)
_, ok = dnsConfig.Routes[domainRouteShared3] _, ok = dnsConfig.Routes[domainRouteShared3]
c.Assert(ok, check.Equals, true) c.Assert(ok, check.Equals, true)
} }
func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) { func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
namespaceShared1, err := app.CreateNamespace("shared1") userShared1, err := app.CreateUser("shared1")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
namespaceShared2, err := app.CreateNamespace("shared2") userShared2, err := app.CreateUser("shared2")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
namespaceShared3, err := app.CreateNamespace("shared3") userShared3, err := app.CreateUser("shared3")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
preAuthKeyInShared1, err := app.CreatePreAuthKey( preAuthKeyInShared1, err := app.CreatePreAuthKey(
namespaceShared1.Name, userShared1.Name,
false, false,
false, false,
nil, nil,
@@ -278,7 +278,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
preAuthKeyInShared2, err := app.CreatePreAuthKey( preAuthKeyInShared2, err := app.CreatePreAuthKey(
namespaceShared2.Name, userShared2.Name,
false, false,
false, false,
nil, nil,
@@ -287,7 +287,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
preAuthKeyInShared3, err := app.CreatePreAuthKey( preAuthKeyInShared3, err := app.CreatePreAuthKey(
namespaceShared3.Name, userShared3.Name,
false, false,
false, false,
nil, nil,
@@ -296,7 +296,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
preAuthKey2InShared1, err := app.CreatePreAuthKey( preAuthKey2InShared1, err := app.CreatePreAuthKey(
namespaceShared1.Name, userShared1.Name,
false, false,
false, false,
nil, nil,
@@ -304,7 +304,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
) )
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachine(namespaceShared1.Name, "test_get_shared_nodes_1") _, err = app.GetMachine(userShared1.Name, "test_get_shared_nodes_1")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
machineInShared1 := &Machine{ machineInShared1 := &Machine{
@@ -313,15 +313,15 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
Hostname: "test_get_shared_nodes_1", Hostname: "test_get_shared_nodes_1",
NamespaceID: namespaceShared1.ID, UserID: userShared1.ID,
Namespace: *namespaceShared1, User: *userShared1,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")}, IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")},
AuthKeyID: uint(preAuthKeyInShared1.ID), AuthKeyID: uint(preAuthKeyInShared1.ID),
} }
app.db.Save(machineInShared1) app.db.Save(machineInShared1)
_, err = app.GetMachine(namespaceShared1.Name, machineInShared1.Hostname) _, err = app.GetMachine(userShared1.Name, machineInShared1.Hostname)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
machineInShared2 := &Machine{ machineInShared2 := &Machine{
@@ -330,15 +330,15 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Hostname: "test_get_shared_nodes_2", Hostname: "test_get_shared_nodes_2",
NamespaceID: namespaceShared2.ID, UserID: userShared2.ID,
Namespace: *namespaceShared2, User: *userShared2,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.2")}, IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.2")},
AuthKeyID: uint(preAuthKeyInShared2.ID), AuthKeyID: uint(preAuthKeyInShared2.ID),
} }
app.db.Save(machineInShared2) app.db.Save(machineInShared2)
_, err = app.GetMachine(namespaceShared2.Name, machineInShared2.Hostname) _, err = app.GetMachine(userShared2.Name, machineInShared2.Hostname)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
machineInShared3 := &Machine{ machineInShared3 := &Machine{
@@ -347,15 +347,15 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Hostname: "test_get_shared_nodes_3", Hostname: "test_get_shared_nodes_3",
NamespaceID: namespaceShared3.ID, UserID: userShared3.ID,
Namespace: *namespaceShared3, User: *userShared3,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.3")}, IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.3")},
AuthKeyID: uint(preAuthKeyInShared3.ID), AuthKeyID: uint(preAuthKeyInShared3.ID),
} }
app.db.Save(machineInShared3) app.db.Save(machineInShared3)
_, err = app.GetMachine(namespaceShared3.Name, machineInShared3.Hostname) _, err = app.GetMachine(userShared3.Name, machineInShared3.Hostname)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
machine2InShared1 := &Machine{ machine2InShared1 := &Machine{
@@ -364,8 +364,8 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Hostname: "test_get_shared_nodes_4", Hostname: "test_get_shared_nodes_4",
NamespaceID: namespaceShared1.ID, UserID: userShared1.ID,
Namespace: *namespaceShared1, User: *userShared1,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.4")}, IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.4")},
AuthKeyID: uint(preAuthKey2InShared1.ID), AuthKeyID: uint(preAuthKey2InShared1.ID),

View File

@@ -1,56 +0,0 @@
# headscale documentation
This page contains the official and community contributed documentation for `headscale`.
If you are having trouble with following the documentation or get unexpected results,
please ask on [Discord](https://discord.gg/c84AZQhmpx) instead of opening an Issue.
## Official documentation
### How-to
- [Running headscale on Linux](running-headscale-linux.md)
- [Control headscale remotely](remote-cli.md)
- [Using a Windows client with headscale](windows-client.md)
- [Configuring OIDC](oidc.md)
### References
- [Configuration](../config-example.yaml)
- [Glossary](glossary.md)
- [TLS](tls.md)
## Community documentation
Community documentation is not actively maintained by the headscale authors and is
written by community members. It is _not_ verified by `headscale` developers.
**It might be outdated and it might miss necessary steps**.
- [Running headscale in a container](running-headscale-container.md)
- [Running headscale on OpenBSD](running-headscale-openbsd.md)
- [Running headscale behind a reverse proxy](reverse-proxy.md)
- [Set Custom DNS records](dns-records.md)
## Misc
### Policy ACLs
Headscale implements the same policy ACLs as Tailscale.com, adapted to the self-hosted environment.
For instance, instead of referring to users when defining groups you must
use namespaces (which are the equivalent to user/logins in Tailscale.com).
Please check https://tailscale.com/kb/1018/acls/, and `./tests/acls/` in this repo for working examples.
When using ACL's the Namespace borders are no longer applied. All machines
whichever the Namespace have the ability to communicate with other hosts as
long as the ACL's permits this exchange.
The [ACLs](acls.md) document should help understand a fictional case of setting
up ACLs in a small company. All concepts presented in this document could be
applied outside of business oriented usage.
### Apple devices
An endpoint with information on how to connect your Apple devices (currently macOS only) is available at `/apple` on your running instance.

View File

@@ -1,4 +1,15 @@
# ACLs use case example Headscale implements the same policy ACLs as Tailscale.com, adapted to the self-hosted environment.
For instance, instead of referring to users when defining groups you must
use users (which are the equivalent to user/logins in Tailscale.com).
Please check https://tailscale.com/kb/1018/acls/, and `./tests/acls/` in this repo for working examples.
When using ACL's the User borders are no longer applied. All machines
whichever the User have the ability to communicate with other hosts as
long as the ACL's permits this exchange.
## ACLs use case example
Let's build an example use case for a small business (It may be the place where Let's build an example use case for a small business (It may be the place where
ACL's are the most useful). ACL's are the most useful).
@@ -29,19 +40,21 @@ servers.
## ACL setup ## ACL setup
Note: Namespaces will be created automatically when users authenticate with the Note: Users will be created automatically when users authenticate with the
Headscale server. Headscale server.
ACLs could be written either on [huJSON](https://github.com/tailscale/hujson) ACLs could be written either on [huJSON](https://github.com/tailscale/hujson)
or YAML. Check the [test ACLs](../tests/acls) for further information. or YAML. Check the [test ACLs](../tests/acls) for further information.
When registering the servers we will need to add the flag When registering the servers we will need to add the flag
`--advertise-tags=tag:<tag1>,tag:<tag2>`, and the user (namespace) that is `--advertise-tags=tag:<tag1>,tag:<tag2>`, and the user that is
registering the server should be allowed to do it. Since anyone can add tags to 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 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 and only valid tags are applied. A tag is valid if the user that is
registering it is allowed to do it. registering it is allowed to do it.
To use ACLs in headscale, you must edit your config.yaml file. In there you will find a `acl_policy_path: ""` parameter. This will need to point to your ACL file. More info on how these policies are written can be found [here](https://tailscale.com/kb/1018/acls/).
Here are the ACL's to implement the same permissions as above: Here are the ACL's to implement the same permissions as above:
```json ```json
@@ -164,8 +177,8 @@ Here are the ACL's to implement the same permissions as above:
"dst": ["tag:dev-app-servers:80,443"] "dst": ["tag:dev-app-servers:80,443"]
}, },
// We still have to allow internal namespaces communications since nothing guarantees that each user have // We still have to allow internal users communications since nothing guarantees that each user have
// their own namespaces. // their own users.
{ "action": "accept", "src": ["boss"], "dst": ["boss:*"] }, { "action": "accept", "src": ["boss"], "dst": ["boss:*"] },
{ "action": "accept", "src": ["dev1"], "dst": ["dev1:*"] }, { "action": "accept", "src": ["dev1"], "dst": ["dev1:*"] },
{ "action": "accept", "src": ["dev2"], "dst": ["dev2:*"] }, { "action": "accept", "src": ["dev2"], "dst": ["dev2:*"] },

View File

@@ -1,5 +1,12 @@
# Setting custom DNS records # Setting custom DNS records
!!! warning "Community documentation"
This page is not actively maintained by the headscale authors and is
written by community members. It is _not_ verified by `headscale` developers.
**It might be outdated and it might miss necessary steps**.
## Goal ## Goal
This documentation has the goal of showing how a user can set custom DNS records with `headscale`s magic dns. This documentation has the goal of showing how a user can set custom DNS records with `headscale`s magic dns.
@@ -80,4 +87,4 @@ server {
## Limitations ## Limitations
[Not all types of records are supported](https://github.com/tailscale/tailscale/blob/main/ipn/ipnlocal/local.go#L2891-L2909), especially no CNAME records. [Not all types of records are supported](https://github.com/tailscale/tailscale/blob/6edf357b96b28ee1be659a70232c0135b2ffedfd/ipn/ipnlocal/local.go#L2989-L3007), especially no CNAME records.

49
docs/exit-node.md Normal file
View File

@@ -0,0 +1,49 @@
# Exit Nodes
## On the node
Register the node and make it advertise itself as an exit node:
```console
$ sudo tailscale up --login-server https://my-server.com --advertise-exit-node
```
If the node is already registered, it can advertise exit capabilities like this:
```console
$ sudo tailscale set --advertise-exit-node
```
To use a node as an exit node, IP forwarding must be enabled on the node. Check the official [Tailscale documentation](https://tailscale.com/kb/1019/subnets/?tab=linux#enable-ip-forwarding) for how to enable IP fowarding.
## On the control server
```console
$ # list nodes
$ headscale routes list
ID | Machine | Prefix | Advertised | Enabled | Primary
1 | | 0.0.0.0/0 | false | false | -
2 | | ::/0 | false | false | -
3 | phobos | 0.0.0.0/0 | true | false | -
4 | phobos | ::/0 | true | false | -
$ # enable routes for phobos
$ headscale routes enable -r 3
$ headscale routes enable -r 4
$ # Check node list again. The routes are now enabled.
$ headscale routes list
ID | Machine | Prefix | Advertised | Enabled | Primary
1 | | 0.0.0.0/0 | false | false | -
2 | | ::/0 | false | false | -
3 | phobos | 0.0.0.0/0 | true | true | -
4 | phobos | ::/0 | true | true | -
```
## On the client
The exit node can now be used with:
```console
$ sudo tailscale set --exit-node phobos
```
Check the official [Tailscale documentation](https://tailscale.com/kb/1103/exit-nodes/?q=exit#step-3-use-the-exit-node) for how to do it on your device.

53
docs/faq.md Normal file
View File

@@ -0,0 +1,53 @@
---
hide:
- navigation
---
# Frequently Asked Questions
## What is the design goal of headscale?
`headscale` aims to implement a self-hosted, open source alternative to the [Tailscale](https://tailscale.com/)
control server.
`headscale`'s goal is to provide self-hosters and hobbyists with an open-source
server they can use for their projects and labs.
It implements a narrow scope, a _single_ Tailnet, suitable for a personal use, or a small
open-source organisation.
## How can I contribute?
Headscale is "Open Source, acknowledged contribution", this means that any
contribution will have to be discussed with the Maintainers before being submitted.
Headscale is open to code contributions for bug fixes without discussion.
If you find mistakes in the documentation, please also submit a fix to the documentation.
## Why is 'acknowledged contribution' the chosen model?
Both maintainers have full-time jobs and families, and we want to avoid burnout. We also want to avoid frustration from contributors when their PRs are not accepted.
We are more than happy to exchange emails, or to have dedicated calls before a PR is submitted.
## When/Why is Feature X going to be implemented?
We don't know. We might be working on it. If you want to help, please send us a PR.
Please be aware that there are a number of reasons why we might not accept specific contributions:
- It is not possible to implement the feature in a way that makes sense in a self-hosted environment.
- Given that we are reverse-engineering Tailscale to satify our own curiosity, we might be interested in implementing the feature ourselves.
- You are not sending unit and integration tests with it.
## Do you support Y method of deploying Headscale?
We currently support deploying `headscale` using our binaries and the DEB packages. Both can be found in the
[GitHub releases page](https://github.com/juanfont/headscale/releases).
In addition to that, there are semi-official RPM packages by the Fedora infra team https://copr.fedorainfracloud.org/coprs/jonathanspw/headscale/
For convenience, we also build Docker images with `headscale`. But **please be aware that we don't officially support deploying `headscale` using Docker**. We have a [Discord channel](https://discord.com/channels/896711691637780480/1070619770942148618) where you can ask for Docker-specific help to the community.
## Why is my reverse proxy not working with Headscale?
We don't know. We don't use reverse proxies with `headscale` ourselves, so we don't have any experience with them. We have [community documentation](https://headscale.net/reverse-proxy/) on how to configure various reverse proxies, and a dedicated [Discord channel](https://discord.com/channels/896711691637780480/1070619818346164324) where you can ask for help to the community.

View File

@@ -1,6 +1,6 @@
# Glossary # Glossary
| Term | Description | | Term | Description |
| --------- | --------------------------------------------------------------------------------------------------------------------- | | --------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| Machine | A machine is a single entity connected to `headscale`, typically an installation of Tailscale. Also known as **Node** | | Machine | A machine is a single entity connected to `headscale`, typically an installation of Tailscale. Also known as **Node** |
| Namespace | A namespace is a logical grouping of machines "owned" by the same entity, in Tailscale, this is typically a User | | Namespace | A namespace was a logical grouping of machines "owned" by the same entity, in Tailscale, this is typically a User (This is now called user) |

30
docs/iOS-client.md Normal file
View File

@@ -0,0 +1,30 @@
# Connecting an iOS client
## Goal
This documentation has the goal of showing how a user can use the official iOS [Tailscale](https://tailscale.com) client with `headscale`.
## Installation
Install the official Tailscale iOS client from the [App Store](https://apps.apple.com/app/tailscale/id1470499037).
Ensure that the installed version is at least 1.38.1, as that is the first release to support alternate control servers.
## Configuring the headscale URL
!!! info "Apple devices"
An endpoint with information on how to connect your Apple devices
(currently macOS only) is available at `/apple` on your running instance.
Ensure that the tailscale app is logged out before proceeding.
Go to iOS settings, scroll down past game center and tv provider to the tailscale app and select it. The headscale URL can be entered into the _"ALTERNATE COORDINATION SERVER URL"_ box.
> **Note**
>
> If the app was previously logged into tailscale, toggle on the _Reset Keychain_ switch.
Restart the app by closing it from the iOS app switcher, open the app and select the regular _Sign in_ option (non-SSO), and it should open up to the headscale authentication page.
Enter your credentials and log in. Headscale should now be working on your iOS device.

43
docs/index.md Normal file
View File

@@ -0,0 +1,43 @@
---
hide:
- navigation
- toc
---
# headscale
`headscale` is an open source, self-hosted implementation of the Tailscale control server.
This page contains the documentation for the latest version of headscale. Please also check our [FAQ](/faq/).
Join our [Discord](https://discord.gg/c84AZQhmpx) server for a chat and community support.
## Design goal
Headscale aims to implement a self-hosted, open source alternative to the Tailscale
control server.
Headscale's goal is to provide self-hosters and hobbyists with an open-source
server they can use for their projects and labs.
It implements a narrower scope, a single Tailnet, suitable for a personal use, or a small
open-source organisation.
## Supporting headscale
If you like `headscale` and find it useful, there is a sponsorship and donation
buttons available in the repo.
## Contributing
Headscale is "Open Source, acknowledged contribution", this means that any
contribution will have to be discussed with the Maintainers before being submitted.
This model has been chosen to reduce the risk of burnout by limiting the
maintenance overhead of reviewing and validating third-party code.
Headscale is open to code contributions for bug fixes without discussion.
If you find mistakes in the documentation, please submit a fix to the documentation.
## About
`headscale` is maintained by [Kristoffer Dalby](https://kradalby.no/) and [Juan Font](https://font.eu).

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2" viewBox="0 0 1280 640"><circle cx="141.023" cy="338.36" r="117.472" style="fill:#f8b5cb" transform="matrix(.997276 0 0 1.00556 10.0024 -14.823)"/><circle cx="352.014" cy="268.302" r="33.095" style="fill:#a2a2a2" transform="matrix(1.01749 0 0 1 -3.15847 0)"/><circle cx="352.014" cy="268.302" r="33.095" style="fill:#a2a2a2" transform="matrix(1.01749 0 0 1 -3.15847 115.914)"/><circle cx="352.014" cy="268.302" r="33.095" style="fill:#a2a2a2" transform="matrix(1.01749 0 0 1 148.43 115.914)"/><circle cx="352.014" cy="268.302" r="33.095" style="fill:#a2a2a2" transform="matrix(1.01749 0 0 1 148.851 0)"/><circle cx="805.557" cy="336.915" r="118.199" style="fill:#8d8d8d" transform="matrix(.99196 0 0 1 3.36978 -10.2458)"/><circle cx="805.557" cy="336.915" r="118.199" style="fill:#8d8d8d" transform="matrix(.99196 0 0 1 255.633 -10.2458)"/><path d="M680.282 124.808h-68.093v390.325h68.081v-28.23H640V153.228h40.282v-28.42Z" style="fill:#303030"/><path d="M680.282 124.808h-68.093v390.325h68.081v-28.23H640V153.228h40.282v-28.42Z" style="fill:#303030" transform="matrix(-1 0 0 1 1857.19 0)"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@@ -20,6 +20,10 @@ oidc:
# Specified/generated by your OIDC provider # Specified/generated by your OIDC provider
client_id: "your-oidc-client-id" client_id: "your-oidc-client-id"
client_secret: "your-oidc-client-secret" client_secret: "your-oidc-client-secret"
# alternatively, set `client_secret_path` to read the secret from the file.
# It resolves environment variables, making integration to systemd's
# `LoadCredential` straightforward:
#client_secret_path: "${CREDENTIALS_DIRECTORY}/oidc_client_secret"
# Customize the scopes used in the OIDC flow, defaults to "openid", "profile" and "email" and add custom query # Customize the scopes used in the OIDC flow, defaults to "openid", "profile" and "email" and add custom query
# parameters to the Authorize Endpoint request. Scopes default to "openid", "profile" and "email". # parameters to the Authorize Endpoint request. Scopes default to "openid", "profile" and "email".
@@ -40,9 +44,9 @@ oidc:
- alice@example.com - alice@example.com
# If `strip_email_domain` is set to `true`, the domain part of the username email address will be removed. # If `strip_email_domain` is set to `true`, the domain part of the username email address will be removed.
# This will transform `first-name.last-name@example.com` to the namespace `first-name.last-name` # This will transform `first-name.last-name@example.com` to the user `first-name.last-name`
# If `strip_email_domain` is set to `false` the domain part will NOT be removed resulting to the following # If `strip_email_domain` is set to `false` the domain part will NOT be removed resulting to the following
# namespace: `first-name.last-name.example.com` # user: `first-name.last-name.example.com`
strip_email_domain: true strip_email_domain: true
``` ```
@@ -135,3 +139,34 @@ oidc:
# Optional: Force the Azure AD account picker # Optional: Force the Azure AD account picker
prompt: select_account prompt: select_account
``` ```
## Google OAuth Example
In order to integrate Headscale with Google, you'll need to have a [Google Cloud Console](https://console.cloud.google.com) account.
Google OAuth has a [verification process](https://support.google.com/cloud/answer/9110914?hl=en) if you need to have users authenticate who are outside of your domain. If you only need to authenticate users from your domain name (ie `@example.com`), you don't need to go through the verification process.
However if you don't have a domain, or need to add users outside of your domain, you can manually add emails via Google Console.
### Steps
1. Go to [Google Console](https://console.cloud.google.com) and login or create an account if you don't have one.
2. Create a project (if you don't already have one).
3. On the left hand menu, go to `APIs and services` -> `Credentials`
4. Click `Create Credentials` -> `OAuth client ID`
5. Under `Application Type`, choose `Web Application`
6. For `Name`, enter whatever you like
7. Under `Authorised redirect URIs`, use `https://example.com/oidc/callback`, replacing example.com with your Headscale URL.
8. Click `Save` at the bottom of the form
9. Take note of the `Client ID` and `Client secret`, you can also download it for reference if you need it.
10. Edit your headscale config, under `oidc`, filling in your `client_id` and `client_secret`:
```yaml
oidc:
issuer: "https://accounts.google.com"
client_id: ""
client_secret: ""
scope: ["openid", "profile", "email"]
```
You can also use `allowed_domains` and `allowed_users` to restrict the users who can authenticate.

5
docs/packaging/README.md Normal file
View File

@@ -0,0 +1,5 @@
# Packaging
We use [nFPM](https://nfpm.goreleaser.com/) for making `.deb`, `.rpm` and `.apk`.
This folder contains files we need to package with these releases.

View File

@@ -0,0 +1,52 @@
[Unit]
After=syslog.target
After=network.target
Description=headscale coordination server for Tailscale
X-Restart-Triggers=/etc/headscale/config.yaml
[Service]
Type=simple
User=headscale
Group=headscale
ExecStart=/usr/bin/headscale serve
Restart=always
RestartSec=5
WorkingDirectory=/var/lib/headscale
ReadWritePaths=/var/lib/headscale /var/run
AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_CHOWN
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_CHOWN
LockPersonality=true
NoNewPrivileges=true
PrivateDevices=true
PrivateMounts=true
PrivateTmp=true
ProcSubset=pid
ProtectClock=true
ProtectControlGroups=true
ProtectHome=true
ProtectHome=yes
ProtectHostname=true
ProtectKernelLogs=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectProc=invisible
ProtectSystem=strict
RemoveIPC=true
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
RuntimeDirectory=headscale
RuntimeDirectoryMode=0750
StateDirectory=headscale
StateDirectoryMode=0750
SystemCallArchitectures=native
SystemCallFilter=@chown
SystemCallFilter=@system-service
SystemCallFilter=~@privileged
UMask=0077
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,86 @@
#!/bin/sh
# Determine OS platform
# shellcheck source=/dev/null
. /etc/os-release
HEADSCALE_EXE="/usr/bin/headscale"
BSD_HIER=""
HEADSCALE_RUN_DIR="/var/run/headscale"
HEADSCALE_USER="headscale"
HEADSCALE_GROUP="headscale"
ensure_sudo() {
if [ "$(id -u)" = "0" ]; then
echo "Sudo permissions detected"
else
echo "No sudo permission detected, please run as sudo"
exit 1
fi
}
ensure_headscale_path() {
if [ ! -f "$HEADSCALE_EXE" ]; then
echo "headscale not in default path, exiting..."
exit 1
fi
printf "Found headscale %s\n" "$HEADSCALE_EXE"
}
create_headscale_user() {
printf "PostInstall: Adding headscale user %s\n" "$HEADSCALE_USER"
useradd -s /bin/sh -c "headscale default user" headscale
}
create_headscale_group() {
if command -V systemctl >/dev/null 2>&1; then
printf "PostInstall: Adding headscale group %s\n" "$HEADSCALE_GROUP"
groupadd "$HEADSCALE_GROUP"
printf "PostInstall: Adding headscale user %s to group %s\n" "$HEADSCALE_USER" "$HEADSCALE_GROUP"
usermod -a -G "$HEADSCALE_GROUP" "$HEADSCALE_USER"
fi
if [ "$ID" = "alpine" ]; then
printf "PostInstall: Adding headscale group %s\n" "$HEADSCALE_GROUP"
addgroup "$HEADSCALE_GROUP"
printf "PostInstall: Adding headscale user %s to group %s\n" "$HEADSCALE_USER" "$HEADSCALE_GROUP"
addgroup "$HEADSCALE_USER" "$HEADSCALE_GROUP"
fi
}
create_run_dir() {
printf "PostInstall: Creating headscale run directory \n"
mkdir -p "$HEADSCALE_RUN_DIR"
printf "PostInstall: Modifying group ownership of headscale run directory \n"
chown "$HEADSCALE_USER":"$HEADSCALE_GROUP" "$HEADSCALE_RUN_DIR"
}
summary() {
echo "----------------------------------------------------------------------"
echo " headscale package has been successfully installed."
echo ""
echo " Please follow the next steps to start the software:"
echo ""
echo " sudo systemctl enable headscale"
echo " sudo systemctl start headscale"
echo ""
echo " Configuration settings can be adjusted here:"
echo " ${BSD_HIER}/etc/headscale/config.yaml"
echo ""
echo "----------------------------------------------------------------------"
}
#
# Main body of the script
#
{
ensure_sudo
ensure_headscale_path
create_headscale_user
create_headscale_group
create_run_dir
summary
}

View File

@@ -0,0 +1,15 @@
#!/bin/sh
# Determine OS platform
# shellcheck source=/dev/null
. /etc/os-release
if command -V systemctl >/dev/null 2>&1; then
echo "Stop and disable headscale service"
systemctl stop headscale >/dev/null 2>&1 || true
systemctl disable headscale >/dev/null 2>&1 || true
echo "Running daemon-reload"
systemctl daemon-reload || true
fi
echo "Removing run directory"
rm -rf "/var/run/headscale.sock"

View File

@@ -1,5 +1,12 @@
# Running headscale behind a reverse proxy # Running headscale behind a reverse proxy
!!! warning "Community documentation"
This page is not actively maintained by the headscale authors and is
written by community members. It is _not_ verified by `headscale` developers.
**It might be outdated and it might miss necessary steps**.
Running headscale behind a reverse proxy is useful when running multiple applications on the same server, and you want to reuse the same external IP and port - usually tcp/443 for HTTPS. Running headscale behind a reverse proxy is useful when running multiple applications on the same server, and you want to reuse the same external IP and port - usually tcp/443 for HTTPS.
### WebSockets ### WebSockets
@@ -112,3 +119,20 @@ The following Caddyfile is all that is necessary to use Caddy as a reverse proxy
Caddy v2 will [automatically](https://caddyserver.com/docs/automatic-https) provision a certficate for your domain/subdomain, force HTTPS, and proxy websockets - no further configuration is necessary. Caddy v2 will [automatically](https://caddyserver.com/docs/automatic-https) provision a certficate for your domain/subdomain, force HTTPS, and proxy websockets - no further configuration is necessary.
For a slightly more complex configuration which utilizes Docker containers to manage Caddy, Headscale, and Headscale-UI, [Guru Computing's guide](https://blog.gurucomputing.com.au/smart-vpns-with-headscale/) is an excellent reference. For a slightly more complex configuration which utilizes Docker containers to manage Caddy, Headscale, and Headscale-UI, [Guru Computing's guide](https://blog.gurucomputing.com.au/smart-vpns-with-headscale/) is an excellent reference.
## Apache
The following minimal Apache config will proxy traffic to the Headscale instance on `<IP:PORT>`. Note that `upgrade=any` is required as a parameter for `ProxyPass` so that WebSockets traffic whose `Upgrade` header value is not equal to `WebSocket` (i. e. Tailscale Control Protocol) is forwarded correctly. See the [Apache docs](https://httpd.apache.org/docs/2.4/mod/mod_proxy_wstunnel.html) for more information on this.
```
<VirtualHost *:443>
ServerName <YOUR_SERVER_NAME>
ProxyPreserveHost On
ProxyPass / http://<IP:PORT>/ upgrade=any
SSLEngine On
SSLCertificateFile <PATH_TO_CERT>
SSLCertificateKeyFile <PATH_CERT_KEY>
</VirtualHost>
```

View File

@@ -1,7 +1,11 @@
# Running headscale in a container # Running headscale in a container
**Note:** the container documentation is maintained by the _community_ and there is no guarentee !!! warning "Community documentation"
it is up to date, or working.
This page is not actively maintained by the headscale authors and is
written by community members. It is _not_ verified by `headscale` developers.
**It might be outdated and it might miss necessary steps**.
## Goal ## Goal
@@ -24,7 +28,7 @@ cd ./headscale
touch ./config/db.sqlite touch ./config/db.sqlite
``` ```
3. **(Strongly Recommended)** Download a copy of the [example configuration](../config-example.yaml) from the [headscale repository](https://github.com/juanfont/headscale/). 3. **(Strongly Recommended)** Download a copy of the [example configuration][config-example.yaml](https://github.com/juanfont/headscale/blob/main/config-example.yaml) from the headscale repository.
Using wget: Using wget:
@@ -101,11 +105,11 @@ Verify `headscale` is available:
curl http://127.0.0.1:9090/metrics curl http://127.0.0.1:9090/metrics
``` ```
6. Create a namespace ([tailnet](https://tailscale.com/kb/1136/tailnet/)): 6. Create a user ([tailnet](https://tailscale.com/kb/1136/tailnet/)):
```shell ```shell
docker exec headscale \ docker exec headscale \
headscale namespaces create myfirstnamespace headscale users create myfirstuser
``` ```
### Register a machine (normal login) ### Register a machine (normal login)
@@ -120,7 +124,7 @@ To register a machine when running `headscale` in a container, take the headscal
```shell ```shell
docker exec headscale \ docker exec headscale \
headscale --namespace myfirstnamespace nodes register --key <YOU_+MACHINE_KEY> headscale --user myfirstuser nodes register --key <YOU_+MACHINE_KEY>
``` ```
### Register machine using a pre authenticated key ### Register machine using a pre authenticated key
@@ -129,7 +133,7 @@ Generate a key using the command line:
```shell ```shell
docker exec headscale \ docker exec headscale \
headscale --namespace myfirstnamespace preauthkeys create --reusable --expiration 24h headscale --user myfirstuser preauthkeys create --reusable --expiration 24h
``` ```
This will return a pre-authenticated key that can be used to connect a node to `headscale` during the `tailscale` command: This will return a pre-authenticated key that can be used to connect a node to `headscale` during the `tailscale` command:

View File

@@ -0,0 +1,198 @@
# Running headscale on Linux
## Note: Outdated and "advanced"
This documentation is considered the "legacy"/advanced/manual version of the documentation, you most likely do not
want to use this documentation and rather look at the distro specific documentation (TODO LINK)[].
## Goal
This documentation has the goal of showing a user how-to set up and run `headscale` on Linux.
In additional to the "get up and running section", there is an optional [SystemD section](#running-headscale-in-the-background-with-systemd)
describing how to make `headscale` run properly in a server environment.
## Configure and run `headscale`
1. Download the latest [`headscale` binary from GitHub's release page](https://github.com/juanfont/headscale/releases):
```shell
wget --output-document=/usr/local/bin/headscale \
https://github.com/juanfont/headscale/releases/download/v<HEADSCALE VERSION>/headscale_<HEADSCALE VERSION>_linux_<ARCH>
```
2. Make `headscale` executable:
```shell
chmod +x /usr/local/bin/headscale
```
3. Prepare a directory to hold `headscale` configuration and the [SQLite](https://www.sqlite.org/) database:
```shell
# Directory for configuration
mkdir -p /etc/headscale
# Directory for Database, and other variable data (like certificates)
mkdir -p /var/lib/headscale
# or if you create a headscale user:
useradd \
--create-home \
--home-dir /var/lib/headscale/ \
--system \
--user-group \
--shell /usr/bin/nologin \
headscale
```
4. Create an empty SQLite database:
```shell
touch /var/lib/headscale/db.sqlite
```
5. Create a `headscale` configuration:
```shell
touch /etc/headscale/config.yaml
```
**(Strongly Recommended)** Download a copy of the [example configuration][config-example.yaml](https://github.com/juanfont/headscale/blob/main/config-example.yaml) from the headscale repository.
6. Start the headscale server:
```shell
headscale serve
```
This command will start `headscale` in the current terminal session.
---
To continue the tutorial, open a new terminal and let it run in the background.
Alternatively use terminal emulators like [tmux](https://github.com/tmux/tmux) or [screen](https://www.gnu.org/software/screen/).
To run `headscale` in the background, please follow the steps in the [SystemD section](#running-headscale-in-the-background-with-systemd) before continuing.
7. Verify `headscale` is running:
Verify `headscale` is available:
```shell
curl http://127.0.0.1:9090/metrics
```
8. Create a user ([tailnet](https://tailscale.com/kb/1136/tailnet/)):
```shell
headscale users create myfirstuser
```
### Register a machine (normal login)
On a client machine, execute the `tailscale` login command:
```shell
tailscale up --login-server YOUR_HEADSCALE_URL
```
Register the machine:
```shell
headscale --user myfirstuser nodes register --key <YOUR_MACHINE_KEY>
```
### Register machine using a pre authenticated key
Generate a key using the command line:
```shell
headscale --user myfirstuser preauthkeys create --reusable --expiration 24h
```
This will return a pre-authenticated key that can be used to connect a node to `headscale` during the `tailscale` command:
```shell
tailscale up --login-server <YOUR_HEADSCALE_URL> --authkey <YOUR_AUTH_KEY>
```
## Running `headscale` in the background with SystemD
:warning: **Deprecated**: This part is very outdated and you should use the [pre-packaged Headscale for this](./running-headscale-linux.md
This section demonstrates how to run `headscale` as a service in the background with [SystemD](https://www.freedesktop.org/wiki/Software/systemd/).
This should work on most modern Linux distributions.
1. Create a SystemD service configuration at `/etc/systemd/system/headscale.service` containing:
```systemd
[Unit]
Description=headscale controller
After=syslog.target
After=network.target
[Service]
Type=simple
User=headscale
Group=headscale
ExecStart=/usr/local/bin/headscale serve
Restart=always
RestartSec=5
# Optional security enhancements
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
WorkingDirectory=/var/lib/headscale
ReadWritePaths=/var/lib/headscale /var/run/headscale
AmbientCapabilities=CAP_NET_BIND_SERVICE
RuntimeDirectory=headscale
[Install]
WantedBy=multi-user.target
```
Note that when running as the headscale user ensure that, either you add your current user to the headscale group:
```shell
usermod -a -G headscale current_user
```
or run all headscale commands as the headscale user:
```shell
su - headscale
```
2. In `/etc/headscale/config.yaml`, override the default `headscale` unix socket with path that is writable by the `headscale` user or group:
```yaml
unix_socket: /var/run/headscale/headscale.sock
```
3. Reload SystemD to load the new configuration file:
```shell
systemctl daemon-reload
```
4. Enable and start the new `headscale` service:
```shell
systemctl enable --now headscale
```
5. Verify the headscale service:
```shell
systemctl status headscale
```
Verify `headscale` is available:
```shell
curl http://127.0.0.1:9090/metrics
```
`headscale` will now run in the background and start at boot.

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