mirror of
https://github.com/juanfont/headscale.git
synced 2024-11-26 08:53:05 +00:00
Compare commits
30 commits
907449bd99
...
02c76bda99
Author | SHA1 | Date | |
---|---|---|---|
|
02c76bda99 | ||
|
af969f602c | ||
|
6422cdf576 | ||
|
6253fc9e72 | ||
|
662dfbf423 | ||
|
03fd7f31b4 | ||
|
4f46d6513b | ||
|
9f6c8ab62e | ||
|
2c1ad6d11a | ||
|
fffd23602b | ||
|
3a2589f1a9 | ||
|
f6276ab9d2 | ||
|
7d9b430ec2 | ||
|
3780c9fd69 | ||
|
281025bb16 | ||
|
5e7c3153b9 | ||
|
7ba0c3d515 | ||
|
4b58dc6eb4 | ||
|
4dd12a2f97 | ||
|
2fe65624c0 | ||
|
35b669fe59 | ||
|
dc07779143 | ||
|
d72663a4d0 | ||
|
0a82d3f17a | ||
|
78214699ad | ||
|
64bb56352f | ||
|
dc17b4d378 | ||
|
a6b19e85db | ||
|
edf9e25001 | ||
|
c6336adb01 |
63 changed files with 6177 additions and 1130 deletions
2
.github/workflows/test-integration.yaml
vendored
2
.github/workflows/test-integration.yaml
vendored
|
@ -21,6 +21,7 @@ jobs:
|
||||||
- TestPolicyUpdateWhileRunningWithCLIInDatabase
|
- TestPolicyUpdateWhileRunningWithCLIInDatabase
|
||||||
- TestOIDCAuthenticationPingAll
|
- TestOIDCAuthenticationPingAll
|
||||||
- TestOIDCExpireNodesBasedOnTokenExpiry
|
- TestOIDCExpireNodesBasedOnTokenExpiry
|
||||||
|
- TestOIDC024UserCreation
|
||||||
- TestAuthWebFlowAuthenticationPingAll
|
- TestAuthWebFlowAuthenticationPingAll
|
||||||
- TestAuthWebFlowLogoutAndRelogin
|
- TestAuthWebFlowLogoutAndRelogin
|
||||||
- TestUserCommand
|
- TestUserCommand
|
||||||
|
@ -38,6 +39,7 @@ jobs:
|
||||||
- TestNodeMoveCommand
|
- TestNodeMoveCommand
|
||||||
- TestPolicyCommand
|
- TestPolicyCommand
|
||||||
- TestPolicyBrokenConfigCommand
|
- TestPolicyBrokenConfigCommand
|
||||||
|
- TestDERPVerifyEndpoint
|
||||||
- TestResolveMagicDNS
|
- TestResolveMagicDNS
|
||||||
- TestValidateResolvConf
|
- TestValidateResolvConf
|
||||||
- TestDERPServerScenario
|
- TestDERPServerScenario
|
||||||
|
|
|
@ -27,6 +27,7 @@ linters:
|
||||||
- nolintlint
|
- nolintlint
|
||||||
- musttag # causes issues with imported libs
|
- musttag # causes issues with imported libs
|
||||||
- depguard
|
- depguard
|
||||||
|
- exportloopref
|
||||||
|
|
||||||
# We should strive to enable these:
|
# We should strive to enable these:
|
||||||
- wrapcheck
|
- wrapcheck
|
||||||
|
@ -56,9 +57,14 @@ linters-settings:
|
||||||
- ok
|
- ok
|
||||||
- c
|
- c
|
||||||
- tt
|
- tt
|
||||||
|
- tx
|
||||||
|
- rx
|
||||||
|
|
||||||
gocritic:
|
gocritic:
|
||||||
disabled-checks:
|
disabled-checks:
|
||||||
- appendAssign
|
- appendAssign
|
||||||
# TODO(kradalby): Remove this
|
# TODO(kradalby): Remove this
|
||||||
- ifElseChain
|
- ifElseChain
|
||||||
|
|
||||||
|
nlreturn:
|
||||||
|
block-size: 4
|
||||||
|
|
79
CHANGELOG.md
79
CHANGELOG.md
|
@ -2,16 +2,82 @@
|
||||||
|
|
||||||
## Next
|
## Next
|
||||||
|
|
||||||
|
### Security fix: OIDC changes in Headscale 0.24.0
|
||||||
|
|
||||||
|
_Headscale v0.23.0 and earlier_ identified OIDC users by the "username" part of their email address (when `strip_email_domain: true`, the default) or whole email address (when `strip_email_domain: false`).
|
||||||
|
|
||||||
|
Depending on how Headscale and your Identity Provider (IdP) were configured, only using the `email` claim could allow a malicious user with an IdP account to take over another Headscale user's account, even when `strip_email_domain: false`.
|
||||||
|
|
||||||
|
This would also cause a user to lose access to their Headscale account if they changed their email address.
|
||||||
|
|
||||||
|
_Headscale v0.24.0_ now identifies OIDC users by the `iss` and `sub` claims. [These are guaranteed by the OIDC specification to be stable and unique](https://openid.net/specs/openid-connect-core-1_0.html#ClaimStability), even if a user changes email address. A well-designed IdP will typically set `sub` to an opaque identifier like a UUID or numeric ID, which has no relation to the user's name or email address.
|
||||||
|
|
||||||
|
This issue _only_ affects Headscale installations which authenticate with OIDC.
|
||||||
|
|
||||||
|
Headscale v0.24.0 and later will also automatically update profile fields with OIDC data on login. This means that users can change those details in your IdP, and have it populate to Headscale automatically the next time they log in. However, this may affect the way you reference users in policies.
|
||||||
|
|
||||||
|
#### Migrating existing installations
|
||||||
|
|
||||||
|
Headscale v0.23.0 and earlier never recorded the `iss` and `sub` fields, so all legacy (existing) OIDC accounts from _need to be migrated_ to be properly secured.
|
||||||
|
|
||||||
|
Headscale v0.24.0 has an automatic migration feature, which is enabled by default (`map_legacy_users: true`). **This will be disabled by default in a future version of Headscale – any unmigrated users will get new accounts.**
|
||||||
|
|
||||||
|
Headscale v0.24.0 will ignore any `email` claim if the IdP does not provide an `email_verified` claim set to `true`. [What "verified" actually means is contextually dependent](https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims) – Headscale uses it as a signal that the contents of the `email` claim is reasonably trustworthy.
|
||||||
|
|
||||||
|
Headscale v0.23.0 and earlier never checked the `email_verified` claim. This means even if an IdP explicitly indicated to Headscale that its `email` claim was untrustworthy, Headscale would have still accepted it.
|
||||||
|
|
||||||
|
##### What does automatic migration do?
|
||||||
|
|
||||||
|
When automatic migration is enabled (`map_legacy_users: true`), Headscale will first match an OIDC account to a Headscale account by `iss` and `sub`, and then fall back to matching OIDC users similarly to how Headscale v0.23.0 did:
|
||||||
|
|
||||||
|
- If `strip_email_domain: true` (the default): the Headscale username matches the "username" part of their email address.
|
||||||
|
- If `strip_email_domain: false`: the Headscale username matches the _whole_ email address.
|
||||||
|
|
||||||
|
On migration, Headscale will change the account's username to their `preferred_username`. **This could break any ACLs or policies which are configured to match by username.**
|
||||||
|
|
||||||
|
Like with Headscale v0.23.0 and earlier, this migration only works for users who haven't changed their email address since their last Headscale login.
|
||||||
|
|
||||||
|
A _successful_ automated migration should otherwise be transparent to users.
|
||||||
|
|
||||||
|
Once a Headscale account has been migrated, it will be _unavailable_ to be matched by the legacy process. An OIDC login with a matching username, but _non-matching_ `iss` and `sub` will instead get a _new_ Headscale account.
|
||||||
|
|
||||||
|
Because of the way OIDC works, Headscale's automated migration process can _only_ work when a user tries to log in after the update. Mass updates would require Headscale implement a protocol like SCIM, which is **extremely** complicated and not available in all identity providers.
|
||||||
|
|
||||||
|
Administrators could also attempt to migrate users manually by editing the database, using their own mapping rules with known-good data sources.
|
||||||
|
|
||||||
|
Legacy account migration should have no effect on new installations where all users have a recorded `sub` and `iss`.
|
||||||
|
|
||||||
|
##### What happens when automatic migration is disabled?
|
||||||
|
|
||||||
|
When automatic migration is disabled (`map_legacy_users: false`), Headscale will only try to match an OIDC account to a Headscale account by `iss` and `sub`.
|
||||||
|
|
||||||
|
If there is no match, it will get a _new_ Headscale account – even if there was a legacy account which _could_ have matched and migrated.
|
||||||
|
|
||||||
|
We recommend new Headscale users explicitly disable automatic migration – but it should otherwise have no effect if every account has a recorded `iss` and `sub`.
|
||||||
|
|
||||||
|
When automatic migration is disabled, the `strip_email_domain` setting will have no effect.
|
||||||
|
|
||||||
|
Special thanks to @micolous for reviewing, proposing and working with us on these changes.
|
||||||
|
|
||||||
|
#### Other OIDC changes
|
||||||
|
|
||||||
|
Headscale now uses [the standard OIDC claims](https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims) to populate and update user information every time they log in:
|
||||||
|
|
||||||
|
| Headscale profile field | OIDC claim | Notes / examples |
|
||||||
|
| ----------------------- | -------------------- | --------------------------------------------------------------------------------------------------------- |
|
||||||
|
| email address | `email` | Only used when `"email_verified": true` |
|
||||||
|
| display name | `name` | eg: `Sam Smith` |
|
||||||
|
| username | `preferred_username` | Varies depending on IdP and configuration, eg: `ssmith`, `ssmith@idp.example.com`, `\\example.com\ssmith` |
|
||||||
|
| profile picture | `picture` | URL to a profile picture or avatar |
|
||||||
|
|
||||||
|
These should show up nicely in the Tailscale client.
|
||||||
|
|
||||||
|
This will also affect the way you [reference users in policies](https://github.com/juanfont/headscale/pull/2205).
|
||||||
|
|
||||||
### BREAKING
|
### BREAKING
|
||||||
|
|
||||||
- Remove `dns.use_username_in_magic_dns` configuration option [#2020](https://github.com/juanfont/headscale/pull/2020)
|
- Remove `dns.use_username_in_magic_dns` configuration option [#2020](https://github.com/juanfont/headscale/pull/2020)
|
||||||
- Having usernames in magic DNS is no longer possible.
|
- Having usernames in magic DNS is no longer possible.
|
||||||
- Redo OpenID Connect configuration [#2020](https://github.com/juanfont/headscale/pull/2020)
|
|
||||||
- `strip_email_domain` has been removed, domain is _always_ part of the username for OIDC.
|
|
||||||
- Users are now identified by `sub` claim in the ID token instead of username, allowing the username, name and email to be updated.
|
|
||||||
- User has been extended to store username, display name, profile picture url and email.
|
|
||||||
- These fields are forwarded to the client, and shows up nicely in the user switcher.
|
|
||||||
- These fields can be made available via the API/CLI for non-OIDC users in the future.
|
|
||||||
- Remove versions older than 1.56 [#2149](https://github.com/juanfont/headscale/pull/2149)
|
- Remove versions older than 1.56 [#2149](https://github.com/juanfont/headscale/pull/2149)
|
||||||
- Clean up old code required by old versions
|
- Clean up old code required by old versions
|
||||||
|
|
||||||
|
@ -23,6 +89,7 @@
|
||||||
- Added conversion of 'Hostname' to 'givenName' in a node with FQDN rules applied [#2198](https://github.com/juanfont/headscale/pull/2198)
|
- Added conversion of 'Hostname' to 'givenName' in a node with FQDN rules applied [#2198](https://github.com/juanfont/headscale/pull/2198)
|
||||||
- Fixed updating of hostname and givenName when it is updated in HostInfo [#2199](https://github.com/juanfont/headscale/pull/2199)
|
- Fixed updating of hostname and givenName when it is updated in HostInfo [#2199](https://github.com/juanfont/headscale/pull/2199)
|
||||||
- Fixed missing `stable-debug` container tag [#2232](https://github.com/juanfont/headscale/pr/2232)
|
- Fixed missing `stable-debug` container tag [#2232](https://github.com/juanfont/headscale/pr/2232)
|
||||||
|
- Loosened up `server_url` and `base_domain` check. It was overly strict in some cases.
|
||||||
|
|
||||||
## 0.23.0 (2024-09-18)
|
## 0.23.0 (2024-09-18)
|
||||||
|
|
||||||
|
|
19
Dockerfile.derper
Normal file
19
Dockerfile.derper
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# For testing purposes only
|
||||||
|
|
||||||
|
FROM golang:alpine AS build-env
|
||||||
|
|
||||||
|
WORKDIR /go/src
|
||||||
|
|
||||||
|
RUN apk add --no-cache git
|
||||||
|
ARG VERSION_BRANCH=main
|
||||||
|
RUN git clone https://github.com/tailscale/tailscale.git --branch=$VERSION_BRANCH --depth=1
|
||||||
|
WORKDIR /go/src/tailscale
|
||||||
|
|
||||||
|
ARG TARGETARCH
|
||||||
|
RUN GOARCH=$TARGETARCH go install -v ./cmd/derper
|
||||||
|
|
||||||
|
FROM alpine:3.18
|
||||||
|
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables curl
|
||||||
|
|
||||||
|
COPY --from=build-env /go/bin/* /usr/local/bin/
|
||||||
|
ENTRYPOINT [ "/usr/local/bin/derper" ]
|
|
@ -1,8 +1,10 @@
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
@ -64,6 +66,19 @@ func mockOIDC() error {
|
||||||
accessTTL = newTTL
|
accessTTL = newTTL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userStr := os.Getenv("MOCKOIDC_USERS")
|
||||||
|
if userStr == "" {
|
||||||
|
return fmt.Errorf("MOCKOIDC_USERS not defined")
|
||||||
|
}
|
||||||
|
|
||||||
|
var users []mockoidc.MockUser
|
||||||
|
err := json.Unmarshal([]byte(userStr), &users)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unmarshalling users: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Interface("users", users).Msg("loading users from JSON")
|
||||||
|
|
||||||
log.Info().Msgf("Access token TTL: %s", accessTTL)
|
log.Info().Msgf("Access token TTL: %s", accessTTL)
|
||||||
|
|
||||||
port, err := strconv.Atoi(portStr)
|
port, err := strconv.Atoi(portStr)
|
||||||
|
@ -71,7 +86,7 @@ func mockOIDC() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
mock, err := getMockOIDC(clientID, clientSecret)
|
mock, err := getMockOIDC(clientID, clientSecret, users)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -93,12 +108,18 @@ func mockOIDC() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMockOIDC(clientID string, clientSecret string) (*mockoidc.MockOIDC, error) {
|
func getMockOIDC(clientID string, clientSecret string, users []mockoidc.MockUser) (*mockoidc.MockOIDC, error) {
|
||||||
keypair, err := mockoidc.NewKeypair(nil)
|
keypair, err := mockoidc.NewKeypair(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userQueue := mockoidc.UserQueue{}
|
||||||
|
|
||||||
|
for _, user := range users {
|
||||||
|
userQueue.Push(&user)
|
||||||
|
}
|
||||||
|
|
||||||
mock := mockoidc.MockOIDC{
|
mock := mockoidc.MockOIDC{
|
||||||
ClientID: clientID,
|
ClientID: clientID,
|
||||||
ClientSecret: clientSecret,
|
ClientSecret: clientSecret,
|
||||||
|
@ -107,9 +128,19 @@ func getMockOIDC(clientID string, clientSecret string) (*mockoidc.MockOIDC, erro
|
||||||
CodeChallengeMethodsSupported: []string{"plain", "S256"},
|
CodeChallengeMethodsSupported: []string{"plain", "S256"},
|
||||||
Keypair: keypair,
|
Keypair: keypair,
|
||||||
SessionStore: mockoidc.NewSessionStore(),
|
SessionStore: mockoidc.NewSessionStore(),
|
||||||
UserQueue: &mockoidc.UserQueue{},
|
UserQueue: &userQueue,
|
||||||
ErrorQueue: &mockoidc.ErrorQueue{},
|
ErrorQueue: &mockoidc.ErrorQueue{},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mock.AddMiddleware(func(h http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Info().Msgf("Request: %+v", r)
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
if r.Response != nil {
|
||||||
|
log.Info().Msgf("Response: %+v", r.Response)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
return &mock, nil
|
return &mock, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -168,6 +168,11 @@ database:
|
||||||
# https://www.sqlite.org/wal.html
|
# https://www.sqlite.org/wal.html
|
||||||
write_ahead_log: true
|
write_ahead_log: true
|
||||||
|
|
||||||
|
# Maximum number of WAL file frames before the WAL file is automatically checkpointed.
|
||||||
|
# https://www.sqlite.org/c3ref/wal_autocheckpoint.html
|
||||||
|
# Set to 0 to disable automatic checkpointing.
|
||||||
|
wal_autocheckpoint: 1000
|
||||||
|
|
||||||
# # Postgres config
|
# # Postgres config
|
||||||
# Please note that using Postgres is highly discouraged as it is only supported for legacy reasons.
|
# Please note that using Postgres is highly discouraged as it is only supported for legacy reasons.
|
||||||
# See database.type for more information.
|
# See database.type for more information.
|
||||||
|
@ -364,12 +369,18 @@ unix_socket_permission: "0770"
|
||||||
# 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.
|
# # Map legacy users from pre-0.24.0 versions of headscale to the new OIDC users
|
||||||
# # This will transform `first-name.last-name@example.com` to the user `first-name.last-name`
|
# # by taking the username from the legacy user and matching it with the username
|
||||||
# # If `strip_email_domain` is set to `false` the domain part will NOT be removed resulting to the following
|
# # provided by the OIDC. This is useful when migrating from legacy users to OIDC
|
||||||
# user: `first-name.last-name.example.com`
|
# # to force them using the unique identifier from the OIDC and to give them a
|
||||||
#
|
# # proper display name and picture if available.
|
||||||
# strip_email_domain: true
|
# # Note that this will only work if the username from the legacy user is the same
|
||||||
|
# # and ther is a posibility for account takeover should a username have changed
|
||||||
|
# # with the provider.
|
||||||
|
# # Disabling this feature will cause all new logins to be created as new users.
|
||||||
|
# # Note this option will be removed in the future and should be set to false
|
||||||
|
# # on all new installations, or when all users have logged in with OIDC once.
|
||||||
|
# map_legacy_users: true
|
||||||
|
|
||||||
# Logtail configuration
|
# Logtail configuration
|
||||||
# Logtail is Tailscales logging and auditing infrastructure, it allows the control panel
|
# Logtail is Tailscales logging and auditing infrastructure, it allows the control panel
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
cairosvg~=2.7.1
|
mkdocs-include-markdown-plugin~=7.1
|
||||||
mkdocs-include-markdown-plugin~=6.2.2
|
mkdocs-macros-plugin~=1.3
|
||||||
mkdocs-macros-plugin~=1.2.0
|
mkdocs-material[imaging]~=9.5
|
||||||
mkdocs-material~=9.5.18
|
mkdocs-minify-plugin~=0.7
|
||||||
mkdocs-minify-plugin~=0.7.1
|
mkdocs-redirects~=1.2
|
||||||
mkdocs-redirects~=1.2.1
|
|
||||||
pillow~=10.1.0
|
|
||||||
|
|
|
@ -20,11 +20,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1731763621,
|
"lastModified": 1731890469,
|
||||||
"narHash": "sha256-ddcX4lQL0X05AYkrkV2LMFgGdRvgap7Ho8kgon3iWZk=",
|
"narHash": "sha256-D1FNZ70NmQEwNxpSSdTXCSklBH1z2isPR84J6DQrJGs=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "c69a9bffbecde46b4b939465422ddc59493d3e4d",
|
"rev": "5083ec887760adfe12af64830a66807423a859a7",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
|
|
||||||
# When updating go.mod or go.sum, a new sha will need to be calculated,
|
# When updating go.mod or go.sum, a new sha will need to be calculated,
|
||||||
# update this if you have a mismatch after doing a change to thos files.
|
# update this if you have a mismatch after doing a change to thos files.
|
||||||
vendorHash = "sha256-CMkYTRjmhvTTrB7JbLj0cj9VEyzpG0iUWXkaOagwYTk=";
|
vendorHash = "sha256-4VNiHUblvtcl9UetwiL6ZeVYb0h2e9zhYVsirhAkvOg=";
|
||||||
|
|
||||||
subPackages = ["cmd/headscale"];
|
subPackages = ["cmd/headscale"];
|
||||||
|
|
||||||
|
@ -102,6 +102,7 @@
|
||||||
ko
|
ko
|
||||||
yq-go
|
yq-go
|
||||||
ripgrep
|
ripgrep
|
||||||
|
postgresql
|
||||||
|
|
||||||
# 'dot' is needed for pprof graphs
|
# 'dot' is needed for pprof graphs
|
||||||
# go tool pprof -http=: <source>
|
# go tool pprof -http=: <source>
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -49,6 +49,7 @@ require (
|
||||||
gorm.io/gorm v1.25.11
|
gorm.io/gorm v1.25.11
|
||||||
tailscale.com v1.75.0-pre.0.20240926101731-7d1160ddaab7
|
tailscale.com v1.75.0-pre.0.20240926101731-7d1160ddaab7
|
||||||
zgo.at/zcache/v2 v2.1.0
|
zgo.at/zcache/v2 v2.1.0
|
||||||
|
zombiezen.com/go/postgrestest v1.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
@ -134,6 +135,7 @@ require (
|
||||||
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect
|
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect
|
||||||
github.com/kr/pretty v0.3.1 // indirect
|
github.com/kr/pretty v0.3.1 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
|
github.com/lib/pq v1.10.9 // indirect
|
||||||
github.com/lithammer/fuzzysearch v1.1.8 // indirect
|
github.com/lithammer/fuzzysearch v1.1.8 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
|
3
go.sum
3
go.sum
|
@ -311,6 +311,7 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
||||||
|
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||||
|
@ -731,3 +732,5 @@ tailscale.com v1.75.0-pre.0.20240926101731-7d1160ddaab7 h1:nfRWV6ECxwNvvXKtbqSVs
|
||||||
tailscale.com v1.75.0-pre.0.20240926101731-7d1160ddaab7/go.mod h1:xKxYf3B3PuezFlRaMT+VhuVu8XTFUTLy+VCzLPMJVmg=
|
tailscale.com v1.75.0-pre.0.20240926101731-7d1160ddaab7/go.mod h1:xKxYf3B3PuezFlRaMT+VhuVu8XTFUTLy+VCzLPMJVmg=
|
||||||
zgo.at/zcache/v2 v2.1.0 h1:USo+ubK+R4vtjw4viGzTe/zjXyPw6R7SK/RL3epBBxs=
|
zgo.at/zcache/v2 v2.1.0 h1:USo+ubK+R4vtjw4viGzTe/zjXyPw6R7SK/RL3epBBxs=
|
||||||
zgo.at/zcache/v2 v2.1.0/go.mod h1:gyCeoLVo01QjDZynjime8xUGHHMbsLiPyUTBpDGd4Gk=
|
zgo.at/zcache/v2 v2.1.0/go.mod h1:gyCeoLVo01QjDZynjime8xUGHHMbsLiPyUTBpDGd4Gk=
|
||||||
|
zombiezen.com/go/postgrestest v1.0.1 h1:aXoADQAJmZDU3+xilYVut0pHhgc0sF8ZspPW9gFNwP4=
|
||||||
|
zombiezen.com/go/postgrestest v1.0.1/go.mod h1:marlZezr+k2oSJrvXHnZUs1olHqpE9czlz8ZYkVxliQ=
|
||||||
|
|
|
@ -30,6 +30,7 @@ import (
|
||||||
"github.com/juanfont/headscale/hscontrol/mapper"
|
"github.com/juanfont/headscale/hscontrol/mapper"
|
||||||
"github.com/juanfont/headscale/hscontrol/notifier"
|
"github.com/juanfont/headscale/hscontrol/notifier"
|
||||||
"github.com/juanfont/headscale/hscontrol/policy"
|
"github.com/juanfont/headscale/hscontrol/policy"
|
||||||
|
"github.com/juanfont/headscale/hscontrol/policyv2"
|
||||||
"github.com/juanfont/headscale/hscontrol/types"
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
"github.com/juanfont/headscale/hscontrol/util"
|
"github.com/juanfont/headscale/hscontrol/util"
|
||||||
zerolog "github.com/philip-bui/grpc-zerolog"
|
zerolog "github.com/philip-bui/grpc-zerolog"
|
||||||
|
@ -88,7 +89,8 @@ type Headscale struct {
|
||||||
DERPMap *tailcfg.DERPMap
|
DERPMap *tailcfg.DERPMap
|
||||||
DERPServer *derpServer.DERPServer
|
DERPServer *derpServer.DERPServer
|
||||||
|
|
||||||
ACLPolicy *policy.ACLPolicy
|
ACLPolicy *policy.ACLPolicy
|
||||||
|
PolicyManager *policyv2.PolicyManager
|
||||||
|
|
||||||
mapper *mapper.Mapper
|
mapper *mapper.Mapper
|
||||||
nodeNotifier *notifier.Notifier
|
nodeNotifier *notifier.Notifier
|
||||||
|
@ -457,6 +459,8 @@ func (h *Headscale) createRouter(grpcMux *grpcRuntime.ServeMux) *mux.Router {
|
||||||
router.HandleFunc("/swagger/v1/openapiv2.json", headscale.SwaggerAPIv1).
|
router.HandleFunc("/swagger/v1/openapiv2.json", headscale.SwaggerAPIv1).
|
||||||
Methods(http.MethodGet)
|
Methods(http.MethodGet)
|
||||||
|
|
||||||
|
router.HandleFunc("/verify", h.VerifyHandler).Methods(http.MethodPost)
|
||||||
|
|
||||||
if h.cfg.DERP.ServerEnabled {
|
if h.cfg.DERP.ServerEnabled {
|
||||||
router.HandleFunc("/derp", h.DERPServer.DERPHandler)
|
router.HandleFunc("/derp", h.DERPServer.DERPHandler)
|
||||||
router.HandleFunc("/derp/probe", derpServer.DERPProbeHandler)
|
router.HandleFunc("/derp/probe", derpServer.DERPProbeHandler)
|
||||||
|
@ -1027,14 +1031,18 @@ func (h *Headscale) loadACLPolicy() error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("loading nodes from database to validate policy: %w", err)
|
return fmt.Errorf("loading nodes from database to validate policy: %w", err)
|
||||||
}
|
}
|
||||||
|
users, err := h.db.ListUsers()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("loading users from database to validate policy: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
_, err = pol.CompileFilterRules(nodes)
|
_, err = pol.CompileFilterRules(users, nodes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("verifying policy rules: %w", err)
|
return fmt.Errorf("verifying policy rules: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(nodes) > 0 {
|
if len(nodes) > 0 {
|
||||||
_, err = pol.CompileSSHPolicy(nodes[0], nodes)
|
_, err = pol.CompileSSHPolicy(nodes[0], users, nodes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("verifying SSH rules: %w", err)
|
return fmt.Errorf("verifying SSH rules: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -474,6 +474,8 @@ func NewHeadscaleDatabase(
|
||||||
Rollback: func(db *gorm.DB) error { return nil },
|
Rollback: func(db *gorm.DB) error { return nil },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
// Pick up new user fields used for OIDC and to
|
||||||
|
// populate the user with more interesting information.
|
||||||
ID: "202407191627",
|
ID: "202407191627",
|
||||||
Migrate: func(tx *gorm.DB) error {
|
Migrate: func(tx *gorm.DB) error {
|
||||||
err := tx.AutoMigrate(&types.User{})
|
err := tx.AutoMigrate(&types.User{})
|
||||||
|
@ -485,6 +487,40 @@ func NewHeadscaleDatabase(
|
||||||
},
|
},
|
||||||
Rollback: func(db *gorm.DB) error { return nil },
|
Rollback: func(db *gorm.DB) error { return nil },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// The unique constraint of Name has been dropped
|
||||||
|
// in favour of a unique together of name and
|
||||||
|
// provider identity.
|
||||||
|
ID: "202408181235",
|
||||||
|
Migrate: func(tx *gorm.DB) error {
|
||||||
|
err := tx.AutoMigrate(&types.User{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up indexes and unique constraints outside of GORM, it does not support
|
||||||
|
// conditional unique constraints.
|
||||||
|
// This ensures the following:
|
||||||
|
// - A user name and provider_identifier is unique
|
||||||
|
// - A provider_identifier is unique
|
||||||
|
// - A user name is unique if there is no provider_identifier is not set
|
||||||
|
for _, idx := range []string{
|
||||||
|
"DROP INDEX IF EXISTS idx_provider_identifier",
|
||||||
|
"DROP INDEX IF EXISTS idx_name_provider_identifier",
|
||||||
|
"CREATE UNIQUE INDEX IF NOT EXISTS idx_provider_identifier ON users (provider_identifier) WHERE provider_identifier IS NOT NULL;",
|
||||||
|
"CREATE UNIQUE INDEX IF NOT EXISTS idx_name_provider_identifier ON users (name,provider_identifier);",
|
||||||
|
"CREATE UNIQUE INDEX IF NOT EXISTS idx_name_no_provider_identifier ON users (name) WHERE provider_identifier IS NULL;",
|
||||||
|
} {
|
||||||
|
err = tx.Exec(idx).Error
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating username index: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Rollback: func(db *gorm.DB) error { return nil },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -543,10 +579,10 @@ func openDB(cfg types.DatabaseConfig) (*gorm.DB, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Sqlite.WriteAheadLog {
|
if cfg.Sqlite.WriteAheadLog {
|
||||||
if err := db.Exec(`
|
if err := db.Exec(fmt.Sprintf(`
|
||||||
PRAGMA journal_mode=WAL;
|
PRAGMA journal_mode=WAL;
|
||||||
PRAGMA wal_autocheckpoint=0;
|
PRAGMA wal_autocheckpoint=%d;
|
||||||
`).Error; err != nil {
|
`, cfg.Sqlite.WALAutoCheckPoint)).Error; err != nil {
|
||||||
return nil, fmt.Errorf("setting WAL mode: %w", err)
|
return nil, fmt.Errorf("setting WAL mode: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
@ -8,6 +9,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -16,6 +18,7 @@ import (
|
||||||
"github.com/juanfont/headscale/hscontrol/types"
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
"github.com/juanfont/headscale/hscontrol/util"
|
"github.com/juanfont/headscale/hscontrol/util"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"zgo.at/zcache/v2"
|
"zgo.at/zcache/v2"
|
||||||
)
|
)
|
||||||
|
@ -44,7 +47,7 @@ func TestMigrations(t *testing.T) {
|
||||||
routes, err := Read(h.DB, func(rx *gorm.DB) (types.Routes, error) {
|
routes, err := Read(h.DB, func(rx *gorm.DB) (types.Routes, error) {
|
||||||
return GetRoutes(rx)
|
return GetRoutes(rx)
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, routes, 10)
|
assert.Len(t, routes, 10)
|
||||||
want := types.Routes{
|
want := types.Routes{
|
||||||
|
@ -70,7 +73,7 @@ func TestMigrations(t *testing.T) {
|
||||||
routes, err := Read(h.DB, func(rx *gorm.DB) (types.Routes, error) {
|
routes, err := Read(h.DB, func(rx *gorm.DB) (types.Routes, error) {
|
||||||
return GetRoutes(rx)
|
return GetRoutes(rx)
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, routes, 4)
|
assert.Len(t, routes, 4)
|
||||||
want := types.Routes{
|
want := types.Routes{
|
||||||
|
@ -120,19 +123,19 @@ func TestMigrations(t *testing.T) {
|
||||||
dbPath: "testdata/0-23-0-to-0-24-0-preauthkey-tags-table.sqlite",
|
dbPath: "testdata/0-23-0-to-0-24-0-preauthkey-tags-table.sqlite",
|
||||||
wantFunc: func(t *testing.T, h *HSDatabase) {
|
wantFunc: func(t *testing.T, h *HSDatabase) {
|
||||||
keys, err := Read(h.DB, func(rx *gorm.DB) ([]types.PreAuthKey, error) {
|
keys, err := Read(h.DB, func(rx *gorm.DB) ([]types.PreAuthKey, error) {
|
||||||
kratest, err := ListPreAuthKeys(rx, "kratest")
|
kratest, err := ListPreAuthKeysByUser(rx, 1) // kratest
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
testkra, err := ListPreAuthKeys(rx, "testkra")
|
testkra, err := ListPreAuthKeysByUser(rx, 2) // testkra
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return append(kratest, testkra...), nil
|
return append(kratest, testkra...), nil
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, keys, 5)
|
assert.Len(t, keys, 5)
|
||||||
want := []types.PreAuthKey{
|
want := []types.PreAuthKey{
|
||||||
|
@ -177,7 +180,7 @@ func TestMigrations(t *testing.T) {
|
||||||
nodes, err := Read(h.DB, func(rx *gorm.DB) (types.Nodes, error) {
|
nodes, err := Read(h.DB, func(rx *gorm.DB) (types.Nodes, error) {
|
||||||
return ListNodes(rx)
|
return ListNodes(rx)
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
assert.Falsef(t, node.MachineKey.IsZero(), "expected non zero machinekey")
|
assert.Falsef(t, node.MachineKey.IsZero(), "expected non zero machinekey")
|
||||||
|
@ -256,3 +259,120 @@ func testCopyOfDatabase(src string) (string, error) {
|
||||||
func emptyCache() *zcache.Cache[string, types.Node] {
|
func emptyCache() *zcache.Cache[string, types.Node] {
|
||||||
return zcache.New[string, types.Node](time.Minute, time.Hour)
|
return zcache.New[string, types.Node](time.Minute, time.Hour)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// requireConstraintFailed checks if the error is a constraint failure with
|
||||||
|
// either SQLite and PostgreSQL error messages.
|
||||||
|
func requireConstraintFailed(t *testing.T, err error) {
|
||||||
|
t.Helper()
|
||||||
|
require.Error(t, err)
|
||||||
|
if !strings.Contains(err.Error(), "UNIQUE constraint failed:") && !strings.Contains(err.Error(), "violates unique constraint") {
|
||||||
|
require.Failf(t, "expected error to contain a constraint failure, got: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConstraints(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
run func(*testing.T, *gorm.DB)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no-duplicate-username-if-no-oidc",
|
||||||
|
run: func(t *testing.T, db *gorm.DB) {
|
||||||
|
_, err := CreateUser(db, "user1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = CreateUser(db, "user1")
|
||||||
|
requireConstraintFailed(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no-oidc-duplicate-username-and-id",
|
||||||
|
run: func(t *testing.T, db *gorm.DB) {
|
||||||
|
user := types.User{
|
||||||
|
Model: gorm.Model{ID: 1},
|
||||||
|
Name: "user1",
|
||||||
|
}
|
||||||
|
user.ProviderIdentifier = sql.NullString{String: "http://test.com/user1", Valid: true}
|
||||||
|
|
||||||
|
err := db.Save(&user).Error
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
user = types.User{
|
||||||
|
Model: gorm.Model{ID: 2},
|
||||||
|
Name: "user1",
|
||||||
|
}
|
||||||
|
user.ProviderIdentifier = sql.NullString{String: "http://test.com/user1", Valid: true}
|
||||||
|
|
||||||
|
err = db.Save(&user).Error
|
||||||
|
requireConstraintFailed(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no-oidc-duplicate-id",
|
||||||
|
run: func(t *testing.T, db *gorm.DB) {
|
||||||
|
user := types.User{
|
||||||
|
Model: gorm.Model{ID: 1},
|
||||||
|
Name: "user1",
|
||||||
|
}
|
||||||
|
user.ProviderIdentifier = sql.NullString{String: "http://test.com/user1", Valid: true}
|
||||||
|
|
||||||
|
err := db.Save(&user).Error
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
user = types.User{
|
||||||
|
Model: gorm.Model{ID: 2},
|
||||||
|
Name: "user1.1",
|
||||||
|
}
|
||||||
|
user.ProviderIdentifier = sql.NullString{String: "http://test.com/user1", Valid: true}
|
||||||
|
|
||||||
|
err = db.Save(&user).Error
|
||||||
|
requireConstraintFailed(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "allow-duplicate-username-cli-then-oidc",
|
||||||
|
run: func(t *testing.T, db *gorm.DB) {
|
||||||
|
_, err := CreateUser(db, "user1") // Create CLI username
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
user := types.User{
|
||||||
|
Name: "user1",
|
||||||
|
ProviderIdentifier: sql.NullString{String: "http://test.com/user1", Valid: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Save(&user).Error
|
||||||
|
require.NoError(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "allow-duplicate-username-oidc-then-cli",
|
||||||
|
run: func(t *testing.T, db *gorm.DB) {
|
||||||
|
user := types.User{
|
||||||
|
Name: "user1",
|
||||||
|
ProviderIdentifier: sql.NullString{String: "http://test.com/user1", Valid: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := db.Save(&user).Error
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = CreateUser(db, "user1") // Create CLI username
|
||||||
|
require.NoError(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name+"-postgres", func(t *testing.T) {
|
||||||
|
db := newPostgresTestDB(t)
|
||||||
|
tt.run(t, db.DB.Debug())
|
||||||
|
})
|
||||||
|
t.Run(tt.name+"-sqlite", func(t *testing.T) {
|
||||||
|
db, err := newSQLiteTestDB()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("creating database: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tt.run(t, db.DB.Debug())
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"github.com/juanfont/headscale/hscontrol/types"
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
"github.com/juanfont/headscale/hscontrol/util"
|
"github.com/juanfont/headscale/hscontrol/util"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"tailscale.com/net/tsaddr"
|
"tailscale.com/net/tsaddr"
|
||||||
"tailscale.com/types/ptr"
|
"tailscale.com/types/ptr"
|
||||||
)
|
)
|
||||||
|
@ -457,7 +458,12 @@ func TestBackfillIPAddresses(t *testing.T) {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
db := tt.dbFunc()
|
db := tt.dbFunc()
|
||||||
|
|
||||||
alloc, err := NewIPAllocator(db, tt.prefix4, tt.prefix6, types.IPAllocationStrategySequential)
|
alloc, err := NewIPAllocator(
|
||||||
|
db,
|
||||||
|
tt.prefix4,
|
||||||
|
tt.prefix6,
|
||||||
|
types.IPAllocationStrategySequential,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to set up ip alloc: %s", err)
|
t.Fatalf("failed to set up ip alloc: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -482,24 +488,29 @@ func TestBackfillIPAddresses(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIPAllocatorNextNoReservedIPs(t *testing.T) {
|
func TestIPAllocatorNextNoReservedIPs(t *testing.T) {
|
||||||
alloc, err := NewIPAllocator(db, ptr.To(tsaddr.CGNATRange()), ptr.To(tsaddr.TailscaleULARange()), types.IPAllocationStrategySequential)
|
alloc, err := NewIPAllocator(
|
||||||
|
db,
|
||||||
|
ptr.To(tsaddr.CGNATRange()),
|
||||||
|
ptr.To(tsaddr.TailscaleULARange()),
|
||||||
|
types.IPAllocationStrategySequential,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to set up ip alloc: %s", err)
|
t.Fatalf("failed to set up ip alloc: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate that we do not give out 100.100.100.100
|
// Validate that we do not give out 100.100.100.100
|
||||||
nextQuad100, err := alloc.next(na("100.100.100.99"), ptr.To(tsaddr.CGNATRange()))
|
nextQuad100, err := alloc.next(na("100.100.100.99"), ptr.To(tsaddr.CGNATRange()))
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, na("100.100.100.101"), *nextQuad100)
|
assert.Equal(t, na("100.100.100.101"), *nextQuad100)
|
||||||
|
|
||||||
// Validate that we do not give out fd7a:115c:a1e0::53
|
// Validate that we do not give out fd7a:115c:a1e0::53
|
||||||
nextQuad100v6, err := alloc.next(na("fd7a:115c:a1e0::52"), ptr.To(tsaddr.TailscaleULARange()))
|
nextQuad100v6, err := alloc.next(na("fd7a:115c:a1e0::52"), ptr.To(tsaddr.TailscaleULARange()))
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, na("fd7a:115c:a1e0::54"), *nextQuad100v6)
|
assert.Equal(t, na("fd7a:115c:a1e0::54"), *nextQuad100v6)
|
||||||
|
|
||||||
// Validate that we do not give out fd7a:115c:a1e0::53
|
// Validate that we do not give out fd7a:115c:a1e0::53
|
||||||
nextChrome, err := alloc.next(na("100.115.91.255"), ptr.To(tsaddr.CGNATRange()))
|
nextChrome, err := alloc.next(na("100.115.91.255"), ptr.To(tsaddr.CGNATRange()))
|
||||||
t.Logf("chrome: %s", nextChrome.String())
|
t.Logf("chrome: %s", nextChrome.String())
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, na("100.115.94.0"), *nextChrome)
|
assert.Equal(t, na("100.115.94.0"), *nextChrome)
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,15 +91,15 @@ func (hsdb *HSDatabase) ListEphemeralNodes() (types.Nodes, error) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hsdb *HSDatabase) getNode(user string, name string) (*types.Node, error) {
|
func (hsdb *HSDatabase) getNode(uid types.UserID, name string) (*types.Node, error) {
|
||||||
return Read(hsdb.DB, func(rx *gorm.DB) (*types.Node, error) {
|
return Read(hsdb.DB, func(rx *gorm.DB) (*types.Node, error) {
|
||||||
return getNode(rx, user, name)
|
return getNode(rx, uid, name)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// getNode finds a Node by name and user and returns the Node struct.
|
// getNode finds a Node by name and user and returns the Node struct.
|
||||||
func getNode(tx *gorm.DB, user string, name string) (*types.Node, error) {
|
func getNode(tx *gorm.DB, uid types.UserID, name string) (*types.Node, error) {
|
||||||
nodes, err := ListNodesByUser(tx, user)
|
nodes, err := ListNodesByUser(tx, uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import (
|
||||||
"github.com/juanfont/headscale/hscontrol/util"
|
"github.com/juanfont/headscale/hscontrol/util"
|
||||||
"github.com/puzpuzpuz/xsync/v3"
|
"github.com/puzpuzpuz/xsync/v3"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"gopkg.in/check.v1"
|
"gopkg.in/check.v1"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"tailscale.com/net/tsaddr"
|
"tailscale.com/net/tsaddr"
|
||||||
|
@ -29,10 +30,10 @@ func (s *Suite) TestGetNode(c *check.C) {
|
||||||
user, err := db.CreateUser("test")
|
user, err := db.CreateUser("test")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil)
|
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
_, err = db.getNode("test", "testnode")
|
_, err = db.getNode(types.UserID(user.ID), "testnode")
|
||||||
c.Assert(err, check.NotNil)
|
c.Assert(err, check.NotNil)
|
||||||
|
|
||||||
nodeKey := key.NewNode()
|
nodeKey := key.NewNode()
|
||||||
|
@ -50,7 +51,7 @@ func (s *Suite) TestGetNode(c *check.C) {
|
||||||
trx := db.DB.Save(node)
|
trx := db.DB.Save(node)
|
||||||
c.Assert(trx.Error, check.IsNil)
|
c.Assert(trx.Error, check.IsNil)
|
||||||
|
|
||||||
_, err = db.getNode("test", "testnode")
|
_, err = db.getNode(types.UserID(user.ID), "testnode")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,7 +59,7 @@ func (s *Suite) TestGetNodeByID(c *check.C) {
|
||||||
user, err := db.CreateUser("test")
|
user, err := db.CreateUser("test")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil)
|
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
_, err = db.GetNodeByID(0)
|
_, err = db.GetNodeByID(0)
|
||||||
|
@ -87,7 +88,7 @@ func (s *Suite) TestGetNodeByAnyNodeKey(c *check.C) {
|
||||||
user, err := db.CreateUser("test")
|
user, err := db.CreateUser("test")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil)
|
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
_, err = db.GetNodeByID(0)
|
_, err = db.GetNodeByID(0)
|
||||||
|
@ -135,7 +136,7 @@ func (s *Suite) TestHardDeleteNode(c *check.C) {
|
||||||
_, err = db.DeleteNode(&node, xsync.NewMapOf[types.NodeID, bool]())
|
_, err = db.DeleteNode(&node, xsync.NewMapOf[types.NodeID, bool]())
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
_, err = db.getNode(user.Name, "testnode3")
|
_, err = db.getNode(types.UserID(user.ID), "testnode3")
|
||||||
c.Assert(err, check.NotNil)
|
c.Assert(err, check.NotNil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,7 +144,7 @@ func (s *Suite) TestListPeers(c *check.C) {
|
||||||
user, err := db.CreateUser("test")
|
user, err := db.CreateUser("test")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil)
|
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
_, err = db.GetNodeByID(0)
|
_, err = db.GetNodeByID(0)
|
||||||
|
@ -189,7 +190,7 @@ func (s *Suite) TestGetACLFilteredPeers(c *check.C) {
|
||||||
for _, name := range []string{"test", "admin"} {
|
for _, name := range []string{"test", "admin"} {
|
||||||
user, err := db.CreateUser(name)
|
user, err := db.CreateUser(name)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil)
|
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
stor = append(stor, base{user, pak})
|
stor = append(stor, base{user, pak})
|
||||||
}
|
}
|
||||||
|
@ -255,10 +256,10 @@ func (s *Suite) TestGetACLFilteredPeers(c *check.C) {
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
c.Assert(len(testPeers), check.Equals, 9)
|
c.Assert(len(testPeers), check.Equals, 9)
|
||||||
|
|
||||||
adminRules, _, err := policy.GenerateFilterAndSSHRulesForTests(aclPolicy, adminNode, adminPeers)
|
adminRules, _, err := policy.GenerateFilterAndSSHRulesForTests(aclPolicy, adminNode, adminPeers, []types.User{*stor[0].user, *stor[1].user})
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
testRules, _, err := policy.GenerateFilterAndSSHRulesForTests(aclPolicy, testNode, testPeers)
|
testRules, _, err := policy.GenerateFilterAndSSHRulesForTests(aclPolicy, testNode, testPeers, []types.User{*stor[0].user, *stor[1].user})
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
peersOfAdminNode := policy.FilterNodesByACL(adminNode, adminPeers, adminRules)
|
peersOfAdminNode := policy.FilterNodesByACL(adminNode, adminPeers, adminRules)
|
||||||
|
@ -281,10 +282,10 @@ func (s *Suite) TestExpireNode(c *check.C) {
|
||||||
user, err := db.CreateUser("test")
|
user, err := db.CreateUser("test")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil)
|
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
_, err = db.getNode("test", "testnode")
|
_, err = db.getNode(types.UserID(user.ID), "testnode")
|
||||||
c.Assert(err, check.NotNil)
|
c.Assert(err, check.NotNil)
|
||||||
|
|
||||||
nodeKey := key.NewNode()
|
nodeKey := key.NewNode()
|
||||||
|
@ -302,7 +303,7 @@ func (s *Suite) TestExpireNode(c *check.C) {
|
||||||
}
|
}
|
||||||
db.DB.Save(node)
|
db.DB.Save(node)
|
||||||
|
|
||||||
nodeFromDB, err := db.getNode("test", "testnode")
|
nodeFromDB, err := db.getNode(types.UserID(user.ID), "testnode")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
c.Assert(nodeFromDB, check.NotNil)
|
c.Assert(nodeFromDB, check.NotNil)
|
||||||
|
|
||||||
|
@ -312,7 +313,7 @@ func (s *Suite) TestExpireNode(c *check.C) {
|
||||||
err = db.NodeSetExpiry(nodeFromDB.ID, now)
|
err = db.NodeSetExpiry(nodeFromDB.ID, now)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
nodeFromDB, err = db.getNode("test", "testnode")
|
nodeFromDB, err = db.getNode(types.UserID(user.ID), "testnode")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
c.Assert(nodeFromDB.IsExpired(), check.Equals, true)
|
c.Assert(nodeFromDB.IsExpired(), check.Equals, true)
|
||||||
|
@ -322,10 +323,10 @@ func (s *Suite) TestSetTags(c *check.C) {
|
||||||
user, err := db.CreateUser("test")
|
user, err := db.CreateUser("test")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil)
|
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
_, err = db.getNode("test", "testnode")
|
_, err = db.getNode(types.UserID(user.ID), "testnode")
|
||||||
c.Assert(err, check.NotNil)
|
c.Assert(err, check.NotNil)
|
||||||
|
|
||||||
nodeKey := key.NewNode()
|
nodeKey := key.NewNode()
|
||||||
|
@ -348,7 +349,7 @@ func (s *Suite) TestSetTags(c *check.C) {
|
||||||
sTags := []string{"tag:test", "tag:foo"}
|
sTags := []string{"tag:test", "tag:foo"}
|
||||||
err = db.SetTags(node.ID, sTags)
|
err = db.SetTags(node.ID, sTags)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
node, err = db.getNode("test", "testnode")
|
node, err = db.getNode(types.UserID(user.ID), "testnode")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
c.Assert(node.ForcedTags, check.DeepEquals, sTags)
|
c.Assert(node.ForcedTags, check.DeepEquals, sTags)
|
||||||
|
|
||||||
|
@ -356,7 +357,7 @@ func (s *Suite) TestSetTags(c *check.C) {
|
||||||
eTags := []string{"tag:bar", "tag:test", "tag:unknown", "tag:test"}
|
eTags := []string{"tag:bar", "tag:test", "tag:unknown", "tag:test"}
|
||||||
err = db.SetTags(node.ID, eTags)
|
err = db.SetTags(node.ID, eTags)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
node, err = db.getNode("test", "testnode")
|
node, err = db.getNode(types.UserID(user.ID), "testnode")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
c.Assert(
|
c.Assert(
|
||||||
node.ForcedTags,
|
node.ForcedTags,
|
||||||
|
@ -367,7 +368,7 @@ func (s *Suite) TestSetTags(c *check.C) {
|
||||||
// test removing tags
|
// test removing tags
|
||||||
err = db.SetTags(node.ID, []string{})
|
err = db.SetTags(node.ID, []string{})
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
node, err = db.getNode("test", "testnode")
|
node, err = db.getNode(types.UserID(user.ID), "testnode")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
c.Assert(node.ForcedTags, check.DeepEquals, []string{})
|
c.Assert(node.ForcedTags, check.DeepEquals, []string{})
|
||||||
}
|
}
|
||||||
|
@ -557,18 +558,18 @@ func TestAutoApproveRoutes(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
adb, err := newTestDB()
|
adb, err := newSQLiteTestDB()
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
pol, err := policy.LoadACLPolicyFromBytes([]byte(tt.acl))
|
pol, err := policy.LoadACLPolicyFromBytes([]byte(tt.acl))
|
||||||
|
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotNil(t, pol)
|
assert.NotNil(t, pol)
|
||||||
|
|
||||||
user, err := adb.CreateUser("test")
|
user, err := adb.CreateUser("test")
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
pak, err := adb.CreatePreAuthKey(user.Name, false, false, nil, nil)
|
pak, err := adb.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
nodeKey := key.NewNode()
|
nodeKey := key.NewNode()
|
||||||
machineKey := key.NewMachine()
|
machineKey := key.NewMachine()
|
||||||
|
@ -590,21 +591,21 @@ func TestAutoApproveRoutes(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
trx := adb.DB.Save(&node)
|
trx := adb.DB.Save(&node)
|
||||||
assert.NoError(t, trx.Error)
|
require.NoError(t, trx.Error)
|
||||||
|
|
||||||
sendUpdate, err := adb.SaveNodeRoutes(&node)
|
sendUpdate, err := adb.SaveNodeRoutes(&node)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.False(t, sendUpdate)
|
assert.False(t, sendUpdate)
|
||||||
|
|
||||||
node0ByID, err := adb.GetNodeByID(0)
|
node0ByID, err := adb.GetNodeByID(0)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// TODO(kradalby): Check state update
|
// TODO(kradalby): Check state update
|
||||||
err = adb.EnableAutoApprovedRoutes(pol, node0ByID)
|
err = adb.EnableAutoApprovedRoutes(pol, node0ByID)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
enabledRoutes, err := adb.GetEnabledRoutes(node0ByID)
|
enabledRoutes, err := adb.GetEnabledRoutes(node0ByID)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, enabledRoutes, len(tt.want))
|
assert.Len(t, enabledRoutes, len(tt.want))
|
||||||
|
|
||||||
tsaddr.SortPrefixes(enabledRoutes)
|
tsaddr.SortPrefixes(enabledRoutes)
|
||||||
|
@ -691,19 +692,19 @@ func generateRandomNumber(t *testing.T, max int64) int64 {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestListEphemeralNodes(t *testing.T) {
|
func TestListEphemeralNodes(t *testing.T) {
|
||||||
db, err := newTestDB()
|
db, err := newSQLiteTestDB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("creating db: %s", err)
|
t.Fatalf("creating db: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := db.CreateUser("test")
|
user, err := db.CreateUser("test")
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil)
|
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
pakEph, err := db.CreatePreAuthKey(user.Name, false, true, nil, nil)
|
pakEph, err := db.CreatePreAuthKey(types.UserID(user.ID), false, true, nil, nil)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
node := types.Node{
|
node := types.Node{
|
||||||
ID: 0,
|
ID: 0,
|
||||||
|
@ -726,16 +727,16 @@ func TestListEphemeralNodes(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
err = db.DB.Save(&node).Error
|
err = db.DB.Save(&node).Error
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = db.DB.Save(&nodeEph).Error
|
err = db.DB.Save(&nodeEph).Error
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
nodes, err := db.ListNodes()
|
nodes, err := db.ListNodes()
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ephemeralNodes, err := db.ListEphemeralNodes()
|
ephemeralNodes, err := db.ListEphemeralNodes()
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, nodes, 2)
|
assert.Len(t, nodes, 2)
|
||||||
assert.Len(t, ephemeralNodes, 1)
|
assert.Len(t, ephemeralNodes, 1)
|
||||||
|
@ -747,16 +748,16 @@ func TestListEphemeralNodes(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRenameNode(t *testing.T) {
|
func TestRenameNode(t *testing.T) {
|
||||||
db, err := newTestDB()
|
db, err := newSQLiteTestDB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("creating db: %s", err)
|
t.Fatalf("creating db: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := db.CreateUser("test")
|
user, err := db.CreateUser("test")
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
user2, err := db.CreateUser("test2")
|
user2, err := db.CreateUser("test2")
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
node := types.Node{
|
node := types.Node{
|
||||||
ID: 0,
|
ID: 0,
|
||||||
|
@ -777,10 +778,10 @@ func TestRenameNode(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
err = db.DB.Save(&node).Error
|
err = db.DB.Save(&node).Error
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = db.DB.Save(&node2).Error
|
err = db.DB.Save(&node2).Error
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = db.DB.Transaction(func(tx *gorm.DB) error {
|
err = db.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
_, err := RegisterNode(tx, node, nil, nil)
|
_, err := RegisterNode(tx, node, nil, nil)
|
||||||
|
@ -790,10 +791,10 @@ func TestRenameNode(t *testing.T) {
|
||||||
_, err = RegisterNode(tx, node2, nil, nil)
|
_, err = RegisterNode(tx, node2, nil, nil)
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
nodes, err := db.ListNodes()
|
nodes, err := db.ListNodes()
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, nodes, 2)
|
assert.Len(t, nodes, 2)
|
||||||
|
|
||||||
|
@ -815,26 +816,26 @@ func TestRenameNode(t *testing.T) {
|
||||||
err = db.Write(func(tx *gorm.DB) error {
|
err = db.Write(func(tx *gorm.DB) error {
|
||||||
return RenameNode(tx, nodes[0].ID, "newname")
|
return RenameNode(tx, nodes[0].ID, "newname")
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
nodes, err = db.ListNodes()
|
nodes, err = db.ListNodes()
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, nodes, 2)
|
assert.Len(t, nodes, 2)
|
||||||
assert.Equal(t, nodes[0].Hostname, "test")
|
assert.Equal(t, "test", nodes[0].Hostname)
|
||||||
assert.Equal(t, nodes[0].GivenName, "newname")
|
assert.Equal(t, "newname", nodes[0].GivenName)
|
||||||
|
|
||||||
// Nodes can reuse name that is no longer used
|
// Nodes can reuse name that is no longer used
|
||||||
err = db.Write(func(tx *gorm.DB) error {
|
err = db.Write(func(tx *gorm.DB) error {
|
||||||
return RenameNode(tx, nodes[1].ID, "test")
|
return RenameNode(tx, nodes[1].ID, "test")
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
nodes, err = db.ListNodes()
|
nodes, err = db.ListNodes()
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, nodes, 2)
|
assert.Len(t, nodes, 2)
|
||||||
assert.Equal(t, nodes[0].Hostname, "test")
|
assert.Equal(t, "test", nodes[0].Hostname)
|
||||||
assert.Equal(t, nodes[0].GivenName, "newname")
|
assert.Equal(t, "newname", nodes[0].GivenName)
|
||||||
assert.Equal(t, nodes[1].GivenName, "test")
|
assert.Equal(t, "test", nodes[1].GivenName)
|
||||||
|
|
||||||
// Nodes cannot be renamed to used names
|
// Nodes cannot be renamed to used names
|
||||||
err = db.Write(func(tx *gorm.DB) error {
|
err = db.Write(func(tx *gorm.DB) error {
|
||||||
|
|
|
@ -23,29 +23,27 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (hsdb *HSDatabase) CreatePreAuthKey(
|
func (hsdb *HSDatabase) CreatePreAuthKey(
|
||||||
// TODO(kradalby): Should be ID, not name
|
uid types.UserID,
|
||||||
userName string,
|
|
||||||
reusable bool,
|
reusable bool,
|
||||||
ephemeral bool,
|
ephemeral bool,
|
||||||
expiration *time.Time,
|
expiration *time.Time,
|
||||||
aclTags []string,
|
aclTags []string,
|
||||||
) (*types.PreAuthKey, error) {
|
) (*types.PreAuthKey, error) {
|
||||||
return Write(hsdb.DB, func(tx *gorm.DB) (*types.PreAuthKey, error) {
|
return Write(hsdb.DB, func(tx *gorm.DB) (*types.PreAuthKey, error) {
|
||||||
return CreatePreAuthKey(tx, userName, reusable, ephemeral, expiration, aclTags)
|
return CreatePreAuthKey(tx, uid, reusable, ephemeral, expiration, aclTags)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreatePreAuthKey creates a new PreAuthKey in a user, and returns it.
|
// CreatePreAuthKey creates a new PreAuthKey in a user, and returns it.
|
||||||
func CreatePreAuthKey(
|
func CreatePreAuthKey(
|
||||||
tx *gorm.DB,
|
tx *gorm.DB,
|
||||||
// TODO(kradalby): Should be ID, not name
|
uid types.UserID,
|
||||||
userName string,
|
|
||||||
reusable bool,
|
reusable bool,
|
||||||
ephemeral bool,
|
ephemeral bool,
|
||||||
expiration *time.Time,
|
expiration *time.Time,
|
||||||
aclTags []string,
|
aclTags []string,
|
||||||
) (*types.PreAuthKey, error) {
|
) (*types.PreAuthKey, error) {
|
||||||
user, err := GetUserByUsername(tx, userName)
|
user, err := GetUserByID(tx, uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -89,15 +87,15 @@ func CreatePreAuthKey(
|
||||||
return &key, nil
|
return &key, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hsdb *HSDatabase) ListPreAuthKeys(userName string) ([]types.PreAuthKey, error) {
|
func (hsdb *HSDatabase) ListPreAuthKeys(uid types.UserID) ([]types.PreAuthKey, error) {
|
||||||
return Read(hsdb.DB, func(rx *gorm.DB) ([]types.PreAuthKey, error) {
|
return Read(hsdb.DB, func(rx *gorm.DB) ([]types.PreAuthKey, error) {
|
||||||
return ListPreAuthKeys(rx, userName)
|
return ListPreAuthKeysByUser(rx, uid)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListPreAuthKeys returns the list of PreAuthKeys for a user.
|
// ListPreAuthKeysByUser returns the list of PreAuthKeys for a user.
|
||||||
func ListPreAuthKeys(tx *gorm.DB, userName string) ([]types.PreAuthKey, error) {
|
func ListPreAuthKeysByUser(tx *gorm.DB, uid types.UserID) ([]types.PreAuthKey, error) {
|
||||||
user, err := GetUserByUsername(tx, userName)
|
user, err := GetUserByID(tx, uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,14 +11,14 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (*Suite) TestCreatePreAuthKey(c *check.C) {
|
func (*Suite) TestCreatePreAuthKey(c *check.C) {
|
||||||
_, err := db.CreatePreAuthKey("bogus", true, false, nil, nil)
|
// ID does not exist
|
||||||
|
_, err := db.CreatePreAuthKey(12345, true, false, nil, nil)
|
||||||
c.Assert(err, check.NotNil)
|
c.Assert(err, check.NotNil)
|
||||||
|
|
||||||
user, err := db.CreateUser("test")
|
user, err := db.CreateUser("test")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
key, err := db.CreatePreAuthKey(user.Name, true, false, nil, nil)
|
key, err := db.CreatePreAuthKey(types.UserID(user.ID), true, false, nil, nil)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
// Did we get a valid key?
|
// Did we get a valid key?
|
||||||
|
@ -26,17 +26,18 @@ func (*Suite) TestCreatePreAuthKey(c *check.C) {
|
||||||
c.Assert(len(key.Key), check.Equals, 48)
|
c.Assert(len(key.Key), check.Equals, 48)
|
||||||
|
|
||||||
// Make sure the User association is populated
|
// Make sure the User association is populated
|
||||||
c.Assert(key.User.Name, check.Equals, user.Name)
|
c.Assert(key.User.ID, check.Equals, user.ID)
|
||||||
|
|
||||||
_, err = db.ListPreAuthKeys("bogus")
|
// ID does not exist
|
||||||
|
_, err = db.ListPreAuthKeys(1000000)
|
||||||
c.Assert(err, check.NotNil)
|
c.Assert(err, check.NotNil)
|
||||||
|
|
||||||
keys, err := db.ListPreAuthKeys(user.Name)
|
keys, err := db.ListPreAuthKeys(types.UserID(user.ID))
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
c.Assert(len(keys), check.Equals, 1)
|
c.Assert(len(keys), check.Equals, 1)
|
||||||
|
|
||||||
// Make sure the User association is populated
|
// Make sure the User association is populated
|
||||||
c.Assert((keys)[0].User.Name, check.Equals, user.Name)
|
c.Assert((keys)[0].User.ID, check.Equals, user.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*Suite) TestExpiredPreAuthKey(c *check.C) {
|
func (*Suite) TestExpiredPreAuthKey(c *check.C) {
|
||||||
|
@ -44,7 +45,7 @@ func (*Suite) TestExpiredPreAuthKey(c *check.C) {
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
now := time.Now().Add(-5 * time.Second)
|
now := time.Now().Add(-5 * time.Second)
|
||||||
pak, err := db.CreatePreAuthKey(user.Name, true, false, &now, nil)
|
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), true, false, &now, nil)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
key, err := db.ValidatePreAuthKey(pak.Key)
|
key, err := db.ValidatePreAuthKey(pak.Key)
|
||||||
|
@ -62,7 +63,7 @@ func (*Suite) TestValidateKeyOk(c *check.C) {
|
||||||
user, err := db.CreateUser("test3")
|
user, err := db.CreateUser("test3")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
pak, err := db.CreatePreAuthKey(user.Name, true, false, nil, nil)
|
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), true, false, nil, nil)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
key, err := db.ValidatePreAuthKey(pak.Key)
|
key, err := db.ValidatePreAuthKey(pak.Key)
|
||||||
|
@ -74,7 +75,7 @@ func (*Suite) TestAlreadyUsedKey(c *check.C) {
|
||||||
user, err := db.CreateUser("test4")
|
user, err := db.CreateUser("test4")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil)
|
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
node := types.Node{
|
node := types.Node{
|
||||||
|
@ -96,7 +97,7 @@ func (*Suite) TestReusableBeingUsedKey(c *check.C) {
|
||||||
user, err := db.CreateUser("test5")
|
user, err := db.CreateUser("test5")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
pak, err := db.CreatePreAuthKey(user.Name, true, false, nil, nil)
|
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), true, false, nil, nil)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
node := types.Node{
|
node := types.Node{
|
||||||
|
@ -118,7 +119,7 @@ func (*Suite) TestNotReusableNotBeingUsedKey(c *check.C) {
|
||||||
user, err := db.CreateUser("test6")
|
user, err := db.CreateUser("test6")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil)
|
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
key, err := db.ValidatePreAuthKey(pak.Key)
|
key, err := db.ValidatePreAuthKey(pak.Key)
|
||||||
|
@ -130,7 +131,7 @@ func (*Suite) TestExpirePreauthKey(c *check.C) {
|
||||||
user, err := db.CreateUser("test3")
|
user, err := db.CreateUser("test3")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
pak, err := db.CreatePreAuthKey(user.Name, true, false, nil, nil)
|
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), true, false, nil, nil)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
c.Assert(pak.Expiration, check.IsNil)
|
c.Assert(pak.Expiration, check.IsNil)
|
||||||
|
|
||||||
|
@ -147,7 +148,7 @@ func (*Suite) TestNotReusableMarkedAsUsed(c *check.C) {
|
||||||
user, err := db.CreateUser("test6")
|
user, err := db.CreateUser("test6")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil)
|
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
pak.Used = true
|
pak.Used = true
|
||||||
db.DB.Save(&pak)
|
db.DB.Save(&pak)
|
||||||
|
@ -160,15 +161,15 @@ func (*Suite) TestPreAuthKeyACLTags(c *check.C) {
|
||||||
user, err := db.CreateUser("test8")
|
user, err := db.CreateUser("test8")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
_, err = db.CreatePreAuthKey(user.Name, false, false, nil, []string{"badtag"})
|
_, err = db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, []string{"badtag"})
|
||||||
c.Assert(err, check.NotNil) // Confirm that malformed tags are rejected
|
c.Assert(err, check.NotNil) // Confirm that malformed tags are rejected
|
||||||
|
|
||||||
tags := []string{"tag:test1", "tag:test2"}
|
tags := []string{"tag:test1", "tag:test2"}
|
||||||
tagsWithDuplicate := []string{"tag:test1", "tag:test2", "tag:test2"}
|
tagsWithDuplicate := []string{"tag:test1", "tag:test2", "tag:test2"}
|
||||||
_, err = db.CreatePreAuthKey(user.Name, false, false, nil, tagsWithDuplicate)
|
_, err = db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, tagsWithDuplicate)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
listedPaks, err := db.ListPreAuthKeys("test8")
|
listedPaks, err := db.ListPreAuthKeys(types.UserID(user.ID))
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
gotTags := listedPaks[0].Proto().GetAclTags()
|
gotTags := listedPaks[0].Proto().GetAclTags()
|
||||||
sort.Sort(sort.StringSlice(gotTags))
|
sort.Sort(sort.StringSlice(gotTags))
|
||||||
|
|
|
@ -639,7 +639,7 @@ func EnableAutoApprovedRoutes(
|
||||||
|
|
||||||
log.Trace().
|
log.Trace().
|
||||||
Str("node", node.Hostname).
|
Str("node", node.Hostname).
|
||||||
Str("user", node.User.Name).
|
Uint("user.id", node.User.ID).
|
||||||
Strs("routeApprovers", routeApprovers).
|
Strs("routeApprovers", routeApprovers).
|
||||||
Str("prefix", netip.Prefix(advertisedRoute.Prefix).String()).
|
Str("prefix", netip.Prefix(advertisedRoute.Prefix).String()).
|
||||||
Msg("looking up route for autoapproving")
|
Msg("looking up route for autoapproving")
|
||||||
|
@ -648,8 +648,13 @@ func EnableAutoApprovedRoutes(
|
||||||
if approvedAlias == node.User.Username() {
|
if approvedAlias == node.User.Username() {
|
||||||
approvedRoutes = append(approvedRoutes, advertisedRoute)
|
approvedRoutes = append(approvedRoutes, advertisedRoute)
|
||||||
} else {
|
} else {
|
||||||
|
users, err := ListUsers(tx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("looking up users to expand route alias: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// TODO(kradalby): figure out how to get this to depend on less stuff
|
// TODO(kradalby): figure out how to get this to depend on less stuff
|
||||||
approvedIps, err := aclPolicy.ExpandAlias(types.Nodes{node}, approvedAlias)
|
approvedIps, err := aclPolicy.ExpandAlias(types.Nodes{node}, users, approvedAlias)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("expanding alias %q for autoApprovers: %w", approvedAlias, err)
|
return fmt.Errorf("expanding alias %q for autoApprovers: %w", approvedAlias, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,10 +35,10 @@ func (s *Suite) TestGetRoutes(c *check.C) {
|
||||||
user, err := db.CreateUser("test")
|
user, err := db.CreateUser("test")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil)
|
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
_, err = db.getNode("test", "test_get_route_node")
|
_, err = db.getNode(types.UserID(user.ID), "test_get_route_node")
|
||||||
c.Assert(err, check.NotNil)
|
c.Assert(err, check.NotNil)
|
||||||
|
|
||||||
route, err := netip.ParsePrefix("10.0.0.0/24")
|
route, err := netip.ParsePrefix("10.0.0.0/24")
|
||||||
|
@ -79,10 +79,10 @@ func (s *Suite) TestGetEnableRoutes(c *check.C) {
|
||||||
user, err := db.CreateUser("test")
|
user, err := db.CreateUser("test")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil)
|
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
_, err = db.getNode("test", "test_enable_route_node")
|
_, err = db.getNode(types.UserID(user.ID), "test_enable_route_node")
|
||||||
c.Assert(err, check.NotNil)
|
c.Assert(err, check.NotNil)
|
||||||
|
|
||||||
route, err := netip.ParsePrefix(
|
route, err := netip.ParsePrefix(
|
||||||
|
@ -153,10 +153,10 @@ func (s *Suite) TestIsUniquePrefix(c *check.C) {
|
||||||
user, err := db.CreateUser("test")
|
user, err := db.CreateUser("test")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil)
|
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
_, err = db.getNode("test", "test_enable_route_node")
|
_, err = db.getNode(types.UserID(user.ID), "test_enable_route_node")
|
||||||
c.Assert(err, check.NotNil)
|
c.Assert(err, check.NotNil)
|
||||||
|
|
||||||
route, err := netip.ParsePrefix(
|
route, err := netip.ParsePrefix(
|
||||||
|
@ -234,10 +234,10 @@ func (s *Suite) TestDeleteRoutes(c *check.C) {
|
||||||
user, err := db.CreateUser("test")
|
user, err := db.CreateUser("test")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil)
|
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
_, err = db.getNode("test", "test_enable_route_node")
|
_, err = db.getNode(types.UserID(user.ID), "test_enable_route_node")
|
||||||
c.Assert(err, check.NotNil)
|
c.Assert(err, check.NotNil)
|
||||||
|
|
||||||
prefix, err := netip.ParsePrefix(
|
prefix, err := netip.ParsePrefix(
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"log"
|
"log"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/juanfont/headscale/hscontrol/types"
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
"gopkg.in/check.v1"
|
"gopkg.in/check.v1"
|
||||||
|
"zombiezen.com/go/postgrestest"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test(t *testing.T) {
|
func Test(t *testing.T) {
|
||||||
|
@ -36,13 +41,15 @@ func (s *Suite) ResetDB(c *check.C) {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
db, err = newTestDB()
|
db, err = newSQLiteTestDB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Fatal(err)
|
c.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestDB() (*HSDatabase, error) {
|
// TODO(kradalby): make this a t.Helper when we dont depend
|
||||||
|
// on check test framework.
|
||||||
|
func newSQLiteTestDB() (*HSDatabase, error) {
|
||||||
var err error
|
var err error
|
||||||
tmpDir, err = os.MkdirTemp("", "headscale-db-test-*")
|
tmpDir, err = os.MkdirTemp("", "headscale-db-test-*")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -53,7 +60,7 @@ func newTestDB() (*HSDatabase, error) {
|
||||||
|
|
||||||
db, err = NewHeadscaleDatabase(
|
db, err = NewHeadscaleDatabase(
|
||||||
types.DatabaseConfig{
|
types.DatabaseConfig{
|
||||||
Type: "sqlite3",
|
Type: types.DatabaseSqlite,
|
||||||
Sqlite: types.SqliteConfig{
|
Sqlite: types.SqliteConfig{
|
||||||
Path: tmpDir + "/headscale_test.db",
|
Path: tmpDir + "/headscale_test.db",
|
||||||
},
|
},
|
||||||
|
@ -67,3 +74,53 @@ func newTestDB() (*HSDatabase, error) {
|
||||||
|
|
||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newPostgresTestDB(t *testing.T) *HSDatabase {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var err error
|
||||||
|
tmpDir, err = os.MkdirTemp("", "headscale-db-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("database path: %s", tmpDir+"/headscale_test.db")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
srv, err := postgrestest.Start(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(srv.Cleanup)
|
||||||
|
|
||||||
|
u, err := srv.CreateDatabase(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Logf("created local postgres: %s", u)
|
||||||
|
pu, _ := url.Parse(u)
|
||||||
|
|
||||||
|
pass, _ := pu.User.Password()
|
||||||
|
port, _ := strconv.Atoi(pu.Port())
|
||||||
|
|
||||||
|
db, err = NewHeadscaleDatabase(
|
||||||
|
types.DatabaseConfig{
|
||||||
|
Type: types.DatabasePostgres,
|
||||||
|
Postgres: types.PostgresConfig{
|
||||||
|
Host: pu.Hostname(),
|
||||||
|
User: pu.User.Username(),
|
||||||
|
Name: strings.TrimLeft(pu.Path, "/"),
|
||||||
|
Pass: pass,
|
||||||
|
Port: port,
|
||||||
|
Ssl: "disable",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"",
|
||||||
|
emptyCache(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
|
@ -28,11 +28,9 @@ func CreateUser(tx *gorm.DB, name string) (*types.User, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
user := types.User{}
|
user := types.User{
|
||||||
if err := tx.Where("name = ?", name).First(&user).Error; err == nil {
|
Name: name,
|
||||||
return nil, ErrUserExists
|
|
||||||
}
|
}
|
||||||
user.Name = name
|
|
||||||
if err := tx.Create(&user).Error; err != nil {
|
if err := tx.Create(&user).Error; err != nil {
|
||||||
return nil, fmt.Errorf("creating user: %w", err)
|
return nil, fmt.Errorf("creating user: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -40,21 +38,21 @@ func CreateUser(tx *gorm.DB, name string) (*types.User, error) {
|
||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hsdb *HSDatabase) DestroyUser(name string) error {
|
func (hsdb *HSDatabase) DestroyUser(uid types.UserID) error {
|
||||||
return hsdb.Write(func(tx *gorm.DB) error {
|
return hsdb.Write(func(tx *gorm.DB) error {
|
||||||
return DestroyUser(tx, name)
|
return DestroyUser(tx, uid)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// DestroyUser destroys a User. Returns error if the User does
|
// DestroyUser destroys a User. Returns error if the User does
|
||||||
// not exist or if there are nodes associated with it.
|
// not exist or if there are nodes associated with it.
|
||||||
func DestroyUser(tx *gorm.DB, name string) error {
|
func DestroyUser(tx *gorm.DB, uid types.UserID) error {
|
||||||
user, err := GetUserByUsername(tx, name)
|
user, err := GetUserByID(tx, uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrUserNotFound
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
nodes, err := ListNodesByUser(tx, name)
|
nodes, err := ListNodesByUser(tx, uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -62,7 +60,7 @@ func DestroyUser(tx *gorm.DB, name string) error {
|
||||||
return ErrUserStillHasNodes
|
return ErrUserStillHasNodes
|
||||||
}
|
}
|
||||||
|
|
||||||
keys, err := ListPreAuthKeys(tx, name)
|
keys, err := ListPreAuthKeysByUser(tx, uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -80,17 +78,17 @@ func DestroyUser(tx *gorm.DB, name string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hsdb *HSDatabase) RenameUser(oldName, newName string) error {
|
func (hsdb *HSDatabase) RenameUser(uid types.UserID, newName string) error {
|
||||||
return hsdb.Write(func(tx *gorm.DB) error {
|
return hsdb.Write(func(tx *gorm.DB) error {
|
||||||
return RenameUser(tx, oldName, newName)
|
return RenameUser(tx, uid, newName)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenameUser renames a User. Returns error if the User does
|
// RenameUser renames a User. Returns error if the User does
|
||||||
// not exist or if another User exists with the new name.
|
// not exist or if another User exists with the new name.
|
||||||
func RenameUser(tx *gorm.DB, oldName, newName string) error {
|
func RenameUser(tx *gorm.DB, uid types.UserID, newName string) error {
|
||||||
var err error
|
var err error
|
||||||
oldUser, err := GetUserByUsername(tx, oldName)
|
oldUser, err := GetUserByID(tx, uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -98,50 +96,25 @@ func RenameUser(tx *gorm.DB, oldName, newName string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = GetUserByUsername(tx, newName)
|
|
||||||
if err == nil {
|
|
||||||
return ErrUserExists
|
|
||||||
}
|
|
||||||
if !errors.Is(err, ErrUserNotFound) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
oldUser.Name = newName
|
oldUser.Name = newName
|
||||||
|
|
||||||
if result := tx.Save(&oldUser); result.Error != nil {
|
if err := tx.Save(&oldUser).Error; err != nil {
|
||||||
return result.Error
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hsdb *HSDatabase) GetUserByName(name string) (*types.User, error) {
|
func (hsdb *HSDatabase) GetUserByID(uid types.UserID) (*types.User, error) {
|
||||||
return Read(hsdb.DB, func(rx *gorm.DB) (*types.User, error) {
|
return Read(hsdb.DB, func(rx *gorm.DB) (*types.User, error) {
|
||||||
return GetUserByUsername(rx, name)
|
return GetUserByID(rx, uid)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetUserByUsername(tx *gorm.DB, name string) (*types.User, error) {
|
func GetUserByID(tx *gorm.DB, uid types.UserID) (*types.User, error) {
|
||||||
user := types.User{}
|
user := types.User{}
|
||||||
if result := tx.First(&user, "name = ?", name); errors.Is(
|
if result := tx.First(&user, "id = ?", uid); errors.Is(
|
||||||
result.Error,
|
|
||||||
gorm.ErrRecordNotFound,
|
|
||||||
) {
|
|
||||||
return nil, ErrUserNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
return &user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hsdb *HSDatabase) GetUserByID(id types.UserID) (*types.User, error) {
|
|
||||||
return Read(hsdb.DB, func(rx *gorm.DB) (*types.User, error) {
|
|
||||||
return GetUserByID(rx, id)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetUserByID(tx *gorm.DB, id types.UserID) (*types.User, error) {
|
|
||||||
user := types.User{}
|
|
||||||
if result := tx.First(&user, "id = ?", id); errors.Is(
|
|
||||||
result.Error,
|
result.Error,
|
||||||
gorm.ErrRecordNotFound,
|
gorm.ErrRecordNotFound,
|
||||||
) {
|
) {
|
||||||
|
@ -169,54 +142,69 @@ func GetUserByOIDCIdentifier(tx *gorm.DB, id string) (*types.User, error) {
|
||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hsdb *HSDatabase) ListUsers() ([]types.User, error) {
|
func (hsdb *HSDatabase) ListUsers(where ...*types.User) ([]types.User, error) {
|
||||||
return Read(hsdb.DB, func(rx *gorm.DB) ([]types.User, error) {
|
return Read(hsdb.DB, func(rx *gorm.DB) ([]types.User, error) {
|
||||||
return ListUsers(rx)
|
return ListUsers(rx, where...)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListUsers gets all the existing users.
|
// ListUsers gets all the existing users.
|
||||||
func ListUsers(tx *gorm.DB) ([]types.User, error) {
|
func ListUsers(tx *gorm.DB, where ...*types.User) ([]types.User, error) {
|
||||||
|
if len(where) > 1 {
|
||||||
|
return nil, fmt.Errorf("expect 0 or 1 where User structs, got %d", len(where))
|
||||||
|
}
|
||||||
|
|
||||||
|
var user *types.User
|
||||||
|
if len(where) == 1 {
|
||||||
|
user = where[0]
|
||||||
|
}
|
||||||
|
|
||||||
users := []types.User{}
|
users := []types.User{}
|
||||||
if err := tx.Find(&users).Error; err != nil {
|
if err := tx.Where(user).Find(&users).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListNodesByUser gets all the nodes in a given user.
|
// GetUserByName returns a user if the provided username is
|
||||||
func ListNodesByUser(tx *gorm.DB, name string) (types.Nodes, error) {
|
// unique, and otherwise an error.
|
||||||
err := util.CheckForFQDNRules(name)
|
func (hsdb *HSDatabase) GetUserByName(name string) (*types.User, error) {
|
||||||
if err != nil {
|
users, err := hsdb.ListUsers(&types.User{Name: name})
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
user, err := GetUserByUsername(tx, name)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(users) == 0 {
|
||||||
|
return nil, ErrUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(users) != 1 {
|
||||||
|
return nil, fmt.Errorf("expected exactly one user, found %d", len(users))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &users[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListNodesByUser gets all the nodes in a given user.
|
||||||
|
func ListNodesByUser(tx *gorm.DB, uid types.UserID) (types.Nodes, error) {
|
||||||
nodes := types.Nodes{}
|
nodes := types.Nodes{}
|
||||||
if err := tx.Preload("AuthKey").Preload("AuthKey.User").Preload("User").Where(&types.Node{UserID: user.ID}).Find(&nodes).Error; err != nil {
|
if err := tx.Preload("AuthKey").Preload("AuthKey.User").Preload("User").Where(&types.Node{UserID: uint(uid)}).Find(&nodes).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nodes, nil
|
return nodes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hsdb *HSDatabase) AssignNodeToUser(node *types.Node, username string) error {
|
func (hsdb *HSDatabase) AssignNodeToUser(node *types.Node, uid types.UserID) error {
|
||||||
return hsdb.Write(func(tx *gorm.DB) error {
|
return hsdb.Write(func(tx *gorm.DB) error {
|
||||||
return AssignNodeToUser(tx, node, username)
|
return AssignNodeToUser(tx, node, uid)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// AssignNodeToUser assigns a Node to a user.
|
// AssignNodeToUser assigns a Node to a user.
|
||||||
func AssignNodeToUser(tx *gorm.DB, node *types.Node, username string) error {
|
func AssignNodeToUser(tx *gorm.DB, node *types.Node, uid types.UserID) error {
|
||||||
err := util.CheckForFQDNRules(username)
|
user, err := GetUserByID(tx, uid)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
user, err := GetUserByUsername(tx, username)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/juanfont/headscale/hscontrol/types"
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
"github.com/juanfont/headscale/hscontrol/util"
|
"github.com/juanfont/headscale/hscontrol/util"
|
||||||
"gopkg.in/check.v1"
|
"gopkg.in/check.v1"
|
||||||
|
@ -17,24 +19,24 @@ func (s *Suite) TestCreateAndDestroyUser(c *check.C) {
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
c.Assert(len(users), check.Equals, 1)
|
c.Assert(len(users), check.Equals, 1)
|
||||||
|
|
||||||
err = db.DestroyUser("test")
|
err = db.DestroyUser(types.UserID(user.ID))
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
_, err = db.GetUserByName("test")
|
_, err = db.GetUserByID(types.UserID(user.ID))
|
||||||
c.Assert(err, check.NotNil)
|
c.Assert(err, check.NotNil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Suite) TestDestroyUserErrors(c *check.C) {
|
func (s *Suite) TestDestroyUserErrors(c *check.C) {
|
||||||
err := db.DestroyUser("test")
|
err := db.DestroyUser(9998)
|
||||||
c.Assert(err, check.Equals, ErrUserNotFound)
|
c.Assert(err, check.Equals, ErrUserNotFound)
|
||||||
|
|
||||||
user, err := db.CreateUser("test")
|
user, err := db.CreateUser("test")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil)
|
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
err = db.DestroyUser("test")
|
err = db.DestroyUser(types.UserID(user.ID))
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
result := db.DB.Preload("User").First(&pak, "key = ?", pak.Key)
|
result := db.DB.Preload("User").First(&pak, "key = ?", pak.Key)
|
||||||
|
@ -44,7 +46,7 @@ func (s *Suite) TestDestroyUserErrors(c *check.C) {
|
||||||
user, err = db.CreateUser("test")
|
user, err = db.CreateUser("test")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
pak, err = db.CreatePreAuthKey(user.Name, false, false, nil, nil)
|
pak, err = db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
node := types.Node{
|
node := types.Node{
|
||||||
|
@ -57,7 +59,7 @@ func (s *Suite) TestDestroyUserErrors(c *check.C) {
|
||||||
trx := db.DB.Save(&node)
|
trx := db.DB.Save(&node)
|
||||||
c.Assert(trx.Error, check.IsNil)
|
c.Assert(trx.Error, check.IsNil)
|
||||||
|
|
||||||
err = db.DestroyUser("test")
|
err = db.DestroyUser(types.UserID(user.ID))
|
||||||
c.Assert(err, check.Equals, ErrUserStillHasNodes)
|
c.Assert(err, check.Equals, ErrUserStillHasNodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,24 +72,29 @@ func (s *Suite) TestRenameUser(c *check.C) {
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
c.Assert(len(users), check.Equals, 1)
|
c.Assert(len(users), check.Equals, 1)
|
||||||
|
|
||||||
err = db.RenameUser("test", "test-renamed")
|
err = db.RenameUser(types.UserID(userTest.ID), "test-renamed")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
_, err = db.GetUserByName("test")
|
users, err = db.ListUsers(&types.User{Name: "test"})
|
||||||
c.Assert(err, check.Equals, ErrUserNotFound)
|
c.Assert(err, check.Equals, nil)
|
||||||
|
c.Assert(len(users), check.Equals, 0)
|
||||||
|
|
||||||
_, err = db.GetUserByName("test-renamed")
|
users, err = db.ListUsers(&types.User{Name: "test-renamed"})
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
c.Assert(len(users), check.Equals, 1)
|
||||||
|
|
||||||
err = db.RenameUser("test-does-not-exit", "test")
|
err = db.RenameUser(99988, "test")
|
||||||
c.Assert(err, check.Equals, ErrUserNotFound)
|
c.Assert(err, check.Equals, ErrUserNotFound)
|
||||||
|
|
||||||
userTest2, err := db.CreateUser("test2")
|
userTest2, err := db.CreateUser("test2")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
c.Assert(userTest2.Name, check.Equals, "test2")
|
c.Assert(userTest2.Name, check.Equals, "test2")
|
||||||
|
|
||||||
err = db.RenameUser("test2", "test-renamed")
|
want := "UNIQUE constraint failed"
|
||||||
c.Assert(err, check.Equals, ErrUserExists)
|
err = db.RenameUser(types.UserID(userTest2.ID), "test-renamed")
|
||||||
|
if err == nil || !strings.Contains(err.Error(), want) {
|
||||||
|
c.Fatalf("expected failure with unique constraint, want: %q got: %q", want, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Suite) TestSetMachineUser(c *check.C) {
|
func (s *Suite) TestSetMachineUser(c *check.C) {
|
||||||
|
@ -97,7 +104,7 @@ func (s *Suite) TestSetMachineUser(c *check.C) {
|
||||||
newUser, err := db.CreateUser("new")
|
newUser, err := db.CreateUser("new")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
pak, err := db.CreatePreAuthKey(oldUser.Name, false, false, nil, nil)
|
pak, err := db.CreatePreAuthKey(types.UserID(oldUser.ID), false, false, nil, nil)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
node := types.Node{
|
node := types.Node{
|
||||||
|
@ -111,15 +118,15 @@ func (s *Suite) TestSetMachineUser(c *check.C) {
|
||||||
c.Assert(trx.Error, check.IsNil)
|
c.Assert(trx.Error, check.IsNil)
|
||||||
c.Assert(node.UserID, check.Equals, oldUser.ID)
|
c.Assert(node.UserID, check.Equals, oldUser.ID)
|
||||||
|
|
||||||
err = db.AssignNodeToUser(&node, newUser.Name)
|
err = db.AssignNodeToUser(&node, types.UserID(newUser.ID))
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
c.Assert(node.UserID, check.Equals, newUser.ID)
|
c.Assert(node.UserID, check.Equals, newUser.ID)
|
||||||
c.Assert(node.User.Name, check.Equals, newUser.Name)
|
c.Assert(node.User.Name, check.Equals, newUser.Name)
|
||||||
|
|
||||||
err = db.AssignNodeToUser(&node, "non-existing-user")
|
err = db.AssignNodeToUser(&node, 9584849)
|
||||||
c.Assert(err, check.Equals, ErrUserNotFound)
|
c.Assert(err, check.Equals, ErrUserNotFound)
|
||||||
|
|
||||||
err = db.AssignNodeToUser(&node, newUser.Name)
|
err = db.AssignNodeToUser(&node, types.UserID(newUser.ID))
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
c.Assert(node.UserID, check.Equals, newUser.ID)
|
c.Assert(node.UserID, check.Equals, newUser.ID)
|
||||||
c.Assert(node.User.Name, check.Equals, newUser.Name)
|
c.Assert(node.User.Name, check.Equals, newUser.Name)
|
||||||
|
|
|
@ -65,24 +65,34 @@ func (api headscaleV1APIServer) RenameUser(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
request *v1.RenameUserRequest,
|
request *v1.RenameUserRequest,
|
||||||
) (*v1.RenameUserResponse, error) {
|
) (*v1.RenameUserResponse, error) {
|
||||||
err := api.h.db.RenameUser(request.GetOldName(), request.GetNewName())
|
oldUser, err := api.h.db.GetUserByName(request.GetOldName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := api.h.db.GetUserByName(request.GetNewName())
|
err = api.h.db.RenameUser(types.UserID(oldUser.ID), request.GetNewName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &v1.RenameUserResponse{User: user.Proto()}, nil
|
newUser, err := api.h.db.GetUserByName(request.GetNewName())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &v1.RenameUserResponse{User: newUser.Proto()}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api headscaleV1APIServer) DeleteUser(
|
func (api headscaleV1APIServer) DeleteUser(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
request *v1.DeleteUserRequest,
|
request *v1.DeleteUserRequest,
|
||||||
) (*v1.DeleteUserResponse, error) {
|
) (*v1.DeleteUserResponse, error) {
|
||||||
err := api.h.db.DestroyUser(request.GetName())
|
user, err := api.h.db.GetUserByName(request.GetName())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = api.h.db.DestroyUser(types.UserID(user.ID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -131,8 +141,13 @@ func (api headscaleV1APIServer) CreatePreAuthKey(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user, err := api.h.db.GetUserByName(request.GetUser())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
preAuthKey, err := api.h.db.CreatePreAuthKey(
|
preAuthKey, err := api.h.db.CreatePreAuthKey(
|
||||||
request.GetUser(),
|
types.UserID(user.ID),
|
||||||
request.GetReusable(),
|
request.GetReusable(),
|
||||||
request.GetEphemeral(),
|
request.GetEphemeral(),
|
||||||
&expiration,
|
&expiration,
|
||||||
|
@ -168,7 +183,12 @@ func (api headscaleV1APIServer) ListPreAuthKeys(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
request *v1.ListPreAuthKeysRequest,
|
request *v1.ListPreAuthKeysRequest,
|
||||||
) (*v1.ListPreAuthKeysResponse, error) {
|
) (*v1.ListPreAuthKeysResponse, error) {
|
||||||
preAuthKeys, err := api.h.db.ListPreAuthKeys(request.GetUser())
|
user, err := api.h.db.GetUserByName(request.GetUser())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
preAuthKeys, err := api.h.db.ListPreAuthKeys(types.UserID(user.ID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -406,10 +426,20 @@ func (api headscaleV1APIServer) ListNodes(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
request *v1.ListNodesRequest,
|
request *v1.ListNodesRequest,
|
||||||
) (*v1.ListNodesResponse, error) {
|
) (*v1.ListNodesResponse, error) {
|
||||||
|
// TODO(kradalby): it looks like this can be simplified a lot,
|
||||||
|
// the filtering of nodes by user, vs nodes as a whole can
|
||||||
|
// probably be done once.
|
||||||
|
// TODO(kradalby): This should be done in one tx.
|
||||||
|
|
||||||
isLikelyConnected := api.h.nodeNotifier.LikelyConnectedMap()
|
isLikelyConnected := api.h.nodeNotifier.LikelyConnectedMap()
|
||||||
if request.GetUser() != "" {
|
if request.GetUser() != "" {
|
||||||
|
user, err := api.h.db.GetUserByName(request.GetUser())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
nodes, err := db.Read(api.h.db.DB, func(rx *gorm.DB) (types.Nodes, error) {
|
nodes, err := db.Read(api.h.db.DB, func(rx *gorm.DB) (types.Nodes, error) {
|
||||||
return db.ListNodesByUser(rx, request.GetUser())
|
return db.ListNodesByUser(rx, types.UserID(user.ID))
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -465,12 +495,18 @@ func (api headscaleV1APIServer) MoveNode(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
request *v1.MoveNodeRequest,
|
request *v1.MoveNodeRequest,
|
||||||
) (*v1.MoveNodeResponse, error) {
|
) (*v1.MoveNodeResponse, error) {
|
||||||
|
// TODO(kradalby): This should be done in one tx.
|
||||||
node, err := api.h.db.GetNodeByID(types.NodeID(request.GetNodeId()))
|
node, err := api.h.db.GetNodeByID(types.NodeID(request.GetNodeId()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = api.h.db.AssignNodeToUser(node, request.GetUser())
|
user, err := api.h.db.GetUserByName(request.GetUser())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = api.h.db.AssignNodeToUser(node, types.UserID(user.ID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -737,14 +773,18 @@ func (api headscaleV1APIServer) SetPolicy(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("loading nodes from database to validate policy: %w", err)
|
return nil, fmt.Errorf("loading nodes from database to validate policy: %w", err)
|
||||||
}
|
}
|
||||||
|
users, err := api.h.db.ListUsers()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("loading users from database to validate policy: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
_, err = pol.CompileFilterRules(nodes)
|
_, err = pol.CompileFilterRules(users, nodes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("verifying policy rules: %w", err)
|
return nil, fmt.Errorf("verifying policy rules: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(nodes) > 0 {
|
if len(nodes) > 0 {
|
||||||
_, err = pol.CompileSSHPolicy(nodes[0], nodes)
|
_, err = pol.CompileSSHPolicy(nodes[0], users, nodes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("verifying SSH rules: %w", err)
|
return nil, fmt.Errorf("verifying SSH rules: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -56,6 +57,65 @@ func parseCabailityVersion(req *http.Request) (tailcfg.CapabilityVersion, error)
|
||||||
return tailcfg.CapabilityVersion(clientCapabilityVersion), nil
|
return tailcfg.CapabilityVersion(clientCapabilityVersion), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Headscale) handleVerifyRequest(
|
||||||
|
req *http.Request,
|
||||||
|
) (bool, error) {
|
||||||
|
body, err := io.ReadAll(req.Body)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("cannot read request body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var derpAdmitClientRequest tailcfg.DERPAdmitClientRequest
|
||||||
|
if err := json.Unmarshal(body, &derpAdmitClientRequest); err != nil {
|
||||||
|
return false, fmt.Errorf("cannot parse derpAdmitClientRequest: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes, err := h.db.ListNodes()
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("cannot list nodes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes.ContainsNodeKey(derpAdmitClientRequest.NodePublic), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// see https://github.com/tailscale/tailscale/blob/964282d34f06ecc06ce644769c66b0b31d118340/derp/derp_server.go#L1159, Derp use verifyClientsURL to verify whether a client is allowed to connect to the DERP server.
|
||||||
|
func (h *Headscale) VerifyHandler(
|
||||||
|
writer http.ResponseWriter,
|
||||||
|
req *http.Request,
|
||||||
|
) {
|
||||||
|
if req.Method != http.MethodPost {
|
||||||
|
http.Error(writer, "Wrong method", http.StatusMethodNotAllowed)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Debug().
|
||||||
|
Str("handler", "/verify").
|
||||||
|
Msg("verify client")
|
||||||
|
|
||||||
|
allow, err := h.handleVerifyRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Err(err).
|
||||||
|
Msg("Failed to verify client")
|
||||||
|
http.Error(writer, "Internal error", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := tailcfg.DERPAdmitClientResponse{
|
||||||
|
Allow: allow,
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Header().Set("Content-Type", "application/json")
|
||||||
|
writer.WriteHeader(http.StatusOK)
|
||||||
|
err = json.NewEncoder(writer).Encode(resp)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Err(err).
|
||||||
|
Msg("Failed to write response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// KeyHandler provides the Headscale pub key
|
// KeyHandler provides the Headscale pub key
|
||||||
// Listens in /key.
|
// Listens in /key.
|
||||||
func (h *Headscale) KeyHandler(
|
func (h *Headscale) KeyHandler(
|
||||||
|
|
|
@ -153,6 +153,7 @@ func addNextDNSMetadata(resolvers []*dnstype.Resolver, node *types.Node) {
|
||||||
func (m *Mapper) fullMapResponse(
|
func (m *Mapper) fullMapResponse(
|
||||||
node *types.Node,
|
node *types.Node,
|
||||||
peers types.Nodes,
|
peers types.Nodes,
|
||||||
|
users []types.User,
|
||||||
pol *policy.ACLPolicy,
|
pol *policy.ACLPolicy,
|
||||||
capVer tailcfg.CapabilityVersion,
|
capVer tailcfg.CapabilityVersion,
|
||||||
) (*tailcfg.MapResponse, error) {
|
) (*tailcfg.MapResponse, error) {
|
||||||
|
@ -167,6 +168,7 @@ func (m *Mapper) fullMapResponse(
|
||||||
pol,
|
pol,
|
||||||
node,
|
node,
|
||||||
capVer,
|
capVer,
|
||||||
|
users,
|
||||||
peers,
|
peers,
|
||||||
peers,
|
peers,
|
||||||
m.cfg,
|
m.cfg,
|
||||||
|
@ -189,8 +191,12 @@ func (m *Mapper) FullMapResponse(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
users, err := m.db.ListUsers()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := m.fullMapResponse(node, peers, pol, mapRequest.Version)
|
resp, err := m.fullMapResponse(node, peers, users, pol, mapRequest.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -253,6 +259,11 @@ func (m *Mapper) PeerChangedResponse(
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
users, err := m.db.ListUsers()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("listing users for map response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
var removedIDs []tailcfg.NodeID
|
var removedIDs []tailcfg.NodeID
|
||||||
var changedIDs []types.NodeID
|
var changedIDs []types.NodeID
|
||||||
for nodeID, nodeChanged := range changed {
|
for nodeID, nodeChanged := range changed {
|
||||||
|
@ -276,6 +287,7 @@ func (m *Mapper) PeerChangedResponse(
|
||||||
pol,
|
pol,
|
||||||
node,
|
node,
|
||||||
mapRequest.Version,
|
mapRequest.Version,
|
||||||
|
users,
|
||||||
peers,
|
peers,
|
||||||
changedNodes,
|
changedNodes,
|
||||||
m.cfg,
|
m.cfg,
|
||||||
|
@ -508,16 +520,17 @@ func appendPeerChanges(
|
||||||
pol *policy.ACLPolicy,
|
pol *policy.ACLPolicy,
|
||||||
node *types.Node,
|
node *types.Node,
|
||||||
capVer tailcfg.CapabilityVersion,
|
capVer tailcfg.CapabilityVersion,
|
||||||
|
users []types.User,
|
||||||
peers types.Nodes,
|
peers types.Nodes,
|
||||||
changed types.Nodes,
|
changed types.Nodes,
|
||||||
cfg *types.Config,
|
cfg *types.Config,
|
||||||
) error {
|
) error {
|
||||||
packetFilter, err := pol.CompileFilterRules(append(peers, node))
|
packetFilter, err := pol.CompileFilterRules(users, append(peers, node))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
sshPolicy, err := pol.CompileSSHPolicy(node, peers)
|
sshPolicy, err := pol.CompileSSHPolicy(node, users, peers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -159,6 +159,9 @@ func Test_fullMapResponse(t *testing.T) {
|
||||||
lastSeen := time.Date(2009, time.November, 10, 23, 9, 0, 0, time.UTC)
|
lastSeen := time.Date(2009, time.November, 10, 23, 9, 0, 0, time.UTC)
|
||||||
expire := time.Date(2500, time.November, 11, 23, 0, 0, 0, time.UTC)
|
expire := time.Date(2500, time.November, 11, 23, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
user1 := types.User{Model: gorm.Model{ID: 0}, Name: "mini"}
|
||||||
|
user2 := types.User{Model: gorm.Model{ID: 1}, Name: "peer2"}
|
||||||
|
|
||||||
mini := &types.Node{
|
mini := &types.Node{
|
||||||
ID: 0,
|
ID: 0,
|
||||||
MachineKey: mustMK(
|
MachineKey: mustMK(
|
||||||
|
@ -173,8 +176,8 @@ func Test_fullMapResponse(t *testing.T) {
|
||||||
IPv4: iap("100.64.0.1"),
|
IPv4: iap("100.64.0.1"),
|
||||||
Hostname: "mini",
|
Hostname: "mini",
|
||||||
GivenName: "mini",
|
GivenName: "mini",
|
||||||
UserID: 0,
|
UserID: user1.ID,
|
||||||
User: types.User{Name: "mini"},
|
User: user1,
|
||||||
ForcedTags: []string{},
|
ForcedTags: []string{},
|
||||||
AuthKey: &types.PreAuthKey{},
|
AuthKey: &types.PreAuthKey{},
|
||||||
LastSeen: &lastSeen,
|
LastSeen: &lastSeen,
|
||||||
|
@ -253,8 +256,8 @@ func Test_fullMapResponse(t *testing.T) {
|
||||||
IPv4: iap("100.64.0.2"),
|
IPv4: iap("100.64.0.2"),
|
||||||
Hostname: "peer1",
|
Hostname: "peer1",
|
||||||
GivenName: "peer1",
|
GivenName: "peer1",
|
||||||
UserID: 0,
|
UserID: user1.ID,
|
||||||
User: types.User{Name: "mini"},
|
User: user1,
|
||||||
ForcedTags: []string{},
|
ForcedTags: []string{},
|
||||||
LastSeen: &lastSeen,
|
LastSeen: &lastSeen,
|
||||||
Expiry: &expire,
|
Expiry: &expire,
|
||||||
|
@ -308,8 +311,8 @@ func Test_fullMapResponse(t *testing.T) {
|
||||||
IPv4: iap("100.64.0.3"),
|
IPv4: iap("100.64.0.3"),
|
||||||
Hostname: "peer2",
|
Hostname: "peer2",
|
||||||
GivenName: "peer2",
|
GivenName: "peer2",
|
||||||
UserID: 1,
|
UserID: user2.ID,
|
||||||
User: types.User{Name: "peer2"},
|
User: user2,
|
||||||
ForcedTags: []string{},
|
ForcedTags: []string{},
|
||||||
LastSeen: &lastSeen,
|
LastSeen: &lastSeen,
|
||||||
Expiry: &expire,
|
Expiry: &expire,
|
||||||
|
@ -468,6 +471,7 @@ func Test_fullMapResponse(t *testing.T) {
|
||||||
got, err := mappy.fullMapResponse(
|
got, err := mappy.fullMapResponse(
|
||||||
tt.node,
|
tt.node,
|
||||||
tt.peers,
|
tt.peers,
|
||||||
|
[]types.User{user1, user2},
|
||||||
tt.pol,
|
tt.pol,
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
|
|
|
@ -436,25 +436,42 @@ func (a *AuthProviderOIDC) createOrUpdateUserFromClaim(
|
||||||
) (*types.User, error) {
|
) (*types.User, error) {
|
||||||
var user *types.User
|
var user *types.User
|
||||||
var err error
|
var err error
|
||||||
user, err = a.db.GetUserByOIDCIdentifier(claims.Sub)
|
user, err = a.db.GetUserByOIDCIdentifier(claims.Identifier())
|
||||||
if err != nil && !errors.Is(err, db.ErrUserNotFound) {
|
if err != nil && !errors.Is(err, db.ErrUserNotFound) {
|
||||||
return nil, fmt.Errorf("creating or updating user: %w", err)
|
return nil, fmt.Errorf("creating or updating user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// This check is for legacy, if the user cannot be found by the OIDC identifier
|
// This check is for legacy, if the user cannot be found by the OIDC identifier
|
||||||
// look it up by username. This should only be needed once.
|
// look it up by username. This should only be needed once.
|
||||||
if user == nil {
|
// This branch will presist for a number of versions after the OIDC migration and
|
||||||
user, err = a.db.GetUserByName(claims.Username)
|
// then be removed following a deprecation.
|
||||||
if err != nil && !errors.Is(err, db.ErrUserNotFound) {
|
// TODO(kradalby): Remove when strip_email_domain and migration is removed
|
||||||
return nil, fmt.Errorf("creating or updating user: %w", err)
|
// after #2170 is cleaned up.
|
||||||
}
|
if a.cfg.MapLegacyUsers && user == nil {
|
||||||
|
log.Trace().Str("username", claims.Username).Str("sub", claims.Sub).Msg("user not found by OIDC identifier, looking up by username")
|
||||||
|
if oldUsername, err := getUserName(claims, a.cfg.StripEmaildomain); err == nil {
|
||||||
|
log.Trace().Str("old_username", oldUsername).Str("sub", claims.Sub).Msg("found username")
|
||||||
|
user, err = a.db.GetUserByName(oldUsername)
|
||||||
|
if err != nil && !errors.Is(err, db.ErrUserNotFound) {
|
||||||
|
return nil, fmt.Errorf("getting user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// if the user is still not found, create a new empty user.
|
// If the user exists, but it already has a provider identifier (OIDC sub), create a new user.
|
||||||
if user == nil {
|
// This is to prevent users that have already been migrated to the new OIDC format
|
||||||
user = &types.User{}
|
// to be updated with the new OIDC identifier inexplicitly which might be the cause of an
|
||||||
|
// account takeover.
|
||||||
|
if user != nil && user.ProviderIdentifier.Valid {
|
||||||
|
log.Info().Str("username", claims.Username).Str("sub", claims.Sub).Msg("user found by username, but has provider identifier, creating new user.")
|
||||||
|
user = &types.User{}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if the user is still not found, create a new empty user.
|
||||||
|
if user == nil {
|
||||||
|
user = &types.User{}
|
||||||
|
}
|
||||||
|
|
||||||
user.FromClaim(claims)
|
user.FromClaim(claims)
|
||||||
err = a.db.DB.Save(user).Error
|
err = a.db.DB.Save(user).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -488,7 +505,7 @@ func (a *AuthProviderOIDC) registerNode(
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(kradalby):
|
// TODO(kradalby):
|
||||||
// Rewrite in elem-go
|
// Rewrite in elem-go.
|
||||||
func renderOIDCCallbackTemplate(
|
func renderOIDCCallbackTemplate(
|
||||||
user *types.User,
|
user *types.User,
|
||||||
) (*bytes.Buffer, error) {
|
) (*bytes.Buffer, error) {
|
||||||
|
@ -502,3 +519,24 @@ func renderOIDCCallbackTemplate(
|
||||||
|
|
||||||
return &content, nil
|
return &content, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(kradalby): Reintroduce when strip_email_domain is removed
|
||||||
|
// after #2170 is cleaned up
|
||||||
|
// DEPRECATED: DO NOT USE
|
||||||
|
func getUserName(
|
||||||
|
claims *types.OIDCClaims,
|
||||||
|
stripEmaildomain bool,
|
||||||
|
) (string, error) {
|
||||||
|
if !claims.EmailVerified {
|
||||||
|
return "", fmt.Errorf("email not verified")
|
||||||
|
}
|
||||||
|
userName, err := util.NormalizeToFQDNRules(
|
||||||
|
claims.Email,
|
||||||
|
stripEmaildomain,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return userName, nil
|
||||||
|
}
|
||||||
|
|
|
@ -137,20 +137,21 @@ func GenerateFilterAndSSHRulesForTests(
|
||||||
policy *ACLPolicy,
|
policy *ACLPolicy,
|
||||||
node *types.Node,
|
node *types.Node,
|
||||||
peers types.Nodes,
|
peers types.Nodes,
|
||||||
|
users []types.User,
|
||||||
) ([]tailcfg.FilterRule, *tailcfg.SSHPolicy, error) {
|
) ([]tailcfg.FilterRule, *tailcfg.SSHPolicy, error) {
|
||||||
// If there is no policy defined, we default to allow all
|
// If there is no policy defined, we default to allow all
|
||||||
if policy == nil {
|
if policy == nil {
|
||||||
return tailcfg.FilterAllowAll, &tailcfg.SSHPolicy{}, nil
|
return tailcfg.FilterAllowAll, &tailcfg.SSHPolicy{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
rules, err := policy.CompileFilterRules(append(peers, node))
|
rules, err := policy.CompileFilterRules(users, append(peers, node))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []tailcfg.FilterRule{}, &tailcfg.SSHPolicy{}, err
|
return []tailcfg.FilterRule{}, &tailcfg.SSHPolicy{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Trace().Interface("ACL", rules).Str("node", node.GivenName).Msg("ACL rules")
|
log.Trace().Interface("ACL", rules).Str("node", node.GivenName).Msg("ACL rules")
|
||||||
|
|
||||||
sshPolicy, err := policy.CompileSSHPolicy(node, peers)
|
sshPolicy, err := policy.CompileSSHPolicy(node, users, peers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []tailcfg.FilterRule{}, &tailcfg.SSHPolicy{}, err
|
return []tailcfg.FilterRule{}, &tailcfg.SSHPolicy{}, err
|
||||||
}
|
}
|
||||||
|
@ -161,6 +162,7 @@ func GenerateFilterAndSSHRulesForTests(
|
||||||
// CompileFilterRules takes a set of nodes and an ACLPolicy and generates a
|
// CompileFilterRules takes a set of nodes and an ACLPolicy and generates a
|
||||||
// set of Tailscale compatible FilterRules used to allow traffic on clients.
|
// set of Tailscale compatible FilterRules used to allow traffic on clients.
|
||||||
func (pol *ACLPolicy) CompileFilterRules(
|
func (pol *ACLPolicy) CompileFilterRules(
|
||||||
|
users []types.User,
|
||||||
nodes types.Nodes,
|
nodes types.Nodes,
|
||||||
) ([]tailcfg.FilterRule, error) {
|
) ([]tailcfg.FilterRule, error) {
|
||||||
if pol == nil {
|
if pol == nil {
|
||||||
|
@ -176,9 +178,14 @@ func (pol *ACLPolicy) CompileFilterRules(
|
||||||
|
|
||||||
var srcIPs []string
|
var srcIPs []string
|
||||||
for srcIndex, src := range acl.Sources {
|
for srcIndex, src := range acl.Sources {
|
||||||
srcs, err := pol.expandSource(src, nodes)
|
srcs, err := pol.expandSource(src, users, nodes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parsing policy, acl index: %d->%d: %w", index, srcIndex, err)
|
return nil, fmt.Errorf(
|
||||||
|
"parsing policy, acl index: %d->%d: %w",
|
||||||
|
index,
|
||||||
|
srcIndex,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
srcIPs = append(srcIPs, srcs...)
|
srcIPs = append(srcIPs, srcs...)
|
||||||
}
|
}
|
||||||
|
@ -197,6 +204,7 @@ func (pol *ACLPolicy) CompileFilterRules(
|
||||||
|
|
||||||
expanded, err := pol.ExpandAlias(
|
expanded, err := pol.ExpandAlias(
|
||||||
nodes,
|
nodes,
|
||||||
|
users,
|
||||||
alias,
|
alias,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -281,6 +289,7 @@ func ReduceFilterRules(node *types.Node, rules []tailcfg.FilterRule) []tailcfg.F
|
||||||
|
|
||||||
func (pol *ACLPolicy) CompileSSHPolicy(
|
func (pol *ACLPolicy) CompileSSHPolicy(
|
||||||
node *types.Node,
|
node *types.Node,
|
||||||
|
users []types.User,
|
||||||
peers types.Nodes,
|
peers types.Nodes,
|
||||||
) (*tailcfg.SSHPolicy, error) {
|
) (*tailcfg.SSHPolicy, error) {
|
||||||
if pol == nil {
|
if pol == nil {
|
||||||
|
@ -312,7 +321,7 @@ func (pol *ACLPolicy) CompileSSHPolicy(
|
||||||
for index, sshACL := range pol.SSHs {
|
for index, sshACL := range pol.SSHs {
|
||||||
var dest netipx.IPSetBuilder
|
var dest netipx.IPSetBuilder
|
||||||
for _, src := range sshACL.Destinations {
|
for _, src := range sshACL.Destinations {
|
||||||
expanded, err := pol.ExpandAlias(append(peers, node), src)
|
expanded, err := pol.ExpandAlias(append(peers, node), users, src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -335,12 +344,21 @@ func (pol *ACLPolicy) CompileSSHPolicy(
|
||||||
case "check":
|
case "check":
|
||||||
checkAction, err := sshCheckAction(sshACL.CheckPeriod)
|
checkAction, err := sshCheckAction(sshACL.CheckPeriod)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parsing SSH policy, parsing check duration, index: %d: %w", index, err)
|
return nil, fmt.Errorf(
|
||||||
|
"parsing SSH policy, parsing check duration, index: %d: %w",
|
||||||
|
index,
|
||||||
|
err,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
action = *checkAction
|
action = *checkAction
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("parsing SSH policy, unknown action %q, index: %d: %w", sshACL.Action, index, err)
|
return nil, fmt.Errorf(
|
||||||
|
"parsing SSH policy, unknown action %q, index: %d: %w",
|
||||||
|
sshACL.Action,
|
||||||
|
index,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
principals := make([]*tailcfg.SSHPrincipal, 0, len(sshACL.Sources))
|
principals := make([]*tailcfg.SSHPrincipal, 0, len(sshACL.Sources))
|
||||||
|
@ -363,6 +381,7 @@ func (pol *ACLPolicy) CompileSSHPolicy(
|
||||||
} else {
|
} else {
|
||||||
expandedSrcs, err := pol.ExpandAlias(
|
expandedSrcs, err := pol.ExpandAlias(
|
||||||
peers,
|
peers,
|
||||||
|
users,
|
||||||
rawSrc,
|
rawSrc,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -512,9 +531,10 @@ func parseProtocol(protocol string) ([]int, bool, error) {
|
||||||
// with the given src alias.
|
// with the given src alias.
|
||||||
func (pol *ACLPolicy) expandSource(
|
func (pol *ACLPolicy) expandSource(
|
||||||
src string,
|
src string,
|
||||||
|
users []types.User,
|
||||||
nodes types.Nodes,
|
nodes types.Nodes,
|
||||||
) ([]string, error) {
|
) ([]string, error) {
|
||||||
ipSet, err := pol.ExpandAlias(nodes, src)
|
ipSet, err := pol.ExpandAlias(nodes, users, src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []string{}, err
|
return []string{}, err
|
||||||
}
|
}
|
||||||
|
@ -538,6 +558,7 @@ func (pol *ACLPolicy) expandSource(
|
||||||
// and transform these in IPAddresses.
|
// and transform these in IPAddresses.
|
||||||
func (pol *ACLPolicy) ExpandAlias(
|
func (pol *ACLPolicy) ExpandAlias(
|
||||||
nodes types.Nodes,
|
nodes types.Nodes,
|
||||||
|
users []types.User,
|
||||||
alias string,
|
alias string,
|
||||||
) (*netipx.IPSet, error) {
|
) (*netipx.IPSet, error) {
|
||||||
if isWildcard(alias) {
|
if isWildcard(alias) {
|
||||||
|
@ -552,12 +573,12 @@ func (pol *ACLPolicy) ExpandAlias(
|
||||||
|
|
||||||
// if alias is a group
|
// if alias is a group
|
||||||
if isGroup(alias) {
|
if isGroup(alias) {
|
||||||
return pol.expandIPsFromGroup(alias, nodes)
|
return pol.expandIPsFromGroup(alias, users, nodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if alias is a tag
|
// if alias is a tag
|
||||||
if isTag(alias) {
|
if isTag(alias) {
|
||||||
return pol.expandIPsFromTag(alias, nodes)
|
return pol.expandIPsFromTag(alias, users, nodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
if isAutoGroup(alias) {
|
if isAutoGroup(alias) {
|
||||||
|
@ -565,7 +586,7 @@ func (pol *ACLPolicy) ExpandAlias(
|
||||||
}
|
}
|
||||||
|
|
||||||
// if alias is a user
|
// if alias is a user
|
||||||
if ips, err := pol.expandIPsFromUser(alias, nodes); ips != nil {
|
if ips, err := pol.expandIPsFromUser(alias, users, nodes); ips != nil {
|
||||||
return ips, err
|
return ips, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -574,7 +595,7 @@ func (pol *ACLPolicy) ExpandAlias(
|
||||||
if h, ok := pol.Hosts[alias]; ok {
|
if h, ok := pol.Hosts[alias]; ok {
|
||||||
log.Trace().Str("host", h.String()).Msg("ExpandAlias got hosts entry")
|
log.Trace().Str("host", h.String()).Msg("ExpandAlias got hosts entry")
|
||||||
|
|
||||||
return pol.ExpandAlias(nodes, h.String())
|
return pol.ExpandAlias(nodes, users, h.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// if alias is an IP
|
// if alias is an IP
|
||||||
|
@ -599,7 +620,7 @@ func (pol *ACLPolicy) ExpandAlias(
|
||||||
// TODO(kradalby): It is quite hard to understand what this function is doing,
|
// TODO(kradalby): It is quite hard to understand what this function is doing,
|
||||||
// it seems like it trying to ensure that we dont include nodes that are tagged
|
// it seems like it trying to ensure that we dont include nodes that are tagged
|
||||||
// when we look up the nodes owned by a user.
|
// when we look up the nodes owned by a user.
|
||||||
// This should be refactored to be more clear as part of the Tags work in #1369
|
// This should be refactored to be more clear as part of the Tags work in #1369.
|
||||||
func excludeCorrectlyTaggedNodes(
|
func excludeCorrectlyTaggedNodes(
|
||||||
aclPolicy *ACLPolicy,
|
aclPolicy *ACLPolicy,
|
||||||
nodes types.Nodes,
|
nodes types.Nodes,
|
||||||
|
@ -751,16 +772,17 @@ func (pol *ACLPolicy) expandUsersFromGroup(
|
||||||
|
|
||||||
func (pol *ACLPolicy) expandIPsFromGroup(
|
func (pol *ACLPolicy) expandIPsFromGroup(
|
||||||
group string,
|
group string,
|
||||||
|
users []types.User,
|
||||||
nodes types.Nodes,
|
nodes types.Nodes,
|
||||||
) (*netipx.IPSet, error) {
|
) (*netipx.IPSet, error) {
|
||||||
var build netipx.IPSetBuilder
|
var build netipx.IPSetBuilder
|
||||||
|
|
||||||
users, err := pol.expandUsersFromGroup(group)
|
userTokens, err := pol.expandUsersFromGroup(group)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &netipx.IPSet{}, err
|
return &netipx.IPSet{}, err
|
||||||
}
|
}
|
||||||
for _, user := range users {
|
for _, user := range userTokens {
|
||||||
filteredNodes := filterNodesByUser(nodes, user)
|
filteredNodes := filterNodesByUser(nodes, users, user)
|
||||||
for _, node := range filteredNodes {
|
for _, node := range filteredNodes {
|
||||||
node.AppendToIPSet(&build)
|
node.AppendToIPSet(&build)
|
||||||
}
|
}
|
||||||
|
@ -771,6 +793,7 @@ func (pol *ACLPolicy) expandIPsFromGroup(
|
||||||
|
|
||||||
func (pol *ACLPolicy) expandIPsFromTag(
|
func (pol *ACLPolicy) expandIPsFromTag(
|
||||||
alias string,
|
alias string,
|
||||||
|
users []types.User,
|
||||||
nodes types.Nodes,
|
nodes types.Nodes,
|
||||||
) (*netipx.IPSet, error) {
|
) (*netipx.IPSet, error) {
|
||||||
var build netipx.IPSetBuilder
|
var build netipx.IPSetBuilder
|
||||||
|
@ -803,7 +826,7 @@ func (pol *ACLPolicy) expandIPsFromTag(
|
||||||
|
|
||||||
// filter out nodes per tag owner
|
// filter out nodes per tag owner
|
||||||
for _, user := range owners {
|
for _, user := range owners {
|
||||||
nodes := filterNodesByUser(nodes, user)
|
nodes := filterNodesByUser(nodes, users, user)
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
if node.Hostinfo == nil {
|
if node.Hostinfo == nil {
|
||||||
continue
|
continue
|
||||||
|
@ -820,11 +843,12 @@ func (pol *ACLPolicy) expandIPsFromTag(
|
||||||
|
|
||||||
func (pol *ACLPolicy) expandIPsFromUser(
|
func (pol *ACLPolicy) expandIPsFromUser(
|
||||||
user string,
|
user string,
|
||||||
|
users []types.User,
|
||||||
nodes types.Nodes,
|
nodes types.Nodes,
|
||||||
) (*netipx.IPSet, error) {
|
) (*netipx.IPSet, error) {
|
||||||
var build netipx.IPSetBuilder
|
var build netipx.IPSetBuilder
|
||||||
|
|
||||||
filteredNodes := filterNodesByUser(nodes, user)
|
filteredNodes := filterNodesByUser(nodes, users, user)
|
||||||
filteredNodes = excludeCorrectlyTaggedNodes(pol, filteredNodes, user)
|
filteredNodes = excludeCorrectlyTaggedNodes(pol, filteredNodes, user)
|
||||||
|
|
||||||
// shortcurcuit if we have no nodes to get ips from.
|
// shortcurcuit if we have no nodes to get ips from.
|
||||||
|
@ -953,10 +977,43 @@ func (pol *ACLPolicy) TagsOfNode(
|
||||||
return validTags, invalidTags
|
return validTags, invalidTags
|
||||||
}
|
}
|
||||||
|
|
||||||
func filterNodesByUser(nodes types.Nodes, user string) types.Nodes {
|
// filterNodesByUser returns a list of nodes that match the given userToken from a
|
||||||
|
// policy.
|
||||||
|
// Matching nodes are determined by first matching the user token to a user by checking:
|
||||||
|
// - If it is an ID that mactches the user database ID
|
||||||
|
// - It is the Provider Identifier from OIDC
|
||||||
|
// - It matches the username or email of a user
|
||||||
|
//
|
||||||
|
// If the token matches more than one user, zero nodes will returned.
|
||||||
|
func filterNodesByUser(nodes types.Nodes, users []types.User, userToken string) types.Nodes {
|
||||||
var out types.Nodes
|
var out types.Nodes
|
||||||
|
|
||||||
|
var potentialUsers []types.User
|
||||||
|
for _, user := range users {
|
||||||
|
if user.ProviderIdentifier.Valid && user.ProviderIdentifier.String == userToken {
|
||||||
|
// If a user is matching with a known unique field,
|
||||||
|
// disgard all other users and only keep the current
|
||||||
|
// user.
|
||||||
|
potentialUsers = []types.User{user}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if user.Email == userToken {
|
||||||
|
potentialUsers = append(potentialUsers, user)
|
||||||
|
}
|
||||||
|
if user.Name == userToken {
|
||||||
|
potentialUsers = append(potentialUsers, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(potentialUsers) != 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
user := potentialUsers[0]
|
||||||
|
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
if node.User.Username() == user {
|
if node.User.ID == user.ID {
|
||||||
out = append(out, node)
|
out = append(out, node)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -977,10 +1034,7 @@ func FilterNodesByACL(
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Checking if %s can access %s", node.Hostname, peer.Hostname)
|
|
||||||
|
|
||||||
if node.CanAccess(filter, nodes[index]) || peer.CanAccess(filter, node) {
|
if node.CanAccess(filter, nodes[index]) || peer.CanAccess(filter, node) {
|
||||||
log.Printf("CAN ACCESS %s can access %s", node.Hostname, peer.Hostname)
|
|
||||||
result = append(result, peer)
|
result = append(result, peer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
180
hscontrol/policyv2/filter.go
Normal file
180
hscontrol/policyv2/filter.go
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
package policyv2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
|
"go4.org/netipx"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidAction = errors.New("invalid action")
|
||||||
|
)
|
||||||
|
|
||||||
|
// CompileFilterRules takes a set of nodes and an ACLPolicy and generates a
|
||||||
|
// set of Tailscale compatible FilterRules used to allow traffic on clients.
|
||||||
|
func (pol *Policy) CompileFilterRules(
|
||||||
|
users types.Users,
|
||||||
|
nodes types.Nodes,
|
||||||
|
) ([]tailcfg.FilterRule, error) {
|
||||||
|
if pol == nil {
|
||||||
|
return tailcfg.FilterAllowAll, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var rules []tailcfg.FilterRule
|
||||||
|
|
||||||
|
for _, acl := range pol.ACLs {
|
||||||
|
if acl.Action != "accept" {
|
||||||
|
return nil, ErrInvalidAction
|
||||||
|
}
|
||||||
|
|
||||||
|
srcIPs, err := acl.Sources.Resolve(pol, users, nodes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("resolving source ips: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(kradalby): integrate type into schema
|
||||||
|
// TODO(kradalby): figure out the _ is wildcard stuff
|
||||||
|
protocols, _, err := parseProtocol(acl.Protocol)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing policy, protocol err: %w ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var destPorts []tailcfg.NetPortRange
|
||||||
|
for _, dest := range acl.Destinations {
|
||||||
|
ips, err := dest.Alias.Resolve(pol, users, nodes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pref := range ips.Prefixes() {
|
||||||
|
for _, port := range dest.Ports {
|
||||||
|
pr := tailcfg.NetPortRange{
|
||||||
|
IP: pref.String(),
|
||||||
|
Ports: port,
|
||||||
|
}
|
||||||
|
destPorts = append(destPorts, pr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rules = append(rules, tailcfg.FilterRule{
|
||||||
|
SrcIPs: ipSetToPrefixStringList(srcIPs),
|
||||||
|
DstPorts: destPorts,
|
||||||
|
IPProto: protocols,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return rules, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sshAction(accept bool, duration time.Duration) tailcfg.SSHAction {
|
||||||
|
return tailcfg.SSHAction{
|
||||||
|
Reject: !accept,
|
||||||
|
Accept: accept,
|
||||||
|
SessionDuration: duration,
|
||||||
|
AllowAgentForwarding: true,
|
||||||
|
AllowLocalPortForwarding: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pol *Policy) CompileSSHPolicy(
|
||||||
|
users types.Users,
|
||||||
|
node types.Node,
|
||||||
|
nodes types.Nodes,
|
||||||
|
) (*tailcfg.SSHPolicy, error) {
|
||||||
|
if pol == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var rules []*tailcfg.SSHRule
|
||||||
|
|
||||||
|
for index, rule := range pol.SSHs {
|
||||||
|
var dest netipx.IPSetBuilder
|
||||||
|
for _, src := range rule.Destinations {
|
||||||
|
ips, err := src.Resolve(pol, users, nodes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dest.AddSet(ips)
|
||||||
|
}
|
||||||
|
|
||||||
|
destSet, err := dest.IPSet()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !node.InIPSet(destSet) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var action tailcfg.SSHAction
|
||||||
|
switch rule.Action {
|
||||||
|
case "accept":
|
||||||
|
action = sshAction(true, 0)
|
||||||
|
case "check":
|
||||||
|
action = sshAction(true, rule.CheckPeriod)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("parsing SSH policy, unknown action %q, index: %d: %w", rule.Action, index, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var principals []*tailcfg.SSHPrincipal
|
||||||
|
for _, src := range rule.Sources {
|
||||||
|
if isWildcard(rawSrc) {
|
||||||
|
principals = append(principals, &tailcfg.SSHPrincipal{
|
||||||
|
Any: true,
|
||||||
|
})
|
||||||
|
} else if isGroup(rawSrc) {
|
||||||
|
users, err := pol.expandUsersFromGroup(rawSrc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing SSH policy, expanding user from group, index: %d->%d: %w", index, innerIndex, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, user := range users {
|
||||||
|
principals = append(principals, &tailcfg.SSHPrincipal{
|
||||||
|
UserLogin: user,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
expandedSrcs, err := pol.ExpandAlias(
|
||||||
|
peers,
|
||||||
|
rawSrc,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing SSH policy, expanding alias, index: %d->%d: %w", index, innerIndex, err)
|
||||||
|
}
|
||||||
|
for _, expandedSrc := range expandedSrcs.Prefixes() {
|
||||||
|
principals = append(principals, &tailcfg.SSHPrincipal{
|
||||||
|
NodeIP: expandedSrc.Addr().String(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userMap := make(map[string]string, len(rule.Users))
|
||||||
|
for _, user := range rule.Users {
|
||||||
|
userMap[user] = "="
|
||||||
|
}
|
||||||
|
rules = append(rules, &tailcfg.SSHRule{
|
||||||
|
Principals: principals,
|
||||||
|
SSHUsers: userMap,
|
||||||
|
Action: &action,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &tailcfg.SSHPolicy{
|
||||||
|
Rules: rules,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ipSetToPrefixStringList(ips *netipx.IPSet) []string {
|
||||||
|
var out []string
|
||||||
|
|
||||||
|
for _, pref := range ips.Prefixes() {
|
||||||
|
out = append(out, pref.String())
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
1127
hscontrol/policyv2/filter_test.go
Normal file
1127
hscontrol/policyv2/filter_test.go
Normal file
File diff suppressed because it is too large
Load diff
80
hscontrol/policyv2/policy.go
Normal file
80
hscontrol/policyv2/policy.go
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
package policyv2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PolicyManager struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
pol *Policy
|
||||||
|
users []types.User
|
||||||
|
nodes types.Nodes
|
||||||
|
|
||||||
|
filter []tailcfg.FilterRule
|
||||||
|
|
||||||
|
// TODO(kradalby): Implement SSH policy
|
||||||
|
sshPolicy *tailcfg.SSHPolicy
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPolicyManager creates a new PolicyManager from a policy file and a list of users and nodes.
|
||||||
|
// It returns an error if the policy file is invalid.
|
||||||
|
// The policy manager will update the filter rules based on the users and nodes.
|
||||||
|
func NewPolicyManager(b []byte, users []types.User, nodes types.Nodes) (*PolicyManager, error) {
|
||||||
|
policy, err := policyFromBytes(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing policy: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pm := PolicyManager{
|
||||||
|
pol: policy,
|
||||||
|
users: users,
|
||||||
|
nodes: nodes,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = pm.updateLocked()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pm, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter returns the current filter rules for the entire tailnet.
|
||||||
|
func (pm *PolicyManager) Filter() []tailcfg.FilterRule {
|
||||||
|
pm.mu.Lock()
|
||||||
|
defer pm.mu.Unlock()
|
||||||
|
return pm.filter
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateLocked updates the filter rules based on the current policy and nodes.
|
||||||
|
// It must be called with the lock held.
|
||||||
|
func (pm *PolicyManager) updateLocked() error {
|
||||||
|
filter, err := pm.pol.CompileFilterRules(pm.users, pm.nodes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("compiling filter rules: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pm.filter = filter
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUsers updates the users in the policy manager and updates the filter rules.
|
||||||
|
func (pm *PolicyManager) SetUsers(users []types.User) error {
|
||||||
|
pm.mu.Lock()
|
||||||
|
defer pm.mu.Unlock()
|
||||||
|
pm.users = users
|
||||||
|
return pm.updateLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNodes updates the nodes in the policy manager and updates the filter rules.
|
||||||
|
func (pm *PolicyManager) SetNodes(nodes types.Nodes) error {
|
||||||
|
pm.mu.Lock()
|
||||||
|
defer pm.mu.Unlock()
|
||||||
|
pm.nodes = nodes
|
||||||
|
return pm.updateLocked()
|
||||||
|
}
|
58
hscontrol/policyv2/policy_test.go
Normal file
58
hscontrol/policyv2/policy_test.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package policyv2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
|
)
|
||||||
|
|
||||||
|
func node(name, ipv4, ipv6 string, user types.User, hostinfo *tailcfg.Hostinfo) *types.Node {
|
||||||
|
return &types.Node{
|
||||||
|
ID: 0,
|
||||||
|
Hostname: name,
|
||||||
|
IPv4: ap(ipv4),
|
||||||
|
IPv6: ap(ipv6),
|
||||||
|
User: user,
|
||||||
|
UserID: user.ID,
|
||||||
|
Hostinfo: hostinfo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPolicyManager(t *testing.T) {
|
||||||
|
users := types.Users{
|
||||||
|
{Model: gorm.Model{ID: 1}, Name: "testuser", Email: "testuser@headscale.net"},
|
||||||
|
{Model: gorm.Model{ID: 2}, Name: "otheruser", Email: "otheruser@headscale.net"},
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
pol string
|
||||||
|
nodes types.Nodes
|
||||||
|
wantFilter []tailcfg.FilterRule
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty-policy",
|
||||||
|
pol: "{}",
|
||||||
|
nodes: types.Nodes{},
|
||||||
|
wantFilter: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
pm, err := NewPolicyManager([]byte(tt.pol), users, tt.nodes)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
filter := pm.Filter()
|
||||||
|
if diff := cmp.Diff(filter, tt.wantFilter); diff != "" {
|
||||||
|
t.Errorf("Filter() mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(kradalby): Test SSH Policy
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
821
hscontrol/policyv2/types.go
Normal file
821
hscontrol/policyv2/types.go
Normal file
|
@ -0,0 +1,821 @@
|
||||||
|
package policyv2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
|
"github.com/juanfont/headscale/hscontrol/util"
|
||||||
|
"github.com/tailscale/hujson"
|
||||||
|
"go4.org/netipx"
|
||||||
|
"tailscale.com/net/tsaddr"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
|
"tailscale.com/types/ptr"
|
||||||
|
)
|
||||||
|
|
||||||
|
var theInternetSet *netipx.IPSet
|
||||||
|
|
||||||
|
// theInternet returns the IPSet for the Internet.
|
||||||
|
// https://www.youtube.com/watch?v=iDbyYGrswtg
|
||||||
|
func theInternet() *netipx.IPSet {
|
||||||
|
if theInternetSet != nil {
|
||||||
|
return theInternetSet
|
||||||
|
}
|
||||||
|
|
||||||
|
var internetBuilder netipx.IPSetBuilder
|
||||||
|
internetBuilder.AddPrefix(netip.MustParsePrefix("2000::/3"))
|
||||||
|
internetBuilder.AddPrefix(tsaddr.AllIPv4())
|
||||||
|
|
||||||
|
// Delete Private network addresses
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc1918
|
||||||
|
internetBuilder.RemovePrefix(netip.MustParsePrefix("fc00::/7"))
|
||||||
|
internetBuilder.RemovePrefix(netip.MustParsePrefix("10.0.0.0/8"))
|
||||||
|
internetBuilder.RemovePrefix(netip.MustParsePrefix("172.16.0.0/12"))
|
||||||
|
internetBuilder.RemovePrefix(netip.MustParsePrefix("192.168.0.0/16"))
|
||||||
|
|
||||||
|
// Delete Tailscale networks
|
||||||
|
internetBuilder.RemovePrefix(tsaddr.TailscaleULARange())
|
||||||
|
internetBuilder.RemovePrefix(tsaddr.CGNATRange())
|
||||||
|
|
||||||
|
// Delete "cant find DHCP networks"
|
||||||
|
internetBuilder.RemovePrefix(netip.MustParsePrefix("fe80::/10")) // link-loca
|
||||||
|
internetBuilder.RemovePrefix(netip.MustParsePrefix("169.254.0.0/16"))
|
||||||
|
|
||||||
|
theInternetSet, _ := internetBuilder.IPSet()
|
||||||
|
return theInternetSet
|
||||||
|
}
|
||||||
|
|
||||||
|
type Asterix int
|
||||||
|
|
||||||
|
func (a Asterix) Validate() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Asterix) String() string {
|
||||||
|
return "*"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Asterix) UnmarshalJSON(b []byte) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Asterix) Resolve(_ *Policy, _ types.Users, nodes types.Nodes) (*netipx.IPSet, error) {
|
||||||
|
var ips netipx.IPSetBuilder
|
||||||
|
|
||||||
|
ips.AddPrefix(tsaddr.AllIPv4())
|
||||||
|
ips.AddPrefix(tsaddr.AllIPv6())
|
||||||
|
|
||||||
|
return ips.IPSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Username is a string that represents a username, it must contain an @.
|
||||||
|
type Username string
|
||||||
|
|
||||||
|
func (u Username) Validate() error {
|
||||||
|
if strings.Contains(string(u), "@") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("Username has to contain @, got: %q", u)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Username) String() string {
|
||||||
|
return string(*u)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Username) UnmarshalJSON(b []byte) error {
|
||||||
|
*u = Username(strings.Trim(string(b), `"`))
|
||||||
|
if err := u.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u Username) CanBeTagOwner() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u Username) resolveUser(users types.Users) (*types.User, error) {
|
||||||
|
var potentialUsers types.Users
|
||||||
|
for _, user := range users {
|
||||||
|
if user.ProviderIdentifier == string(u) {
|
||||||
|
potentialUsers = append(potentialUsers, user)
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if user.Email == string(u) {
|
||||||
|
potentialUsers = append(potentialUsers, user)
|
||||||
|
}
|
||||||
|
if user.Name == string(u) {
|
||||||
|
potentialUsers = append(potentialUsers, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(potentialUsers) > 1 {
|
||||||
|
return nil, fmt.Errorf("unable to resolve user identifier to distinct: %s matched multiple %s", u, potentialUsers)
|
||||||
|
} else if len(potentialUsers) == 0 {
|
||||||
|
return nil, fmt.Errorf("unable to resolve user identifier, no user found: %s not in %s", u, users)
|
||||||
|
}
|
||||||
|
|
||||||
|
user := potentialUsers[0]
|
||||||
|
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u Username) Resolve(_ *Policy, users types.Users, nodes types.Nodes) (*netipx.IPSet, error) {
|
||||||
|
var ips netipx.IPSetBuilder
|
||||||
|
|
||||||
|
user, err := u.resolveUser(users)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, node := range nodes {
|
||||||
|
if node.IsTagged() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.User.ID == user.ID {
|
||||||
|
node.AppendToIPSet(&ips)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ips.IPSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group is a special string which is always prefixed with `group:`
|
||||||
|
type Group string
|
||||||
|
|
||||||
|
func (g Group) Validate() error {
|
||||||
|
if strings.HasPrefix(string(g), "group:") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf(`Group has to start with "group:", got: %q`, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) UnmarshalJSON(b []byte) error {
|
||||||
|
*g = Group(strings.Trim(string(b), `"`))
|
||||||
|
if err := g.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g Group) CanBeTagOwner() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g Group) Resolve(p *Policy, users types.Users, nodes types.Nodes) (*netipx.IPSet, error) {
|
||||||
|
var ips netipx.IPSetBuilder
|
||||||
|
|
||||||
|
for _, user := range p.Groups[g] {
|
||||||
|
uips, err := user.Resolve(nil, users, nodes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ips.AddSet(uips)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ips.IPSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag is a special string which is always prefixed with `tag:`
|
||||||
|
type Tag string
|
||||||
|
|
||||||
|
func (t Tag) Validate() error {
|
||||||
|
if strings.HasPrefix(string(t), "tag:") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf(`tag has to start with "tag:", got: %q`, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tag) UnmarshalJSON(b []byte) error {
|
||||||
|
*t = Tag(strings.Trim(string(b), `"`))
|
||||||
|
if err := t.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tag) Resolve(p *Policy, _ types.Users, nodes types.Nodes) (*netipx.IPSet, error) {
|
||||||
|
var ips netipx.IPSetBuilder
|
||||||
|
|
||||||
|
for _, node := range nodes {
|
||||||
|
if node.HasTag(string(t)) {
|
||||||
|
node.AppendToIPSet(&ips)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ips.IPSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Host is a string that represents a hostname.
|
||||||
|
type Host string
|
||||||
|
|
||||||
|
func (h Host) Validate() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Host) UnmarshalJSON(b []byte) error {
|
||||||
|
*h = Host(strings.Trim(string(b), `"`))
|
||||||
|
if err := h.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Host) Resolve(p *Policy, _ types.Users, nodes types.Nodes) (*netipx.IPSet, error) {
|
||||||
|
var ips netipx.IPSetBuilder
|
||||||
|
|
||||||
|
pref, ok := p.Hosts[h]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unable to resolve host: %q", h)
|
||||||
|
}
|
||||||
|
err := pref.Validate()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the IP is a single host, look for a node to ensure we add all the IPs of
|
||||||
|
// the node to the IPSet.
|
||||||
|
appendIfNodeHasIP(nodes, &ips, pref)
|
||||||
|
ips.AddPrefix(netip.Prefix(pref))
|
||||||
|
|
||||||
|
return ips.IPSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendIfNodeHasIP(nodes types.Nodes, ips *netipx.IPSetBuilder, pref Prefix) {
|
||||||
|
if netip.Prefix(pref).IsSingleIP() {
|
||||||
|
addr := netip.Prefix(pref).Addr()
|
||||||
|
for _, node := range nodes {
|
||||||
|
if node.HasIP(addr) {
|
||||||
|
node.AppendToIPSet(ips)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Prefix netip.Prefix
|
||||||
|
|
||||||
|
func (p Prefix) Validate() error {
|
||||||
|
if !netip.Prefix(p).IsValid() {
|
||||||
|
return fmt.Errorf("Prefix %q is invalid", p)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Prefix) String() string {
|
||||||
|
return netip.Prefix(p).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Prefix) parseString(addr string) error {
|
||||||
|
if !strings.Contains(addr, "/") {
|
||||||
|
addr, err := netip.ParseAddr(addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
addrPref, err := addr.Prefix(addr.BitLen())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = Prefix(addrPref)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pref, err := netip.ParsePrefix(addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*p = Prefix(pref)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Prefix) UnmarshalJSON(b []byte) error {
|
||||||
|
err := p.parseString(strings.Trim(string(b), `"`))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := p.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Prefix) Resolve(_ *Policy, _ types.Users, nodes types.Nodes) (*netipx.IPSet, error) {
|
||||||
|
var ips netipx.IPSetBuilder
|
||||||
|
|
||||||
|
appendIfNodeHasIP(nodes, &ips, p)
|
||||||
|
ips.AddPrefix(netip.Prefix(p))
|
||||||
|
|
||||||
|
return ips.IPSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutoGroup is a special string which is always prefixed with `autogroup:`
|
||||||
|
type AutoGroup string
|
||||||
|
|
||||||
|
const (
|
||||||
|
AutoGroupInternet = "autogroup:internet"
|
||||||
|
)
|
||||||
|
|
||||||
|
var autogroups = []string{AutoGroupInternet}
|
||||||
|
|
||||||
|
func (ag AutoGroup) Validate() error {
|
||||||
|
for _, valid := range autogroups {
|
||||||
|
if valid == string(ag) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("AutoGroup is invalid, got: %q, must be one of %v", ag, autogroups)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ag *AutoGroup) UnmarshalJSON(b []byte) error {
|
||||||
|
*ag = AutoGroup(strings.Trim(string(b), `"`))
|
||||||
|
if err := ag.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ag AutoGroup) Resolve(_ *Policy, _ types.Users, _ types.Nodes) (*netipx.IPSet, error) {
|
||||||
|
switch ag {
|
||||||
|
case AutoGroupInternet:
|
||||||
|
return theInternet(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Alias interface {
|
||||||
|
Validate() error
|
||||||
|
UnmarshalJSON([]byte) error
|
||||||
|
Resolve(*Policy, types.Users, types.Nodes) (*netipx.IPSet, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type AliasWithPorts struct {
|
||||||
|
Alias
|
||||||
|
Ports []tailcfg.PortRange
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ve *AliasWithPorts) UnmarshalJSON(b []byte) error {
|
||||||
|
// TODO(kradalby): use encoding/json/v2 (go-json-experiment)
|
||||||
|
dec := json.NewDecoder(bytes.NewReader(b))
|
||||||
|
var v any
|
||||||
|
if err := dec.Decode(&v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch vs := v.(type) {
|
||||||
|
case string:
|
||||||
|
var portsPart string
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if strings.Contains(vs, ":") {
|
||||||
|
vs, portsPart, err = splitDestination(vs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ports, err := parsePorts(portsPart)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ve.Ports = ports
|
||||||
|
}
|
||||||
|
|
||||||
|
ve.Alias = parseAlias(vs)
|
||||||
|
if ve.Alias == nil {
|
||||||
|
return fmt.Errorf("could not determine the type of %q", vs)
|
||||||
|
}
|
||||||
|
if err := ve.Alias.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("type %T not supported", vs)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAlias(vs string) Alias {
|
||||||
|
// case netip.Addr:
|
||||||
|
// ve.Alias = Addr(val)
|
||||||
|
// case netip.Prefix:
|
||||||
|
// ve.Alias = Prefix(val)
|
||||||
|
var pref Prefix
|
||||||
|
err := pref.parseString(vs)
|
||||||
|
if err == nil {
|
||||||
|
return &pref
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case vs == "*":
|
||||||
|
return Asterix(0)
|
||||||
|
case strings.Contains(vs, "@"):
|
||||||
|
return ptr.To(Username(vs))
|
||||||
|
case strings.HasPrefix(vs, "group:"):
|
||||||
|
return ptr.To(Group(vs))
|
||||||
|
case strings.HasPrefix(vs, "tag:"):
|
||||||
|
return ptr.To(Tag(vs))
|
||||||
|
case strings.HasPrefix(vs, "autogroup:"):
|
||||||
|
return ptr.To(AutoGroup(vs))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(vs, "@") && !strings.Contains(vs, ":") {
|
||||||
|
return ptr.To(Host(vs))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AliasEnc is used to deserialize a Alias.
|
||||||
|
type AliasEnc struct{ Alias }
|
||||||
|
|
||||||
|
func (ve *AliasEnc) UnmarshalJSON(b []byte) error {
|
||||||
|
// TODO(kradalby): use encoding/json/v2 (go-json-experiment)
|
||||||
|
dec := json.NewDecoder(bytes.NewReader(b))
|
||||||
|
var v any
|
||||||
|
if err := dec.Decode(&v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch val := v.(type) {
|
||||||
|
case string:
|
||||||
|
ve.Alias = parseAlias(val)
|
||||||
|
if ve.Alias == nil {
|
||||||
|
return fmt.Errorf("could not determine the type of %q", val)
|
||||||
|
}
|
||||||
|
if err := ve.Alias.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("type %T not supported", val)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Aliases []Alias
|
||||||
|
|
||||||
|
func (a *Aliases) UnmarshalJSON(b []byte) error {
|
||||||
|
var aliases []AliasEnc
|
||||||
|
err := json.Unmarshal(b, &aliases)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*a = make([]Alias, len(aliases))
|
||||||
|
for i, alias := range aliases {
|
||||||
|
(*a)[i] = alias.Alias
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Aliases) Resolve(p *Policy, users types.Users, nodes types.Nodes) (*netipx.IPSet, error) {
|
||||||
|
var ips netipx.IPSetBuilder
|
||||||
|
|
||||||
|
for _, alias := range a {
|
||||||
|
aips, err := alias.Resolve(p, users, nodes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ips.AddSet(aips)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ips.IPSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Owner interface {
|
||||||
|
CanBeTagOwner() bool
|
||||||
|
UnmarshalJSON([]byte) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// OwnerEnc is used to deserialize a Owner.
|
||||||
|
type OwnerEnc struct{ Owner }
|
||||||
|
|
||||||
|
func (ve *OwnerEnc) UnmarshalJSON(b []byte) error {
|
||||||
|
// TODO(kradalby): use encoding/json/v2 (go-json-experiment)
|
||||||
|
dec := json.NewDecoder(bytes.NewReader(b))
|
||||||
|
var v any
|
||||||
|
if err := dec.Decode(&v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch val := v.(type) {
|
||||||
|
case string:
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.Contains(val, "@"):
|
||||||
|
ve.Owner = ptr.To(Username(val))
|
||||||
|
case strings.HasPrefix(val, "group:"):
|
||||||
|
ve.Owner = ptr.To(Group(val))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("type %T not supported", val)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Owners []Owner
|
||||||
|
|
||||||
|
func (o *Owners) UnmarshalJSON(b []byte) error {
|
||||||
|
var owners []OwnerEnc
|
||||||
|
err := json.Unmarshal(b, &owners)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*o = make([]Owner, len(owners))
|
||||||
|
for i, owner := range owners {
|
||||||
|
(*o)[i] = owner.Owner
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Usernames []Username
|
||||||
|
|
||||||
|
// Groups are a map of Group to a list of Username.
|
||||||
|
type Groups map[Group]Usernames
|
||||||
|
|
||||||
|
// Hosts are alias for IP addresses or subnets.
|
||||||
|
type Hosts map[Host]Prefix
|
||||||
|
|
||||||
|
// TagOwners are a map of Tag to a list of the UserEntities that own the tag.
|
||||||
|
type TagOwners map[Tag]Owners
|
||||||
|
|
||||||
|
type AutoApprovers struct {
|
||||||
|
Routes map[string][]string `json:"routes"`
|
||||||
|
ExitNode []string `json:"exitNode"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ACL struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
Protocol string `json:"proto"`
|
||||||
|
Sources Aliases `json:"src"`
|
||||||
|
Destinations []AliasWithPorts `json:"dst"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Policy represents a Tailscale Network Policy.
|
||||||
|
// TODO(kradalby):
|
||||||
|
// Add validation method checking:
|
||||||
|
// All users exists
|
||||||
|
// All groups and users are valid tag TagOwners
|
||||||
|
// Everything referred to in ACLs exists in other
|
||||||
|
// entities.
|
||||||
|
type Policy struct {
|
||||||
|
// validated is set if the policy has been validated.
|
||||||
|
// It is not safe to use before it is validated, and
|
||||||
|
// callers using it should panic if not
|
||||||
|
validated bool `json:"-"`
|
||||||
|
|
||||||
|
Groups Groups `json:"groups"`
|
||||||
|
Hosts Hosts `json:"hosts"`
|
||||||
|
TagOwners TagOwners `json:"tagOwners"`
|
||||||
|
ACLs []ACL `json:"acls"`
|
||||||
|
AutoApprovers AutoApprovers `json:"autoApprovers"`
|
||||||
|
SSHs []SSH `json:"ssh"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSH controls who can ssh into which machines.
|
||||||
|
type SSH struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
Sources SSHSrcAliases `json:"src"`
|
||||||
|
Destinations SSHDstAliases `json:"dst"`
|
||||||
|
Users []SSHUser `json:"users"`
|
||||||
|
CheckPeriod time.Duration `json:"checkPeriod,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSHSrcAliases is a list of aliases that can be used as sources in an SSH rule.
|
||||||
|
// It can be a list of usernames, groups, tags or autogroups.
|
||||||
|
type SSHSrcAliases []Alias
|
||||||
|
|
||||||
|
func (a *SSHSrcAliases) UnmarshalJSON(b []byte) error {
|
||||||
|
var aliases []AliasEnc
|
||||||
|
err := json.Unmarshal(b, &aliases)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*a = make([]Alias, len(aliases))
|
||||||
|
for i, alias := range aliases {
|
||||||
|
switch alias.Alias.(type) {
|
||||||
|
case *Username, *Group, *Tag, *AutoGroup:
|
||||||
|
(*a)[i] = alias.Alias
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("type %T not supported", alias.Alias)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSHDstAliases is a list of aliases that can be used as destinations in an SSH rule.
|
||||||
|
// It can be a list of usernames, tags or autogroups.
|
||||||
|
type SSHDstAliases []Alias
|
||||||
|
|
||||||
|
func (a *SSHDstAliases) UnmarshalJSON(b []byte) error {
|
||||||
|
var aliases []AliasEnc
|
||||||
|
err := json.Unmarshal(b, &aliases)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*a = make([]Alias, len(aliases))
|
||||||
|
for i, alias := range aliases {
|
||||||
|
switch alias.Alias.(type) {
|
||||||
|
case *Username, *Tag, *AutoGroup:
|
||||||
|
(*a)[i] = alias.Alias
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("type %T not supported", alias.Alias)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type SSHUser string
|
||||||
|
|
||||||
|
func policyFromBytes(b []byte) (*Policy, error) {
|
||||||
|
var policy Policy
|
||||||
|
ast, err := hujson.Parse(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing HuJSON: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ast.Standardize()
|
||||||
|
acl := ast.Pack()
|
||||||
|
|
||||||
|
err = json.Unmarshal(acl, &policy)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing policy from bytes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &policy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
expectedTokenItems = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO(kradalby): copy tests from parseDestination in policy
|
||||||
|
func splitDestination(dest string) (string, string, error) {
|
||||||
|
var tokens []string
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
port := tokens[len(tokens)-1]
|
||||||
|
|
||||||
|
maybeIPv6Str := strings.TrimSuffix(dest, ":"+port)
|
||||||
|
|
||||||
|
filteredMaybeIPv6Str := maybeIPv6Str
|
||||||
|
if strings.Contains(maybeIPv6Str, "/") {
|
||||||
|
networkParts := strings.Split(maybeIPv6Str, "/")
|
||||||
|
filteredMaybeIPv6Str = networkParts[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if maybeIPv6, err := netip.ParseAddr(filteredMaybeIPv6Str); err != nil && !maybeIPv6.Is6() {
|
||||||
|
return "", "", fmt.Errorf(
|
||||||
|
"failed to split destination: %v",
|
||||||
|
tokens,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
tokens = []string{maybeIPv6Str, port}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var alias string
|
||||||
|
// We can have here stuff like:
|
||||||
|
// git-server:*
|
||||||
|
// 192.168.1.0/24:22
|
||||||
|
// fd7a:115c:a1e0::2:22
|
||||||
|
// fd7a:115c:a1e0::2/128:22
|
||||||
|
// tag:montreal-webserver:80,443
|
||||||
|
// tag:api-server:443
|
||||||
|
// example-host-1:*
|
||||||
|
if len(tokens) == expectedTokenItems {
|
||||||
|
alias = tokens[0]
|
||||||
|
} else {
|
||||||
|
alias = fmt.Sprintf("%s:%s", tokens[0], tokens[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return alias, tokens[len(tokens)-1], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(kradalby): write/copy tests from expandPorts in policy
|
||||||
|
func parsePorts(portsStr string) ([]tailcfg.PortRange, error) {
|
||||||
|
if portsStr == "*" {
|
||||||
|
return []tailcfg.PortRange{
|
||||||
|
tailcfg.PortRangeAny,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ports []tailcfg.PortRange
|
||||||
|
for _, portStr := range strings.Split(portsStr, ",") {
|
||||||
|
rang := strings.Split(portStr, "-")
|
||||||
|
switch len(rang) {
|
||||||
|
case 1:
|
||||||
|
port, err := strconv.ParseUint(rang[0], util.Base10, util.BitSize16)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ports = append(ports, tailcfg.PortRange{
|
||||||
|
First: uint16(port),
|
||||||
|
Last: uint16(port),
|
||||||
|
})
|
||||||
|
|
||||||
|
case expectedTokenItems:
|
||||||
|
start, err := strconv.ParseUint(rang[0], util.Base10, util.BitSize16)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
last, err := strconv.ParseUint(rang[1], util.Base10, util.BitSize16)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ports = append(ports, tailcfg.PortRange{
|
||||||
|
First: uint16(start),
|
||||||
|
Last: uint16(last),
|
||||||
|
})
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, errors.New("invalid ports")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ports, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// For some reason golang.org/x/net/internal/iana is an internal package.
|
||||||
|
const (
|
||||||
|
protocolICMP = 1 // Internet Control Message
|
||||||
|
protocolIGMP = 2 // Internet Group Management
|
||||||
|
protocolIPv4 = 4 // IPv4 encapsulation
|
||||||
|
protocolTCP = 6 // Transmission Control
|
||||||
|
protocolEGP = 8 // Exterior Gateway Protocol
|
||||||
|
protocolIGP = 9 // any private interior gateway (used by Cisco for their IGRP)
|
||||||
|
protocolUDP = 17 // User Datagram
|
||||||
|
protocolGRE = 47 // Generic Routing Encapsulation
|
||||||
|
protocolESP = 50 // Encap Security Payload
|
||||||
|
protocolAH = 51 // Authentication Header
|
||||||
|
protocolIPv6ICMP = 58 // ICMP for IPv6
|
||||||
|
protocolSCTP = 132 // Stream Control Transmission Protocol
|
||||||
|
ProtocolFC = 133 // Fibre Channel
|
||||||
|
)
|
||||||
|
|
||||||
|
// parseProtocol reads the proto field of the ACL and generates a list of
|
||||||
|
// protocols that will be allowed, following the IANA IP protocol number
|
||||||
|
// https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml
|
||||||
|
//
|
||||||
|
// If the ACL proto field is empty, it allows ICMPv4, ICMPv6, TCP, and UDP,
|
||||||
|
// as per Tailscale behaviour (see tailcfg.FilterRule).
|
||||||
|
//
|
||||||
|
// Also returns a boolean indicating if the protocol
|
||||||
|
// requires all the destinations to use wildcard as port number (only TCP,
|
||||||
|
// UDP and SCTP support specifying ports).
|
||||||
|
func parseProtocol(protocol string) ([]int, bool, error) {
|
||||||
|
switch protocol {
|
||||||
|
case "":
|
||||||
|
return nil, false, nil
|
||||||
|
case "igmp":
|
||||||
|
return []int{protocolIGMP}, true, nil
|
||||||
|
case "ipv4", "ip-in-ip":
|
||||||
|
return []int{protocolIPv4}, true, nil
|
||||||
|
case "tcp":
|
||||||
|
return []int{protocolTCP}, false, nil
|
||||||
|
case "egp":
|
||||||
|
return []int{protocolEGP}, true, nil
|
||||||
|
case "igp":
|
||||||
|
return []int{protocolIGP}, true, nil
|
||||||
|
case "udp":
|
||||||
|
return []int{protocolUDP}, false, nil
|
||||||
|
case "gre":
|
||||||
|
return []int{protocolGRE}, true, nil
|
||||||
|
case "esp":
|
||||||
|
return []int{protocolESP}, true, nil
|
||||||
|
case "ah":
|
||||||
|
return []int{protocolAH}, true, nil
|
||||||
|
case "sctp":
|
||||||
|
return []int{protocolSCTP}, false, nil
|
||||||
|
case "icmp":
|
||||||
|
return []int{protocolICMP, protocolIPv6ICMP}, true, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
protocolNumber, err := strconv.Atoi(protocol)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, fmt.Errorf("parsing protocol number: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(kradalby): What is this?
|
||||||
|
needsWildcard := protocolNumber != protocolTCP &&
|
||||||
|
protocolNumber != protocolUDP &&
|
||||||
|
protocolNumber != protocolSCTP
|
||||||
|
|
||||||
|
return []int{protocolNumber}, needsWildcard, nil
|
||||||
|
}
|
||||||
|
}
|
555
hscontrol/policyv2/types_test.go
Normal file
555
hscontrol/policyv2/types_test.go
Normal file
|
@ -0,0 +1,555 @@
|
||||||
|
package policyv2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
|
"github.com/juanfont/headscale/hscontrol/util"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
|
"tailscale.com/types/ptr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUnmarshalPolicy(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
want *Policy
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
input: "{}",
|
||||||
|
want: &Policy{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "groups",
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
"groups": {
|
||||||
|
"group:example": [
|
||||||
|
"derp@headscale.net",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
want: &Policy{
|
||||||
|
Groups: Groups{
|
||||||
|
Group("group:example"): []Username{Username("derp@headscale.net")},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "basic-types",
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
"groups": {
|
||||||
|
"group:example": [
|
||||||
|
"testuser@headscale.net",
|
||||||
|
],
|
||||||
|
"group:other": [
|
||||||
|
"otheruser@headscale.net",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
"tagOwners": {
|
||||||
|
"tag:user": ["testuser@headscale.net"],
|
||||||
|
"tag:group": ["group:other"],
|
||||||
|
"tag:userandgroup": ["testuser@headscale.net", "group:other"],
|
||||||
|
},
|
||||||
|
|
||||||
|
"hosts": {
|
||||||
|
"host-1": "100.100.100.100",
|
||||||
|
"subnet-1": "100.100.101.100/24",
|
||||||
|
"outside": "192.168.0.0/16",
|
||||||
|
},
|
||||||
|
|
||||||
|
"acls": [
|
||||||
|
// All
|
||||||
|
{
|
||||||
|
"action": "accept",
|
||||||
|
"proto": "tcp",
|
||||||
|
"src": ["*"],
|
||||||
|
"dst": ["*:*"],
|
||||||
|
},
|
||||||
|
// Users
|
||||||
|
{
|
||||||
|
"action": "accept",
|
||||||
|
"proto": "tcp",
|
||||||
|
"src": ["testuser@headscale.net"],
|
||||||
|
"dst": ["otheruser@headscale.net:80"],
|
||||||
|
},
|
||||||
|
// Groups
|
||||||
|
{
|
||||||
|
"action": "accept",
|
||||||
|
"proto": "tcp",
|
||||||
|
"src": ["group:example"],
|
||||||
|
"dst": ["group:other:80"],
|
||||||
|
},
|
||||||
|
// Tailscale IP
|
||||||
|
{
|
||||||
|
"action": "accept",
|
||||||
|
"proto": "tcp",
|
||||||
|
"src": ["100.101.102.103"],
|
||||||
|
"dst": ["100.101.102.104:80"],
|
||||||
|
},
|
||||||
|
// Subnet
|
||||||
|
{
|
||||||
|
"action": "accept",
|
||||||
|
"proto": "udp",
|
||||||
|
"src": ["10.0.0.0/8"],
|
||||||
|
"dst": ["172.16.0.0/16:80"],
|
||||||
|
},
|
||||||
|
// Hosts
|
||||||
|
{
|
||||||
|
"action": "accept",
|
||||||
|
"proto": "tcp",
|
||||||
|
"src": ["subnet-1"],
|
||||||
|
"dst": ["host-1:80-88"],
|
||||||
|
},
|
||||||
|
// Tags
|
||||||
|
{
|
||||||
|
"action": "accept",
|
||||||
|
"proto": "tcp",
|
||||||
|
"src": ["tag:group"],
|
||||||
|
"dst": ["tag:user:80,443"],
|
||||||
|
},
|
||||||
|
// Autogroup
|
||||||
|
{
|
||||||
|
"action": "accept",
|
||||||
|
"proto": "tcp",
|
||||||
|
"src": ["tag:group"],
|
||||||
|
"dst": ["autogroup:internet:80"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
want: &Policy{
|
||||||
|
Groups: Groups{
|
||||||
|
Group("group:example"): []Username{Username("testuser@headscale.net")},
|
||||||
|
Group("group:other"): []Username{Username("otheruser@headscale.net")},
|
||||||
|
},
|
||||||
|
TagOwners: TagOwners{
|
||||||
|
Tag("tag:user"): Owners{up("testuser@headscale.net")},
|
||||||
|
Tag("tag:group"): Owners{gp("group:other")},
|
||||||
|
Tag("tag:userandgroup"): Owners{up("testuser@headscale.net"), gp("group:other")},
|
||||||
|
},
|
||||||
|
Hosts: Hosts{
|
||||||
|
"host-1": Prefix(netip.MustParsePrefix("100.100.100.100/32")),
|
||||||
|
"subnet-1": Prefix(netip.MustParsePrefix("100.100.101.100/24")),
|
||||||
|
"outside": Prefix(netip.MustParsePrefix("192.168.0.0/16")),
|
||||||
|
},
|
||||||
|
ACLs: []ACL{
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Protocol: "tcp",
|
||||||
|
Sources: Aliases{
|
||||||
|
// TODO(kradalby): Should this be host?
|
||||||
|
// It is:
|
||||||
|
// All traffic originating from Tailscale devices in your tailnet,
|
||||||
|
// any approved subnets and autogroup:shared.
|
||||||
|
// It does not allow traffic originating from
|
||||||
|
// non-tailscale devices (unless it is an approved route).
|
||||||
|
hp("*"),
|
||||||
|
},
|
||||||
|
Destinations: []AliasWithPorts{
|
||||||
|
{
|
||||||
|
// TODO(kradalby): Should this be host?
|
||||||
|
// It is:
|
||||||
|
// Includes any destination (no restrictions).
|
||||||
|
Alias: hp("*"),
|
||||||
|
Ports: []tailcfg.PortRange{tailcfg.PortRangeAny},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Protocol: "tcp",
|
||||||
|
Sources: Aliases{
|
||||||
|
ptr.To(Username("testuser@headscale.net")),
|
||||||
|
},
|
||||||
|
Destinations: []AliasWithPorts{
|
||||||
|
{
|
||||||
|
Alias: ptr.To(Username("otheruser@headscale.net")),
|
||||||
|
Ports: []tailcfg.PortRange{{First: 80, Last: 80}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Protocol: "tcp",
|
||||||
|
Sources: Aliases{
|
||||||
|
gp("group:example"),
|
||||||
|
},
|
||||||
|
Destinations: []AliasWithPorts{
|
||||||
|
{
|
||||||
|
Alias: gp("group:other"),
|
||||||
|
Ports: []tailcfg.PortRange{{First: 80, Last: 80}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Protocol: "tcp",
|
||||||
|
Sources: Aliases{
|
||||||
|
pp("100.101.102.103/32"),
|
||||||
|
},
|
||||||
|
Destinations: []AliasWithPorts{
|
||||||
|
{
|
||||||
|
Alias: pp("100.101.102.104/32"),
|
||||||
|
Ports: []tailcfg.PortRange{{First: 80, Last: 80}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Protocol: "udp",
|
||||||
|
Sources: Aliases{
|
||||||
|
pp("10.0.0.0/8"),
|
||||||
|
},
|
||||||
|
Destinations: []AliasWithPorts{
|
||||||
|
{
|
||||||
|
Alias: pp("172.16.0.0/16"),
|
||||||
|
Ports: []tailcfg.PortRange{{First: 80, Last: 80}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Protocol: "tcp",
|
||||||
|
Sources: Aliases{
|
||||||
|
hp("subnet-1"),
|
||||||
|
},
|
||||||
|
Destinations: []AliasWithPorts{
|
||||||
|
{
|
||||||
|
Alias: hp("host-1"),
|
||||||
|
Ports: []tailcfg.PortRange{{First: 80, Last: 88}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Protocol: "tcp",
|
||||||
|
Sources: Aliases{
|
||||||
|
tp("tag:group"),
|
||||||
|
},
|
||||||
|
Destinations: []AliasWithPorts{
|
||||||
|
{
|
||||||
|
Alias: tp("tag:user"),
|
||||||
|
Ports: []tailcfg.PortRange{
|
||||||
|
{First: 80, Last: 80},
|
||||||
|
{First: 443, Last: 443},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Protocol: "tcp",
|
||||||
|
Sources: Aliases{
|
||||||
|
tp("tag:group"),
|
||||||
|
},
|
||||||
|
Destinations: []AliasWithPorts{
|
||||||
|
{
|
||||||
|
Alias: agp("autogroup:internet"),
|
||||||
|
Ports: []tailcfg.PortRange{
|
||||||
|
{First: 80, Last: 80},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid-username",
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
"groups": {
|
||||||
|
"group:example": [
|
||||||
|
"valid@",
|
||||||
|
"invalid",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
wantErr: `Username has to contain @, got: "invalid"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid-group",
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
"groups": {
|
||||||
|
"grou:example": [
|
||||||
|
"valid@",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
wantErr: `Group has to start with "group:", got: "grou:example"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "group-in-group",
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
"groups": {
|
||||||
|
"group:inner": [],
|
||||||
|
"group:example": [
|
||||||
|
"group:inner",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
wantErr: `Username has to contain @, got: "group:inner"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid-prefix",
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
"hosts": {
|
||||||
|
"derp": "10.0",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
wantErr: `ParseAddr("10.0"): IPv4 address too short`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid-auto-group",
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
"acls": [
|
||||||
|
// Autogroup
|
||||||
|
{
|
||||||
|
"action": "accept",
|
||||||
|
"proto": "tcp",
|
||||||
|
"src": ["tag:group"],
|
||||||
|
"dst": ["autogroup:invalid:80"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
wantErr: `AutoGroup is invalid, got: "autogroup:invalid", must be one of [autogroup:internet]`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmps := append(util.Comparers, cmp.Comparer(func(x, y Prefix) bool {
|
||||||
|
return x == y
|
||||||
|
}))
|
||||||
|
cmps = append(cmps, cmpopts.IgnoreUnexported(Policy{}))
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
policy, err := policyFromBytes([]byte(tt.input))
|
||||||
|
// TODO(kradalby): This error checking is broken,
|
||||||
|
// but so is my brain, #longflight
|
||||||
|
if err == nil {
|
||||||
|
if tt.wantErr == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Fatalf("got success; wanted error %q", tt.wantErr)
|
||||||
|
}
|
||||||
|
if err.Error() != tt.wantErr {
|
||||||
|
t.Fatalf("got error %q; want %q", err, tt.wantErr)
|
||||||
|
// } else if err.Error() == tt.wantErr {
|
||||||
|
// return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected err: %q", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(tt.want, &policy, cmps...); diff != "" {
|
||||||
|
t.Fatalf("unexpected policy (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func gp(s string) *Group { return ptr.To(Group(s)) }
|
||||||
|
func up(s string) *Username { return ptr.To(Username(s)) }
|
||||||
|
func hp(s string) *Host { return ptr.To(Host(s)) }
|
||||||
|
func tp(s string) *Tag { return ptr.To(Tag(s)) }
|
||||||
|
func agp(s string) *AutoGroup { return ptr.To(AutoGroup(s)) }
|
||||||
|
func mp(pref string) netip.Prefix { return netip.MustParsePrefix(pref) }
|
||||||
|
func ap(addr string) *netip.Addr { return ptr.To(netip.MustParseAddr(addr)) }
|
||||||
|
func pp(pref string) *Prefix { return ptr.To(Prefix(netip.MustParsePrefix(pref))) }
|
||||||
|
func p(pref string) Prefix { return Prefix(netip.MustParsePrefix(pref)) }
|
||||||
|
|
||||||
|
func TestResolvePolicy(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
nodes types.Nodes
|
||||||
|
pol *Policy
|
||||||
|
toResolve Alias
|
||||||
|
want []netip.Prefix
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "prefix",
|
||||||
|
toResolve: pp("100.100.101.101/32"),
|
||||||
|
want: []netip.Prefix{mp("100.100.101.101/32")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host",
|
||||||
|
pol: &Policy{
|
||||||
|
Hosts: Hosts{
|
||||||
|
"testhost": p("100.100.101.102/32"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
toResolve: hp("testhost"),
|
||||||
|
want: []netip.Prefix{mp("100.100.101.102/32")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "username",
|
||||||
|
toResolve: ptr.To(Username("testuser")),
|
||||||
|
nodes: types.Nodes{
|
||||||
|
// Not matching other user
|
||||||
|
{
|
||||||
|
User: types.User{
|
||||||
|
Name: "notme",
|
||||||
|
},
|
||||||
|
IPv4: ap("100.100.101.1"),
|
||||||
|
},
|
||||||
|
// Not matching forced tags
|
||||||
|
{
|
||||||
|
User: types.User{
|
||||||
|
Name: "testuser",
|
||||||
|
},
|
||||||
|
ForcedTags: []string{"tag:anything"},
|
||||||
|
IPv4: ap("100.100.101.2"),
|
||||||
|
},
|
||||||
|
// not matchin pak tag
|
||||||
|
{
|
||||||
|
User: types.User{
|
||||||
|
Name: "testuser",
|
||||||
|
},
|
||||||
|
AuthKey: &types.PreAuthKey{
|
||||||
|
Tags: []string{"alsotagged"},
|
||||||
|
},
|
||||||
|
IPv4: ap("100.100.101.3"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: types.User{
|
||||||
|
Name: "testuser",
|
||||||
|
},
|
||||||
|
IPv4: ap("100.100.101.103"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: types.User{
|
||||||
|
Name: "testuser",
|
||||||
|
},
|
||||||
|
IPv4: ap("100.100.101.104"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []netip.Prefix{mp("100.100.101.103/32"), mp("100.100.101.104/32")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "group",
|
||||||
|
toResolve: ptr.To(Group("group:testgroup")),
|
||||||
|
nodes: types.Nodes{
|
||||||
|
// Not matching other user
|
||||||
|
{
|
||||||
|
User: types.User{
|
||||||
|
Name: "notmetoo",
|
||||||
|
},
|
||||||
|
IPv4: ap("100.100.101.4"),
|
||||||
|
},
|
||||||
|
// Not matching forced tags
|
||||||
|
{
|
||||||
|
User: types.User{
|
||||||
|
Name: "groupuser",
|
||||||
|
},
|
||||||
|
ForcedTags: []string{"tag:anything"},
|
||||||
|
IPv4: ap("100.100.101.5"),
|
||||||
|
},
|
||||||
|
// not matchin pak tag
|
||||||
|
{
|
||||||
|
User: types.User{
|
||||||
|
Name: "groupuser",
|
||||||
|
},
|
||||||
|
AuthKey: &types.PreAuthKey{
|
||||||
|
Tags: []string{"tag:alsotagged"},
|
||||||
|
},
|
||||||
|
IPv4: ap("100.100.101.6"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: types.User{
|
||||||
|
Name: "groupuser",
|
||||||
|
},
|
||||||
|
IPv4: ap("100.100.101.203"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: types.User{
|
||||||
|
Name: "groupuser",
|
||||||
|
},
|
||||||
|
IPv4: ap("100.100.101.204"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pol: &Policy{
|
||||||
|
Groups: Groups{
|
||||||
|
"group:testgroup": Usernames{"groupuser"},
|
||||||
|
"group:othergroup": Usernames{"notmetoo"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []netip.Prefix{mp("100.100.101.203/32"), mp("100.100.101.204/32")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tag",
|
||||||
|
toResolve: tp("tag:test"),
|
||||||
|
nodes: types.Nodes{
|
||||||
|
// Not matching other user
|
||||||
|
{
|
||||||
|
User: types.User{
|
||||||
|
Name: "notmetoo",
|
||||||
|
},
|
||||||
|
IPv4: ap("100.100.101.9"),
|
||||||
|
},
|
||||||
|
// Not matching forced tags
|
||||||
|
{
|
||||||
|
ForcedTags: []string{"tag:anything"},
|
||||||
|
IPv4: ap("100.100.101.10"),
|
||||||
|
},
|
||||||
|
// not matchin pak tag
|
||||||
|
{
|
||||||
|
AuthKey: &types.PreAuthKey{
|
||||||
|
Tags: []string{"tag:alsotagged"},
|
||||||
|
},
|
||||||
|
IPv4: ap("100.100.101.11"),
|
||||||
|
},
|
||||||
|
// Not matching forced tags
|
||||||
|
{
|
||||||
|
ForcedTags: []string{"tag:test"},
|
||||||
|
IPv4: ap("100.100.101.234"),
|
||||||
|
},
|
||||||
|
// not matchin pak tag
|
||||||
|
{
|
||||||
|
AuthKey: &types.PreAuthKey{
|
||||||
|
Tags: []string{"tag:test"},
|
||||||
|
},
|
||||||
|
IPv4: ap("100.100.101.239"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// TODO(kradalby): tests handling TagOwners + hostinfo
|
||||||
|
pol: &Policy{},
|
||||||
|
want: []netip.Prefix{mp("100.100.101.234/32"), mp("100.100.101.239/32")},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
ips, err := tt.toResolve.Resolve(tt.pol,
|
||||||
|
types.Users{},
|
||||||
|
tt.nodes)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to resolve: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs := ips.Prefixes()
|
||||||
|
|
||||||
|
if diff := cmp.Diff(tt.want, prefs, util.Comparers...); diff != "" {
|
||||||
|
t.Fatalf("unexpected prefs (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,7 +13,7 @@ func Windows(url string) *elem.Element {
|
||||||
elem.Text("headscale - Windows"),
|
elem.Text("headscale - Windows"),
|
||||||
),
|
),
|
||||||
elem.Body(attrs.Props{
|
elem.Body(attrs.Props{
|
||||||
attrs.Style : bodyStyle.ToInline(),
|
attrs.Style: bodyStyle.ToInline(),
|
||||||
},
|
},
|
||||||
headerOne("headscale: Windows configuration"),
|
headerOne("headscale: Windows configuration"),
|
||||||
elem.P(nil,
|
elem.P(nil,
|
||||||
|
@ -21,7 +21,8 @@ func Windows(url string) *elem.Element {
|
||||||
elem.A(attrs.Props{
|
elem.A(attrs.Props{
|
||||||
attrs.Href: "https://tailscale.com/download/windows",
|
attrs.Href: "https://tailscale.com/download/windows",
|
||||||
attrs.Rel: "noreferrer noopener",
|
attrs.Rel: "noreferrer noopener",
|
||||||
attrs.Target: "_blank"},
|
attrs.Target: "_blank",
|
||||||
|
},
|
||||||
elem.Text("Tailscale for Windows ")),
|
elem.Text("Tailscale for Windows ")),
|
||||||
elem.Text("and install it."),
|
elem.Text("and install it."),
|
||||||
),
|
),
|
||||||
|
|
|
@ -28,8 +28,9 @@ const (
|
||||||
maxDuration time.Duration = 1<<63 - 1
|
maxDuration time.Duration = 1<<63 - 1
|
||||||
)
|
)
|
||||||
|
|
||||||
var errOidcMutuallyExclusive = errors.New(
|
var (
|
||||||
"oidc_client_secret and oidc_client_secret_path are mutually exclusive",
|
errOidcMutuallyExclusive = errors.New("oidc_client_secret and oidc_client_secret_path are mutually exclusive")
|
||||||
|
errServerURLSuffix = errors.New("server_url cannot be part of base_domain in a way that could make the DERP and headscale server unreachable")
|
||||||
)
|
)
|
||||||
|
|
||||||
type IPAllocationStrategy string
|
type IPAllocationStrategy string
|
||||||
|
@ -102,8 +103,9 @@ type Nameservers struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type SqliteConfig struct {
|
type SqliteConfig struct {
|
||||||
Path string
|
Path string
|
||||||
WriteAheadLog bool
|
WriteAheadLog bool
|
||||||
|
WALAutoCheckPoint int
|
||||||
}
|
}
|
||||||
|
|
||||||
type PostgresConfig struct {
|
type PostgresConfig struct {
|
||||||
|
@ -162,8 +164,10 @@ type OIDCConfig struct {
|
||||||
AllowedDomains []string
|
AllowedDomains []string
|
||||||
AllowedUsers []string
|
AllowedUsers []string
|
||||||
AllowedGroups []string
|
AllowedGroups []string
|
||||||
|
StripEmaildomain bool
|
||||||
Expiry time.Duration
|
Expiry time.Duration
|
||||||
UseExpiryFromToken bool
|
UseExpiryFromToken bool
|
||||||
|
MapLegacyUsers bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type DERPConfig struct {
|
type DERPConfig struct {
|
||||||
|
@ -270,11 +274,14 @@ func LoadConfig(path string, isFile bool) error {
|
||||||
viper.SetDefault("database.postgres.conn_max_idle_time_secs", 3600)
|
viper.SetDefault("database.postgres.conn_max_idle_time_secs", 3600)
|
||||||
|
|
||||||
viper.SetDefault("database.sqlite.write_ahead_log", true)
|
viper.SetDefault("database.sqlite.write_ahead_log", true)
|
||||||
|
viper.SetDefault("database.sqlite.wal_autocheckpoint", 1000) // SQLite default
|
||||||
|
|
||||||
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.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.expiry", "180d")
|
||||||
viper.SetDefault("oidc.use_expiry_from_token", false)
|
viper.SetDefault("oidc.use_expiry_from_token", false)
|
||||||
|
viper.SetDefault("oidc.map_legacy_users", true)
|
||||||
|
|
||||||
viper.SetDefault("logtail.enabled", false)
|
viper.SetDefault("logtail.enabled", false)
|
||||||
viper.SetDefault("randomize_client_port", false)
|
viper.SetDefault("randomize_client_port", false)
|
||||||
|
@ -318,14 +325,18 @@ func validateServerConfig() error {
|
||||||
depr.warn("dns_config.use_username_in_magic_dns")
|
depr.warn("dns_config.use_username_in_magic_dns")
|
||||||
depr.warn("dns.use_username_in_magic_dns")
|
depr.warn("dns.use_username_in_magic_dns")
|
||||||
|
|
||||||
depr.fatal("oidc.strip_email_domain")
|
// TODO(kradalby): Reintroduce when strip_email_domain is removed
|
||||||
|
// after #2170 is cleaned up
|
||||||
|
// depr.fatal("oidc.strip_email_domain")
|
||||||
depr.fatal("dns.use_username_in_musername_in_magic_dns")
|
depr.fatal("dns.use_username_in_musername_in_magic_dns")
|
||||||
depr.fatal("dns_config.use_username_in_musername_in_magic_dns")
|
depr.fatal("dns_config.use_username_in_musername_in_magic_dns")
|
||||||
|
|
||||||
depr.Log()
|
depr.Log()
|
||||||
|
|
||||||
for _, removed := range []string{
|
for _, removed := range []string{
|
||||||
"oidc.strip_email_domain",
|
// TODO(kradalby): Reintroduce when strip_email_domain is removed
|
||||||
|
// after #2170 is cleaned up
|
||||||
|
// "oidc.strip_email_domain",
|
||||||
"dns_config.use_username_in_musername_in_magic_dns",
|
"dns_config.use_username_in_musername_in_magic_dns",
|
||||||
} {
|
} {
|
||||||
if viper.IsSet(removed) {
|
if viper.IsSet(removed) {
|
||||||
|
@ -542,7 +553,8 @@ func databaseConfig() DatabaseConfig {
|
||||||
Path: util.AbsolutePathFromConfigPath(
|
Path: util.AbsolutePathFromConfigPath(
|
||||||
viper.GetString("database.sqlite.path"),
|
viper.GetString("database.sqlite.path"),
|
||||||
),
|
),
|
||||||
WriteAheadLog: viper.GetBool("database.sqlite.write_ahead_log"),
|
WriteAheadLog: viper.GetBool("database.sqlite.write_ahead_log"),
|
||||||
|
WALAutoCheckPoint: viper.GetInt("database.sqlite.wal_autocheckpoint"),
|
||||||
},
|
},
|
||||||
Postgres: PostgresConfig{
|
Postgres: PostgresConfig{
|
||||||
Host: viper.GetString("database.postgres.host"),
|
Host: viper.GetString("database.postgres.host"),
|
||||||
|
@ -827,11 +839,10 @@ func LoadServerConfig() (*Config, error) {
|
||||||
// - DERP run on their own domains
|
// - DERP run on their own domains
|
||||||
// - Control plane runs on login.tailscale.com/controlplane.tailscale.com
|
// - Control plane runs on login.tailscale.com/controlplane.tailscale.com
|
||||||
// - MagicDNS (BaseDomain) for users is on a *.ts.net domain per tailnet (e.g. tail-scale.ts.net)
|
// - MagicDNS (BaseDomain) for users is on a *.ts.net domain per tailnet (e.g. tail-scale.ts.net)
|
||||||
if dnsConfig.BaseDomain != "" &&
|
if dnsConfig.BaseDomain != "" {
|
||||||
strings.Contains(serverURL, dnsConfig.BaseDomain) {
|
if err := isSafeServerURL(serverURL, dnsConfig.BaseDomain); err != nil {
|
||||||
return nil, errors.New(
|
return nil, err
|
||||||
"server_url cannot contain the base_domain, this will cause the headscale server and embedded DERP to become unreachable from the Tailscale node.",
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Config{
|
return &Config{
|
||||||
|
@ -897,6 +908,10 @@ func LoadServerConfig() (*Config, error) {
|
||||||
}
|
}
|
||||||
}(),
|
}(),
|
||||||
UseExpiryFromToken: viper.GetBool("oidc.use_expiry_from_token"),
|
UseExpiryFromToken: viper.GetBool("oidc.use_expiry_from_token"),
|
||||||
|
// TODO(kradalby): Remove when strip_email_domain is removed
|
||||||
|
// after #2170 is cleaned up
|
||||||
|
StripEmaildomain: viper.GetBool("oidc.strip_email_domain"),
|
||||||
|
MapLegacyUsers: viper.GetBool("oidc.map_legacy_users"),
|
||||||
},
|
},
|
||||||
|
|
||||||
LogTail: logTailConfig,
|
LogTail: logTailConfig,
|
||||||
|
@ -924,6 +939,37 @@ func LoadServerConfig() (*Config, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BaseDomain cannot be a suffix of the server URL.
|
||||||
|
// This is because Tailscale takes over the domain in BaseDomain,
|
||||||
|
// causing the headscale server and DERP to be unreachable.
|
||||||
|
// For Tailscale upstream, the following is true:
|
||||||
|
// - DERP run on their own domains.
|
||||||
|
// - Control plane runs on login.tailscale.com/controlplane.tailscale.com.
|
||||||
|
// - MagicDNS (BaseDomain) for users is on a *.ts.net domain per tailnet (e.g. tail-scale.ts.net).
|
||||||
|
func isSafeServerURL(serverURL, baseDomain string) error {
|
||||||
|
server, err := url.Parse(serverURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
serverDomainParts := strings.Split(server.Host, ".")
|
||||||
|
baseDomainParts := strings.Split(baseDomain, ".")
|
||||||
|
|
||||||
|
if len(serverDomainParts) <= len(baseDomainParts) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
s := len(serverDomainParts)
|
||||||
|
b := len(baseDomainParts)
|
||||||
|
for i := range len(baseDomainParts) {
|
||||||
|
if serverDomainParts[s-i-1] != baseDomainParts[b-i-1] {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errServerURLSuffix
|
||||||
|
}
|
||||||
|
|
||||||
type deprecator struct {
|
type deprecator struct {
|
||||||
warns set.Set[string]
|
warns set.Set[string]
|
||||||
fatals set.Set[string]
|
fatals set.Set[string]
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -8,6 +9,7 @@ import (
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/dnstype"
|
"tailscale.com/types/dnstype"
|
||||||
)
|
)
|
||||||
|
@ -35,8 +37,17 @@ func TestReadConfig(t *testing.T) {
|
||||||
MagicDNS: true,
|
MagicDNS: true,
|
||||||
BaseDomain: "example.com",
|
BaseDomain: "example.com",
|
||||||
Nameservers: Nameservers{
|
Nameservers: Nameservers{
|
||||||
Global: []string{"1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001", "https://dns.nextdns.io/abc123"},
|
Global: []string{
|
||||||
Split: map[string][]string{"darp.headscale.net": {"1.1.1.1", "8.8.8.8"}, "foo.bar.com": {"1.1.1.1"}},
|
"1.1.1.1",
|
||||||
|
"1.0.0.1",
|
||||||
|
"2606:4700:4700::1111",
|
||||||
|
"2606:4700:4700::1001",
|
||||||
|
"https://dns.nextdns.io/abc123",
|
||||||
|
},
|
||||||
|
Split: map[string][]string{
|
||||||
|
"darp.headscale.net": {"1.1.1.1", "8.8.8.8"},
|
||||||
|
"foo.bar.com": {"1.1.1.1"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
ExtraRecords: []tailcfg.DNSRecord{
|
ExtraRecords: []tailcfg.DNSRecord{
|
||||||
{Name: "grafana.myvpn.example.com", Type: "A", Value: "100.64.0.3"},
|
{Name: "grafana.myvpn.example.com", Type: "A", Value: "100.64.0.3"},
|
||||||
|
@ -91,8 +102,17 @@ func TestReadConfig(t *testing.T) {
|
||||||
MagicDNS: false,
|
MagicDNS: false,
|
||||||
BaseDomain: "example.com",
|
BaseDomain: "example.com",
|
||||||
Nameservers: Nameservers{
|
Nameservers: Nameservers{
|
||||||
Global: []string{"1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001", "https://dns.nextdns.io/abc123"},
|
Global: []string{
|
||||||
Split: map[string][]string{"darp.headscale.net": {"1.1.1.1", "8.8.8.8"}, "foo.bar.com": {"1.1.1.1"}},
|
"1.1.1.1",
|
||||||
|
"1.0.0.1",
|
||||||
|
"2606:4700:4700::1111",
|
||||||
|
"2606:4700:4700::1001",
|
||||||
|
"https://dns.nextdns.io/abc123",
|
||||||
|
},
|
||||||
|
Split: map[string][]string{
|
||||||
|
"darp.headscale.net": {"1.1.1.1", "8.8.8.8"},
|
||||||
|
"foo.bar.com": {"1.1.1.1"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
ExtraRecords: []tailcfg.DNSRecord{
|
ExtraRecords: []tailcfg.DNSRecord{
|
||||||
{Name: "grafana.myvpn.example.com", Type: "A", Value: "100.64.0.3"},
|
{Name: "grafana.myvpn.example.com", Type: "A", Value: "100.64.0.3"},
|
||||||
|
@ -139,7 +159,7 @@ func TestReadConfig(t *testing.T) {
|
||||||
return LoadServerConfig()
|
return LoadServerConfig()
|
||||||
},
|
},
|
||||||
want: nil,
|
want: nil,
|
||||||
wantErr: "server_url cannot contain the base_domain, this will cause the headscale server and embedded DERP to become unreachable from the Tailscale node.",
|
wantErr: errServerURLSuffix.Error(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "base-domain-not-in-server-url",
|
name: "base-domain-not-in-server-url",
|
||||||
|
@ -186,7 +206,7 @@ func TestReadConfig(t *testing.T) {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
viper.Reset()
|
viper.Reset()
|
||||||
err := LoadConfig(tt.configPath, true)
|
err := LoadConfig(tt.configPath, true)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
conf, err := tt.setup(t)
|
conf, err := tt.setup(t)
|
||||||
|
|
||||||
|
@ -196,7 +216,7 @@ func TestReadConfig(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
if diff := cmp.Diff(tt.want, conf); diff != "" {
|
if diff := cmp.Diff(tt.want, conf); diff != "" {
|
||||||
t.Errorf("ReadConfig() mismatch (-want +got):\n%s", diff)
|
t.Errorf("ReadConfig() mismatch (-want +got):\n%s", diff)
|
||||||
|
@ -276,10 +296,10 @@ func TestReadConfigFromEnv(t *testing.T) {
|
||||||
|
|
||||||
viper.Reset()
|
viper.Reset()
|
||||||
err := LoadConfig("testdata/minimal.yaml", true)
|
err := LoadConfig("testdata/minimal.yaml", true)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
conf, err := tt.setup(t)
|
conf, err := tt.setup(t)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
if diff := cmp.Diff(tt.want, conf); diff != "" {
|
if diff := cmp.Diff(tt.want, conf); diff != "" {
|
||||||
t.Errorf("ReadConfig() mismatch (-want +got):\n%s", diff)
|
t.Errorf("ReadConfig() mismatch (-want +got):\n%s", diff)
|
||||||
|
@ -310,13 +330,25 @@ noise:
|
||||||
|
|
||||||
// Check configuration validation errors (1)
|
// Check configuration validation errors (1)
|
||||||
err = LoadConfig(tmpDir, false)
|
err = LoadConfig(tmpDir, false)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = validateServerConfig()
|
err = validateServerConfig()
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both")
|
assert.Contains(
|
||||||
assert.Contains(t, err.Error(), "Fatal config error: the only supported values for tls_letsencrypt_challenge_type are")
|
t,
|
||||||
assert.Contains(t, err.Error(), "Fatal config error: server_url must start with https:// or http://")
|
err.Error(),
|
||||||
|
"Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both",
|
||||||
|
)
|
||||||
|
assert.Contains(
|
||||||
|
t,
|
||||||
|
err.Error(),
|
||||||
|
"Fatal config error: the only supported values for tls_letsencrypt_challenge_type are",
|
||||||
|
)
|
||||||
|
assert.Contains(
|
||||||
|
t,
|
||||||
|
err.Error(),
|
||||||
|
"Fatal config error: server_url must start with https:// or http://",
|
||||||
|
)
|
||||||
|
|
||||||
// Check configuration validation errors (2)
|
// Check configuration validation errors (2)
|
||||||
configYaml = []byte(`---
|
configYaml = []byte(`---
|
||||||
|
@ -331,5 +363,66 @@ tls_letsencrypt_challenge_type: TLS-ALPN-01
|
||||||
t.Fatalf("Couldn't write file %s", configFilePath)
|
t.Fatalf("Couldn't write file %s", configFilePath)
|
||||||
}
|
}
|
||||||
err = LoadConfig(tmpDir, false)
|
err = LoadConfig(tmpDir, false)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OK
|
||||||
|
// server_url: headscale.com, base: clients.headscale.com
|
||||||
|
// server_url: headscale.com, base: headscale.net
|
||||||
|
//
|
||||||
|
// NOT OK
|
||||||
|
// server_url: server.headscale.com, base: headscale.com.
|
||||||
|
func TestSafeServerURL(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
serverURL, baseDomain,
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
serverURL: "https://example.com",
|
||||||
|
baseDomain: "example.org",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serverURL: "https://headscale.com",
|
||||||
|
baseDomain: "headscale.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serverURL: "https://headscale.com",
|
||||||
|
baseDomain: "clients.headscale.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serverURL: "https://headscale.com",
|
||||||
|
baseDomain: "clients.subdomain.headscale.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serverURL: "https://headscale.kristoffer.com",
|
||||||
|
baseDomain: "mybase",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serverURL: "https://server.headscale.com",
|
||||||
|
baseDomain: "headscale.com",
|
||||||
|
wantErr: errServerURLSuffix.Error(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serverURL: "https://server.subdomain.headscale.com",
|
||||||
|
baseDomain: "headscale.com",
|
||||||
|
wantErr: errServerURLSuffix.Error(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serverURL: "http://foo\x00",
|
||||||
|
wantErr: `parse "http://foo\x00": net/url: invalid control character in URL`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
testName := fmt.Sprintf("server=%s domain=%s", tt.serverURL, tt.baseDomain)
|
||||||
|
t.Run(testName, func(t *testing.T) {
|
||||||
|
err := isSafeServerURL(tt.serverURL, tt.baseDomain)
|
||||||
|
if tt.wantErr != "" {
|
||||||
|
assert.EqualError(t, err, tt.wantErr)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -134,6 +135,60 @@ func (node *Node) IPs() []netip.Addr {
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasIP reports if a node has a given IP address.
|
||||||
|
func (node *Node) HasIP(i netip.Addr) bool {
|
||||||
|
for _, ip := range node.IPs() {
|
||||||
|
if ip.Compare(i) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTagged reports if a device is tagged
|
||||||
|
// and therefore should not be treated as a
|
||||||
|
// user owned device.
|
||||||
|
// Currently, this function only handles tags set
|
||||||
|
// via CLI ("forced tags" and preauthkeys)
|
||||||
|
func (node *Node) IsTagged() bool {
|
||||||
|
if len(node.ForcedTags) > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.AuthKey != nil && len(node.AuthKey.Tags) > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.Hostinfo == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(kradalby): Figure out how tagging should work
|
||||||
|
// and hostinfo.requestedtags.
|
||||||
|
// Do this in other work.
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasTag reports if a node has a given tag.
|
||||||
|
// Currently, this function only handles tags set
|
||||||
|
// via CLI ("forced tags" and preauthkeys)
|
||||||
|
func (node *Node) HasTag(tag string) bool {
|
||||||
|
if slices.Contains(node.ForcedTags, tag) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.AuthKey != nil && slices.Contains(node.AuthKey.Tags, tag) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(kradalby): Figure out how tagging should work
|
||||||
|
// and hostinfo.requestedtags.
|
||||||
|
// Do this in other work.
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (node *Node) Prefixes() []netip.Prefix {
|
func (node *Node) Prefixes() []netip.Prefix {
|
||||||
addrs := []netip.Prefix{}
|
addrs := []netip.Prefix{}
|
||||||
for _, nodeAddress := range node.IPs() {
|
for _, nodeAddress := range node.IPs() {
|
||||||
|
@ -223,6 +278,16 @@ func (nodes Nodes) FilterByIP(ip netip.Addr) Nodes {
|
||||||
return found
|
return found
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (nodes Nodes) ContainsNodeKey(nodeKey key.NodePublic) bool {
|
||||||
|
for _, node := range nodes {
|
||||||
|
if node.NodeKey == nodeKey {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (node *Node) Proto() *v1.Node {
|
func (node *Node) Proto() *v1.Node {
|
||||||
nodeProto := &v1.Node{
|
nodeProto := &v1.Node{
|
||||||
Id: uint64(node.ID),
|
Id: uint64(node.ID),
|
||||||
|
|
|
@ -26,7 +26,7 @@ type PreAuthKey struct {
|
||||||
|
|
||||||
func (key *PreAuthKey) Proto() *v1.PreAuthKey {
|
func (key *PreAuthKey) Proto() *v1.PreAuthKey {
|
||||||
protoKey := v1.PreAuthKey{
|
protoKey := v1.PreAuthKey{
|
||||||
User: key.User.Name,
|
User: key.User.Username(),
|
||||||
Id: strconv.FormatUint(key.ID, util.Base10),
|
Id: strconv.FormatUint(key.ID, util.Base10),
|
||||||
Key: key.Key,
|
Key: key.Key,
|
||||||
Ephemeral: key.Ephemeral,
|
Ephemeral: key.Ephemeral,
|
||||||
|
|
|
@ -8,7 +8,7 @@ prefixes:
|
||||||
database:
|
database:
|
||||||
type: sqlite3
|
type: sqlite3
|
||||||
|
|
||||||
server_url: "https://derp.no"
|
server_url: "https://server.derp.no"
|
||||||
|
|
||||||
dns:
|
dns:
|
||||||
magic_dns: true
|
magic_dns: true
|
||||||
|
|
|
@ -2,7 +2,11 @@ package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"cmp"
|
"cmp"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"net/mail"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||||
"github.com/juanfont/headscale/hscontrol/util"
|
"github.com/juanfont/headscale/hscontrol/util"
|
||||||
|
@ -13,16 +17,33 @@ import (
|
||||||
|
|
||||||
type UserID uint64
|
type UserID uint64
|
||||||
|
|
||||||
|
type Users []User
|
||||||
|
|
||||||
|
func (u Users) String() string {
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString("[ ")
|
||||||
|
for _, user := range u {
|
||||||
|
fmt.Fprintf(&sb, "%d: %s, ", user.ID, user.Name)
|
||||||
|
}
|
||||||
|
sb.WriteString(" ]")
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
// User is the way Headscale implements the concept of users in Tailscale
|
// User is the way Headscale implements the concept of users in Tailscale
|
||||||
//
|
//
|
||||||
// At the end of the day, users in Tailscale are some kind of 'bubbles' or users
|
// At the end of the day, users in Tailscale are some kind of 'bubbles' or users
|
||||||
// that contain our machines.
|
// that contain our machines.
|
||||||
type User struct {
|
type User struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
|
// The index `idx_name_provider_identifier` is to enforce uniqueness
|
||||||
|
// between Name and ProviderIdentifier. This ensures that
|
||||||
|
// you can have multiple users with the same name in OIDC,
|
||||||
|
// but not if you only run with CLI users.
|
||||||
|
|
||||||
// Username for the user, is used if email is empty
|
// Username for the user, is used if email is empty
|
||||||
// Should not be used, please use Username().
|
// Should not be used, please use Username().
|
||||||
Name string `gorm:"unique"`
|
Name string
|
||||||
|
|
||||||
// Typically the full name of the user
|
// Typically the full name of the user
|
||||||
DisplayName string
|
DisplayName string
|
||||||
|
@ -34,7 +55,7 @@ type User struct {
|
||||||
// Unique identifier of the user from OIDC,
|
// Unique identifier of the user from OIDC,
|
||||||
// comes from `sub` claim in the OIDC token
|
// comes from `sub` claim in the OIDC token
|
||||||
// and is used to lookup the user.
|
// and is used to lookup the user.
|
||||||
ProviderIdentifier string `gorm:"index"`
|
ProviderIdentifier sql.NullString
|
||||||
|
|
||||||
// Provider is the origin of the user account,
|
// Provider is the origin of the user account,
|
||||||
// same as RegistrationMethod, without authkey.
|
// same as RegistrationMethod, without authkey.
|
||||||
|
@ -51,7 +72,12 @@ type User struct {
|
||||||
// should be used throughout headscale, in information returned to the
|
// should be used throughout headscale, in information returned to the
|
||||||
// user and the Policy engine.
|
// user and the Policy engine.
|
||||||
func (u *User) Username() string {
|
func (u *User) Username() string {
|
||||||
return cmp.Or(u.Email, u.Name, u.ProviderIdentifier, strconv.FormatUint(uint64(u.ID), 10))
|
return cmp.Or(
|
||||||
|
u.Email,
|
||||||
|
u.Name,
|
||||||
|
u.ProviderIdentifier.String,
|
||||||
|
strconv.FormatUint(uint64(u.ID), 10),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisplayNameOrUsername returns the DisplayName if it exists, otherwise
|
// DisplayNameOrUsername returns the DisplayName if it exists, otherwise
|
||||||
|
@ -107,7 +133,7 @@ func (u *User) Proto() *v1.User {
|
||||||
CreatedAt: timestamppb.New(u.CreatedAt),
|
CreatedAt: timestamppb.New(u.CreatedAt),
|
||||||
DisplayName: u.DisplayName,
|
DisplayName: u.DisplayName,
|
||||||
Email: u.Email,
|
Email: u.Email,
|
||||||
ProviderId: u.ProviderIdentifier,
|
ProviderId: u.ProviderIdentifier.String,
|
||||||
Provider: u.Provider,
|
Provider: u.Provider,
|
||||||
ProfilePicUrl: u.ProfilePicURL,
|
ProfilePicUrl: u.ProfilePicURL,
|
||||||
}
|
}
|
||||||
|
@ -116,6 +142,7 @@ func (u *User) Proto() *v1.User {
|
||||||
type OIDCClaims struct {
|
type OIDCClaims struct {
|
||||||
// Sub is the user's unique identifier at the provider.
|
// Sub is the user's unique identifier at the provider.
|
||||||
Sub string `json:"sub"`
|
Sub string `json:"sub"`
|
||||||
|
Iss string `json:"iss"`
|
||||||
|
|
||||||
// Name is the user's full name.
|
// Name is the user's full name.
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
|
@ -126,13 +153,27 @@ type OIDCClaims struct {
|
||||||
Username string `json:"preferred_username,omitempty"`
|
Username string `json:"preferred_username,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *OIDCClaims) Identifier() string {
|
||||||
|
return c.Iss + "/" + c.Sub
|
||||||
|
}
|
||||||
|
|
||||||
// FromClaim overrides a User from OIDC claims.
|
// FromClaim overrides a User from OIDC claims.
|
||||||
// All fields will be updated, except for the ID.
|
// All fields will be updated, except for the ID.
|
||||||
func (u *User) FromClaim(claims *OIDCClaims) {
|
func (u *User) FromClaim(claims *OIDCClaims) {
|
||||||
u.ProviderIdentifier = claims.Sub
|
err := util.CheckForFQDNRules(claims.Username)
|
||||||
|
if err == nil {
|
||||||
|
u.Name = claims.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims.EmailVerified {
|
||||||
|
_, err = mail.ParseAddress(claims.Email)
|
||||||
|
if err == nil {
|
||||||
|
u.Email = claims.Email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
u.ProviderIdentifier = sql.NullString{String: claims.Identifier(), Valid: true}
|
||||||
u.DisplayName = claims.Name
|
u.DisplayName = claims.Name
|
||||||
u.Email = claims.Email
|
|
||||||
u.Name = claims.Username
|
|
||||||
u.ProfilePicURL = claims.ProfilePictureURL
|
u.ProfilePicURL = claims.ProfilePictureURL
|
||||||
u.Provider = util.RegisterMethodOIDC
|
u.Provider = util.RegisterMethodOIDC
|
||||||
}
|
}
|
||||||
|
|
|
@ -182,3 +182,33 @@ func GenerateIPv6DNSRootDomain(ipPrefix netip.Prefix) []dnsname.FQDN {
|
||||||
|
|
||||||
return fqdns
|
return fqdns
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(kradalby): Reintroduce when strip_email_domain is removed
|
||||||
|
// after #2170 is cleaned up
|
||||||
|
// DEPRECATED: DO NOT USE
|
||||||
|
// NormalizeToFQDNRules will replace forbidden chars in user
|
||||||
|
// it can also return an error if the user doesn't respect RFC 952 and 1123.
|
||||||
|
func NormalizeToFQDNRules(name string, stripEmailDomain bool) (string, error) {
|
||||||
|
|
||||||
|
name = strings.ToLower(name)
|
||||||
|
name = strings.ReplaceAll(name, "'", "")
|
||||||
|
atIdx := strings.Index(name, "@")
|
||||||
|
if stripEmailDomain && atIdx > 0 {
|
||||||
|
name = name[:atIdx]
|
||||||
|
} else {
|
||||||
|
name = strings.ReplaceAll(name, "@", ".")
|
||||||
|
}
|
||||||
|
name = invalidCharsInUserRegex.ReplaceAllString(name, "-")
|
||||||
|
|
||||||
|
for _, elt := range strings.Split(name, ".") {
|
||||||
|
if len(elt) > LabelHostnameLength {
|
||||||
|
return "", fmt.Errorf(
|
||||||
|
"label %v is more than 63 chars: %w",
|
||||||
|
elt,
|
||||||
|
ErrInvalidUserName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return name, nil
|
||||||
|
}
|
||||||
|
|
|
@ -4,12 +4,13 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGenerateRandomStringDNSSafe(t *testing.T) {
|
func TestGenerateRandomStringDNSSafe(t *testing.T) {
|
||||||
for i := 0; i < 100000; i++ {
|
for i := 0; i < 100000; i++ {
|
||||||
str, err := GenerateRandomStringDNSSafe(8)
|
str, err := GenerateRandomStringDNSSafe(8)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, str, 8)
|
assert.Len(t, str, 8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,10 +11,10 @@ Tests are located in files ending with `_test.go` and the framework are located
|
||||||
|
|
||||||
## Running integration tests locally
|
## Running integration tests locally
|
||||||
|
|
||||||
The easiest way to run tests locally is to use `[act](INSERT LINK)`, a local GitHub Actions runner:
|
The easiest way to run tests locally is to use [act](https://github.com/nektos/act), a local GitHub Actions runner:
|
||||||
|
|
||||||
```
|
```
|
||||||
act pull_request -W .github/workflows/test-integration-v2-TestPingAllByIP.yaml
|
act pull_request -W .github/workflows/test-integration.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternatively, the `docker run` command in each GitHub workflow file can be used.
|
Alternatively, the `docker run` command in each GitHub workflow file can be used.
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"github.com/juanfont/headscale/integration/hsic"
|
"github.com/juanfont/headscale/integration/hsic"
|
||||||
"github.com/juanfont/headscale/integration/tsic"
|
"github.com/juanfont/headscale/integration/tsic"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
var veryLargeDestination = []string{
|
var veryLargeDestination = []string{
|
||||||
|
@ -54,7 +55,7 @@ func aclScenario(
|
||||||
) *Scenario {
|
) *Scenario {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
"user1": clientsPerUser,
|
"user1": clientsPerUser,
|
||||||
|
@ -77,10 +78,10 @@ func aclScenario(
|
||||||
hsic.WithACLPolicy(policy),
|
hsic.WithACLPolicy(policy),
|
||||||
hsic.WithTestName("acl"),
|
hsic.WithTestName("acl"),
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
_, err = scenario.ListTailscaleClientsFQDNs()
|
_, err = scenario.ListTailscaleClientsFQDNs()
|
||||||
assertNoErrListFQDN(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
return scenario
|
return scenario
|
||||||
}
|
}
|
||||||
|
@ -267,7 +268,7 @@ func TestACLHostsInNetMapTable(t *testing.T) {
|
||||||
for name, testCase := range tests {
|
for name, testCase := range tests {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
spec := testCase.users
|
spec := testCase.users
|
||||||
|
|
||||||
|
@ -275,22 +276,22 @@ func TestACLHostsInNetMapTable(t *testing.T) {
|
||||||
[]tsic.Option{},
|
[]tsic.Option{},
|
||||||
hsic.WithACLPolicy(&testCase.policy),
|
hsic.WithACLPolicy(&testCase.policy),
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
allClients, err := scenario.ListTailscaleClients()
|
allClients, err := scenario.ListTailscaleClients()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = scenario.WaitForTailscaleSyncWithPeerCount(testCase.want["user1"])
|
err = scenario.WaitForTailscaleSyncWithPeerCount(testCase.want["user1"])
|
||||||
assertNoErrSync(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
for _, client := range allClients {
|
for _, client := range allClients {
|
||||||
status, err := client.Status()
|
status, err := client.Status()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
user := status.User[status.Self.UserID].LoginName
|
user := status.User[status.Self.UserID].LoginName
|
||||||
|
|
||||||
assert.Equal(t, (testCase.want[user]), len(status.Peer))
|
assert.Len(t, status.Peer, (testCase.want[user]))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -319,23 +320,23 @@ func TestACLAllowUser80Dst(t *testing.T) {
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
user1Clients, err := scenario.ListTailscaleClients("user1")
|
user1Clients, err := scenario.ListTailscaleClients("user1")
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
user2Clients, err := scenario.ListTailscaleClients("user2")
|
user2Clients, err := scenario.ListTailscaleClients("user2")
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Test that user1 can visit all user2
|
// Test that user1 can visit all user2
|
||||||
for _, client := range user1Clients {
|
for _, client := range user1Clients {
|
||||||
for _, peer := range user2Clients {
|
for _, peer := range user2Clients {
|
||||||
fqdn, err := peer.FQDN()
|
fqdn, err := peer.FQDN()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
||||||
t.Logf("url from %s to %s", client.Hostname(), url)
|
t.Logf("url from %s to %s", client.Hostname(), url)
|
||||||
|
|
||||||
result, err := client.Curl(url)
|
result, err := client.Curl(url)
|
||||||
assert.Len(t, result, 13)
|
assert.Len(t, result, 13)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -343,14 +344,14 @@ func TestACLAllowUser80Dst(t *testing.T) {
|
||||||
for _, client := range user2Clients {
|
for _, client := range user2Clients {
|
||||||
for _, peer := range user1Clients {
|
for _, peer := range user1Clients {
|
||||||
fqdn, err := peer.FQDN()
|
fqdn, err := peer.FQDN()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
||||||
t.Logf("url from %s to %s", client.Hostname(), url)
|
t.Logf("url from %s to %s", client.Hostname(), url)
|
||||||
|
|
||||||
result, err := client.Curl(url)
|
result, err := client.Curl(url)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -376,10 +377,10 @@ func TestACLDenyAllPort80(t *testing.T) {
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
allClients, err := scenario.ListTailscaleClients()
|
allClients, err := scenario.ListTailscaleClients()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
allHostnames, err := scenario.ListTailscaleClientsFQDNs()
|
allHostnames, err := scenario.ListTailscaleClientsFQDNs()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
for _, client := range allClients {
|
for _, client := range allClients {
|
||||||
for _, hostname := range allHostnames {
|
for _, hostname := range allHostnames {
|
||||||
|
@ -394,7 +395,7 @@ func TestACLDenyAllPort80(t *testing.T) {
|
||||||
|
|
||||||
result, err := client.Curl(url)
|
result, err := client.Curl(url)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -420,23 +421,23 @@ func TestACLAllowUserDst(t *testing.T) {
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
user1Clients, err := scenario.ListTailscaleClients("user1")
|
user1Clients, err := scenario.ListTailscaleClients("user1")
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
user2Clients, err := scenario.ListTailscaleClients("user2")
|
user2Clients, err := scenario.ListTailscaleClients("user2")
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Test that user1 can visit all user2
|
// Test that user1 can visit all user2
|
||||||
for _, client := range user1Clients {
|
for _, client := range user1Clients {
|
||||||
for _, peer := range user2Clients {
|
for _, peer := range user2Clients {
|
||||||
fqdn, err := peer.FQDN()
|
fqdn, err := peer.FQDN()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
||||||
t.Logf("url from %s to %s", client.Hostname(), url)
|
t.Logf("url from %s to %s", client.Hostname(), url)
|
||||||
|
|
||||||
result, err := client.Curl(url)
|
result, err := client.Curl(url)
|
||||||
assert.Len(t, result, 13)
|
assert.Len(t, result, 13)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -444,14 +445,14 @@ func TestACLAllowUserDst(t *testing.T) {
|
||||||
for _, client := range user2Clients {
|
for _, client := range user2Clients {
|
||||||
for _, peer := range user1Clients {
|
for _, peer := range user1Clients {
|
||||||
fqdn, err := peer.FQDN()
|
fqdn, err := peer.FQDN()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
||||||
t.Logf("url from %s to %s", client.Hostname(), url)
|
t.Logf("url from %s to %s", client.Hostname(), url)
|
||||||
|
|
||||||
result, err := client.Curl(url)
|
result, err := client.Curl(url)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -476,23 +477,23 @@ func TestACLAllowStarDst(t *testing.T) {
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
user1Clients, err := scenario.ListTailscaleClients("user1")
|
user1Clients, err := scenario.ListTailscaleClients("user1")
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
user2Clients, err := scenario.ListTailscaleClients("user2")
|
user2Clients, err := scenario.ListTailscaleClients("user2")
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Test that user1 can visit all user2
|
// Test that user1 can visit all user2
|
||||||
for _, client := range user1Clients {
|
for _, client := range user1Clients {
|
||||||
for _, peer := range user2Clients {
|
for _, peer := range user2Clients {
|
||||||
fqdn, err := peer.FQDN()
|
fqdn, err := peer.FQDN()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
||||||
t.Logf("url from %s to %s", client.Hostname(), url)
|
t.Logf("url from %s to %s", client.Hostname(), url)
|
||||||
|
|
||||||
result, err := client.Curl(url)
|
result, err := client.Curl(url)
|
||||||
assert.Len(t, result, 13)
|
assert.Len(t, result, 13)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -500,14 +501,14 @@ func TestACLAllowStarDst(t *testing.T) {
|
||||||
for _, client := range user2Clients {
|
for _, client := range user2Clients {
|
||||||
for _, peer := range user1Clients {
|
for _, peer := range user1Clients {
|
||||||
fqdn, err := peer.FQDN()
|
fqdn, err := peer.FQDN()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
||||||
t.Logf("url from %s to %s", client.Hostname(), url)
|
t.Logf("url from %s to %s", client.Hostname(), url)
|
||||||
|
|
||||||
result, err := client.Curl(url)
|
result, err := client.Curl(url)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -537,23 +538,23 @@ func TestACLNamedHostsCanReachBySubnet(t *testing.T) {
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
user1Clients, err := scenario.ListTailscaleClients("user1")
|
user1Clients, err := scenario.ListTailscaleClients("user1")
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
user2Clients, err := scenario.ListTailscaleClients("user2")
|
user2Clients, err := scenario.ListTailscaleClients("user2")
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Test that user1 can visit all user2
|
// Test that user1 can visit all user2
|
||||||
for _, client := range user1Clients {
|
for _, client := range user1Clients {
|
||||||
for _, peer := range user2Clients {
|
for _, peer := range user2Clients {
|
||||||
fqdn, err := peer.FQDN()
|
fqdn, err := peer.FQDN()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
||||||
t.Logf("url from %s to %s", client.Hostname(), url)
|
t.Logf("url from %s to %s", client.Hostname(), url)
|
||||||
|
|
||||||
result, err := client.Curl(url)
|
result, err := client.Curl(url)
|
||||||
assert.Len(t, result, 13)
|
assert.Len(t, result, 13)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -561,14 +562,14 @@ func TestACLNamedHostsCanReachBySubnet(t *testing.T) {
|
||||||
for _, client := range user2Clients {
|
for _, client := range user2Clients {
|
||||||
for _, peer := range user1Clients {
|
for _, peer := range user1Clients {
|
||||||
fqdn, err := peer.FQDN()
|
fqdn, err := peer.FQDN()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
||||||
t.Logf("url from %s to %s", client.Hostname(), url)
|
t.Logf("url from %s to %s", client.Hostname(), url)
|
||||||
|
|
||||||
result, err := client.Curl(url)
|
result, err := client.Curl(url)
|
||||||
assert.Len(t, result, 13)
|
assert.Len(t, result, 13)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -679,10 +680,10 @@ func TestACLNamedHostsCanReach(t *testing.T) {
|
||||||
test1ip4 := netip.MustParseAddr("100.64.0.1")
|
test1ip4 := netip.MustParseAddr("100.64.0.1")
|
||||||
test1ip6 := netip.MustParseAddr("fd7a:115c:a1e0::1")
|
test1ip6 := netip.MustParseAddr("fd7a:115c:a1e0::1")
|
||||||
test1, err := scenario.FindTailscaleClientByIP(test1ip6)
|
test1, err := scenario.FindTailscaleClientByIP(test1ip6)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
test1fqdn, err := test1.FQDN()
|
test1fqdn, err := test1.FQDN()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
test1ip4URL := fmt.Sprintf("http://%s/etc/hostname", test1ip4.String())
|
test1ip4URL := fmt.Sprintf("http://%s/etc/hostname", test1ip4.String())
|
||||||
test1ip6URL := fmt.Sprintf("http://[%s]/etc/hostname", test1ip6.String())
|
test1ip6URL := fmt.Sprintf("http://[%s]/etc/hostname", test1ip6.String())
|
||||||
test1fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test1fqdn)
|
test1fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test1fqdn)
|
||||||
|
@ -690,10 +691,10 @@ func TestACLNamedHostsCanReach(t *testing.T) {
|
||||||
test2ip4 := netip.MustParseAddr("100.64.0.2")
|
test2ip4 := netip.MustParseAddr("100.64.0.2")
|
||||||
test2ip6 := netip.MustParseAddr("fd7a:115c:a1e0::2")
|
test2ip6 := netip.MustParseAddr("fd7a:115c:a1e0::2")
|
||||||
test2, err := scenario.FindTailscaleClientByIP(test2ip6)
|
test2, err := scenario.FindTailscaleClientByIP(test2ip6)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
test2fqdn, err := test2.FQDN()
|
test2fqdn, err := test2.FQDN()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
test2ip4URL := fmt.Sprintf("http://%s/etc/hostname", test2ip4.String())
|
test2ip4URL := fmt.Sprintf("http://%s/etc/hostname", test2ip4.String())
|
||||||
test2ip6URL := fmt.Sprintf("http://[%s]/etc/hostname", test2ip6.String())
|
test2ip6URL := fmt.Sprintf("http://[%s]/etc/hostname", test2ip6.String())
|
||||||
test2fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test2fqdn)
|
test2fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test2fqdn)
|
||||||
|
@ -701,10 +702,10 @@ func TestACLNamedHostsCanReach(t *testing.T) {
|
||||||
test3ip4 := netip.MustParseAddr("100.64.0.3")
|
test3ip4 := netip.MustParseAddr("100.64.0.3")
|
||||||
test3ip6 := netip.MustParseAddr("fd7a:115c:a1e0::3")
|
test3ip6 := netip.MustParseAddr("fd7a:115c:a1e0::3")
|
||||||
test3, err := scenario.FindTailscaleClientByIP(test3ip6)
|
test3, err := scenario.FindTailscaleClientByIP(test3ip6)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
test3fqdn, err := test3.FQDN()
|
test3fqdn, err := test3.FQDN()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
test3ip4URL := fmt.Sprintf("http://%s/etc/hostname", test3ip4.String())
|
test3ip4URL := fmt.Sprintf("http://%s/etc/hostname", test3ip4.String())
|
||||||
test3ip6URL := fmt.Sprintf("http://[%s]/etc/hostname", test3ip6.String())
|
test3ip6URL := fmt.Sprintf("http://[%s]/etc/hostname", test3ip6.String())
|
||||||
test3fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test3fqdn)
|
test3fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test3fqdn)
|
||||||
|
@ -719,7 +720,7 @@ func TestACLNamedHostsCanReach(t *testing.T) {
|
||||||
test3ip4URL,
|
test3ip4URL,
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
result, err = test1.Curl(test3ip6URL)
|
result, err = test1.Curl(test3ip6URL)
|
||||||
assert.Lenf(
|
assert.Lenf(
|
||||||
|
@ -730,7 +731,7 @@ func TestACLNamedHostsCanReach(t *testing.T) {
|
||||||
test3ip6URL,
|
test3ip6URL,
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
result, err = test1.Curl(test3fqdnURL)
|
result, err = test1.Curl(test3fqdnURL)
|
||||||
assert.Lenf(
|
assert.Lenf(
|
||||||
|
@ -741,7 +742,7 @@ func TestACLNamedHostsCanReach(t *testing.T) {
|
||||||
test3fqdnURL,
|
test3fqdnURL,
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// test2 can query test3
|
// test2 can query test3
|
||||||
result, err = test2.Curl(test3ip4URL)
|
result, err = test2.Curl(test3ip4URL)
|
||||||
|
@ -753,7 +754,7 @@ func TestACLNamedHostsCanReach(t *testing.T) {
|
||||||
test3ip4URL,
|
test3ip4URL,
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
result, err = test2.Curl(test3ip6URL)
|
result, err = test2.Curl(test3ip6URL)
|
||||||
assert.Lenf(
|
assert.Lenf(
|
||||||
|
@ -764,7 +765,7 @@ func TestACLNamedHostsCanReach(t *testing.T) {
|
||||||
test3ip6URL,
|
test3ip6URL,
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
result, err = test2.Curl(test3fqdnURL)
|
result, err = test2.Curl(test3fqdnURL)
|
||||||
assert.Lenf(
|
assert.Lenf(
|
||||||
|
@ -775,33 +776,33 @@ func TestACLNamedHostsCanReach(t *testing.T) {
|
||||||
test3fqdnURL,
|
test3fqdnURL,
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// test3 cannot query test1
|
// test3 cannot query test1
|
||||||
result, err = test3.Curl(test1ip4URL)
|
result, err = test3.Curl(test1ip4URL)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
result, err = test3.Curl(test1ip6URL)
|
result, err = test3.Curl(test1ip6URL)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
result, err = test3.Curl(test1fqdnURL)
|
result, err = test3.Curl(test1fqdnURL)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
// test3 cannot query test2
|
// test3 cannot query test2
|
||||||
result, err = test3.Curl(test2ip4URL)
|
result, err = test3.Curl(test2ip4URL)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
result, err = test3.Curl(test2ip6URL)
|
result, err = test3.Curl(test2ip6URL)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
result, err = test3.Curl(test2fqdnURL)
|
result, err = test3.Curl(test2fqdnURL)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
// test1 can query test2
|
// test1 can query test2
|
||||||
result, err = test1.Curl(test2ip4URL)
|
result, err = test1.Curl(test2ip4URL)
|
||||||
|
@ -814,7 +815,7 @@ func TestACLNamedHostsCanReach(t *testing.T) {
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
|
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
result, err = test1.Curl(test2ip6URL)
|
result, err = test1.Curl(test2ip6URL)
|
||||||
assert.Lenf(
|
assert.Lenf(
|
||||||
t,
|
t,
|
||||||
|
@ -824,7 +825,7 @@ func TestACLNamedHostsCanReach(t *testing.T) {
|
||||||
test2ip6URL,
|
test2ip6URL,
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
result, err = test1.Curl(test2fqdnURL)
|
result, err = test1.Curl(test2fqdnURL)
|
||||||
assert.Lenf(
|
assert.Lenf(
|
||||||
|
@ -835,20 +836,20 @@ func TestACLNamedHostsCanReach(t *testing.T) {
|
||||||
test2fqdnURL,
|
test2fqdnURL,
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// test2 cannot query test1
|
// test2 cannot query test1
|
||||||
result, err = test2.Curl(test1ip4URL)
|
result, err = test2.Curl(test1ip4URL)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
result, err = test2.Curl(test1ip6URL)
|
result, err = test2.Curl(test1ip6URL)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
result, err = test2.Curl(test1fqdnURL)
|
result, err = test2.Curl(test1fqdnURL)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -946,10 +947,10 @@ func TestACLDevice1CanAccessDevice2(t *testing.T) {
|
||||||
test1ip6 := netip.MustParseAddr("fd7a:115c:a1e0::1")
|
test1ip6 := netip.MustParseAddr("fd7a:115c:a1e0::1")
|
||||||
test1, err := scenario.FindTailscaleClientByIP(test1ip)
|
test1, err := scenario.FindTailscaleClientByIP(test1ip)
|
||||||
assert.NotNil(t, test1)
|
assert.NotNil(t, test1)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
test1fqdn, err := test1.FQDN()
|
test1fqdn, err := test1.FQDN()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
test1ipURL := fmt.Sprintf("http://%s/etc/hostname", test1ip.String())
|
test1ipURL := fmt.Sprintf("http://%s/etc/hostname", test1ip.String())
|
||||||
test1ip6URL := fmt.Sprintf("http://[%s]/etc/hostname", test1ip6.String())
|
test1ip6URL := fmt.Sprintf("http://[%s]/etc/hostname", test1ip6.String())
|
||||||
test1fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test1fqdn)
|
test1fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test1fqdn)
|
||||||
|
@ -958,10 +959,10 @@ func TestACLDevice1CanAccessDevice2(t *testing.T) {
|
||||||
test2ip6 := netip.MustParseAddr("fd7a:115c:a1e0::2")
|
test2ip6 := netip.MustParseAddr("fd7a:115c:a1e0::2")
|
||||||
test2, err := scenario.FindTailscaleClientByIP(test2ip)
|
test2, err := scenario.FindTailscaleClientByIP(test2ip)
|
||||||
assert.NotNil(t, test2)
|
assert.NotNil(t, test2)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
test2fqdn, err := test2.FQDN()
|
test2fqdn, err := test2.FQDN()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
test2ipURL := fmt.Sprintf("http://%s/etc/hostname", test2ip.String())
|
test2ipURL := fmt.Sprintf("http://%s/etc/hostname", test2ip.String())
|
||||||
test2ip6URL := fmt.Sprintf("http://[%s]/etc/hostname", test2ip6.String())
|
test2ip6URL := fmt.Sprintf("http://[%s]/etc/hostname", test2ip6.String())
|
||||||
test2fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test2fqdn)
|
test2fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test2fqdn)
|
||||||
|
@ -976,7 +977,7 @@ func TestACLDevice1CanAccessDevice2(t *testing.T) {
|
||||||
test2ipURL,
|
test2ipURL,
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
result, err = test1.Curl(test2ip6URL)
|
result, err = test1.Curl(test2ip6URL)
|
||||||
assert.Lenf(
|
assert.Lenf(
|
||||||
|
@ -987,7 +988,7 @@ func TestACLDevice1CanAccessDevice2(t *testing.T) {
|
||||||
test2ip6URL,
|
test2ip6URL,
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
result, err = test1.Curl(test2fqdnURL)
|
result, err = test1.Curl(test2fqdnURL)
|
||||||
assert.Lenf(
|
assert.Lenf(
|
||||||
|
@ -998,19 +999,19 @@ func TestACLDevice1CanAccessDevice2(t *testing.T) {
|
||||||
test2fqdnURL,
|
test2fqdnURL,
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
result, err = test2.Curl(test1ipURL)
|
result, err = test2.Curl(test1ipURL)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
result, err = test2.Curl(test1ip6URL)
|
result, err = test2.Curl(test1ip6URL)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
result, err = test2.Curl(test1fqdnURL)
|
result, err = test2.Curl(test1fqdnURL)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1020,7 +1021,7 @@ func TestPolicyUpdateWhileRunningWithCLIInDatabase(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
|
@ -1046,19 +1047,19 @@ func TestPolicyUpdateWhileRunningWithCLIInDatabase(t *testing.T) {
|
||||||
"HEADSCALE_POLICY_MODE": "database",
|
"HEADSCALE_POLICY_MODE": "database",
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
_, err = scenario.ListTailscaleClientsFQDNs()
|
_, err = scenario.ListTailscaleClientsFQDNs()
|
||||||
assertNoErrListFQDN(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = scenario.WaitForTailscaleSync()
|
err = scenario.WaitForTailscaleSync()
|
||||||
assertNoErrSync(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
user1Clients, err := scenario.ListTailscaleClients("user1")
|
user1Clients, err := scenario.ListTailscaleClients("user1")
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
user2Clients, err := scenario.ListTailscaleClients("user2")
|
user2Clients, err := scenario.ListTailscaleClients("user2")
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
all := append(user1Clients, user2Clients...)
|
all := append(user1Clients, user2Clients...)
|
||||||
|
|
||||||
|
@ -1070,19 +1071,19 @@ func TestPolicyUpdateWhileRunningWithCLIInDatabase(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fqdn, err := peer.FQDN()
|
fqdn, err := peer.FQDN()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
||||||
t.Logf("url from %s to %s", client.Hostname(), url)
|
t.Logf("url from %s to %s", client.Hostname(), url)
|
||||||
|
|
||||||
result, err := client.Curl(url)
|
result, err := client.Curl(url)
|
||||||
assert.Len(t, result, 13)
|
assert.Len(t, result, 13)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
headscale, err := scenario.Headscale()
|
headscale, err := scenario.Headscale()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
p := policy.ACLPolicy{
|
p := policy.ACLPolicy{
|
||||||
ACLs: []policy.ACL{
|
ACLs: []policy.ACL{
|
||||||
|
@ -1100,7 +1101,7 @@ func TestPolicyUpdateWhileRunningWithCLIInDatabase(t *testing.T) {
|
||||||
policyFilePath := "/etc/headscale/policy.json"
|
policyFilePath := "/etc/headscale/policy.json"
|
||||||
|
|
||||||
err = headscale.WriteFile(policyFilePath, pBytes)
|
err = headscale.WriteFile(policyFilePath, pBytes)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// No policy is present at this time.
|
// No policy is present at this time.
|
||||||
// Add a new policy from a file.
|
// Add a new policy from a file.
|
||||||
|
@ -1113,7 +1114,7 @@ func TestPolicyUpdateWhileRunningWithCLIInDatabase(t *testing.T) {
|
||||||
policyFilePath,
|
policyFilePath,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Get the current policy and check
|
// Get the current policy and check
|
||||||
// if it is the same as the one we set.
|
// if it is the same as the one we set.
|
||||||
|
@ -1129,7 +1130,7 @@ func TestPolicyUpdateWhileRunningWithCLIInDatabase(t *testing.T) {
|
||||||
},
|
},
|
||||||
&output,
|
&output,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, output.ACLs, 1)
|
assert.Len(t, output.ACLs, 1)
|
||||||
|
|
||||||
|
@ -1141,14 +1142,14 @@ func TestPolicyUpdateWhileRunningWithCLIInDatabase(t *testing.T) {
|
||||||
for _, client := range user1Clients {
|
for _, client := range user1Clients {
|
||||||
for _, peer := range user2Clients {
|
for _, peer := range user2Clients {
|
||||||
fqdn, err := peer.FQDN()
|
fqdn, err := peer.FQDN()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
||||||
t.Logf("url from %s to %s", client.Hostname(), url)
|
t.Logf("url from %s to %s", client.Hostname(), url)
|
||||||
|
|
||||||
result, err := client.Curl(url)
|
result, err := client.Curl(url)
|
||||||
assert.Len(t, result, 13)
|
assert.Len(t, result, 13)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1156,14 +1157,14 @@ func TestPolicyUpdateWhileRunningWithCLIInDatabase(t *testing.T) {
|
||||||
for _, client := range user2Clients {
|
for _, client := range user2Clients {
|
||||||
for _, peer := range user1Clients {
|
for _, peer := range user1Clients {
|
||||||
fqdn, err := peer.FQDN()
|
fqdn, err := peer.FQDN()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
||||||
t.Logf("url from %s to %s", client.Hostname(), url)
|
t.Logf("url from %s to %s", client.Hostname(), url)
|
||||||
|
|
||||||
result, err := client.Curl(url)
|
result, err := client.Curl(url)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package integration
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
@ -10,14 +11,19 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||||
"github.com/juanfont/headscale/hscontrol/types"
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
"github.com/juanfont/headscale/hscontrol/util"
|
"github.com/juanfont/headscale/hscontrol/util"
|
||||||
"github.com/juanfont/headscale/integration/dockertestutil"
|
"github.com/juanfont/headscale/integration/dockertestutil"
|
||||||
"github.com/juanfont/headscale/integration/hsic"
|
"github.com/juanfont/headscale/integration/hsic"
|
||||||
|
"github.com/oauth2-proxy/mockoidc"
|
||||||
"github.com/ory/dockertest/v3"
|
"github.com/ory/dockertest/v3"
|
||||||
"github.com/ory/dockertest/v3/docker"
|
"github.com/ory/dockertest/v3/docker"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
|
@ -48,20 +54,34 @@ func TestOIDCAuthenticationPingAll(t *testing.T) {
|
||||||
scenario := AuthOIDCScenario{
|
scenario := AuthOIDCScenario{
|
||||||
Scenario: baseScenario,
|
Scenario: baseScenario,
|
||||||
}
|
}
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
// defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
|
// Logins to MockOIDC is served by a queue with a strict order,
|
||||||
|
// if we use more than one node per user, the order of the logins
|
||||||
|
// will not be deterministic and the test will fail.
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
"user1": len(MustTestVersions),
|
"user1": 1,
|
||||||
|
"user2": 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
oidcConfig, err := scenario.runMockOIDC(defaultAccessTTL)
|
mockusers := []mockoidc.MockUser{
|
||||||
|
oidcMockUser("user1", true),
|
||||||
|
oidcMockUser("user2", false),
|
||||||
|
}
|
||||||
|
|
||||||
|
oidcConfig, err := scenario.runMockOIDC(defaultAccessTTL, mockusers)
|
||||||
assertNoErrf(t, "failed to run mock OIDC server: %s", err)
|
assertNoErrf(t, "failed to run mock OIDC server: %s", err)
|
||||||
|
defer scenario.mockOIDC.Close()
|
||||||
|
|
||||||
oidcMap := map[string]string{
|
oidcMap := map[string]string{
|
||||||
"HEADSCALE_OIDC_ISSUER": oidcConfig.Issuer,
|
"HEADSCALE_OIDC_ISSUER": oidcConfig.Issuer,
|
||||||
"HEADSCALE_OIDC_CLIENT_ID": oidcConfig.ClientID,
|
"HEADSCALE_OIDC_CLIENT_ID": oidcConfig.ClientID,
|
||||||
"CREDENTIALS_DIRECTORY_TEST": "/tmp",
|
"CREDENTIALS_DIRECTORY_TEST": "/tmp",
|
||||||
"HEADSCALE_OIDC_CLIENT_SECRET_PATH": "${CREDENTIALS_DIRECTORY_TEST}/hs_client_oidc_secret",
|
"HEADSCALE_OIDC_CLIENT_SECRET_PATH": "${CREDENTIALS_DIRECTORY_TEST}/hs_client_oidc_secret",
|
||||||
|
// TODO(kradalby): Remove when strip_email_domain is removed
|
||||||
|
// after #2170 is cleaned up
|
||||||
|
"HEADSCALE_OIDC_MAP_LEGACY_USERS": "0",
|
||||||
|
"HEADSCALE_OIDC_STRIP_EMAIL_DOMAIN": "0",
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(
|
err = scenario.CreateHeadscaleEnv(
|
||||||
|
@ -91,6 +111,55 @@ func TestOIDCAuthenticationPingAll(t *testing.T) {
|
||||||
|
|
||||||
success := pingAllHelper(t, allClients, allAddrs)
|
success := pingAllHelper(t, allClients, allAddrs)
|
||||||
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
|
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
|
||||||
|
|
||||||
|
headscale, err := scenario.Headscale()
|
||||||
|
assertNoErr(t, err)
|
||||||
|
|
||||||
|
var listUsers []v1.User
|
||||||
|
err = executeAndUnmarshal(headscale,
|
||||||
|
[]string{
|
||||||
|
"headscale",
|
||||||
|
"users",
|
||||||
|
"list",
|
||||||
|
"--output",
|
||||||
|
"json",
|
||||||
|
},
|
||||||
|
&listUsers,
|
||||||
|
)
|
||||||
|
assertNoErr(t, err)
|
||||||
|
|
||||||
|
want := []v1.User{
|
||||||
|
{
|
||||||
|
Id: "1",
|
||||||
|
Name: "user1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "2",
|
||||||
|
Name: "user1",
|
||||||
|
Email: "user1@headscale.net",
|
||||||
|
Provider: "oidc",
|
||||||
|
ProviderId: oidcConfig.Issuer + "/user1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "3",
|
||||||
|
Name: "user2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "4",
|
||||||
|
Name: "user2",
|
||||||
|
Email: "", // Unverified
|
||||||
|
Provider: "oidc",
|
||||||
|
ProviderId: oidcConfig.Issuer + "/user2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(listUsers, func(i, j int) bool {
|
||||||
|
return listUsers[i].Id < listUsers[j].Id
|
||||||
|
})
|
||||||
|
|
||||||
|
if diff := cmp.Diff(want, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
|
||||||
|
t.Fatalf("unexpected users: %s", diff)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This test is really flaky.
|
// This test is really flaky.
|
||||||
|
@ -111,11 +180,16 @@ func TestOIDCExpireNodesBasedOnTokenExpiry(t *testing.T) {
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
"user1": 3,
|
"user1": 1,
|
||||||
|
"user2": 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
oidcConfig, err := scenario.runMockOIDC(shortAccessTTL)
|
oidcConfig, err := scenario.runMockOIDC(shortAccessTTL, []mockoidc.MockUser{
|
||||||
|
oidcMockUser("user1", true),
|
||||||
|
oidcMockUser("user2", false),
|
||||||
|
})
|
||||||
assertNoErrf(t, "failed to run mock OIDC server: %s", err)
|
assertNoErrf(t, "failed to run mock OIDC server: %s", err)
|
||||||
|
defer scenario.mockOIDC.Close()
|
||||||
|
|
||||||
oidcMap := map[string]string{
|
oidcMap := map[string]string{
|
||||||
"HEADSCALE_OIDC_ISSUER": oidcConfig.Issuer,
|
"HEADSCALE_OIDC_ISSUER": oidcConfig.Issuer,
|
||||||
|
@ -159,6 +233,297 @@ func TestOIDCExpireNodesBasedOnTokenExpiry(t *testing.T) {
|
||||||
assertTailscaleNodesLogout(t, allClients)
|
assertTailscaleNodesLogout(t, allClients)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(kradalby):
|
||||||
|
// - Test that creates a new user when one exists when migration is turned off
|
||||||
|
// - Test that takes over a user when one exists when migration is turned on
|
||||||
|
// - But email is not verified
|
||||||
|
// - stripped email domain on/off
|
||||||
|
func TestOIDC024UserCreation(t *testing.T) {
|
||||||
|
IntegrationSkip(t)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config map[string]string
|
||||||
|
emailVerified bool
|
||||||
|
cliUsers []string
|
||||||
|
oidcUsers []string
|
||||||
|
want func(iss string) []v1.User
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no-migration-verified-email",
|
||||||
|
config: map[string]string{
|
||||||
|
"HEADSCALE_OIDC_MAP_LEGACY_USERS": "0",
|
||||||
|
},
|
||||||
|
emailVerified: true,
|
||||||
|
cliUsers: []string{"user1", "user2"},
|
||||||
|
oidcUsers: []string{"user1", "user2"},
|
||||||
|
want: func(iss string) []v1.User {
|
||||||
|
return []v1.User{
|
||||||
|
{
|
||||||
|
Id: "1",
|
||||||
|
Name: "user1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "2",
|
||||||
|
Name: "user1",
|
||||||
|
Email: "user1@headscale.net",
|
||||||
|
Provider: "oidc",
|
||||||
|
ProviderId: iss + "/user1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "3",
|
||||||
|
Name: "user2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "4",
|
||||||
|
Name: "user2",
|
||||||
|
Email: "user2@headscale.net",
|
||||||
|
Provider: "oidc",
|
||||||
|
ProviderId: iss + "/user2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no-migration-not-verified-email",
|
||||||
|
config: map[string]string{
|
||||||
|
"HEADSCALE_OIDC_MAP_LEGACY_USERS": "0",
|
||||||
|
},
|
||||||
|
emailVerified: false,
|
||||||
|
cliUsers: []string{"user1", "user2"},
|
||||||
|
oidcUsers: []string{"user1", "user2"},
|
||||||
|
want: func(iss string) []v1.User {
|
||||||
|
return []v1.User{
|
||||||
|
{
|
||||||
|
Id: "1",
|
||||||
|
Name: "user1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "2",
|
||||||
|
Name: "user1",
|
||||||
|
Provider: "oidc",
|
||||||
|
ProviderId: iss + "/user1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "3",
|
||||||
|
Name: "user2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "4",
|
||||||
|
Name: "user2",
|
||||||
|
Provider: "oidc",
|
||||||
|
ProviderId: iss + "/user2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "migration-strip-domains-verified-email",
|
||||||
|
config: map[string]string{
|
||||||
|
"HEADSCALE_OIDC_MAP_LEGACY_USERS": "1",
|
||||||
|
"HEADSCALE_OIDC_STRIP_EMAIL_DOMAIN": "1",
|
||||||
|
},
|
||||||
|
emailVerified: true,
|
||||||
|
cliUsers: []string{"user1", "user2"},
|
||||||
|
oidcUsers: []string{"user1", "user2"},
|
||||||
|
want: func(iss string) []v1.User {
|
||||||
|
return []v1.User{
|
||||||
|
{
|
||||||
|
Id: "1",
|
||||||
|
Name: "user1",
|
||||||
|
Email: "user1@headscale.net",
|
||||||
|
Provider: "oidc",
|
||||||
|
ProviderId: iss + "/user1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "2",
|
||||||
|
Name: "user2",
|
||||||
|
Email: "user2@headscale.net",
|
||||||
|
Provider: "oidc",
|
||||||
|
ProviderId: iss + "/user2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "migration-strip-domains-not-verified-email",
|
||||||
|
config: map[string]string{
|
||||||
|
"HEADSCALE_OIDC_MAP_LEGACY_USERS": "1",
|
||||||
|
"HEADSCALE_OIDC_STRIP_EMAIL_DOMAIN": "1",
|
||||||
|
},
|
||||||
|
emailVerified: false,
|
||||||
|
cliUsers: []string{"user1", "user2"},
|
||||||
|
oidcUsers: []string{"user1", "user2"},
|
||||||
|
want: func(iss string) []v1.User {
|
||||||
|
return []v1.User{
|
||||||
|
{
|
||||||
|
Id: "1",
|
||||||
|
Name: "user1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "2",
|
||||||
|
Name: "user1",
|
||||||
|
Provider: "oidc",
|
||||||
|
ProviderId: iss + "/user1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "3",
|
||||||
|
Name: "user2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "4",
|
||||||
|
Name: "user2",
|
||||||
|
Provider: "oidc",
|
||||||
|
ProviderId: iss + "/user2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "migration-no-strip-domains-verified-email",
|
||||||
|
config: map[string]string{
|
||||||
|
"HEADSCALE_OIDC_MAP_LEGACY_USERS": "1",
|
||||||
|
"HEADSCALE_OIDC_STRIP_EMAIL_DOMAIN": "0",
|
||||||
|
},
|
||||||
|
emailVerified: true,
|
||||||
|
cliUsers: []string{"user1.headscale.net", "user2.headscale.net"},
|
||||||
|
oidcUsers: []string{"user1", "user2"},
|
||||||
|
want: func(iss string) []v1.User {
|
||||||
|
return []v1.User{
|
||||||
|
// Hmm I think we will have to overwrite the initial name here
|
||||||
|
// createuser with "user1.headscale.net", but oidc with "user1"
|
||||||
|
{
|
||||||
|
Id: "1",
|
||||||
|
Name: "user1",
|
||||||
|
Email: "user1@headscale.net",
|
||||||
|
Provider: "oidc",
|
||||||
|
ProviderId: iss + "/user1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "2",
|
||||||
|
Name: "user2",
|
||||||
|
Email: "user2@headscale.net",
|
||||||
|
Provider: "oidc",
|
||||||
|
ProviderId: iss + "/user2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "migration-no-strip-domains-not-verified-email",
|
||||||
|
config: map[string]string{
|
||||||
|
"HEADSCALE_OIDC_MAP_LEGACY_USERS": "1",
|
||||||
|
"HEADSCALE_OIDC_STRIP_EMAIL_DOMAIN": "0",
|
||||||
|
},
|
||||||
|
emailVerified: false,
|
||||||
|
cliUsers: []string{"user1.headscale.net", "user2.headscale.net"},
|
||||||
|
oidcUsers: []string{"user1", "user2"},
|
||||||
|
want: func(iss string) []v1.User {
|
||||||
|
return []v1.User{
|
||||||
|
{
|
||||||
|
Id: "1",
|
||||||
|
Name: "user1.headscale.net",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "2",
|
||||||
|
Name: "user1",
|
||||||
|
Provider: "oidc",
|
||||||
|
ProviderId: iss + "/user1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "3",
|
||||||
|
Name: "user2.headscale.net",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "4",
|
||||||
|
Name: "user2",
|
||||||
|
Provider: "oidc",
|
||||||
|
ProviderId: iss + "/user2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
baseScenario, err := NewScenario(dockertestMaxWait())
|
||||||
|
assertNoErr(t, err)
|
||||||
|
|
||||||
|
scenario := AuthOIDCScenario{
|
||||||
|
Scenario: baseScenario,
|
||||||
|
}
|
||||||
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
|
spec := map[string]int{}
|
||||||
|
for _, user := range tt.cliUsers {
|
||||||
|
spec[user] = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var mockusers []mockoidc.MockUser
|
||||||
|
for _, user := range tt.oidcUsers {
|
||||||
|
mockusers = append(mockusers, oidcMockUser(user, tt.emailVerified))
|
||||||
|
}
|
||||||
|
|
||||||
|
oidcConfig, err := scenario.runMockOIDC(defaultAccessTTL, mockusers)
|
||||||
|
assertNoErrf(t, "failed to run mock OIDC server: %s", err)
|
||||||
|
defer scenario.mockOIDC.Close()
|
||||||
|
|
||||||
|
oidcMap := map[string]string{
|
||||||
|
"HEADSCALE_OIDC_ISSUER": oidcConfig.Issuer,
|
||||||
|
"HEADSCALE_OIDC_CLIENT_ID": oidcConfig.ClientID,
|
||||||
|
"CREDENTIALS_DIRECTORY_TEST": "/tmp",
|
||||||
|
"HEADSCALE_OIDC_CLIENT_SECRET_PATH": "${CREDENTIALS_DIRECTORY_TEST}/hs_client_oidc_secret",
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range tt.config {
|
||||||
|
oidcMap[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
err = scenario.CreateHeadscaleEnv(
|
||||||
|
spec,
|
||||||
|
hsic.WithTestName("oidcmigration"),
|
||||||
|
hsic.WithConfigEnv(oidcMap),
|
||||||
|
hsic.WithTLS(),
|
||||||
|
hsic.WithHostnameAsServerURL(),
|
||||||
|
hsic.WithFileInContainer("/tmp/hs_client_oidc_secret", []byte(oidcConfig.ClientSecret)),
|
||||||
|
)
|
||||||
|
assertNoErrHeadscaleEnv(t, err)
|
||||||
|
|
||||||
|
// Ensure that the nodes have logged in, this is what
|
||||||
|
// triggers user creation via OIDC.
|
||||||
|
err = scenario.WaitForTailscaleSync()
|
||||||
|
assertNoErrSync(t, err)
|
||||||
|
|
||||||
|
headscale, err := scenario.Headscale()
|
||||||
|
assertNoErr(t, err)
|
||||||
|
|
||||||
|
want := tt.want(oidcConfig.Issuer)
|
||||||
|
|
||||||
|
var listUsers []v1.User
|
||||||
|
err = executeAndUnmarshal(headscale,
|
||||||
|
[]string{
|
||||||
|
"headscale",
|
||||||
|
"users",
|
||||||
|
"list",
|
||||||
|
"--output",
|
||||||
|
"json",
|
||||||
|
},
|
||||||
|
&listUsers,
|
||||||
|
)
|
||||||
|
assertNoErr(t, err)
|
||||||
|
|
||||||
|
sort.Slice(listUsers, func(i, j int) bool {
|
||||||
|
return listUsers[i].Id < listUsers[j].Id
|
||||||
|
})
|
||||||
|
|
||||||
|
if diff := cmp.Diff(want, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
|
||||||
|
t.Errorf("unexpected users: %s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *AuthOIDCScenario) CreateHeadscaleEnv(
|
func (s *AuthOIDCScenario) CreateHeadscaleEnv(
|
||||||
users map[string]int,
|
users map[string]int,
|
||||||
opts ...hsic.Option,
|
opts ...hsic.Option,
|
||||||
|
@ -174,6 +539,13 @@ func (s *AuthOIDCScenario) CreateHeadscaleEnv(
|
||||||
}
|
}
|
||||||
|
|
||||||
for userName, clientCount := range users {
|
for userName, clientCount := range users {
|
||||||
|
if clientCount != 1 {
|
||||||
|
// OIDC scenario only supports one client per user.
|
||||||
|
// This is because the MockOIDC server can only serve login
|
||||||
|
// requests based on a queue it has been given on startup.
|
||||||
|
// We currently only populates it with one login request per user.
|
||||||
|
return fmt.Errorf("client count must be 1 for OIDC scenario.")
|
||||||
|
}
|
||||||
log.Printf("creating user %s with %d clients", userName, clientCount)
|
log.Printf("creating user %s with %d clients", userName, clientCount)
|
||||||
err = s.CreateUser(userName)
|
err = s.CreateUser(userName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -194,7 +566,7 @@ func (s *AuthOIDCScenario) CreateHeadscaleEnv(
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AuthOIDCScenario) runMockOIDC(accessTTL time.Duration) (*types.OIDCConfig, error) {
|
func (s *AuthOIDCScenario) runMockOIDC(accessTTL time.Duration, users []mockoidc.MockUser) (*types.OIDCConfig, error) {
|
||||||
port, err := dockertestutil.RandomFreeHostPort()
|
port, err := dockertestutil.RandomFreeHostPort()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("could not find an open port: %s", err)
|
log.Fatalf("could not find an open port: %s", err)
|
||||||
|
@ -205,6 +577,11 @@ func (s *AuthOIDCScenario) runMockOIDC(accessTTL time.Duration) (*types.OIDCConf
|
||||||
|
|
||||||
hostname := fmt.Sprintf("hs-oidcmock-%s", hash)
|
hostname := fmt.Sprintf("hs-oidcmock-%s", hash)
|
||||||
|
|
||||||
|
usersJSON, err := json.Marshal(users)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
mockOidcOptions := &dockertest.RunOptions{
|
mockOidcOptions := &dockertest.RunOptions{
|
||||||
Name: hostname,
|
Name: hostname,
|
||||||
Cmd: []string{"headscale", "mockoidc"},
|
Cmd: []string{"headscale", "mockoidc"},
|
||||||
|
@ -219,11 +596,12 @@ func (s *AuthOIDCScenario) runMockOIDC(accessTTL time.Duration) (*types.OIDCConf
|
||||||
"MOCKOIDC_CLIENT_ID=superclient",
|
"MOCKOIDC_CLIENT_ID=superclient",
|
||||||
"MOCKOIDC_CLIENT_SECRET=supersecret",
|
"MOCKOIDC_CLIENT_SECRET=supersecret",
|
||||||
fmt.Sprintf("MOCKOIDC_ACCESS_TTL=%s", accessTTL.String()),
|
fmt.Sprintf("MOCKOIDC_ACCESS_TTL=%s", accessTTL.String()),
|
||||||
|
fmt.Sprintf("MOCKOIDC_USERS=%s", string(usersJSON)),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
headscaleBuildOptions := &dockertest.BuildOptions{
|
headscaleBuildOptions := &dockertest.BuildOptions{
|
||||||
Dockerfile: "Dockerfile.debug",
|
Dockerfile: hsic.IntegrationTestDockerFileName,
|
||||||
ContextDir: dockerContextPath,
|
ContextDir: dockerContextPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -310,45 +688,40 @@ func (s *AuthOIDCScenario) runTailscaleUp(
|
||||||
|
|
||||||
log.Printf("%s login url: %s\n", c.Hostname(), loginURL.String())
|
log.Printf("%s login url: %s\n", c.Hostname(), loginURL.String())
|
||||||
|
|
||||||
if err := s.pool.Retry(func() error {
|
log.Printf("%s logging in with url", c.Hostname())
|
||||||
log.Printf("%s logging in with url", c.Hostname())
|
httpClient := &http.Client{Transport: insecureTransport}
|
||||||
httpClient := &http.Client{Transport: insecureTransport}
|
ctx := context.Background()
|
||||||
ctx := context.Background()
|
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, loginURL.String(), nil)
|
||||||
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, loginURL.String(), nil)
|
resp, err := httpClient.Do(req)
|
||||||
resp, err := httpClient.Do(req)
|
if err != nil {
|
||||||
if err != nil {
|
log.Printf(
|
||||||
log.Printf(
|
"%s failed to login using url %s: %s",
|
||||||
"%s failed to login using url %s: %s",
|
c.Hostname(),
|
||||||
c.Hostname(),
|
loginURL,
|
||||||
loginURL,
|
err,
|
||||||
err,
|
)
|
||||||
)
|
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
log.Printf("%s response code of oidc login request was %s", c.Hostname(), resp.Status)
|
log.Printf("%s response code of oidc login request was %s", c.Hostname(), resp.Status)
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
log.Printf("body: %s", body)
|
||||||
|
|
||||||
return errStatusCodeNotOK
|
return errStatusCodeNotOK
|
||||||
}
|
}
|
||||||
|
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
_, err = io.ReadAll(resp.Body)
|
_, err = io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("%s failed to read response body: %s", c.Hostname(), err)
|
log.Printf("%s failed to read response body: %s", c.Hostname(), err)
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Finished request for %s to join tailnet", c.Hostname())
|
log.Printf("Finished request for %s to join tailnet", c.Hostname())
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -395,3 +768,12 @@ func assertTailscaleNodesLogout(t *testing.T, clients []TailscaleClient) {
|
||||||
assert.Equal(t, "NeedsLogin", status.BackendState)
|
assert.Equal(t, "NeedsLogin", status.BackendState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func oidcMockUser(username string, emailVerified bool) mockoidc.MockUser {
|
||||||
|
return mockoidc.MockUser{
|
||||||
|
Subject: username,
|
||||||
|
PreferredUsername: username,
|
||||||
|
Email: fmt.Sprintf("%s@headscale.net", username),
|
||||||
|
EmailVerified: emailVerified,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/juanfont/headscale/integration/hsic"
|
"github.com/juanfont/headscale/integration/hsic"
|
||||||
"github.com/juanfont/headscale/integration/tsic"
|
"github.com/juanfont/headscale/integration/tsic"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func executeAndUnmarshal[T any](headscale ControlServer, command []string, result T) error {
|
func executeAndUnmarshal[T any](headscale ControlServer, command []string, result T) error {
|
||||||
|
@ -34,7 +35,7 @@ func TestUserCommand(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
|
@ -43,10 +44,10 @@ func TestUserCommand(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins"))
|
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins"))
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
headscale, err := scenario.Headscale()
|
headscale, err := scenario.Headscale()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var listUsers []v1.User
|
var listUsers []v1.User
|
||||||
err = executeAndUnmarshal(headscale,
|
err = executeAndUnmarshal(headscale,
|
||||||
|
@ -59,7 +60,7 @@ func TestUserCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listUsers,
|
&listUsers,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
result := []string{listUsers[0].GetName(), listUsers[1].GetName()}
|
result := []string{listUsers[0].GetName(), listUsers[1].GetName()}
|
||||||
sort.Strings(result)
|
sort.Strings(result)
|
||||||
|
@ -81,7 +82,7 @@ func TestUserCommand(t *testing.T) {
|
||||||
"newname",
|
"newname",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var listAfterRenameUsers []v1.User
|
var listAfterRenameUsers []v1.User
|
||||||
err = executeAndUnmarshal(headscale,
|
err = executeAndUnmarshal(headscale,
|
||||||
|
@ -94,7 +95,7 @@ func TestUserCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listAfterRenameUsers,
|
&listAfterRenameUsers,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
result = []string{listAfterRenameUsers[0].GetName(), listAfterRenameUsers[1].GetName()}
|
result = []string{listAfterRenameUsers[0].GetName(), listAfterRenameUsers[1].GetName()}
|
||||||
sort.Strings(result)
|
sort.Strings(result)
|
||||||
|
@ -114,7 +115,7 @@ func TestPreAuthKeyCommand(t *testing.T) {
|
||||||
count := 3
|
count := 3
|
||||||
|
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
|
@ -122,13 +123,13 @@ func TestPreAuthKeyCommand(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clipak"))
|
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clipak"))
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
headscale, err := scenario.Headscale()
|
headscale, err := scenario.Headscale()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
keys := make([]*v1.PreAuthKey, count)
|
keys := make([]*v1.PreAuthKey, count)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
for index := 0; index < count; index++ {
|
for index := 0; index < count; index++ {
|
||||||
var preAuthKey v1.PreAuthKey
|
var preAuthKey v1.PreAuthKey
|
||||||
|
@ -150,7 +151,7 @@ func TestPreAuthKeyCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&preAuthKey,
|
&preAuthKey,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
keys[index] = &preAuthKey
|
keys[index] = &preAuthKey
|
||||||
}
|
}
|
||||||
|
@ -171,7 +172,7 @@ func TestPreAuthKeyCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listedPreAuthKeys,
|
&listedPreAuthKeys,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// There is one key created by "scenario.CreateHeadscaleEnv"
|
// There is one key created by "scenario.CreateHeadscaleEnv"
|
||||||
assert.Len(t, listedPreAuthKeys, 4)
|
assert.Len(t, listedPreAuthKeys, 4)
|
||||||
|
@ -212,7 +213,9 @@ func TestPreAuthKeyCommand(t *testing.T) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, listedPreAuthKeys[index].GetAclTags(), []string{"tag:test1", "tag:test2"})
|
tags := listedPreAuthKeys[index].GetAclTags()
|
||||||
|
sort.Strings(tags)
|
||||||
|
assert.Equal(t, []string{"tag:test1", "tag:test2"}, tags)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test key expiry
|
// Test key expiry
|
||||||
|
@ -226,7 +229,7 @@ func TestPreAuthKeyCommand(t *testing.T) {
|
||||||
listedPreAuthKeys[1].GetKey(),
|
listedPreAuthKeys[1].GetKey(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var listedPreAuthKeysAfterExpire []v1.PreAuthKey
|
var listedPreAuthKeysAfterExpire []v1.PreAuthKey
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
|
@ -242,7 +245,7 @@ func TestPreAuthKeyCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listedPreAuthKeysAfterExpire,
|
&listedPreAuthKeysAfterExpire,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.True(t, listedPreAuthKeysAfterExpire[1].GetExpiration().AsTime().Before(time.Now()))
|
assert.True(t, listedPreAuthKeysAfterExpire[1].GetExpiration().AsTime().Before(time.Now()))
|
||||||
assert.True(t, listedPreAuthKeysAfterExpire[2].GetExpiration().AsTime().After(time.Now()))
|
assert.True(t, listedPreAuthKeysAfterExpire[2].GetExpiration().AsTime().After(time.Now()))
|
||||||
|
@ -256,7 +259,7 @@ func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) {
|
||||||
user := "pre-auth-key-without-exp-user"
|
user := "pre-auth-key-without-exp-user"
|
||||||
|
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
|
@ -264,10 +267,10 @@ func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clipaknaexp"))
|
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clipaknaexp"))
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
headscale, err := scenario.Headscale()
|
headscale, err := scenario.Headscale()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var preAuthKey v1.PreAuthKey
|
var preAuthKey v1.PreAuthKey
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
|
@ -284,7 +287,7 @@ func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) {
|
||||||
},
|
},
|
||||||
&preAuthKey,
|
&preAuthKey,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var listedPreAuthKeys []v1.PreAuthKey
|
var listedPreAuthKeys []v1.PreAuthKey
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
|
@ -300,7 +303,7 @@ func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listedPreAuthKeys,
|
&listedPreAuthKeys,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// There is one key created by "scenario.CreateHeadscaleEnv"
|
// There is one key created by "scenario.CreateHeadscaleEnv"
|
||||||
assert.Len(t, listedPreAuthKeys, 2)
|
assert.Len(t, listedPreAuthKeys, 2)
|
||||||
|
@ -319,7 +322,7 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
|
||||||
user := "pre-auth-key-reus-ephm-user"
|
user := "pre-auth-key-reus-ephm-user"
|
||||||
|
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
|
@ -327,10 +330,10 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clipakresueeph"))
|
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clipakresueeph"))
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
headscale, err := scenario.Headscale()
|
headscale, err := scenario.Headscale()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var preAuthReusableKey v1.PreAuthKey
|
var preAuthReusableKey v1.PreAuthKey
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
|
@ -347,7 +350,7 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
|
||||||
},
|
},
|
||||||
&preAuthReusableKey,
|
&preAuthReusableKey,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var preAuthEphemeralKey v1.PreAuthKey
|
var preAuthEphemeralKey v1.PreAuthKey
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
|
@ -364,7 +367,7 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
|
||||||
},
|
},
|
||||||
&preAuthEphemeralKey,
|
&preAuthEphemeralKey,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.True(t, preAuthEphemeralKey.GetEphemeral())
|
assert.True(t, preAuthEphemeralKey.GetEphemeral())
|
||||||
assert.False(t, preAuthEphemeralKey.GetReusable())
|
assert.False(t, preAuthEphemeralKey.GetReusable())
|
||||||
|
@ -383,7 +386,7 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listedPreAuthKeys,
|
&listedPreAuthKeys,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// There is one key created by "scenario.CreateHeadscaleEnv"
|
// There is one key created by "scenario.CreateHeadscaleEnv"
|
||||||
assert.Len(t, listedPreAuthKeys, 3)
|
assert.Len(t, listedPreAuthKeys, 3)
|
||||||
|
@ -397,7 +400,7 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) {
|
||||||
user2 := "user2"
|
user2 := "user2"
|
||||||
|
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
|
@ -413,10 +416,10 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) {
|
||||||
hsic.WithTLS(),
|
hsic.WithTLS(),
|
||||||
hsic.WithHostnameAsServerURL(),
|
hsic.WithHostnameAsServerURL(),
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
headscale, err := scenario.Headscale()
|
headscale, err := scenario.Headscale()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var user2Key v1.PreAuthKey
|
var user2Key v1.PreAuthKey
|
||||||
|
|
||||||
|
@ -438,10 +441,10 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&user2Key,
|
&user2Key,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
allClients, err := scenario.ListTailscaleClients()
|
allClients, err := scenario.ListTailscaleClients()
|
||||||
assertNoErrListClients(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, allClients, 1)
|
assert.Len(t, allClients, 1)
|
||||||
|
|
||||||
|
@ -449,22 +452,22 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) {
|
||||||
|
|
||||||
// Log out from user1
|
// Log out from user1
|
||||||
err = client.Logout()
|
err = client.Logout()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = scenario.WaitForTailscaleLogout()
|
err = scenario.WaitForTailscaleLogout()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
status, err := client.Status()
|
status, err := client.Status()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
if status.BackendState == "Starting" || status.BackendState == "Running" {
|
if status.BackendState == "Starting" || status.BackendState == "Running" {
|
||||||
t.Fatalf("expected node to be logged out, backend state: %s", status.BackendState)
|
t.Fatalf("expected node to be logged out, backend state: %s", status.BackendState)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = client.Login(headscale.GetEndpoint(), user2Key.GetKey())
|
err = client.Login(headscale.GetEndpoint(), user2Key.GetKey())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
status, err = client.Status()
|
status, err = client.Status()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
if status.BackendState != "Running" {
|
if status.BackendState != "Running" {
|
||||||
t.Fatalf("expected node to be logged in, backend state: %s", status.BackendState)
|
t.Fatalf("expected node to be logged in, backend state: %s", status.BackendState)
|
||||||
}
|
}
|
||||||
|
@ -485,7 +488,7 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listNodes,
|
&listNodes,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, listNodes, 1)
|
assert.Len(t, listNodes, 1)
|
||||||
|
|
||||||
assert.Equal(t, "user2", listNodes[0].GetUser().GetName())
|
assert.Equal(t, "user2", listNodes[0].GetUser().GetName())
|
||||||
|
@ -498,7 +501,7 @@ func TestApiKeyCommand(t *testing.T) {
|
||||||
count := 5
|
count := 5
|
||||||
|
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
|
@ -507,10 +510,10 @@ func TestApiKeyCommand(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins"))
|
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins"))
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
headscale, err := scenario.Headscale()
|
headscale, err := scenario.Headscale()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
keys := make([]string, count)
|
keys := make([]string, count)
|
||||||
|
|
||||||
|
@ -526,7 +529,7 @@ func TestApiKeyCommand(t *testing.T) {
|
||||||
"json",
|
"json",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotEmpty(t, apiResult)
|
assert.NotEmpty(t, apiResult)
|
||||||
|
|
||||||
keys[idx] = apiResult
|
keys[idx] = apiResult
|
||||||
|
@ -545,7 +548,7 @@ func TestApiKeyCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listedAPIKeys,
|
&listedAPIKeys,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, listedAPIKeys, 5)
|
assert.Len(t, listedAPIKeys, 5)
|
||||||
|
|
||||||
|
@ -601,7 +604,7 @@ func TestApiKeyCommand(t *testing.T) {
|
||||||
listedAPIKeys[idx].GetPrefix(),
|
listedAPIKeys[idx].GetPrefix(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
expiredPrefixes[listedAPIKeys[idx].GetPrefix()] = true
|
expiredPrefixes[listedAPIKeys[idx].GetPrefix()] = true
|
||||||
}
|
}
|
||||||
|
@ -617,7 +620,7 @@ func TestApiKeyCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listedAfterExpireAPIKeys,
|
&listedAfterExpireAPIKeys,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
for index := range listedAfterExpireAPIKeys {
|
for index := range listedAfterExpireAPIKeys {
|
||||||
if _, ok := expiredPrefixes[listedAfterExpireAPIKeys[index].GetPrefix()]; ok {
|
if _, ok := expiredPrefixes[listedAfterExpireAPIKeys[index].GetPrefix()]; ok {
|
||||||
|
@ -643,7 +646,7 @@ func TestApiKeyCommand(t *testing.T) {
|
||||||
"--prefix",
|
"--prefix",
|
||||||
listedAPIKeys[0].GetPrefix(),
|
listedAPIKeys[0].GetPrefix(),
|
||||||
})
|
})
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var listedAPIKeysAfterDelete []v1.ApiKey
|
var listedAPIKeysAfterDelete []v1.ApiKey
|
||||||
err = executeAndUnmarshal(headscale,
|
err = executeAndUnmarshal(headscale,
|
||||||
|
@ -656,7 +659,7 @@ func TestApiKeyCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listedAPIKeysAfterDelete,
|
&listedAPIKeysAfterDelete,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, listedAPIKeysAfterDelete, 4)
|
assert.Len(t, listedAPIKeysAfterDelete, 4)
|
||||||
}
|
}
|
||||||
|
@ -666,7 +669,7 @@ func TestNodeTagCommand(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
|
@ -674,17 +677,17 @@ func TestNodeTagCommand(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins"))
|
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins"))
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
headscale, err := scenario.Headscale()
|
headscale, err := scenario.Headscale()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
machineKeys := []string{
|
machineKeys := []string{
|
||||||
"mkey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
|
"mkey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
|
||||||
"mkey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
|
"mkey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
|
||||||
}
|
}
|
||||||
nodes := make([]*v1.Node, len(machineKeys))
|
nodes := make([]*v1.Node, len(machineKeys))
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
for index, machineKey := range machineKeys {
|
for index, machineKey := range machineKeys {
|
||||||
_, err := headscale.Execute(
|
_, err := headscale.Execute(
|
||||||
|
@ -702,7 +705,7 @@ func TestNodeTagCommand(t *testing.T) {
|
||||||
"json",
|
"json",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var node v1.Node
|
var node v1.Node
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
|
@ -720,7 +723,7 @@ func TestNodeTagCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&node,
|
&node,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
nodes[index] = &node
|
nodes[index] = &node
|
||||||
}
|
}
|
||||||
|
@ -739,7 +742,7 @@ func TestNodeTagCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&node,
|
&node,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, []string{"tag:test"}, node.GetForcedTags())
|
assert.Equal(t, []string{"tag:test"}, node.GetForcedTags())
|
||||||
|
|
||||||
|
@ -753,7 +756,7 @@ func TestNodeTagCommand(t *testing.T) {
|
||||||
"--output", "json",
|
"--output", "json",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert.ErrorContains(t, err, "tag must start with the string 'tag:'")
|
require.ErrorContains(t, err, "tag must start with the string 'tag:'")
|
||||||
|
|
||||||
// Test list all nodes after added seconds
|
// Test list all nodes after added seconds
|
||||||
resultMachines := make([]*v1.Node, len(machineKeys))
|
resultMachines := make([]*v1.Node, len(machineKeys))
|
||||||
|
@ -767,7 +770,7 @@ func TestNodeTagCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&resultMachines,
|
&resultMachines,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
found := false
|
found := false
|
||||||
for _, node := range resultMachines {
|
for _, node := range resultMachines {
|
||||||
if node.GetForcedTags() != nil {
|
if node.GetForcedTags() != nil {
|
||||||
|
@ -778,9 +781,8 @@ func TestNodeTagCommand(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert.Equal(
|
assert.True(
|
||||||
t,
|
t,
|
||||||
true,
|
|
||||||
found,
|
found,
|
||||||
"should find a node with the tag 'tag:test' in the list of nodes",
|
"should find a node with the tag 'tag:test' in the list of nodes",
|
||||||
)
|
)
|
||||||
|
@ -791,18 +793,22 @@ func TestNodeAdvertiseTagNoACLCommand(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
"user1": 1,
|
"user1": 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{tsic.WithTags([]string{"tag:test"})}, hsic.WithTestName("cliadvtags"))
|
err = scenario.CreateHeadscaleEnv(
|
||||||
assertNoErr(t, err)
|
spec,
|
||||||
|
[]tsic.Option{tsic.WithTags([]string{"tag:test"})},
|
||||||
|
hsic.WithTestName("cliadvtags"),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
headscale, err := scenario.Headscale()
|
headscale, err := scenario.Headscale()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Test list all nodes after added seconds
|
// Test list all nodes after added seconds
|
||||||
resultMachines := make([]*v1.Node, spec["user1"])
|
resultMachines := make([]*v1.Node, spec["user1"])
|
||||||
|
@ -817,7 +823,7 @@ func TestNodeAdvertiseTagNoACLCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&resultMachines,
|
&resultMachines,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
found := false
|
found := false
|
||||||
for _, node := range resultMachines {
|
for _, node := range resultMachines {
|
||||||
if node.GetInvalidTags() != nil {
|
if node.GetInvalidTags() != nil {
|
||||||
|
@ -828,9 +834,8 @@ func TestNodeAdvertiseTagNoACLCommand(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert.Equal(
|
assert.True(
|
||||||
t,
|
t,
|
||||||
true,
|
|
||||||
found,
|
found,
|
||||||
"should not find a node with the tag 'tag:test' in the list of nodes",
|
"should not find a node with the tag 'tag:test' in the list of nodes",
|
||||||
)
|
)
|
||||||
|
@ -841,31 +846,36 @@ func TestNodeAdvertiseTagWithACLCommand(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
"user1": 1,
|
"user1": 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{tsic.WithTags([]string{"tag:exists"})}, hsic.WithTestName("cliadvtags"), hsic.WithACLPolicy(
|
err = scenario.CreateHeadscaleEnv(
|
||||||
&policy.ACLPolicy{
|
spec,
|
||||||
ACLs: []policy.ACL{
|
[]tsic.Option{tsic.WithTags([]string{"tag:exists"})},
|
||||||
{
|
hsic.WithTestName("cliadvtags"),
|
||||||
Action: "accept",
|
hsic.WithACLPolicy(
|
||||||
Sources: []string{"*"},
|
&policy.ACLPolicy{
|
||||||
Destinations: []string{"*:*"},
|
ACLs: []policy.ACL{
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Sources: []string{"*"},
|
||||||
|
Destinations: []string{"*:*"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TagOwners: map[string][]string{
|
||||||
|
"tag:exists": {"user1"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
TagOwners: map[string][]string{
|
),
|
||||||
"tag:exists": {"user1"},
|
)
|
||||||
},
|
require.NoError(t, err)
|
||||||
},
|
|
||||||
))
|
|
||||||
assertNoErr(t, err)
|
|
||||||
|
|
||||||
headscale, err := scenario.Headscale()
|
headscale, err := scenario.Headscale()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Test list all nodes after added seconds
|
// Test list all nodes after added seconds
|
||||||
resultMachines := make([]*v1.Node, spec["user1"])
|
resultMachines := make([]*v1.Node, spec["user1"])
|
||||||
|
@ -880,7 +890,7 @@ func TestNodeAdvertiseTagWithACLCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&resultMachines,
|
&resultMachines,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
found := false
|
found := false
|
||||||
for _, node := range resultMachines {
|
for _, node := range resultMachines {
|
||||||
if node.GetValidTags() != nil {
|
if node.GetValidTags() != nil {
|
||||||
|
@ -891,9 +901,8 @@ func TestNodeAdvertiseTagWithACLCommand(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert.Equal(
|
assert.True(
|
||||||
t,
|
t,
|
||||||
true,
|
|
||||||
found,
|
found,
|
||||||
"should not find a node with the tag 'tag:exists' in the list of nodes",
|
"should not find a node with the tag 'tag:exists' in the list of nodes",
|
||||||
)
|
)
|
||||||
|
@ -904,7 +913,7 @@ func TestNodeCommand(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
|
@ -913,10 +922,10 @@ func TestNodeCommand(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins"))
|
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins"))
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
headscale, err := scenario.Headscale()
|
headscale, err := scenario.Headscale()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Pregenerated machine keys
|
// Pregenerated machine keys
|
||||||
machineKeys := []string{
|
machineKeys := []string{
|
||||||
|
@ -927,7 +936,7 @@ func TestNodeCommand(t *testing.T) {
|
||||||
"mkey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
|
"mkey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
|
||||||
}
|
}
|
||||||
nodes := make([]*v1.Node, len(machineKeys))
|
nodes := make([]*v1.Node, len(machineKeys))
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
for index, machineKey := range machineKeys {
|
for index, machineKey := range machineKeys {
|
||||||
_, err := headscale.Execute(
|
_, err := headscale.Execute(
|
||||||
|
@ -945,7 +954,7 @@ func TestNodeCommand(t *testing.T) {
|
||||||
"json",
|
"json",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var node v1.Node
|
var node v1.Node
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
|
@ -963,7 +972,7 @@ func TestNodeCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&node,
|
&node,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
nodes[index] = &node
|
nodes[index] = &node
|
||||||
}
|
}
|
||||||
|
@ -983,7 +992,7 @@ func TestNodeCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listAll,
|
&listAll,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, listAll, 5)
|
assert.Len(t, listAll, 5)
|
||||||
|
|
||||||
|
@ -1004,7 +1013,7 @@ func TestNodeCommand(t *testing.T) {
|
||||||
"mkey:dc721977ac7415aafa87f7d4574cbe07c6b171834a6d37375782bdc1fb6b3584",
|
"mkey:dc721977ac7415aafa87f7d4574cbe07c6b171834a6d37375782bdc1fb6b3584",
|
||||||
}
|
}
|
||||||
otherUserMachines := make([]*v1.Node, len(otherUserMachineKeys))
|
otherUserMachines := make([]*v1.Node, len(otherUserMachineKeys))
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
for index, machineKey := range otherUserMachineKeys {
|
for index, machineKey := range otherUserMachineKeys {
|
||||||
_, err := headscale.Execute(
|
_, err := headscale.Execute(
|
||||||
|
@ -1022,7 +1031,7 @@ func TestNodeCommand(t *testing.T) {
|
||||||
"json",
|
"json",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var node v1.Node
|
var node v1.Node
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
|
@ -1040,7 +1049,7 @@ func TestNodeCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&node,
|
&node,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
otherUserMachines[index] = &node
|
otherUserMachines[index] = &node
|
||||||
}
|
}
|
||||||
|
@ -1060,7 +1069,7 @@ func TestNodeCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listAllWithotherUser,
|
&listAllWithotherUser,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// All nodes, nodes + otherUser
|
// All nodes, nodes + otherUser
|
||||||
assert.Len(t, listAllWithotherUser, 7)
|
assert.Len(t, listAllWithotherUser, 7)
|
||||||
|
@ -1086,7 +1095,7 @@ func TestNodeCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listOnlyotherUserMachineUser,
|
&listOnlyotherUserMachineUser,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, listOnlyotherUserMachineUser, 2)
|
assert.Len(t, listOnlyotherUserMachineUser, 2)
|
||||||
|
|
||||||
|
@ -1118,7 +1127,7 @@ func TestNodeCommand(t *testing.T) {
|
||||||
"--force",
|
"--force",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Test: list main user after node is deleted
|
// Test: list main user after node is deleted
|
||||||
var listOnlyMachineUserAfterDelete []v1.Node
|
var listOnlyMachineUserAfterDelete []v1.Node
|
||||||
|
@ -1135,7 +1144,7 @@ func TestNodeCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listOnlyMachineUserAfterDelete,
|
&listOnlyMachineUserAfterDelete,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, listOnlyMachineUserAfterDelete, 4)
|
assert.Len(t, listOnlyMachineUserAfterDelete, 4)
|
||||||
}
|
}
|
||||||
|
@ -1145,7 +1154,7 @@ func TestNodeExpireCommand(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
|
@ -1153,10 +1162,10 @@ func TestNodeExpireCommand(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins"))
|
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins"))
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
headscale, err := scenario.Headscale()
|
headscale, err := scenario.Headscale()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Pregenerated machine keys
|
// Pregenerated machine keys
|
||||||
machineKeys := []string{
|
machineKeys := []string{
|
||||||
|
@ -1184,7 +1193,7 @@ func TestNodeExpireCommand(t *testing.T) {
|
||||||
"json",
|
"json",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var node v1.Node
|
var node v1.Node
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
|
@ -1202,7 +1211,7 @@ func TestNodeExpireCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&node,
|
&node,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
nodes[index] = &node
|
nodes[index] = &node
|
||||||
}
|
}
|
||||||
|
@ -1221,7 +1230,7 @@ func TestNodeExpireCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listAll,
|
&listAll,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, listAll, 5)
|
assert.Len(t, listAll, 5)
|
||||||
|
|
||||||
|
@ -1241,7 +1250,7 @@ func TestNodeExpireCommand(t *testing.T) {
|
||||||
fmt.Sprintf("%d", listAll[idx].GetId()),
|
fmt.Sprintf("%d", listAll[idx].GetId()),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var listAllAfterExpiry []v1.Node
|
var listAllAfterExpiry []v1.Node
|
||||||
|
@ -1256,7 +1265,7 @@ func TestNodeExpireCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listAllAfterExpiry,
|
&listAllAfterExpiry,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, listAllAfterExpiry, 5)
|
assert.Len(t, listAllAfterExpiry, 5)
|
||||||
|
|
||||||
|
@ -1272,7 +1281,7 @@ func TestNodeRenameCommand(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
|
@ -1280,10 +1289,10 @@ func TestNodeRenameCommand(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins"))
|
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins"))
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
headscale, err := scenario.Headscale()
|
headscale, err := scenario.Headscale()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Pregenerated machine keys
|
// Pregenerated machine keys
|
||||||
machineKeys := []string{
|
machineKeys := []string{
|
||||||
|
@ -1294,7 +1303,7 @@ func TestNodeRenameCommand(t *testing.T) {
|
||||||
"mkey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
|
"mkey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
|
||||||
}
|
}
|
||||||
nodes := make([]*v1.Node, len(machineKeys))
|
nodes := make([]*v1.Node, len(machineKeys))
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
for index, machineKey := range machineKeys {
|
for index, machineKey := range machineKeys {
|
||||||
_, err := headscale.Execute(
|
_, err := headscale.Execute(
|
||||||
|
@ -1312,7 +1321,7 @@ func TestNodeRenameCommand(t *testing.T) {
|
||||||
"json",
|
"json",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var node v1.Node
|
var node v1.Node
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
|
@ -1330,7 +1339,7 @@ func TestNodeRenameCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&node,
|
&node,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
nodes[index] = &node
|
nodes[index] = &node
|
||||||
}
|
}
|
||||||
|
@ -1349,7 +1358,7 @@ func TestNodeRenameCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listAll,
|
&listAll,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, listAll, 5)
|
assert.Len(t, listAll, 5)
|
||||||
|
|
||||||
|
@ -1370,7 +1379,7 @@ func TestNodeRenameCommand(t *testing.T) {
|
||||||
fmt.Sprintf("newnode-%d", idx+1),
|
fmt.Sprintf("newnode-%d", idx+1),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Contains(t, res, "Node renamed")
|
assert.Contains(t, res, "Node renamed")
|
||||||
}
|
}
|
||||||
|
@ -1387,7 +1396,7 @@ func TestNodeRenameCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listAllAfterRename,
|
&listAllAfterRename,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, listAllAfterRename, 5)
|
assert.Len(t, listAllAfterRename, 5)
|
||||||
|
|
||||||
|
@ -1408,7 +1417,7 @@ func TestNodeRenameCommand(t *testing.T) {
|
||||||
strings.Repeat("t", 64),
|
strings.Repeat("t", 64),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert.ErrorContains(t, err, "not be over 63 chars")
|
require.ErrorContains(t, err, "not be over 63 chars")
|
||||||
|
|
||||||
var listAllAfterRenameAttempt []v1.Node
|
var listAllAfterRenameAttempt []v1.Node
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
|
@ -1422,7 +1431,7 @@ func TestNodeRenameCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listAllAfterRenameAttempt,
|
&listAllAfterRenameAttempt,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, listAllAfterRenameAttempt, 5)
|
assert.Len(t, listAllAfterRenameAttempt, 5)
|
||||||
|
|
||||||
|
@ -1438,7 +1447,7 @@ func TestNodeMoveCommand(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
|
@ -1447,10 +1456,10 @@ func TestNodeMoveCommand(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins"))
|
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins"))
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
headscale, err := scenario.Headscale()
|
headscale, err := scenario.Headscale()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Randomly generated node key
|
// Randomly generated node key
|
||||||
machineKey := "mkey:688411b767663479632d44140f08a9fde87383adc7cdeb518f62ce28a17ef0aa"
|
machineKey := "mkey:688411b767663479632d44140f08a9fde87383adc7cdeb518f62ce28a17ef0aa"
|
||||||
|
@ -1470,7 +1479,7 @@ func TestNodeMoveCommand(t *testing.T) {
|
||||||
"json",
|
"json",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var node v1.Node
|
var node v1.Node
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
|
@ -1488,11 +1497,11 @@ func TestNodeMoveCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&node,
|
&node,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, uint64(1), node.GetId())
|
assert.Equal(t, uint64(1), node.GetId())
|
||||||
assert.Equal(t, "nomad-node", node.GetName())
|
assert.Equal(t, "nomad-node", node.GetName())
|
||||||
assert.Equal(t, node.GetUser().GetName(), "old-user")
|
assert.Equal(t, "old-user", node.GetUser().GetName())
|
||||||
|
|
||||||
nodeID := fmt.Sprintf("%d", node.GetId())
|
nodeID := fmt.Sprintf("%d", node.GetId())
|
||||||
|
|
||||||
|
@ -1511,9 +1520,9 @@ func TestNodeMoveCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&node,
|
&node,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, node.GetUser().GetName(), "new-user")
|
assert.Equal(t, "new-user", node.GetUser().GetName())
|
||||||
|
|
||||||
var allNodes []v1.Node
|
var allNodes []v1.Node
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
|
@ -1527,13 +1536,13 @@ func TestNodeMoveCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&allNodes,
|
&allNodes,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, allNodes, 1)
|
assert.Len(t, allNodes, 1)
|
||||||
|
|
||||||
assert.Equal(t, allNodes[0].GetId(), node.GetId())
|
assert.Equal(t, allNodes[0].GetId(), node.GetId())
|
||||||
assert.Equal(t, allNodes[0].GetUser(), node.GetUser())
|
assert.Equal(t, allNodes[0].GetUser(), node.GetUser())
|
||||||
assert.Equal(t, allNodes[0].GetUser().GetName(), "new-user")
|
assert.Equal(t, "new-user", allNodes[0].GetUser().GetName())
|
||||||
|
|
||||||
_, err = headscale.Execute(
|
_, err = headscale.Execute(
|
||||||
[]string{
|
[]string{
|
||||||
|
@ -1548,12 +1557,12 @@ func TestNodeMoveCommand(t *testing.T) {
|
||||||
"json",
|
"json",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert.ErrorContains(
|
require.ErrorContains(
|
||||||
t,
|
t,
|
||||||
err,
|
err,
|
||||||
"user not found",
|
"user not found",
|
||||||
)
|
)
|
||||||
assert.Equal(t, node.GetUser().GetName(), "new-user")
|
assert.Equal(t, "new-user", node.GetUser().GetName())
|
||||||
|
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
headscale,
|
headscale,
|
||||||
|
@ -1570,9 +1579,9 @@ func TestNodeMoveCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&node,
|
&node,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, node.GetUser().GetName(), "old-user")
|
assert.Equal(t, "old-user", node.GetUser().GetName())
|
||||||
|
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
headscale,
|
headscale,
|
||||||
|
@ -1589,9 +1598,9 @@ func TestNodeMoveCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&node,
|
&node,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, node.GetUser().GetName(), "old-user")
|
assert.Equal(t, "old-user", node.GetUser().GetName())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPolicyCommand(t *testing.T) {
|
func TestPolicyCommand(t *testing.T) {
|
||||||
|
@ -1599,7 +1608,7 @@ func TestPolicyCommand(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
|
@ -1614,10 +1623,10 @@ func TestPolicyCommand(t *testing.T) {
|
||||||
"HEADSCALE_POLICY_MODE": "database",
|
"HEADSCALE_POLICY_MODE": "database",
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
headscale, err := scenario.Headscale()
|
headscale, err := scenario.Headscale()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
p := policy.ACLPolicy{
|
p := policy.ACLPolicy{
|
||||||
ACLs: []policy.ACL{
|
ACLs: []policy.ACL{
|
||||||
|
@ -1637,7 +1646,7 @@ func TestPolicyCommand(t *testing.T) {
|
||||||
policyFilePath := "/etc/headscale/policy.json"
|
policyFilePath := "/etc/headscale/policy.json"
|
||||||
|
|
||||||
err = headscale.WriteFile(policyFilePath, pBytes)
|
err = headscale.WriteFile(policyFilePath, pBytes)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// No policy is present at this time.
|
// No policy is present at this time.
|
||||||
// Add a new policy from a file.
|
// Add a new policy from a file.
|
||||||
|
@ -1651,7 +1660,7 @@ func TestPolicyCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Get the current policy and check
|
// Get the current policy and check
|
||||||
// if it is the same as the one we set.
|
// if it is the same as the one we set.
|
||||||
|
@ -1667,11 +1676,11 @@ func TestPolicyCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&output,
|
&output,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, output.TagOwners, 1)
|
assert.Len(t, output.TagOwners, 1)
|
||||||
assert.Len(t, output.ACLs, 1)
|
assert.Len(t, output.ACLs, 1)
|
||||||
assert.Equal(t, output.TagOwners["tag:exists"], []string{"policy-user"})
|
assert.Equal(t, []string{"policy-user"}, output.TagOwners["tag:exists"])
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPolicyBrokenConfigCommand(t *testing.T) {
|
func TestPolicyBrokenConfigCommand(t *testing.T) {
|
||||||
|
@ -1679,7 +1688,7 @@ func TestPolicyBrokenConfigCommand(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
|
@ -1694,10 +1703,10 @@ func TestPolicyBrokenConfigCommand(t *testing.T) {
|
||||||
"HEADSCALE_POLICY_MODE": "database",
|
"HEADSCALE_POLICY_MODE": "database",
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
headscale, err := scenario.Headscale()
|
headscale, err := scenario.Headscale()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
p := policy.ACLPolicy{
|
p := policy.ACLPolicy{
|
||||||
ACLs: []policy.ACL{
|
ACLs: []policy.ACL{
|
||||||
|
@ -1719,7 +1728,7 @@ func TestPolicyBrokenConfigCommand(t *testing.T) {
|
||||||
policyFilePath := "/etc/headscale/policy.json"
|
policyFilePath := "/etc/headscale/policy.json"
|
||||||
|
|
||||||
err = headscale.WriteFile(policyFilePath, pBytes)
|
err = headscale.WriteFile(policyFilePath, pBytes)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// No policy is present at this time.
|
// No policy is present at this time.
|
||||||
// Add a new policy from a file.
|
// Add a new policy from a file.
|
||||||
|
@ -1732,7 +1741,7 @@ func TestPolicyBrokenConfigCommand(t *testing.T) {
|
||||||
policyFilePath,
|
policyFilePath,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert.ErrorContains(t, err, "verifying policy rules: invalid action")
|
require.ErrorContains(t, err, "verifying policy rules: invalid action")
|
||||||
|
|
||||||
// The new policy was invalid, the old one should still be in place, which
|
// The new policy was invalid, the old one should still be in place, which
|
||||||
// is none.
|
// is none.
|
||||||
|
@ -1745,5 +1754,5 @@ func TestPolicyBrokenConfigCommand(t *testing.T) {
|
||||||
"json",
|
"json",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert.ErrorContains(t, err, "acl policy not found")
|
require.ErrorContains(t, err, "acl policy not found")
|
||||||
}
|
}
|
||||||
|
|
96
integration/derp_verify_endpoint_test.go
Normal file
96
integration/derp_verify_endpoint_test.go
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/juanfont/headscale/hscontrol/util"
|
||||||
|
"github.com/juanfont/headscale/integration/dsic"
|
||||||
|
"github.com/juanfont/headscale/integration/hsic"
|
||||||
|
"github.com/juanfont/headscale/integration/integrationutil"
|
||||||
|
"github.com/juanfont/headscale/integration/tsic"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDERPVerifyEndpoint(t *testing.T) {
|
||||||
|
IntegrationSkip(t)
|
||||||
|
|
||||||
|
// Generate random hostname for the headscale instance
|
||||||
|
hash, err := util.GenerateRandomStringDNSSafe(6)
|
||||||
|
assertNoErr(t, err)
|
||||||
|
testName := "derpverify"
|
||||||
|
hostname := fmt.Sprintf("hs-%s-%s", testName, hash)
|
||||||
|
|
||||||
|
headscalePort := 8080
|
||||||
|
|
||||||
|
// Create cert for headscale
|
||||||
|
certHeadscale, keyHeadscale, err := integrationutil.CreateCertificate(hostname)
|
||||||
|
assertNoErr(t, err)
|
||||||
|
|
||||||
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
|
assertNoErr(t, err)
|
||||||
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
|
spec := map[string]int{
|
||||||
|
"user1": len(MustTestVersions),
|
||||||
|
}
|
||||||
|
|
||||||
|
derper, err := scenario.CreateDERPServer("head",
|
||||||
|
dsic.WithCACert(certHeadscale),
|
||||||
|
dsic.WithVerifyClientURL(fmt.Sprintf("https://%s/verify", net.JoinHostPort(hostname, strconv.Itoa(headscalePort)))),
|
||||||
|
)
|
||||||
|
assertNoErr(t, err)
|
||||||
|
|
||||||
|
derpMap := tailcfg.DERPMap{
|
||||||
|
Regions: map[int]*tailcfg.DERPRegion{
|
||||||
|
900: {
|
||||||
|
RegionID: 900,
|
||||||
|
RegionCode: "test-derpverify",
|
||||||
|
RegionName: "TestDerpVerify",
|
||||||
|
Nodes: []*tailcfg.DERPNode{
|
||||||
|
{
|
||||||
|
Name: "TestDerpVerify",
|
||||||
|
RegionID: 900,
|
||||||
|
HostName: derper.GetHostname(),
|
||||||
|
STUNPort: derper.GetSTUNPort(),
|
||||||
|
STUNOnly: false,
|
||||||
|
DERPPort: derper.GetDERPPort(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{tsic.WithCACert(derper.GetCert())},
|
||||||
|
hsic.WithHostname(hostname),
|
||||||
|
hsic.WithPort(headscalePort),
|
||||||
|
hsic.WithCustomTLS(certHeadscale, keyHeadscale),
|
||||||
|
hsic.WithHostnameAsServerURL(),
|
||||||
|
hsic.WithDERPConfig(derpMap))
|
||||||
|
assertNoErrHeadscaleEnv(t, err)
|
||||||
|
|
||||||
|
allClients, err := scenario.ListTailscaleClients()
|
||||||
|
assertNoErrListClients(t, err)
|
||||||
|
|
||||||
|
for _, client := range allClients {
|
||||||
|
report, err := client.DebugDERPRegion("test-derpverify")
|
||||||
|
assertNoErr(t, err)
|
||||||
|
successful := false
|
||||||
|
for _, line := range report.Info {
|
||||||
|
if strings.Contains(line, "Successfully established a DERP connection with node") {
|
||||||
|
successful = true
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !successful {
|
||||||
|
stJSON, err := json.Marshal(report)
|
||||||
|
assertNoErr(t, err)
|
||||||
|
t.Errorf("Client %s could not establish a DERP connection: %s", client.Hostname(), string(stJSON))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -74,7 +74,7 @@ func ExecuteCommand(
|
||||||
select {
|
select {
|
||||||
case res := <-resultChan:
|
case res := <-resultChan:
|
||||||
if res.err != nil {
|
if res.err != nil {
|
||||||
return stdout.String(), stderr.String(), res.err
|
return stdout.String(), stderr.String(), fmt.Errorf("command failed, stderr: %s: %w", stderr.String(), res.err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if res.exitCode != 0 {
|
if res.exitCode != 0 {
|
||||||
|
@ -83,12 +83,12 @@ func ExecuteCommand(
|
||||||
// log.Println("stdout: ", stdout.String())
|
// log.Println("stdout: ", stdout.String())
|
||||||
// log.Println("stderr: ", stderr.String())
|
// log.Println("stderr: ", stderr.String())
|
||||||
|
|
||||||
return stdout.String(), stderr.String(), ErrDockertestCommandFailed
|
return stdout.String(), stderr.String(), fmt.Errorf("command failed, stderr: %s: %w", stderr.String(), ErrDockertestCommandFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
return stdout.String(), stderr.String(), nil
|
return stdout.String(), stderr.String(), nil
|
||||||
case <-time.After(execConfig.timeout):
|
case <-time.After(execConfig.timeout):
|
||||||
|
|
||||||
return stdout.String(), stderr.String(), ErrDockertestCommandTimeout
|
return stdout.String(), stderr.String(), fmt.Errorf("command failed, stderr: %s: %w", stderr.String(), ErrDockertestCommandTimeout)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
321
integration/dsic/dsic.go
Normal file
321
integration/dsic/dsic.go
Normal file
|
@ -0,0 +1,321 @@
|
||||||
|
package dsic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/juanfont/headscale/hscontrol/util"
|
||||||
|
"github.com/juanfont/headscale/integration/dockertestutil"
|
||||||
|
"github.com/juanfont/headscale/integration/integrationutil"
|
||||||
|
"github.com/ory/dockertest/v3"
|
||||||
|
"github.com/ory/dockertest/v3/docker"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
dsicHashLength = 6
|
||||||
|
dockerContextPath = "../."
|
||||||
|
caCertRoot = "/usr/local/share/ca-certificates"
|
||||||
|
DERPerCertRoot = "/usr/local/share/derper-certs"
|
||||||
|
dockerExecuteTimeout = 60 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
var errDERPerStatusCodeNotOk = errors.New("DERPer status code not OK")
|
||||||
|
|
||||||
|
// DERPServerInContainer represents DERP Server in Container (DSIC).
|
||||||
|
type DERPServerInContainer struct {
|
||||||
|
version string
|
||||||
|
hostname string
|
||||||
|
|
||||||
|
pool *dockertest.Pool
|
||||||
|
container *dockertest.Resource
|
||||||
|
network *dockertest.Network
|
||||||
|
|
||||||
|
stunPort int
|
||||||
|
derpPort int
|
||||||
|
caCerts [][]byte
|
||||||
|
tlsCert []byte
|
||||||
|
tlsKey []byte
|
||||||
|
withExtraHosts []string
|
||||||
|
withVerifyClientURL string
|
||||||
|
workdir string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option represent optional settings that can be given to a
|
||||||
|
// DERPer instance.
|
||||||
|
type Option = func(c *DERPServerInContainer)
|
||||||
|
|
||||||
|
// WithCACert adds it to the trusted surtificate of the Tailscale container.
|
||||||
|
func WithCACert(cert []byte) Option {
|
||||||
|
return func(dsic *DERPServerInContainer) {
|
||||||
|
dsic.caCerts = append(dsic.caCerts, cert)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithOrCreateNetwork sets the Docker container network to use with
|
||||||
|
// the DERPer instance, if the parameter is nil, a new network,
|
||||||
|
// isolating the DERPer, will be created. If a network is
|
||||||
|
// passed, the DERPer instance will join the given network.
|
||||||
|
func WithOrCreateNetwork(network *dockertest.Network) Option {
|
||||||
|
return func(tsic *DERPServerInContainer) {
|
||||||
|
if network != nil {
|
||||||
|
tsic.network = network
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
network, err := dockertestutil.GetFirstOrCreateNetwork(
|
||||||
|
tsic.pool,
|
||||||
|
tsic.hostname+"-network",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to create network: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tsic.network = network
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDockerWorkdir allows the docker working directory to be set.
|
||||||
|
func WithDockerWorkdir(dir string) Option {
|
||||||
|
return func(tsic *DERPServerInContainer) {
|
||||||
|
tsic.workdir = dir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithVerifyClientURL sets the URL to verify the client.
|
||||||
|
func WithVerifyClientURL(url string) Option {
|
||||||
|
return func(tsic *DERPServerInContainer) {
|
||||||
|
tsic.withVerifyClientURL = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithExtraHosts adds extra hosts to the container.
|
||||||
|
func WithExtraHosts(hosts []string) Option {
|
||||||
|
return func(tsic *DERPServerInContainer) {
|
||||||
|
tsic.withExtraHosts = hosts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new TailscaleInContainer instance.
|
||||||
|
func New(
|
||||||
|
pool *dockertest.Pool,
|
||||||
|
version string,
|
||||||
|
network *dockertest.Network,
|
||||||
|
opts ...Option,
|
||||||
|
) (*DERPServerInContainer, error) {
|
||||||
|
hash, err := util.GenerateRandomStringDNSSafe(dsicHashLength)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
hostname := fmt.Sprintf("derp-%s-%s", strings.ReplaceAll(version, ".", "-"), hash)
|
||||||
|
tlsCert, tlsKey, err := integrationutil.CreateCertificate(hostname)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create certificates for headscale test: %w", err)
|
||||||
|
}
|
||||||
|
dsic := &DERPServerInContainer{
|
||||||
|
version: version,
|
||||||
|
hostname: hostname,
|
||||||
|
pool: pool,
|
||||||
|
network: network,
|
||||||
|
tlsCert: tlsCert,
|
||||||
|
tlsKey: tlsKey,
|
||||||
|
stunPort: 3478, //nolint
|
||||||
|
derpPort: 443, //nolint
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(dsic)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmdArgs strings.Builder
|
||||||
|
fmt.Fprintf(&cmdArgs, "--hostname=%s", hostname)
|
||||||
|
fmt.Fprintf(&cmdArgs, " --certmode=manual")
|
||||||
|
fmt.Fprintf(&cmdArgs, " --certdir=%s", DERPerCertRoot)
|
||||||
|
fmt.Fprintf(&cmdArgs, " --a=:%d", dsic.derpPort)
|
||||||
|
fmt.Fprintf(&cmdArgs, " --stun=true")
|
||||||
|
fmt.Fprintf(&cmdArgs, " --stun-port=%d", dsic.stunPort)
|
||||||
|
if dsic.withVerifyClientURL != "" {
|
||||||
|
fmt.Fprintf(&cmdArgs, " --verify-client-url=%s", dsic.withVerifyClientURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
runOptions := &dockertest.RunOptions{
|
||||||
|
Name: hostname,
|
||||||
|
Networks: []*dockertest.Network{dsic.network},
|
||||||
|
ExtraHosts: dsic.withExtraHosts,
|
||||||
|
// we currently need to give us some time to inject the certificate further down.
|
||||||
|
Entrypoint: []string{"/bin/sh", "-c", "/bin/sleep 3 ; update-ca-certificates ; derper " + cmdArgs.String()},
|
||||||
|
ExposedPorts: []string{
|
||||||
|
"80/tcp",
|
||||||
|
fmt.Sprintf("%d/tcp", dsic.derpPort),
|
||||||
|
fmt.Sprintf("%d/udp", dsic.stunPort),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if dsic.workdir != "" {
|
||||||
|
runOptions.WorkingDir = dsic.workdir
|
||||||
|
}
|
||||||
|
|
||||||
|
// dockertest isnt very good at handling containers that has already
|
||||||
|
// been created, this is an attempt to make sure this container isnt
|
||||||
|
// present.
|
||||||
|
err = pool.RemoveContainerByName(hostname)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var container *dockertest.Resource
|
||||||
|
buildOptions := &dockertest.BuildOptions{
|
||||||
|
Dockerfile: "Dockerfile.derper",
|
||||||
|
ContextDir: dockerContextPath,
|
||||||
|
BuildArgs: []docker.BuildArg{},
|
||||||
|
}
|
||||||
|
switch version {
|
||||||
|
case "head":
|
||||||
|
buildOptions.BuildArgs = append(buildOptions.BuildArgs, docker.BuildArg{
|
||||||
|
Name: "VERSION_BRANCH",
|
||||||
|
Value: "main",
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
buildOptions.BuildArgs = append(buildOptions.BuildArgs, docker.BuildArg{
|
||||||
|
Name: "VERSION_BRANCH",
|
||||||
|
Value: "v" + version,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
container, err = pool.BuildAndRunWithBuildOptions(
|
||||||
|
buildOptions,
|
||||||
|
runOptions,
|
||||||
|
dockertestutil.DockerRestartPolicy,
|
||||||
|
dockertestutil.DockerAllowLocalIPv6,
|
||||||
|
dockertestutil.DockerAllowNetworkAdministration,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"%s could not start tailscale DERPer container (version: %s): %w",
|
||||||
|
hostname,
|
||||||
|
version,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
log.Printf("Created %s container\n", hostname)
|
||||||
|
|
||||||
|
dsic.container = container
|
||||||
|
|
||||||
|
for i, cert := range dsic.caCerts {
|
||||||
|
err = dsic.WriteFile(fmt.Sprintf("%s/user-%d.crt", caCertRoot, i), cert)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write TLS certificate to container: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(dsic.tlsCert) != 0 {
|
||||||
|
err = dsic.WriteFile(fmt.Sprintf("%s/%s.crt", DERPerCertRoot, dsic.hostname), dsic.tlsCert)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write TLS certificate to container: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(dsic.tlsKey) != 0 {
|
||||||
|
err = dsic.WriteFile(fmt.Sprintf("%s/%s.key", DERPerCertRoot, dsic.hostname), dsic.tlsKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write TLS key to container: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dsic, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown stops and cleans up the DERPer container.
|
||||||
|
func (t *DERPServerInContainer) Shutdown() error {
|
||||||
|
err := t.SaveLog("/tmp/control")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(
|
||||||
|
"Failed to save log from %s: %s",
|
||||||
|
t.hostname,
|
||||||
|
fmt.Errorf("failed to save log: %w", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.pool.Purge(t.container)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCert returns the TLS certificate of the DERPer instance.
|
||||||
|
func (t *DERPServerInContainer) GetCert() []byte {
|
||||||
|
return t.tlsCert
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hostname returns the hostname of the DERPer instance.
|
||||||
|
func (t *DERPServerInContainer) Hostname() string {
|
||||||
|
return t.hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version returns the running DERPer version of the instance.
|
||||||
|
func (t *DERPServerInContainer) Version() string {
|
||||||
|
return t.version
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID returns the Docker container ID of the DERPServerInContainer
|
||||||
|
// instance.
|
||||||
|
func (t *DERPServerInContainer) ID() string {
|
||||||
|
return t.container.Container.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *DERPServerInContainer) GetHostname() string {
|
||||||
|
return t.hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSTUNPort returns the STUN port of the DERPer instance.
|
||||||
|
func (t *DERPServerInContainer) GetSTUNPort() int {
|
||||||
|
return t.stunPort
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDERPPort returns the DERP port of the DERPer instance.
|
||||||
|
func (t *DERPServerInContainer) GetDERPPort() int {
|
||||||
|
return t.derpPort
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitForRunning blocks until the DERPer instance is ready to be used.
|
||||||
|
func (t *DERPServerInContainer) WaitForRunning() error {
|
||||||
|
url := "https://" + net.JoinHostPort(t.GetHostname(), strconv.Itoa(t.GetDERPPort())) + "/"
|
||||||
|
log.Printf("waiting for DERPer to be ready at %s", url)
|
||||||
|
|
||||||
|
insecureTransport := http.DefaultTransport.(*http.Transport).Clone() //nolint
|
||||||
|
insecureTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint
|
||||||
|
client := &http.Client{Transport: insecureTransport}
|
||||||
|
|
||||||
|
return t.pool.Retry(func() error {
|
||||||
|
resp, err := client.Get(url) //nolint
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("headscale is not ready: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return errDERPerStatusCodeNotOk
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectToNetwork connects the DERPer instance to a network.
|
||||||
|
func (t *DERPServerInContainer) ConnectToNetwork(network *dockertest.Network) error {
|
||||||
|
return t.container.ConnectToNetwork(network)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteFile save file inside the container.
|
||||||
|
func (t *DERPServerInContainer) WriteFile(path string, data []byte) error {
|
||||||
|
return integrationutil.WriteFileToContainer(t.pool, t.container, path, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveLog saves the current stdout log of the container to a path
|
||||||
|
// on the host system.
|
||||||
|
func (t *DERPServerInContainer) SaveLog(path string) error {
|
||||||
|
_, _, err := dockertestutil.SaveLog(t.pool, t.container, path)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
|
@ -310,7 +310,7 @@ func (s *EmbeddedDERPServerScenario) CreateTailscaleIsolatedNodesInUser(
|
||||||
cert := hsServer.GetCert()
|
cert := hsServer.GetCert()
|
||||||
|
|
||||||
opts = append(opts,
|
opts = append(opts,
|
||||||
tsic.WithHeadscaleTLS(cert),
|
tsic.WithCACert(cert),
|
||||||
)
|
)
|
||||||
|
|
||||||
user.createWaitGroup.Go(func() error {
|
user.createWaitGroup.Go(func() error {
|
||||||
|
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
"tailscale.com/client/tailscale/apitype"
|
"tailscale.com/client/tailscale/apitype"
|
||||||
"tailscale.com/types/key"
|
"tailscale.com/types/key"
|
||||||
|
@ -244,7 +245,11 @@ func TestEphemeral(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEphemeralInAlternateTimezone(t *testing.T) {
|
func TestEphemeralInAlternateTimezone(t *testing.T) {
|
||||||
testEphemeralWithOptions(t, hsic.WithTestName("ephemeral-tz"), hsic.WithTimezone("America/Los_Angeles"))
|
testEphemeralWithOptions(
|
||||||
|
t,
|
||||||
|
hsic.WithTestName("ephemeral-tz"),
|
||||||
|
hsic.WithTimezone("America/Los_Angeles"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testEphemeralWithOptions(t *testing.T, opts ...hsic.Option) {
|
func testEphemeralWithOptions(t *testing.T, opts ...hsic.Option) {
|
||||||
|
@ -1164,10 +1169,10 @@ func Test2118DeletingOnlineNodePanics(t *testing.T) {
|
||||||
},
|
},
|
||||||
&nodeList,
|
&nodeList,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, nodeList, 2)
|
assert.Len(t, nodeList, 2)
|
||||||
assert.True(t, nodeList[0].Online)
|
assert.True(t, nodeList[0].GetOnline())
|
||||||
assert.True(t, nodeList[1].Online)
|
assert.True(t, nodeList[1].GetOnline())
|
||||||
|
|
||||||
// Delete the first node, which is online
|
// Delete the first node, which is online
|
||||||
_, err = headscale.Execute(
|
_, err = headscale.Execute(
|
||||||
|
@ -1177,13 +1182,13 @@ func Test2118DeletingOnlineNodePanics(t *testing.T) {
|
||||||
"delete",
|
"delete",
|
||||||
"--identifier",
|
"--identifier",
|
||||||
// Delete the last added machine
|
// Delete the last added machine
|
||||||
fmt.Sprintf("%d", nodeList[0].Id),
|
fmt.Sprintf("%d", nodeList[0].GetId()),
|
||||||
"--output",
|
"--output",
|
||||||
"json",
|
"json",
|
||||||
"--force",
|
"--force",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
@ -1200,9 +1205,8 @@ func Test2118DeletingOnlineNodePanics(t *testing.T) {
|
||||||
},
|
},
|
||||||
&nodeListAfter,
|
&nodeListAfter,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, nodeListAfter, 1)
|
assert.Len(t, nodeListAfter, 1)
|
||||||
assert.True(t, nodeListAfter[0].Online)
|
assert.True(t, nodeListAfter[0].GetOnline())
|
||||||
assert.Equal(t, nodeList[1].Id, nodeListAfter[0].Id)
|
assert.Equal(t, nodeList[1].GetId(), nodeListAfter[0].GetId())
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,12 @@
|
||||||
package hsic
|
package hsic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
|
||||||
"crypto/x509/pkix"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/pem"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"math/big"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -32,15 +25,19 @@ import (
|
||||||
"github.com/juanfont/headscale/integration/integrationutil"
|
"github.com/juanfont/headscale/integration/integrationutil"
|
||||||
"github.com/ory/dockertest/v3"
|
"github.com/ory/dockertest/v3"
|
||||||
"github.com/ory/dockertest/v3/docker"
|
"github.com/ory/dockertest/v3/docker"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
hsicHashLength = 6
|
hsicHashLength = 6
|
||||||
dockerContextPath = "../."
|
dockerContextPath = "../."
|
||||||
aclPolicyPath = "/etc/headscale/acl.hujson"
|
caCertRoot = "/usr/local/share/ca-certificates"
|
||||||
tlsCertPath = "/etc/headscale/tls.cert"
|
aclPolicyPath = "/etc/headscale/acl.hujson"
|
||||||
tlsKeyPath = "/etc/headscale/tls.key"
|
tlsCertPath = "/etc/headscale/tls.cert"
|
||||||
headscaleDefaultPort = 8080
|
tlsKeyPath = "/etc/headscale/tls.key"
|
||||||
|
headscaleDefaultPort = 8080
|
||||||
|
IntegrationTestDockerFileName = "Dockerfile.integration"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errHeadscaleStatusCodeNotOk = errors.New("headscale status code not ok")
|
var errHeadscaleStatusCodeNotOk = errors.New("headscale status code not ok")
|
||||||
|
@ -64,6 +61,7 @@ type HeadscaleInContainer struct {
|
||||||
// optional config
|
// optional config
|
||||||
port int
|
port int
|
||||||
extraPorts []string
|
extraPorts []string
|
||||||
|
caCerts [][]byte
|
||||||
hostPortBindings map[string][]string
|
hostPortBindings map[string][]string
|
||||||
aclPolicy *policy.ACLPolicy
|
aclPolicy *policy.ACLPolicy
|
||||||
env map[string]string
|
env map[string]string
|
||||||
|
@ -88,18 +86,29 @@ func WithACLPolicy(acl *policy.ACLPolicy) Option {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithCACert adds it to the trusted surtificate of the container.
|
||||||
|
func WithCACert(cert []byte) Option {
|
||||||
|
return func(hsic *HeadscaleInContainer) {
|
||||||
|
hsic.caCerts = append(hsic.caCerts, cert)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WithTLS creates certificates and enables HTTPS.
|
// WithTLS creates certificates and enables HTTPS.
|
||||||
func WithTLS() Option {
|
func WithTLS() Option {
|
||||||
return func(hsic *HeadscaleInContainer) {
|
return func(hsic *HeadscaleInContainer) {
|
||||||
cert, key, err := createCertificate(hsic.hostname)
|
cert, key, err := integrationutil.CreateCertificate(hsic.hostname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to create certificates for headscale test: %s", err)
|
log.Fatalf("failed to create certificates for headscale test: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(kradalby): Move somewhere appropriate
|
hsic.tlsCert = cert
|
||||||
hsic.env["HEADSCALE_TLS_CERT_PATH"] = tlsCertPath
|
hsic.tlsKey = key
|
||||||
hsic.env["HEADSCALE_TLS_KEY_PATH"] = tlsKeyPath
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithCustomTLS uses the given certificates for the Headscale instance.
|
||||||
|
func WithCustomTLS(cert, key []byte) Option {
|
||||||
|
return func(hsic *HeadscaleInContainer) {
|
||||||
hsic.tlsCert = cert
|
hsic.tlsCert = cert
|
||||||
hsic.tlsKey = key
|
hsic.tlsKey = key
|
||||||
}
|
}
|
||||||
|
@ -146,6 +155,13 @@ func WithTestName(testName string) Option {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithHostname sets the hostname of the Headscale instance.
|
||||||
|
func WithHostname(hostname string) Option {
|
||||||
|
return func(hsic *HeadscaleInContainer) {
|
||||||
|
hsic.hostname = hostname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WithHostnameAsServerURL sets the Headscale ServerURL based on
|
// WithHostnameAsServerURL sets the Headscale ServerURL based on
|
||||||
// the Hostname.
|
// the Hostname.
|
||||||
func WithHostnameAsServerURL() Option {
|
func WithHostnameAsServerURL() Option {
|
||||||
|
@ -203,6 +219,34 @@ func WithEmbeddedDERPServerOnly() Option {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithDERPConfig configures Headscale use a custom
|
||||||
|
// DERP server only.
|
||||||
|
func WithDERPConfig(derpMap tailcfg.DERPMap) Option {
|
||||||
|
return func(hsic *HeadscaleInContainer) {
|
||||||
|
contents, err := yaml.Marshal(derpMap)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to marshal DERP map: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hsic.env["HEADSCALE_DERP_PATHS"] = "/etc/headscale/derp.yml"
|
||||||
|
hsic.filesInContainer = append(hsic.filesInContainer,
|
||||||
|
fileInContainer{
|
||||||
|
path: "/etc/headscale/derp.yml",
|
||||||
|
contents: contents,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Disable global DERP server and embedded DERP server
|
||||||
|
hsic.env["HEADSCALE_DERP_URLS"] = ""
|
||||||
|
hsic.env["HEADSCALE_DERP_SERVER_ENABLED"] = "false"
|
||||||
|
|
||||||
|
// Envknob for enabling DERP debug logs
|
||||||
|
hsic.env["DERP_DEBUG_LOGS"] = "true"
|
||||||
|
hsic.env["DERP_PROBER_DEBUG_LOGS"] = "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WithTuning allows changing the tuning settings easily.
|
// WithTuning allows changing the tuning settings easily.
|
||||||
func WithTuning(batchTimeout time.Duration, mapSessionChanSize int) Option {
|
func WithTuning(batchTimeout time.Duration, mapSessionChanSize int) Option {
|
||||||
return func(hsic *HeadscaleInContainer) {
|
return func(hsic *HeadscaleInContainer) {
|
||||||
|
@ -260,7 +304,7 @@ func New(
|
||||||
}
|
}
|
||||||
|
|
||||||
headscaleBuildOptions := &dockertest.BuildOptions{
|
headscaleBuildOptions := &dockertest.BuildOptions{
|
||||||
Dockerfile: "Dockerfile.debug",
|
Dockerfile: IntegrationTestDockerFileName,
|
||||||
ContextDir: dockerContextPath,
|
ContextDir: dockerContextPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -300,6 +344,10 @@ func New(
|
||||||
"HEADSCALE_DEBUG_HIGH_CARDINALITY_METRICS=1",
|
"HEADSCALE_DEBUG_HIGH_CARDINALITY_METRICS=1",
|
||||||
"HEADSCALE_DEBUG_DUMP_CONFIG=1",
|
"HEADSCALE_DEBUG_DUMP_CONFIG=1",
|
||||||
}
|
}
|
||||||
|
if hsic.hasTLS() {
|
||||||
|
hsic.env["HEADSCALE_TLS_CERT_PATH"] = tlsCertPath
|
||||||
|
hsic.env["HEADSCALE_TLS_KEY_PATH"] = tlsKeyPath
|
||||||
|
}
|
||||||
for key, value := range hsic.env {
|
for key, value := range hsic.env {
|
||||||
env = append(env, fmt.Sprintf("%s=%s", key, value))
|
env = append(env, fmt.Sprintf("%s=%s", key, value))
|
||||||
}
|
}
|
||||||
|
@ -313,7 +361,7 @@ func New(
|
||||||
// Cmd: []string{"headscale", "serve"},
|
// Cmd: []string{"headscale", "serve"},
|
||||||
// TODO(kradalby): Get rid of this hack, we currently need to give us some
|
// TODO(kradalby): Get rid of this hack, we currently need to give us some
|
||||||
// to inject the headscale configuration further down.
|
// to inject the headscale configuration further down.
|
||||||
Entrypoint: []string{"/bin/bash", "-c", "/bin/sleep 3 ; headscale serve ; /bin/sleep 30"},
|
Entrypoint: []string{"/bin/bash", "-c", "/bin/sleep 3 ; update-ca-certificates ; headscale serve ; /bin/sleep 30"},
|
||||||
Env: env,
|
Env: env,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -351,6 +399,14 @@ func New(
|
||||||
|
|
||||||
hsic.container = container
|
hsic.container = container
|
||||||
|
|
||||||
|
// Write the CA certificates to the container
|
||||||
|
for i, cert := range hsic.caCerts {
|
||||||
|
err = hsic.WriteFile(fmt.Sprintf("%s/user-%d.crt", caCertRoot, i), cert)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write TLS certificate to container: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err = hsic.WriteFile("/etc/headscale/config.yaml", []byte(MinimumConfigYAML()))
|
err = hsic.WriteFile("/etc/headscale/config.yaml", []byte(MinimumConfigYAML()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to write headscale config to container: %w", err)
|
return nil, fmt.Errorf("failed to write headscale config to container: %w", err)
|
||||||
|
@ -749,86 +805,3 @@ func (t *HeadscaleInContainer) SendInterrupt() error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint
|
|
||||||
func createCertificate(hostname string) ([]byte, []byte, error) {
|
|
||||||
// From:
|
|
||||||
// https://shaneutt.com/blog/golang-ca-and-signed-cert-go/
|
|
||||||
|
|
||||||
ca := &x509.Certificate{
|
|
||||||
SerialNumber: big.NewInt(2019),
|
|
||||||
Subject: pkix.Name{
|
|
||||||
Organization: []string{"Headscale testing INC"},
|
|
||||||
Country: []string{"NL"},
|
|
||||||
Locality: []string{"Leiden"},
|
|
||||||
},
|
|
||||||
NotBefore: time.Now(),
|
|
||||||
NotAfter: time.Now().Add(60 * time.Hour),
|
|
||||||
IsCA: true,
|
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{
|
|
||||||
x509.ExtKeyUsageClientAuth,
|
|
||||||
x509.ExtKeyUsageServerAuth,
|
|
||||||
},
|
|
||||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
|
||||||
BasicConstraintsValid: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cert := &x509.Certificate{
|
|
||||||
SerialNumber: big.NewInt(1658),
|
|
||||||
Subject: pkix.Name{
|
|
||||||
CommonName: hostname,
|
|
||||||
Organization: []string{"Headscale testing INC"},
|
|
||||||
Country: []string{"NL"},
|
|
||||||
Locality: []string{"Leiden"},
|
|
||||||
},
|
|
||||||
NotBefore: time.Now(),
|
|
||||||
NotAfter: time.Now().Add(60 * time.Minute),
|
|
||||||
SubjectKeyId: []byte{1, 2, 3, 4, 6},
|
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
|
||||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
|
||||||
DNSNames: []string{hostname},
|
|
||||||
}
|
|
||||||
|
|
||||||
certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
certBytes, err := x509.CreateCertificate(
|
|
||||||
rand.Reader,
|
|
||||||
cert,
|
|
||||||
ca,
|
|
||||||
&certPrivKey.PublicKey,
|
|
||||||
caPrivKey,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
certPEM := new(bytes.Buffer)
|
|
||||||
|
|
||||||
err = pem.Encode(certPEM, &pem.Block{
|
|
||||||
Type: "CERTIFICATE",
|
|
||||||
Bytes: certBytes,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
certPrivKeyPEM := new(bytes.Buffer)
|
|
||||||
|
|
||||||
err = pem.Encode(certPrivKeyPEM, &pem.Block{
|
|
||||||
Type: "RSA PRIVATE KEY",
|
|
||||||
Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return certPEM.Bytes(), certPrivKeyPEM.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,9 +3,16 @@ package integrationutil
|
||||||
import (
|
import (
|
||||||
"archive/tar"
|
"archive/tar"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math/big"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/juanfont/headscale/integration/dockertestutil"
|
"github.com/juanfont/headscale/integration/dockertestutil"
|
||||||
"github.com/ory/dockertest/v3"
|
"github.com/ory/dockertest/v3"
|
||||||
|
@ -93,3 +100,86 @@ func FetchPathFromContainer(
|
||||||
|
|
||||||
return buf.Bytes(), nil
|
return buf.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nolint
|
||||||
|
func CreateCertificate(hostname string) ([]byte, []byte, error) {
|
||||||
|
// From:
|
||||||
|
// https://shaneutt.com/blog/golang-ca-and-signed-cert-go/
|
||||||
|
|
||||||
|
ca := &x509.Certificate{
|
||||||
|
SerialNumber: big.NewInt(2019),
|
||||||
|
Subject: pkix.Name{
|
||||||
|
Organization: []string{"Headscale testing INC"},
|
||||||
|
Country: []string{"NL"},
|
||||||
|
Locality: []string{"Leiden"},
|
||||||
|
},
|
||||||
|
NotBefore: time.Now(),
|
||||||
|
NotAfter: time.Now().Add(60 * time.Hour),
|
||||||
|
IsCA: true,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{
|
||||||
|
x509.ExtKeyUsageClientAuth,
|
||||||
|
x509.ExtKeyUsageServerAuth,
|
||||||
|
},
|
||||||
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cert := &x509.Certificate{
|
||||||
|
SerialNumber: big.NewInt(1658),
|
||||||
|
Subject: pkix.Name{
|
||||||
|
CommonName: hostname,
|
||||||
|
Organization: []string{"Headscale testing INC"},
|
||||||
|
Country: []string{"NL"},
|
||||||
|
Locality: []string{"Leiden"},
|
||||||
|
},
|
||||||
|
NotBefore: time.Now(),
|
||||||
|
NotAfter: time.Now().Add(60 * time.Minute),
|
||||||
|
SubjectKeyId: []byte{1, 2, 3, 4, 6},
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
||||||
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||||
|
DNSNames: []string{hostname},
|
||||||
|
}
|
||||||
|
|
||||||
|
certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
certBytes, err := x509.CreateCertificate(
|
||||||
|
rand.Reader,
|
||||||
|
cert,
|
||||||
|
ca,
|
||||||
|
&certPrivKey.PublicKey,
|
||||||
|
caPrivKey,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
certPEM := new(bytes.Buffer)
|
||||||
|
|
||||||
|
err = pem.Encode(certPEM, &pem.Block{
|
||||||
|
Type: "CERTIFICATE",
|
||||||
|
Bytes: certBytes,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
certPrivKeyPEM := new(bytes.Buffer)
|
||||||
|
|
||||||
|
err = pem.Encode(certPrivKeyPEM, &pem.Block{
|
||||||
|
Type: "RSA PRIVATE KEY",
|
||||||
|
Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return certPEM.Bytes(), certPrivKeyPEM.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
|
@ -92,9 +92,9 @@ func TestEnablingRoutes(t *testing.T) {
|
||||||
assert.Len(t, routes, 3)
|
assert.Len(t, routes, 3)
|
||||||
|
|
||||||
for _, route := range routes {
|
for _, route := range routes {
|
||||||
assert.Equal(t, true, route.GetAdvertised())
|
assert.True(t, route.GetAdvertised())
|
||||||
assert.Equal(t, false, route.GetEnabled())
|
assert.False(t, route.GetEnabled())
|
||||||
assert.Equal(t, false, route.GetIsPrimary())
|
assert.False(t, route.GetIsPrimary())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify that no routes has been sent to the client,
|
// Verify that no routes has been sent to the client,
|
||||||
|
@ -139,9 +139,9 @@ func TestEnablingRoutes(t *testing.T) {
|
||||||
assert.Len(t, enablingRoutes, 3)
|
assert.Len(t, enablingRoutes, 3)
|
||||||
|
|
||||||
for _, route := range enablingRoutes {
|
for _, route := range enablingRoutes {
|
||||||
assert.Equal(t, true, route.GetAdvertised())
|
assert.True(t, route.GetAdvertised())
|
||||||
assert.Equal(t, true, route.GetEnabled())
|
assert.True(t, route.GetEnabled())
|
||||||
assert.Equal(t, true, route.GetIsPrimary())
|
assert.True(t, route.GetIsPrimary())
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(5 * time.Second)
|
time.Sleep(5 * time.Second)
|
||||||
|
@ -212,18 +212,18 @@ func TestEnablingRoutes(t *testing.T) {
|
||||||
assertNoErr(t, err)
|
assertNoErr(t, err)
|
||||||
|
|
||||||
for _, route := range disablingRoutes {
|
for _, route := range disablingRoutes {
|
||||||
assert.Equal(t, true, route.GetAdvertised())
|
assert.True(t, route.GetAdvertised())
|
||||||
|
|
||||||
if route.GetId() == routeToBeDisabled.GetId() {
|
if route.GetId() == routeToBeDisabled.GetId() {
|
||||||
assert.Equal(t, false, route.GetEnabled())
|
assert.False(t, route.GetEnabled())
|
||||||
|
|
||||||
// since this is the only route of this cidr,
|
// since this is the only route of this cidr,
|
||||||
// it will not failover, and remain Primary
|
// it will not failover, and remain Primary
|
||||||
// until something can replace it.
|
// until something can replace it.
|
||||||
assert.Equal(t, true, route.GetIsPrimary())
|
assert.True(t, route.GetIsPrimary())
|
||||||
} else {
|
} else {
|
||||||
assert.Equal(t, true, route.GetEnabled())
|
assert.True(t, route.GetEnabled())
|
||||||
assert.Equal(t, true, route.GetIsPrimary())
|
assert.True(t, route.GetIsPrimary())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -342,9 +342,9 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||||
t.Logf("initial routes %#v", routes)
|
t.Logf("initial routes %#v", routes)
|
||||||
|
|
||||||
for _, route := range routes {
|
for _, route := range routes {
|
||||||
assert.Equal(t, true, route.GetAdvertised())
|
assert.True(t, route.GetAdvertised())
|
||||||
assert.Equal(t, false, route.GetEnabled())
|
assert.False(t, route.GetEnabled())
|
||||||
assert.Equal(t, false, route.GetIsPrimary())
|
assert.False(t, route.GetIsPrimary())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify that no routes has been sent to the client,
|
// Verify that no routes has been sent to the client,
|
||||||
|
@ -391,14 +391,14 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||||
assert.Len(t, enablingRoutes, 2)
|
assert.Len(t, enablingRoutes, 2)
|
||||||
|
|
||||||
// Node 1 is primary
|
// Node 1 is primary
|
||||||
assert.Equal(t, true, enablingRoutes[0].GetAdvertised())
|
assert.True(t, enablingRoutes[0].GetAdvertised())
|
||||||
assert.Equal(t, true, enablingRoutes[0].GetEnabled())
|
assert.True(t, enablingRoutes[0].GetEnabled())
|
||||||
assert.Equal(t, true, enablingRoutes[0].GetIsPrimary(), "both subnet routers are up, expected r1 to be primary")
|
assert.True(t, enablingRoutes[0].GetIsPrimary(), "both subnet routers are up, expected r1 to be primary")
|
||||||
|
|
||||||
// Node 2 is not primary
|
// Node 2 is not primary
|
||||||
assert.Equal(t, true, enablingRoutes[1].GetAdvertised())
|
assert.True(t, enablingRoutes[1].GetAdvertised())
|
||||||
assert.Equal(t, true, enablingRoutes[1].GetEnabled())
|
assert.True(t, enablingRoutes[1].GetEnabled())
|
||||||
assert.Equal(t, false, enablingRoutes[1].GetIsPrimary(), "both subnet routers are up, expected r2 to be non-primary")
|
assert.False(t, enablingRoutes[1].GetIsPrimary(), "both subnet routers are up, expected r2 to be non-primary")
|
||||||
|
|
||||||
// Verify that the client has routes from the primary machine
|
// Verify that the client has routes from the primary machine
|
||||||
srs1, err := subRouter1.Status()
|
srs1, err := subRouter1.Status()
|
||||||
|
@ -446,14 +446,14 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||||
assert.Len(t, routesAfterMove, 2)
|
assert.Len(t, routesAfterMove, 2)
|
||||||
|
|
||||||
// Node 1 is not primary
|
// Node 1 is not primary
|
||||||
assert.Equal(t, true, routesAfterMove[0].GetAdvertised())
|
assert.True(t, routesAfterMove[0].GetAdvertised())
|
||||||
assert.Equal(t, true, routesAfterMove[0].GetEnabled())
|
assert.True(t, routesAfterMove[0].GetEnabled())
|
||||||
assert.Equal(t, false, routesAfterMove[0].GetIsPrimary(), "r1 is down, expected r2 to be primary")
|
assert.False(t, routesAfterMove[0].GetIsPrimary(), "r1 is down, expected r2 to be primary")
|
||||||
|
|
||||||
// Node 2 is primary
|
// Node 2 is primary
|
||||||
assert.Equal(t, true, routesAfterMove[1].GetAdvertised())
|
assert.True(t, routesAfterMove[1].GetAdvertised())
|
||||||
assert.Equal(t, true, routesAfterMove[1].GetEnabled())
|
assert.True(t, routesAfterMove[1].GetEnabled())
|
||||||
assert.Equal(t, true, routesAfterMove[1].GetIsPrimary(), "r1 is down, expected r2 to be primary")
|
assert.True(t, routesAfterMove[1].GetIsPrimary(), "r1 is down, expected r2 to be primary")
|
||||||
|
|
||||||
srs2, err = subRouter2.Status()
|
srs2, err = subRouter2.Status()
|
||||||
|
|
||||||
|
@ -501,16 +501,16 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||||
assert.Len(t, routesAfterBothDown, 2)
|
assert.Len(t, routesAfterBothDown, 2)
|
||||||
|
|
||||||
// Node 1 is not primary
|
// Node 1 is not primary
|
||||||
assert.Equal(t, true, routesAfterBothDown[0].GetAdvertised())
|
assert.True(t, routesAfterBothDown[0].GetAdvertised())
|
||||||
assert.Equal(t, true, routesAfterBothDown[0].GetEnabled())
|
assert.True(t, routesAfterBothDown[0].GetEnabled())
|
||||||
assert.Equal(t, false, routesAfterBothDown[0].GetIsPrimary(), "r1 and r2 is down, expected r2 to _still_ be primary")
|
assert.False(t, routesAfterBothDown[0].GetIsPrimary(), "r1 and r2 is down, expected r2 to _still_ be primary")
|
||||||
|
|
||||||
// Node 2 is primary
|
// Node 2 is primary
|
||||||
// if the node goes down, but no other suitable route is
|
// if the node goes down, but no other suitable route is
|
||||||
// available, keep the last known good route.
|
// available, keep the last known good route.
|
||||||
assert.Equal(t, true, routesAfterBothDown[1].GetAdvertised())
|
assert.True(t, routesAfterBothDown[1].GetAdvertised())
|
||||||
assert.Equal(t, true, routesAfterBothDown[1].GetEnabled())
|
assert.True(t, routesAfterBothDown[1].GetEnabled())
|
||||||
assert.Equal(t, true, routesAfterBothDown[1].GetIsPrimary(), "r1 and r2 is down, expected r2 to _still_ be primary")
|
assert.True(t, routesAfterBothDown[1].GetIsPrimary(), "r1 and r2 is down, expected r2 to _still_ be primary")
|
||||||
|
|
||||||
// TODO(kradalby): Check client status
|
// TODO(kradalby): Check client status
|
||||||
// Both are expected to be down
|
// Both are expected to be down
|
||||||
|
@ -560,14 +560,14 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||||
assert.Len(t, routesAfter1Up, 2)
|
assert.Len(t, routesAfter1Up, 2)
|
||||||
|
|
||||||
// Node 1 is primary
|
// Node 1 is primary
|
||||||
assert.Equal(t, true, routesAfter1Up[0].GetAdvertised())
|
assert.True(t, routesAfter1Up[0].GetAdvertised())
|
||||||
assert.Equal(t, true, routesAfter1Up[0].GetEnabled())
|
assert.True(t, routesAfter1Up[0].GetEnabled())
|
||||||
assert.Equal(t, true, routesAfter1Up[0].GetIsPrimary(), "r1 is back up, expected r1 to become be primary")
|
assert.True(t, routesAfter1Up[0].GetIsPrimary(), "r1 is back up, expected r1 to become be primary")
|
||||||
|
|
||||||
// Node 2 is not primary
|
// Node 2 is not primary
|
||||||
assert.Equal(t, true, routesAfter1Up[1].GetAdvertised())
|
assert.True(t, routesAfter1Up[1].GetAdvertised())
|
||||||
assert.Equal(t, true, routesAfter1Up[1].GetEnabled())
|
assert.True(t, routesAfter1Up[1].GetEnabled())
|
||||||
assert.Equal(t, false, routesAfter1Up[1].GetIsPrimary(), "r1 is back up, expected r1 to become be primary")
|
assert.False(t, routesAfter1Up[1].GetIsPrimary(), "r1 is back up, expected r1 to become be primary")
|
||||||
|
|
||||||
// Verify that the route is announced from subnet router 1
|
// Verify that the route is announced from subnet router 1
|
||||||
clientStatus, err = client.Status()
|
clientStatus, err = client.Status()
|
||||||
|
@ -614,14 +614,14 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||||
assert.Len(t, routesAfter2Up, 2)
|
assert.Len(t, routesAfter2Up, 2)
|
||||||
|
|
||||||
// Node 1 is not primary
|
// Node 1 is not primary
|
||||||
assert.Equal(t, true, routesAfter2Up[0].GetAdvertised())
|
assert.True(t, routesAfter2Up[0].GetAdvertised())
|
||||||
assert.Equal(t, true, routesAfter2Up[0].GetEnabled())
|
assert.True(t, routesAfter2Up[0].GetEnabled())
|
||||||
assert.Equal(t, true, routesAfter2Up[0].GetIsPrimary(), "r1 and r2 is back up, expected r1 to _still_ be primary")
|
assert.True(t, routesAfter2Up[0].GetIsPrimary(), "r1 and r2 is back up, expected r1 to _still_ be primary")
|
||||||
|
|
||||||
// Node 2 is primary
|
// Node 2 is primary
|
||||||
assert.Equal(t, true, routesAfter2Up[1].GetAdvertised())
|
assert.True(t, routesAfter2Up[1].GetAdvertised())
|
||||||
assert.Equal(t, true, routesAfter2Up[1].GetEnabled())
|
assert.True(t, routesAfter2Up[1].GetEnabled())
|
||||||
assert.Equal(t, false, routesAfter2Up[1].GetIsPrimary(), "r1 and r2 is back up, expected r1 to _still_ be primary")
|
assert.False(t, routesAfter2Up[1].GetIsPrimary(), "r1 and r2 is back up, expected r1 to _still_ be primary")
|
||||||
|
|
||||||
// Verify that the route is announced from subnet router 1
|
// Verify that the route is announced from subnet router 1
|
||||||
clientStatus, err = client.Status()
|
clientStatus, err = client.Status()
|
||||||
|
@ -677,14 +677,14 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||||
t.Logf("routes after disabling r1 %#v", routesAfterDisabling1)
|
t.Logf("routes after disabling r1 %#v", routesAfterDisabling1)
|
||||||
|
|
||||||
// Node 1 is not primary
|
// Node 1 is not primary
|
||||||
assert.Equal(t, true, routesAfterDisabling1[0].GetAdvertised())
|
assert.True(t, routesAfterDisabling1[0].GetAdvertised())
|
||||||
assert.Equal(t, false, routesAfterDisabling1[0].GetEnabled())
|
assert.False(t, routesAfterDisabling1[0].GetEnabled())
|
||||||
assert.Equal(t, false, routesAfterDisabling1[0].GetIsPrimary())
|
assert.False(t, routesAfterDisabling1[0].GetIsPrimary())
|
||||||
|
|
||||||
// Node 2 is primary
|
// Node 2 is primary
|
||||||
assert.Equal(t, true, routesAfterDisabling1[1].GetAdvertised())
|
assert.True(t, routesAfterDisabling1[1].GetAdvertised())
|
||||||
assert.Equal(t, true, routesAfterDisabling1[1].GetEnabled())
|
assert.True(t, routesAfterDisabling1[1].GetEnabled())
|
||||||
assert.Equal(t, true, routesAfterDisabling1[1].GetIsPrimary())
|
assert.True(t, routesAfterDisabling1[1].GetIsPrimary())
|
||||||
|
|
||||||
// Verify that the route is announced from subnet router 1
|
// Verify that the route is announced from subnet router 1
|
||||||
clientStatus, err = client.Status()
|
clientStatus, err = client.Status()
|
||||||
|
@ -735,14 +735,14 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||||
assert.Len(t, routesAfterEnabling1, 2)
|
assert.Len(t, routesAfterEnabling1, 2)
|
||||||
|
|
||||||
// Node 1 is not primary
|
// Node 1 is not primary
|
||||||
assert.Equal(t, true, routesAfterEnabling1[0].GetAdvertised())
|
assert.True(t, routesAfterEnabling1[0].GetAdvertised())
|
||||||
assert.Equal(t, true, routesAfterEnabling1[0].GetEnabled())
|
assert.True(t, routesAfterEnabling1[0].GetEnabled())
|
||||||
assert.Equal(t, false, routesAfterEnabling1[0].GetIsPrimary())
|
assert.False(t, routesAfterEnabling1[0].GetIsPrimary())
|
||||||
|
|
||||||
// Node 2 is primary
|
// Node 2 is primary
|
||||||
assert.Equal(t, true, routesAfterEnabling1[1].GetAdvertised())
|
assert.True(t, routesAfterEnabling1[1].GetAdvertised())
|
||||||
assert.Equal(t, true, routesAfterEnabling1[1].GetEnabled())
|
assert.True(t, routesAfterEnabling1[1].GetEnabled())
|
||||||
assert.Equal(t, true, routesAfterEnabling1[1].GetIsPrimary())
|
assert.True(t, routesAfterEnabling1[1].GetIsPrimary())
|
||||||
|
|
||||||
// Verify that the route is announced from subnet router 1
|
// Verify that the route is announced from subnet router 1
|
||||||
clientStatus, err = client.Status()
|
clientStatus, err = client.Status()
|
||||||
|
@ -795,9 +795,9 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||||
t.Logf("routes after deleting r2 %#v", routesAfterDeleting2)
|
t.Logf("routes after deleting r2 %#v", routesAfterDeleting2)
|
||||||
|
|
||||||
// Node 1 is primary
|
// Node 1 is primary
|
||||||
assert.Equal(t, true, routesAfterDeleting2[0].GetAdvertised())
|
assert.True(t, routesAfterDeleting2[0].GetAdvertised())
|
||||||
assert.Equal(t, true, routesAfterDeleting2[0].GetEnabled())
|
assert.True(t, routesAfterDeleting2[0].GetEnabled())
|
||||||
assert.Equal(t, true, routesAfterDeleting2[0].GetIsPrimary())
|
assert.True(t, routesAfterDeleting2[0].GetIsPrimary())
|
||||||
|
|
||||||
// Verify that the route is announced from subnet router 1
|
// Verify that the route is announced from subnet router 1
|
||||||
clientStatus, err = client.Status()
|
clientStatus, err = client.Status()
|
||||||
|
@ -893,9 +893,9 @@ func TestEnableDisableAutoApprovedRoute(t *testing.T) {
|
||||||
assert.Len(t, routes, 1)
|
assert.Len(t, routes, 1)
|
||||||
|
|
||||||
// All routes should be auto approved and enabled
|
// All routes should be auto approved and enabled
|
||||||
assert.Equal(t, true, routes[0].GetAdvertised())
|
assert.True(t, routes[0].GetAdvertised())
|
||||||
assert.Equal(t, true, routes[0].GetEnabled())
|
assert.True(t, routes[0].GetEnabled())
|
||||||
assert.Equal(t, true, routes[0].GetIsPrimary())
|
assert.True(t, routes[0].GetIsPrimary())
|
||||||
|
|
||||||
// Stop advertising route
|
// Stop advertising route
|
||||||
command = []string{
|
command = []string{
|
||||||
|
@ -924,9 +924,9 @@ func TestEnableDisableAutoApprovedRoute(t *testing.T) {
|
||||||
assert.Len(t, notAdvertisedRoutes, 1)
|
assert.Len(t, notAdvertisedRoutes, 1)
|
||||||
|
|
||||||
// Route is no longer advertised
|
// Route is no longer advertised
|
||||||
assert.Equal(t, false, notAdvertisedRoutes[0].GetAdvertised())
|
assert.False(t, notAdvertisedRoutes[0].GetAdvertised())
|
||||||
assert.Equal(t, false, notAdvertisedRoutes[0].GetEnabled())
|
assert.False(t, notAdvertisedRoutes[0].GetEnabled())
|
||||||
assert.Equal(t, true, notAdvertisedRoutes[0].GetIsPrimary())
|
assert.True(t, notAdvertisedRoutes[0].GetIsPrimary())
|
||||||
|
|
||||||
// Advertise route again
|
// Advertise route again
|
||||||
command = []string{
|
command = []string{
|
||||||
|
@ -955,9 +955,9 @@ func TestEnableDisableAutoApprovedRoute(t *testing.T) {
|
||||||
assert.Len(t, reAdvertisedRoutes, 1)
|
assert.Len(t, reAdvertisedRoutes, 1)
|
||||||
|
|
||||||
// All routes should be auto approved and enabled
|
// All routes should be auto approved and enabled
|
||||||
assert.Equal(t, true, reAdvertisedRoutes[0].GetAdvertised())
|
assert.True(t, reAdvertisedRoutes[0].GetAdvertised())
|
||||||
assert.Equal(t, true, reAdvertisedRoutes[0].GetEnabled())
|
assert.True(t, reAdvertisedRoutes[0].GetEnabled())
|
||||||
assert.Equal(t, true, reAdvertisedRoutes[0].GetIsPrimary())
|
assert.True(t, reAdvertisedRoutes[0].GetIsPrimary())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAutoApprovedSubRoute2068(t *testing.T) {
|
func TestAutoApprovedSubRoute2068(t *testing.T) {
|
||||||
|
@ -1163,9 +1163,9 @@ func TestSubnetRouteACL(t *testing.T) {
|
||||||
assert.Len(t, routes, 1)
|
assert.Len(t, routes, 1)
|
||||||
|
|
||||||
for _, route := range routes {
|
for _, route := range routes {
|
||||||
assert.Equal(t, true, route.GetAdvertised())
|
assert.True(t, route.GetAdvertised())
|
||||||
assert.Equal(t, false, route.GetEnabled())
|
assert.False(t, route.GetEnabled())
|
||||||
assert.Equal(t, false, route.GetIsPrimary())
|
assert.False(t, route.GetIsPrimary())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify that no routes has been sent to the client,
|
// Verify that no routes has been sent to the client,
|
||||||
|
@ -1212,9 +1212,9 @@ func TestSubnetRouteACL(t *testing.T) {
|
||||||
assert.Len(t, enablingRoutes, 1)
|
assert.Len(t, enablingRoutes, 1)
|
||||||
|
|
||||||
// Node 1 has active route
|
// Node 1 has active route
|
||||||
assert.Equal(t, true, enablingRoutes[0].GetAdvertised())
|
assert.True(t, enablingRoutes[0].GetAdvertised())
|
||||||
assert.Equal(t, true, enablingRoutes[0].GetEnabled())
|
assert.True(t, enablingRoutes[0].GetEnabled())
|
||||||
assert.Equal(t, true, enablingRoutes[0].GetIsPrimary())
|
assert.True(t, enablingRoutes[0].GetIsPrimary())
|
||||||
|
|
||||||
// Verify that the client has routes from the primary machine
|
// Verify that the client has routes from the primary machine
|
||||||
srs1, _ := subRouter1.Status()
|
srs1, _ := subRouter1.Status()
|
||||||
|
|
|
@ -14,12 +14,14 @@ import (
|
||||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||||
"github.com/juanfont/headscale/hscontrol/util"
|
"github.com/juanfont/headscale/hscontrol/util"
|
||||||
"github.com/juanfont/headscale/integration/dockertestutil"
|
"github.com/juanfont/headscale/integration/dockertestutil"
|
||||||
|
"github.com/juanfont/headscale/integration/dsic"
|
||||||
"github.com/juanfont/headscale/integration/hsic"
|
"github.com/juanfont/headscale/integration/hsic"
|
||||||
"github.com/juanfont/headscale/integration/tsic"
|
"github.com/juanfont/headscale/integration/tsic"
|
||||||
"github.com/ory/dockertest/v3"
|
"github.com/ory/dockertest/v3"
|
||||||
"github.com/puzpuzpuz/xsync/v3"
|
"github.com/puzpuzpuz/xsync/v3"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
"tailscale.com/envknob"
|
"tailscale.com/envknob"
|
||||||
)
|
)
|
||||||
|
@ -140,6 +142,7 @@ type Scenario struct {
|
||||||
// TODO(kradalby): support multiple headcales for later, currently only
|
// TODO(kradalby): support multiple headcales for later, currently only
|
||||||
// use one.
|
// use one.
|
||||||
controlServers *xsync.MapOf[string, ControlServer]
|
controlServers *xsync.MapOf[string, ControlServer]
|
||||||
|
derpServers []*dsic.DERPServerInContainer
|
||||||
|
|
||||||
users map[string]*User
|
users map[string]*User
|
||||||
|
|
||||||
|
@ -203,11 +206,11 @@ func (s *Scenario) ShutdownAssertNoPanics(t *testing.T) {
|
||||||
|
|
||||||
if t != nil {
|
if t != nil {
|
||||||
stdout, err := os.ReadFile(stdoutPath)
|
stdout, err := os.ReadFile(stdoutPath)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotContains(t, string(stdout), "panic")
|
assert.NotContains(t, string(stdout), "panic")
|
||||||
|
|
||||||
stderr, err := os.ReadFile(stderrPath)
|
stderr, err := os.ReadFile(stderrPath)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotContains(t, string(stderr), "panic")
|
assert.NotContains(t, string(stderr), "panic")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -224,6 +227,13 @@ func (s *Scenario) ShutdownAssertNoPanics(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, derp := range s.derpServers {
|
||||||
|
err := derp.Shutdown()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to tear down derp server: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.pool.RemoveNetwork(s.network); err != nil {
|
if err := s.pool.RemoveNetwork(s.network); err != nil {
|
||||||
log.Printf("failed to remove network: %s", err)
|
log.Printf("failed to remove network: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -352,7 +362,7 @@ func (s *Scenario) CreateTailscaleNodesInUser(
|
||||||
hostname := headscale.GetHostname()
|
hostname := headscale.GetHostname()
|
||||||
|
|
||||||
opts = append(opts,
|
opts = append(opts,
|
||||||
tsic.WithHeadscaleTLS(cert),
|
tsic.WithCACert(cert),
|
||||||
tsic.WithHeadscaleName(hostname),
|
tsic.WithHeadscaleName(hostname),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -651,3 +661,20 @@ func (s *Scenario) WaitForTailscaleLogout() error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateDERPServer creates a new DERP server in a container.
|
||||||
|
func (s *Scenario) CreateDERPServer(version string, opts ...dsic.Option) (*dsic.DERPServerInContainer, error) {
|
||||||
|
derp, err := dsic.New(s.pool, version, s.network, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create DERP server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = derp.WaitForRunning()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to reach DERP server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.derpServers = append(s.derpServers, derp)
|
||||||
|
|
||||||
|
return derp, nil
|
||||||
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ type TailscaleClient interface {
|
||||||
FQDN() (string, error)
|
FQDN() (string, error)
|
||||||
Status(...bool) (*ipnstate.Status, error)
|
Status(...bool) (*ipnstate.Status, error)
|
||||||
Netmap() (*netmap.NetworkMap, error)
|
Netmap() (*netmap.NetworkMap, error)
|
||||||
|
DebugDERPRegion(region string) (*ipnstate.DebugDERPRegionReport, error)
|
||||||
Netcheck() (*netcheck.Report, error)
|
Netcheck() (*netcheck.Report, error)
|
||||||
WaitForNeedsLogin() error
|
WaitForNeedsLogin() error
|
||||||
WaitForRunning() error
|
WaitForRunning() error
|
||||||
|
|
|
@ -33,7 +33,7 @@ const (
|
||||||
defaultPingTimeout = 300 * time.Millisecond
|
defaultPingTimeout = 300 * time.Millisecond
|
||||||
defaultPingCount = 10
|
defaultPingCount = 10
|
||||||
dockerContextPath = "../."
|
dockerContextPath = "../."
|
||||||
headscaleCertPath = "/usr/local/share/ca-certificates/headscale.crt"
|
caCertRoot = "/usr/local/share/ca-certificates"
|
||||||
dockerExecuteTimeout = 60 * time.Second
|
dockerExecuteTimeout = 60 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -71,7 +71,7 @@ type TailscaleInContainer struct {
|
||||||
fqdn string
|
fqdn string
|
||||||
|
|
||||||
// optional config
|
// optional config
|
||||||
headscaleCert []byte
|
caCerts [][]byte
|
||||||
headscaleHostname string
|
headscaleHostname string
|
||||||
withWebsocketDERP bool
|
withWebsocketDERP bool
|
||||||
withSSH bool
|
withSSH bool
|
||||||
|
@ -93,11 +93,10 @@ type TailscaleInContainerBuildConfig struct {
|
||||||
// Tailscale instance.
|
// Tailscale instance.
|
||||||
type Option = func(c *TailscaleInContainer)
|
type Option = func(c *TailscaleInContainer)
|
||||||
|
|
||||||
// WithHeadscaleTLS takes the certificate of the Headscale instance
|
// WithCACert adds it to the trusted surtificate of the Tailscale container.
|
||||||
// and adds it to the trusted surtificate of the Tailscale container.
|
func WithCACert(cert []byte) Option {
|
||||||
func WithHeadscaleTLS(cert []byte) Option {
|
|
||||||
return func(tsic *TailscaleInContainer) {
|
return func(tsic *TailscaleInContainer) {
|
||||||
tsic.headscaleCert = cert
|
tsic.caCerts = append(tsic.caCerts, cert)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,7 +125,7 @@ func WithOrCreateNetwork(network *dockertest.Network) Option {
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithHeadscaleName set the name of the headscale instance,
|
// WithHeadscaleName set the name of the headscale instance,
|
||||||
// mostly useful in combination with TLS and WithHeadscaleTLS.
|
// mostly useful in combination with TLS and WithCACert.
|
||||||
func WithHeadscaleName(hsName string) Option {
|
func WithHeadscaleName(hsName string) Option {
|
||||||
return func(tsic *TailscaleInContainer) {
|
return func(tsic *TailscaleInContainer) {
|
||||||
tsic.headscaleHostname = hsName
|
tsic.headscaleHostname = hsName
|
||||||
|
@ -260,12 +259,8 @@ func New(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tsic.headscaleHostname != "" {
|
tailscaleOptions.ExtraHosts = append(tailscaleOptions.ExtraHosts,
|
||||||
tailscaleOptions.ExtraHosts = []string{
|
"host.docker.internal:host-gateway")
|
||||||
"host.docker.internal:host-gateway",
|
|
||||||
fmt.Sprintf("%s:host-gateway", tsic.headscaleHostname),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if tsic.workdir != "" {
|
if tsic.workdir != "" {
|
||||||
tailscaleOptions.WorkingDir = tsic.workdir
|
tailscaleOptions.WorkingDir = tsic.workdir
|
||||||
|
@ -351,8 +346,8 @@ func New(
|
||||||
|
|
||||||
tsic.container = container
|
tsic.container = container
|
||||||
|
|
||||||
if tsic.hasTLS() {
|
for i, cert := range tsic.caCerts {
|
||||||
err = tsic.WriteFile(headscaleCertPath, tsic.headscaleCert)
|
err = tsic.WriteFile(fmt.Sprintf("%s/user-%d.crt", caCertRoot, i), cert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to write TLS certificate to container: %w", err)
|
return nil, fmt.Errorf("failed to write TLS certificate to container: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -361,10 +356,6 @@ func New(
|
||||||
return tsic, nil
|
return tsic, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TailscaleInContainer) hasTLS() bool {
|
|
||||||
return len(t.headscaleCert) != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shutdown stops and cleans up the Tailscale container.
|
// Shutdown stops and cleans up the Tailscale container.
|
||||||
func (t *TailscaleInContainer) Shutdown() error {
|
func (t *TailscaleInContainer) Shutdown() error {
|
||||||
err := t.SaveLog("/tmp/control")
|
err := t.SaveLog("/tmp/control")
|
||||||
|
@ -739,6 +730,34 @@ func (t *TailscaleInContainer) watchIPN(ctx context.Context) (*ipn.Notify, error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *TailscaleInContainer) DebugDERPRegion(region string) (*ipnstate.DebugDERPRegionReport, error) {
|
||||||
|
if !util.TailscaleVersionNewerOrEqual("1.34", t.version) {
|
||||||
|
panic("tsic.DebugDERPRegion() called with unsupported version: " + t.version)
|
||||||
|
}
|
||||||
|
|
||||||
|
command := []string{
|
||||||
|
"tailscale",
|
||||||
|
"debug",
|
||||||
|
"derp",
|
||||||
|
region,
|
||||||
|
}
|
||||||
|
|
||||||
|
result, stderr, err := t.Execute(command)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("stderr: %s\n", stderr) // nolint
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("failed to execute tailscale debug derp command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var report ipnstate.DebugDERPRegionReport
|
||||||
|
err = json.Unmarshal([]byte(result), &report)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal tailscale derp region report: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &report, err
|
||||||
|
}
|
||||||
|
|
||||||
// Netcheck returns the current Netcheck Report (netcheck.Report) of the Tailscale instance.
|
// Netcheck returns the current Netcheck Report (netcheck.Report) of the Tailscale instance.
|
||||||
func (t *TailscaleInContainer) Netcheck() (*netcheck.Report, error) {
|
func (t *TailscaleInContainer) Netcheck() (*netcheck.Report, error) {
|
||||||
command := []string{
|
command := []string{
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
---
|
||||||
site_name: Headscale
|
site_name: Headscale
|
||||||
site_url: https://juanfont.github.io/headscale
|
site_url: https://juanfont.github.io/headscale/
|
||||||
edit_uri: blob/main/docs/ # Change the master branch to main as we are using main as a main branch
|
edit_uri: blob/main/docs/ # Change the master branch to main as we are using main as a main branch
|
||||||
site_author: Headscale authors
|
site_author: Headscale authors
|
||||||
site_description: >-
|
site_description: >-
|
||||||
|
|
Loading…
Reference in a new issue