mirror of
https://github.com/juanfont/headscale.git
synced 2026-02-12 10:47:42 +01:00
Compare commits
53 Commits
v0.24.0-be
...
v0.25.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5037c25a6 | ||
|
|
c53ff2ce00 | ||
|
|
b4ac8cd9a3 | ||
|
|
22277d1fc7 | ||
|
|
9ae3570154 | ||
|
|
f12cb2e048 | ||
|
|
8c09afe20c | ||
|
|
8b92c017ec | ||
|
|
9a7890d56b | ||
|
|
45752db0f6 | ||
|
|
1c7f3bc440 | ||
|
|
9bd143852f | ||
|
|
d57a55c024 | ||
|
|
e172c29360 | ||
|
|
cd3b8e68ff | ||
|
|
f44b1d37c4 | ||
|
|
7ba6ad3489 | ||
|
|
2c279e0a7b | ||
|
|
4c8e847f47 | ||
|
|
97e5d95399 | ||
|
|
d1dbe4ece9 | ||
|
|
9e3f945eda | ||
|
|
615ee5df75 | ||
|
|
c1f42cdf4b | ||
|
|
aa76980b43 | ||
|
|
5b986ed0a7 | ||
|
|
8076c94444 | ||
|
|
e88406e837 | ||
|
|
e4a3dcc3b8 | ||
|
|
caad5c613d | ||
|
|
38aef77e54 | ||
|
|
1ab7b315a2 | ||
|
|
610597bfb7 | ||
|
|
ede4f97a16 | ||
|
|
fa641e38b8 | ||
|
|
41bad2b9fd | ||
|
|
f9bbfa5eab | ||
|
|
b81420bef1 | ||
|
|
9313e5b058 | ||
|
|
770f3dcb93 | ||
|
|
af4508b9dc | ||
|
|
bbc93a90a2 | ||
|
|
0acb2b5647 | ||
|
|
3269cfdca0 | ||
|
|
319ce67c87 | ||
|
|
47b405d6c6 | ||
|
|
65304a0ce7 | ||
|
|
e270169c13 | ||
|
|
7d937c6bd0 | ||
|
|
ccc895b4c6 | ||
|
|
5345f19693 | ||
|
|
ec8729b772 | ||
|
|
e00b9d9a91 |
2
.github/workflows/check-tests.yaml
vendored
2
.github/workflows/check-tests.yaml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
- name: Generate and check integration tests
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
run: |
|
||||
nix develop --command bash -c "cd cmd/gh-action-integration-generator/ && go generate"
|
||||
nix develop --command bash -c "cd .github/workflows && go generate"
|
||||
git diff --exit-code .github/workflows/test-integration.yaml
|
||||
|
||||
- name: Show missing tests
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package main
|
||||
|
||||
//go:generate go run ./main.go
|
||||
//go:generate go run ./gh-action-integration-generator.go
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -42,15 +42,19 @@ func updateYAML(tests []string) {
|
||||
testsForYq := fmt.Sprintf("[%s]", strings.Join(tests, ", "))
|
||||
|
||||
yqCommand := fmt.Sprintf(
|
||||
"yq eval '.jobs.integration-test.strategy.matrix.test = %s' ../../.github/workflows/test-integration.yaml -i",
|
||||
"yq eval '.jobs.integration-test.strategy.matrix.test = %s' ./test-integration.yaml -i",
|
||||
testsForYq,
|
||||
)
|
||||
cmd := exec.Command("bash", "-c", yqCommand)
|
||||
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
log.Printf("stdout: %s", stdout.String())
|
||||
log.Printf("stderr: %s", stderr.String())
|
||||
log.Fatalf("failed to run yq command: %s", err)
|
||||
}
|
||||
|
||||
5
.github/workflows/test-integration.yaml
vendored
5
.github/workflows/test-integration.yaml
vendored
@@ -22,9 +22,13 @@ jobs:
|
||||
- TestACLNamedHostsCanReach
|
||||
- TestACLDevice1CanAccessDevice2
|
||||
- TestPolicyUpdateWhileRunningWithCLIInDatabase
|
||||
- TestAuthKeyLogoutAndReloginSameUser
|
||||
- TestAuthKeyLogoutAndReloginNewUser
|
||||
- TestOIDCAuthenticationPingAll
|
||||
- TestOIDCExpireNodesBasedOnTokenExpiry
|
||||
- TestOIDC024UserCreation
|
||||
- TestOIDCAuthenticationWithPKCE
|
||||
- TestOIDCReloginSameNodeNewUser
|
||||
- TestAuthWebFlowAuthenticationPingAll
|
||||
- TestAuthWebFlowLogoutAndRelogin
|
||||
- TestUserCommand
|
||||
@@ -49,7 +53,6 @@ jobs:
|
||||
- TestDERPServerWebsocketScenario
|
||||
- TestPingAllByIP
|
||||
- TestPingAllByIPPublicDERP
|
||||
- TestAuthKeyLogoutAndRelogin
|
||||
- TestEphemeral
|
||||
- TestEphemeralInAlternateTimezone
|
||||
- TestEphemeral2006DeletedTooQuickly
|
||||
|
||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -34,4 +34,10 @@ jobs:
|
||||
|
||||
- name: Run tests
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
env:
|
||||
# As of 2025-01-06, these env vars was not automatically
|
||||
# set anymore which breaks the initdb for postgres on
|
||||
# some of the database migration tests.
|
||||
LC_ALL: "en_US.UTF-8"
|
||||
LC_CTYPE: "en_US.UTF-8"
|
||||
run: nix develop --command -- gotestsum
|
||||
|
||||
@@ -82,7 +82,9 @@ nfpms:
|
||||
|
||||
kos:
|
||||
- id: ghcr
|
||||
repository: ghcr.io/juanfont/headscale
|
||||
repositories:
|
||||
- ghcr.io/juanfont/headscale
|
||||
- headscale/headscale
|
||||
|
||||
# bare tells KO to only use the repository
|
||||
# for tagging and naming the container.
|
||||
@@ -110,31 +112,11 @@ kos:
|
||||
- '{{ trimprefix .Tag "v" }}'
|
||||
- "sha-{{ .ShortCommit }}"
|
||||
|
||||
- id: dockerhub
|
||||
build: headscale
|
||||
base_image: gcr.io/distroless/base-debian12
|
||||
repository: headscale/headscale
|
||||
bare: true
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/386
|
||||
- linux/arm64
|
||||
- linux/arm/v7
|
||||
tags:
|
||||
- "{{ if not .Prerelease }}latest{{ end }}"
|
||||
- "{{ if not .Prerelease }}{{ .Major }}.{{ .Minor }}.{{ .Patch }}{{ end }}"
|
||||
- "{{ if not .Prerelease }}{{ .Major }}.{{ .Minor }}{{ end }}"
|
||||
- "{{ if not .Prerelease }}{{ .Major }}{{ end }}"
|
||||
- "{{ if not .Prerelease }}v{{ .Major }}.{{ .Minor }}.{{ .Patch }}{{ end }}"
|
||||
- "{{ if not .Prerelease }}v{{ .Major }}.{{ .Minor }}{{ end }}"
|
||||
- "{{ if not .Prerelease }}v{{ .Major }}{{ end }}"
|
||||
- "{{ if not .Prerelease }}stable{{ else }}unstable{{ end }}"
|
||||
- "{{ .Tag }}"
|
||||
- '{{ trimprefix .Tag "v" }}'
|
||||
- "sha-{{ .ShortCommit }}"
|
||||
|
||||
- id: ghcr-debug
|
||||
repository: ghcr.io/juanfont/headscale
|
||||
repositories:
|
||||
- ghcr.io/juanfont/headscale
|
||||
- headscale/headscale
|
||||
|
||||
bare: true
|
||||
base_image: gcr.io/distroless/base-debian12:debug
|
||||
build: headscale
|
||||
@@ -159,29 +141,6 @@ kos:
|
||||
- '{{ trimprefix .Tag "v" }}-debug'
|
||||
- "sha-{{ .ShortCommit }}-debug"
|
||||
|
||||
- id: dockerhub-debug
|
||||
build: headscale
|
||||
base_image: gcr.io/distroless/base-debian12:debug
|
||||
repository: headscale/headscale
|
||||
bare: true
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/386
|
||||
- linux/arm64
|
||||
- linux/arm/v7
|
||||
tags:
|
||||
- "{{ if not .Prerelease }}latest-debug{{ end }}"
|
||||
- "{{ if not .Prerelease }}{{ .Major }}.{{ .Minor }}.{{ .Patch }}-debug{{ end }}"
|
||||
- "{{ if not .Prerelease }}{{ .Major }}.{{ .Minor }}-debug{{ end }}"
|
||||
- "{{ if not .Prerelease }}{{ .Major }}-debug{{ end }}"
|
||||
- "{{ if not .Prerelease }}v{{ .Major }}.{{ .Minor }}.{{ .Patch }}-debug{{ end }}"
|
||||
- "{{ if not .Prerelease }}v{{ .Major }}.{{ .Minor }}-debug{{ end }}"
|
||||
- "{{ if not .Prerelease }}v{{ .Major }}-debug{{ end }}"
|
||||
- "{{ if not .Prerelease }}stable-debug{{ else }}unstable-debug{{ end }}"
|
||||
- "{{ .Tag }}-debug"
|
||||
- '{{ trimprefix .Tag "v" }}-debug'
|
||||
- "sha-{{ .ShortCommit }}-debug"
|
||||
|
||||
checksum:
|
||||
name_template: "checksums.txt"
|
||||
snapshot:
|
||||
|
||||
109
CHANGELOG.md
109
CHANGELOG.md
@@ -2,7 +2,75 @@
|
||||
|
||||
## Next
|
||||
|
||||
## 0.24.0 (2024-xx-xx)
|
||||
|
||||
## 0.25.0 (2025-02-11)
|
||||
|
||||
### BREAKING
|
||||
|
||||
- Authentication flow has been rewritten
|
||||
[#2374](https://github.com/juanfont/headscale/pull/2374) This change should be
|
||||
transparent to users with the exception of some buxfixes that has been
|
||||
discovered and was fixed as part of the rewrite.
|
||||
- When a node is registered with _a new user_, it will be registered as a new
|
||||
node ([#2327](https://github.com/juanfont/headscale/issues/2327) and
|
||||
[#1310](https://github.com/juanfont/headscale/issues/1310)).
|
||||
- A logged out node logging in with the same user will replace the existing
|
||||
node.
|
||||
- Remove support for Tailscale clients older than 1.62 (Capability version 87)
|
||||
[#2405](https://github.com/juanfont/headscale/pull/2405)
|
||||
|
||||
### Changes
|
||||
|
||||
- `oidc.map_legacy_users` is now `false` by default
|
||||
[#2350](https://github.com/juanfont/headscale/pull/2350)
|
||||
- Print Tailscale version instead of capability versions for outdated nodes
|
||||
[#2391](https://github.com/juanfont/headscale/pull/2391)
|
||||
- Do not allow renaming of users from OIDC
|
||||
[#2393](https://github.com/juanfont/headscale/pull/2393)
|
||||
- Change minimum hostname length to 2
|
||||
[#2393](https://github.com/juanfont/headscale/pull/2393)
|
||||
- Fix migration error caused by nodes having invalid auth keys
|
||||
[#2412](https://github.com/juanfont/headscale/pull/2412)
|
||||
- Pre auth keys belonging to a user are no longer deleted with the user
|
||||
[#2396](https://github.com/juanfont/headscale/pull/2396)
|
||||
- Pre auth keys that are used by a node can no longer be deleted
|
||||
[#2396](https://github.com/juanfont/headscale/pull/2396)
|
||||
- Rehaul HTTP errors, return better status code and errors to users
|
||||
[#2398](https://github.com/juanfont/headscale/pull/2398)
|
||||
|
||||
## 0.24.3 (2025-02-07)
|
||||
|
||||
### Changes
|
||||
- Fix migration error caused by nodes having invalid auth keys
|
||||
[#2412](https://github.com/juanfont/headscale/pull/2412)
|
||||
- Pre auth keys belonging to a user are no longer deleted with the user
|
||||
[#2396](https://github.com/juanfont/headscale/pull/2396)
|
||||
- Pre auth keys that are used by a node can no longer be deleted
|
||||
[#2396](https://github.com/juanfont/headscale/pull/2396)
|
||||
|
||||
## 0.24.2 (2025-01-30)
|
||||
|
||||
### Changes
|
||||
|
||||
- Fix issue where email and username being equal fails to match in Policy
|
||||
[#2388](https://github.com/juanfont/headscale/pull/2388)
|
||||
- Delete invalid routes before adding a NOT NULL constraint on node_id
|
||||
[#2386](https://github.com/juanfont/headscale/pull/2386)
|
||||
|
||||
## 0.24.1 (2025-01-23)
|
||||
|
||||
### Changes
|
||||
|
||||
- Fix migration issue with user table for PostgreSQL
|
||||
[#2367](https://github.com/juanfont/headscale/pull/2367)
|
||||
- Relax username validation to allow emails
|
||||
[#2364](https://github.com/juanfont/headscale/pull/2364)
|
||||
- Remove invalid routes and add stronger constraints for routes to avoid API
|
||||
panic [#2371](https://github.com/juanfont/headscale/pull/2371)
|
||||
- Fix panic when `derp.update_frequency` is 0
|
||||
[#2368](https://github.com/juanfont/headscale/pull/2368)
|
||||
|
||||
## 0.24.0 (2025-01-17)
|
||||
|
||||
### Security fix: OIDC changes in Headscale 0.24.0
|
||||
|
||||
@@ -33,8 +101,7 @@ and have it populate to Headscale automatically the next time they log in.
|
||||
However, this may affect the way you reference users in policies.
|
||||
|
||||
Headscale v0.23.0 and earlier never recorded the `iss` and `sub` fields, so all
|
||||
legacy (existing) OIDC accounts _need to be migrated_ to be properly
|
||||
secured.
|
||||
legacy (existing) OIDC accounts _need to be migrated_ to be properly secured.
|
||||
|
||||
#### What do I need to do to migrate?
|
||||
|
||||
@@ -46,8 +113,8 @@ The migration will mostly be done automatically, with one exception. If your
|
||||
OIDC does not provide an `email_verified` claim, Headscale will ignore the
|
||||
`email`. This means that either the administrator will have to mark the user
|
||||
emails as verified, or ensure the users verify their emails. Any unverified
|
||||
emails will be ignored, meaning that the users will get new accounts instead
|
||||
of being migrated.
|
||||
emails will be ignored, meaning that the users will get new accounts instead of
|
||||
being migrated.
|
||||
|
||||
After this exception is ensured, make all users log into Headscale with their
|
||||
account, and Headscale will automatically update the account record. This will
|
||||
@@ -148,12 +215,13 @@ This will also affect the way you
|
||||
- User gRPC/API [#2261](https://github.com/juanfont/headscale/pull/2261):
|
||||
- If you depend on a Headscale Web UI, you should wait with this update until
|
||||
the UI have been updated to match the new API.
|
||||
- `GET /api/v1/user/{name}` and `GetUser` have been removed in favour of `ListUsers` with an ID parameter
|
||||
- `GET /api/v1/user/{name}` and `GetUser` have been removed in favour of
|
||||
`ListUsers` with an ID parameter
|
||||
- `RenameUser` and `DeleteUser` now require an ID instead of a name.
|
||||
|
||||
### Changes
|
||||
|
||||
- Improved compatibilty of built-in DERP server with clients connecting over
|
||||
- Improved compatibility of built-in DERP server with clients connecting over
|
||||
WebSocket [#2132](https://github.com/juanfont/headscale/pull/2132)
|
||||
- Allow nodes to use SSH agent forwarding
|
||||
[#2145](https://github.com/juanfont/headscale/pull/2145)
|
||||
@@ -164,13 +232,18 @@ This will also affect the way you
|
||||
- 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)
|
||||
[#2232](https://github.com/juanfont/headscale/pull/2232)
|
||||
- Loosened up `server_url` and `base_domain` check. It was overly strict in some
|
||||
cases. [#2248](https://github.com/juanfont/headscale/pull/2248)
|
||||
- CLI for managing users now accepts `--identifier` in addition to `--name`,
|
||||
usage of `--identifier` is recommended
|
||||
[#2261](https://github.com/juanfont/headscale/pull/2261)
|
||||
- Add `dns.extra_records_path` configuration option [#2262](https://github.com/juanfont/headscale/issues/2262)
|
||||
- Add `dns.extra_records_path` configuration option
|
||||
[#2262](https://github.com/juanfont/headscale/issues/2262)
|
||||
- Support client verify for DERP
|
||||
[#2046](https://github.com/juanfont/headscale/pull/2046)
|
||||
- Add PKCE Verifier for OIDC
|
||||
[#2314](https://github.com/juanfont/headscale/pull/2314)
|
||||
|
||||
## 0.23.0 (2024-09-18)
|
||||
|
||||
@@ -253,14 +326,14 @@ part of adopting [#1460](https://github.com/juanfont/headscale/pull/1460).
|
||||
- Entrypoint of container image has changed from shell to headscale, require
|
||||
change from `headscale serve` to `serve`
|
||||
- `/var/lib/headscale` and `/var/run/headscale` is no longer created
|
||||
automatically, see [container docs](./docs/running-headscale-container.md)
|
||||
automatically, see [container docs](./docs/setup/install/container.md)
|
||||
- Prefixes are now defined per v4 and v6 range.
|
||||
[#1756](https://github.com/juanfont/headscale/pull/1756)
|
||||
- `ip_prefixes` option is now `prefixes.v4` and `prefixes.v6`
|
||||
- `prefixes.allocation` can be set to assign IPs at `sequential` or `random`.
|
||||
[#1869](https://github.com/juanfont/headscale/pull/1869)
|
||||
- MagicDNS domains no longer contain usernames []()
|
||||
- This is in preperation to fix Headscales implementation of tags which
|
||||
- This is in preparation to fix Headscales implementation of tags which
|
||||
currently does not correctly remove the link between a tagged device and a
|
||||
user. As tagged devices will not have a user, this will require a change to
|
||||
the DNS generation, removing the username, see
|
||||
@@ -346,7 +419,7 @@ part of adopting [#1460](https://github.com/juanfont/headscale/pull/1460).
|
||||
[#1391](https://github.com/juanfont/headscale/pull/1391)
|
||||
- Improvements on Noise implementation
|
||||
[#1379](https://github.com/juanfont/headscale/pull/1379)
|
||||
- Replace node filter logic, ensuring nodes with access can see eachother
|
||||
- Replace node filter logic, ensuring nodes with access can see each other
|
||||
[#1381](https://github.com/juanfont/headscale/pull/1381)
|
||||
- Disable (or delete) both exit routes at the same time
|
||||
[#1428](https://github.com/juanfont/headscale/pull/1428)
|
||||
@@ -643,7 +716,7 @@ part of adopting [#1460](https://github.com/juanfont/headscale/pull/1460).
|
||||
|
||||
- Boundaries between Namespaces has been removed and all nodes can communicate
|
||||
by default [#357](https://github.com/juanfont/headscale/pull/357)
|
||||
- To limit access between nodes, use [ACLs](./docs/acls.md).
|
||||
- To limit access between nodes, use [ACLs](./docs/ref/acls.md).
|
||||
- `/metrics` is now a configurable host:port endpoint:
|
||||
[#344](https://github.com/juanfont/headscale/pull/344). You must update your
|
||||
`config.yaml` file to include:
|
||||
@@ -701,13 +774,12 @@ behaviour.
|
||||
- All machines can communicate with all machines by default
|
||||
- Tags should now work correctly and adding a host to Headscale should now
|
||||
reload the rules.
|
||||
- The documentation have a [fictional example](docs/acls.md) that should cover
|
||||
some use cases of the ACLs features
|
||||
- The documentation have a [fictional example](./docs/ref/acls.md) that should
|
||||
cover some use cases of the ACLs features
|
||||
|
||||
### Features
|
||||
|
||||
- Add support for configurable mTLS
|
||||
[docs](docs/tls.md#configuring-mutual-tls-authentication-mtls)
|
||||
- Add support for configurable mTLS [docs](./docs/ref/tls.md)
|
||||
[#297](https://github.com/juanfont/headscale/pull/297)
|
||||
|
||||
### Changes
|
||||
@@ -721,7 +793,8 @@ behaviour.
|
||||
|
||||
- Add IPv6 support to the prefix assigned to namespaces
|
||||
- Add API Key support
|
||||
- Enable remote control of `headscale` via CLI [docs](docs/remote-cli.md)
|
||||
- Enable remote control of `headscale` via CLI
|
||||
[docs](./docs/ref/remote-cli.md)
|
||||
- Enable HTTP API (beta, subject to change)
|
||||
- OpenID Connect users will be mapped per namespaces
|
||||
- Each user will get its own namespace, created if it does not exist
|
||||
|
||||
4
Makefile
4
Makefile
@@ -22,7 +22,7 @@ build:
|
||||
dev: lint test build
|
||||
|
||||
test:
|
||||
gotestsum -- -short -coverprofile=coverage.out ./...
|
||||
gotestsum -- -short -race -coverprofile=coverage.out ./...
|
||||
|
||||
test_integration:
|
||||
docker run \
|
||||
@@ -33,7 +33,7 @@ test_integration:
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v $$PWD/control_logs:/tmp/control \
|
||||
golang:1 \
|
||||
go run gotest.tools/gotestsum@latest -- -failfast ./... -timeout 120m -parallel 8
|
||||
go run gotest.tools/gotestsum@latest -- -race -failfast ./... -timeout 120m -parallel 8
|
||||
|
||||
lint:
|
||||
golangci-lint run --fix --timeout 10m
|
||||
|
||||
12
README.md
12
README.md
@@ -32,12 +32,12 @@ organisation.
|
||||
|
||||
## Design goal
|
||||
|
||||
Headscale aims to implement a self-hosted, open source alternative to the Tailscale
|
||||
control server.
|
||||
Headscale's goal is to provide self-hosters and hobbyists with an open-source
|
||||
server they can use for their projects and labs.
|
||||
It implements a narrow scope, a single Tailnet, suitable for a personal use, or a small
|
||||
open-source organisation.
|
||||
Headscale aims to implement a self-hosted, open source alternative to the
|
||||
[Tailscale](https://tailscale.com/) control server. Headscale's goal is to
|
||||
provide self-hosters and hobbyists with an open-source server they can use for
|
||||
their projects and labs. It implements a narrow scope, a _single_ Tailscale
|
||||
network (tailnet), suitable for a personal use, or a small open-source
|
||||
organisation.
|
||||
|
||||
## Supporting Headscale
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc/status"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -79,7 +79,7 @@ var createNodeCmd = &cobra.Command{
|
||||
)
|
||||
}
|
||||
|
||||
machineKey, err := cmd.Flags().GetString("key")
|
||||
registrationID, err := cmd.Flags().GetString("key")
|
||||
if err != nil {
|
||||
ErrorOutput(
|
||||
err,
|
||||
@@ -88,8 +88,7 @@ var createNodeCmd = &cobra.Command{
|
||||
)
|
||||
}
|
||||
|
||||
var mkey key.MachinePublic
|
||||
err = mkey.UnmarshalText([]byte(machineKey))
|
||||
_, err = types.RegistrationIDFromString(registrationID)
|
||||
if err != nil {
|
||||
ErrorOutput(
|
||||
err,
|
||||
@@ -108,7 +107,7 @@ var createNodeCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
request := &v1.DebugCreateNodeRequest{
|
||||
Key: machineKey,
|
||||
Key: registrationID,
|
||||
Name: name,
|
||||
User: user,
|
||||
Routes: routes,
|
||||
|
||||
@@ -39,33 +39,33 @@ func init() {
|
||||
|
||||
err := registerNodeCmd.MarkFlagRequired("user")
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
registerNodeCmd.Flags().StringP("key", "k", "", "Key")
|
||||
err = registerNodeCmd.MarkFlagRequired("key")
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
nodeCmd.AddCommand(registerNodeCmd)
|
||||
|
||||
expireNodeCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
|
||||
err = expireNodeCmd.MarkFlagRequired("identifier")
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
nodeCmd.AddCommand(expireNodeCmd)
|
||||
|
||||
renameNodeCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
|
||||
err = renameNodeCmd.MarkFlagRequired("identifier")
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
nodeCmd.AddCommand(renameNodeCmd)
|
||||
|
||||
deleteNodeCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
|
||||
err = deleteNodeCmd.MarkFlagRequired("identifier")
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
nodeCmd.AddCommand(deleteNodeCmd)
|
||||
|
||||
@@ -73,7 +73,7 @@ func init() {
|
||||
|
||||
err = moveNodeCmd.MarkFlagRequired("identifier")
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
moveNodeCmd.Flags().StringP("user", "u", "", "New user")
|
||||
@@ -85,7 +85,7 @@ func init() {
|
||||
|
||||
err = moveNodeCmd.MarkFlagRequired("user")
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
nodeCmd.AddCommand(moveNodeCmd)
|
||||
|
||||
@@ -93,7 +93,7 @@ func init() {
|
||||
|
||||
err = tagCmd.MarkFlagRequired("identifier")
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
tagCmd.Flags().
|
||||
StringSliceP("tags", "t", []string{}, "List of tags to add to the node")
|
||||
@@ -122,7 +122,7 @@ var registerNodeCmd = &cobra.Command{
|
||||
defer cancel()
|
||||
defer conn.Close()
|
||||
|
||||
machineKey, err := cmd.Flags().GetString("key")
|
||||
registrationID, err := cmd.Flags().GetString("key")
|
||||
if err != nil {
|
||||
ErrorOutput(
|
||||
err,
|
||||
@@ -132,7 +132,7 @@ var registerNodeCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
request := &v1.RegisterNodeRequest{
|
||||
Key: machineKey,
|
||||
Key: registrationID,
|
||||
User: user,
|
||||
}
|
||||
|
||||
|
||||
@@ -25,21 +25,21 @@ func init() {
|
||||
enableRouteCmd.Flags().Uint64P("route", "r", 0, "Route identifier (ID)")
|
||||
err := enableRouteCmd.MarkFlagRequired("route")
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
routesCmd.AddCommand(enableRouteCmd)
|
||||
|
||||
disableRouteCmd.Flags().Uint64P("route", "r", 0, "Route identifier (ID)")
|
||||
err = disableRouteCmd.MarkFlagRequired("route")
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
routesCmd.AddCommand(disableRouteCmd)
|
||||
|
||||
deleteRouteCmd.Flags().Uint64P("route", "r", 0, "Route identifier (ID)")
|
||||
err = deleteRouteCmd.MarkFlagRequired("route")
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
routesCmd.AddCommand(deleteRouteCmd)
|
||||
}
|
||||
@@ -251,10 +251,15 @@ func routesToPtables(routes []*v1.Route) pterm.TableData {
|
||||
isPrimaryStr = strconv.FormatBool(route.GetIsPrimary())
|
||||
}
|
||||
|
||||
var nodeName string
|
||||
if route.GetNode() != nil {
|
||||
nodeName = route.GetNode().GetGivenName()
|
||||
}
|
||||
|
||||
tableData = append(tableData,
|
||||
[]string{
|
||||
strconv.FormatUint(route.GetId(), Base10),
|
||||
route.GetNode().GetGivenName(),
|
||||
nodeName,
|
||||
route.GetPrefix(),
|
||||
strconv.FormatBool(route.GetAdvertised()),
|
||||
strconv.FormatBool(route.GetEnabled()),
|
||||
|
||||
@@ -3,6 +3,7 @@ package cli
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
survey "github.com/AlecAivazis/survey/v2"
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
@@ -40,6 +41,9 @@ func usernameAndIDFromFlag(cmd *cobra.Command) (uint64, string) {
|
||||
func init() {
|
||||
rootCmd.AddCommand(userCmd)
|
||||
userCmd.AddCommand(createUserCmd)
|
||||
createUserCmd.Flags().StringP("display-name", "d", "", "Display name")
|
||||
createUserCmd.Flags().StringP("email", "e", "", "Email")
|
||||
createUserCmd.Flags().StringP("picture-url", "p", "", "Profile picture URL")
|
||||
userCmd.AddCommand(listUsersCmd)
|
||||
usernameAndIDFlag(listUsersCmd)
|
||||
listUsersCmd.Flags().StringP("email", "e", "", "Email")
|
||||
@@ -83,6 +87,28 @@ var createUserCmd = &cobra.Command{
|
||||
|
||||
request := &v1.CreateUserRequest{Name: userName}
|
||||
|
||||
if displayName, _ := cmd.Flags().GetString("display-name"); displayName != "" {
|
||||
request.DisplayName = displayName
|
||||
}
|
||||
|
||||
if email, _ := cmd.Flags().GetString("email"); email != "" {
|
||||
request.Email = email
|
||||
}
|
||||
|
||||
if pictureURL, _ := cmd.Flags().GetString("picture-url"); pictureURL != "" {
|
||||
if _, err := url.Parse(pictureURL); err != nil {
|
||||
ErrorOutput(
|
||||
err,
|
||||
fmt.Sprintf(
|
||||
"Invalid Picture URL: %s",
|
||||
err,
|
||||
),
|
||||
output,
|
||||
)
|
||||
}
|
||||
request.PictureUrl = pictureURL
|
||||
}
|
||||
|
||||
log.Trace().Interface("request", request).Msg("Sending CreateUser request")
|
||||
response, err := client.CreateUser(ctx, request)
|
||||
if err != nil {
|
||||
|
||||
@@ -301,7 +301,7 @@ dns:
|
||||
search_domains: []
|
||||
|
||||
# Extra DNS records
|
||||
# so far only A-records are supported (on the tailscale side)
|
||||
# so far only A and AAAA records are supported (on the tailscale side)
|
||||
# See: docs/ref/dns.md
|
||||
extra_records: []
|
||||
# - name: "grafana.myvpn.example.com"
|
||||
@@ -310,6 +310,10 @@ dns:
|
||||
#
|
||||
# # you can also put it in one line
|
||||
# - { name: "prometheus.myvpn.example.com", type: "A", value: "100.64.0.3" }
|
||||
#
|
||||
# Alternatively, extra DNS records can be loaded from a JSON file.
|
||||
# Headscale processes this file on each change.
|
||||
# extra_records_path: /var/lib/headscale/extra-records.json
|
||||
|
||||
# Unix socket used for the CLI to connect without authentication
|
||||
# Note: for production you will want to set this to something like:
|
||||
@@ -360,6 +364,18 @@ unix_socket_permission: "0770"
|
||||
# allowed_users:
|
||||
# - alice@example.com
|
||||
#
|
||||
# # Optional: PKCE (Proof Key for Code Exchange) configuration
|
||||
# # PKCE adds an additional layer of security to the OAuth 2.0 authorization code flow
|
||||
# # by preventing authorization code interception attacks
|
||||
# # See https://datatracker.ietf.org/doc/html/rfc7636
|
||||
# pkce:
|
||||
# # Enable or disable PKCE support (default: false)
|
||||
# enabled: false
|
||||
# # PKCE method to use:
|
||||
# # - plain: Use plain code verifier
|
||||
# # - S256: Use SHA256 hashed code verifier (default, recommended)
|
||||
# method: S256
|
||||
#
|
||||
# # Map legacy users from pre-0.24.0 versions of headscale to the new OIDC users
|
||||
# # by taking the username from the legacy user and matching it with the username
|
||||
# # provided by the OIDC. This is useful when migrating from legacy users to OIDC
|
||||
@@ -368,10 +384,10 @@ unix_socket_permission: "0770"
|
||||
# # Note that this will only work if the username from the legacy user is the same
|
||||
# # and there is a possibility 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.
|
||||
# # When this feature is disabled, it 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
|
||||
# map_legacy_users: false
|
||||
|
||||
# Logtail configuration
|
||||
# Logtail is Tailscales logging and auditing infrastructure, it allows the control panel
|
||||
|
||||
@@ -10,7 +10,7 @@ headscale.
|
||||
| OpenBSD | Yes |
|
||||
| FreeBSD | Yes |
|
||||
| Windows | Yes (see [docs](../usage/connect/windows.md) and `/windows` on your headscale for more information) |
|
||||
| Android | Yes (see [docs](../usage/connect/android.md)) |
|
||||
| Android | Yes (see [docs](../usage/connect/android.md) for more information) |
|
||||
| macOS | Yes (see [docs](../usage/connect/apple.md#macos) and `/apple` on your headscale for more information) |
|
||||
| iOS | Yes (see [docs](../usage/connect/apple.md#ios) and `/apple` on your headscale for more information) |
|
||||
| tvOS | Yes (see [docs](../usage/connect/apple.md#tvos) and `/apple` on your headscale for more information) |
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
## What is the design goal of headscale?
|
||||
|
||||
Headscale aims to implement a self-hosted, open source alternative to the [Tailscale](https://tailscale.com/)
|
||||
control server.
|
||||
Headscale's goal is to provide self-hosters and hobbyists with an open-source
|
||||
server they can use for their projects and labs.
|
||||
It implements a narrow scope, a _single_ Tailnet, suitable for a personal use, or a small
|
||||
open-source organisation.
|
||||
Headscale aims to implement a self-hosted, open source alternative to the
|
||||
[Tailscale](https://tailscale.com/) control server. Headscale's goal is to
|
||||
provide self-hosters and hobbyists with an open-source server they can use for
|
||||
their projects and labs. It implements a narrow scope, a _single_ Tailscale
|
||||
network (tailnet), suitable for a personal use, or a small open-source
|
||||
organisation.
|
||||
|
||||
## How can I contribute?
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ provides on overview of headscale's feature and compatibility with the Tailscale
|
||||
- [x] [MagicDNS](https://tailscale.com/kb/1081/magicdns)
|
||||
- [x] [Global and restricted nameservers (split DNS)](https://tailscale.com/kb/1054/dns#nameservers)
|
||||
- [x] [search domains](https://tailscale.com/kb/1054/dns#search-domains)
|
||||
- [x] [Extra DNS records (headscale only)](../ref/dns.md#setting-custom-dns-records)
|
||||
- [x] [Extra DNS records (headscale only)](../ref/dns.md#setting-extra-dns-records)
|
||||
- [x] [Taildrop (File Sharing)](https://tailscale.com/kb/1106/taildrop)
|
||||
- [x] Routing advertising (including exit nodes)
|
||||
- [x] Dual stack (IPv4 and IPv6)
|
||||
@@ -25,6 +25,7 @@ provides on overview of headscale's feature and compatibility with the Tailscale
|
||||
- [ ] `autogroup:member`
|
||||
* [ ] Node registration using Single-Sign-On (OpenID Connect) ([GitHub label "OIDC"](https://github.com/juanfont/headscale/labels/OIDC))
|
||||
- [x] Basic registration
|
||||
- [x] Update user profile from identity provider
|
||||
- [ ] Dynamic ACL support
|
||||
- [ ] OIDC groups cannot be used in ACLs
|
||||
- [ ] [Funnel](https://tailscale.com/kb/1223/funnel) ([#1040](https://github.com/juanfont/headscale/issues/1040))
|
||||
|
||||
@@ -14,12 +14,12 @@ Join our [Discord server](https://discord.gg/c84AZQhmpx) for a chat and communit
|
||||
|
||||
## Design goal
|
||||
|
||||
Headscale aims to implement a self-hosted, open source alternative to the Tailscale
|
||||
control server.
|
||||
Headscale's goal is to provide self-hosters and hobbyists with an open-source
|
||||
server they can use for their projects and labs.
|
||||
It implements a narrower scope, a single Tailnet, suitable for a personal use, or a small
|
||||
open-source organisation.
|
||||
Headscale aims to implement a self-hosted, open source alternative to the
|
||||
[Tailscale](https://tailscale.com/) control server. Headscale's goal is to
|
||||
provide self-hosters and hobbyists with an open-source server they can use for
|
||||
their projects and labs. It implements a narrow scope, a _single_ Tailscale
|
||||
network (tailnet), suitable for a personal use, or a small open-source
|
||||
organisation.
|
||||
|
||||
## Supporting headscale
|
||||
|
||||
|
||||
@@ -40,9 +40,6 @@ servers.
|
||||
|
||||
## ACL setup
|
||||
|
||||
Note: Users will be created automatically when users authenticate with the
|
||||
headscale server.
|
||||
|
||||
ACLs have to be written in [huJSON](https://github.com/tailscale/hujson).
|
||||
|
||||
When [registering the servers](../usage/getting-started.md#register-a-node) we
|
||||
@@ -52,11 +49,17 @@ tags to a server they can register, the check of the tags is done on headscale
|
||||
server and only valid tags are applied. A tag is valid if the user that is
|
||||
registering it is allowed to do it.
|
||||
|
||||
To use ACLs in headscale, you must edit your `config.yaml` file. In there you will find a `policy.path` parameter. This will need to point to your ACL file. More info on how these policies are written can be found [here](https://tailscale.com/kb/1018/acls/).
|
||||
To use ACLs in headscale, you must edit your `config.yaml` file. In there you will find a `policy.path` parameter. This
|
||||
will need to point to your ACL file. More info on how these policies are written can be found
|
||||
[here](https://tailscale.com/kb/1018/acls/).
|
||||
|
||||
Please reload or restart Headscale after updating the ACL file. Headscale may be reloaded either via its systemd service
|
||||
(`sudo systemctl reload headscale`) or by sending a SIGHUP signal (`sudo kill -HUP $(pidof headscale)`) to the main
|
||||
process. Headscale logs the result of ACL policy processing after each reload.
|
||||
|
||||
Here are the ACL's to implement the same permissions as above:
|
||||
|
||||
```json
|
||||
```json title="acl.json"
|
||||
{
|
||||
// groups are collections of users having a common scope. A user can be in multiple groups
|
||||
// groups cannot be composed of groups
|
||||
|
||||
@@ -1,44 +1,76 @@
|
||||
# DNS
|
||||
|
||||
Headscale supports [most DNS features](../about/features.md) from Tailscale and DNS releated settings can be configured
|
||||
in the [configuration file](./configuration.md) within the `dns` section.
|
||||
Headscale supports [most DNS features](../about/features.md) from Tailscale. DNS related settings can be configured
|
||||
within `dns` section of the [configuration file](./configuration.md).
|
||||
|
||||
## Setting custom DNS records
|
||||
## Setting extra DNS records
|
||||
|
||||
!!! warning "Community documentation"
|
||||
Headscale allows to set extra DNS records which are made available via
|
||||
[MagicDNS](https://tailscale.com/kb/1081/magicdns). Extra DNS records can be configured either via static entries in the
|
||||
[configuration file](./configuration.md) or from a JSON file that Headscale continuously watches for changes:
|
||||
|
||||
This page is not actively maintained by the headscale authors and is
|
||||
written by community members. It is _not_ verified by headscale developers.
|
||||
* Use the `dns.extra_records` option in the [configuration file](./configuration.md) for entries that are static and
|
||||
don't change while Headscale is running. Those entries are processed when Headscale is starting up and changes to the
|
||||
configuration require a restart of Headscale.
|
||||
* For dynamic DNS records that may be added, updated or removed while Headscale is running or DNS records that are
|
||||
generated by scripts the option `dns.extra_records_path` in the [configuration file](./configuration.md) is useful.
|
||||
Set it to the absolute path of the JSON file containing DNS records and Headscale processes this file as it detects
|
||||
changes.
|
||||
|
||||
**It might be outdated and it might miss necessary steps**.
|
||||
|
||||
Headscale allows to set custom DNS records which are made available via
|
||||
[MagicDNS](https://tailscale.com/kb/1081/magicdns). An example use case is to serve multiple apps on the same host via a
|
||||
reverse proxy like NGINX, in this case a Prometheus monitoring stack. This allows to nicely access the service with
|
||||
"http://grafana.myvpn.example.com" instead of the hostname and port combination
|
||||
"http://hostname-in-magic-dns.myvpn.example.com:3000".
|
||||
An example use case is to serve multiple apps on the same host via a reverse proxy like NGINX, in this case a Prometheus
|
||||
monitoring stack. This allows to nicely access the service with "http://grafana.myvpn.example.com" instead of the
|
||||
hostname and port combination "http://hostname-in-magic-dns.myvpn.example.com:3000".
|
||||
|
||||
!!! warning "Limitations"
|
||||
|
||||
[Not all types of records are supported](https://github.com/tailscale/tailscale/blob/6edf357b96b28ee1be659a70232c0135b2ffedfd/ipn/ipnlocal/local.go#L2989-L3007), especially no CNAME records.
|
||||
Currently, [only A and AAAA records are processed by Tailscale](https://github.com/tailscale/tailscale/blob/v1.78.3/ipn/ipnlocal/local.go#L4461-L4479).
|
||||
|
||||
1. Update the [configuration file](./configuration.md) to contain the desired records like so:
|
||||
|
||||
```yaml
|
||||
dns:
|
||||
...
|
||||
extra_records:
|
||||
- name: "prometheus.myvpn.example.com"
|
||||
type: "A"
|
||||
value: "100.64.0.3"
|
||||
1. Configure extra DNS records using one of the available configuration options:
|
||||
|
||||
- name: "grafana.myvpn.example.com"
|
||||
type: "A"
|
||||
value: "100.64.0.3"
|
||||
...
|
||||
```
|
||||
=== "Static entries, via `dns.extra_records`"
|
||||
|
||||
1. Restart your headscale instance.
|
||||
```yaml title="config.yaml"
|
||||
dns:
|
||||
...
|
||||
extra_records:
|
||||
- name: "grafana.myvpn.example.com"
|
||||
type: "A"
|
||||
value: "100.64.0.3"
|
||||
|
||||
- name: "prometheus.myvpn.example.com"
|
||||
type: "A"
|
||||
value: "100.64.0.3"
|
||||
...
|
||||
```
|
||||
|
||||
Restart your headscale instance.
|
||||
|
||||
=== "Dynamic entries, via `dns.extra_records_path`"
|
||||
|
||||
```json title="extra-records.json"
|
||||
[
|
||||
{
|
||||
"name": "grafana.myvpn.example.com",
|
||||
"type": "A",
|
||||
"value": "100.64.0.3"
|
||||
},
|
||||
{
|
||||
"name": "prometheus.myvpn.example.com",
|
||||
"type": "A",
|
||||
"value": "100.64.0.3"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Headscale picks up changes to the above JSON file automatically.
|
||||
|
||||
!!! tip "Good to know"
|
||||
|
||||
* The `dns.extra_records_path` option in the [configuration file](./configuration.md) needs to reference the
|
||||
JSON file containing extra DNS records.
|
||||
* Be sure to "sort keys" and produce a stable output in case you generate the JSON file with a script.
|
||||
Headscale uses a checksum to detect changes to the file and a stable output avoids unnecessary processing.
|
||||
|
||||
1. Verify that DNS records are properly set using the DNS querying tool of your choice:
|
||||
|
||||
@@ -61,7 +93,7 @@ reverse proxy like NGINX, in this case a Prometheus monitoring stack. This allow
|
||||
The motivating example here was to be able to access internal monitoring services on the same host without
|
||||
specifying a port, depicted as NGINX configuration snippet:
|
||||
|
||||
```
|
||||
```nginx title="nginx.conf"
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
|
||||
@@ -23,7 +23,7 @@ Running headscale behind a cloudflare proxy or cloudflare tunnel is not supporte
|
||||
|
||||
Headscale can be configured not to use TLS, leaving it to the reverse proxy to handle. Add the following configuration values to your headscale config file.
|
||||
|
||||
```yaml
|
||||
```yaml title="config.yaml"
|
||||
server_url: https://<YOUR_SERVER_NAME> # This should be the FQDN at which headscale will be served
|
||||
listen_addr: 0.0.0.0:8080
|
||||
metrics_listen_addr: 0.0.0.0:9090
|
||||
@@ -35,7 +35,7 @@ tls_key_path: ""
|
||||
|
||||
The following example configuration can be used in your nginx setup, substituting values as necessary. `<IP:PORT>` should be the IP address and port where headscale is running. In most cases, this will be `http://localhost:8080`.
|
||||
|
||||
```Nginx
|
||||
```nginx title="nginx.conf"
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
@@ -113,7 +113,7 @@ spec:
|
||||
|
||||
The following Caddyfile is all that is necessary to use Caddy as a reverse proxy for headscale, in combination with the `config.yaml` specifications above to disable headscale's built in TLS. Replace values as necessary - `<YOUR_SERVER_NAME>` should be the FQDN at which headscale will be served, and `<IP:PORT>` should be the IP address and port where headscale is running. In most cases, this will be `localhost:8080`.
|
||||
|
||||
```
|
||||
```none title="Caddyfile"
|
||||
<YOUR_SERVER_NAME> {
|
||||
reverse_proxy <IP:PORT>
|
||||
}
|
||||
@@ -127,7 +127,7 @@ For a slightly more complex configuration which utilizes Docker containers to ma
|
||||
|
||||
The following minimal Apache config will proxy traffic to the headscale instance on `<IP:PORT>`. Note that `upgrade=any` is required as a parameter for `ProxyPass` so that WebSockets traffic whose `Upgrade` header value is not equal to `WebSocket` (i. e. Tailscale Control Protocol) is forwarded correctly. See the [Apache docs](https://httpd.apache.org/docs/2.4/mod/mod_proxy_wstunnel.html) for more information on this.
|
||||
|
||||
```
|
||||
```apache title="apache.conf"
|
||||
<VirtualHost *:443>
|
||||
ServerName <YOUR_SERVER_NAME>
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ Headscale doesn't provide a built-in web interface but users may pick one from t
|
||||
| --------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| headscale-webui | [Github](https://github.com/ifargle/headscale-webui) | A simple headscale web UI for small-scale deployments. |
|
||||
| headscale-ui | [Github](https://github.com/gurucomputing/headscale-ui) | A web frontend for the headscale Tailscale-compatible coordination server |
|
||||
| HeadscaleUi | [GitHub](https://github.com/simcu/headscale-ui) | A static headscale admin ui, no backend enviroment required |
|
||||
| HeadscaleUi | [GitHub](https://github.com/simcu/headscale-ui) | A static headscale admin ui, no backend environment required |
|
||||
| Headplane | [GitHub](https://github.com/tale/headplane) | An advanced Tailscale inspired frontend for headscale |
|
||||
| headscale-admin | [Github](https://github.com/GoodiesHQ/headscale-admin) | Headscale-Admin is meant to be a simple, modern web interface for headscale |
|
||||
| ouroboros | [Github](https://github.com/yellowsink/ouroboros) | Ouroboros is designed for users to manage their own devices, rather than for admins |
|
||||
|
||||
@@ -11,7 +11,7 @@ Known limitations:
|
||||
|
||||
In your `config.yaml`, customize this to your liking:
|
||||
|
||||
```yaml
|
||||
```yaml title="config.yaml"
|
||||
oidc:
|
||||
# Block further startup until the OIDC provider is healthy and available
|
||||
only_start_if_oidc_is_available: true
|
||||
@@ -45,6 +45,18 @@ oidc:
|
||||
allowed_users:
|
||||
- alice@example.com
|
||||
|
||||
# Optional: PKCE (Proof Key for Code Exchange) configuration
|
||||
# PKCE adds an additional layer of security to the OAuth 2.0 authorization code flow
|
||||
# by preventing authorization code interception attacks
|
||||
# See https://datatracker.ietf.org/doc/html/rfc7636
|
||||
pkce:
|
||||
# Enable or disable PKCE support (default: false)
|
||||
enabled: false
|
||||
# PKCE method to use:
|
||||
# - plain: Use plain code verifier
|
||||
# - S256: Use SHA256 hashed code verifier (default, recommended)
|
||||
method: S256
|
||||
|
||||
# If `strip_email_domain` is set to `true`, the domain part of the username email address will be removed.
|
||||
# This will transform `first-name.last-name@example.com` to the user `first-name.last-name`
|
||||
# If `strip_email_domain` is set to `false` the domain part will NOT be removed resulting to the following
|
||||
@@ -56,7 +68,7 @@ oidc:
|
||||
|
||||
In order to integrate headscale with Azure Active Directory, we'll need to provision an App Registration with the correct scopes and redirect URI. Here with Terraform:
|
||||
|
||||
```hcl
|
||||
```hcl title="terraform.hcl"
|
||||
resource "azuread_application" "headscale" {
|
||||
display_name = "Headscale"
|
||||
|
||||
@@ -127,7 +139,7 @@ output "headscale_client_secret" {
|
||||
|
||||
And in your headscale `config.yaml`:
|
||||
|
||||
```yaml
|
||||
```yaml title="config.yaml"
|
||||
oidc:
|
||||
issuer: "https://login.microsoftonline.com/<tenant-UUID>/v2.0"
|
||||
client_id: "<client-id-from-terraform>"
|
||||
@@ -162,7 +174,7 @@ However if you don't have a domain, or need to add users outside of your domain,
|
||||
8. Click `Save` at the bottom of the form
|
||||
9. Take note of the `Client ID` and `Client secret`, you can also download it for reference if you need it.
|
||||
10. Edit your headscale config, under `oidc`, filling in your `client_id` and `client_secret`:
|
||||
```yaml
|
||||
```yaml title="config.yaml"
|
||||
oidc:
|
||||
issuer: "https://accounts.google.com"
|
||||
client_id: ""
|
||||
|
||||
@@ -54,7 +54,7 @@ headscale apikeys expire --prefix "<PREFIX>"
|
||||
|
||||
=== "Minimal YAML configuration file"
|
||||
|
||||
```yaml
|
||||
```yaml title="config.yaml"
|
||||
cli:
|
||||
address: <HEADSCALE_ADDRESS>:<PORT>
|
||||
api_key: <API_KEY_FROM_PREVIOUS_STEP>
|
||||
@@ -69,7 +69,7 @@ headscale apikeys expire --prefix "<PREFIX>"
|
||||
|
||||
!!! bug
|
||||
|
||||
Headscale 0.23.0 requires at least an empty configuration file when environment variables are used to
|
||||
Headscale currently requires at least an empty configuration file when environment variables are used to
|
||||
specify connection details. See [issue 2193](https://github.com/juanfont/headscale/issues/2193) for more
|
||||
information.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
Headscale can be configured to expose its web service via TLS. To configure the certificate and key file manually, set the `tls_cert_path` and `tls_cert_path` configuration parameters. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from.
|
||||
|
||||
```yaml
|
||||
```yaml title="config.yaml"
|
||||
tls_cert_path: ""
|
||||
tls_key_path: ""
|
||||
```
|
||||
@@ -15,7 +15,7 @@ The certificate should contain the full chain, else some clients, like the Tails
|
||||
|
||||
To get a certificate automatically via [Let's Encrypt](https://letsencrypt.org/), set `tls_letsencrypt_hostname` to the desired certificate hostname. This name must resolve to the IP address(es) headscale is reachable on (i.e., it must correspond to the `server_url` configuration parameter). The certificate and Let's Encrypt account credentials will be stored in the directory configured in `tls_letsencrypt_cache_dir`. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from.
|
||||
|
||||
```yaml
|
||||
```yaml title="config.yaml"
|
||||
tls_letsencrypt_hostname: ""
|
||||
tls_letsencrypt_listen: ":http"
|
||||
tls_letsencrypt_cache_dir: ".cache"
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# Running headscale in a cloud
|
||||
|
||||
!!! warning "Community documentation"
|
||||
|
||||
This page is not actively maintained by the headscale authors and is
|
||||
written by community members. It is _not_ verified by headscale developers.
|
||||
|
||||
**It might be outdated and it might miss necessary steps**.
|
||||
|
||||
## Sealos
|
||||
|
||||
[Deploy headscale as service on Sealos.](https://icloudnative.io/en/posts/how-to-set-up-or-migrate-headscale/)
|
||||
|
||||
1. Click the following prebuilt template:
|
||||
|
||||
[](https://cloud.sealos.io/?openapp=system-template%3FtemplateName%3Dheadscale)
|
||||
|
||||
2. Click "Deploy Application" on the template page to start deployment. Upon completion, two applications appear: headscale, and one of its [web interfaces](../../ref/integration/web-ui.md).
|
||||
3. Once deployment concludes, click 'Details' on the headscale application page to navigate to the application's details.
|
||||
4. Wait for the application's status to switch to running. For accessing the headscale server, the Public Address associated with port 8080 is the address of the headscale server. To access the headscale console, simply append `/admin/` to the headscale public URL.
|
||||
|
||||
!!! tip "Remote CLI"
|
||||
|
||||
Headscale can be managed remotely via its remote CLI support. See our [Controlling headscale with remote
|
||||
CLI](../../ref/remote-cli.md) documentation for details.
|
||||
@@ -4,7 +4,7 @@ Several Linux distributions and community members provide packages for headscale
|
||||
the [official releases](./official.md) provided by the headscale maintainers. Such packages offer improved integration
|
||||
for their targeted operating system and usually:
|
||||
|
||||
- setup a dedicated user account to run headscale
|
||||
- setup a dedicated local user account to run headscale
|
||||
- provide a default configuration
|
||||
- install headscale as system service
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ not work with alternatives like [Podman](https://podman.io). The Docker image ca
|
||||
curl http://127.0.0.1:9090/metrics
|
||||
```
|
||||
|
||||
1. Create a user ([tailnet](https://tailscale.com/kb/1136/tailnet/)):
|
||||
1. Create a headscale user:
|
||||
|
||||
```shell
|
||||
docker exec -it headscale \
|
||||
|
||||
@@ -6,8 +6,8 @@ Both are available on the [GitHub releases page](https://github.com/juanfont/hea
|
||||
## Using packages for Debian/Ubuntu (recommended)
|
||||
|
||||
It is recommended to use our DEB packages to install headscale on a Debian based system as those packages configure a
|
||||
user to run headscale, provide a default configuration and ship with a systemd service file. Supported distributions are
|
||||
Ubuntu 20.04 or newer, Debian 11 or newer.
|
||||
local user to run headscale, provide a default configuration and ship with a systemd service file. Supported
|
||||
distributions are Ubuntu 20.04 or newer, Debian 11 or newer.
|
||||
|
||||
1. Download the [latest headscale package](https://github.com/juanfont/headscale/releases/latest) for your platform (`.deb` for Ubuntu and Debian).
|
||||
|
||||
@@ -46,13 +46,13 @@ Ubuntu 20.04 or newer, Debian 11 or newer.
|
||||
|
||||
!!! warning "Advanced"
|
||||
|
||||
This installation method is considered advanced as one needs to take care of the headscale user and the systemd
|
||||
This installation method is considered advanced as one needs to take care of the local user and the systemd
|
||||
service themselves. If possible, use the [DEB packages](#using-packages-for-debianubuntu-recommended) or a
|
||||
[community package](./community.md) instead.
|
||||
|
||||
This section describes the installation of headscale according to the [Requirements and
|
||||
assumptions](../requirements.md#assumptions). Headscale is run by a dedicated user and the service itself is managed by
|
||||
systemd.
|
||||
assumptions](../requirements.md#assumptions). Headscale is run by a dedicated local user and the service itself is
|
||||
managed by systemd.
|
||||
|
||||
1. Download the latest [`headscale` binary from GitHub's release page](https://github.com/juanfont/headscale/releases):
|
||||
|
||||
@@ -67,7 +67,7 @@ systemd.
|
||||
sudo chmod +x /usr/local/bin/headscale
|
||||
```
|
||||
|
||||
1. Add a dedicated user to run headscale:
|
||||
1. Add a dedicated local user to run headscale:
|
||||
|
||||
```shell
|
||||
sudo useradd \
|
||||
@@ -94,7 +94,7 @@ systemd.
|
||||
1. In `/etc/headscale/config.yaml`, override the default `headscale` unix socket with a path that is writable by the
|
||||
`headscale` user or group:
|
||||
|
||||
```yaml
|
||||
```yaml title="config.yaml"
|
||||
unix_socket: /var/run/headscale/headscale.sock
|
||||
```
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ README](https://github.com/juanfont/headscale#contributing) for more information
|
||||
### Install from source
|
||||
|
||||
```shell
|
||||
# Install prerequistes
|
||||
# Install prerequisites
|
||||
pkg_add go
|
||||
|
||||
git clone https://github.com/juanfont/headscale.git
|
||||
@@ -42,7 +42,7 @@ cp headscale /usr/local/sbin
|
||||
### Install from source via cross compile
|
||||
|
||||
```shell
|
||||
# Install prerequistes
|
||||
# Install prerequisites
|
||||
# 1. go v1.20+: headscale newer than 0.21 needs go 1.20+ to compile
|
||||
# 2. gmake: Makefile in the headscale repo is written in GNU make syntax
|
||||
|
||||
|
||||
@@ -6,14 +6,14 @@ Headscale should just work as long as the following requirements are met:
|
||||
recommended.
|
||||
- Headscale is served via HTTPS on port 443[^1].
|
||||
- A reasonably modern Linux or BSD based operating system.
|
||||
- A dedicated user account to run headscale.
|
||||
- A dedicated local user account to run headscale.
|
||||
- A little bit of command line knowledge to configure and operate headscale.
|
||||
|
||||
## Assumptions
|
||||
|
||||
The headscale documentation and the provided examples are written with a few assumptions in mind:
|
||||
|
||||
- Headscale is running as system service via a dedicated user `headscale`.
|
||||
- Headscale is running as system service via a dedicated local user `headscale`.
|
||||
- The [configuration](../ref/configuration.md) is loaded from `/etc/headscale/config.yaml`.
|
||||
- SQLite is used as database.
|
||||
- The data directory for headscale (used for private keys, ACLs, SQLite database, …) is located in `/var/lib/headscale`.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Upgrade an existing installation
|
||||
|
||||
An existing headscale installation can be updated to a new version:
|
||||
Update an existing headscale installation to a new version:
|
||||
|
||||
- Read the announcement on the [GitHub releases](https://github.com/juanfont/headscale/releases) page for the new
|
||||
version. It lists the changes of the release along with possible breaking changes.
|
||||
|
||||
@@ -15,14 +15,10 @@ Install the official Tailscale iOS client from the [App Store](https://apps.appl
|
||||
|
||||
### Configuring the headscale URL
|
||||
|
||||
- Open Tailscale and make sure you are _not_ logged in to any account
|
||||
- Open Settings on the iOS device
|
||||
- Scroll down to the `third party apps` section, under `Game Center` or `TV Provider`
|
||||
- Find Tailscale and select it
|
||||
- If the iOS device was previously logged into Tailscale, switch the `Reset Keychain` toggle to `on`
|
||||
- Enter the URL of your headscale instance (e.g `https://headscale.example.com`) under `Alternate Coordination Server URL`
|
||||
- Restart the app by closing it from the iOS app switcher, open the app and select the regular sign in option
|
||||
_(non-SSO)_. It should open up to the headscale authentication page.
|
||||
- Open the Tailscale app
|
||||
- Click the account icon in the top-right corner and select `Log in…`.
|
||||
- Tap the top-right options menu button and select `Use custom coordination server`.
|
||||
- Enter your instance url (e.g `https://headscale.example.com`)
|
||||
- Enter your credentials and log in. Headscale should now be working on your iOS device.
|
||||
|
||||
## macOS
|
||||
@@ -43,7 +39,7 @@ tailscale login --login-server <YOUR_HEADSCALE_URL>
|
||||
|
||||
#### GUI
|
||||
|
||||
- ALT + Click the Tailscale icon in the menu and hover over the Debug menu
|
||||
- Option + Click the Tailscale icon in the menu and hover over the Debug menu
|
||||
- Under `Custom Login Server`, select `Add Account...`
|
||||
- Enter the URL of your headscale instance (e.g `https://headscale.example.com`) and press `Add Account`
|
||||
- Follow the login procedure in the browser
|
||||
|
||||
@@ -41,13 +41,14 @@ options, run:
|
||||
headscale <COMMAND> --help
|
||||
```
|
||||
|
||||
## Manage users
|
||||
## Manage headscale users
|
||||
|
||||
In headscale, a node (also known as machine or device) is always assigned to a specific user, a
|
||||
[tailnet](https://tailscale.com/kb/1136/tailnet/). Such users can be managed with the `headscale users` command. Invoke
|
||||
the built-in help for more information: `headscale users --help`.
|
||||
In headscale, a node (also known as machine or device) is always assigned to a
|
||||
headscale user. Such a headscale user may have many nodes assigned to them and
|
||||
can be managed with the `headscale users` command. Invoke the built-in help for
|
||||
more information: `headscale users --help`.
|
||||
|
||||
### Create a user
|
||||
### Create a headscale user
|
||||
|
||||
=== "Native"
|
||||
|
||||
@@ -62,7 +63,7 @@ the built-in help for more information: `headscale users --help`.
|
||||
headscale users create <USER>
|
||||
```
|
||||
|
||||
### List existing users
|
||||
### List existing headscale users
|
||||
|
||||
=== "Native"
|
||||
|
||||
|
||||
6
flake.lock
generated
6
flake.lock
generated
@@ -20,11 +20,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1733376361,
|
||||
"narHash": "sha256-aLJxoTDDSqB+/3orsulE6/qdlX6MzDLIITLZqdgMpqo=",
|
||||
"lastModified": 1738297584,
|
||||
"narHash": "sha256-AYvaFBzt8dU0fcSK2jKD0Vg23K2eIRxfsVXIPCW9a0E=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "929116e316068c7318c54eb4d827f7d9756d5e9c",
|
||||
"rev": "9189ac18287c599860e878e905da550aa6dec1cd",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
checkFlags = ["-short"];
|
||||
|
||||
# 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.
|
||||
vendorHash = "sha256-NyXMSIVcmPlUhE3LmEsYZQxJdz+e435r+GZC8umQKqQ=";
|
||||
# update this if you have a mismatch after doing a change to those files.
|
||||
vendorHash = "sha256-ZQj2A0GdLhHc7JLW7qgpGBveXXNWg9ueSG47OZQQXEw=";
|
||||
|
||||
subPackages = ["cmd/headscale"];
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.35.1
|
||||
// protoc-gen-go v1.35.2
|
||||
// protoc (unknown)
|
||||
// source: headscale/v1/apikey.proto
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.35.1
|
||||
// protoc-gen-go v1.35.2
|
||||
// protoc (unknown)
|
||||
// source: headscale/v1/device.proto
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.35.1
|
||||
// protoc-gen-go v1.35.2
|
||||
// protoc (unknown)
|
||||
// source: headscale/v1/headscale.proto
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.35.1
|
||||
// protoc-gen-go v1.35.2
|
||||
// protoc (unknown)
|
||||
// source: headscale/v1/node.proto
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.35.1
|
||||
// protoc-gen-go v1.35.2
|
||||
// protoc (unknown)
|
||||
// source: headscale/v1/policy.proto
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.35.1
|
||||
// protoc-gen-go v1.35.2
|
||||
// protoc (unknown)
|
||||
// source: headscale/v1/preauthkey.proto
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.35.1
|
||||
// protoc-gen-go v1.35.2
|
||||
// protoc (unknown)
|
||||
// source: headscale/v1/routes.proto
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.35.1
|
||||
// protoc-gen-go v1.35.2
|
||||
// protoc (unknown)
|
||||
// source: headscale/v1/user.proto
|
||||
|
||||
@@ -127,7 +127,10 @@ type CreateUserRequest struct {
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
DisplayName string `protobuf:"bytes,2,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"`
|
||||
Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"`
|
||||
PictureUrl string `protobuf:"bytes,4,opt,name=picture_url,json=pictureUrl,proto3" json:"picture_url,omitempty"`
|
||||
}
|
||||
|
||||
func (x *CreateUserRequest) Reset() {
|
||||
@@ -167,6 +170,27 @@ func (x *CreateUserRequest) GetName() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *CreateUserRequest) GetDisplayName() string {
|
||||
if x != nil {
|
||||
return x.DisplayName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *CreateUserRequest) GetEmail() string {
|
||||
if x != nil {
|
||||
return x.Email
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *CreateUserRequest) GetPictureUrl() string {
|
||||
if x != nil {
|
||||
return x.PictureUrl
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type CreateUserResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
@@ -520,38 +544,43 @@ var file_headscale_v1_user_proto_rawDesc = []byte{
|
||||
0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72,
|
||||
0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x0f, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c,
|
||||
0x65, 0x5f, 0x70, 0x69, 0x63, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x0d, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x69, 0x63, 0x55, 0x72, 0x6c, 0x22, 0x27,
|
||||
0x0a, 0x11, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75,
|
||||
0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3c, 0x0a, 0x12, 0x43, 0x72, 0x65, 0x61, 0x74,
|
||||
0x0d, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x69, 0x63, 0x55, 0x72, 0x6c, 0x22, 0x81,
|
||||
0x01, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71,
|
||||
0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70,
|
||||
0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b,
|
||||
0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65,
|
||||
0x6d, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69,
|
||||
0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x75, 0x72, 0x6c,
|
||||
0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x55,
|
||||
0x72, 0x6c, 0x22, 0x3c, 0x0a, 0x12, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72,
|
||||
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72,
|
||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61,
|
||||
0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72,
|
||||
0x22, 0x45, 0x0a, 0x11, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65,
|
||||
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x6f, 0x6c, 0x64, 0x5f, 0x69, 0x64, 0x18,
|
||||
0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x6f, 0x6c, 0x64, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08,
|
||||
0x6e, 0x65, 0x77, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
|
||||
0x6e, 0x65, 0x77, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x3c, 0x0a, 0x12, 0x52, 0x65, 0x6e, 0x61, 0x6d,
|
||||
0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a,
|
||||
0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x68, 0x65,
|
||||
0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52,
|
||||
0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0x45, 0x0a, 0x11, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x55,
|
||||
0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x6f, 0x6c,
|
||||
0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x6f, 0x6c, 0x64, 0x49,
|
||||
0x64, 0x12, 0x19, 0x0a, 0x08, 0x6e, 0x65, 0x77, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x07, 0x6e, 0x65, 0x77, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x3c, 0x0a, 0x12,
|
||||
0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
||||
0x73, 0x65, 0x12, 0x26, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
|
||||
0x32, 0x12, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e,
|
||||
0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0x23, 0x0a, 0x11, 0x44, 0x65,
|
||||
0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
|
||||
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x69, 0x64, 0x22,
|
||||
0x14, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73,
|
||||
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x4c, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65,
|
||||
0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18,
|
||||
0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d,
|
||||
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a,
|
||||
0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d,
|
||||
0x61, 0x69, 0x6c, 0x22, 0x3d, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73,
|
||||
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72,
|
||||
0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63,
|
||||
0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x75, 0x73, 0x65,
|
||||
0x72, 0x73, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
|
||||
0x2f, 0x6a, 0x75, 0x61, 0x6e, 0x66, 0x6f, 0x6e, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63,
|
||||
0x61, 0x6c, 0x65, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0x23, 0x0a, 0x11, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55,
|
||||
0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64,
|
||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x69, 0x64, 0x22, 0x14, 0x0a, 0x12, 0x44, 0x65,
|
||||
0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
|
||||
0x22, 0x4c, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71,
|
||||
0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04,
|
||||
0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69,
|
||||
0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x22, 0x3d,
|
||||
0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
||||
0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03,
|
||||
0x28, 0x0b, 0x32, 0x12, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76,
|
||||
0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x42, 0x29, 0x5a,
|
||||
0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x75, 0x61, 0x6e,
|
||||
0x66, 0x6f, 0x6e, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x67,
|
||||
0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -1039,6 +1039,15 @@
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"pictureUrl": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
130
go.mod
130
go.mod
@@ -4,50 +4,52 @@ go 1.23.1
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
github.com/chasefleming/elem-go v0.29.0
|
||||
github.com/cenkalti/backoff/v4 v4.3.0
|
||||
github.com/chasefleming/elem-go v0.30.0
|
||||
github.com/coder/websocket v1.8.12
|
||||
github.com/coreos/go-oidc/v3 v3.11.0
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
|
||||
github.com/fsnotify/fsnotify v1.8.0
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.2
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.3
|
||||
github.com/gofrs/uuid/v5 v5.3.0
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0
|
||||
github.com/jagottsicher/termcolor v1.0.2
|
||||
github.com/klauspost/compress v1.17.9
|
||||
github.com/klauspost/compress v1.17.11
|
||||
github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25
|
||||
github.com/ory/dockertest/v3 v3.11.0
|
||||
github.com/philip-bui/grpc-zerolog v1.0.1
|
||||
github.com/pkg/profile v1.7.0
|
||||
github.com/prometheus/client_golang v1.20.2
|
||||
github.com/prometheus/common v0.58.0
|
||||
github.com/pterm/pterm v0.12.79
|
||||
github.com/prometheus/client_golang v1.20.5
|
||||
github.com/prometheus/common v0.61.0
|
||||
github.com/pterm/pterm v0.12.80
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0
|
||||
github.com/rs/zerolog v1.33.0
|
||||
github.com/samber/lo v1.47.0
|
||||
github.com/sasha-s/go-deadlock v0.3.5
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spf13/viper v1.20.0-alpha.6
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a
|
||||
github.com/tailscale/tailsql v0.0.0-20240418235827-820559f382c1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/tailscale/hujson v0.0.0-20241010212012-29efb4a0184b
|
||||
github.com/tailscale/tailsql v0.0.0-20241211062219-bf96884c6a49
|
||||
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||
golang.org/x/crypto v0.26.0
|
||||
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948
|
||||
golang.org/x/net v0.28.0
|
||||
golang.org/x/oauth2 v0.22.0
|
||||
golang.org/x/sync v0.8.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1
|
||||
google.golang.org/grpc v1.66.0
|
||||
google.golang.org/protobuf v1.35.1
|
||||
golang.org/x/crypto v0.32.0
|
||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8
|
||||
golang.org/x/net v0.34.0
|
||||
golang.org/x/oauth2 v0.25.0
|
||||
golang.org/x/sync v0.10.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241216192217-9240e9c98484
|
||||
google.golang.org/grpc v1.69.0
|
||||
google.golang.org/protobuf v1.36.0
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/driver/postgres v1.5.9
|
||||
gorm.io/gorm v1.25.11
|
||||
tailscale.com v1.75.0-pre.0.20240926101731-7d1160ddaab7
|
||||
gorm.io/driver/postgres v1.5.11
|
||||
gorm.io/gorm v1.25.12
|
||||
tailscale.com v1.80.0
|
||||
zgo.at/zcache/v2 v2.1.0
|
||||
zombiezen.com/go/postgrestest v1.0.1
|
||||
)
|
||||
@@ -73,7 +75,7 @@ require (
|
||||
modernc.org/libc v1.55.3 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.8.0 // indirect
|
||||
modernc.org/sqlite v1.33.1 // indirect
|
||||
modernc.org/sqlite v1.34.5 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -87,43 +89,41 @@ require (
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
||||
github.com/akutz/memconn v0.1.0 // indirect
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.24.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.26.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.16.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.26.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.45.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect
|
||||
github.com/aws/smithy-go v1.19.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.20.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.28.6 // indirect
|
||||
github.com/aws/smithy-go v1.20.2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.13.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/containerd/console v1.0.4 // indirect
|
||||
github.com/containerd/continuity v0.4.5 // indirect
|
||||
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect
|
||||
github.com/creachadair/mds v0.14.5 // indirect
|
||||
github.com/creachadair/mds v0.20.0 // indirect
|
||||
github.com/dblohm7/wingoes v0.0.0-20240123200102-b75a8a7d7eb0 // indirect
|
||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e // indirect
|
||||
github.com/docker/cli v27.3.1+incompatible // indirect
|
||||
github.com/docker/docker v27.3.1+incompatible // indirect
|
||||
github.com/docker/cli v27.4.1+incompatible // indirect
|
||||
github.com/docker/docker v27.4.1+incompatible // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/felixge/fgprof v0.9.5 // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.6.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/gaissmai/bart v0.11.1 // indirect
|
||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
|
||||
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 // indirect
|
||||
github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect
|
||||
@@ -135,11 +135,11 @@ require (
|
||||
github.com/google/go-github v17.0.0+incompatible // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect
|
||||
github.com/google/pprof v0.0.0-20240829160300-da1f7e9f2b25 // indirect
|
||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gookit/color v1.5.4 // indirect
|
||||
github.com/gorilla/csrf v1.7.2 // indirect
|
||||
github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/hashicorp/go-version v1.7.0 // indirect
|
||||
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
|
||||
@@ -148,12 +148,11 @@ require (
|
||||
github.com/insomniacslk/dhcp v0.0.0-20240129002554-15c9b8791914 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/jackc/pgx/v5 v5.7.1 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 // indirect
|
||||
github.com/jsimonetti/rtnetlink v1.4.1 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect
|
||||
@@ -165,7 +164,7 @@ require (
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mdlayher/genetlink v1.3.2 // indirect
|
||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect
|
||||
github.com/mdlayher/sdnotify v1.0.0 // indirect
|
||||
github.com/mdlayher/socket v0.5.0 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
@@ -178,9 +177,9 @@ require (
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/opencontainers/runc v1.2.2 // indirect
|
||||
github.com/opencontainers/runc v1.2.3 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20241211131331-93ee7e083c43 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
@@ -189,7 +188,7 @@ require (
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||
github.com/safchain/ethtool v0.3.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.6.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
@@ -203,13 +202,12 @@ require (
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 // indirect
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect
|
||||
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 // indirect
|
||||
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 // indirect
|
||||
github.com/tailscale/setec v0.0.0-20240314234648-9da8e7407257 // indirect
|
||||
github.com/tailscale/squibble v0.0.0-20240418235321-9ee0eeb78185 // indirect
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 // indirect
|
||||
github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc // indirect
|
||||
github.com/tcnksm/go-httpstat v0.2.0 // indirect
|
||||
github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e // indirect
|
||||
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect
|
||||
github.com/tailscale/setec v0.0.0-20240930150730-e6eb93658ed3 // indirect
|
||||
github.com/tailscale/squibble v0.0.0-20240909231413-32a80b9743f7 // indirect
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect
|
||||
github.com/tailscale/wireguard-go v0.0.0-20250107165329-0b8b35511f19 // indirect
|
||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
|
||||
github.com/vishvananda/netns v0.0.4 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
@@ -217,16 +215,16 @@ require (
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect
|
||||
golang.org/x/mod v0.20.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/term v0.23.0 // indirect
|
||||
golang.org/x/text v0.17.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.24.0 // indirect
|
||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
|
||||
golang.org/x/mod v0.22.0 // indirect
|
||||
golang.org/x/sys v0.29.1-0.20250107080300-1c14dcadc3ab // indirect
|
||||
golang.org/x/term v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/time v0.9.0 // indirect
|
||||
golang.org/x/tools v0.29.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 // indirect
|
||||
)
|
||||
|
||||
309
go.sum
309
go.sum
@@ -42,44 +42,44 @@ github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1L
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
|
||||
github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 h1:OCs21ST2LrepDfD3lwlQiOqIGp6JiEUqG84GzTDoyJs=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4/go.mod h1:usURWEKSNNAcAZuzRn/9ZYPT8aZQkR7xcCtunK/LkJo=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.26.6 h1:Z/7w9bUqlRI0FFQpetVuFYEsjzE3h7fpU6HuGmfPL/o=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.26.6/go.mod h1:uKU6cnDmYCvJ+pxO9S4cWDb2yWWIH5hra+32hVh1MI4=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.16.16 h1:8q6Rliyv0aUFAVtzaldUEcS+T5gbadPbWdV1WcAddK8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.16.16/go.mod h1:UHVZrdUsv63hPXFo1H7c5fEneoVo9UXiz36QG1GEPi0=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10/go.mod h1:6UV4SZkVvmODfXKql4LCbaZUpF7HO2BX38FgBf9ZOLw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3 h1:n3GDfwqF2tzEkXlv5cuy4iy7LpKDtqDMcNLfZDu9rls=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9 h1:ugD6qzjYtB7zM5PN/ZIeaAIyefPaD82G8+SJopgvUpw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9/go.mod h1:YD0aYBWCrPENpHolhKw2XDlTIWae2GKXT1T4o6N6hiM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9 h1:/90OR2XbSYfXucBMJ4U14wrjlfleq/0SB6dZDPncgmo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9/go.mod h1:dN/Of9/fNZet7UrQQ6kTDo/VSwKPIq94vjlU16bRARc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 h1:DBYTXwIGQSGs9w4jKm60F5dmCQ3EEruxdc0MFh+3EY4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10/go.mod h1:wohMUQiFdzo0NtxbBg0mSRGZ4vL3n0dKjLTINdcIino=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 h1:iEAeF6YC3l4FzlJPP9H3Ko1TXpdjdqWffxXjp8SY6uk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9/go.mod h1:kjsXoK23q9Z/tLBrckZLLyvjhZoS+AGrzqzUfEClvMM=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.47.7 h1:o0ASbVwUAIrfp/WcCac+6jioZt4Hd8k/1X8u7GJ/QeM=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.47.7/go.mod h1:vADO6Jn+Rq4nDtfwNjhgR84qkZwiC6FqCaXdw/kYwjA=
|
||||
github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA=
|
||||
github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 h1:x6xsQXGSmW6frevwDA+vi/wqhp1ct18mVXYN08/93to=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2/go.mod h1:lPprDr1e6cJdyYeGXnRaJoP4Md+cDBvi2eOj00BlGmg=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.11 h1:f47rANd2LQEYHda2ddSCKYId18/8BhSRM4BULGmfgNA=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.11/go.mod h1:SMsV78RIOYdve1vf36z8LmnszlRWkwMQtomCAI0/mIE=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.11 h1:YuIB1dJNf1Re822rriUOTxopaHHvIq0l/pX3fwO+Tzs=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.11/go.mod h1:AQtFPsDH9bI2O+71anW6EKL+NcD7LG3dpKGMV4SShgo=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 h1:FVJ0r5XTHSmIHJV6KuDmdYhEpvlHpiSd38RQWhut5J4=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1/go.mod h1:zusuAeqezXzAB24LGuzuekqMAEgWkVYukBec3kr3jUg=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 h1:aw39xVGeRWlWx9EzGVnhOR4yOjQDHPQ6o6NmBlscyQg=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5/go.mod h1:FSaRudD0dXiMPK2UjknVwwTYyZMRsHv3TtkabsZih5I=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 h1:PG1F3OD1szkuQPzDw3CIQsRIrtTlUC3lP84taWzHlq0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5/go.mod h1:jU1li6RFryMz+so64PpKtudI+QzbKoIEivqdf6LNpOc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.5 h1:81KE7vaZzrl7yHBYHVEzYB8sypz11NMOZ40YlWvPxsU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.5/go.mod h1:LIt2rg7Mcgn09Ygbdh/RdIm0rQ+3BNkbP1gyVMFtRK0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.7 h1:ZMeFZ5yk+Ek+jNr1+uwCd2tG89t6oTS5yVWpa6yy2es=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.7/go.mod h1:mxV05U+4JiHqIpGqqYXOHLPKUC6bDXC44bsUhNjOEwY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 h1:ogRAwT1/gxJBcSWDMZlgyFUM962F51A5CRhDLbxLdmo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7/go.mod h1:YCsIZhXfRPLFFCl5xxY+1T9RKzOKjCut+28JSX2DnAk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5 h1:f9RyWNtS8oH7cZlbn+/JNPpjUk5+5fLd5lM9M0i49Ys=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5/go.mod h1:h5CoMZV2VF297/VLhRhO1WF+XYWOzXo+4HsObA4HjBQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.53.1 h1:6cnno47Me9bRykw9AEv9zkXE+5or7jz8TsskTTccbgc=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.53.1/go.mod h1:qmdkIIAC+GCLASF7R2whgNrJADz0QZPX+Seiw/i4S3o=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.45.0 h1:IOdss+igJDFdic9w3WKwxGCmHqUxydvIhJOm9LJ32Dk=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.45.0/go.mod h1:Q7XIWsMo0JcMpI/6TGD6XXcXcV1DbTj6e9BKNntIMIM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 h1:eajuO3nykDPdYicLlP3AGgOyVN3MOlFmZv7WGTuJPow=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7/go.mod h1:+mJNDdF+qiUlNKNC3fxn74WWNN+sOiGOEImje+3ScPM=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 h1:QPMJf+Jw8E1l7zqhZmMlFw6w1NmfkfiSK8mS4zOx3BA=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U=
|
||||
github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM=
|
||||
github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.20.5 h1:vN8hEbpRnL7+Hopy9dzmRle1xmDc7o8tmY0klsr175w=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.20.5/go.mod h1:qGzynb/msuZIE8I75DVRCUXw3o3ZyBmUvMwQ2t/BrGM=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4 h1:Jux+gDDyi1Lruk+KHF91tK2KCuY61kzoCpvtvJJBtOE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4/go.mod h1:mUYPBhaF2lGiukDEjJX2BLRRKTmoUSitGDUgM4tRxak=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.28.6 h1:cwIxeBttqPN3qkaAjcEcsh8NYr8n2HZPkcKgPAi1phU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.28.6/go.mod h1:FZf1/nKNEkHdGGJP/cI2MoIMquumuRK6ol3QQJNDxmw=
|
||||
github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
|
||||
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
@@ -90,8 +90,8 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chasefleming/elem-go v0.29.0 h1:WwrjQcVn6xldhexluvl2Z3sgKi9HTMuzWeEXO4PHsmg=
|
||||
github.com/chasefleming/elem-go v0.29.0/go.mod h1:hz73qILBIKnTgOujnSMtEj20/epI+f6vg71RUilJAA4=
|
||||
github.com/chasefleming/elem-go v0.30.0 h1:BlhV1ekv1RbFiM8XZUQeln1Ikb4D+bu2eDO4agREvok=
|
||||
github.com/chasefleming/elem-go v0.30.0/go.mod h1:hz73qILBIKnTgOujnSMtEj20/epI+f6vg71RUilJAA4=
|
||||
github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||
github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=
|
||||
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
|
||||
@@ -118,8 +118,8 @@ github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/
|
||||
github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creachadair/mds v0.14.5 h1:2amuO4yCbQkaAyDoLO5iCbwbTRQZz4EpRhOejQbf4+8=
|
||||
github.com/creachadair/mds v0.14.5/go.mod h1:4vrFYUzTXMJpMBU+OA292I6IUxKWCCfZkgXg+/kBZMo=
|
||||
github.com/creachadair/mds v0.20.0 h1:bXQO154c2TDgCY+rRmdIfUqjeqGYejmZ/QayeTNwbp8=
|
||||
github.com/creachadair/mds v0.20.0/go.mod h1:4b//mUiL8YldH6TImXjmW45myzTLNS1LLjOmrk888eg=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0=
|
||||
@@ -134,10 +134,10 @@ github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yez
|
||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
|
||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/docker/cli v27.3.1+incompatible h1:qEGdFBF3Xu6SCvCYhc7CzaQTlBmqDuzxPDpigSyeKQQ=
|
||||
github.com/docker/cli v27.3.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
|
||||
github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/cli v27.4.1+incompatible h1:VzPiUlRJ/xh+otB75gva3r05isHMo5wXDfPRi5/b4hI=
|
||||
github.com/docker/cli v27.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker v27.4.1+incompatible h1:ZJvcY7gfwHn1JF48PfbyXg7Jyt9ZCWDW+GGXOIxEwp4=
|
||||
github.com/docker/docker v27.4.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
@@ -155,12 +155,10 @@ github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY=
|
||||
github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA=
|
||||
github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/gaissmai/bart v0.11.1 h1:5Uv5XwsaFBRo4E5VBcb9TzY8B7zxFf+U7isDxqOrRfc=
|
||||
github.com/gaissmai/bart v0.11.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg=
|
||||
github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
|
||||
@@ -169,16 +167,20 @@ github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec
|
||||
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.2 h1:F/d1hpHbRAvKezziV2CC5KUE82cVe9zTgHSBoOOZ4CY=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.2/go.mod h1:9nHVX6z3FCMCQPA7PThGcA55t22yKQfK/Dnsf5i7hUo=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.3 h1:ei3Vq/rpPI/jCJY9mRHJAKg5vU+EhZyWhBAkaAomQuw=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.3/go.mod h1:VJ9FIOBAur+NmQ8c4tDVwOuiJcgupTG105FexPFrXzA=
|
||||
github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
|
||||
github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||
github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
|
||||
github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
|
||||
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 h1:ymLjT4f35nQbASLnvxEde4XOBL+Sn7rFuV+FOJqkljg=
|
||||
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0/go.mod h1:6daplAwHHGbUGib4990V3Il26O0OC4aRyvewaaAihaA=
|
||||
github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288 h1:KbX3Z3CgiYlbaavUq3Cj9/MjpO+88S7/AGXzynVDv84=
|
||||
github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
@@ -224,8 +226,8 @@ github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdF
|
||||
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4=
|
||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
|
||||
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
|
||||
github.com/google/pprof v0.0.0-20240829160300-da1f7e9f2b25 h1:sEDPKUw6iPjczdu33njxFjO6tYa9bfc0z/QyB/zSsBw=
|
||||
github.com/google/pprof v0.0.0-20240829160300-da1f7e9f2b25/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
|
||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
@@ -234,20 +236,18 @@ github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQ
|
||||
github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo=
|
||||
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
|
||||
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
|
||||
github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI=
|
||||
github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
|
||||
github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30 h1:fiJdrgVBkjZ5B1HJ2WQwNOaXB+QyYcNXTA3t1XYLz0M=
|
||||
github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI=
|
||||
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
|
||||
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
|
||||
github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
|
||||
@@ -264,10 +264,10 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
||||
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs=
|
||||
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jagottsicher/termcolor v1.0.2 h1:fo0c51pQSuLBN1+yVX2ZE+hE+P7ULb/TY8eRowJnrsM=
|
||||
github.com/jagottsicher/termcolor v1.0.2/go.mod h1:RcH8uFwF/0wbEdQmi83rjmlJ+QOKdMSE9Rc1BEB7zFo=
|
||||
github.com/jellydator/ttlcache/v3 v3.1.0 h1:0gPFG0IHHP6xyUyXq+JaD8fwkDCqgqwohXNJBcYE71g=
|
||||
@@ -281,17 +281,14 @@ github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHW
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0WwEDBeqxKwb7WB62QX8bvZ/FJnVXIfk=
|
||||
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8=
|
||||
github.com/jsimonetti/rtnetlink v1.4.1 h1:JfD4jthWBqZMEffc5RjgmlzpYttAVw1sdnmiNaPO3hE=
|
||||
github.com/jsimonetti/rtnetlink v1.4.1/go.mod h1:xJjT7t59UIZ62GLZbv6PLLo8VFrostJMPBAheR6OM8w=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
||||
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
||||
@@ -332,8 +329,8 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
|
||||
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
|
||||
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
|
||||
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg=
|
||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o=
|
||||
github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c=
|
||||
github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE=
|
||||
github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI=
|
||||
@@ -363,19 +360,19 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/opencontainers/runc v1.2.2 h1:jTg3Vw2A5f0N9PoxFTEwUhvpANGaNPT3689Yfd/zaX0=
|
||||
github.com/opencontainers/runc v1.2.2/go.mod h1:/PXzF0h531HTMsYQnmxXkBD7YaGShm/2zcRB79dksUc=
|
||||
github.com/opencontainers/runc v1.2.3 h1:fxE7amCzfZflJO2lHXf4y/y8M1BoAqp+FVmG19oYB80=
|
||||
github.com/opencontainers/runc v1.2.3/go.mod h1:nSxcWUydXrsBZVYNSkTjoQ/N6rcyTtn+1SD5D4+kRIM=
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||
github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA=
|
||||
github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw=
|
||||
github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||
github.com/petermattis/goid v0.0.0-20241211131331-93ee7e083c43 h1:ah1dvbqPMN5+ocrg/ZSgZ6k8bOk+kcZQ7fnyx6UvOm4=
|
||||
github.com/petermattis/goid v0.0.0-20241211131331-93ee7e083c43/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||
github.com/philip-bui/grpc-zerolog v1.0.1 h1:EMacvLRUd2O1K0eWod27ZP5CY1iTNkhBDLSN+Q4JEvA=
|
||||
github.com/philip-bui/grpc-zerolog v1.0.1/go.mod h1:qXbiq/2X4ZUMMshsqlWyTHOcw7ns+GZmlqZZN05ZHcQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
@@ -391,13 +388,13 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4=
|
||||
github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4=
|
||||
github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg=
|
||||
github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.58.0 h1:N+N8vY4/23r6iYfD3UQZUoJPnUYAo7v6LG5XZxjZTXo=
|
||||
github.com/prometheus/common v0.58.0/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0=
|
||||
github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ=
|
||||
github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI=
|
||||
@@ -407,8 +404,8 @@ github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEej
|
||||
github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE=
|
||||
github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8=
|
||||
github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s=
|
||||
github.com/pterm/pterm v0.12.79 h1:lH3yrYMhdpeqX9y5Ep1u7DejyHy7NSQg9qrBjF9dFT4=
|
||||
github.com/pterm/pterm v0.12.79/go.mod h1:1v/gzOF1N0FsjbgTHZ1wVycRkKiatFvJSJC4IGaQAAo=
|
||||
github.com/pterm/pterm v0.12.80 h1:mM55B+GnKUnLMUSqhdINe4s6tOuVQIetQ3my8JGyAIg=
|
||||
github.com/pterm/pterm v0.12.80/go.mod h1:c6DeF9bSnOSeFPZlfs4ZRAFcf5SCoTwvwQ5xaKGQlHo=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
@@ -417,8 +414,8 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
@@ -432,8 +429,8 @@ github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5
|
||||
github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU=
|
||||
github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U=
|
||||
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
@@ -461,8 +458,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP5LWHEY//SWsYkSO3RWRZo4OSWagh3YD2vQ=
|
||||
@@ -473,38 +470,36 @@ github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 h1:rXZGg
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ=
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio=
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
|
||||
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw=
|
||||
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=
|
||||
github.com/tailscale/hujson v0.0.0-20241010212012-29efb4a0184b h1:MNaGusDfB1qxEsl6iVb33Gbe777IKzPP5PDta0xGC8M=
|
||||
github.com/tailscale/hujson v0.0.0-20241010212012-29efb4a0184b/go.mod h1:EbW0wDK/qEUYI0A5bqq0C2kF8JTQwWONmGDBbzsxxHo=
|
||||
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU=
|
||||
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
|
||||
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 h1:Gz0rz40FvFVLTBk/K8UNAenb36EbDSnh+q7Z9ldcC8w=
|
||||
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4/go.mod h1:phI29ccmHQBc+wvroosENp1IF9195449VDnFDhJ4rJU=
|
||||
github.com/tailscale/setec v0.0.0-20240314234648-9da8e7407257 h1:6WsbDYsikRNmmbfZoRoyIEA9tfl0aspPAE0t7nBj2B4=
|
||||
github.com/tailscale/setec v0.0.0-20240314234648-9da8e7407257/go.mod h1:hrq01/0LUDZf4mMkcZ7Ovmy33jvCi4RpESpb9kPxV6E=
|
||||
github.com/tailscale/squibble v0.0.0-20240418235321-9ee0eeb78185 h1:zT+qB+2Ghulj50d5Wq6h6vQYqD2sPdhy4FF6+FHedVE=
|
||||
github.com/tailscale/squibble v0.0.0-20240418235321-9ee0eeb78185/go.mod h1:LoIjI6z/6efr9ebISQ5l2vjQmjc8QJrAYZdy3Ec3sVs=
|
||||
github.com/tailscale/tailsql v0.0.0-20240418235827-820559f382c1 h1:wmsnxEEuRlgK7Bhdkmm0JGrjjc0JoHZThLLo0WXXbLs=
|
||||
github.com/tailscale/tailsql v0.0.0-20240418235827-820559f382c1/go.mod h1:XN193fbz9RR/5stlWPMMIZR+TTa1BUkDJm5Azwzxwgw=
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 h1:tdUdyPqJ0C97SJfjB9tW6EylTtreyee9C44de+UBG0g=
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
|
||||
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA=
|
||||
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc=
|
||||
github.com/tailscale/setec v0.0.0-20240930150730-e6eb93658ed3 h1:Zk341hE1rcVUcDwA9XKmed2acHGGlbeFQzje6gvkuFo=
|
||||
github.com/tailscale/setec v0.0.0-20240930150730-e6eb93658ed3/go.mod h1:nexjfRM8veJVJ5PTbqYI2YrUj/jbk3deffEHO3DH9Q4=
|
||||
github.com/tailscale/squibble v0.0.0-20240909231413-32a80b9743f7 h1:nfklwaP8uNz2IbUygSKOQ1aDzzRRRLaIbPpnQWUUMGc=
|
||||
github.com/tailscale/squibble v0.0.0-20240909231413-32a80b9743f7/go.mod h1:YH/J7n7jNZOq10nTxxPANv2ha/Eg47/6J5b7NnOYAhQ=
|
||||
github.com/tailscale/tailsql v0.0.0-20241211062219-bf96884c6a49 h1:QFXXdoiYFiUS7a6DH7zE6Uacz3wMzH/1/VvWLnR9To4=
|
||||
github.com/tailscale/tailsql v0.0.0-20241211062219-bf96884c6a49/go.mod h1:IX3F8T6iILmg94hZGkkOf6rmjIHJCXNVqxOpiSUwHQQ=
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:UBPHPtv8+nEAy2PD8RyAhOYvau1ek0HDJqLS/Pysi14=
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
|
||||
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M=
|
||||
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc h1:cezaQN9pvKVaw56Ma5qr/G646uKIYP0yQf+OyWN/okc=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20250107165329-0b8b35511f19 h1:BcEJP2ewTIK2ZCsqgl6YGpuO6+oKqqag5HHb7ehljKw=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20250107165329-0b8b35511f19/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
|
||||
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek=
|
||||
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg=
|
||||
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
|
||||
github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
|
||||
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
|
||||
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
|
||||
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e h1:IWllFTiDjjLIf2oeKxpIUmtiDV5sn71VgeQgg6vcE7k=
|
||||
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e/go.mod h1:d7u6HkTYKSv5m6MCKkOQlHwaShTMl3HjqSGW3XtVhXM=
|
||||
github.com/tink-crypto/tink-go/v2 v2.1.0 h1:QXFBguwMwTIaU17EgZpEJWsUSc60b1BAGTzBIoMdmok=
|
||||
github.com/tink-crypto/tink-go/v2 v2.1.0/go.mod h1:y1TnYFt1i2eZVfx4OGc+C+EMp4CoKWAw2VSEuoicHHI=
|
||||
github.com/u-root/u-root v0.12.0 h1:K0AuBFriwr0w/PGS3HawiAw89e3+MU7ks80GpghAsNs=
|
||||
github.com/u-root/u-root v0.12.0/go.mod h1:FYjTOh4IkIZHhjsd17lb8nYW6udgXdJhG1c0r6u0arI=
|
||||
github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e h1:BA9O3BmlTmpjbvajAwzWx4Wo2TRVdpPXZEeemGQcajw=
|
||||
github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
|
||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
|
||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
|
||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
||||
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
@@ -523,14 +518,26 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJu
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
|
||||
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
|
||||
go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
|
||||
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
|
||||
go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk=
|
||||
go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8=
|
||||
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
|
||||
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
|
||||
go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H9rmp5nkDTAst8=
|
||||
go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=
|
||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
@@ -538,15 +545,15 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA=
|
||||
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||
golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a h1:8qmSSA8Gz/1kTrCe0nqR0R3Gb/NDhykzWw2q2mWZydM=
|
||||
golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
|
||||
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
|
||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
||||
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9A8KkmRtY9WvOFIxN8wgfvy6Zm1DV8=
|
||||
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
|
||||
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
@@ -555,8 +562,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
|
||||
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -569,11 +576,11 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
|
||||
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
|
||||
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -582,8 +589,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -603,20 +610,18 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.1-0.20250107080300-1c14dcadc3ab h1:BMkEEWYOjkvOX7+YKOGbp6jCyQ5pR2j0Ah47p1Vdsx4=
|
||||
golang.org/x/sys v0.29.1-0.20250107080300-1c14dcadc3ab/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -624,8 +629,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
|
||||
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
|
||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
@@ -633,10 +638,10 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
@@ -648,8 +653,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
||||
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -663,19 +668,19 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241216192217-9240e9c98484 h1:ChAdCYNQFDk5fYvFZMywKLIijG7TC2m1C2CMEu11G3o=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241216192217-9240e9c98484/go.mod h1:KRUmxRI4JmbpAm8gcZM4Jsffi859fo5LQjILwuqj9z8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484 h1:Z7FRVJPSMaHQxD0uXU8WdgFh8PseLM8Q8NzhnpMrBhQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c=
|
||||
google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
|
||||
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
||||
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/grpc v1.69.0 h1:quSiOM1GJPmPH5XtU+BCoVXcDVJJAzNcoyfC2cCjGkI=
|
||||
google.golang.org/grpc v1.69.0/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
|
||||
google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ=
|
||||
google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -690,10 +695,10 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
|
||||
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
||||
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
|
||||
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
|
||||
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
|
||||
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
||||
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 h1:TU8z2Lh3Bbq77w0t1eG8yRlLcNHzZu3x6mhoH2Mk0c8=
|
||||
@@ -712,8 +717,6 @@ modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
|
||||
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
||||
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
|
||||
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
|
||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||
@@ -724,16 +727,16 @@ modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
||||
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
||||
modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM=
|
||||
modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k=
|
||||
modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g=
|
||||
modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE=
|
||||
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=
|
||||
software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
|
||||
tailscale.com v1.75.0-pre.0.20240926101731-7d1160ddaab7 h1:nfRWV6ECxwNvvXKtbqSVstjlEi1BWktzv3FuxWpyyx0=
|
||||
tailscale.com v1.75.0-pre.0.20240926101731-7d1160ddaab7/go.mod h1:xKxYf3B3PuezFlRaMT+VhuVu8XTFUTLy+VCzLPMJVmg=
|
||||
tailscale.com v1.80.0 h1:7joWtDtdHEHJvGmOag10RNITKp1I4Ts7Hrn6pU33/1I=
|
||||
tailscale.com v1.80.0/go.mod h1:4tasV1xjJAMHuX2xWMWAnXEmlrAA6M3w1xnc32DlpMk=
|
||||
zgo.at/zcache/v2 v2.1.0 h1:USo+ubK+R4vtjw4viGzTe/zjXyPw6R7SK/RL3epBBxs=
|
||||
zgo.at/zcache/v2 v2.1.0/go.mod h1:gyCeoLVo01QjDZynjime8xUGHHMbsLiPyUTBpDGd4Gk=
|
||||
zombiezen.com/go/postgrestest v1.0.1 h1:aXoADQAJmZDU3+xilYVut0pHhgc0sF8ZspPW9gFNwP4=
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
grpcRuntime "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||
"github.com/juanfont/headscale"
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/capver"
|
||||
"github.com/juanfont/headscale/hscontrol/db"
|
||||
"github.com/juanfont/headscale/hscontrol/derp"
|
||||
derpServer "github.com/juanfont/headscale/hscontrol/derp/server"
|
||||
@@ -96,7 +97,7 @@ type Headscale struct {
|
||||
mapper *mapper.Mapper
|
||||
nodeNotifier *notifier.Notifier
|
||||
|
||||
registrationCache *zcache.Cache[string, types.Node]
|
||||
registrationCache *zcache.Cache[types.RegistrationID, types.RegisterNode]
|
||||
|
||||
authProvider AuthProvider
|
||||
|
||||
@@ -123,7 +124,7 @@ func NewHeadscale(cfg *types.Config) (*Headscale, error) {
|
||||
return nil, fmt.Errorf("failed to read or create Noise protocol private key: %w", err)
|
||||
}
|
||||
|
||||
registrationCache := zcache.New[string, types.Node](
|
||||
registrationCache := zcache.New[types.RegistrationID, types.RegisterNode](
|
||||
registerCacheExpiration,
|
||||
registerCacheCleanup,
|
||||
)
|
||||
@@ -245,11 +246,11 @@ func (h *Headscale) scheduledTasks(ctx context.Context) {
|
||||
|
||||
lastExpiryCheck := time.Unix(0, 0)
|
||||
|
||||
derpTicker := time.NewTicker(h.cfg.DERP.UpdateFrequency)
|
||||
defer derpTicker.Stop()
|
||||
// If we dont want auto update, just stop the ticker
|
||||
if !h.cfg.DERP.AutoUpdate {
|
||||
derpTicker.Stop()
|
||||
derpTickerChan := make(<-chan time.Time)
|
||||
if h.cfg.DERP.AutoUpdate && h.cfg.DERP.UpdateFrequency != 0 {
|
||||
derpTicker := time.NewTicker(h.cfg.DERP.UpdateFrequency)
|
||||
defer derpTicker.Stop()
|
||||
derpTickerChan = derpTicker.C
|
||||
}
|
||||
|
||||
var extraRecordsUpdate <-chan []tailcfg.DNSRecord
|
||||
@@ -285,7 +286,7 @@ func (h *Headscale) scheduledTasks(ctx context.Context) {
|
||||
h.nodeNotifier.NotifyAll(ctx, update)
|
||||
}
|
||||
|
||||
case <-derpTicker.C:
|
||||
case <-derpTickerChan:
|
||||
log.Info().Msg("Fetching DERPMap updates")
|
||||
h.DERPMap = derp.GetDERPMap(h.cfg.DERP)
|
||||
if h.cfg.DERP.ServerEnabled && h.cfg.DERP.AutomaticallyAddEmbeddedDerpRegion {
|
||||
@@ -462,7 +463,7 @@ func (h *Headscale) createRouter(grpcMux *grpcRuntime.ServeMux) *mux.Router {
|
||||
|
||||
router.HandleFunc("/health", h.HealthHandler).Methods(http.MethodGet)
|
||||
router.HandleFunc("/key", h.KeyHandler).Methods(http.MethodGet)
|
||||
router.HandleFunc("/register/{mkey}", h.authProvider.RegisterHandler).Methods(http.MethodGet)
|
||||
router.HandleFunc("/register/{registration_id}", h.authProvider.RegisterHandler).Methods(http.MethodGet)
|
||||
|
||||
if provider, ok := h.authProvider.(*AuthProviderOIDC); ok {
|
||||
router.HandleFunc("/oidc/callback", provider.OIDCCallbackHandler).Methods(http.MethodGet)
|
||||
@@ -520,29 +521,34 @@ func usersChangedHook(db *db.HSDatabase, polMan policy.PolicyManager, notif *not
|
||||
|
||||
// TODO(kradalby): Do a variant of this, and polman which only updates the node that has changed.
|
||||
// Maybe we should attempt a new in memory state and not go via the DB?
|
||||
func nodesChangedHook(db *db.HSDatabase, polMan policy.PolicyManager, notif *notifier.Notifier) error {
|
||||
// A bool is returned indicating if a full update was sent to all nodes
|
||||
func nodesChangedHook(db *db.HSDatabase, polMan policy.PolicyManager, notif *notifier.Notifier) (bool, error) {
|
||||
nodes, err := db.ListNodes()
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
changed, err := polMan.SetNodes(nodes)
|
||||
filterChanged, err := polMan.SetNodes(nodes)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
if changed {
|
||||
if filterChanged {
|
||||
ctx := types.NotifyCtx(context.Background(), "acl-nodes-change", "all")
|
||||
notif.NotifyAll(ctx, types.StateUpdate{
|
||||
Type: types.StateFullUpdate,
|
||||
})
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Serve launches the HTTP and gRPC server service Headscale and the API.
|
||||
func (h *Headscale) Serve() error {
|
||||
capver.CanOldCodeBeCleanedUp()
|
||||
|
||||
if profilingEnabled {
|
||||
if profilingPath != "" {
|
||||
err := os.MkdirAll(profilingPath, os.ModePerm)
|
||||
@@ -560,6 +566,11 @@ func (h *Headscale) Serve() error {
|
||||
spew.Dump(h.cfg)
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Caller().
|
||||
Str("minimum_version", capver.TailscaleVersion(capver.MinSupportedCapabilityVersion)).
|
||||
Msg("Clients with a lower minimum version will be rejected")
|
||||
|
||||
// Fetch an initial DERP Map before we start serving
|
||||
h.DERPMap = derp.GetDERPMap(h.cfg.DERP)
|
||||
h.mapper = mapper.NewMapper(h.db, h.cfg, h.DERPMap, h.nodeNotifier, h.polMan)
|
||||
@@ -838,6 +849,10 @@ func (h *Headscale) Serve() error {
|
||||
Str("signal", sig.String()).
|
||||
Msg("Received SIGHUP, reloading ACL and Config")
|
||||
|
||||
if h.cfg.Policy.IsEmpty() {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := h.loadPolicyManager(); err != nil {
|
||||
log.Error().Err(err).Msg("failed to reload Policy")
|
||||
}
|
||||
@@ -1102,6 +1117,10 @@ func (h *Headscale) policyBytes() ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if p.Data == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return []byte(p.Data), err
|
||||
}
|
||||
|
||||
|
||||
@@ -2,16 +2,16 @@ package hscontrol
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/juanfont/headscale/hscontrol/db"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/gorm"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
@@ -20,692 +20,276 @@ import (
|
||||
|
||||
type AuthProvider interface {
|
||||
RegisterHandler(http.ResponseWriter, *http.Request)
|
||||
AuthURL(key.MachinePublic) string
|
||||
AuthURL(types.RegistrationID) string
|
||||
}
|
||||
|
||||
func logAuthFunc(
|
||||
registerRequest tailcfg.RegisterRequest,
|
||||
machineKey key.MachinePublic,
|
||||
) (func(string), func(string), func(error, string)) {
|
||||
return func(msg string) {
|
||||
log.Info().
|
||||
Caller().
|
||||
Str("machine_key", machineKey.ShortString()).
|
||||
Str("node_key", registerRequest.NodeKey.ShortString()).
|
||||
Str("node_key_old", registerRequest.OldNodeKey.ShortString()).
|
||||
Str("node", registerRequest.Hostinfo.Hostname).
|
||||
Str("followup", registerRequest.Followup).
|
||||
Time("expiry", registerRequest.Expiry).
|
||||
Msg(msg)
|
||||
},
|
||||
func(msg string) {
|
||||
log.Trace().
|
||||
Caller().
|
||||
Str("machine_key", machineKey.ShortString()).
|
||||
Str("node_key", registerRequest.NodeKey.ShortString()).
|
||||
Str("node_key_old", registerRequest.OldNodeKey.ShortString()).
|
||||
Str("node", registerRequest.Hostinfo.Hostname).
|
||||
Str("followup", registerRequest.Followup).
|
||||
Time("expiry", registerRequest.Expiry).
|
||||
Msg(msg)
|
||||
},
|
||||
func(err error, msg string) {
|
||||
log.Error().
|
||||
Caller().
|
||||
Str("machine_key", machineKey.ShortString()).
|
||||
Str("node_key", registerRequest.NodeKey.ShortString()).
|
||||
Str("node_key_old", registerRequest.OldNodeKey.ShortString()).
|
||||
Str("node", registerRequest.Hostinfo.Hostname).
|
||||
Str("followup", registerRequest.Followup).
|
||||
Time("expiry", registerRequest.Expiry).
|
||||
Err(err).
|
||||
Msg(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// handleRegister is the logic for registering a client.
|
||||
func (h *Headscale) handleRegister(
|
||||
writer http.ResponseWriter,
|
||||
req *http.Request,
|
||||
ctx context.Context,
|
||||
regReq tailcfg.RegisterRequest,
|
||||
machineKey key.MachinePublic,
|
||||
) {
|
||||
logInfo, logTrace, _ := logAuthFunc(regReq, machineKey)
|
||||
now := time.Now().UTC()
|
||||
logTrace("handleRegister called, looking up machine in DB")
|
||||
node, err := h.db.GetNodeByAnyKey(machineKey, regReq.NodeKey, regReq.OldNodeKey)
|
||||
logTrace("handleRegister database lookup has returned")
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// If the node has AuthKey set, handle registration via PreAuthKeys
|
||||
if regReq.Auth != nil && regReq.Auth.AuthKey != "" {
|
||||
h.handleAuthKey(writer, regReq, machineKey)
|
||||
) (*tailcfg.RegisterResponse, error) {
|
||||
node, err := h.db.GetNodeByNodeKey(regReq.NodeKey)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fmt.Errorf("looking up node in database: %w", err)
|
||||
}
|
||||
|
||||
return
|
||||
if node != nil {
|
||||
resp, err := h.handleExistingNode(node, regReq, machineKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("handling existing node: %w", err)
|
||||
}
|
||||
|
||||
// Check if the node is waiting for interactive login.
|
||||
//
|
||||
// TODO(juan): We could use this field to improve our protocol implementation,
|
||||
// and hold the request until the client closes it, or the interactive
|
||||
// login is completed (i.e., the user registers the node).
|
||||
// This is not implemented yet, as it is no strictly required. The only side-effect
|
||||
// is that the client will hammer headscale with requests until it gets a
|
||||
// successful RegisterResponse.
|
||||
if regReq.Followup != "" {
|
||||
logTrace("register request is a followup")
|
||||
if _, ok := h.registrationCache.Get(machineKey.String()); ok {
|
||||
logTrace("Node is waiting for interactive login")
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
select {
|
||||
case <-req.Context().Done():
|
||||
return
|
||||
case <-time.After(registrationHoldoff):
|
||||
h.handleNewNode(writer, regReq, machineKey)
|
||||
if regReq.Followup != "" {
|
||||
// TODO(kradalby): Does this need to return an error of some sort?
|
||||
// Maybe if the registration fails down the line it can be sent
|
||||
// on the channel and returned here?
|
||||
h.waitForFollowup(ctx, regReq)
|
||||
}
|
||||
|
||||
return
|
||||
if regReq.Auth != nil && regReq.Auth.AuthKey != "" {
|
||||
resp, err := h.handleRegisterWithAuthKey(regReq, machineKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("handling register with auth key: %w", err)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
resp, err := h.handleRegisterInteractive(regReq, machineKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("handling register interactive: %w", err)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (h *Headscale) handleExistingNode(
|
||||
node *types.Node,
|
||||
regReq tailcfg.RegisterRequest,
|
||||
machineKey key.MachinePublic,
|
||||
) (*tailcfg.RegisterResponse, error) {
|
||||
if node.MachineKey != machineKey {
|
||||
return nil, NewHTTPError(http.StatusUnauthorized, "node exist with different machine key", nil)
|
||||
}
|
||||
|
||||
expired := node.IsExpired()
|
||||
if !expired && !regReq.Expiry.IsZero() {
|
||||
requestExpiry := regReq.Expiry
|
||||
|
||||
// The client is trying to extend their key, this is not allowed.
|
||||
if requestExpiry.After(time.Now()) {
|
||||
return nil, NewHTTPError(http.StatusBadRequest, "extending key is not allowed", nil)
|
||||
}
|
||||
|
||||
// If the request expiry is in the past, we consider it a logout.
|
||||
if requestExpiry.Before(time.Now()) {
|
||||
if node.IsEphemeral() {
|
||||
changedNodes, err := h.db.DeleteNode(node, h.nodeNotifier.LikelyConnectedMap())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("deleting ephemeral node: %w", err)
|
||||
}
|
||||
|
||||
ctx := types.NotifyCtx(context.Background(), "logout-ephemeral", "na")
|
||||
h.nodeNotifier.NotifyAll(ctx, types.StateUpdate{
|
||||
Type: types.StatePeerRemoved,
|
||||
Removed: []types.NodeID{node.ID},
|
||||
})
|
||||
if changedNodes != nil {
|
||||
h.nodeNotifier.NotifyAll(ctx, types.StateUpdate{
|
||||
Type: types.StatePeerChanged,
|
||||
ChangeNodes: changedNodes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
expired = true
|
||||
}
|
||||
|
||||
logInfo("Node not found in database, creating new")
|
||||
|
||||
// The node did not have a key to authenticate, which means
|
||||
// that we rely on a method that calls back some how (OpenID or CLI)
|
||||
// We create the node and then keep it around until a callback
|
||||
// happens
|
||||
newNode := types.Node{
|
||||
MachineKey: machineKey,
|
||||
Hostname: regReq.Hostinfo.Hostname,
|
||||
NodeKey: regReq.NodeKey,
|
||||
LastSeen: &now,
|
||||
Expiry: &time.Time{},
|
||||
err := h.db.NodeSetExpiry(node.ID, requestExpiry)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("setting node expiry: %w", err)
|
||||
}
|
||||
|
||||
if !regReq.Expiry.IsZero() {
|
||||
logTrace("Non-zero expiry time requested")
|
||||
newNode.Expiry = ®Req.Expiry
|
||||
}
|
||||
ctx := types.NotifyCtx(context.Background(), "logout-expiry", "na")
|
||||
h.nodeNotifier.NotifyWithIgnore(ctx, types.StateUpdateExpire(node.ID, requestExpiry), node.ID)
|
||||
}
|
||||
|
||||
h.registrationCache.Set(
|
||||
machineKey.String(),
|
||||
newNode,
|
||||
)
|
||||
return &tailcfg.RegisterResponse{
|
||||
// TODO(kradalby): Only send for user-owned nodes
|
||||
// and not tagged nodes when tags is working.
|
||||
User: *node.User.TailscaleUser(),
|
||||
Login: *node.User.TailscaleLogin(),
|
||||
NodeKeyExpired: expired,
|
||||
|
||||
h.handleNewNode(writer, regReq, machineKey)
|
||||
// Headscale does not implement the concept of machine authorization
|
||||
// so we always return true here.
|
||||
// Revisit this if #2176 gets implemented.
|
||||
MachineAuthorized: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *Headscale) waitForFollowup(
|
||||
ctx context.Context,
|
||||
regReq tailcfg.RegisterRequest,
|
||||
) {
|
||||
fu, err := url.Parse(regReq.Followup)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// The node is already in the DB. This could mean one of the following:
|
||||
// - The node is authenticated and ready to /map
|
||||
// - We are doing a key refresh
|
||||
// - The node is logged out (or expired) and pending to be authorized. TODO(juan): We need to keep alive the connection here
|
||||
if node != nil {
|
||||
// (juan): For a while we had a bug where we were not storing the MachineKey for the nodes using the TS2021,
|
||||
// due to a misunderstanding of the protocol https://github.com/juanfont/headscale/issues/1054
|
||||
// So if we have a not valid MachineKey (but we were able to fetch the node with the NodeKeys), we update it.
|
||||
if err != nil || node.MachineKey.IsZero() {
|
||||
if err := h.db.NodeSetMachineKey(node, machineKey); err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Str("func", "RegistrationHandler").
|
||||
Str("node", node.Hostname).
|
||||
Err(err).
|
||||
Msg("Error saving machine key to database")
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// If the NodeKey stored in headscale is the same as the key presented in a registration
|
||||
// request, then we have a node that is either:
|
||||
// - Trying to log out (sending a expiry in the past)
|
||||
// - A valid, registered node, looking for /map
|
||||
// - Expired node wanting to reauthenticate
|
||||
if node.NodeKey.String() == regReq.NodeKey.String() {
|
||||
// The client sends an Expiry in the past if the client is requesting to expire the key (aka logout)
|
||||
// https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L648
|
||||
if !regReq.Expiry.IsZero() &&
|
||||
regReq.Expiry.UTC().Before(now) {
|
||||
h.handleNodeLogOut(writer, *node)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// If node is not expired, and it is register, we have a already accepted this node,
|
||||
// let it proceed with a valid registration
|
||||
if !node.IsExpired() {
|
||||
h.handleNodeWithValidRegistration(writer, *node)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// The NodeKey we have matches OldNodeKey, which means this is a refresh after a key expiration
|
||||
if node.NodeKey.String() == regReq.OldNodeKey.String() &&
|
||||
!node.IsExpired() {
|
||||
h.handleNodeKeyRefresh(
|
||||
writer,
|
||||
regReq,
|
||||
*node,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// When logged out and reauthenticating with OIDC, the OldNodeKey is not passed, but the NodeKey has changed
|
||||
if node.NodeKey.String() != regReq.NodeKey.String() &&
|
||||
regReq.OldNodeKey.IsZero() && !node.IsExpired() {
|
||||
h.handleNodeKeyRefresh(
|
||||
writer,
|
||||
regReq,
|
||||
*node,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if regReq.Followup != "" {
|
||||
select {
|
||||
case <-req.Context().Done():
|
||||
return
|
||||
case <-time.After(registrationHoldoff):
|
||||
}
|
||||
}
|
||||
|
||||
// The node has expired or it is logged out
|
||||
h.handleNodeExpiredOrLoggedOut(writer, regReq, *node, machineKey)
|
||||
|
||||
// TODO(juan): RegisterRequest includes an Expiry time, that we could optionally use
|
||||
node.Expiry = &time.Time{}
|
||||
|
||||
// If we are here it means the client needs to be reauthorized,
|
||||
// we need to make sure the NodeKey matches the one in the request
|
||||
// TODO(juan): What happens when using fast user switching between two
|
||||
// headscale-managed tailnets?
|
||||
node.NodeKey = regReq.NodeKey
|
||||
h.registrationCache.Set(
|
||||
machineKey.String(),
|
||||
*node,
|
||||
)
|
||||
|
||||
followupReg, err := types.RegistrationIDFromString(strings.ReplaceAll(fu.Path, "/register/", ""))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if reg, ok := h.registrationCache.Get(followupReg); ok {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-reg.Registered:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleAuthKey contains the logic to manage auth key client registration
|
||||
// When using Noise, the machineKey is Zero.
|
||||
func (h *Headscale) handleAuthKey(
|
||||
writer http.ResponseWriter,
|
||||
registerRequest tailcfg.RegisterRequest,
|
||||
machineKey key.MachinePublic,
|
||||
) {
|
||||
log.Debug().
|
||||
Caller().
|
||||
Str("node", registerRequest.Hostinfo.Hostname).
|
||||
Msgf("Processing auth key for %s", registerRequest.Hostinfo.Hostname)
|
||||
resp := tailcfg.RegisterResponse{}
|
||||
|
||||
pak, err := h.db.ValidatePreAuthKey(registerRequest.Auth.AuthKey)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Str("node", registerRequest.Hostinfo.Hostname).
|
||||
Err(err).
|
||||
Msg("Failed authentication via AuthKey")
|
||||
resp.MachineAuthorized = false
|
||||
|
||||
respBody, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Str("node", registerRequest.Hostinfo.Hostname).
|
||||
Err(err).
|
||||
Msg("Cannot encode message")
|
||||
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusUnauthorized)
|
||||
_, err = writer.Write(respBody)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Failed to write response")
|
||||
}
|
||||
|
||||
log.Error().
|
||||
Caller().
|
||||
Str("node", registerRequest.Hostinfo.Hostname).
|
||||
Msg("Failed authentication via AuthKey")
|
||||
|
||||
return
|
||||
// canUsePreAuthKey checks if a pre auth key can be used.
|
||||
func canUsePreAuthKey(pak *types.PreAuthKey) error {
|
||||
if pak == nil {
|
||||
return NewHTTPError(http.StatusUnauthorized, "invalid authkey", nil)
|
||||
}
|
||||
if pak.Expiration != nil && pak.Expiration.Before(time.Now()) {
|
||||
return NewHTTPError(http.StatusUnauthorized, "authkey expired", nil)
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Caller().
|
||||
Str("node", registerRequest.Hostinfo.Hostname).
|
||||
Msg("Authentication key was valid, proceeding to acquire IP addresses")
|
||||
// we don't need to check if has been used before
|
||||
if pak.Reusable {
|
||||
return nil
|
||||
}
|
||||
|
||||
nodeKey := registerRequest.NodeKey
|
||||
if pak.Used {
|
||||
return NewHTTPError(http.StatusUnauthorized, "authkey already used", nil)
|
||||
}
|
||||
|
||||
// retrieve node information if it exist
|
||||
// The error is not important, because if it does not
|
||||
// exist, then this is a new node and we will move
|
||||
// on to registration.
|
||||
node, _ := h.db.GetNodeByAnyKey(machineKey, registerRequest.NodeKey, registerRequest.OldNodeKey)
|
||||
if node != nil {
|
||||
log.Trace().
|
||||
Caller().
|
||||
Str("node", node.Hostname).
|
||||
Msg("node was already registered before, refreshing with new auth key")
|
||||
return nil
|
||||
}
|
||||
|
||||
node.NodeKey = nodeKey
|
||||
if pak.ID != 0 {
|
||||
node.AuthKeyID = ptr.To(pak.ID)
|
||||
func (h *Headscale) handleRegisterWithAuthKey(
|
||||
regReq tailcfg.RegisterRequest,
|
||||
machineKey key.MachinePublic,
|
||||
) (*tailcfg.RegisterResponse, error) {
|
||||
pak, err := h.db.GetPreAuthKey(regReq.Auth.AuthKey)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, NewHTTPError(http.StatusUnauthorized, "invalid pre auth key", nil)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
node.Expiry = ®isterRequest.Expiry
|
||||
node.User = pak.User
|
||||
node.UserID = pak.UserID
|
||||
err := h.db.DB.Save(node).Error
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Str("node", node.Hostname).
|
||||
Err(err).
|
||||
Msg("failed to save node after logging in with auth key")
|
||||
err = canUsePreAuthKey(pak)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
nodeToRegister := types.Node{
|
||||
Hostname: regReq.Hostinfo.Hostname,
|
||||
UserID: pak.User.ID,
|
||||
User: pak.User,
|
||||
MachineKey: machineKey,
|
||||
NodeKey: regReq.NodeKey,
|
||||
Hostinfo: regReq.Hostinfo,
|
||||
LastSeen: ptr.To(time.Now()),
|
||||
RegisterMethod: util.RegisterMethodAuthKey,
|
||||
|
||||
aclTags := pak.Proto().GetAclTags()
|
||||
if len(aclTags) > 0 {
|
||||
// This conditional preserves the existing behaviour, although SaaS would reset the tags on auth-key login
|
||||
err = h.db.SetTags(node.ID, aclTags)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Str("node", node.Hostname).
|
||||
Strs("aclTags", aclTags).
|
||||
Err(err).
|
||||
Msg("Failed to set tags after refreshing node")
|
||||
// TODO(kradalby): This should not be set on the node,
|
||||
// they should be looked up through the key, which is
|
||||
// attached to the node.
|
||||
ForcedTags: pak.Proto().GetAclTags(),
|
||||
AuthKey: pak,
|
||||
AuthKeyID: &pak.ID,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
if !regReq.Expiry.IsZero() {
|
||||
nodeToRegister.Expiry = ®Req.Expiry
|
||||
}
|
||||
|
||||
ctx := types.NotifyCtx(context.Background(), "handle-authkey", "na")
|
||||
h.nodeNotifier.NotifyAll(ctx, types.StateUpdate{Type: types.StatePeerChanged, ChangeNodes: []types.NodeID{node.ID}})
|
||||
} else {
|
||||
now := time.Now().UTC()
|
||||
ipv4, ipv6, err := h.ipAlloc.Next()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("allocating IPs: %w", err)
|
||||
}
|
||||
|
||||
nodeToRegister := types.Node{
|
||||
Hostname: registerRequest.Hostinfo.Hostname,
|
||||
UserID: pak.User.ID,
|
||||
User: pak.User,
|
||||
MachineKey: machineKey,
|
||||
RegisterMethod: util.RegisterMethodAuthKey,
|
||||
Expiry: ®isterRequest.Expiry,
|
||||
NodeKey: nodeKey,
|
||||
LastSeen: &now,
|
||||
ForcedTags: pak.Proto().GetAclTags(),
|
||||
}
|
||||
|
||||
ipv4, ipv6, err := h.ipAlloc.Next()
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Str("func", "RegistrationHandler").
|
||||
Str("hostinfo.name", registerRequest.Hostinfo.Hostname).
|
||||
Err(err).
|
||||
Msg("failed to allocate IP ")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
pakID := uint(pak.ID)
|
||||
if pakID != 0 {
|
||||
nodeToRegister.AuthKeyID = ptr.To(pak.ID)
|
||||
}
|
||||
node, err = h.db.RegisterNode(
|
||||
node, err := db.Write(h.db.DB, func(tx *gorm.DB) (*types.Node, error) {
|
||||
node, err := db.RegisterNode(tx,
|
||||
nodeToRegister,
|
||||
ipv4, ipv6,
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("could not register node")
|
||||
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
return nil, fmt.Errorf("registering node: %w", err)
|
||||
}
|
||||
|
||||
err = nodesChangedHook(h.db, h.polMan, h.nodeNotifier)
|
||||
if err != nil {
|
||||
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
if !pak.Reusable {
|
||||
err = db.UsePreAuthKey(tx, pak)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("using pre auth key: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = h.db.Write(func(tx *gorm.DB) error {
|
||||
return db.UsePreAuthKey(tx, pak)
|
||||
return node, nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Failed to use pre-auth key")
|
||||
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.MachineAuthorized = true
|
||||
resp.User = *pak.User.TailscaleUser()
|
||||
// Provide LoginName when registering with pre-auth key
|
||||
// Otherwise it will need to exec `tailscale up` twice to fetch the *LoginName*
|
||||
resp.Login = *pak.User.TailscaleLogin()
|
||||
|
||||
respBody, err := json.Marshal(resp)
|
||||
updateSent, err := nodesChangedHook(h.db, h.polMan, h.nodeNotifier)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Str("node", registerRequest.Hostinfo.Hostname).
|
||||
Err(err).
|
||||
Msg("Cannot encode message")
|
||||
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
_, err = writer.Write(respBody)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Failed to write response")
|
||||
return
|
||||
return nil, fmt.Errorf("nodes changed hook: %w", err)
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Str("node", registerRequest.Hostinfo.Hostname).
|
||||
Msg("Successfully authenticated via AuthKey")
|
||||
if !updateSent {
|
||||
ctx := types.NotifyCtx(context.Background(), "node updated", node.Hostname)
|
||||
h.nodeNotifier.NotifyAll(ctx, types.StateUpdatePeerAdded(node.ID))
|
||||
}
|
||||
|
||||
return &tailcfg.RegisterResponse{
|
||||
MachineAuthorized: true,
|
||||
NodeKeyExpired: node.IsExpired(),
|
||||
User: *pak.User.TailscaleUser(),
|
||||
Login: *pak.User.TailscaleLogin(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// handleNewNode returns the authorisation URL to the client based on what type
|
||||
// of registration headscale is configured with.
|
||||
// This url is then showed to the user by the local Tailscale client.
|
||||
func (h *Headscale) handleNewNode(
|
||||
writer http.ResponseWriter,
|
||||
registerRequest tailcfg.RegisterRequest,
|
||||
machineKey key.MachinePublic,
|
||||
) {
|
||||
logInfo, logTrace, logErr := logAuthFunc(registerRequest, machineKey)
|
||||
|
||||
resp := tailcfg.RegisterResponse{}
|
||||
|
||||
// The node registration is new, redirect the client to the registration URL
|
||||
logTrace("The node seems to be new, sending auth url")
|
||||
|
||||
resp.AuthURL = h.authProvider.AuthURL(machineKey)
|
||||
|
||||
respBody, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
logErr(err, "Cannot encode message")
|
||||
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
_, err = writer.Write(respBody)
|
||||
if err != nil {
|
||||
logErr(err, "Failed to write response")
|
||||
}
|
||||
|
||||
logInfo(fmt.Sprintf("Successfully sent auth url: %s", resp.AuthURL))
|
||||
}
|
||||
|
||||
func (h *Headscale) handleNodeLogOut(
|
||||
writer http.ResponseWriter,
|
||||
node types.Node,
|
||||
) {
|
||||
resp := tailcfg.RegisterResponse{}
|
||||
|
||||
log.Info().
|
||||
Str("node", node.Hostname).
|
||||
Msg("Client requested logout")
|
||||
|
||||
now := time.Now()
|
||||
err := h.db.NodeSetExpiry(node.ID, now)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Failed to expire node")
|
||||
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx := types.NotifyCtx(context.Background(), "logout-expiry", "na")
|
||||
h.nodeNotifier.NotifyWithIgnore(ctx, types.StateUpdateExpire(node.ID, now), node.ID)
|
||||
|
||||
resp.AuthURL = ""
|
||||
resp.MachineAuthorized = false
|
||||
resp.NodeKeyExpired = true
|
||||
resp.User = *node.User.TailscaleUser()
|
||||
respBody, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Cannot encode message")
|
||||
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
_, err = writer.Write(respBody)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Failed to write response")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if node.IsEphemeral() {
|
||||
changedNodes, err := h.db.DeleteNode(&node, h.nodeNotifier.LikelyConnectedMap())
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("node", node.Hostname).
|
||||
Msg("Cannot delete ephemeral node from the database")
|
||||
}
|
||||
|
||||
ctx := types.NotifyCtx(context.Background(), "logout-ephemeral", "na")
|
||||
h.nodeNotifier.NotifyAll(ctx, types.StateUpdate{
|
||||
Type: types.StatePeerRemoved,
|
||||
Removed: []types.NodeID{node.ID},
|
||||
})
|
||||
if changedNodes != nil {
|
||||
h.nodeNotifier.NotifyAll(ctx, types.StateUpdate{
|
||||
Type: types.StatePeerChanged,
|
||||
ChangeNodes: changedNodes,
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Caller().
|
||||
Str("node", node.Hostname).
|
||||
Msg("Successfully logged out")
|
||||
}
|
||||
|
||||
func (h *Headscale) handleNodeWithValidRegistration(
|
||||
writer http.ResponseWriter,
|
||||
node types.Node,
|
||||
) {
|
||||
resp := tailcfg.RegisterResponse{}
|
||||
|
||||
// The node registration is valid, respond with redirect to /map
|
||||
log.Debug().
|
||||
Caller().
|
||||
Str("node", node.Hostname).
|
||||
Msg("Client is registered and we have the current NodeKey. All clear to /map")
|
||||
|
||||
resp.AuthURL = ""
|
||||
resp.MachineAuthorized = true
|
||||
resp.User = *node.User.TailscaleUser()
|
||||
resp.Login = *node.User.TailscaleLogin()
|
||||
|
||||
respBody, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Cannot encode message")
|
||||
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
_, err = writer.Write(respBody)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Failed to write response")
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Caller().
|
||||
Str("node", node.Hostname).
|
||||
Msg("Node successfully authorized")
|
||||
}
|
||||
|
||||
func (h *Headscale) handleNodeKeyRefresh(
|
||||
writer http.ResponseWriter,
|
||||
registerRequest tailcfg.RegisterRequest,
|
||||
node types.Node,
|
||||
) {
|
||||
resp := tailcfg.RegisterResponse{}
|
||||
|
||||
log.Info().
|
||||
Caller().
|
||||
Str("node", node.Hostname).
|
||||
Msg("We have the OldNodeKey in the database. This is a key refresh")
|
||||
|
||||
err := h.db.Write(func(tx *gorm.DB) error {
|
||||
return db.NodeSetNodeKey(tx, &node, registerRequest.NodeKey)
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Failed to update machine key in the database")
|
||||
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
resp.AuthURL = ""
|
||||
resp.User = *node.User.TailscaleUser()
|
||||
respBody, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Cannot encode message")
|
||||
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
_, err = writer.Write(respBody)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Failed to write response")
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Caller().
|
||||
Str("node_key", registerRequest.NodeKey.ShortString()).
|
||||
Str("old_node_key", registerRequest.OldNodeKey.ShortString()).
|
||||
Str("node", node.Hostname).
|
||||
Msg("Node key successfully refreshed")
|
||||
}
|
||||
|
||||
func (h *Headscale) handleNodeExpiredOrLoggedOut(
|
||||
writer http.ResponseWriter,
|
||||
func (h *Headscale) handleRegisterInteractive(
|
||||
regReq tailcfg.RegisterRequest,
|
||||
node types.Node,
|
||||
machineKey key.MachinePublic,
|
||||
) {
|
||||
resp := tailcfg.RegisterResponse{}
|
||||
|
||||
if regReq.Auth != nil && regReq.Auth.AuthKey != "" {
|
||||
h.handleAuthKey(writer, regReq, machineKey)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// The client has registered before, but has expired or logged out
|
||||
log.Trace().
|
||||
Caller().
|
||||
Str("node", node.Hostname).
|
||||
Str("machine_key", machineKey.ShortString()).
|
||||
Str("node_key", regReq.NodeKey.ShortString()).
|
||||
Str("node_key_old", regReq.OldNodeKey.ShortString()).
|
||||
Msg("Node registration has expired or logged out. Sending a auth url to register")
|
||||
|
||||
resp.AuthURL = h.authProvider.AuthURL(machineKey)
|
||||
|
||||
respBody, err := json.Marshal(resp)
|
||||
) (*tailcfg.RegisterResponse, error) {
|
||||
registrationId, err := types.NewRegistrationID()
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Cannot encode message")
|
||||
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
return nil, fmt.Errorf("generating registration ID: %w", err)
|
||||
}
|
||||
|
||||
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
_, err = writer.Write(respBody)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Failed to write response")
|
||||
newNode := types.RegisterNode{
|
||||
Node: types.Node{
|
||||
Hostname: regReq.Hostinfo.Hostname,
|
||||
MachineKey: machineKey,
|
||||
NodeKey: regReq.NodeKey,
|
||||
Hostinfo: regReq.Hostinfo,
|
||||
LastSeen: ptr.To(time.Now()),
|
||||
},
|
||||
Registered: make(chan struct{}),
|
||||
}
|
||||
|
||||
log.Trace().
|
||||
Caller().
|
||||
Str("machine_key", machineKey.ShortString()).
|
||||
Str("node_key", regReq.NodeKey.ShortString()).
|
||||
Str("node_key_old", regReq.OldNodeKey.ShortString()).
|
||||
Str("node", node.Hostname).
|
||||
Msg("Node logged out. Sent AuthURL for reauthentication")
|
||||
if !regReq.Expiry.IsZero() {
|
||||
newNode.Node.Expiry = ®Req.Expiry
|
||||
}
|
||||
|
||||
h.registrationCache.Set(
|
||||
registrationId,
|
||||
newNode,
|
||||
)
|
||||
|
||||
return &tailcfg.RegisterResponse{
|
||||
AuthURL: h.authProvider.AuthURL(registrationId),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
package hscontrol
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
// // NoiseRegistrationHandler handles the actual registration process of a node.
|
||||
func (ns *noiseServer) NoiseRegistrationHandler(
|
||||
writer http.ResponseWriter,
|
||||
req *http.Request,
|
||||
) {
|
||||
log.Trace().Caller().Msgf("Noise registration handler for client %s", req.RemoteAddr)
|
||||
if req.Method != http.MethodPost {
|
||||
http.Error(writer, "Wrong method", http.StatusMethodNotAllowed)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace().
|
||||
Any("headers", req.Header).
|
||||
Caller().
|
||||
Msg("Headers")
|
||||
|
||||
body, _ := io.ReadAll(req.Body)
|
||||
registerRequest := tailcfg.RegisterRequest{}
|
||||
if err := json.Unmarshal(body, ®isterRequest); err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Cannot parse RegisterRequest")
|
||||
http.Error(writer, "Internal error", http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Reject unsupported versions
|
||||
if registerRequest.Version < MinimumCapVersion {
|
||||
log.Info().
|
||||
Caller().
|
||||
Int("min_version", int(MinimumCapVersion)).
|
||||
Int("client_version", int(registerRequest.Version)).
|
||||
Msg("unsupported client connected")
|
||||
http.Error(writer, "Internal error", http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ns.nodeKey = registerRequest.NodeKey
|
||||
|
||||
ns.headscale.handleRegister(writer, req, registerRequest, ns.conn.Peer())
|
||||
}
|
||||
130
hscontrol/auth_test.go
Normal file
130
hscontrol/auth_test.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package hscontrol
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
)
|
||||
|
||||
func TestCanUsePreAuthKey(t *testing.T) {
|
||||
now := time.Now()
|
||||
past := now.Add(-time.Hour)
|
||||
future := now.Add(time.Hour)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
pak *types.PreAuthKey
|
||||
wantErr bool
|
||||
err HTTPError
|
||||
}{
|
||||
{
|
||||
name: "valid reusable key",
|
||||
pak: &types.PreAuthKey{
|
||||
Reusable: true,
|
||||
Used: false,
|
||||
Expiration: &future,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid non-reusable key",
|
||||
pak: &types.PreAuthKey{
|
||||
Reusable: false,
|
||||
Used: false,
|
||||
Expiration: &future,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "expired key",
|
||||
pak: &types.PreAuthKey{
|
||||
Reusable: false,
|
||||
Used: false,
|
||||
Expiration: &past,
|
||||
},
|
||||
wantErr: true,
|
||||
err: NewHTTPError(http.StatusUnauthorized, "authkey expired", nil),
|
||||
},
|
||||
{
|
||||
name: "used non-reusable key",
|
||||
pak: &types.PreAuthKey{
|
||||
Reusable: false,
|
||||
Used: true,
|
||||
Expiration: &future,
|
||||
},
|
||||
wantErr: true,
|
||||
err: NewHTTPError(http.StatusUnauthorized, "authkey already used", nil),
|
||||
},
|
||||
{
|
||||
name: "used reusable key",
|
||||
pak: &types.PreAuthKey{
|
||||
Reusable: true,
|
||||
Used: true,
|
||||
Expiration: &future,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "no expiration date",
|
||||
pak: &types.PreAuthKey{
|
||||
Reusable: false,
|
||||
Used: false,
|
||||
Expiration: nil,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "nil preauth key",
|
||||
pak: nil,
|
||||
wantErr: true,
|
||||
err: NewHTTPError(http.StatusUnauthorized, "invalid authkey", nil),
|
||||
},
|
||||
{
|
||||
name: "expired and used key",
|
||||
pak: &types.PreAuthKey{
|
||||
Reusable: false,
|
||||
Used: true,
|
||||
Expiration: &past,
|
||||
},
|
||||
wantErr: true,
|
||||
err: NewHTTPError(http.StatusUnauthorized, "authkey expired", nil),
|
||||
},
|
||||
{
|
||||
name: "no expiration and used key",
|
||||
pak: &types.PreAuthKey{
|
||||
Reusable: false,
|
||||
Used: true,
|
||||
Expiration: nil,
|
||||
},
|
||||
wantErr: true,
|
||||
err: NewHTTPError(http.StatusUnauthorized, "authkey already used", nil),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := canUsePreAuthKey(tt.pak)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("expected error but got none")
|
||||
} else {
|
||||
httpErr, ok := err.(HTTPError)
|
||||
if !ok {
|
||||
t.Errorf("expected HTTPError but got %T", err)
|
||||
} else {
|
||||
if diff := cmp.Diff(tt.err, httpErr); diff != "" {
|
||||
t.Errorf("unexpected error (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("expected no error but got %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
106
hscontrol/capver/capver.go
Normal file
106
hscontrol/capver/capver.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package capver
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
xmaps "golang.org/x/exp/maps"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
const MinSupportedCapabilityVersion tailcfg.CapabilityVersion = 88
|
||||
|
||||
// CanOldCodeBeCleanedUp is intended to be called on startup to see if
|
||||
// there are old code that can ble cleaned up, entries should contain
|
||||
// a CapVer where something can be cleaned up and a panic if it can.
|
||||
// This is only intended to catch things in tests.
|
||||
//
|
||||
// All uses of Capability version checks should be listed here.
|
||||
func CanOldCodeBeCleanedUp() {
|
||||
if MinSupportedCapabilityVersion >= 111 {
|
||||
panic("LegacyDERP can be cleaned up in tail.go")
|
||||
}
|
||||
}
|
||||
|
||||
func tailscaleVersSorted() []string {
|
||||
vers := xmaps.Keys(tailscaleToCapVer)
|
||||
sort.Strings(vers)
|
||||
return vers
|
||||
}
|
||||
|
||||
func capVersSorted() []tailcfg.CapabilityVersion {
|
||||
capVers := xmaps.Keys(capVerToTailscaleVer)
|
||||
sort.Slice(capVers, func(i, j int) bool {
|
||||
return capVers[i] < capVers[j]
|
||||
})
|
||||
return capVers
|
||||
}
|
||||
|
||||
// TailscaleVersion returns the Tailscale version for the given CapabilityVersion.
|
||||
func TailscaleVersion(ver tailcfg.CapabilityVersion) string {
|
||||
return capVerToTailscaleVer[ver]
|
||||
}
|
||||
|
||||
// CapabilityVersion returns the CapabilityVersion for the given Tailscale version.
|
||||
func CapabilityVersion(ver string) tailcfg.CapabilityVersion {
|
||||
if !strings.HasPrefix(ver, "v") {
|
||||
ver = "v" + ver
|
||||
}
|
||||
return tailscaleToCapVer[ver]
|
||||
}
|
||||
|
||||
// TailscaleLatest returns the n latest Tailscale versions.
|
||||
func TailscaleLatest(n int) []string {
|
||||
if n <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
tsSorted := tailscaleVersSorted()
|
||||
|
||||
if n > len(tsSorted) {
|
||||
return tsSorted
|
||||
}
|
||||
|
||||
return tsSorted[len(tsSorted)-n:]
|
||||
}
|
||||
|
||||
// TailscaleLatestMajorMinor returns the n latest Tailscale versions (e.g. 1.80).
|
||||
func TailscaleLatestMajorMinor(n int, stripV bool) []string {
|
||||
if n <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
majors := set.Set[string]{}
|
||||
for _, vers := range tailscaleVersSorted() {
|
||||
if stripV {
|
||||
vers = strings.TrimPrefix(vers, "v")
|
||||
}
|
||||
v := strings.Split(vers, ".")
|
||||
majors.Add(v[0] + "." + v[1])
|
||||
}
|
||||
|
||||
majorSl := majors.Slice()
|
||||
sort.Strings(majorSl)
|
||||
|
||||
if n > len(majorSl) {
|
||||
return majorSl
|
||||
}
|
||||
|
||||
return majorSl[len(majorSl)-n:]
|
||||
}
|
||||
|
||||
// CapVerLatest returns the n latest CapabilityVersions.
|
||||
func CapVerLatest(n int) []tailcfg.CapabilityVersion {
|
||||
if n <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
s := capVersSorted()
|
||||
|
||||
if n > len(s) {
|
||||
return s
|
||||
}
|
||||
|
||||
return s[len(s)-n:]
|
||||
}
|
||||
56
hscontrol/capver/capver_generated.go
Normal file
56
hscontrol/capver/capver_generated.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package capver
|
||||
|
||||
//Generated DO NOT EDIT
|
||||
|
||||
import "tailscale.com/tailcfg"
|
||||
|
||||
var tailscaleToCapVer = map[string]tailcfg.CapabilityVersion{
|
||||
"v1.44.3": 63,
|
||||
"v1.56.1": 82,
|
||||
"v1.58.0": 85,
|
||||
"v1.58.1": 85,
|
||||
"v1.58.2": 85,
|
||||
"v1.60.0": 87,
|
||||
"v1.60.1": 87,
|
||||
"v1.62.0": 88,
|
||||
"v1.62.1": 88,
|
||||
"v1.64.0": 90,
|
||||
"v1.64.1": 90,
|
||||
"v1.64.2": 90,
|
||||
"v1.66.0": 95,
|
||||
"v1.66.1": 95,
|
||||
"v1.66.2": 95,
|
||||
"v1.66.3": 95,
|
||||
"v1.66.4": 95,
|
||||
"v1.68.0": 97,
|
||||
"v1.68.1": 97,
|
||||
"v1.68.2": 97,
|
||||
"v1.70.0": 102,
|
||||
"v1.72.0": 104,
|
||||
"v1.72.1": 104,
|
||||
"v1.74.0": 106,
|
||||
"v1.74.1": 106,
|
||||
"v1.76.0": 106,
|
||||
"v1.76.1": 106,
|
||||
"v1.76.6": 106,
|
||||
"v1.78.0": 109,
|
||||
"v1.78.1": 109,
|
||||
"v1.80.0": 113,
|
||||
}
|
||||
|
||||
|
||||
var capVerToTailscaleVer = map[tailcfg.CapabilityVersion]string{
|
||||
63: "v1.44.3",
|
||||
82: "v1.56.1",
|
||||
85: "v1.58.0",
|
||||
87: "v1.60.0",
|
||||
88: "v1.62.0",
|
||||
90: "v1.64.0",
|
||||
95: "v1.66.0",
|
||||
97: "v1.68.0",
|
||||
102: "v1.70.0",
|
||||
104: "v1.72.0",
|
||||
106: "v1.74.0",
|
||||
109: "v1.78.0",
|
||||
113: "v1.80.0",
|
||||
}
|
||||
66
hscontrol/capver/capver_test.go
Normal file
66
hscontrol/capver/capver_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package capver
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func TestTailscaleLatestMajorMinor(t *testing.T) {
|
||||
tests := []struct {
|
||||
n int
|
||||
stripV bool
|
||||
expected []string
|
||||
}{
|
||||
{3, false, []string{"v1.76", "v1.78", "v1.80"}},
|
||||
{2, true, []string{"1.78", "1.80"}},
|
||||
// Lazy way to see all supported versions
|
||||
{10, true, []string{
|
||||
"1.62",
|
||||
"1.64",
|
||||
"1.66",
|
||||
"1.68",
|
||||
"1.70",
|
||||
"1.72",
|
||||
"1.74",
|
||||
"1.76",
|
||||
"1.78",
|
||||
"1.80",
|
||||
}},
|
||||
{0, false, nil},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run("", func(t *testing.T) {
|
||||
output := TailscaleLatestMajorMinor(test.n, test.stripV)
|
||||
if diff := cmp.Diff(output, test.expected); diff != "" {
|
||||
t.Errorf("TailscaleLatestMajorMinor(%d, %v) mismatch (-want +got):\n%s", test.n, test.stripV, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapVerMinimumTailscaleVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
input tailcfg.CapabilityVersion
|
||||
expected string
|
||||
}{
|
||||
{85, "v1.58.0"},
|
||||
{90, "v1.64.0"},
|
||||
{95, "v1.66.0"},
|
||||
{106, "v1.74.0"},
|
||||
{109, "v1.78.0"},
|
||||
{9001, ""}, // Test case for a version higher than any in the map
|
||||
{60, ""}, // Test case for a version lower than any in the map
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run("", func(t *testing.T) {
|
||||
output := TailscaleVersion(test.input)
|
||||
if output != test.expected {
|
||||
t.Errorf("CapVerFromTailscaleVersion(%d) = %s; want %s", test.input, output, test.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
157
hscontrol/capver/gen/main.go
Normal file
157
hscontrol/capver/gen/main.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package main
|
||||
|
||||
//go:generate go run main.go
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
xmaps "golang.org/x/exp/maps"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
const (
|
||||
releasesURL = "https://api.github.com/repos/tailscale/tailscale/releases"
|
||||
rawFileURL = "https://github.com/tailscale/tailscale/raw/refs/tags/%s/tailcfg/tailcfg.go"
|
||||
outputFile = "../capver_generated.go"
|
||||
)
|
||||
|
||||
type Release struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func getCapabilityVersions() (map[string]tailcfg.CapabilityVersion, error) {
|
||||
// Fetch the releases
|
||||
resp, err := http.Get(releasesURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error fetching releases: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading response body: %w", err)
|
||||
}
|
||||
|
||||
var releases []Release
|
||||
err = json.Unmarshal(body, &releases)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error unmarshalling JSON: %w", err)
|
||||
}
|
||||
|
||||
// Regular expression to find the CurrentCapabilityVersion line
|
||||
re := regexp.MustCompile(`const CurrentCapabilityVersion CapabilityVersion = (\d+)`)
|
||||
|
||||
versions := make(map[string]tailcfg.CapabilityVersion)
|
||||
|
||||
for _, release := range releases {
|
||||
version := strings.TrimSpace(release.Name)
|
||||
if !strings.HasPrefix(version, "v") {
|
||||
version = "v" + version
|
||||
}
|
||||
|
||||
// Fetch the raw Go file
|
||||
rawURL := fmt.Sprintf(rawFileURL, version)
|
||||
resp, err := http.Get(rawURL)
|
||||
if err != nil {
|
||||
fmt.Printf("Error fetching raw file for version %s: %v\n", version, err)
|
||||
continue
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading raw file for version %s: %v\n", version, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Find the CurrentCapabilityVersion
|
||||
matches := re.FindStringSubmatch(string(body))
|
||||
if len(matches) > 1 {
|
||||
capabilityVersionStr := matches[1]
|
||||
capabilityVersion, _ := strconv.Atoi(capabilityVersionStr)
|
||||
versions[version] = tailcfg.CapabilityVersion(capabilityVersion)
|
||||
} else {
|
||||
fmt.Printf("Version: %s, CurrentCapabilityVersion not found\n", version)
|
||||
}
|
||||
}
|
||||
|
||||
return versions, nil
|
||||
}
|
||||
|
||||
func writeCapabilityVersionsToFile(versions map[string]tailcfg.CapabilityVersion) error {
|
||||
// Open the output file
|
||||
file, err := os.Create(outputFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Write the package declaration and variable
|
||||
file.WriteString("package capver\n\n")
|
||||
file.WriteString("//Generated DO NOT EDIT\n\n")
|
||||
file.WriteString(`import "tailscale.com/tailcfg"`)
|
||||
file.WriteString("\n\n")
|
||||
file.WriteString("var tailscaleToCapVer = map[string]tailcfg.CapabilityVersion{\n")
|
||||
|
||||
sortedVersions := xmaps.Keys(versions)
|
||||
sort.Strings(sortedVersions)
|
||||
for _, version := range sortedVersions {
|
||||
file.WriteString(fmt.Sprintf("\t\"%s\": %d,\n", version, versions[version]))
|
||||
}
|
||||
file.WriteString("}\n")
|
||||
|
||||
file.WriteString("\n\n")
|
||||
file.WriteString("var capVerToTailscaleVer = map[tailcfg.CapabilityVersion]string{\n")
|
||||
|
||||
capVarToTailscaleVer := make(map[tailcfg.CapabilityVersion]string)
|
||||
for _, v := range sortedVersions {
|
||||
cap := versions[v]
|
||||
log.Printf("cap for v: %d, %s", cap, v)
|
||||
|
||||
// If it is already set, skip and continue,
|
||||
// we only want the first tailscale vsion per
|
||||
// capability vsion.
|
||||
if _, ok := capVarToTailscaleVer[cap]; ok {
|
||||
log.Printf("Skipping %d, %s", cap, v)
|
||||
continue
|
||||
}
|
||||
log.Printf("Storing %d, %s", cap, v)
|
||||
capVarToTailscaleVer[cap] = v
|
||||
}
|
||||
|
||||
capsSorted := xmaps.Keys(capVarToTailscaleVer)
|
||||
sort.Slice(capsSorted, func(i, j int) bool {
|
||||
return capsSorted[i] < capsSorted[j]
|
||||
})
|
||||
for _, capVer := range capsSorted {
|
||||
file.WriteString(fmt.Sprintf("\t%d:\t\t\"%s\",\n", capVer, capVarToTailscaleVer[capVer]))
|
||||
}
|
||||
file.WriteString("}\n")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
versions, err := getCapabilityVersions()
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = writeCapabilityVersionsToFile(versions)
|
||||
if err != nil {
|
||||
fmt.Println("Error writing to file:", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Capability versions written to", outputFile)
|
||||
}
|
||||
@@ -41,7 +41,7 @@ type KV struct {
|
||||
type HSDatabase struct {
|
||||
DB *gorm.DB
|
||||
cfg *types.DatabaseConfig
|
||||
regCache *zcache.Cache[string, types.Node]
|
||||
regCache *zcache.Cache[types.RegistrationID, types.RegisterNode]
|
||||
|
||||
baseDomain string
|
||||
}
|
||||
@@ -51,7 +51,7 @@ type HSDatabase struct {
|
||||
func NewHeadscaleDatabase(
|
||||
cfg types.DatabaseConfig,
|
||||
baseDomain string,
|
||||
regCache *zcache.Cache[string, types.Node],
|
||||
regCache *zcache.Cache[types.RegistrationID, types.RegisterNode],
|
||||
) (*HSDatabase, error) {
|
||||
dbConn, err := openDB(cfg)
|
||||
if err != nil {
|
||||
@@ -103,7 +103,7 @@ func NewHeadscaleDatabase(
|
||||
|
||||
dbConn.Model(&types.Node{}).Where("auth_key_id = ?", 0).Update("auth_key_id", nil)
|
||||
// If the Node table has a column for registered,
|
||||
// find all occourences of "false" and drop them. Then
|
||||
// find all occurrences of "false" and drop them. Then
|
||||
// remove the column.
|
||||
if tx.Migrator().HasColumn(&types.Node{}, "registered") {
|
||||
log.Info().
|
||||
@@ -478,9 +478,41 @@ func NewHeadscaleDatabase(
|
||||
// populate the user with more interesting information.
|
||||
ID: "202407191627",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
// Fix an issue where the automigration in GORM expected a constraint to
|
||||
// exists that didnt, and add the one it wanted.
|
||||
// Fixes https://github.com/juanfont/headscale/issues/2351
|
||||
if cfg.Type == types.DatabasePostgres {
|
||||
err := tx.Exec(`
|
||||
BEGIN;
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'uni_users_name'
|
||||
) THEN
|
||||
ALTER TABLE users ADD CONSTRAINT uni_users_name UNIQUE (name);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'users_name_key'
|
||||
) THEN
|
||||
ALTER TABLE users DROP CONSTRAINT users_name_key;
|
||||
END IF;
|
||||
END $$;
|
||||
COMMIT;
|
||||
`).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to rename constraint: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
err := tx.AutoMigrate(&types.User{})
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("automigrating types.User: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -495,7 +527,7 @@ func NewHeadscaleDatabase(
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
err := tx.AutoMigrate(&types.User{})
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("automigrating types.User: %w", err)
|
||||
}
|
||||
|
||||
// Set up indexes and unique constraints outside of GORM, it does not support
|
||||
@@ -521,6 +553,75 @@ func NewHeadscaleDatabase(
|
||||
},
|
||||
Rollback: func(db *gorm.DB) error { return nil },
|
||||
},
|
||||
{
|
||||
// Add a constraint to routes ensuring they cannot exist without a node.
|
||||
ID: "202501221827",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
// Remove any invalid routes associated with a node that does not exist.
|
||||
if tx.Migrator().HasTable(&types.Route{}) && tx.Migrator().HasTable(&types.Node{}) {
|
||||
err := tx.Exec("delete from routes where node_id not in (select id from nodes)").Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any invalid routes without a node_id.
|
||||
if tx.Migrator().HasTable(&types.Route{}) {
|
||||
err := tx.Exec("delete from routes where node_id is null").Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err := tx.AutoMigrate(&types.Route{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("automigrating types.Route: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Rollback: func(db *gorm.DB) error { return nil },
|
||||
},
|
||||
// Add back constraint so you cannot delete preauth keys that
|
||||
// is still used by a node.
|
||||
{
|
||||
ID: "202501311657",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
err := tx.AutoMigrate(&types.PreAuthKey{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("automigrating types.PreAuthKey: %w", err)
|
||||
}
|
||||
err = tx.AutoMigrate(&types.Node{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("automigrating types.Node: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Rollback: func(db *gorm.DB) error { return nil },
|
||||
},
|
||||
// Ensure there are no nodes refering to a deleted preauthkey.
|
||||
{
|
||||
ID: "202502070949",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
if tx.Migrator().HasTable(&types.PreAuthKey{}) {
|
||||
err := tx.Exec(`
|
||||
UPDATE nodes
|
||||
SET auth_key_id = NULL
|
||||
WHERE auth_key_id IS NOT NULL
|
||||
AND auth_key_id NOT IN (
|
||||
SELECT id FROM pre_auth_keys
|
||||
);
|
||||
`).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting auth_key to null on nodes with non-existing keys: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Rollback: func(db *gorm.DB) error { return nil },
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -588,7 +689,7 @@ func openDB(cfg types.DatabaseConfig) (*gorm.DB, error) {
|
||||
}
|
||||
|
||||
// The pure Go SQLite library does not handle locking in
|
||||
// the same way as the C based one and we cant use the gorm
|
||||
// the same way as the C based one and we can't use the gorm
|
||||
// connection pool as of 2022/02/23.
|
||||
sqlDB, _ := db.DB()
|
||||
sqlDB.SetMaxIdleConns(1)
|
||||
@@ -651,7 +752,7 @@ func openDB(cfg types.DatabaseConfig) (*gorm.DB, error) {
|
||||
}
|
||||
|
||||
func runMigrations(cfg types.DatabaseConfig, dbConn *gorm.DB, migrations *gormigrate.Gormigrate) error {
|
||||
// Turn off foreign keys for the duration of the migration if using sqllite to
|
||||
// Turn off foreign keys for the duration of the migration if using sqlite to
|
||||
// prevent data loss due to the way the GORM migrator handles certain schema
|
||||
// changes.
|
||||
if cfg.Type == types.DatabaseSqlite {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"io"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
@@ -23,7 +24,10 @@ import (
|
||||
"zgo.at/zcache/v2"
|
||||
)
|
||||
|
||||
func TestMigrations(t *testing.T) {
|
||||
// TestMigrationsSQLite is the main function for testing migrations,
|
||||
// we focus on SQLite correctness as it is the main database used in headscale.
|
||||
// All migrations that are worth testing should be added here.
|
||||
func TestMigrationsSQLite(t *testing.T) {
|
||||
ipp := func(p string) netip.Prefix {
|
||||
return netip.MustParsePrefix(p)
|
||||
}
|
||||
@@ -197,6 +201,26 @@ func TestMigrations(t *testing.T) {
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
dbPath: "testdata/failing-node-preauth-constraint.sqlite",
|
||||
wantFunc: func(t *testing.T, h *HSDatabase) {
|
||||
nodes, err := Read(h.DB, func(rx *gorm.DB) (types.Nodes, error) {
|
||||
return ListNodes(rx)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, node := range nodes {
|
||||
assert.Falsef(t, node.MachineKey.IsZero(), "expected non zero machinekey")
|
||||
assert.Contains(t, node.MachineKey.String(), "mkey:")
|
||||
assert.Falsef(t, node.NodeKey.IsZero(), "expected non zero nodekey")
|
||||
assert.Contains(t, node.NodeKey.String(), "nodekey:")
|
||||
assert.Falsef(t, node.DiscoKey.IsZero(), "expected non zero discokey")
|
||||
assert.Contains(t, node.DiscoKey.String(), "discokey:")
|
||||
assert.Nil(t, node.AuthKey)
|
||||
assert.Nil(t, node.AuthKeyID)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -256,8 +280,8 @@ func testCopyOfDatabase(src string) (string, error) {
|
||||
return dst, err
|
||||
}
|
||||
|
||||
func emptyCache() *zcache.Cache[string, types.Node] {
|
||||
return zcache.New[string, types.Node](time.Minute, time.Hour)
|
||||
func emptyCache() *zcache.Cache[types.RegistrationID, types.RegisterNode] {
|
||||
return zcache.New[types.RegistrationID, types.RegisterNode](time.Minute, time.Hour)
|
||||
}
|
||||
|
||||
// requireConstraintFailed checks if the error is a constraint failure with
|
||||
@@ -278,9 +302,9 @@ func TestConstraints(t *testing.T) {
|
||||
{
|
||||
name: "no-duplicate-username-if-no-oidc",
|
||||
run: func(t *testing.T, db *gorm.DB) {
|
||||
_, err := CreateUser(db, "user1")
|
||||
_, err := CreateUser(db, types.User{Name: "user1"})
|
||||
require.NoError(t, err)
|
||||
_, err = CreateUser(db, "user1")
|
||||
_, err = CreateUser(db, types.User{Name: "user1"})
|
||||
requireConstraintFailed(t, err)
|
||||
},
|
||||
},
|
||||
@@ -331,7 +355,7 @@ func TestConstraints(t *testing.T) {
|
||||
{
|
||||
name: "allow-duplicate-username-cli-then-oidc",
|
||||
run: func(t *testing.T, db *gorm.DB) {
|
||||
_, err := CreateUser(db, "user1") // Create CLI username
|
||||
_, err := CreateUser(db, types.User{Name: "user1"}) // Create CLI username
|
||||
require.NoError(t, err)
|
||||
|
||||
user := types.User{
|
||||
@@ -354,7 +378,7 @@ func TestConstraints(t *testing.T) {
|
||||
err := db.Save(&user).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = CreateUser(db, "user1") // Create CLI username
|
||||
_, err = CreateUser(db, types.User{Name: "user1"}) // Create CLI username
|
||||
require.NoError(t, err)
|
||||
},
|
||||
},
|
||||
@@ -375,3 +399,58 @@ func TestConstraints(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrationsPostgres(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
dbPath string
|
||||
wantFunc func(*testing.T, *HSDatabase)
|
||||
}{
|
||||
{
|
||||
name: "user-idx-breaking",
|
||||
dbPath: "testdata/pre-24-postgresdb.pssql.dump",
|
||||
wantFunc: func(t *testing.T, h *HSDatabase) {
|
||||
users, err := Read(h.DB, func(rx *gorm.DB) ([]types.User, error) {
|
||||
return ListUsers(rx)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, user := range users {
|
||||
assert.NotEmpty(t, user.Name)
|
||||
assert.Empty(t, user.ProfilePicURL)
|
||||
assert.Empty(t, user.Email)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
u := newPostgresDBForTest(t)
|
||||
|
||||
pgRestorePath, err := exec.LookPath("pg_restore")
|
||||
if err != nil {
|
||||
t.Fatal("pg_restore not found in PATH. Please install it and ensure it is accessible.")
|
||||
}
|
||||
|
||||
// Construct the pg_restore command
|
||||
cmd := exec.Command(pgRestorePath, "--verbose", "--if-exists", "--clean", "--no-owner", "--dbname", u.String(), tt.dbPath)
|
||||
|
||||
// Set the output streams
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
// Execute the command
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to restore postgres database: %s", err)
|
||||
}
|
||||
|
||||
db = newHeadscaleDBFromPostgresURL(t, u)
|
||||
|
||||
if tt.wantFunc != nil {
|
||||
tt.wantFunc(t, db)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,36 +158,28 @@ func GetNodeByMachineKey(
|
||||
return &mach, nil
|
||||
}
|
||||
|
||||
func (hsdb *HSDatabase) GetNodeByAnyKey(
|
||||
machineKey key.MachinePublic,
|
||||
nodeKey key.NodePublic,
|
||||
oldNodeKey key.NodePublic,
|
||||
) (*types.Node, error) {
|
||||
func (hsdb *HSDatabase) GetNodeByNodeKey(nodeKey key.NodePublic) (*types.Node, error) {
|
||||
return Read(hsdb.DB, func(rx *gorm.DB) (*types.Node, error) {
|
||||
return GetNodeByAnyKey(rx, machineKey, nodeKey, oldNodeKey)
|
||||
return GetNodeByNodeKey(rx, nodeKey)
|
||||
})
|
||||
}
|
||||
|
||||
// GetNodeByAnyKey finds a Node by its MachineKey, its current NodeKey or the old one, and returns the Node struct.
|
||||
// TODO(kradalby): see if we can remove this.
|
||||
func GetNodeByAnyKey(
|
||||
// GetNodeByNodeKey finds a Node by its NodeKey and returns the Node struct.
|
||||
func GetNodeByNodeKey(
|
||||
tx *gorm.DB,
|
||||
machineKey key.MachinePublic, nodeKey key.NodePublic, oldNodeKey key.NodePublic,
|
||||
nodeKey key.NodePublic,
|
||||
) (*types.Node, error) {
|
||||
node := types.Node{}
|
||||
mach := types.Node{}
|
||||
if result := tx.
|
||||
Preload("AuthKey").
|
||||
Preload("AuthKey.User").
|
||||
Preload("User").
|
||||
Preload("Routes").
|
||||
First(&node, "machine_key = ? OR node_key = ? OR node_key = ?",
|
||||
machineKey.String(),
|
||||
nodeKey.String(),
|
||||
oldNodeKey.String()); result.Error != nil {
|
||||
First(&mach, "node_key = ?", nodeKey.String()); result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
|
||||
return &node, nil
|
||||
return &mach, nil
|
||||
}
|
||||
|
||||
func (hsdb *HSDatabase) SetTags(
|
||||
@@ -245,7 +237,7 @@ func RenameNode(tx *gorm.DB,
|
||||
return fmt.Errorf("renaming node: %w", err)
|
||||
}
|
||||
|
||||
uniq, err := isUnqiueName(tx, newName)
|
||||
uniq, err := isUniqueName(tx, newName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking if name is unique: %w", err)
|
||||
}
|
||||
@@ -319,60 +311,83 @@ func SetLastSeen(tx *gorm.DB, nodeID types.NodeID, lastSeen time.Time) error {
|
||||
return tx.Model(&types.Node{}).Where("id = ?", nodeID).Update("last_seen", lastSeen).Error
|
||||
}
|
||||
|
||||
func (hsdb *HSDatabase) RegisterNodeFromAuthCallback(
|
||||
mkey key.MachinePublic,
|
||||
// HandleNodeFromAuthPath is called from the OIDC or CLI auth path
|
||||
// with a registrationID to register or reauthenticate a node.
|
||||
// If the node found in the registration cache is not already registered,
|
||||
// it will be registered with the user and the node will be removed from the cache.
|
||||
// If the node is already registered, the expiry will be updated.
|
||||
// The node, and a boolean indicating if it was a new node or not, will be returned.
|
||||
func (hsdb *HSDatabase) HandleNodeFromAuthPath(
|
||||
registrationID types.RegistrationID,
|
||||
userID types.UserID,
|
||||
nodeExpiry *time.Time,
|
||||
registrationMethod string,
|
||||
ipv4 *netip.Addr,
|
||||
ipv6 *netip.Addr,
|
||||
) (*types.Node, error) {
|
||||
return Write(hsdb.DB, func(tx *gorm.DB) (*types.Node, error) {
|
||||
if node, ok := hsdb.regCache.Get(mkey.String()); ok {
|
||||
user, err := GetUserByID(tx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"failed to find user in register node from auth callback, %w",
|
||||
err,
|
||||
) (*types.Node, bool, error) {
|
||||
var newNode bool
|
||||
node, err := Write(hsdb.DB, func(tx *gorm.DB) (*types.Node, error) {
|
||||
if reg, ok := hsdb.regCache.Get(registrationID); ok {
|
||||
if node, _ := GetNodeByNodeKey(tx, reg.Node.NodeKey); node == nil {
|
||||
user, err := GetUserByID(tx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"failed to find user in register node from auth callback, %w",
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Str("registration_id", registrationID.String()).
|
||||
Str("username", user.Username()).
|
||||
Str("registrationMethod", registrationMethod).
|
||||
Str("expiresAt", fmt.Sprintf("%v", nodeExpiry)).
|
||||
Msg("Registering node from API/CLI or auth callback")
|
||||
|
||||
// TODO(kradalby): This looks quite wrong? why ID 0?
|
||||
// Why not always?
|
||||
// Registration of expired node with different user
|
||||
if reg.Node.ID != 0 &&
|
||||
reg.Node.UserID != user.ID {
|
||||
return nil, ErrDifferentRegisteredUser
|
||||
}
|
||||
|
||||
reg.Node.UserID = user.ID
|
||||
reg.Node.User = *user
|
||||
reg.Node.RegisterMethod = registrationMethod
|
||||
|
||||
if nodeExpiry != nil {
|
||||
reg.Node.Expiry = nodeExpiry
|
||||
}
|
||||
|
||||
node, err := RegisterNode(
|
||||
tx,
|
||||
reg.Node,
|
||||
ipv4, ipv6,
|
||||
)
|
||||
|
||||
if err == nil {
|
||||
hsdb.regCache.Delete(registrationID)
|
||||
}
|
||||
|
||||
// Signal to waiting clients that the machine has been registered.
|
||||
close(reg.Registered)
|
||||
newNode = true
|
||||
return node, err
|
||||
} else {
|
||||
// If the node is already registered, this is a refresh.
|
||||
err := NodeSetExpiry(tx, node.ID, *nodeExpiry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return node, nil
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Str("machine_key", mkey.ShortString()).
|
||||
Str("username", user.Username()).
|
||||
Str("registrationMethod", registrationMethod).
|
||||
Str("expiresAt", fmt.Sprintf("%v", nodeExpiry)).
|
||||
Msg("Registering node from API/CLI or auth callback")
|
||||
|
||||
// Registration of expired node with different user
|
||||
if node.ID != 0 &&
|
||||
node.UserID != user.ID {
|
||||
return nil, ErrDifferentRegisteredUser
|
||||
}
|
||||
|
||||
node.UserID = user.ID
|
||||
node.User = *user
|
||||
node.RegisterMethod = registrationMethod
|
||||
|
||||
if nodeExpiry != nil {
|
||||
node.Expiry = nodeExpiry
|
||||
}
|
||||
|
||||
node, err := RegisterNode(
|
||||
tx,
|
||||
node,
|
||||
ipv4, ipv6,
|
||||
)
|
||||
|
||||
if err == nil {
|
||||
hsdb.regCache.Delete(mkey.String())
|
||||
}
|
||||
|
||||
return node, err
|
||||
}
|
||||
|
||||
return nil, ErrNodeNotFoundRegistrationCache
|
||||
})
|
||||
|
||||
return node, newNode, err
|
||||
}
|
||||
|
||||
func (hsdb *HSDatabase) RegisterNode(node types.Node, ipv4 *netip.Addr, ipv6 *netip.Addr) (*types.Node, error) {
|
||||
@@ -390,6 +405,18 @@ func RegisterNode(tx *gorm.DB, node types.Node, ipv4 *netip.Addr, ipv6 *netip.Ad
|
||||
Str("user", node.User.Username()).
|
||||
Msg("Registering node")
|
||||
|
||||
// If the a new node is registered with the same machine key, to the same user,
|
||||
// update the existing node.
|
||||
// If the same node is registered again, but to a new user, then that is considered
|
||||
// a new node.
|
||||
oldNode, _ := GetNodeByMachineKey(tx, node.MachineKey)
|
||||
if oldNode != nil && oldNode.UserID == node.UserID {
|
||||
node.ID = oldNode.ID
|
||||
node.GivenName = oldNode.GivenName
|
||||
ipv4 = oldNode.IPv4
|
||||
ipv6 = oldNode.IPv6
|
||||
}
|
||||
|
||||
// If the node exists and it already has IP(s), we just save it
|
||||
// so we store the node.Expire and node.Nodekey that has been set when
|
||||
// adding it to the registrationCache
|
||||
@@ -630,7 +657,7 @@ func generateGivenName(suppliedName string, randomSuffix bool) (string, error) {
|
||||
return suppliedName, nil
|
||||
}
|
||||
|
||||
func isUnqiueName(tx *gorm.DB, name string) (bool, error) {
|
||||
func isUniqueName(tx *gorm.DB, name string) (bool, error) {
|
||||
nodes := types.Nodes{}
|
||||
if err := tx.
|
||||
Where("given_name = ?", name).Find(&nodes).Error; err != nil {
|
||||
@@ -649,7 +676,7 @@ func ensureUniqueGivenName(
|
||||
return "", err
|
||||
}
|
||||
|
||||
unique, err := isUnqiueName(tx, givenName)
|
||||
unique, err := isUniqueName(tx, givenName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
)
|
||||
|
||||
func (s *Suite) TestGetNode(c *check.C) {
|
||||
user, err := db.CreateUser("test")
|
||||
user, err := db.CreateUser(types.User{Name: "test"})
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||
@@ -56,7 +56,7 @@ func (s *Suite) TestGetNode(c *check.C) {
|
||||
}
|
||||
|
||||
func (s *Suite) TestGetNodeByID(c *check.C) {
|
||||
user, err := db.CreateUser("test")
|
||||
user, err := db.CreateUser(types.User{Name: "test"})
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||
@@ -84,39 +84,8 @@ func (s *Suite) TestGetNodeByID(c *check.C) {
|
||||
c.Assert(err, check.IsNil)
|
||||
}
|
||||
|
||||
func (s *Suite) TestGetNodeByAnyNodeKey(c *check.C) {
|
||||
user, err := db.CreateUser("test")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
_, err = db.GetNodeByID(0)
|
||||
c.Assert(err, check.NotNil)
|
||||
|
||||
nodeKey := key.NewNode()
|
||||
oldNodeKey := key.NewNode()
|
||||
|
||||
machineKey := key.NewMachine()
|
||||
|
||||
node := types.Node{
|
||||
ID: 0,
|
||||
MachineKey: machineKey.Public(),
|
||||
NodeKey: nodeKey.Public(),
|
||||
Hostname: "testnode",
|
||||
UserID: user.ID,
|
||||
RegisterMethod: util.RegisterMethodAuthKey,
|
||||
AuthKeyID: ptr.To(pak.ID),
|
||||
}
|
||||
trx := db.DB.Save(&node)
|
||||
c.Assert(trx.Error, check.IsNil)
|
||||
|
||||
_, err = db.GetNodeByAnyKey(machineKey.Public(), nodeKey.Public(), oldNodeKey.Public())
|
||||
c.Assert(err, check.IsNil)
|
||||
}
|
||||
|
||||
func (s *Suite) TestHardDeleteNode(c *check.C) {
|
||||
user, err := db.CreateUser("test")
|
||||
user, err := db.CreateUser(types.User{Name: "test"})
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
nodeKey := key.NewNode()
|
||||
@@ -141,7 +110,7 @@ func (s *Suite) TestHardDeleteNode(c *check.C) {
|
||||
}
|
||||
|
||||
func (s *Suite) TestListPeers(c *check.C) {
|
||||
user, err := db.CreateUser("test")
|
||||
user, err := db.CreateUser(types.User{Name: "test"})
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||
@@ -188,7 +157,7 @@ func (s *Suite) TestGetACLFilteredPeers(c *check.C) {
|
||||
stor := make([]base, 0)
|
||||
|
||||
for _, name := range []string{"test", "admin"} {
|
||||
user, err := db.CreateUser(name)
|
||||
user, err := db.CreateUser(types.User{Name: name})
|
||||
c.Assert(err, check.IsNil)
|
||||
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
@@ -279,7 +248,7 @@ func (s *Suite) TestGetACLFilteredPeers(c *check.C) {
|
||||
}
|
||||
|
||||
func (s *Suite) TestExpireNode(c *check.C) {
|
||||
user, err := db.CreateUser("test")
|
||||
user, err := db.CreateUser(types.User{Name: "test"})
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||
@@ -320,7 +289,7 @@ func (s *Suite) TestExpireNode(c *check.C) {
|
||||
}
|
||||
|
||||
func (s *Suite) TestSetTags(c *check.C) {
|
||||
user, err := db.CreateUser("test")
|
||||
user, err := db.CreateUser(types.User{Name: "test"})
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||
@@ -565,7 +534,7 @@ func TestAutoApproveRoutes(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, pol)
|
||||
|
||||
user, err := adb.CreateUser("test")
|
||||
user, err := adb.CreateUser(types.User{Name: "test"})
|
||||
require.NoError(t, err)
|
||||
|
||||
pak, err := adb.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||
@@ -706,7 +675,7 @@ func TestListEphemeralNodes(t *testing.T) {
|
||||
t.Fatalf("creating db: %s", err)
|
||||
}
|
||||
|
||||
user, err := db.CreateUser("test")
|
||||
user, err := db.CreateUser(types.User{Name: "test"})
|
||||
require.NoError(t, err)
|
||||
|
||||
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||
@@ -762,10 +731,10 @@ func TestRenameNode(t *testing.T) {
|
||||
t.Fatalf("creating db: %s", err)
|
||||
}
|
||||
|
||||
user, err := db.CreateUser("test")
|
||||
user, err := db.CreateUser(types.User{Name: "test"})
|
||||
require.NoError(t, err)
|
||||
|
||||
user2, err := db.CreateUser("test2")
|
||||
user2, err := db.CreateUser(types.User{Name: "user2"})
|
||||
require.NoError(t, err)
|
||||
|
||||
node := types.Node{
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"gorm.io/gorm"
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
@@ -64,6 +63,7 @@ func CreatePreAuthKey(
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
// TODO(kradalby): unify the key generations spread all over the code.
|
||||
kstr, err := generateKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -108,18 +108,21 @@ func ListPreAuthKeysByUser(tx *gorm.DB, uid types.UserID) ([]types.PreAuthKey, e
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
// GetPreAuthKey returns a PreAuthKey for a given key.
|
||||
func GetPreAuthKey(tx *gorm.DB, user string, key string) (*types.PreAuthKey, error) {
|
||||
pak, err := ValidatePreAuthKey(tx, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func (hsdb *HSDatabase) GetPreAuthKey(key string) (*types.PreAuthKey, error) {
|
||||
return Read(hsdb.DB, func(rx *gorm.DB) (*types.PreAuthKey, error) {
|
||||
return GetPreAuthKey(rx, key)
|
||||
})
|
||||
}
|
||||
|
||||
// GetPreAuthKey returns a PreAuthKey for a given key. The caller is responsible
|
||||
// for checking if the key is usable (expired or used).
|
||||
func GetPreAuthKey(tx *gorm.DB, key string) (*types.PreAuthKey, error) {
|
||||
pak := types.PreAuthKey{}
|
||||
if err := tx.Preload("User").First(&pak, "key = ?", key).Error; err != nil {
|
||||
return nil, ErrPreAuthKeyNotFound
|
||||
}
|
||||
|
||||
if pak.User.Name != user {
|
||||
return nil, ErrUserMismatch
|
||||
}
|
||||
|
||||
return pak, nil
|
||||
return &pak, nil
|
||||
}
|
||||
|
||||
// DestroyPreAuthKey destroys a preauthkey. Returns error if the PreAuthKey
|
||||
@@ -140,15 +143,6 @@ func (hsdb *HSDatabase) ExpirePreAuthKey(k *types.PreAuthKey) error {
|
||||
})
|
||||
}
|
||||
|
||||
// MarkExpirePreAuthKey marks a PreAuthKey as expired.
|
||||
func ExpirePreAuthKey(tx *gorm.DB, k *types.PreAuthKey) error {
|
||||
if err := tx.Model(&k).Update("Expiration", time.Now()).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UsePreAuthKey marks a PreAuthKey as used.
|
||||
func UsePreAuthKey(tx *gorm.DB, k *types.PreAuthKey) error {
|
||||
k.Used = true
|
||||
@@ -159,44 +153,13 @@ func UsePreAuthKey(tx *gorm.DB, k *types.PreAuthKey) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (hsdb *HSDatabase) ValidatePreAuthKey(k string) (*types.PreAuthKey, error) {
|
||||
return Read(hsdb.DB, func(rx *gorm.DB) (*types.PreAuthKey, error) {
|
||||
return ValidatePreAuthKey(rx, k)
|
||||
})
|
||||
}
|
||||
|
||||
// ValidatePreAuthKey does the heavy lifting for validation of the PreAuthKey coming from a node
|
||||
// If returns no error and a PreAuthKey, it can be used.
|
||||
func ValidatePreAuthKey(tx *gorm.DB, k string) (*types.PreAuthKey, error) {
|
||||
pak := types.PreAuthKey{}
|
||||
if result := tx.Preload("User").First(&pak, "key = ?", k); errors.Is(
|
||||
result.Error,
|
||||
gorm.ErrRecordNotFound,
|
||||
) {
|
||||
return nil, ErrPreAuthKeyNotFound
|
||||
// MarkExpirePreAuthKey marks a PreAuthKey as expired.
|
||||
func ExpirePreAuthKey(tx *gorm.DB, k *types.PreAuthKey) error {
|
||||
if err := tx.Model(&k).Update("Expiration", time.Now()).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if pak.Expiration != nil && pak.Expiration.Before(time.Now()) {
|
||||
return nil, ErrPreAuthKeyExpired
|
||||
}
|
||||
|
||||
if pak.Reusable { // we don't need to check if has been used before
|
||||
return &pak, nil
|
||||
}
|
||||
|
||||
nodes := types.Nodes{}
|
||||
if err := tx.
|
||||
Preload("AuthKey").
|
||||
Where(&types.Node{AuthKeyID: ptr.To(pak.ID)}).
|
||||
Find(&nodes).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(nodes) != 0 || pak.Used {
|
||||
return nil, ErrSingleUseAuthKeyHasBeenUsed
|
||||
}
|
||||
|
||||
return &pak, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateKey() (string, error) {
|
||||
|
||||
@@ -2,12 +2,15 @@ package db
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"time"
|
||||
"testing"
|
||||
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"gopkg.in/check.v1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"tailscale.com/types/ptr"
|
||||
|
||||
"gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
func (*Suite) TestCreatePreAuthKey(c *check.C) {
|
||||
@@ -15,7 +18,7 @@ func (*Suite) TestCreatePreAuthKey(c *check.C) {
|
||||
_, err := db.CreatePreAuthKey(12345, true, false, nil, nil)
|
||||
c.Assert(err, check.NotNil)
|
||||
|
||||
user, err := db.CreateUser("test")
|
||||
user, err := db.CreateUser(types.User{Name: "test"})
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
key, err := db.CreatePreAuthKey(types.UserID(user.ID), true, false, nil, nil)
|
||||
@@ -40,125 +43,8 @@ func (*Suite) TestCreatePreAuthKey(c *check.C) {
|
||||
c.Assert((keys)[0].User.ID, check.Equals, user.ID)
|
||||
}
|
||||
|
||||
func (*Suite) TestExpiredPreAuthKey(c *check.C) {
|
||||
user, err := db.CreateUser("test2")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
now := time.Now().Add(-5 * time.Second)
|
||||
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), true, false, &now, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
key, err := db.ValidatePreAuthKey(pak.Key)
|
||||
c.Assert(err, check.Equals, ErrPreAuthKeyExpired)
|
||||
c.Assert(key, check.IsNil)
|
||||
}
|
||||
|
||||
func (*Suite) TestPreAuthKeyDoesNotExist(c *check.C) {
|
||||
key, err := db.ValidatePreAuthKey("potatoKey")
|
||||
c.Assert(err, check.Equals, ErrPreAuthKeyNotFound)
|
||||
c.Assert(key, check.IsNil)
|
||||
}
|
||||
|
||||
func (*Suite) TestValidateKeyOk(c *check.C) {
|
||||
user, err := db.CreateUser("test3")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), true, false, nil, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
key, err := db.ValidatePreAuthKey(pak.Key)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(key.ID, check.Equals, pak.ID)
|
||||
}
|
||||
|
||||
func (*Suite) TestAlreadyUsedKey(c *check.C) {
|
||||
user, err := db.CreateUser("test4")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
node := types.Node{
|
||||
ID: 0,
|
||||
Hostname: "testest",
|
||||
UserID: user.ID,
|
||||
RegisterMethod: util.RegisterMethodAuthKey,
|
||||
AuthKeyID: ptr.To(pak.ID),
|
||||
}
|
||||
trx := db.DB.Save(&node)
|
||||
c.Assert(trx.Error, check.IsNil)
|
||||
|
||||
key, err := db.ValidatePreAuthKey(pak.Key)
|
||||
c.Assert(err, check.Equals, ErrSingleUseAuthKeyHasBeenUsed)
|
||||
c.Assert(key, check.IsNil)
|
||||
}
|
||||
|
||||
func (*Suite) TestReusableBeingUsedKey(c *check.C) {
|
||||
user, err := db.CreateUser("test5")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), true, false, nil, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
node := types.Node{
|
||||
ID: 1,
|
||||
Hostname: "testest",
|
||||
UserID: user.ID,
|
||||
RegisterMethod: util.RegisterMethodAuthKey,
|
||||
AuthKeyID: ptr.To(pak.ID),
|
||||
}
|
||||
trx := db.DB.Save(&node)
|
||||
c.Assert(trx.Error, check.IsNil)
|
||||
|
||||
key, err := db.ValidatePreAuthKey(pak.Key)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(key.ID, check.Equals, pak.ID)
|
||||
}
|
||||
|
||||
func (*Suite) TestNotReusableNotBeingUsedKey(c *check.C) {
|
||||
user, err := db.CreateUser("test6")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
key, err := db.ValidatePreAuthKey(pak.Key)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(key.ID, check.Equals, pak.ID)
|
||||
}
|
||||
|
||||
func (*Suite) TestExpirePreauthKey(c *check.C) {
|
||||
user, err := db.CreateUser("test3")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), true, false, nil, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(pak.Expiration, check.IsNil)
|
||||
|
||||
err = db.ExpirePreAuthKey(pak)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(pak.Expiration, check.NotNil)
|
||||
|
||||
key, err := db.ValidatePreAuthKey(pak.Key)
|
||||
c.Assert(err, check.Equals, ErrPreAuthKeyExpired)
|
||||
c.Assert(key, check.IsNil)
|
||||
}
|
||||
|
||||
func (*Suite) TestNotReusableMarkedAsUsed(c *check.C) {
|
||||
user, err := db.CreateUser("test6")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
pak.Used = true
|
||||
db.DB.Save(&pak)
|
||||
|
||||
_, err = db.ValidatePreAuthKey(pak.Key)
|
||||
c.Assert(err, check.Equals, ErrSingleUseAuthKeyHasBeenUsed)
|
||||
}
|
||||
|
||||
func (*Suite) TestPreAuthKeyACLTags(c *check.C) {
|
||||
user, err := db.CreateUser("test8")
|
||||
user, err := db.CreateUser(types.User{Name: "test8"})
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
_, err = db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, []string{"badtag"})
|
||||
@@ -175,3 +61,25 @@ func (*Suite) TestPreAuthKeyACLTags(c *check.C) {
|
||||
sort.Sort(sort.StringSlice(gotTags))
|
||||
c.Assert(gotTags, check.DeepEquals, tags)
|
||||
}
|
||||
|
||||
func TestCannotDeleteAssignedPreAuthKey(t *testing.T) {
|
||||
db, err := newSQLiteTestDB()
|
||||
require.NoError(t, err)
|
||||
user, err := db.CreateUser(types.User{Name: "test8"})
|
||||
assert.NoError(t, err)
|
||||
|
||||
key, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, []string{"tag:good"})
|
||||
assert.NoError(t, err)
|
||||
|
||||
node := types.Node{
|
||||
ID: 0,
|
||||
Hostname: "testest",
|
||||
UserID: user.ID,
|
||||
RegisterMethod: util.RegisterMethodAuthKey,
|
||||
AuthKeyID: ptr.To(key.ID),
|
||||
}
|
||||
db.DB.Save(&node)
|
||||
|
||||
err = db.DB.Delete(key).Error
|
||||
require.ErrorContains(t, err, "constraint failed: FOREIGN KEY constraint failed")
|
||||
}
|
||||
|
||||
@@ -417,10 +417,10 @@ func SaveNodeRoutes(tx *gorm.DB, node *types.Node) (bool, error) {
|
||||
return sendUpdate, nil
|
||||
}
|
||||
|
||||
// FailoverNodeRoutesIfNeccessary takes a node and checks if the node's route
|
||||
// FailoverNodeRoutesIfNecessary takes a node and checks if the node's route
|
||||
// need to be failed over to another host.
|
||||
// If needed, the failover will be attempted.
|
||||
func FailoverNodeRoutesIfNeccessary(
|
||||
func FailoverNodeRoutesIfNecessary(
|
||||
tx *gorm.DB,
|
||||
isLikelyConnected *xsync.MapOf[types.NodeID, bool],
|
||||
node *types.Node,
|
||||
@@ -473,7 +473,7 @@ nodeRouteLoop:
|
||||
return &types.StateUpdate{
|
||||
Type: types.StatePeerChanged,
|
||||
ChangeNodes: chng,
|
||||
Message: "called from db.FailoverNodeRoutesIfNeccessary",
|
||||
Message: "called from db.FailoverNodeRoutesIfNecessary",
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ var mp = func(p string) netip.Prefix {
|
||||
}
|
||||
|
||||
func (s *Suite) TestGetRoutes(c *check.C) {
|
||||
user, err := db.CreateUser("test")
|
||||
user, err := db.CreateUser(types.User{Name: "test"})
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||
@@ -76,7 +76,7 @@ func (s *Suite) TestGetRoutes(c *check.C) {
|
||||
}
|
||||
|
||||
func (s *Suite) TestGetEnableRoutes(c *check.C) {
|
||||
user, err := db.CreateUser("test")
|
||||
user, err := db.CreateUser(types.User{Name: "test"})
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||
@@ -150,7 +150,7 @@ func (s *Suite) TestGetEnableRoutes(c *check.C) {
|
||||
}
|
||||
|
||||
func (s *Suite) TestIsUniquePrefix(c *check.C) {
|
||||
user, err := db.CreateUser("test")
|
||||
user, err := db.CreateUser(types.User{Name: "test"})
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||
@@ -231,7 +231,7 @@ func (s *Suite) TestIsUniquePrefix(c *check.C) {
|
||||
}
|
||||
|
||||
func (s *Suite) TestDeleteRoutes(c *check.C) {
|
||||
user, err := db.CreateUser("test")
|
||||
user, err := db.CreateUser(types.User{Name: "test"})
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||
@@ -342,7 +342,7 @@ func dbForTest(t *testing.T, testName string) *HSDatabase {
|
||||
return db
|
||||
}
|
||||
|
||||
func TestFailoverNodeRoutesIfNeccessary(t *testing.T) {
|
||||
func TestFailoverNodeRoutesIfNecessary(t *testing.T) {
|
||||
su := func(nids ...types.NodeID) *types.StateUpdate {
|
||||
return &types.StateUpdate{
|
||||
ChangeNodes: nids,
|
||||
@@ -648,7 +648,7 @@ func TestFailoverNodeRoutesIfNeccessary(t *testing.T) {
|
||||
want := tt.want[step]
|
||||
|
||||
got, err := Write(db.DB, func(tx *gorm.DB) (*types.StateUpdate, error) {
|
||||
return FailoverNodeRoutesIfNeccessary(tx, smap(isConnected), node)
|
||||
return FailoverNodeRoutesIfNecessary(tx, smap(isConnected), node)
|
||||
})
|
||||
|
||||
if (err != nil) != tt.wantErr {
|
||||
|
||||
@@ -78,13 +78,11 @@ func newSQLiteTestDB() (*HSDatabase, error) {
|
||||
func newPostgresTestDB(t *testing.T) *HSDatabase {
|
||||
t.Helper()
|
||||
|
||||
var err error
|
||||
tmpDir, err = os.MkdirTemp("", "headscale-db-test-*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return newHeadscaleDBFromPostgresURL(t, newPostgresDBForTest(t))
|
||||
}
|
||||
|
||||
log.Printf("database path: %s", tmpDir+"/headscale_test.db")
|
||||
func newPostgresDBForTest(t *testing.T) *url.URL {
|
||||
t.Helper()
|
||||
|
||||
ctx := context.Background()
|
||||
srv, err := postgrestest.Start(ctx)
|
||||
@@ -100,10 +98,16 @@ func newPostgresTestDB(t *testing.T) *HSDatabase {
|
||||
t.Logf("created local postgres: %s", u)
|
||||
pu, _ := url.Parse(u)
|
||||
|
||||
return pu
|
||||
}
|
||||
|
||||
func newHeadscaleDBFromPostgresURL(t *testing.T, pu *url.URL) *HSDatabase {
|
||||
t.Helper()
|
||||
|
||||
pass, _ := pu.User.Password()
|
||||
port, _ := strconv.Atoi(pu.Port())
|
||||
|
||||
db, err = NewHeadscaleDatabase(
|
||||
db, err := NewHeadscaleDatabase(
|
||||
types.DatabaseConfig{
|
||||
Type: types.DatabasePostgres,
|
||||
Postgres: types.PostgresConfig{
|
||||
|
||||
BIN
hscontrol/db/testdata/failing-node-preauth-constraint.sqlite
vendored
Normal file
BIN
hscontrol/db/testdata/failing-node-preauth-constraint.sqlite
vendored
Normal file
Binary file not shown.
BIN
hscontrol/db/testdata/pre-24-postgresdb.pssql.dump
vendored
Normal file
BIN
hscontrol/db/testdata/pre-24-postgresdb.pssql.dump
vendored
Normal file
Binary file not shown.
@@ -15,22 +15,19 @@ var (
|
||||
ErrUserStillHasNodes = errors.New("user not empty: node(s) found")
|
||||
)
|
||||
|
||||
func (hsdb *HSDatabase) CreateUser(name string) (*types.User, error) {
|
||||
func (hsdb *HSDatabase) CreateUser(user types.User) (*types.User, error) {
|
||||
return Write(hsdb.DB, func(tx *gorm.DB) (*types.User, error) {
|
||||
return CreateUser(tx, name)
|
||||
return CreateUser(tx, user)
|
||||
})
|
||||
}
|
||||
|
||||
// CreateUser creates a new User. Returns error if could not be created
|
||||
// or another user already exists.
|
||||
func CreateUser(tx *gorm.DB, name string) (*types.User, error) {
|
||||
err := util.CheckForFQDNRules(name)
|
||||
func CreateUser(tx *gorm.DB, user types.User) (*types.User, error) {
|
||||
err := util.ValidateUsername(user.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user := types.User{
|
||||
Name: name,
|
||||
}
|
||||
if err := tx.Create(&user).Error; err != nil {
|
||||
return nil, fmt.Errorf("creating user: %w", err)
|
||||
}
|
||||
@@ -84,6 +81,8 @@ func (hsdb *HSDatabase) RenameUser(uid types.UserID, newName string) error {
|
||||
})
|
||||
}
|
||||
|
||||
var ErrCannotChangeOIDCUser = errors.New("cannot edit OIDC user")
|
||||
|
||||
// RenameUser renames a User. Returns error if the User does
|
||||
// not exist or if another User exists with the new name.
|
||||
func RenameUser(tx *gorm.DB, uid types.UserID, newName string) error {
|
||||
@@ -92,11 +91,15 @@ func RenameUser(tx *gorm.DB, uid types.UserID, newName string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = util.CheckForFQDNRules(newName)
|
||||
err = util.ValidateUsername(newName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if oldUser.Provider == util.RegisterMethodOIDC {
|
||||
return ErrCannotChangeOIDCUser
|
||||
}
|
||||
|
||||
oldUser.Name = newName
|
||||
|
||||
if err := tx.Save(&oldUser).Error; err != nil {
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
func (s *Suite) TestCreateAndDestroyUser(c *check.C) {
|
||||
user, err := db.CreateUser("test")
|
||||
user, err := db.CreateUser(types.User{Name: "test"})
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(user.Name, check.Equals, "test")
|
||||
|
||||
@@ -30,7 +30,7 @@ func (s *Suite) TestDestroyUserErrors(c *check.C) {
|
||||
err := db.DestroyUser(9998)
|
||||
c.Assert(err, check.Equals, ErrUserNotFound)
|
||||
|
||||
user, err := db.CreateUser("test")
|
||||
user, err := db.CreateUser(types.User{Name: "test"})
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||
@@ -43,7 +43,7 @@ func (s *Suite) TestDestroyUserErrors(c *check.C) {
|
||||
// destroying a user also deletes all associated preauthkeys
|
||||
c.Assert(result.Error, check.Equals, gorm.ErrRecordNotFound)
|
||||
|
||||
user, err = db.CreateUser("test")
|
||||
user, err = db.CreateUser(types.User{Name: "test"})
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak, err = db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil)
|
||||
@@ -64,7 +64,7 @@ func (s *Suite) TestDestroyUserErrors(c *check.C) {
|
||||
}
|
||||
|
||||
func (s *Suite) TestRenameUser(c *check.C) {
|
||||
userTest, err := db.CreateUser("test")
|
||||
userTest, err := db.CreateUser(types.User{Name: "test"})
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(userTest.Name, check.Equals, "test")
|
||||
|
||||
@@ -86,7 +86,7 @@ func (s *Suite) TestRenameUser(c *check.C) {
|
||||
err = db.RenameUser(99988, "test")
|
||||
c.Assert(err, check.Equals, ErrUserNotFound)
|
||||
|
||||
userTest2, err := db.CreateUser("test2")
|
||||
userTest2, err := db.CreateUser(types.User{Name: "test2"})
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(userTest2.Name, check.Equals, "test2")
|
||||
|
||||
@@ -98,10 +98,10 @@ func (s *Suite) TestRenameUser(c *check.C) {
|
||||
}
|
||||
|
||||
func (s *Suite) TestSetMachineUser(c *check.C) {
|
||||
oldUser, err := db.CreateUser("old")
|
||||
oldUser, err := db.CreateUser(types.User{Name: "old"})
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
newUser, err := db.CreateUser("new")
|
||||
newUser, err := db.CreateUser(types.User{Name: "new"})
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak, err := db.CreatePreAuthKey(types.UserID(oldUser.ID), false, false, nil, nil)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/rs/zerolog/log"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -83,12 +84,39 @@ func (e *ExtraRecordsMan) Run() {
|
||||
log.Error().Caller().Msgf("file watcher event channel closing")
|
||||
return
|
||||
}
|
||||
switch event.Op {
|
||||
case fsnotify.Create, fsnotify.Write, fsnotify.Chmod:
|
||||
log.Trace().Caller().Str("path", event.Name).Str("op", event.Op.String()).Msg("extra records received filewatch event")
|
||||
if event.Name != e.path {
|
||||
continue
|
||||
}
|
||||
e.updateRecords()
|
||||
|
||||
log.Trace().Caller().Str("path", event.Name).Str("op", event.Op.String()).Msg("extra records received filewatch event")
|
||||
if event.Name != e.path {
|
||||
continue
|
||||
// If a file is removed or renamed, fsnotify will loose track of it
|
||||
// and not watch it. We will therefore attempt to re-add it with a backoff.
|
||||
case fsnotify.Remove, fsnotify.Rename:
|
||||
err := backoff.Retry(func() error {
|
||||
if _, err := os.Stat(e.path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}, backoff.NewExponentialBackOff())
|
||||
|
||||
if err != nil {
|
||||
log.Error().Caller().Err(err).Msgf("extra records filewatcher retrying to find file after delete")
|
||||
continue
|
||||
}
|
||||
|
||||
err = e.watcher.Add(e.path)
|
||||
if err != nil {
|
||||
log.Error().Caller().Err(err).Msgf("extra records filewatcher re-adding file after delete failed, giving up.")
|
||||
return
|
||||
} else {
|
||||
log.Trace().Caller().Str("path", e.path).Msg("extra records file re-added after delete")
|
||||
e.updateRecords()
|
||||
}
|
||||
}
|
||||
e.updateRecords()
|
||||
|
||||
case err, ok := <-e.watcher.Errors:
|
||||
if !ok {
|
||||
@@ -116,6 +144,11 @@ func (e *ExtraRecordsMan) updateRecords() {
|
||||
return
|
||||
}
|
||||
|
||||
// If there are no records, ignore the update.
|
||||
if records == nil {
|
||||
return
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
@@ -143,6 +176,12 @@ func readExtraRecordsFromPath(path string) ([]tailcfg.DNSRecord, [32]byte, error
|
||||
return nil, [32]byte{}, fmt.Errorf("reading path: %s, err: %w", path, err)
|
||||
}
|
||||
|
||||
// If the read was triggered too fast, and the file is not complete, ignore the update
|
||||
// if the file is empty. A consecutive update will be triggered when the file is complete.
|
||||
if len(b) == 0 {
|
||||
return nil, [32]byte{}, nil
|
||||
}
|
||||
|
||||
var records []tailcfg.DNSRecord
|
||||
err = json.Unmarshal(b, &records)
|
||||
if err != nil {
|
||||
|
||||
@@ -11,7 +11,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/puzpuzpuz/xsync/v3"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/samber/lo"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
@@ -21,6 +23,7 @@ import (
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/db"
|
||||
"github.com/juanfont/headscale/hscontrol/policy"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
)
|
||||
@@ -40,7 +43,13 @@ func (api headscaleV1APIServer) CreateUser(
|
||||
ctx context.Context,
|
||||
request *v1.CreateUserRequest,
|
||||
) (*v1.CreateUserResponse, error) {
|
||||
user, err := api.h.db.CreateUser(request.GetName())
|
||||
newUser := types.User{
|
||||
Name: request.GetName(),
|
||||
DisplayName: request.GetDisplayName(),
|
||||
Email: request.GetEmail(),
|
||||
ProfilePicURL: request.GetPictureUrl(),
|
||||
}
|
||||
user, err := api.h.db.CreateUser(newUser)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -172,11 +181,15 @@ func (api headscaleV1APIServer) ExpirePreAuthKey(
|
||||
request *v1.ExpirePreAuthKeyRequest,
|
||||
) (*v1.ExpirePreAuthKeyResponse, error) {
|
||||
err := api.h.db.Write(func(tx *gorm.DB) error {
|
||||
preAuthKey, err := db.GetPreAuthKey(tx, request.GetUser(), request.Key)
|
||||
preAuthKey, err := db.GetPreAuthKey(tx, request.Key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if preAuthKey.User.Name != request.GetUser() {
|
||||
return fmt.Errorf("preauth key does not belong to user")
|
||||
}
|
||||
|
||||
return db.ExpirePreAuthKey(tx, preAuthKey)
|
||||
})
|
||||
if err != nil {
|
||||
@@ -218,11 +231,10 @@ func (api headscaleV1APIServer) RegisterNode(
|
||||
) (*v1.RegisterNodeResponse, error) {
|
||||
log.Trace().
|
||||
Str("user", request.GetUser()).
|
||||
Str("machine_key", request.GetKey()).
|
||||
Str("registration_id", request.GetKey()).
|
||||
Msg("Registering node")
|
||||
|
||||
var mkey key.MachinePublic
|
||||
err := mkey.UnmarshalText([]byte(request.GetKey()))
|
||||
registrationId, err := types.RegistrationIDFromString(request.GetKey())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -237,8 +249,8 @@ func (api headscaleV1APIServer) RegisterNode(
|
||||
return nil, fmt.Errorf("looking up user: %w", err)
|
||||
}
|
||||
|
||||
node, err := api.h.db.RegisterNodeFromAuthCallback(
|
||||
mkey,
|
||||
node, _, err := api.h.db.HandleNodeFromAuthPath(
|
||||
registrationId,
|
||||
types.UserID(user.ID),
|
||||
nil,
|
||||
util.RegisterMethodCLI,
|
||||
@@ -248,10 +260,17 @@ func (api headscaleV1APIServer) RegisterNode(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = nodesChangedHook(api.h.db, api.h.polMan, api.h.nodeNotifier)
|
||||
updateSent, err := nodesChangedHook(api.h.db, api.h.polMan, api.h.nodeNotifier)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("updating resources using node: %w", err)
|
||||
}
|
||||
if !updateSent {
|
||||
ctx = types.NotifyCtx(context.Background(), "web-node-login", node.Hostname)
|
||||
api.h.nodeNotifier.NotifyAll(ctx, types.StateUpdate{
|
||||
Type: types.StatePeerChanged,
|
||||
ChangeNodes: []types.NodeID{node.ID},
|
||||
})
|
||||
}
|
||||
|
||||
return &v1.RegisterNodeResponse{Node: node.Proto()}, nil
|
||||
}
|
||||
@@ -457,19 +476,7 @@ func (api headscaleV1APIServer) ListNodes(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := make([]*v1.Node, len(nodes))
|
||||
for index, node := range nodes {
|
||||
resp := node.Proto()
|
||||
|
||||
// Populate the online field based on
|
||||
// currently connected nodes.
|
||||
if val, ok := isLikelyConnected.Load(node.ID); ok && val {
|
||||
resp.Online = true
|
||||
}
|
||||
|
||||
response[index] = resp
|
||||
}
|
||||
|
||||
response := nodesToProto(api.h.polMan, isLikelyConnected, nodes)
|
||||
return &v1.ListNodesResponse{Nodes: response}, nil
|
||||
}
|
||||
|
||||
@@ -482,6 +489,11 @@ func (api headscaleV1APIServer) ListNodes(
|
||||
return nodes[i].ID < nodes[j].ID
|
||||
})
|
||||
|
||||
response := nodesToProto(api.h.polMan, isLikelyConnected, nodes)
|
||||
return &v1.ListNodesResponse{Nodes: response}, nil
|
||||
}
|
||||
|
||||
func nodesToProto(polMan policy.PolicyManager, isLikelyConnected *xsync.MapOf[types.NodeID, bool], nodes types.Nodes) []*v1.Node {
|
||||
response := make([]*v1.Node, len(nodes))
|
||||
for index, node := range nodes {
|
||||
resp := node.Proto()
|
||||
@@ -492,12 +504,12 @@ func (api headscaleV1APIServer) ListNodes(
|
||||
resp.Online = true
|
||||
}
|
||||
|
||||
validTags := api.h.polMan.Tags(node)
|
||||
resp.ValidTags = validTags
|
||||
tags := polMan.Tags(node)
|
||||
resp.ValidTags = lo.Uniq(append(tags, node.ForcedTags...))
|
||||
response[index] = resp
|
||||
}
|
||||
|
||||
return &v1.ListNodesResponse{Nodes: response}, nil
|
||||
return response
|
||||
}
|
||||
|
||||
func (api headscaleV1APIServer) MoveNode(
|
||||
@@ -837,36 +849,36 @@ func (api headscaleV1APIServer) DebugCreateNode(
|
||||
Hostname: "DebugTestNode",
|
||||
}
|
||||
|
||||
var mkey key.MachinePublic
|
||||
err = mkey.UnmarshalText([]byte(request.GetKey()))
|
||||
registrationId, err := types.RegistrationIDFromString(request.GetKey())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nodeKey := key.NewNode()
|
||||
newNode := types.RegisterNode{
|
||||
Node: types.Node{
|
||||
NodeKey: key.NewNode().Public(),
|
||||
MachineKey: key.NewMachine().Public(),
|
||||
Hostname: request.GetName(),
|
||||
User: *user,
|
||||
|
||||
newNode := types.Node{
|
||||
MachineKey: mkey,
|
||||
NodeKey: nodeKey.Public(),
|
||||
Hostname: request.GetName(),
|
||||
User: *user,
|
||||
Expiry: &time.Time{},
|
||||
LastSeen: &time.Time{},
|
||||
|
||||
Expiry: &time.Time{},
|
||||
LastSeen: &time.Time{},
|
||||
|
||||
Hostinfo: &hostinfo,
|
||||
Hostinfo: &hostinfo,
|
||||
},
|
||||
Registered: make(chan struct{}),
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Str("machine_key", mkey.ShortString()).
|
||||
Str("registration_id", registrationId.String()).
|
||||
Msg("adding debug machine via CLI, appending to registration cache")
|
||||
|
||||
api.h.registrationCache.Set(
|
||||
mkey.String(),
|
||||
registrationId,
|
||||
newNode,
|
||||
)
|
||||
|
||||
return &v1.DebugCreateNodeResponse{Node: newNode.Proto()}, nil
|
||||
return &v1.DebugCreateNodeResponse{Node: newNode.Node.Proto()}, nil
|
||||
}
|
||||
|
||||
func (api headscaleV1APIServer) mustEmbedUnimplementedHeadscaleServiceServer() {}
|
||||
|
||||
@@ -8,16 +8,13 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/chasefleming/elem-go"
|
||||
"github.com/chasefleming/elem-go/attrs"
|
||||
"github.com/chasefleming/elem-go/styles"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/juanfont/headscale/hscontrol/templates"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/rs/zerolog/log"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -32,11 +29,38 @@ const (
|
||||
// See also https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go
|
||||
NoiseCapabilityVersion = 39
|
||||
|
||||
// TODO(juan): remove this once https://github.com/juanfont/headscale/issues/727 is fixed.
|
||||
registrationHoldoff = time.Second * 5
|
||||
reservedResponseHeaderSize = 4
|
||||
)
|
||||
|
||||
// httpError logs an error and sends an HTTP error response with the given
|
||||
func httpError(w http.ResponseWriter, err error) {
|
||||
var herr HTTPError
|
||||
if errors.As(err, &herr) {
|
||||
http.Error(w, herr.Msg, herr.Code)
|
||||
log.Error().Err(herr.Err).Int("code", herr.Code).Msgf("user msg: %s", herr.Msg)
|
||||
} else {
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
log.Error().Err(err).Int("code", http.StatusInternalServerError).Msg("http internal server error")
|
||||
}
|
||||
}
|
||||
|
||||
// HTTPError represents an error that is surfaced to the user via web.
|
||||
type HTTPError struct {
|
||||
Code int // HTTP response code to send to client; 0 means 500
|
||||
Msg string // Response body to send to client
|
||||
Err error // Detailed error to log on the server
|
||||
}
|
||||
|
||||
func (e HTTPError) Error() string { return fmt.Sprintf("http error[%d]: %s, %s", e.Code, e.Msg, e.Err) }
|
||||
func (e HTTPError) Unwrap() error { return e.Err }
|
||||
|
||||
// Error returns an HTTPError containing the given information.
|
||||
func NewHTTPError(code int, msg string, err error) HTTPError {
|
||||
return HTTPError{Code: code, Msg: msg, Err: err}
|
||||
}
|
||||
|
||||
var errMethodNotAllowed = NewHTTPError(http.StatusMethodNotAllowed, "method not allowed", nil)
|
||||
|
||||
var ErrRegisterMethodCLIDoesNotSupportExpire = errors.New(
|
||||
"machines registered with CLI does not support expire",
|
||||
)
|
||||
@@ -46,18 +70,18 @@ func parseCabailityVersion(req *http.Request) (tailcfg.CapabilityVersion, error)
|
||||
clientCapabilityStr := req.URL.Query().Get("v")
|
||||
|
||||
if clientCapabilityStr == "" {
|
||||
return 0, ErrNoCapabilityVersion
|
||||
return 0, NewHTTPError(http.StatusBadRequest, "capability version must be set", nil)
|
||||
}
|
||||
|
||||
clientCapabilityVersion, err := strconv.Atoi(clientCapabilityStr)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to parse capability version: %w", err)
|
||||
return 0, NewHTTPError(http.StatusBadRequest, "invalid capability version", fmt.Errorf("failed to parse capability version: %w", err))
|
||||
}
|
||||
|
||||
return tailcfg.CapabilityVersion(clientCapabilityVersion), nil
|
||||
}
|
||||
|
||||
func (h *Headscale) handleVerifyRequest(
|
||||
func (h *Headscale) derpRequestIsAllowed(
|
||||
req *http.Request,
|
||||
) (bool, error) {
|
||||
body, err := io.ReadAll(req.Body)
|
||||
@@ -84,21 +108,14 @@ func (h *Headscale) VerifyHandler(
|
||||
req *http.Request,
|
||||
) {
|
||||
if req.Method != http.MethodPost {
|
||||
http.Error(writer, "Wrong method", http.StatusMethodNotAllowed)
|
||||
|
||||
httpError(writer, errMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
log.Debug().
|
||||
Str("handler", "/verify").
|
||||
Msg("verify client")
|
||||
|
||||
allow, err := h.handleVerifyRequest(req)
|
||||
allow, err := h.derpRequestIsAllowed(req)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Failed to verify client")
|
||||
http.Error(writer, "Internal error", http.StatusInternalServerError)
|
||||
httpError(writer, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := tailcfg.DERPAdmitClientResponse{
|
||||
@@ -106,14 +123,7 @@ func (h *Headscale) VerifyHandler(
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
json.NewEncoder(writer).Encode(resp)
|
||||
}
|
||||
|
||||
// KeyHandler provides the Headscale pub key
|
||||
@@ -125,35 +135,17 @@ func (h *Headscale) KeyHandler(
|
||||
// New Tailscale clients send a 'v' parameter to indicate the CurrentCapabilityVersion
|
||||
capVer, err := parseCabailityVersion(req)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("could not get capability version")
|
||||
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
httpError(writer, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Str("handler", "/key").
|
||||
Int("cap_ver", int(capVer)).
|
||||
Msg("New noise client")
|
||||
|
||||
// TS2021 (Tailscale v2 protocol) requires to have a different key
|
||||
if capVer >= NoiseCapabilityVersion {
|
||||
resp := tailcfg.OverTLSPublicKeyResponse{
|
||||
PublicKey: h.noisePrivateKey.Public(),
|
||||
}
|
||||
writer.Header().Set("Content-Type", "application/json")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
err = json.NewEncoder(writer).Encode(resp)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Failed to write response")
|
||||
}
|
||||
json.NewEncoder(writer).Encode(resp)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -174,18 +166,10 @@ func (h *Headscale) HealthHandler(
|
||||
|
||||
if err != nil {
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
log.Error().Caller().Err(err).Msg("health check failed")
|
||||
res.Status = "fail"
|
||||
}
|
||||
|
||||
buf, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
log.Error().Caller().Err(err).Msg("marshal failed")
|
||||
}
|
||||
_, err = writer.Write(buf)
|
||||
if err != nil {
|
||||
log.Error().Caller().Err(err).Msg("write failed")
|
||||
}
|
||||
json.NewEncoder(writer).Encode(res)
|
||||
}
|
||||
|
||||
if err := h.db.PingDB(req.Context()); err != nil {
|
||||
@@ -204,31 +188,6 @@ var codeStyleRegisterWebAPI = styles.Props{
|
||||
styles.BackgroundColor: "#eee",
|
||||
}
|
||||
|
||||
func registerWebHTML(key string) *elem.Element {
|
||||
return elem.Html(nil,
|
||||
elem.Head(
|
||||
nil,
|
||||
elem.Title(nil, elem.Text("Registration - Headscale")),
|
||||
elem.Meta(attrs.Props{
|
||||
attrs.Name: "viewport",
|
||||
attrs.Content: "width=device-width, initial-scale=1",
|
||||
}),
|
||||
),
|
||||
elem.Body(attrs.Props{
|
||||
attrs.Style: styles.Props{
|
||||
styles.FontFamily: "sans",
|
||||
}.ToInline(),
|
||||
},
|
||||
elem.H1(nil, elem.Text("headscale")),
|
||||
elem.H2(nil, elem.Text("Machine registration")),
|
||||
elem.P(nil, elem.Text("Run the command below in the headscale server to add this machine to your network:")),
|
||||
elem.Code(attrs.Props{attrs.Style: codeStyleRegisterWebAPI.ToInline()},
|
||||
elem.Text(fmt.Sprintf("headscale nodes register --user USERNAME --key %s", key)),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
type AuthProviderWeb struct {
|
||||
serverURL string
|
||||
}
|
||||
@@ -239,15 +198,15 @@ func NewAuthProviderWeb(serverURL string) *AuthProviderWeb {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AuthProviderWeb) AuthURL(mKey key.MachinePublic) string {
|
||||
func (a *AuthProviderWeb) AuthURL(registrationId types.RegistrationID) string {
|
||||
return fmt.Sprintf(
|
||||
"%s/register/%s",
|
||||
strings.TrimSuffix(a.serverURL, "/"),
|
||||
mKey.String())
|
||||
registrationId.String())
|
||||
}
|
||||
|
||||
// RegisterWebAPI shows a simple message in the browser to point to the CLI
|
||||
// Listens in /register/:nkey.
|
||||
// Listens in /register/:registration_id.
|
||||
//
|
||||
// This is not part of the Tailscale control API, as we could send whatever URL
|
||||
// in the RegisterResponse.AuthURL field.
|
||||
@@ -256,39 +215,18 @@ func (a *AuthProviderWeb) RegisterHandler(
|
||||
req *http.Request,
|
||||
) {
|
||||
vars := mux.Vars(req)
|
||||
machineKeyStr := vars["mkey"]
|
||||
registrationIdStr := vars["registration_id"]
|
||||
|
||||
// We need to make sure we dont open for XSS style injections, if the parameter that
|
||||
// is passed as a key is not parsable/validated as a NodePublic key, then fail to render
|
||||
// the template and log an error.
|
||||
var machineKey key.MachinePublic
|
||||
err := machineKey.UnmarshalText(
|
||||
[]byte(machineKeyStr),
|
||||
)
|
||||
registrationId, err := types.RegistrationIDFromString(registrationIdStr)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to parse incoming machinekey")
|
||||
|
||||
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
_, err := writer.Write([]byte("Wrong params"))
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Failed to write response")
|
||||
}
|
||||
|
||||
httpError(writer, NewHTTPError(http.StatusBadRequest, "invalid registration id", err))
|
||||
return
|
||||
}
|
||||
|
||||
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
if _, err := writer.Write([]byte(registerWebHTML(machineKey.String()).Render())); err != nil {
|
||||
if _, err := writer.Write([]byte(templates.RegisterWeb(machineKey.String()).Render())); err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Failed to write response")
|
||||
}
|
||||
}
|
||||
writer.Write([]byte(templates.RegisterWeb(registrationId).Render()))
|
||||
}
|
||||
|
||||
@@ -105,8 +105,7 @@ func generateUserProfiles(
|
||||
|
||||
var profiles []tailcfg.UserProfile
|
||||
for _, user := range userMap {
|
||||
profiles = append(profiles,
|
||||
user.TailscaleUserProfile())
|
||||
profiles = append(profiles, user.TailscaleUserProfile())
|
||||
}
|
||||
|
||||
return profiles
|
||||
@@ -455,7 +454,7 @@ func (m *Mapper) baseWithConfigMapResponse(
|
||||
|
||||
resp.DERPMap = m.derpMap
|
||||
|
||||
resp.Domain = m.cfg.BaseDomain
|
||||
resp.Domain = m.cfg.Domain()
|
||||
|
||||
// Do not instruct clients to collect services we do not
|
||||
// support or do anything with them
|
||||
|
||||
@@ -227,7 +227,8 @@ func Test_fullMapResponse(t *testing.T) {
|
||||
tsaddr.AllIPv4(),
|
||||
netip.MustParsePrefix("192.168.0.0/24"),
|
||||
},
|
||||
DERP: "127.3.3.40:0",
|
||||
HomeDERP: 0,
|
||||
LegacyDERPString: "127.3.3.40:0",
|
||||
Hostinfo: hiview(tailcfg.Hostinfo{}),
|
||||
Created: created,
|
||||
Tags: []string{},
|
||||
@@ -282,7 +283,8 @@ func Test_fullMapResponse(t *testing.T) {
|
||||
),
|
||||
Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.2/32")},
|
||||
AllowedIPs: []netip.Prefix{netip.MustParsePrefix("100.64.0.2/32")},
|
||||
DERP: "127.3.3.40:0",
|
||||
HomeDERP: 0,
|
||||
LegacyDERPString: "127.3.3.40:0",
|
||||
Hostinfo: hiview(tailcfg.Hostinfo{}),
|
||||
Created: created,
|
||||
Tags: []string{},
|
||||
|
||||
@@ -62,11 +62,16 @@ func tailNode(
|
||||
}
|
||||
}
|
||||
|
||||
var derp string
|
||||
var derp int
|
||||
|
||||
// TODO(kradalby): legacyDERP was removed in tailscale/tailscale@2fc4455e6dd9ab7f879d4e2f7cffc2be81f14077
|
||||
// and should be removed after 111 is the minimum capver.
|
||||
var legacyDERP string
|
||||
if node.Hostinfo != nil && node.Hostinfo.NetInfo != nil {
|
||||
derp = fmt.Sprintf("127.3.3.40:%d", node.Hostinfo.NetInfo.PreferredDERP)
|
||||
legacyDERP = fmt.Sprintf("127.3.3.40:%d", node.Hostinfo.NetInfo.PreferredDERP)
|
||||
derp = node.Hostinfo.NetInfo.PreferredDERP
|
||||
} else {
|
||||
derp = "127.3.3.40:0" // Zero means disconnected or unknown.
|
||||
legacyDERP = "127.3.3.40:0" // Zero means disconnected or unknown.
|
||||
}
|
||||
|
||||
var keyExpiry time.Time
|
||||
@@ -95,14 +100,15 @@ func tailNode(
|
||||
Key: node.NodeKey,
|
||||
KeyExpiry: keyExpiry.UTC(),
|
||||
|
||||
Machine: node.MachineKey,
|
||||
DiscoKey: node.DiscoKey,
|
||||
Addresses: addrs,
|
||||
AllowedIPs: allowedIPs,
|
||||
Endpoints: node.Endpoints,
|
||||
DERP: derp,
|
||||
Hostinfo: node.Hostinfo.View(),
|
||||
Created: node.CreatedAt.UTC(),
|
||||
Machine: node.MachineKey,
|
||||
DiscoKey: node.DiscoKey,
|
||||
Addresses: addrs,
|
||||
AllowedIPs: allowedIPs,
|
||||
Endpoints: node.Endpoints,
|
||||
HomeDERP: derp,
|
||||
LegacyDERPString: legacyDERP,
|
||||
Hostinfo: node.Hostinfo.View(),
|
||||
Created: node.CreatedAt.UTC(),
|
||||
|
||||
Online: node.IsOnline,
|
||||
|
||||
|
||||
@@ -68,7 +68,8 @@ func TestTailNode(t *testing.T) {
|
||||
StableID: "0",
|
||||
Addresses: []netip.Prefix{},
|
||||
AllowedIPs: []netip.Prefix{},
|
||||
DERP: "127.3.3.40:0",
|
||||
HomeDERP: 0,
|
||||
LegacyDERPString: "127.3.3.40:0",
|
||||
Hostinfo: hiview(tailcfg.Hostinfo{}),
|
||||
Tags: []string{},
|
||||
PrimaryRoutes: []netip.Prefix{},
|
||||
@@ -156,9 +157,10 @@ func TestTailNode(t *testing.T) {
|
||||
tsaddr.AllIPv4(),
|
||||
netip.MustParsePrefix("192.168.0.0/24"),
|
||||
},
|
||||
DERP: "127.3.3.40:0",
|
||||
Hostinfo: hiview(tailcfg.Hostinfo{}),
|
||||
Created: created,
|
||||
HomeDERP: 0,
|
||||
LegacyDERPString: "127.3.3.40:0",
|
||||
Hostinfo: hiview(tailcfg.Hostinfo{}),
|
||||
Created: created,
|
||||
|
||||
Tags: []string{},
|
||||
|
||||
|
||||
@@ -3,15 +3,19 @@ package hscontrol
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/juanfont/headscale/hscontrol/capver"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/net/http2"
|
||||
"gorm.io/gorm"
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/control/controlhttp"
|
||||
"tailscale.com/control/controlhttp/controlhttpserver"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
@@ -71,7 +75,7 @@ func (h *Headscale) NoiseUpgradeHandler(
|
||||
challenge: key.NewChallenge(),
|
||||
}
|
||||
|
||||
noiseConn, err := controlhttp.AcceptHTTP(
|
||||
noiseConn, err := controlhttpserver.AcceptHTTP(
|
||||
req.Context(),
|
||||
writer,
|
||||
req,
|
||||
@@ -79,9 +83,7 @@ func (h *Headscale) NoiseUpgradeHandler(
|
||||
noiseServer.earlyNoise,
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("noise upgrade failed")
|
||||
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
httpError(writer, fmt.Errorf("noise upgrade failed: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -115,18 +117,8 @@ func (h *Headscale) NoiseUpgradeHandler(
|
||||
}
|
||||
|
||||
func (ns *noiseServer) earlyNoise(protocolVersion int, writer io.Writer) error {
|
||||
log.Trace().
|
||||
Caller().
|
||||
Int("protocol_version", protocolVersion).
|
||||
Str("challenge", ns.challenge.Public().String()).
|
||||
Msg("earlyNoise called")
|
||||
|
||||
if protocolVersion < earlyNoiseCapabilityVersion {
|
||||
log.Trace().
|
||||
Caller().
|
||||
Msgf("protocol version %d does not support early noise", protocolVersion)
|
||||
|
||||
return nil
|
||||
if !isSupportedVersion(tailcfg.CapabilityVersion(protocolVersion)) {
|
||||
return fmt.Errorf("unsupported client version: %d", protocolVersion)
|
||||
}
|
||||
|
||||
earlyJSON, err := json.Marshal(&tailcfg.EarlyNoise{
|
||||
@@ -158,9 +150,34 @@ func (ns *noiseServer) earlyNoise(protocolVersion int, writer io.Writer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
MinimumCapVersion tailcfg.CapabilityVersion = 82
|
||||
)
|
||||
func isSupportedVersion(version tailcfg.CapabilityVersion) bool {
|
||||
return version >= capver.MinSupportedCapabilityVersion
|
||||
}
|
||||
|
||||
func rejectUnsupported(
|
||||
writer http.ResponseWriter,
|
||||
version tailcfg.CapabilityVersion,
|
||||
mkey key.MachinePublic,
|
||||
nkey key.NodePublic,
|
||||
) bool {
|
||||
// Reject unsupported versions
|
||||
if !isSupportedVersion(version) {
|
||||
log.Error().
|
||||
Caller().
|
||||
Int("minimum_cap_ver", int(capver.MinSupportedCapabilityVersion)).
|
||||
Int("client_cap_ver", int(version)).
|
||||
Str("minimum_version", capver.TailscaleVersion(capver.MinSupportedCapabilityVersion)).
|
||||
Str("client_version", capver.TailscaleVersion(version)).
|
||||
Str("node_key", nkey.ShortString()).
|
||||
Str("machine_key", mkey.ShortString()).
|
||||
Msg("unsupported client connected")
|
||||
http.Error(writer, "unsupported client version", http.StatusBadRequest)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// NoisePollNetMapHandler takes care of /machine/:id/map using the Noise protocol
|
||||
//
|
||||
@@ -177,50 +194,26 @@ func (ns *noiseServer) NoisePollNetMapHandler(
|
||||
) {
|
||||
body, _ := io.ReadAll(req.Body)
|
||||
|
||||
mapRequest := tailcfg.MapRequest{}
|
||||
var mapRequest tailcfg.MapRequest
|
||||
if err := json.Unmarshal(body, &mapRequest); err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Cannot parse MapRequest")
|
||||
http.Error(writer, "Internal error", http.StatusInternalServerError)
|
||||
|
||||
httpError(writer, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace().
|
||||
Caller().
|
||||
Str("handler", "NoisePollNetMap").
|
||||
Any("headers", req.Header).
|
||||
Str("node", mapRequest.Hostinfo.Hostname).
|
||||
Int("capver", int(mapRequest.Version)).
|
||||
Msg("PollNetMapHandler called")
|
||||
|
||||
// Reject unsupported versions
|
||||
if mapRequest.Version < MinimumCapVersion {
|
||||
log.Info().
|
||||
Caller().
|
||||
Int("min_version", int(MinimumCapVersion)).
|
||||
Int("client_version", int(mapRequest.Version)).
|
||||
Msg("unsupported client connected")
|
||||
http.Error(writer, "Internal error", http.StatusBadRequest)
|
||||
|
||||
if rejectUnsupported(writer, mapRequest.Version, ns.machineKey, mapRequest.NodeKey) {
|
||||
return
|
||||
}
|
||||
|
||||
ns.nodeKey = mapRequest.NodeKey
|
||||
|
||||
node, err := ns.headscale.db.GetNodeByAnyKey(
|
||||
ns.conn.Peer(),
|
||||
mapRequest.NodeKey,
|
||||
key.NodePublic{},
|
||||
)
|
||||
node, err := ns.headscale.db.GetNodeByNodeKey(mapRequest.NodeKey)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("handler", "NoisePollNetMap").
|
||||
Msgf("Failed to fetch node from the database with node key: %s", mapRequest.NodeKey.String())
|
||||
http.Error(writer, "Internal error", http.StatusInternalServerError)
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
httpError(writer, NewHTTPError(http.StatusNotFound, "node not found", nil))
|
||||
return
|
||||
}
|
||||
httpError(writer, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -232,3 +225,64 @@ func (ns *noiseServer) NoisePollNetMapHandler(
|
||||
sess.serveLongPoll()
|
||||
}
|
||||
}
|
||||
|
||||
// NoiseRegistrationHandler handles the actual registration process of a node.
|
||||
func (ns *noiseServer) NoiseRegistrationHandler(
|
||||
writer http.ResponseWriter,
|
||||
req *http.Request,
|
||||
) {
|
||||
if req.Method != http.MethodPost {
|
||||
httpError(writer, errMethodNotAllowed)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
registerRequest, registerResponse, err := func() (*tailcfg.RegisterRequest, []byte, error) {
|
||||
body, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var registerRequest tailcfg.RegisterRequest
|
||||
if err := json.Unmarshal(body, ®isterRequest); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
ns.nodeKey = registerRequest.NodeKey
|
||||
|
||||
resp, err := ns.headscale.handleRegister(req.Context(), registerRequest, ns.conn.Peer())
|
||||
// TODO(kradalby): Here we could have two error types, one that is surfaced to the client
|
||||
// and one that returns 500.
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
respBody, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return ®isterRequest, respBody, nil
|
||||
}()
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Error handling registration")
|
||||
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Reject unsupported versions
|
||||
if rejectUnsupported(writer, registerRequest.Version, ns.machineKey, registerRequest.NodeKey) {
|
||||
return
|
||||
}
|
||||
|
||||
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
_, err = writer.Write(registerResponse)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Failed to write response")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ func (n *Notifier) IsConnected(nodeID types.NodeID) bool {
|
||||
}
|
||||
|
||||
// IsLikelyConnected reports if a node is connected to headscale and has a
|
||||
// poll session open, but doesnt lock, so might be wrong.
|
||||
// poll session open, but doesn't lock, so might be wrong.
|
||||
func (n *Notifier) IsLikelyConnected(nodeID types.NodeID) bool {
|
||||
if val, ok := n.connected.Load(nodeID); ok {
|
||||
return val
|
||||
@@ -243,7 +243,7 @@ func (n *Notifier) sendAll(update types.StateUpdate) {
|
||||
// has shut down the channel and is waiting for the lock held here in RemoveNode.
|
||||
// This means that there is potential for a deadlock which would stop all updates
|
||||
// going out to clients. This timeout prevents that from happening by moving on to the
|
||||
// next node if the context is cancelled. Afther sendAll releases the lock, the add/remove
|
||||
// next node if the context is cancelled. After sendAll releases the lock, the add/remove
|
||||
// call will succeed and the update will go to the correct nodes on the next call.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), n.cfg.Tuning.NotifierSendTimeout)
|
||||
defer cancel()
|
||||
|
||||
@@ -223,7 +223,7 @@ func TestBatcher(t *testing.T) {
|
||||
// so do not run the worker.
|
||||
BatchChangeDelay: time.Hour,
|
||||
|
||||
// Since we do not load the config, we wont get the
|
||||
// Since we do not load the config, we won't get the
|
||||
// default, so set it manually so we dont time out
|
||||
// and have flakes.
|
||||
NotifierSendTimeout: time.Second,
|
||||
|
||||
@@ -3,9 +3,7 @@ package hscontrol
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
_ "embed"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
@@ -23,17 +21,18 @@ import (
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/oauth2"
|
||||
"tailscale.com/types/key"
|
||||
"zgo.at/zcache/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
randomByteSize = 16
|
||||
randomByteSize = 16
|
||||
defaultOAuthOptionsCount = 3
|
||||
)
|
||||
|
||||
var (
|
||||
errEmptyOIDCCallbackParams = errors.New("empty OIDC callback params")
|
||||
errNoOIDCIDToken = errors.New("could not extract ID Token for OIDC callback")
|
||||
errNoOIDCRegistrationInfo = errors.New("could not get registration info from cache")
|
||||
errOIDCAllowedDomains = errors.New(
|
||||
"authenticated principal does not match any allowed domain",
|
||||
)
|
||||
@@ -47,11 +46,17 @@ var (
|
||||
errOIDCNodeKeyMissing = errors.New("could not get node key from cache")
|
||||
)
|
||||
|
||||
// RegistrationInfo contains both machine key and verifier information for OIDC validation.
|
||||
type RegistrationInfo struct {
|
||||
RegistrationID types.RegistrationID
|
||||
Verifier *string
|
||||
}
|
||||
|
||||
type AuthProviderOIDC struct {
|
||||
serverURL string
|
||||
cfg *types.OIDCConfig
|
||||
db *db.HSDatabase
|
||||
registrationCache *zcache.Cache[string, key.MachinePublic]
|
||||
registrationCache *zcache.Cache[string, RegistrationInfo]
|
||||
notifier *notifier.Notifier
|
||||
ipAlloc *db.IPAllocator
|
||||
polMan policy.PolicyManager
|
||||
@@ -87,7 +92,7 @@ func NewAuthProviderOIDC(
|
||||
Scopes: cfg.Scope,
|
||||
}
|
||||
|
||||
registrationCache := zcache.New[string, key.MachinePublic](
|
||||
registrationCache := zcache.New[string, RegistrationInfo](
|
||||
registerCacheExpiration,
|
||||
registerCacheCleanup,
|
||||
)
|
||||
@@ -106,11 +111,11 @@ func NewAuthProviderOIDC(
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *AuthProviderOIDC) AuthURL(mKey key.MachinePublic) string {
|
||||
func (a *AuthProviderOIDC) AuthURL(registrationID types.RegistrationID) string {
|
||||
return fmt.Sprintf(
|
||||
"%s/register/%s",
|
||||
strings.TrimSuffix(a.serverURL, "/"),
|
||||
mKey.String())
|
||||
registrationID.String())
|
||||
}
|
||||
|
||||
func (a *AuthProviderOIDC) determineNodeExpiry(idTokenExpiration time.Time) time.Time {
|
||||
@@ -123,54 +128,69 @@ func (a *AuthProviderOIDC) determineNodeExpiry(idTokenExpiration time.Time) time
|
||||
|
||||
// RegisterOIDC redirects to the OIDC provider for authentication
|
||||
// Puts NodeKey in cache so the callback can retrieve it using the oidc state param
|
||||
// Listens in /register/:mKey.
|
||||
// Listens in /register/:registration_id.
|
||||
func (a *AuthProviderOIDC) RegisterHandler(
|
||||
writer http.ResponseWriter,
|
||||
req *http.Request,
|
||||
) {
|
||||
vars := mux.Vars(req)
|
||||
machineKeyStr, ok := vars["mkey"]
|
||||
|
||||
log.Debug().
|
||||
Caller().
|
||||
Str("machine_key", machineKeyStr).
|
||||
Bool("ok", ok).
|
||||
Msg("Received oidc register call")
|
||||
registrationIdStr, _ := vars["registration_id"]
|
||||
|
||||
// We need to make sure we dont open for XSS style injections, if the parameter that
|
||||
// is passed as a key is not parsable/validated as a NodePublic key, then fail to render
|
||||
// the template and log an error.
|
||||
var machineKey key.MachinePublic
|
||||
err := machineKey.UnmarshalText(
|
||||
[]byte(machineKeyStr),
|
||||
)
|
||||
registrationId, err := types.RegistrationIDFromString(registrationIdStr)
|
||||
if err != nil {
|
||||
http.Error(writer, err.Error(), http.StatusBadRequest)
|
||||
httpError(writer, NewHTTPError(http.StatusBadRequest, "invalid registration id", err))
|
||||
return
|
||||
}
|
||||
|
||||
randomBlob := make([]byte, randomByteSize)
|
||||
if _, err := rand.Read(randomBlob); err != nil {
|
||||
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||||
// Set the state and nonce cookies to protect against CSRF attacks
|
||||
state, err := setCSRFCookie(writer, req, "state")
|
||||
if err != nil {
|
||||
httpError(writer, err)
|
||||
return
|
||||
}
|
||||
|
||||
stateStr := hex.EncodeToString(randomBlob)[:32]
|
||||
// Set the state and nonce cookies to protect against CSRF attacks
|
||||
nonce, err := setCSRFCookie(writer, req, "nonce")
|
||||
if err != nil {
|
||||
httpError(writer, err)
|
||||
return
|
||||
}
|
||||
|
||||
// place the node key into the state cache, so it can be retrieved later
|
||||
a.registrationCache.Set(
|
||||
stateStr,
|
||||
machineKey,
|
||||
)
|
||||
// Initialize registration info with machine key
|
||||
registrationInfo := RegistrationInfo{
|
||||
RegistrationID: registrationId,
|
||||
}
|
||||
|
||||
// Add any extra parameter provided in the configuration to the Authorize Endpoint request
|
||||
extras := make([]oauth2.AuthCodeOption, 0, len(a.cfg.ExtraParams))
|
||||
extras := make([]oauth2.AuthCodeOption, 0, len(a.cfg.ExtraParams)+defaultOAuthOptionsCount)
|
||||
// Add PKCE verification if enabled
|
||||
if a.cfg.PKCE.Enabled {
|
||||
verifier := oauth2.GenerateVerifier()
|
||||
registrationInfo.Verifier = &verifier
|
||||
|
||||
extras = append(extras, oauth2.AccessTypeOffline)
|
||||
|
||||
switch a.cfg.PKCE.Method {
|
||||
case types.PKCEMethodS256:
|
||||
extras = append(extras, oauth2.S256ChallengeOption(verifier))
|
||||
case types.PKCEMethodPlain:
|
||||
// oauth2 does not have a plain challenge option, so we add it manually
|
||||
extras = append(extras, oauth2.SetAuthURLParam("code_challenge_method", "plain"), oauth2.SetAuthURLParam("code_challenge", verifier))
|
||||
}
|
||||
}
|
||||
|
||||
// Add any extra parameters from configuration
|
||||
for k, v := range a.cfg.ExtraParams {
|
||||
extras = append(extras, oauth2.SetAuthURLParam(k, v))
|
||||
}
|
||||
extras = append(extras, oidc.Nonce(nonce))
|
||||
|
||||
authURL := a.oauth2Config.AuthCodeURL(stateStr, extras...)
|
||||
// Cache the registration info
|
||||
a.registrationCache.Set(state, registrationInfo)
|
||||
|
||||
authURL := a.oauth2Config.AuthCodeURL(state, extras...)
|
||||
log.Debug().Msgf("Redirecting to %s for authentication", authURL)
|
||||
|
||||
http.Redirect(writer, req, authURL, http.StatusFound)
|
||||
@@ -199,89 +219,89 @@ func (a *AuthProviderOIDC) OIDCCallbackHandler(
|
||||
) {
|
||||
code, state, err := extractCodeAndStateParamFromRequest(req)
|
||||
if err != nil {
|
||||
http.Error(writer, err.Error(), http.StatusBadRequest)
|
||||
httpError(writer, err)
|
||||
return
|
||||
}
|
||||
|
||||
idToken, err := a.extractIDToken(req.Context(), code)
|
||||
cookieState, err := req.Cookie("state")
|
||||
if err != nil {
|
||||
http.Error(writer, err.Error(), http.StatusBadRequest)
|
||||
httpError(writer, NewHTTPError(http.StatusBadRequest, "state not found", err))
|
||||
return
|
||||
}
|
||||
|
||||
if state != cookieState.Value {
|
||||
httpError(writer, NewHTTPError(http.StatusForbidden, "state did not match", nil))
|
||||
return
|
||||
}
|
||||
|
||||
idToken, err := a.extractIDToken(req.Context(), code, state)
|
||||
if err != nil {
|
||||
httpError(writer, err)
|
||||
return
|
||||
}
|
||||
|
||||
nonce, err := req.Cookie("nonce")
|
||||
if err != nil {
|
||||
httpError(writer, NewHTTPError(http.StatusBadRequest, "nonce not found", err))
|
||||
return
|
||||
}
|
||||
if idToken.Nonce != nonce.Value {
|
||||
httpError(writer, NewHTTPError(http.StatusForbidden, "nonce did not match", nil))
|
||||
return
|
||||
}
|
||||
|
||||
nodeExpiry := a.determineNodeExpiry(idToken.Expiry)
|
||||
|
||||
var claims types.OIDCClaims
|
||||
if err := idToken.Claims(&claims); err != nil {
|
||||
http.Error(writer, fmt.Errorf("failed to decode ID token claims: %w", err).Error(), http.StatusInternalServerError)
|
||||
httpError(writer, fmt.Errorf("decoding ID token claims: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateOIDCAllowedDomains(a.cfg.AllowedDomains, &claims); err != nil {
|
||||
http.Error(writer, err.Error(), http.StatusUnauthorized)
|
||||
httpError(writer, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateOIDCAllowedGroups(a.cfg.AllowedGroups, &claims); err != nil {
|
||||
http.Error(writer, err.Error(), http.StatusUnauthorized)
|
||||
httpError(writer, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateOIDCAllowedUsers(a.cfg.AllowedUsers, &claims); err != nil {
|
||||
http.Error(writer, err.Error(), http.StatusUnauthorized)
|
||||
httpError(writer, err)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := a.createOrUpdateUserFromClaim(&claims)
|
||||
if err != nil {
|
||||
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
||||
httpError(writer, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Retrieve the node and the machine key from the state cache and
|
||||
// database.
|
||||
// TODO(kradalby): Is this comment right?
|
||||
// If the node exists, then the node should be reauthenticated,
|
||||
// if the node does not exist, and the machine key exists, then
|
||||
// this is a new node that should be registered.
|
||||
node, mKey := a.getMachineKeyFromState(state)
|
||||
registrationId := a.getRegistrationIDFromState(state)
|
||||
|
||||
// Reauthenticate the node if it does exists.
|
||||
if node != nil {
|
||||
err := a.reauthenticateNode(node, nodeExpiry)
|
||||
// Register the node if it does not exist.
|
||||
if registrationId != nil {
|
||||
verb := "Reauthenticated"
|
||||
newNode, err := a.handleRegistration(user, *registrationId, nodeExpiry)
|
||||
if err != nil {
|
||||
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
||||
httpError(writer, err)
|
||||
return
|
||||
}
|
||||
|
||||
if newNode {
|
||||
verb = "Authenticated"
|
||||
}
|
||||
|
||||
// TODO(kradalby): replace with go-elem
|
||||
var content bytes.Buffer
|
||||
if err := oidcCallbackTemplate.Execute(&content, oidcCallbackTemplateConfig{
|
||||
User: user.DisplayNameOrUsername(),
|
||||
Verb: "Reauthenticated",
|
||||
}); err != nil {
|
||||
http.Error(writer, fmt.Errorf("rendering OIDC callback template: %w", err).Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
_, err = writer.Write(content.Bytes())
|
||||
content, err := renderOIDCCallbackTemplate(user, verb)
|
||||
if err != nil {
|
||||
util.LogErr(err, "Failed to write response")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Register the node if it does not exist.
|
||||
if mKey != nil {
|
||||
if err := a.registerNode(user, mKey, nodeExpiry); err != nil {
|
||||
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
content, err := renderOIDCCallbackTemplate(user)
|
||||
if err != nil {
|
||||
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
||||
httpError(writer, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -296,7 +316,7 @@ func (a *AuthProviderOIDC) OIDCCallbackHandler(
|
||||
|
||||
// Neither node nor machine key was found in the state cache meaning
|
||||
// that we could not reauth nor register the node.
|
||||
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
||||
httpError(writer, NewHTTPError(http.StatusGone, "login session expired, try again", nil))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -307,7 +327,7 @@ func extractCodeAndStateParamFromRequest(
|
||||
state := req.URL.Query().Get("state")
|
||||
|
||||
if code == "" || state == "" {
|
||||
return "", "", errEmptyOIDCCallbackParams
|
||||
return "", "", NewHTTPError(http.StatusBadRequest, "missing code or state parameter", errEmptyOIDCCallbackParams)
|
||||
}
|
||||
|
||||
return code, state, nil
|
||||
@@ -318,21 +338,34 @@ func extractCodeAndStateParamFromRequest(
|
||||
func (a *AuthProviderOIDC) extractIDToken(
|
||||
ctx context.Context,
|
||||
code string,
|
||||
state string,
|
||||
) (*oidc.IDToken, error) {
|
||||
oauth2Token, err := a.oauth2Config.Exchange(ctx, code)
|
||||
var exchangeOpts []oauth2.AuthCodeOption
|
||||
|
||||
if a.cfg.PKCE.Enabled {
|
||||
regInfo, ok := a.registrationCache.Get(state)
|
||||
if !ok {
|
||||
return nil, NewHTTPError(http.StatusNotFound, "registration not found", errNoOIDCRegistrationInfo)
|
||||
}
|
||||
if regInfo.Verifier != nil {
|
||||
exchangeOpts = []oauth2.AuthCodeOption{oauth2.VerifierOption(*regInfo.Verifier)}
|
||||
}
|
||||
}
|
||||
|
||||
oauth2Token, err := a.oauth2Config.Exchange(ctx, code, exchangeOpts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not exchange code for token: %w", err)
|
||||
return nil, NewHTTPError(http.StatusForbidden, "invalid code", fmt.Errorf("could not exchange code for token: %w", err))
|
||||
}
|
||||
|
||||
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return nil, errNoOIDCIDToken
|
||||
return nil, NewHTTPError(http.StatusBadRequest, "no id_token", errNoOIDCIDToken)
|
||||
}
|
||||
|
||||
verifier := a.oidcProvider.Verifier(&oidc.Config{ClientID: a.cfg.ClientID})
|
||||
idToken, err := verifier.Verify(ctx, rawIDToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to verify ID token: %w", err)
|
||||
return nil, NewHTTPError(http.StatusForbidden, "failed to verify id_token", fmt.Errorf("failed to verify ID token: %w", err))
|
||||
}
|
||||
|
||||
return idToken, nil
|
||||
@@ -347,7 +380,7 @@ func validateOIDCAllowedDomains(
|
||||
if len(allowedDomains) > 0 {
|
||||
if at := strings.LastIndex(claims.Email, "@"); at < 0 ||
|
||||
!slices.Contains(allowedDomains, claims.Email[at+1:]) {
|
||||
return errOIDCAllowedDomains
|
||||
return NewHTTPError(http.StatusUnauthorized, "unauthorised domain", errOIDCAllowedDomains)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,7 +402,7 @@ func validateOIDCAllowedGroups(
|
||||
}
|
||||
}
|
||||
|
||||
return errOIDCAllowedGroups
|
||||
return NewHTTPError(http.StatusUnauthorized, "unauthorised group", errOIDCAllowedGroups)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -383,56 +416,20 @@ func validateOIDCAllowedUsers(
|
||||
) error {
|
||||
if len(allowedUsers) > 0 &&
|
||||
!slices.Contains(allowedUsers, claims.Email) {
|
||||
log.Trace().Msg("authenticated principal does not match any allowed user")
|
||||
return errOIDCAllowedUsers
|
||||
return NewHTTPError(http.StatusUnauthorized, "unauthorised user", errOIDCAllowedUsers)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getMachineKeyFromState retrieves the machine key from the state
|
||||
// cache. If the machine key is found, it will try retrieve the
|
||||
// node information from the database.
|
||||
func (a *AuthProviderOIDC) getMachineKeyFromState(state string) (*types.Node, *key.MachinePublic) {
|
||||
machineKey, ok := a.registrationCache.Get(state)
|
||||
// getRegistrationIDFromState retrieves the registration ID from the state.
|
||||
func (a *AuthProviderOIDC) getRegistrationIDFromState(state string) *types.RegistrationID {
|
||||
regInfo, ok := a.registrationCache.Get(state)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// retrieve node information if it exist
|
||||
// The error is not important, because if it does not
|
||||
// exist, then this is a new node and we will move
|
||||
// on to registration.
|
||||
node, _ := a.db.GetNodeByMachineKey(machineKey)
|
||||
|
||||
return node, &machineKey
|
||||
}
|
||||
|
||||
// reauthenticateNode updates the node expiry in the database
|
||||
// and notifies the node and its peers about the change.
|
||||
func (a *AuthProviderOIDC) reauthenticateNode(
|
||||
node *types.Node,
|
||||
expiry time.Time,
|
||||
) error {
|
||||
err := a.db.NodeSetExpiry(node.ID, expiry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := types.NotifyCtx(context.Background(), "oidc-expiry-self", node.Hostname)
|
||||
a.notifier.NotifyByNodeID(
|
||||
ctx,
|
||||
types.StateUpdate{
|
||||
Type: types.StateSelfUpdate,
|
||||
ChangeNodes: []types.NodeID{node.ID},
|
||||
},
|
||||
node.ID,
|
||||
)
|
||||
|
||||
ctx = types.NotifyCtx(context.Background(), "oidc-expiry-peers", node.Hostname)
|
||||
a.notifier.NotifyWithIgnore(ctx, types.StateUpdateExpire(node.ID, expiry), node.ID)
|
||||
|
||||
return nil
|
||||
return ®Info.RegistrationID
|
||||
}
|
||||
|
||||
func (a *AuthProviderOIDC) createOrUpdateUserFromClaim(
|
||||
@@ -490,43 +487,60 @@ func (a *AuthProviderOIDC) createOrUpdateUserFromClaim(
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (a *AuthProviderOIDC) registerNode(
|
||||
func (a *AuthProviderOIDC) handleRegistration(
|
||||
user *types.User,
|
||||
machineKey *key.MachinePublic,
|
||||
registrationID types.RegistrationID,
|
||||
expiry time.Time,
|
||||
) error {
|
||||
) (bool, error) {
|
||||
ipv4, ipv6, err := a.ipAlloc.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
if _, err := a.db.RegisterNodeFromAuthCallback(
|
||||
*machineKey,
|
||||
node, newNode, err := a.db.HandleNodeFromAuthPath(
|
||||
registrationID,
|
||||
types.UserID(user.ID),
|
||||
&expiry,
|
||||
util.RegisterMethodOIDC,
|
||||
ipv4, ipv6,
|
||||
); err != nil {
|
||||
return fmt.Errorf("could not register node: %w", err)
|
||||
}
|
||||
|
||||
err = nodesChangedHook(a.db, a.polMan, a.notifier)
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("updating resources using node: %w", err)
|
||||
return false, fmt.Errorf("could not register node: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
// Send an update to all nodes if this is a new node that they need to know
|
||||
// about.
|
||||
// If this is a refresh, just send new expiry updates.
|
||||
updateSent, err := nodesChangedHook(a.db, a.polMan, a.notifier)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("updating resources using node: %w", err)
|
||||
}
|
||||
|
||||
if !updateSent {
|
||||
ctx := types.NotifyCtx(context.Background(), "oidc-expiry-self", node.Hostname)
|
||||
a.notifier.NotifyByNodeID(
|
||||
ctx,
|
||||
types.StateSelf(node.ID),
|
||||
node.ID,
|
||||
)
|
||||
|
||||
ctx = types.NotifyCtx(context.Background(), "oidc-expiry-peers", node.Hostname)
|
||||
a.notifier.NotifyWithIgnore(ctx, types.StateUpdatePeerAdded(node.ID), node.ID)
|
||||
}
|
||||
|
||||
return newNode, nil
|
||||
}
|
||||
|
||||
// TODO(kradalby):
|
||||
// Rewrite in elem-go.
|
||||
func renderOIDCCallbackTemplate(
|
||||
user *types.User,
|
||||
verb string,
|
||||
) (*bytes.Buffer, error) {
|
||||
var content bytes.Buffer
|
||||
if err := oidcCallbackTemplate.Execute(&content, oidcCallbackTemplateConfig{
|
||||
User: user.DisplayNameOrUsername(),
|
||||
Verb: "Authenticated",
|
||||
Verb: verb,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("rendering OIDC callback template: %w", err)
|
||||
}
|
||||
@@ -554,3 +568,22 @@ func getUserName(
|
||||
|
||||
return userName, nil
|
||||
}
|
||||
|
||||
func setCSRFCookie(w http.ResponseWriter, r *http.Request, name string) (string, error) {
|
||||
val, err := util.GenerateRandomStringURLSafe(64)
|
||||
if err != nil {
|
||||
return val, err
|
||||
}
|
||||
|
||||
c := &http.Cookie{
|
||||
Path: "/oidc/callback",
|
||||
Name: name,
|
||||
Value: val,
|
||||
MaxAge: int(time.Hour.Seconds()),
|
||||
Secure: r.TLS != nil,
|
||||
HttpOnly: true,
|
||||
}
|
||||
http.SetCookie(w, c)
|
||||
|
||||
return val, nil
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"github.com/gofrs/uuid/v5"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/juanfont/headscale/hscontrol/templates"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// WindowsConfigMessage shows a simple message in the browser for how to configure the Windows Tailscale client.
|
||||
@@ -20,13 +19,7 @@ func (h *Headscale) WindowsConfigMessage(
|
||||
) {
|
||||
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
|
||||
if _, err := writer.Write([]byte(templates.Windows(h.cfg.ServerURL).Render())); err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Failed to write response")
|
||||
}
|
||||
writer.Write([]byte(templates.Windows(h.cfg.ServerURL).Render()))
|
||||
}
|
||||
|
||||
// AppleConfigMessage shows a simple message in the browser to point the user to the iOS/MacOS profile and instructions for how to install it.
|
||||
@@ -36,13 +29,7 @@ func (h *Headscale) AppleConfigMessage(
|
||||
) {
|
||||
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
|
||||
if _, err := writer.Write([]byte(templates.Apple(h.cfg.ServerURL).Render())); err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Failed to write response")
|
||||
}
|
||||
writer.Write([]byte(templates.Apple(h.cfg.ServerURL).Render()))
|
||||
}
|
||||
|
||||
func (h *Headscale) ApplePlatformConfig(
|
||||
@@ -52,51 +39,19 @@ func (h *Headscale) ApplePlatformConfig(
|
||||
vars := mux.Vars(req)
|
||||
platform, ok := vars["platform"]
|
||||
if !ok {
|
||||
log.Error().
|
||||
Str("handler", "ApplePlatformConfig").
|
||||
Msg("No platform specified")
|
||||
http.Error(writer, "No platform specified", http.StatusBadRequest)
|
||||
|
||||
httpError(writer, NewHTTPError(http.StatusBadRequest, "no platform specified", nil))
|
||||
return
|
||||
}
|
||||
|
||||
id, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("handler", "ApplePlatformConfig").
|
||||
Err(err).
|
||||
Msg("Failed not create UUID")
|
||||
|
||||
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
_, err := writer.Write([]byte("Failed to create UUID"))
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Failed to write response")
|
||||
}
|
||||
|
||||
httpError(writer, err)
|
||||
return
|
||||
}
|
||||
|
||||
contentID, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("handler", "ApplePlatformConfig").
|
||||
Err(err).
|
||||
Msg("Failed not create UUID")
|
||||
|
||||
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
_, err := writer.Write([]byte("Failed to create content UUID"))
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Failed to write response")
|
||||
}
|
||||
|
||||
httpError(writer, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -106,68 +61,25 @@ func (h *Headscale) ApplePlatformConfig(
|
||||
}
|
||||
|
||||
var payload bytes.Buffer
|
||||
handleMacError := func(ierr error) {
|
||||
log.Error().
|
||||
Str("handler", "ApplePlatformConfig").
|
||||
Err(ierr).
|
||||
Msg("Could not render Apple macOS template")
|
||||
|
||||
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
_, err := writer.Write([]byte("Could not render Apple macOS template"))
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Failed to write response")
|
||||
}
|
||||
}
|
||||
|
||||
switch platform {
|
||||
case "macos-standalone":
|
||||
if err := macosStandaloneTemplate.Execute(&payload, platformConfig); err != nil {
|
||||
handleMacError(err)
|
||||
|
||||
httpError(writer, err)
|
||||
return
|
||||
}
|
||||
case "macos-app-store":
|
||||
if err := macosAppStoreTemplate.Execute(&payload, platformConfig); err != nil {
|
||||
handleMacError(err)
|
||||
|
||||
httpError(writer, err)
|
||||
return
|
||||
}
|
||||
case "ios":
|
||||
if err := iosTemplate.Execute(&payload, platformConfig); err != nil {
|
||||
log.Error().
|
||||
Str("handler", "ApplePlatformConfig").
|
||||
Err(err).
|
||||
Msg("Could not render Apple iOS template")
|
||||
|
||||
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
_, err := writer.Write([]byte("Could not render Apple iOS template"))
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Failed to write response")
|
||||
}
|
||||
|
||||
httpError(writer, err)
|
||||
return
|
||||
}
|
||||
default:
|
||||
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
_, err := writer.Write(
|
||||
[]byte("Invalid platform. Only ios, macos-app-store and macos-standalone are supported"),
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Failed to write response")
|
||||
}
|
||||
|
||||
httpError(writer, NewHTTPError(http.StatusBadRequest, "platform must be ios, macos-app-store or macos-standalone", nil))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -179,34 +91,14 @@ func (h *Headscale) ApplePlatformConfig(
|
||||
|
||||
var content bytes.Buffer
|
||||
if err := commonTemplate.Execute(&content, config); err != nil {
|
||||
log.Error().
|
||||
Str("handler", "ApplePlatformConfig").
|
||||
Err(err).
|
||||
Msg("Could not render Apple platform template")
|
||||
|
||||
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
_, err := writer.Write([]byte("Could not render Apple platform template"))
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Failed to write response")
|
||||
}
|
||||
|
||||
httpError(writer, err)
|
||||
return
|
||||
}
|
||||
|
||||
writer.Header().
|
||||
Set("Content-Type", "application/x-apple-aspen-config; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
_, err = writer.Write(content.Bytes())
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Failed to write response")
|
||||
}
|
||||
writer.Write(content.Bytes())
|
||||
}
|
||||
|
||||
type AppleMobileConfig struct {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"iter"
|
||||
"net/netip"
|
||||
"os"
|
||||
"slices"
|
||||
@@ -60,8 +61,8 @@ func theInternet() *netipx.IPSet {
|
||||
internetBuilder.RemovePrefix(tsaddr.TailscaleULARange())
|
||||
internetBuilder.RemovePrefix(tsaddr.CGNATRange())
|
||||
|
||||
// Delete "cant find DHCP networks"
|
||||
internetBuilder.RemovePrefix(netip.MustParsePrefix("fe80::/10")) // link-loca
|
||||
// Delete "can't find DHCP networks"
|
||||
internetBuilder.RemovePrefix(netip.MustParsePrefix("fe80::/10")) // link-local
|
||||
internetBuilder.RemovePrefix(netip.MustParsePrefix("169.254.0.0/16"))
|
||||
|
||||
theInternetSet, _ := internetBuilder.IPSet()
|
||||
@@ -250,7 +251,7 @@ func ReduceFilterRules(node *types.Node, rules []tailcfg.FilterRule) []tailcfg.F
|
||||
DEST_LOOP:
|
||||
for _, dest := range rule.DstPorts {
|
||||
expanded, err := util.ParseIPSet(dest.IP, nil)
|
||||
// Fail closed, if we cant parse it, then we should not allow
|
||||
// Fail closed, if we can't parse it, then we should not allow
|
||||
// access.
|
||||
if err != nil {
|
||||
continue DEST_LOOP
|
||||
@@ -361,37 +362,67 @@ func (pol *ACLPolicy) CompileSSHPolicy(
|
||||
)
|
||||
}
|
||||
|
||||
principals := make([]*tailcfg.SSHPrincipal, 0, len(sshACL.Sources))
|
||||
for innerIndex, rawSrc := range sshACL.Sources {
|
||||
if isWildcard(rawSrc) {
|
||||
principals = append(principals, &tailcfg.SSHPrincipal{
|
||||
var principals []*tailcfg.SSHPrincipal
|
||||
for innerIndex, srcToken := range sshACL.Sources {
|
||||
if isWildcard(srcToken) {
|
||||
principals = []*tailcfg.SSHPrincipal{{
|
||||
Any: true,
|
||||
})
|
||||
} else if isGroup(rawSrc) {
|
||||
users, err := pol.expandUsersFromGroup(rawSrc)
|
||||
}}
|
||||
break
|
||||
}
|
||||
|
||||
// If the token is a group, expand the users and validate
|
||||
// them. Then use the .Username() to get the login name
|
||||
// that corresponds with the User info in the netmap.
|
||||
if isGroup(srcToken) {
|
||||
usersFromGroup, err := pol.expandUsersFromGroup(srcToken)
|
||||
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 {
|
||||
for _, userStr := range usersFromGroup {
|
||||
user, err := findUserFromToken(users, userStr)
|
||||
if err != nil {
|
||||
log.Trace().Err(err).Msg("user not found")
|
||||
continue
|
||||
}
|
||||
|
||||
principals = append(principals, &tailcfg.SSHPrincipal{
|
||||
UserLogin: user,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
expandedSrcs, err := pol.ExpandAlias(
|
||||
peers,
|
||||
users,
|
||||
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(),
|
||||
UserLogin: user.Username(),
|
||||
})
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// Try to check if the token is a user, if it is, then we
|
||||
// can use the .Username() to get the login name that
|
||||
// corresponds with the User info in the netmap.
|
||||
// TODO(kradalby): This is a bit of a hack, and it should go
|
||||
// away with the new policy where users can be reliably determined.
|
||||
if user, err := findUserFromToken(users, srcToken); err == nil {
|
||||
principals = append(principals, &tailcfg.SSHPrincipal{
|
||||
UserLogin: user.Username(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// This is kind of then non-ideal scenario where we dont really know
|
||||
// what to do with the token, so we expand it to IP addresses of nodes.
|
||||
// The pro here is that we have a pretty good lockdown on the mapping
|
||||
// between users and node, but it can explode if a user owns many nodes.
|
||||
ips, err := pol.ExpandAlias(
|
||||
peers,
|
||||
users,
|
||||
srcToken,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing SSH policy, expanding alias, index: %d->%d: %w", index, innerIndex, err)
|
||||
}
|
||||
for addr := range ipSetAll(ips) {
|
||||
principals = append(principals, &tailcfg.SSHPrincipal{
|
||||
NodeIP: addr.String(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,6 +442,19 @@ func (pol *ACLPolicy) CompileSSHPolicy(
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ipSetAll returns a function that iterates over all the IPs in the IPSet.
|
||||
func ipSetAll(ipSet *netipx.IPSet) iter.Seq[netip.Addr] {
|
||||
return func(yield func(netip.Addr) bool) {
|
||||
for _, rng := range ipSet.Ranges() {
|
||||
for ip := rng.From(); ip.Compare(rng.To()) <= 0; ip = ip.Next() {
|
||||
if !yield(ip) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sshCheckAction(duration string) (*tailcfg.SSHAction, error) {
|
||||
sessionLength, err := time.ParseDuration(duration)
|
||||
if err != nil {
|
||||
@@ -890,7 +934,7 @@ func (pol *ACLPolicy) expandIPsFromIPPrefix(
|
||||
build.AddPrefix(prefix)
|
||||
|
||||
// This is suboptimal and quite expensive, but if we only add the prefix, we will miss all the relevant IPv6
|
||||
// addresses for the hosts that belong to tailscale. This doesnt really affect stuff like subnet routers.
|
||||
// addresses for the hosts that belong to tailscale. This doesn't really affect stuff like subnet routers.
|
||||
for _, node := range nodes {
|
||||
for _, ip := range node.IPs() {
|
||||
// log.Trace().
|
||||
@@ -934,6 +978,7 @@ func isAutoGroup(str string) bool {
|
||||
// Invalid tags are tags added by a user on a node, and that user doesn't have authority to add this tag.
|
||||
// Valid tags are tags added by a user that is allowed in the ACL policy to add this tag.
|
||||
func (pol *ACLPolicy) TagsOfNode(
|
||||
users []types.User,
|
||||
node *types.Node,
|
||||
) ([]string, []string) {
|
||||
var validTags []string
|
||||
@@ -956,7 +1001,12 @@ func (pol *ACLPolicy) TagsOfNode(
|
||||
}
|
||||
var found bool
|
||||
for _, owner := range owners {
|
||||
if node.User.Username() == owner {
|
||||
user, err := findUserFromToken(users, owner)
|
||||
if err != nil {
|
||||
log.Trace().Caller().Err(err).Msg("could not determine user to filter tags by")
|
||||
}
|
||||
|
||||
if node.User.ID == user.ID {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
@@ -988,30 +1038,12 @@ func (pol *ACLPolicy) TagsOfNode(
|
||||
func filterNodesByUser(nodes types.Nodes, users []types.User, userToken string) 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)
|
||||
}
|
||||
user, err := findUserFromToken(users, userToken)
|
||||
if err != nil {
|
||||
log.Trace().Caller().Err(err).Msg("could not determine user to filter nodes by")
|
||||
return out
|
||||
}
|
||||
|
||||
if len(potentialUsers) != 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
user := potentialUsers[0]
|
||||
|
||||
for _, node := range nodes {
|
||||
if node.User.ID == user.ID {
|
||||
out = append(out, node)
|
||||
@@ -1021,6 +1053,39 @@ func filterNodesByUser(nodes types.Nodes, users []types.User, userToken string)
|
||||
return out
|
||||
}
|
||||
|
||||
var (
|
||||
ErrorNoUserMatching = errors.New("no user matching")
|
||||
ErrorMultipleUserMatching = errors.New("multiple users matching")
|
||||
)
|
||||
|
||||
// findUserFromToken finds and returns a user based on the given token, prioritizing matches by ProviderIdentifier, followed by email or name.
|
||||
// If no matching user is found, it returns an error of type ErrorNoUserMatching.
|
||||
// If multiple users match the token, it returns an error indicating multiple matches.
|
||||
func findUserFromToken(users []types.User, token string) (types.User, error) {
|
||||
var potentialUsers []types.User
|
||||
|
||||
for _, user := range users {
|
||||
if user.ProviderIdentifier.Valid && user.ProviderIdentifier.String == token {
|
||||
// Prioritize ProviderIdentifier match and exit early
|
||||
return user, nil
|
||||
}
|
||||
|
||||
if user.Email == token || user.Name == token {
|
||||
potentialUsers = append(potentialUsers, user)
|
||||
}
|
||||
}
|
||||
|
||||
if len(potentialUsers) == 0 {
|
||||
return types.User{}, fmt.Errorf("user with token %q not found: %w", token, ErrorNoUserMatching)
|
||||
}
|
||||
|
||||
if len(potentialUsers) > 1 {
|
||||
return types.User{}, fmt.Errorf("multiple users with token %q found: %w", token, ErrorNoUserMatching)
|
||||
}
|
||||
|
||||
return potentialUsers[0], nil
|
||||
}
|
||||
|
||||
// FilterNodesByACL returns the list of peers authorized to be accessed from a given node.
|
||||
func FilterNodesByACL(
|
||||
node *types.Node,
|
||||
|
||||
@@ -2735,6 +2735,12 @@ func TestReduceFilterRules(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_getTags(t *testing.T) {
|
||||
users := []types.User{
|
||||
{
|
||||
Model: gorm.Model{ID: 1},
|
||||
Name: "joe",
|
||||
},
|
||||
}
|
||||
type args struct {
|
||||
aclPolicy *ACLPolicy
|
||||
node *types.Node
|
||||
@@ -2754,9 +2760,7 @@ func Test_getTags(t *testing.T) {
|
||||
},
|
||||
},
|
||||
node: &types.Node{
|
||||
User: types.User{
|
||||
Name: "joe",
|
||||
},
|
||||
User: users[0],
|
||||
Hostinfo: &tailcfg.Hostinfo{
|
||||
RequestTags: []string{"tag:valid"},
|
||||
},
|
||||
@@ -2774,9 +2778,7 @@ func Test_getTags(t *testing.T) {
|
||||
},
|
||||
},
|
||||
node: &types.Node{
|
||||
User: types.User{
|
||||
Name: "joe",
|
||||
},
|
||||
User: users[0],
|
||||
Hostinfo: &tailcfg.Hostinfo{
|
||||
RequestTags: []string{"tag:valid", "tag:invalid"},
|
||||
},
|
||||
@@ -2794,9 +2796,7 @@ func Test_getTags(t *testing.T) {
|
||||
},
|
||||
},
|
||||
node: &types.Node{
|
||||
User: types.User{
|
||||
Name: "joe",
|
||||
},
|
||||
User: users[0],
|
||||
Hostinfo: &tailcfg.Hostinfo{
|
||||
RequestTags: []string{
|
||||
"tag:invalid",
|
||||
@@ -2818,9 +2818,7 @@ func Test_getTags(t *testing.T) {
|
||||
},
|
||||
},
|
||||
node: &types.Node{
|
||||
User: types.User{
|
||||
Name: "joe",
|
||||
},
|
||||
User: users[0],
|
||||
Hostinfo: &tailcfg.Hostinfo{
|
||||
RequestTags: []string{"tag:invalid", "very-invalid"},
|
||||
},
|
||||
@@ -2834,9 +2832,7 @@ func Test_getTags(t *testing.T) {
|
||||
args: args{
|
||||
aclPolicy: &ACLPolicy{},
|
||||
node: &types.Node{
|
||||
User: types.User{
|
||||
Name: "joe",
|
||||
},
|
||||
User: users[0],
|
||||
Hostinfo: &tailcfg.Hostinfo{
|
||||
RequestTags: []string{"tag:invalid", "very-invalid"},
|
||||
},
|
||||
@@ -2849,6 +2845,7 @@ func Test_getTags(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
gotValid, gotInvalid := test.args.aclPolicy.TagsOfNode(
|
||||
users,
|
||||
test.args.node,
|
||||
)
|
||||
for _, valid := range gotValid {
|
||||
@@ -3542,6 +3539,11 @@ func Test_getFilteredByACLPeers(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSSHRules(t *testing.T) {
|
||||
users := []types.User{
|
||||
{
|
||||
Name: "user1",
|
||||
},
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
node types.Node
|
||||
@@ -3555,18 +3557,14 @@ func TestSSHRules(t *testing.T) {
|
||||
Hostname: "testnodes",
|
||||
IPv4: iap("100.64.99.42"),
|
||||
UserID: 0,
|
||||
User: types.User{
|
||||
Name: "user1",
|
||||
},
|
||||
User: users[0],
|
||||
},
|
||||
peers: types.Nodes{
|
||||
&types.Node{
|
||||
Hostname: "testnodes2",
|
||||
IPv4: iap("100.64.0.1"),
|
||||
UserID: 0,
|
||||
User: types.User{
|
||||
Name: "user1",
|
||||
},
|
||||
User: users[0],
|
||||
},
|
||||
},
|
||||
pol: ACLPolicy{
|
||||
@@ -3679,18 +3677,14 @@ func TestSSHRules(t *testing.T) {
|
||||
Hostname: "testnodes",
|
||||
IPv4: iap("100.64.0.1"),
|
||||
UserID: 0,
|
||||
User: types.User{
|
||||
Name: "user1",
|
||||
},
|
||||
User: users[0],
|
||||
},
|
||||
peers: types.Nodes{
|
||||
&types.Node{
|
||||
Hostname: "testnodes2",
|
||||
IPv4: iap("100.64.99.42"),
|
||||
UserID: 0,
|
||||
User: types.User{
|
||||
Name: "user1",
|
||||
},
|
||||
User: users[0],
|
||||
},
|
||||
},
|
||||
pol: ACLPolicy{
|
||||
@@ -3728,7 +3722,7 @@ func TestSSHRules(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tt.pol.CompileSSHPolicy(&tt.node, []types.User{}, tt.peers)
|
||||
got, err := tt.pol.CompileSSHPolicy(&tt.node, users, tt.peers)
|
||||
require.NoError(t, err)
|
||||
|
||||
if diff := cmp.Diff(tt.want, got); diff != "" {
|
||||
@@ -4052,3 +4046,315 @@ func TestValidTagInvalidUser(t *testing.T) {
|
||||
t.Errorf("TestValidTagInvalidUser() unexpected result (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindUserByToken(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
users []types.User
|
||||
token string
|
||||
want types.User
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "exact match by ProviderIdentifier",
|
||||
users: []types.User{
|
||||
{ProviderIdentifier: sql.NullString{Valid: true, String: "token1"}},
|
||||
{Email: "user2@example.com"},
|
||||
},
|
||||
token: "token1",
|
||||
want: types.User{ProviderIdentifier: sql.NullString{Valid: true, String: "token1"}},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "no matches found",
|
||||
users: []types.User{
|
||||
{Email: "user1@example.com"},
|
||||
{Name: "username"},
|
||||
},
|
||||
token: "nonexistent-token",
|
||||
want: types.User{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "multiple matches by email and name",
|
||||
users: []types.User{
|
||||
{Email: "token2", Name: "notoken"},
|
||||
{Name: "token2", Email: "notoken@example.com"},
|
||||
},
|
||||
token: "token2",
|
||||
want: types.User{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "match by email",
|
||||
users: []types.User{
|
||||
{Email: "token3@example.com"},
|
||||
{ProviderIdentifier: sql.NullString{Valid: true, String: "othertoken"}},
|
||||
},
|
||||
token: "token3@example.com",
|
||||
want: types.User{Email: "token3@example.com"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "match by name",
|
||||
users: []types.User{
|
||||
{Name: "token4"},
|
||||
{Email: "user5@example.com"},
|
||||
},
|
||||
token: "token4",
|
||||
want: types.User{Name: "token4"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "provider identifier takes precedence over email and name matches",
|
||||
users: []types.User{
|
||||
{ProviderIdentifier: sql.NullString{Valid: true, String: "token5"}},
|
||||
{Email: "token5@example.com", Name: "token5"},
|
||||
},
|
||||
token: "token5",
|
||||
want: types.User{ProviderIdentifier: sql.NullString{Valid: true, String: "token5"}},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty token finds no users",
|
||||
users: []types.User{
|
||||
{Email: "user6@example.com"},
|
||||
{Name: "username6"},
|
||||
},
|
||||
token: "",
|
||||
want: types.User{},
|
||||
wantErr: true,
|
||||
},
|
||||
// Test case 1: Duplicate Emails with Unique ProviderIdentifiers
|
||||
{
|
||||
name: "duplicate emails with unique provider identifiers",
|
||||
users: []types.User{
|
||||
{ProviderIdentifier: sql.NullString{Valid: true, String: "pid1"}, Email: "user@example.com"},
|
||||
{ProviderIdentifier: sql.NullString{Valid: true, String: "pid2"}, Email: "user@example.com"},
|
||||
},
|
||||
token: "user@example.com",
|
||||
want: types.User{},
|
||||
wantErr: true,
|
||||
},
|
||||
|
||||
// Test case 2: Duplicate Names with Unique ProviderIdentifiers
|
||||
{
|
||||
name: "duplicate names with unique provider identifiers",
|
||||
users: []types.User{
|
||||
{ProviderIdentifier: sql.NullString{Valid: true, String: "pid3"}, Name: "John Doe"},
|
||||
{ProviderIdentifier: sql.NullString{Valid: true, String: "pid4"}, Name: "John Doe"},
|
||||
},
|
||||
token: "John Doe",
|
||||
want: types.User{},
|
||||
wantErr: true,
|
||||
},
|
||||
|
||||
// Test case 3: Duplicate Emails and Names with Unique ProviderIdentifiers
|
||||
{
|
||||
name: "duplicate emails and names with unique provider identifiers",
|
||||
users: []types.User{
|
||||
{ProviderIdentifier: sql.NullString{Valid: true, String: "pid5"}, Email: "user@example.com", Name: "John Doe"},
|
||||
{ProviderIdentifier: sql.NullString{Valid: true, String: "pid6"}, Email: "user@example.com", Name: "John Doe"},
|
||||
},
|
||||
token: "user@example.com",
|
||||
want: types.User{},
|
||||
wantErr: true,
|
||||
},
|
||||
|
||||
// Test case 4: Unique Names without ProviderIdentifiers
|
||||
{
|
||||
name: "unique names without provider identifiers",
|
||||
users: []types.User{
|
||||
{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "johndoe@example.com"},
|
||||
{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "Jane Smith", Email: "janesmith@example.com"},
|
||||
},
|
||||
token: "John Doe",
|
||||
want: types.User{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "johndoe@example.com"},
|
||||
wantErr: false,
|
||||
},
|
||||
|
||||
// Test case 5: Duplicate Emails without ProviderIdentifiers but Unique Names
|
||||
{
|
||||
name: "duplicate emails without provider identifiers but unique names",
|
||||
users: []types.User{
|
||||
{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "user@example.com"},
|
||||
{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "Jane Smith", Email: "user@example.com"},
|
||||
},
|
||||
token: "John Doe",
|
||||
want: types.User{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "user@example.com"},
|
||||
wantErr: false,
|
||||
},
|
||||
|
||||
// Test case 6: Duplicate Names and Emails without ProviderIdentifiers
|
||||
{
|
||||
name: "duplicate names and emails without provider identifiers",
|
||||
users: []types.User{
|
||||
{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "user@example.com"},
|
||||
{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "user@example.com"},
|
||||
},
|
||||
token: "John Doe",
|
||||
want: types.User{},
|
||||
wantErr: true,
|
||||
},
|
||||
|
||||
// Test case 7: Multiple Users with the Same Email but Different Names and Unique ProviderIdentifiers
|
||||
{
|
||||
name: "multiple users with same email, different names, unique provider identifiers",
|
||||
users: []types.User{
|
||||
{ProviderIdentifier: sql.NullString{Valid: true, String: "pid7"}, Email: "user@example.com", Name: "John Doe"},
|
||||
{ProviderIdentifier: sql.NullString{Valid: true, String: "pid8"}, Email: "user@example.com", Name: "Jane Smith"},
|
||||
},
|
||||
token: "user@example.com",
|
||||
want: types.User{},
|
||||
wantErr: true,
|
||||
},
|
||||
|
||||
// Test case 8: Multiple Users with the Same Name but Different Emails and Unique ProviderIdentifiers
|
||||
{
|
||||
name: "multiple users with same name, different emails, unique provider identifiers",
|
||||
users: []types.User{
|
||||
{ProviderIdentifier: sql.NullString{Valid: true, String: "pid9"}, Email: "johndoe@example.com", Name: "John Doe"},
|
||||
{ProviderIdentifier: sql.NullString{Valid: true, String: "pid10"}, Email: "janedoe@example.com", Name: "John Doe"},
|
||||
},
|
||||
token: "John Doe",
|
||||
want: types.User{},
|
||||
wantErr: true,
|
||||
},
|
||||
|
||||
// Test case 9: Multiple Users with Same Email and Name but Unique ProviderIdentifiers
|
||||
{
|
||||
name: "multiple users with same email and name, unique provider identifiers",
|
||||
users: []types.User{
|
||||
{ProviderIdentifier: sql.NullString{Valid: true, String: "pid11"}, Email: "user@example.com", Name: "John Doe"},
|
||||
{ProviderIdentifier: sql.NullString{Valid: true, String: "pid12"}, Email: "user@example.com", Name: "John Doe"},
|
||||
},
|
||||
token: "user@example.com",
|
||||
want: types.User{},
|
||||
wantErr: true,
|
||||
},
|
||||
|
||||
// Test case 10: Multiple Users without ProviderIdentifiers but with Unique Names and Emails
|
||||
{
|
||||
name: "multiple users without provider identifiers, unique names and emails",
|
||||
users: []types.User{
|
||||
{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "johndoe@example.com"},
|
||||
{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "Jane Smith", Email: "janesmith@example.com"},
|
||||
},
|
||||
token: "John Doe",
|
||||
want: types.User{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "johndoe@example.com"},
|
||||
wantErr: false,
|
||||
},
|
||||
|
||||
// Test case 11: Multiple Users without ProviderIdentifiers and Duplicate Emails but Unique Names
|
||||
{
|
||||
name: "multiple users without provider identifiers, duplicate emails but unique names",
|
||||
users: []types.User{
|
||||
{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "user@example.com"},
|
||||
{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "Jane Smith", Email: "user@example.com"},
|
||||
},
|
||||
token: "John Doe",
|
||||
want: types.User{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "user@example.com"},
|
||||
wantErr: false,
|
||||
},
|
||||
|
||||
// Test case 12: Multiple Users without ProviderIdentifiers and Duplicate Names but Unique Emails
|
||||
{
|
||||
name: "multiple users without provider identifiers, duplicate names but unique emails",
|
||||
users: []types.User{
|
||||
{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "johndoe@example.com"},
|
||||
{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "janedoe@example.com"},
|
||||
},
|
||||
token: "John Doe",
|
||||
want: types.User{},
|
||||
wantErr: true,
|
||||
},
|
||||
|
||||
// Test case 13: Multiple Users without ProviderIdentifiers and Duplicate Both Names and Emails
|
||||
{
|
||||
name: "multiple users without provider identifiers, duplicate names and emails",
|
||||
users: []types.User{
|
||||
{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "user@example.com"},
|
||||
{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "user@example.com"},
|
||||
},
|
||||
token: "John Doe",
|
||||
want: types.User{},
|
||||
wantErr: true,
|
||||
},
|
||||
|
||||
// Test case 14: Multiple Users with Same Email Without ProviderIdentifiers
|
||||
{
|
||||
name: "multiple users with same email without provider identifiers",
|
||||
users: []types.User{
|
||||
{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "user@example.com"},
|
||||
{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "Jane Smith", Email: "user@example.com"},
|
||||
},
|
||||
token: "user@example.com",
|
||||
want: types.User{},
|
||||
wantErr: true,
|
||||
},
|
||||
|
||||
// Test case 15: Multiple Users with Same Name Without ProviderIdentifiers
|
||||
{
|
||||
name: "multiple users with same name without provider identifiers",
|
||||
users: []types.User{
|
||||
{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "johndoe@example.com"},
|
||||
{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "janedoe@example.com"},
|
||||
},
|
||||
token: "John Doe",
|
||||
want: types.User{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Name field used as email address match",
|
||||
users: []types.User{
|
||||
{ProviderIdentifier: sql.NullString{Valid: true, String: "pid3"}, Name: "user@example.com", Email: "another@example.com"},
|
||||
},
|
||||
token: "user@example.com",
|
||||
want: types.User{ProviderIdentifier: sql.NullString{Valid: true, String: "pid3"}, Name: "user@example.com", Email: "another@example.com"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "multiple users with same name as email and unique provider identifiers",
|
||||
users: []types.User{
|
||||
{ProviderIdentifier: sql.NullString{Valid: true, String: "pid4"}, Name: "user@example.com", Email: "user1@example.com"},
|
||||
{ProviderIdentifier: sql.NullString{Valid: true, String: "pid5"}, Name: "user@example.com", Email: "user2@example.com"},
|
||||
},
|
||||
token: "user@example.com",
|
||||
want: types.User{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "no provider identifier and duplicate names as emails",
|
||||
users: []types.User{
|
||||
{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "user@example.com", Email: "another1@example.com"},
|
||||
{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "user@example.com", Email: "another2@example.com"},
|
||||
},
|
||||
token: "user@example.com",
|
||||
want: types.User{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "name as email with multiple matches when provider identifier is not set",
|
||||
users: []types.User{
|
||||
{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "user@example.com", Email: "another1@example.com"},
|
||||
{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "user@example.com", Email: "another2@example.com"},
|
||||
},
|
||||
token: "user@example.com",
|
||||
want: types.User{},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotUser, err := findUserFromToken(tt.users, tt.token)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("findUserFromToken() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if diff := cmp.Diff(tt.want, gotUser, util.Comparers...); diff != "" {
|
||||
t.Errorf("findUserFromToken() unexpected result (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/rs/zerolog/log"
|
||||
"go4.org/netipx"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/deephash"
|
||||
@@ -122,6 +123,10 @@ func (pm *PolicyManagerV1) SSHPolicy(node *types.Node) (*tailcfg.SSHPolicy, erro
|
||||
}
|
||||
|
||||
func (pm *PolicyManagerV1) SetPolicy(polB []byte) (bool, error) {
|
||||
if len(polB) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
pol, err := LoadACLPolicyFromBytes(polB)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("parsing policy: %w", err)
|
||||
@@ -157,7 +162,8 @@ func (pm *PolicyManagerV1) Tags(node *types.Node) []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
tags, _ := pm.pol.TagsOfNode(node)
|
||||
tags, invalid := pm.pol.TagsOfNode(pm.users, node)
|
||||
log.Debug().Strs("authorised_tags", tags).Strs("unauthorised_tags", invalid).Uint64("node.id", node.ID.Uint64()).Msg("tags provided by policy")
|
||||
return tags
|
||||
}
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ func (m *mapSession) serve() {
|
||||
// current configuration.
|
||||
//
|
||||
// If OmitPeers is true, Stream is false, and ReadOnly is false,
|
||||
// then te server will let clients update their endpoints without
|
||||
// then the server will let clients update their endpoints without
|
||||
// breaking existing long-polling (Stream == true) connections.
|
||||
// In this case, the server can omit the entire response; the client
|
||||
// only checks the HTTP response status code.
|
||||
@@ -387,7 +387,7 @@ func (m *mapSession) serveLongPoll() {
|
||||
|
||||
func (m *mapSession) pollFailoverRoutes(where string, node *types.Node) {
|
||||
update, err := db.Write(m.h.db.DB, func(tx *gorm.DB) (*types.StateUpdate, error) {
|
||||
return db.FailoverNodeRoutesIfNeccessary(tx, m.h.nodeNotifier.LikelyConnectedMap(), node)
|
||||
return db.FailoverNodeRoutesIfNecessary(tx, m.h.nodeNotifier.LikelyConnectedMap(), node)
|
||||
})
|
||||
if err != nil {
|
||||
m.errf(err, fmt.Sprintf("failed to ensure failover routes, %s", where))
|
||||
@@ -453,7 +453,7 @@ func (m *mapSession) handleEndpointUpdate() {
|
||||
// If there is no NetInfo, keep the previous one.
|
||||
// From 1.66 the client only sends it if changed:
|
||||
// https://github.com/tailscale/tailscale/commit/e1011f138737286ecf5123ff887a7a5800d129a2
|
||||
// TODO(kradalby): evaulate if we need better comparing of hostinfo
|
||||
// TODO(kradalby): evaluate if we need better comparing of hostinfo
|
||||
// before we take the changes.
|
||||
if m.req.Hostinfo.NetInfo == nil && m.node.Hostinfo != nil {
|
||||
m.req.Hostinfo.NetInfo = m.node.Hostinfo.NetInfo
|
||||
@@ -691,7 +691,7 @@ func hostInfoChanged(old, new *tailcfg.Hostinfo) (bool, bool) {
|
||||
}
|
||||
|
||||
// Services is mostly useful for discovery and not critical,
|
||||
// except for peerapi, which is how nodes talk to eachother.
|
||||
// except for peerapi, which is how nodes talk to each other.
|
||||
// If peerapi was not part of the initial mapresponse, we
|
||||
// need to make sure its sent out later as it is needed for
|
||||
// Taildrop.
|
||||
|
||||
@@ -27,50 +27,27 @@ func Apple(url string) *elem.Element {
|
||||
elem.Text("App store"),
|
||||
),
|
||||
),
|
||||
elem.Li(nil,
|
||||
elem.Text("Open Tailscale and make sure you are "),
|
||||
elem.I(nil, elem.Text("not ")),
|
||||
elem.Text("logged in to any account"),
|
||||
),
|
||||
elem.Li(nil,
|
||||
elem.Text("Open Settings on the iOS device"),
|
||||
elem.Li(
|
||||
nil,
|
||||
elem.Text("Open the Tailscale app"),
|
||||
),
|
||||
elem.Li(
|
||||
nil,
|
||||
elem.Text(
|
||||
`Scroll down to the "third party apps" section, under "Game Center" or "TV Provider"`,
|
||||
),
|
||||
elem.Text(`Click the account icon in the top-right corner and select "Log in…".`),
|
||||
),
|
||||
elem.Li(nil,
|
||||
elem.Text("Find Tailscale and select it"),
|
||||
elem.Ul(nil,
|
||||
elem.Li(
|
||||
nil,
|
||||
elem.Text(
|
||||
`If the iOS device was previously logged into Tailscale, switch the "Reset Keychain" toggle to "on"`,
|
||||
),
|
||||
),
|
||||
),
|
||||
elem.Li(
|
||||
nil,
|
||||
elem.Text(`Tap the top-right options menu button and select "Use custom coordination server".`),
|
||||
),
|
||||
elem.Li(
|
||||
nil,
|
||||
elem.Text(
|
||||
fmt.Sprintf(
|
||||
`Enter "%s" under "Alternate Coordination Server URL"`,
|
||||
`Enter your instance URL: "%s"`,
|
||||
url,
|
||||
),
|
||||
),
|
||||
),
|
||||
elem.Li(
|
||||
nil,
|
||||
elem.Text(
|
||||
"Restart the app by closing it from the iOS app switcher, open the app and select the regular sign in option ",
|
||||
),
|
||||
elem.I(nil, elem.Text("(non-SSO)")),
|
||||
elem.Text(
|
||||
". It should open up to the headscale authentication page.",
|
||||
),
|
||||
),
|
||||
elem.Li(
|
||||
nil,
|
||||
elem.Text(
|
||||
@@ -93,7 +70,7 @@ func Apple(url string) *elem.Element {
|
||||
elem.Li(
|
||||
nil,
|
||||
elem.Text(
|
||||
"ALT + Click the Tailscale icon in the menu and hover over the Debug menu",
|
||||
"Option + Click the Tailscale icon in the menu and hover over the Debug menu",
|
||||
),
|
||||
),
|
||||
elem.Li(nil,
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/chasefleming/elem-go"
|
||||
"github.com/chasefleming/elem-go/attrs"
|
||||
"github.com/chasefleming/elem-go/styles"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
)
|
||||
|
||||
var codeStyleRegisterWebAPI = styles.Props{
|
||||
@@ -15,7 +16,7 @@ var codeStyleRegisterWebAPI = styles.Props{
|
||||
styles.BackgroundColor: "#eee",
|
||||
}
|
||||
|
||||
func RegisterWeb(key string) *elem.Element {
|
||||
func RegisterWeb(registrationID types.RegistrationID) *elem.Element {
|
||||
return HtmlStructure(
|
||||
elem.Title(nil, elem.Text("Registration - Headscale")),
|
||||
elem.Body(attrs.Props{
|
||||
@@ -27,7 +28,7 @@ func RegisterWeb(key string) *elem.Element {
|
||||
elem.H2(nil, elem.Text("Machine registration")),
|
||||
elem.P(nil, elem.Text("Run the command below in the headscale server to add this machine to your network: ")),
|
||||
elem.Code(attrs.Props{attrs.Style: codeStyleRegisterWebAPI.ToInline()},
|
||||
elem.Text(fmt.Sprintf("headscale nodes register --user USERNAME --key %s", key)),
|
||||
elem.Text(fmt.Sprintf("headscale nodes register --user USERNAME --key %s", registrationID.String())),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -3,8 +3,10 @@ package types
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/ctxkey"
|
||||
)
|
||||
@@ -100,6 +102,20 @@ func (su *StateUpdate) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func StateSelf(nodeID NodeID) StateUpdate {
|
||||
return StateUpdate{
|
||||
Type: StateSelfUpdate,
|
||||
ChangeNodes: []NodeID{nodeID},
|
||||
}
|
||||
}
|
||||
|
||||
func StateUpdatePeerAdded(nodeIDs ...NodeID) StateUpdate {
|
||||
return StateUpdate{
|
||||
Type: StatePeerChanged,
|
||||
ChangeNodes: nodeIDs,
|
||||
}
|
||||
}
|
||||
|
||||
func StateUpdateExpire(nodeID NodeID, expiry time.Time) StateUpdate {
|
||||
return StateUpdate{
|
||||
Type: StatePeerChangedPatch,
|
||||
@@ -123,3 +139,40 @@ func NotifyCtx(ctx context.Context, origin, hostname string) context.Context {
|
||||
ctx2 = NotifyHostnameKey.WithValue(ctx2, hostname)
|
||||
return ctx2
|
||||
}
|
||||
|
||||
const RegistrationIDLength = 24
|
||||
|
||||
type RegistrationID string
|
||||
|
||||
func NewRegistrationID() (RegistrationID, error) {
|
||||
rid, err := util.GenerateRandomStringURLSafe(RegistrationIDLength)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return RegistrationID(rid), nil
|
||||
}
|
||||
|
||||
func MustRegistrationID() RegistrationID {
|
||||
rid, err := NewRegistrationID()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return rid
|
||||
}
|
||||
|
||||
func RegistrationIDFromString(str string) (RegistrationID, error) {
|
||||
if len(str) != RegistrationIDLength {
|
||||
return "", fmt.Errorf("registration ID must be %d characters long", RegistrationIDLength)
|
||||
}
|
||||
return RegistrationID(str), nil
|
||||
}
|
||||
|
||||
func (r RegistrationID) String() string {
|
||||
return string(r)
|
||||
}
|
||||
|
||||
type RegisterNode struct {
|
||||
Node Node
|
||||
Registered chan struct{}
|
||||
}
|
||||
|
||||
@@ -26,11 +26,14 @@ import (
|
||||
const (
|
||||
defaultOIDCExpiryTime = 180 * 24 * time.Hour // 180 Days
|
||||
maxDuration time.Duration = 1<<63 - 1
|
||||
PKCEMethodPlain string = "plain"
|
||||
PKCEMethodS256 string = "S256"
|
||||
)
|
||||
|
||||
var (
|
||||
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")
|
||||
errInvalidPKCEMethod = errors.New("pkce.method must be either 'plain' or 'S256'")
|
||||
)
|
||||
|
||||
type IPAllocationStrategy string
|
||||
@@ -162,6 +165,11 @@ type LetsEncryptConfig struct {
|
||||
ChallengeType string
|
||||
}
|
||||
|
||||
type PKCEConfig struct {
|
||||
Enabled bool
|
||||
Method string
|
||||
}
|
||||
|
||||
type OIDCConfig struct {
|
||||
OnlyStartIfOIDCIsAvailable bool
|
||||
Issuer string
|
||||
@@ -176,6 +184,7 @@ type OIDCConfig struct {
|
||||
Expiry time.Duration
|
||||
UseExpiryFromToken bool
|
||||
MapLegacyUsers bool
|
||||
PKCE PKCEConfig
|
||||
}
|
||||
|
||||
type DERPConfig struct {
|
||||
@@ -211,6 +220,10 @@ type PolicyConfig struct {
|
||||
Mode PolicyMode
|
||||
}
|
||||
|
||||
func (p *PolicyConfig) IsEmpty() bool {
|
||||
return p.Mode == PolicyModeFile && p.Path == ""
|
||||
}
|
||||
|
||||
type LogConfig struct {
|
||||
Format string
|
||||
Level zerolog.Level
|
||||
@@ -222,6 +235,24 @@ type Tuning struct {
|
||||
NodeMapSessionBufferedChanSize int
|
||||
}
|
||||
|
||||
func validatePKCEMethod(method string) error {
|
||||
if method != PKCEMethodPlain && method != PKCEMethodS256 {
|
||||
return errInvalidPKCEMethod
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Domain returns the hostname/domain part of the ServerURL.
|
||||
// If the ServerURL is not a valid URL, it returns the BaseDomain.
|
||||
func (c *Config) Domain() string {
|
||||
u, err := url.Parse(c.ServerURL)
|
||||
if err != nil {
|
||||
return c.BaseDomain
|
||||
}
|
||||
|
||||
return u.Hostname()
|
||||
}
|
||||
|
||||
// LoadConfig prepares and loads the Headscale configuration into Viper.
|
||||
// This means it sets the default values, reads the configuration file and
|
||||
// environment variables, and handles deprecated configuration options.
|
||||
@@ -288,7 +319,9 @@ func LoadConfig(path string, isFile bool) error {
|
||||
viper.SetDefault("oidc.only_start_if_oidc_is_available", true)
|
||||
viper.SetDefault("oidc.expiry", "180d")
|
||||
viper.SetDefault("oidc.use_expiry_from_token", false)
|
||||
viper.SetDefault("oidc.map_legacy_users", true)
|
||||
viper.SetDefault("oidc.map_legacy_users", false)
|
||||
viper.SetDefault("oidc.pkce.enabled", false)
|
||||
viper.SetDefault("oidc.pkce.method", "S256")
|
||||
|
||||
viper.SetDefault("logtail.enabled", false)
|
||||
viper.SetDefault("randomize_client_port", false)
|
||||
@@ -336,6 +369,12 @@ func validateServerConfig() error {
|
||||
// after #2170 is cleaned up
|
||||
// depr.fatal("oidc.strip_email_domain")
|
||||
|
||||
if viper.GetBool("oidc.enabled") {
|
||||
if err := validatePKCEMethod(viper.GetString("oidc.pkce.method")); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
depr.Log()
|
||||
|
||||
for _, removed := range []string{
|
||||
@@ -589,7 +628,7 @@ func dns() (DNSConfig, error) {
|
||||
// UnmarshalKey is compatible with Environment Variables.
|
||||
// err := viper.UnmarshalKey("dns", &dns)
|
||||
// if err != nil {
|
||||
// return DNSConfig{}, fmt.Errorf("unmarshaling dns config: %w", err)
|
||||
// return DNSConfig{}, fmt.Errorf("unmarshalling dns config: %w", err)
|
||||
// }
|
||||
|
||||
dns.MagicDNS = viper.GetBool("dns.magic_dns")
|
||||
@@ -604,7 +643,7 @@ func dns() (DNSConfig, error) {
|
||||
|
||||
err := viper.UnmarshalKey("dns.extra_records", &extraRecords)
|
||||
if err != nil {
|
||||
return DNSConfig{}, fmt.Errorf("unmarshaling dns extra records: %w", err)
|
||||
return DNSConfig{}, fmt.Errorf("unmarshalling dns extra records: %w", err)
|
||||
}
|
||||
dns.ExtraRecords = extraRecords
|
||||
}
|
||||
@@ -924,6 +963,10 @@ func LoadServerConfig() (*Config, error) {
|
||||
// after #2170 is cleaned up
|
||||
StripEmaildomain: viper.GetBool("oidc.strip_email_domain"),
|
||||
MapLegacyUsers: viper.GetBool("oidc.map_legacy_users"),
|
||||
PKCE: PKCEConfig{
|
||||
Enabled: viper.GetBool("oidc.pkce.enabled"),
|
||||
Method: viper.GetString("oidc.pkce.method"),
|
||||
},
|
||||
},
|
||||
|
||||
LogTail: logTailConfig,
|
||||
|
||||
@@ -77,9 +77,12 @@ type Node struct {
|
||||
|
||||
ForcedTags []string `gorm:"serializer:json"`
|
||||
|
||||
// TODO(kradalby): This seems like irrelevant information?
|
||||
AuthKeyID *uint64 `sql:"DEFAULT:NULL"`
|
||||
AuthKey *PreAuthKey `gorm:"constraint:OnDelete:SET NULL;"`
|
||||
// When a node has been created with a PreAuthKey, we need to
|
||||
// prevent the preauthkey from being deleted before the node.
|
||||
// The preauthkey can define "tags" of the node so we need it
|
||||
// around.
|
||||
AuthKeyID *uint64 `sql:"DEFAULT:NULL"`
|
||||
AuthKey *PreAuthKey
|
||||
|
||||
LastSeen *time.Time
|
||||
Expiry *time.Time
|
||||
@@ -105,7 +108,7 @@ func (node *Node) GivenNameHasBeenChanged() bool {
|
||||
// IsExpired returns whether the node registration has expired.
|
||||
func (node Node) IsExpired() bool {
|
||||
// If Expiry is not set, the client has not indicated that
|
||||
// it wants an expiry time, it is therefor considered
|
||||
// it wants an expiry time, it is therefore considered
|
||||
// to mean "not expired"
|
||||
if node.Expiry == nil || node.Expiry.IsZero() {
|
||||
return false
|
||||
@@ -180,7 +183,7 @@ func (node *Node) CanAccess(filter []tailcfg.FilterRule, node2 *Node) bool {
|
||||
src := node.IPs()
|
||||
allowedIPs := node2.IPs()
|
||||
|
||||
// TODO(kradalby): Regenerate this everytime the filter change, instead of
|
||||
// TODO(kradalby): Regenerate this every time the filter change, instead of
|
||||
// every time we use it.
|
||||
matchers := make([]matcher.Match, len(filter))
|
||||
for i, rule := range filter {
|
||||
|
||||
@@ -14,7 +14,7 @@ type PreAuthKey struct {
|
||||
ID uint64 `gorm:"primary_key"`
|
||||
Key string
|
||||
UserID uint
|
||||
User User `gorm:"constraint:OnDelete:CASCADE;"`
|
||||
User User `gorm:"constraint:OnDelete:SET NULL;"`
|
||||
Reusable bool
|
||||
Ephemeral bool `gorm:"default:false"`
|
||||
Used bool `gorm:"default:false"`
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
type Route struct {
|
||||
gorm.Model
|
||||
|
||||
NodeID uint64
|
||||
NodeID uint64 `gorm:"not null"`
|
||||
Node *Node
|
||||
|
||||
// TODO(kradalby): change this custom type to netip.Prefix
|
||||
@@ -79,7 +79,6 @@ func (rs Routes) Proto() []*v1.Route {
|
||||
for _, route := range rs {
|
||||
protoRoute := v1.Route{
|
||||
Id: uint64(route.ID),
|
||||
Node: route.Node.Proto(),
|
||||
Prefix: route.Prefix.String(),
|
||||
Advertised: route.Advertised,
|
||||
Enabled: route.Enabled,
|
||||
@@ -88,6 +87,10 @@ func (rs Routes) Proto() []*v1.Route {
|
||||
UpdatedAt: timestamppb.New(route.UpdatedAt),
|
||||
}
|
||||
|
||||
if route.Node != nil {
|
||||
protoRoute.Node = route.Node.Proto()
|
||||
}
|
||||
|
||||
if route.DeletedAt.Valid {
|
||||
protoRoute.DeletedAt = timestamppb.New(route.DeletedAt.Time)
|
||||
}
|
||||
|
||||
@@ -3,11 +3,14 @@ package types
|
||||
import (
|
||||
"cmp"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"strconv"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"github.com/rs/zerolog/log"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
"gorm.io/gorm"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -26,8 +29,9 @@ type User struct {
|
||||
// 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
|
||||
// Name (username) for the user, is used if email is empty
|
||||
// Should not be used, please use Username().
|
||||
// It is unique if ProviderIdentifier is not set.
|
||||
Name string
|
||||
|
||||
// Typically the full name of the user
|
||||
@@ -37,9 +41,11 @@ type User struct {
|
||||
// Should not be used, please use Username().
|
||||
Email string
|
||||
|
||||
// Unique identifier of the user from OIDC,
|
||||
// comes from `sub` claim in the OIDC token
|
||||
// and is used to lookup the user.
|
||||
// ProviderIdentifier is a unique or not set identifier of the
|
||||
// user from OIDC. It is the combination of `iss`
|
||||
// and `sub` claim in the OIDC token.
|
||||
// It is unique if set.
|
||||
// It is unique together with Name.
|
||||
ProviderIdentifier sql.NullString
|
||||
|
||||
// Provider is the origin of the user account,
|
||||
@@ -74,10 +80,8 @@ func (u *User) profilePicURL() string {
|
||||
func (u *User) TailscaleUser() *tailcfg.User {
|
||||
user := tailcfg.User{
|
||||
ID: tailcfg.UserID(u.ID),
|
||||
LoginName: u.Username(),
|
||||
DisplayName: u.DisplayNameOrUsername(),
|
||||
ProfilePicURL: u.profilePicURL(),
|
||||
Logins: []tailcfg.LoginID{},
|
||||
Created: u.CreatedAt,
|
||||
}
|
||||
|
||||
@@ -119,18 +123,49 @@ func (u *User) Proto() *v1.User {
|
||||
}
|
||||
}
|
||||
|
||||
// JumpCloud returns a JSON where email_verified is returned as a
|
||||
// string "true" or "false" instead of a boolean.
|
||||
// This maps bool to a specific type with a custom unmarshaler to
|
||||
// ensure we can decode it from a string.
|
||||
// https://github.com/juanfont/headscale/issues/2293
|
||||
type FlexibleBoolean bool
|
||||
|
||||
func (bit *FlexibleBoolean) UnmarshalJSON(data []byte) error {
|
||||
var val interface{}
|
||||
err := json.Unmarshal(data, &val)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not unmarshal data: %w", err)
|
||||
}
|
||||
|
||||
switch v := val.(type) {
|
||||
case bool:
|
||||
*bit = FlexibleBoolean(v)
|
||||
case string:
|
||||
pv, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse %s as boolean: %w", v, err)
|
||||
}
|
||||
*bit = FlexibleBoolean(pv)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("could not parse %v as boolean", v)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type OIDCClaims struct {
|
||||
// Sub is the user's unique identifier at the provider.
|
||||
Sub string `json:"sub"`
|
||||
Iss string `json:"iss"`
|
||||
|
||||
// Name is the user's full name.
|
||||
Name string `json:"name,omitempty"`
|
||||
Groups []string `json:"groups,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
EmailVerified bool `json:"email_verified,omitempty"`
|
||||
ProfilePictureURL string `json:"picture,omitempty"`
|
||||
Username string `json:"preferred_username,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Groups []string `json:"groups,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
EmailVerified FlexibleBoolean `json:"email_verified,omitempty"`
|
||||
ProfilePictureURL string `json:"picture,omitempty"`
|
||||
Username string `json:"preferred_username,omitempty"`
|
||||
}
|
||||
|
||||
func (c *OIDCClaims) Identifier() string {
|
||||
@@ -140,9 +175,11 @@ func (c *OIDCClaims) Identifier() string {
|
||||
// FromClaim overrides a User from OIDC claims.
|
||||
// All fields will be updated, except for the ID.
|
||||
func (u *User) FromClaim(claims *OIDCClaims) {
|
||||
err := util.CheckForFQDNRules(claims.Username)
|
||||
err := util.ValidateUsername(claims.Username)
|
||||
if err == nil {
|
||||
u.Name = claims.Username
|
||||
} else {
|
||||
log.Debug().Err(err).Msgf("Username %s is not valid", claims.Username)
|
||||
}
|
||||
|
||||
if claims.EmailVerified {
|
||||
|
||||
223
hscontrol/types/users_test.go
Normal file
223
hscontrol/types/users_test.go
Normal file
@@ -0,0 +1,223 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
)
|
||||
|
||||
func TestUnmarshallOIDCClaims(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
jsonstr string
|
||||
want OIDCClaims
|
||||
}{
|
||||
{
|
||||
name: "normal-bool",
|
||||
jsonstr: `
|
||||
{
|
||||
"sub": "test",
|
||||
"email": "test@test.no",
|
||||
"email_verified": true
|
||||
}
|
||||
`,
|
||||
want: OIDCClaims{
|
||||
Sub: "test",
|
||||
Email: "test@test.no",
|
||||
EmailVerified: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "string-bool-true",
|
||||
jsonstr: `
|
||||
{
|
||||
"sub": "test2",
|
||||
"email": "test2@test.no",
|
||||
"email_verified": "true"
|
||||
}
|
||||
`,
|
||||
want: OIDCClaims{
|
||||
Sub: "test2",
|
||||
Email: "test2@test.no",
|
||||
EmailVerified: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "string-bool-false",
|
||||
jsonstr: `
|
||||
{
|
||||
"sub": "test3",
|
||||
"email": "test3@test.no",
|
||||
"email_verified": "false"
|
||||
}
|
||||
`,
|
||||
want: OIDCClaims{
|
||||
Sub: "test3",
|
||||
Email: "test3@test.no",
|
||||
EmailVerified: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var got OIDCClaims
|
||||
if err := json.Unmarshal([]byte(tt.jsonstr), &got); err != nil {
|
||||
t.Errorf("UnmarshallOIDCClaims() error = %v", err)
|
||||
return
|
||||
}
|
||||
if diff := cmp.Diff(got, tt.want); diff != "" {
|
||||
t.Errorf("UnmarshallOIDCClaims() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOIDCClaimsJSONToUser(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
jsonstr string
|
||||
want User
|
||||
}{
|
||||
{
|
||||
name: "normal-bool",
|
||||
jsonstr: `
|
||||
{
|
||||
"sub": "test",
|
||||
"email": "test@test.no",
|
||||
"email_verified": true
|
||||
}
|
||||
`,
|
||||
want: User{
|
||||
Provider: util.RegisterMethodOIDC,
|
||||
Email: "test@test.no",
|
||||
ProviderIdentifier: sql.NullString{
|
||||
String: "/test",
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "string-bool-true",
|
||||
jsonstr: `
|
||||
{
|
||||
"sub": "test2",
|
||||
"email": "test2@test.no",
|
||||
"email_verified": "true"
|
||||
}
|
||||
`,
|
||||
want: User{
|
||||
Provider: util.RegisterMethodOIDC,
|
||||
Email: "test2@test.no",
|
||||
ProviderIdentifier: sql.NullString{
|
||||
String: "/test2",
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "string-bool-false",
|
||||
jsonstr: `
|
||||
{
|
||||
"sub": "test3",
|
||||
"email": "test3@test.no",
|
||||
"email_verified": "false"
|
||||
}
|
||||
`,
|
||||
want: User{
|
||||
Provider: util.RegisterMethodOIDC,
|
||||
ProviderIdentifier: sql.NullString{
|
||||
String: "/test3",
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// From https://github.com/juanfont/headscale/issues/2333
|
||||
name: "okta-oidc-claim-20250121",
|
||||
jsonstr: `
|
||||
{
|
||||
"sub": "00u7dr4qp7XXXXXXXXXX",
|
||||
"name": "Tim Horton",
|
||||
"email": "tim.horton@company.com",
|
||||
"ver": 1,
|
||||
"iss": "https://sso.company.com/oauth2/default",
|
||||
"aud": "0oa8neto4tXXXXXXXXXX",
|
||||
"iat": 1737455152,
|
||||
"exp": 1737458752,
|
||||
"jti": "ID.zzJz93koTunMKv5Bq-XXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||
"amr": [
|
||||
"pwd"
|
||||
],
|
||||
"idp": "00o42r3s2cXXXXXXXX",
|
||||
"nonce": "nonce",
|
||||
"preferred_username": "tim.horton@company.com",
|
||||
"auth_time": 1000,
|
||||
"at_hash": "preview_at_hash"
|
||||
}
|
||||
`,
|
||||
want: User{
|
||||
Provider: util.RegisterMethodOIDC,
|
||||
DisplayName: "Tim Horton",
|
||||
Name: "tim.horton@company.com",
|
||||
ProviderIdentifier: sql.NullString{
|
||||
String: "https://sso.company.com/oauth2/default/00u7dr4qp7XXXXXXXXXX",
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// From https://github.com/juanfont/headscale/issues/2333
|
||||
name: "okta-oidc-claim-20250121",
|
||||
jsonstr: `
|
||||
{
|
||||
"aud": "79xxxxxx-xxxx-xxxx-xxxx-892146xxxxxx",
|
||||
"iss": "https://login.microsoftonline.com//v2.0",
|
||||
"iat": 1737346441,
|
||||
"nbf": 1737346441,
|
||||
"exp": 1737350341,
|
||||
"aio": "AWQAm/8ZAAAABKne9EWr6ygVO2DbcRmoPIpRM819qqlP/mmK41AAWv/C2tVkld4+znbG8DaXFdLQa9jRUzokvsT7rt9nAT6Fg7QC+/ecDWsF5U+QX11f9Ox7ZkK4UAIWFcIXpuZZvRS7",
|
||||
"email": "user@domain.com",
|
||||
"name": "XXXXXX XXXX",
|
||||
"oid": "54c2323d-5052-4130-9588-ad751909003f",
|
||||
"preferred_username": "user@domain.com",
|
||||
"rh": "1.AXUAXdg0Rfc11UifLDJv67ChfSluoXmD9z1EmK-JIUYuSK9cAQl1AA.",
|
||||
"sid": "5250a0a2-0b4e-4e68-8652-b4e97866411d",
|
||||
"sub": "I-70OQnj3TogrNSfkZQqB3f7dGwyBWSm1dolHNKrMzQ",
|
||||
"tid": "<redacted>",
|
||||
"uti": "zAuXeEtMM0GwcTAcOsBZAA",
|
||||
"ver": "2.0"
|
||||
}
|
||||
`,
|
||||
want: User{
|
||||
Provider: util.RegisterMethodOIDC,
|
||||
DisplayName: "XXXXXX XXXX",
|
||||
Name: "user@domain.com",
|
||||
ProviderIdentifier: sql.NullString{
|
||||
String: "https://login.microsoftonline.com//v2.0/I-70OQnj3TogrNSfkZQqB3f7dGwyBWSm1dolHNKrMzQ",
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var got OIDCClaims
|
||||
if err := json.Unmarshal([]byte(tt.jsonstr), &got); err != nil {
|
||||
t.Errorf("TestOIDCClaimsJSONToUser() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var user User
|
||||
|
||||
user.FromClaim(&got)
|
||||
if diff := cmp.Diff(user, tt.want); diff != "" {
|
||||
t.Errorf("TestOIDCClaimsJSONToUser() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net/netip"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"go4.org/netipx"
|
||||
"tailscale.com/util/dnsname"
|
||||
@@ -20,11 +21,55 @@ const (
|
||||
LabelHostnameLength = 63
|
||||
)
|
||||
|
||||
var invalidDNSRegex = regexp.MustCompile("[^a-z0-9-.]+")
|
||||
var invalidCharsInUserRegex = regexp.MustCompile("[^a-z0-9-.]+")
|
||||
|
||||
var ErrInvalidUserName = errors.New("invalid user name")
|
||||
|
||||
// ValidateUsername checks if a username is valid.
|
||||
// It must be at least 2 characters long, start with a letter, and contain
|
||||
// only letters, numbers, hyphens, dots, and underscores.
|
||||
// It cannot contain more than one '@'.
|
||||
// It cannot contain invalid characters.
|
||||
func ValidateUsername(username string) error {
|
||||
// Ensure the username meets the minimum length requirement
|
||||
if len(username) < 2 {
|
||||
return errors.New("username must be at least 2 characters long")
|
||||
}
|
||||
|
||||
// Ensure the username does not start with a number
|
||||
if unicode.IsDigit(rune(username[0])) {
|
||||
return errors.New("username cannot start with a number")
|
||||
}
|
||||
|
||||
atCount := 0
|
||||
for _, char := range username {
|
||||
switch {
|
||||
case unicode.IsLetter(char),
|
||||
unicode.IsDigit(char),
|
||||
char == '-',
|
||||
char == '.',
|
||||
char == '_':
|
||||
// Valid characters
|
||||
case char == '@':
|
||||
atCount++
|
||||
if atCount > 1 {
|
||||
return errors.New("username cannot contain more than one '@'")
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("username contains invalid character: '%c'", char)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CheckForFQDNRules(name string) error {
|
||||
// Ensure the username meets the minimum length requirement
|
||||
if len(name) < 2 {
|
||||
return errors.New("name must be at least 2 characters long")
|
||||
}
|
||||
|
||||
if len(name) > LabelHostnameLength {
|
||||
return fmt.Errorf(
|
||||
"DNS segment must not be over 63 chars. %v doesn't comply with this rule: %w",
|
||||
@@ -39,9 +84,9 @@ func CheckForFQDNRules(name string) error {
|
||||
ErrInvalidUserName,
|
||||
)
|
||||
}
|
||||
if invalidCharsInUserRegex.MatchString(name) {
|
||||
if invalidDNSRegex.MatchString(name) {
|
||||
return fmt.Errorf(
|
||||
"DNS segment should only be composed of lowercase ASCII letters numbers, hyphen and dots. %v doesn't comply with theses rules: %w",
|
||||
"DNS segment should only be composed of lowercase ASCII letters numbers, hyphen and dots. %v doesn't comply with these rules: %w",
|
||||
name,
|
||||
ErrInvalidUserName,
|
||||
)
|
||||
@@ -52,7 +97,7 @@ func CheckForFQDNRules(name string) error {
|
||||
|
||||
func ConvertWithFQDNRules(name string) string {
|
||||
name = strings.ToLower(name)
|
||||
name = invalidCharsInUserRegex.ReplaceAllString(name, "")
|
||||
name = invalidDNSRegex.ReplaceAllString(name, "")
|
||||
|
||||
return name
|
||||
}
|
||||
@@ -197,7 +242,7 @@ func NormalizeToFQDNRules(name string, stripEmailDomain bool) (string, error) {
|
||||
} else {
|
||||
name = strings.ReplaceAll(name, "@", ".")
|
||||
}
|
||||
name = invalidCharsInUserRegex.ReplaceAllString(name, "-")
|
||||
name = invalidDNSRegex.ReplaceAllString(name, "-")
|
||||
|
||||
for _, elt := range strings.Split(name, ".") {
|
||||
if len(elt) > LabelHostnameLength {
|
||||
|
||||
@@ -32,7 +32,8 @@ func GenerateRandomBytes(n int) ([]byte, error) {
|
||||
func GenerateRandomStringURLSafe(n int) (string, error) {
|
||||
b, err := GenerateRandomBytes(n)
|
||||
|
||||
return base64.RawURLEncoding.EncodeToString(b), err
|
||||
uenc := base64.RawURLEncoding.EncodeToString(b)
|
||||
return uenc[:n], err
|
||||
}
|
||||
|
||||
// GenerateRandomStringDNSSafe returns a DNS-safe
|
||||
|
||||
@@ -119,12 +119,12 @@ func TestACLHostsInNetMapTable(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}, want: map[string]int{
|
||||
"user1": 3, // ns1 + ns2
|
||||
"user2": 3, // ns2 + ns1
|
||||
"user1@test.no": 3, // ns1 + ns2
|
||||
"user2@test.no": 3, // ns2 + ns1
|
||||
},
|
||||
},
|
||||
// Test that when we have two users, which cannot see
|
||||
// eachother, each node has only the number of pairs from
|
||||
// each other, each node has only the number of pairs from
|
||||
// their own user.
|
||||
"two-isolated-users": {
|
||||
users: map[string]int{
|
||||
@@ -145,8 +145,8 @@ func TestACLHostsInNetMapTable(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}, want: map[string]int{
|
||||
"user1": 1,
|
||||
"user2": 1,
|
||||
"user1@test.no": 1,
|
||||
"user2@test.no": 1,
|
||||
},
|
||||
},
|
||||
// Test that when we have two users, with ACLs and they
|
||||
@@ -181,8 +181,8 @@ func TestACLHostsInNetMapTable(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}, want: map[string]int{
|
||||
"user1": 3,
|
||||
"user2": 3,
|
||||
"user1@test.no": 3,
|
||||
"user2@test.no": 3,
|
||||
},
|
||||
},
|
||||
// Test that when we have two users, that are isolated,
|
||||
@@ -213,8 +213,8 @@ func TestACLHostsInNetMapTable(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}, want: map[string]int{
|
||||
"user1": 3, // ns1 + ns2
|
||||
"user2": 3, // ns1 + ns2 (return path)
|
||||
"user1@test.no": 3, // ns1 + ns2
|
||||
"user2@test.no": 3, // ns1 + ns2 (return path)
|
||||
},
|
||||
},
|
||||
"very-large-destination-prefix-1372": {
|
||||
@@ -241,8 +241,8 @@ func TestACLHostsInNetMapTable(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}, want: map[string]int{
|
||||
"user1": 3, // ns1 + ns2
|
||||
"user2": 3, // ns1 + ns2 (return path)
|
||||
"user1@test.no": 3, // ns1 + ns2
|
||||
"user2@test.no": 3, // ns1 + ns2 (return path)
|
||||
},
|
||||
},
|
||||
"ipv6-acls-1470": {
|
||||
@@ -259,8 +259,8 @@ func TestACLHostsInNetMapTable(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}, want: map[string]int{
|
||||
"user1": 3, // ns1 + ns2
|
||||
"user2": 3, // ns2 + ns1
|
||||
"user1@test.no": 3, // ns1 + ns2
|
||||
"user2@test.no": 3, // ns2 + ns1
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -282,7 +282,7 @@ func TestACLHostsInNetMapTable(t *testing.T) {
|
||||
allClients, err := scenario.ListTailscaleClients()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = scenario.WaitForTailscaleSyncWithPeerCount(testCase.want["user1"])
|
||||
err = scenario.WaitForTailscaleSyncWithPeerCount(testCase.want["user1@test.no"])
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, client := range allClients {
|
||||
|
||||
230
integration/auth_key_test.go
Normal file
230
integration/auth_key_test.go
Normal file
@@ -0,0 +1,230 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/juanfont/headscale/integration/hsic"
|
||||
"github.com/juanfont/headscale/integration/tsic"
|
||||
"github.com/samber/lo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAuthKeyLogoutAndReloginSameUser(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
t.Parallel()
|
||||
|
||||
for _, https := range []bool{true, false} {
|
||||
t.Run(fmt.Sprintf("with-https-%t", https), func(t *testing.T) {
|
||||
scenario, err := NewScenario(dockertestMaxWait())
|
||||
assertNoErr(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
spec := map[string]int{
|
||||
"user1": len(MustTestVersions),
|
||||
"user2": len(MustTestVersions),
|
||||
}
|
||||
|
||||
opts := []hsic.Option{hsic.WithTestName("pingallbyip")}
|
||||
if https {
|
||||
opts = append(opts, []hsic.Option{
|
||||
hsic.WithTLS(),
|
||||
}...)
|
||||
}
|
||||
|
||||
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, opts...)
|
||||
assertNoErrHeadscaleEnv(t, err)
|
||||
|
||||
allClients, err := scenario.ListTailscaleClients()
|
||||
assertNoErrListClients(t, err)
|
||||
|
||||
err = scenario.WaitForTailscaleSync()
|
||||
assertNoErrSync(t, err)
|
||||
|
||||
// assertClientsState(t, allClients)
|
||||
|
||||
clientIPs := make(map[TailscaleClient][]netip.Addr)
|
||||
for _, client := range allClients {
|
||||
ips, err := client.IPs()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get IPs for client %s: %s", client.Hostname(), err)
|
||||
}
|
||||
clientIPs[client] = ips
|
||||
}
|
||||
|
||||
headscale, err := scenario.Headscale()
|
||||
assertNoErrGetHeadscale(t, err)
|
||||
|
||||
listNodes, err := headscale.ListNodes()
|
||||
assert.Equal(t, len(listNodes), len(allClients))
|
||||
nodeCountBeforeLogout := len(listNodes)
|
||||
t.Logf("node count before logout: %d", nodeCountBeforeLogout)
|
||||
|
||||
for _, client := range allClients {
|
||||
err := client.Logout()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to logout client %s: %s", client.Hostname(), err)
|
||||
}
|
||||
}
|
||||
|
||||
err = scenario.WaitForTailscaleLogout()
|
||||
assertNoErrLogout(t, err)
|
||||
|
||||
t.Logf("all clients logged out")
|
||||
|
||||
// if the server is not running with HTTPS, we have to wait a bit before
|
||||
// reconnection as the newest Tailscale client has a measure that will only
|
||||
// reconnect over HTTPS if they saw a noise connection previously.
|
||||
// https://github.com/tailscale/tailscale/commit/1eaad7d3deb0815e8932e913ca1a862afa34db38
|
||||
// https://github.com/juanfont/headscale/issues/2164
|
||||
if !https {
|
||||
time.Sleep(5 * time.Minute)
|
||||
}
|
||||
|
||||
for userName := range spec {
|
||||
key, err := scenario.CreatePreAuthKey(userName, true, false)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create pre-auth key for user %s: %s", userName, err)
|
||||
}
|
||||
|
||||
err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.GetKey())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to run tailscale up for user %s: %s", userName, err)
|
||||
}
|
||||
}
|
||||
|
||||
listNodes, err = headscale.ListNodes()
|
||||
require.Equal(t, nodeCountBeforeLogout, len(listNodes))
|
||||
|
||||
allIps, err := scenario.ListTailscaleClientsIPs()
|
||||
assertNoErrListClientIPs(t, err)
|
||||
|
||||
allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string {
|
||||
return x.String()
|
||||
})
|
||||
|
||||
success := pingAllHelper(t, allClients, allAddrs)
|
||||
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
|
||||
|
||||
for _, client := range allClients {
|
||||
ips, err := client.IPs()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get IPs for client %s: %s", client.Hostname(), err)
|
||||
}
|
||||
|
||||
// lets check if the IPs are the same
|
||||
if len(ips) != len(clientIPs[client]) {
|
||||
t.Fatalf("IPs changed for client %s", client.Hostname())
|
||||
}
|
||||
|
||||
for _, ip := range ips {
|
||||
found := false
|
||||
for _, oldIP := range clientIPs[client] {
|
||||
if ip == oldIP {
|
||||
found = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Fatalf(
|
||||
"IPs changed for client %s. Used to be %v now %v",
|
||||
client.Hostname(),
|
||||
clientIPs[client],
|
||||
ips,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// This test will first log in two sets of nodes to two sets of users, then
|
||||
// it will log out all users from user2 and log them in as user1.
|
||||
// This should leave us with all nodes connected to user1, while user2
|
||||
// still has nodes, but they are not connected.
|
||||
func TestAuthKeyLogoutAndReloginNewUser(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
t.Parallel()
|
||||
|
||||
scenario, err := NewScenario(dockertestMaxWait())
|
||||
assertNoErr(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
spec := map[string]int{
|
||||
"user1": len(MustTestVersions),
|
||||
"user2": len(MustTestVersions),
|
||||
}
|
||||
|
||||
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{},
|
||||
hsic.WithTestName("keyrelognewuser"),
|
||||
hsic.WithTLS(),
|
||||
)
|
||||
assertNoErrHeadscaleEnv(t, err)
|
||||
|
||||
allClients, err := scenario.ListTailscaleClients()
|
||||
assertNoErrListClients(t, err)
|
||||
|
||||
err = scenario.WaitForTailscaleSync()
|
||||
assertNoErrSync(t, err)
|
||||
|
||||
// assertClientsState(t, allClients)
|
||||
|
||||
headscale, err := scenario.Headscale()
|
||||
assertNoErrGetHeadscale(t, err)
|
||||
|
||||
listNodes, err := headscale.ListNodes()
|
||||
assert.Equal(t, len(listNodes), len(allClients))
|
||||
nodeCountBeforeLogout := len(listNodes)
|
||||
t.Logf("node count before logout: %d", nodeCountBeforeLogout)
|
||||
|
||||
for _, client := range allClients {
|
||||
err := client.Logout()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to logout client %s: %s", client.Hostname(), err)
|
||||
}
|
||||
}
|
||||
|
||||
err = scenario.WaitForTailscaleLogout()
|
||||
assertNoErrLogout(t, err)
|
||||
|
||||
t.Logf("all clients logged out")
|
||||
|
||||
// Create a new authkey for user1, to be used for all clients
|
||||
key, err := scenario.CreatePreAuthKey("user1", true, false)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create pre-auth key for user1: %s", err)
|
||||
}
|
||||
|
||||
// Log in all clients as user1, iterating over the spec only returns the
|
||||
// clients, not the usernames.
|
||||
for userName := range spec {
|
||||
err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.GetKey())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to run tailscale up for user %s: %s", userName, err)
|
||||
}
|
||||
}
|
||||
|
||||
user1Nodes, err := headscale.ListNodes("user1")
|
||||
assertNoErr(t, err)
|
||||
assert.Len(t, user1Nodes, len(allClients))
|
||||
|
||||
// Validate that all the old nodes are still present with user2
|
||||
user2Nodes, err := headscale.ListNodes("user2")
|
||||
assertNoErr(t, err)
|
||||
assert.Len(t, user2Nodes, len(allClients)/2)
|
||||
|
||||
for _, client := range allClients {
|
||||
status, err := client.Status()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get status for client %s: %s", client.Hostname(), err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "user1@test.no", status.User[status.Self.UserID].LoginName)
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,9 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"testing"
|
||||
@@ -54,7 +56,7 @@ func TestOIDCAuthenticationPingAll(t *testing.T) {
|
||||
scenario := AuthOIDCScenario{
|
||||
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
|
||||
@@ -89,7 +91,6 @@ func TestOIDCAuthenticationPingAll(t *testing.T) {
|
||||
hsic.WithTestName("oidcauthping"),
|
||||
hsic.WithConfigEnv(oidcMap),
|
||||
hsic.WithTLS(),
|
||||
hsic.WithHostnameAsServerURL(),
|
||||
hsic.WithFileInContainer("/tmp/hs_client_oidc_secret", []byte(oidcConfig.ClientSecret)),
|
||||
)
|
||||
assertNoErrHeadscaleEnv(t, err)
|
||||
@@ -115,23 +116,14 @@ func TestOIDCAuthenticationPingAll(t *testing.T) {
|
||||
headscale, err := scenario.Headscale()
|
||||
assertNoErr(t, err)
|
||||
|
||||
var listUsers []v1.User
|
||||
err = executeAndUnmarshal(headscale,
|
||||
[]string{
|
||||
"headscale",
|
||||
"users",
|
||||
"list",
|
||||
"--output",
|
||||
"json",
|
||||
},
|
||||
&listUsers,
|
||||
)
|
||||
listUsers, err := headscale.ListUsers()
|
||||
assertNoErr(t, err)
|
||||
|
||||
want := []v1.User{
|
||||
want := []*v1.User{
|
||||
{
|
||||
Id: 1,
|
||||
Name: "user1",
|
||||
Id: 1,
|
||||
Name: "user1",
|
||||
Email: "user1@test.no",
|
||||
},
|
||||
{
|
||||
Id: 2,
|
||||
@@ -141,8 +133,9 @@ func TestOIDCAuthenticationPingAll(t *testing.T) {
|
||||
ProviderId: oidcConfig.Issuer + "/user1",
|
||||
},
|
||||
{
|
||||
Id: 3,
|
||||
Name: "user2",
|
||||
Id: 3,
|
||||
Name: "user2",
|
||||
Email: "user2@test.no",
|
||||
},
|
||||
{
|
||||
Id: 4,
|
||||
@@ -202,7 +195,6 @@ func TestOIDCExpireNodesBasedOnTokenExpiry(t *testing.T) {
|
||||
spec,
|
||||
hsic.WithTestName("oidcexpirenodes"),
|
||||
hsic.WithConfigEnv(oidcMap),
|
||||
hsic.WithHostnameAsServerURL(),
|
||||
)
|
||||
assertNoErrHeadscaleEnv(t, err)
|
||||
|
||||
@@ -226,7 +218,7 @@ func TestOIDCExpireNodesBasedOnTokenExpiry(t *testing.T) {
|
||||
|
||||
// This is not great, but this sadly is a time dependent test, so the
|
||||
// safe thing to do is wait out the whole TTL time before checking if
|
||||
// the clients have logged out. The Wait function cant do it itself
|
||||
// the clients have logged out. The Wait function can't do it itself
|
||||
// as it has an upper bound of 1 min.
|
||||
time.Sleep(shortAccessTTL)
|
||||
|
||||
@@ -247,7 +239,7 @@ func TestOIDC024UserCreation(t *testing.T) {
|
||||
emailVerified bool
|
||||
cliUsers []string
|
||||
oidcUsers []string
|
||||
want func(iss string) []v1.User
|
||||
want func(iss string) []*v1.User
|
||||
}{
|
||||
{
|
||||
name: "no-migration-verified-email",
|
||||
@@ -257,11 +249,12 @@ func TestOIDC024UserCreation(t *testing.T) {
|
||||
emailVerified: true,
|
||||
cliUsers: []string{"user1", "user2"},
|
||||
oidcUsers: []string{"user1", "user2"},
|
||||
want: func(iss string) []v1.User {
|
||||
return []v1.User{
|
||||
want: func(iss string) []*v1.User {
|
||||
return []*v1.User{
|
||||
{
|
||||
Id: 1,
|
||||
Name: "user1",
|
||||
Id: 1,
|
||||
Name: "user1",
|
||||
Email: "user1@test.no",
|
||||
},
|
||||
{
|
||||
Id: 2,
|
||||
@@ -271,8 +264,9 @@ func TestOIDC024UserCreation(t *testing.T) {
|
||||
ProviderId: iss + "/user1",
|
||||
},
|
||||
{
|
||||
Id: 3,
|
||||
Name: "user2",
|
||||
Id: 3,
|
||||
Name: "user2",
|
||||
Email: "user2@test.no",
|
||||
},
|
||||
{
|
||||
Id: 4,
|
||||
@@ -292,11 +286,12 @@ func TestOIDC024UserCreation(t *testing.T) {
|
||||
emailVerified: false,
|
||||
cliUsers: []string{"user1", "user2"},
|
||||
oidcUsers: []string{"user1", "user2"},
|
||||
want: func(iss string) []v1.User {
|
||||
return []v1.User{
|
||||
want: func(iss string) []*v1.User {
|
||||
return []*v1.User{
|
||||
{
|
||||
Id: 1,
|
||||
Name: "user1",
|
||||
Id: 1,
|
||||
Name: "user1",
|
||||
Email: "user1@test.no",
|
||||
},
|
||||
{
|
||||
Id: 2,
|
||||
@@ -305,8 +300,9 @@ func TestOIDC024UserCreation(t *testing.T) {
|
||||
ProviderId: iss + "/user1",
|
||||
},
|
||||
{
|
||||
Id: 3,
|
||||
Name: "user2",
|
||||
Id: 3,
|
||||
Name: "user2",
|
||||
Email: "user2@test.no",
|
||||
},
|
||||
{
|
||||
Id: 4,
|
||||
@@ -326,8 +322,8 @@ func TestOIDC024UserCreation(t *testing.T) {
|
||||
emailVerified: true,
|
||||
cliUsers: []string{"user1", "user2"},
|
||||
oidcUsers: []string{"user1", "user2"},
|
||||
want: func(iss string) []v1.User {
|
||||
return []v1.User{
|
||||
want: func(iss string) []*v1.User {
|
||||
return []*v1.User{
|
||||
{
|
||||
Id: 1,
|
||||
Name: "user1",
|
||||
@@ -354,11 +350,12 @@ func TestOIDC024UserCreation(t *testing.T) {
|
||||
emailVerified: false,
|
||||
cliUsers: []string{"user1", "user2"},
|
||||
oidcUsers: []string{"user1", "user2"},
|
||||
want: func(iss string) []v1.User {
|
||||
return []v1.User{
|
||||
want: func(iss string) []*v1.User {
|
||||
return []*v1.User{
|
||||
{
|
||||
Id: 1,
|
||||
Name: "user1",
|
||||
Id: 1,
|
||||
Name: "user1",
|
||||
Email: "user1@test.no",
|
||||
},
|
||||
{
|
||||
Id: 2,
|
||||
@@ -367,8 +364,9 @@ func TestOIDC024UserCreation(t *testing.T) {
|
||||
ProviderId: iss + "/user1",
|
||||
},
|
||||
{
|
||||
Id: 3,
|
||||
Name: "user2",
|
||||
Id: 3,
|
||||
Name: "user2",
|
||||
Email: "user2@test.no",
|
||||
},
|
||||
{
|
||||
Id: 4,
|
||||
@@ -388,8 +386,8 @@ func TestOIDC024UserCreation(t *testing.T) {
|
||||
emailVerified: true,
|
||||
cliUsers: []string{"user1.headscale.net", "user2.headscale.net"},
|
||||
oidcUsers: []string{"user1", "user2"},
|
||||
want: func(iss string) []v1.User {
|
||||
return []v1.User{
|
||||
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"
|
||||
{
|
||||
@@ -418,11 +416,12 @@ func TestOIDC024UserCreation(t *testing.T) {
|
||||
emailVerified: false,
|
||||
cliUsers: []string{"user1.headscale.net", "user2.headscale.net"},
|
||||
oidcUsers: []string{"user1", "user2"},
|
||||
want: func(iss string) []v1.User {
|
||||
return []v1.User{
|
||||
want: func(iss string) []*v1.User {
|
||||
return []*v1.User{
|
||||
{
|
||||
Id: 1,
|
||||
Name: "user1.headscale.net",
|
||||
Id: 1,
|
||||
Name: "user1.headscale.net",
|
||||
Email: "user1.headscale.net@test.no",
|
||||
},
|
||||
{
|
||||
Id: 2,
|
||||
@@ -431,8 +430,9 @@ func TestOIDC024UserCreation(t *testing.T) {
|
||||
ProviderId: iss + "/user1",
|
||||
},
|
||||
{
|
||||
Id: 3,
|
||||
Name: "user2.headscale.net",
|
||||
Id: 3,
|
||||
Name: "user2.headscale.net",
|
||||
Email: "user2.headscale.net@test.no",
|
||||
},
|
||||
{
|
||||
Id: 4,
|
||||
@@ -485,7 +485,6 @@ func TestOIDC024UserCreation(t *testing.T) {
|
||||
hsic.WithTestName("oidcmigration"),
|
||||
hsic.WithConfigEnv(oidcMap),
|
||||
hsic.WithTLS(),
|
||||
hsic.WithHostnameAsServerURL(),
|
||||
hsic.WithFileInContainer("/tmp/hs_client_oidc_secret", []byte(oidcConfig.ClientSecret)),
|
||||
)
|
||||
assertNoErrHeadscaleEnv(t, err)
|
||||
@@ -500,17 +499,7 @@ func TestOIDC024UserCreation(t *testing.T) {
|
||||
|
||||
want := tt.want(oidcConfig.Issuer)
|
||||
|
||||
var listUsers []v1.User
|
||||
err = executeAndUnmarshal(headscale,
|
||||
[]string{
|
||||
"headscale",
|
||||
"users",
|
||||
"list",
|
||||
"--output",
|
||||
"json",
|
||||
},
|
||||
&listUsers,
|
||||
)
|
||||
listUsers, err := headscale.ListUsers()
|
||||
assertNoErr(t, err)
|
||||
|
||||
sort.Slice(listUsers, func(i, j int) bool {
|
||||
@@ -524,6 +513,290 @@ func TestOIDC024UserCreation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOIDCAuthenticationWithPKCE(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
t.Parallel()
|
||||
|
||||
baseScenario, err := NewScenario(dockertestMaxWait())
|
||||
assertNoErr(t, err)
|
||||
|
||||
scenario := AuthOIDCScenario{
|
||||
Scenario: baseScenario,
|
||||
}
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
// Single user with one node for testing PKCE flow
|
||||
spec := map[string]int{
|
||||
"user1": 1,
|
||||
}
|
||||
|
||||
mockusers := []mockoidc.MockUser{
|
||||
oidcMockUser("user1", true),
|
||||
}
|
||||
|
||||
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,
|
||||
"HEADSCALE_OIDC_CLIENT_SECRET_PATH": "${CREDENTIALS_DIRECTORY_TEST}/hs_client_oidc_secret",
|
||||
"CREDENTIALS_DIRECTORY_TEST": "/tmp",
|
||||
"HEADSCALE_OIDC_PKCE_ENABLED": "1", // Enable PKCE
|
||||
"HEADSCALE_OIDC_MAP_LEGACY_USERS": "0",
|
||||
"HEADSCALE_OIDC_STRIP_EMAIL_DOMAIN": "0",
|
||||
}
|
||||
|
||||
err = scenario.CreateHeadscaleEnv(
|
||||
spec,
|
||||
hsic.WithTestName("oidcauthpkce"),
|
||||
hsic.WithConfigEnv(oidcMap),
|
||||
hsic.WithTLS(),
|
||||
hsic.WithFileInContainer("/tmp/hs_client_oidc_secret", []byte(oidcConfig.ClientSecret)),
|
||||
)
|
||||
assertNoErrHeadscaleEnv(t, err)
|
||||
|
||||
// Get all clients and verify they can connect
|
||||
allClients, err := scenario.ListTailscaleClients()
|
||||
assertNoErrListClients(t, err)
|
||||
|
||||
allIps, err := scenario.ListTailscaleClientsIPs()
|
||||
assertNoErrListClientIPs(t, err)
|
||||
|
||||
err = scenario.WaitForTailscaleSync()
|
||||
assertNoErrSync(t, err)
|
||||
|
||||
allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string {
|
||||
return x.String()
|
||||
})
|
||||
|
||||
success := pingAllHelper(t, allClients, allAddrs)
|
||||
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
|
||||
}
|
||||
|
||||
func TestOIDCReloginSameNodeNewUser(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
t.Parallel()
|
||||
|
||||
baseScenario, err := NewScenario(dockertestMaxWait())
|
||||
assertNoErr(t, err)
|
||||
|
||||
scenario := AuthOIDCScenario{
|
||||
Scenario: baseScenario,
|
||||
}
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
// Create no nodes and no users
|
||||
spec := map[string]int{}
|
||||
|
||||
// First login creates the first OIDC user
|
||||
// Second login logs in the same node, which creates a new node
|
||||
// Third login logs in the same node back into the original user
|
||||
mockusers := []mockoidc.MockUser{
|
||||
oidcMockUser("user1", true),
|
||||
oidcMockUser("user2", true),
|
||||
oidcMockUser("user1", true),
|
||||
}
|
||||
|
||||
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",
|
||||
// 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(
|
||||
spec,
|
||||
hsic.WithTestName("oidcauthrelog"),
|
||||
hsic.WithConfigEnv(oidcMap),
|
||||
hsic.WithTLS(),
|
||||
hsic.WithFileInContainer("/tmp/hs_client_oidc_secret", []byte(oidcConfig.ClientSecret)),
|
||||
hsic.WithEmbeddedDERPServerOnly(),
|
||||
)
|
||||
assertNoErrHeadscaleEnv(t, err)
|
||||
|
||||
headscale, err := scenario.Headscale()
|
||||
assertNoErr(t, err)
|
||||
|
||||
listUsers, err := headscale.ListUsers()
|
||||
assertNoErr(t, err)
|
||||
assert.Len(t, listUsers, 0)
|
||||
|
||||
ts, err := scenario.CreateTailscaleNode("unstable")
|
||||
assertNoErr(t, err)
|
||||
|
||||
u, err := ts.LoginWithURL(headscale.GetEndpoint())
|
||||
assertNoErr(t, err)
|
||||
|
||||
_, err = doLoginURL(ts.Hostname(), u)
|
||||
assertNoErr(t, err)
|
||||
|
||||
listUsers, err = headscale.ListUsers()
|
||||
assertNoErr(t, err)
|
||||
assert.Len(t, listUsers, 1)
|
||||
wantUsers := []*v1.User{
|
||||
{
|
||||
Id: 1,
|
||||
Name: "user1",
|
||||
Email: "user1@headscale.net",
|
||||
Provider: "oidc",
|
||||
ProviderId: oidcConfig.Issuer + "/user1",
|
||||
},
|
||||
}
|
||||
|
||||
sort.Slice(listUsers, func(i, j int) bool {
|
||||
return listUsers[i].GetId() < listUsers[j].GetId()
|
||||
})
|
||||
|
||||
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
|
||||
t.Fatalf("unexpected users: %s", diff)
|
||||
}
|
||||
|
||||
listNodes, err := headscale.ListNodes()
|
||||
assertNoErr(t, err)
|
||||
assert.Len(t, listNodes, 1)
|
||||
|
||||
// Log out user1 and log in user2, this should create a new node
|
||||
// for user2, the node should have the same machine key and
|
||||
// a new node key.
|
||||
err = ts.Logout()
|
||||
assertNoErr(t, err)
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
// TODO(kradalby): Not sure why we need to logout twice, but it fails and
|
||||
// logs in immediately after the first logout and I cannot reproduce it
|
||||
// manually.
|
||||
err = ts.Logout()
|
||||
assertNoErr(t, err)
|
||||
|
||||
u, err = ts.LoginWithURL(headscale.GetEndpoint())
|
||||
assertNoErr(t, err)
|
||||
|
||||
_, err = doLoginURL(ts.Hostname(), u)
|
||||
assertNoErr(t, err)
|
||||
|
||||
listUsers, err = headscale.ListUsers()
|
||||
assertNoErr(t, err)
|
||||
assert.Len(t, listUsers, 2)
|
||||
wantUsers = []*v1.User{
|
||||
{
|
||||
Id: 1,
|
||||
Name: "user1",
|
||||
Email: "user1@headscale.net",
|
||||
Provider: "oidc",
|
||||
ProviderId: oidcConfig.Issuer + "/user1",
|
||||
},
|
||||
{
|
||||
Id: 2,
|
||||
Name: "user2",
|
||||
Email: "user2@headscale.net",
|
||||
Provider: "oidc",
|
||||
ProviderId: oidcConfig.Issuer + "/user2",
|
||||
},
|
||||
}
|
||||
|
||||
sort.Slice(listUsers, func(i, j int) bool {
|
||||
return listUsers[i].GetId() < listUsers[j].GetId()
|
||||
})
|
||||
|
||||
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
|
||||
t.Fatalf("unexpected users: %s", diff)
|
||||
}
|
||||
|
||||
listNodesAfterNewUserLogin, err := headscale.ListNodes()
|
||||
assertNoErr(t, err)
|
||||
assert.Len(t, listNodesAfterNewUserLogin, 2)
|
||||
|
||||
// Machine key is the same as the "machine" has not changed,
|
||||
// but Node key is not as it is a new node
|
||||
assert.Equal(t, listNodes[0].MachineKey, listNodesAfterNewUserLogin[0].MachineKey)
|
||||
assert.Equal(t, listNodesAfterNewUserLogin[0].MachineKey, listNodesAfterNewUserLogin[1].MachineKey)
|
||||
assert.NotEqual(t, listNodesAfterNewUserLogin[0].NodeKey, listNodesAfterNewUserLogin[1].NodeKey)
|
||||
|
||||
// Log out user2, and log into user1, no new node should be created,
|
||||
// the node should now "become" node1 again
|
||||
err = ts.Logout()
|
||||
assertNoErr(t, err)
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
// TODO(kradalby): Not sure why we need to logout twice, but it fails and
|
||||
// logs in immediately after the first logout and I cannot reproduce it
|
||||
// manually.
|
||||
err = ts.Logout()
|
||||
assertNoErr(t, err)
|
||||
|
||||
u, err = ts.LoginWithURL(headscale.GetEndpoint())
|
||||
assertNoErr(t, err)
|
||||
|
||||
_, err = doLoginURL(ts.Hostname(), u)
|
||||
assertNoErr(t, err)
|
||||
|
||||
listUsers, err = headscale.ListUsers()
|
||||
assertNoErr(t, err)
|
||||
assert.Len(t, listUsers, 2)
|
||||
wantUsers = []*v1.User{
|
||||
{
|
||||
Id: 1,
|
||||
Name: "user1",
|
||||
Email: "user1@headscale.net",
|
||||
Provider: "oidc",
|
||||
ProviderId: oidcConfig.Issuer + "/user1",
|
||||
},
|
||||
{
|
||||
Id: 2,
|
||||
Name: "user2",
|
||||
Email: "user2@headscale.net",
|
||||
Provider: "oidc",
|
||||
ProviderId: oidcConfig.Issuer + "/user2",
|
||||
},
|
||||
}
|
||||
|
||||
sort.Slice(listUsers, func(i, j int) bool {
|
||||
return listUsers[i].GetId() < listUsers[j].GetId()
|
||||
})
|
||||
|
||||
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
|
||||
t.Fatalf("unexpected users: %s", diff)
|
||||
}
|
||||
|
||||
listNodesAfterLoggingBackIn, err := headscale.ListNodes()
|
||||
assertNoErr(t, err)
|
||||
assert.Len(t, listNodesAfterLoggingBackIn, 2)
|
||||
|
||||
// Validate that the machine we had when we logged in the first time, has the same
|
||||
// machine key, but a different ID than the newly logged in version of the same
|
||||
// machine.
|
||||
assert.Equal(t, listNodes[0].MachineKey, listNodesAfterNewUserLogin[0].MachineKey)
|
||||
assert.Equal(t, listNodes[0].NodeKey, listNodesAfterNewUserLogin[0].NodeKey)
|
||||
assert.Equal(t, listNodes[0].Id, listNodesAfterNewUserLogin[0].Id)
|
||||
assert.Equal(t, listNodes[0].MachineKey, listNodesAfterNewUserLogin[1].MachineKey)
|
||||
assert.NotEqual(t, listNodes[0].Id, listNodesAfterNewUserLogin[1].Id)
|
||||
assert.NotEqual(t, listNodes[0].User.Id, listNodesAfterNewUserLogin[1].User.Id)
|
||||
|
||||
// Even tho we are logging in again with the same user, the previous key has been expired
|
||||
// and a new one has been generated. The node entry in the database should be the same
|
||||
// as the user + machinekey still matches.
|
||||
assert.Equal(t, listNodes[0].MachineKey, listNodesAfterLoggingBackIn[0].MachineKey)
|
||||
assert.NotEqual(t, listNodes[0].NodeKey, listNodesAfterLoggingBackIn[0].NodeKey)
|
||||
assert.Equal(t, listNodes[0].Id, listNodesAfterLoggingBackIn[0].Id)
|
||||
|
||||
// The "logged back in" machine should have the same machinekey but a different nodekey
|
||||
// than the version logged in with a different user.
|
||||
assert.Equal(t, listNodesAfterLoggingBackIn[0].MachineKey, listNodesAfterLoggingBackIn[1].MachineKey)
|
||||
assert.NotEqual(t, listNodesAfterLoggingBackIn[0].NodeKey, listNodesAfterLoggingBackIn[1].NodeKey)
|
||||
}
|
||||
|
||||
func (s *AuthOIDCScenario) CreateHeadscaleEnv(
|
||||
users map[string]int,
|
||||
opts ...hsic.Option,
|
||||
@@ -657,71 +930,42 @@ func (s *AuthOIDCScenario) runMockOIDC(accessTTL time.Duration, users []mockoidc
|
||||
}, nil
|
||||
}
|
||||
|
||||
type LoggingRoundTripper struct{}
|
||||
|
||||
func (t LoggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
noTls := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // nolint
|
||||
}
|
||||
resp, err := noTls.RoundTrip(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Printf("---")
|
||||
log.Printf("method: %s | url: %s", resp.Request.Method, resp.Request.URL.String())
|
||||
log.Printf("status: %d | cookies: %+v", resp.StatusCode, resp.Cookies())
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *AuthOIDCScenario) runTailscaleUp(
|
||||
userStr, loginServer string,
|
||||
) error {
|
||||
headscale, err := s.Headscale()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("running tailscale up for user %s", userStr)
|
||||
if user, ok := s.users[userStr]; ok {
|
||||
for _, client := range user.Clients {
|
||||
c := client
|
||||
tsc := client
|
||||
user.joinWaitGroup.Go(func() error {
|
||||
loginURL, err := c.LoginWithURL(loginServer)
|
||||
loginURL, err := tsc.LoginWithURL(loginServer)
|
||||
if err != nil {
|
||||
log.Printf("%s failed to run tailscale up: %s", c.Hostname(), err)
|
||||
log.Printf("%s failed to run tailscale up: %s", tsc.Hostname(), err)
|
||||
}
|
||||
|
||||
loginURL.Host = fmt.Sprintf("%s:8080", headscale.GetIP())
|
||||
loginURL.Scheme = "http"
|
||||
|
||||
if len(headscale.GetCert()) > 0 {
|
||||
loginURL.Scheme = "https"
|
||||
}
|
||||
|
||||
insecureTransport := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // nolint
|
||||
}
|
||||
|
||||
log.Printf("%s login url: %s\n", c.Hostname(), loginURL.String())
|
||||
|
||||
log.Printf("%s logging in with url", c.Hostname())
|
||||
httpClient := &http.Client{Transport: insecureTransport}
|
||||
ctx := context.Background()
|
||||
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, loginURL.String(), nil)
|
||||
resp, err := httpClient.Do(req)
|
||||
_, err = doLoginURL(tsc.Hostname(), loginURL)
|
||||
if err != nil {
|
||||
log.Printf(
|
||||
"%s failed to login using url %s: %s",
|
||||
c.Hostname(),
|
||||
loginURL,
|
||||
err,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
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
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
_, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Printf("%s failed to read response body: %s", c.Hostname(), err)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("Finished request for %s to join tailnet", c.Hostname())
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -749,6 +993,49 @@ func (s *AuthOIDCScenario) runTailscaleUp(
|
||||
return fmt.Errorf("failed to up tailscale node: %w", errNoUserAvailable)
|
||||
}
|
||||
|
||||
// doLoginURL visits the given login URL and returns the body as a
|
||||
// string.
|
||||
func doLoginURL(hostname string, loginURL *url.URL) (string, error) {
|
||||
log.Printf("%s login url: %s\n", hostname, loginURL.String())
|
||||
|
||||
var err error
|
||||
hc := &http.Client{
|
||||
Transport: LoggingRoundTripper{},
|
||||
}
|
||||
hc.Jar, err = cookiejar.New(nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s failed to create cookiejar : %w", hostname, err)
|
||||
}
|
||||
|
||||
log.Printf("%s logging in with url", hostname)
|
||||
ctx := context.Background()
|
||||
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, loginURL.String(), nil)
|
||||
resp, err := hc.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s failed to send http request: %w", hostname, err)
|
||||
}
|
||||
|
||||
log.Printf("cookies: %+v", hc.Jar.Cookies(loginURL))
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
log.Printf("body: %s", body)
|
||||
|
||||
return "", fmt.Errorf("%s response code of login request was %w", hostname, err)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Printf("%s failed to read response body: %s", hostname, err)
|
||||
|
||||
return "", fmt.Errorf("%s failed to read response body: %w", hostname, err)
|
||||
}
|
||||
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
func (s *AuthOIDCScenario) Shutdown() {
|
||||
err := s.pool.Purge(s.mockOIDC)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"strings"
|
||||
@@ -15,6 +11,8 @@ import (
|
||||
|
||||
"github.com/juanfont/headscale/integration/hsic"
|
||||
"github.com/samber/lo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var errParseAuthPage = errors.New("failed to parse auth page")
|
||||
@@ -47,7 +45,6 @@ func TestAuthWebFlowAuthenticationPingAll(t *testing.T) {
|
||||
hsic.WithTestName("webauthping"),
|
||||
hsic.WithEmbeddedDERPServerOnly(),
|
||||
hsic.WithTLS(),
|
||||
hsic.WithHostnameAsServerURL(),
|
||||
)
|
||||
assertNoErrHeadscaleEnv(t, err)
|
||||
|
||||
@@ -87,7 +84,10 @@ func TestAuthWebFlowLogoutAndRelogin(t *testing.T) {
|
||||
"user2": len(MustTestVersions),
|
||||
}
|
||||
|
||||
err = scenario.CreateHeadscaleEnv(spec, hsic.WithTestName("weblogout"))
|
||||
err = scenario.CreateHeadscaleEnv(spec,
|
||||
hsic.WithTestName("weblogout"),
|
||||
hsic.WithTLS(),
|
||||
)
|
||||
assertNoErrHeadscaleEnv(t, err)
|
||||
|
||||
allClients, err := scenario.ListTailscaleClients()
|
||||
@@ -108,6 +108,14 @@ func TestAuthWebFlowLogoutAndRelogin(t *testing.T) {
|
||||
success := pingAllHelper(t, allClients, allAddrs)
|
||||
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
|
||||
|
||||
headscale, err := scenario.Headscale()
|
||||
assertNoErrGetHeadscale(t, err)
|
||||
|
||||
listNodes, err := headscale.ListNodes()
|
||||
assert.Equal(t, len(listNodes), len(allClients))
|
||||
nodeCountBeforeLogout := len(listNodes)
|
||||
t.Logf("node count before logout: %d", nodeCountBeforeLogout)
|
||||
|
||||
clientIPs := make(map[TailscaleClient][]netip.Addr)
|
||||
for _, client := range allClients {
|
||||
ips, err := client.IPs()
|
||||
@@ -129,21 +137,15 @@ func TestAuthWebFlowLogoutAndRelogin(t *testing.T) {
|
||||
|
||||
t.Logf("all clients logged out")
|
||||
|
||||
headscale, err := scenario.Headscale()
|
||||
assertNoErrGetHeadscale(t, err)
|
||||
|
||||
for userName := range spec {
|
||||
err = scenario.runTailscaleUp(userName, headscale.GetEndpoint())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to run tailscale up: %s", err)
|
||||
t.Fatalf("failed to run tailscale up (%q): %s", headscale.GetEndpoint(), err)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("all clients logged in again")
|
||||
|
||||
allClients, err = scenario.ListTailscaleClients()
|
||||
assertNoErrListClients(t, err)
|
||||
|
||||
allIps, err = scenario.ListTailscaleClientsIPs()
|
||||
assertNoErrListClientIPs(t, err)
|
||||
|
||||
@@ -154,6 +156,10 @@ func TestAuthWebFlowLogoutAndRelogin(t *testing.T) {
|
||||
success = pingAllHelper(t, allClients, allAddrs)
|
||||
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
|
||||
|
||||
listNodes, err = headscale.ListNodes()
|
||||
require.Equal(t, nodeCountBeforeLogout, len(listNodes))
|
||||
t.Logf("node count first login: %d, after relogin: %d", nodeCountBeforeLogout, len(listNodes))
|
||||
|
||||
for _, client := range allClients {
|
||||
ips, err := client.IPs()
|
||||
if err != nil {
|
||||
@@ -227,11 +233,12 @@ func (s *AuthWebFlowScenario) CreateHeadscaleEnv(
|
||||
func (s *AuthWebFlowScenario) runTailscaleUp(
|
||||
userStr, loginServer string,
|
||||
) error {
|
||||
log.Printf("running tailscale up for user %s", userStr)
|
||||
log.Printf("running tailscale up for user %q", userStr)
|
||||
if user, ok := s.users[userStr]; ok {
|
||||
for _, client := range user.Clients {
|
||||
c := client
|
||||
user.joinWaitGroup.Go(func() error {
|
||||
log.Printf("logging %q into %q", c.Hostname(), loginServer)
|
||||
loginURL, err := c.LoginWithURL(loginServer)
|
||||
if err != nil {
|
||||
log.Printf("failed to run tailscale up (%s): %s", c.Hostname(), err)
|
||||
@@ -273,39 +280,11 @@ func (s *AuthWebFlowScenario) runTailscaleUp(
|
||||
}
|
||||
|
||||
func (s *AuthWebFlowScenario) runHeadscaleRegister(userStr string, loginURL *url.URL) error {
|
||||
headscale, err := s.Headscale()
|
||||
body, err := doLoginURL("web-auth-not-set", loginURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("loginURL: %s", loginURL)
|
||||
loginURL.Host = fmt.Sprintf("%s:8080", headscale.GetIP())
|
||||
loginURL.Scheme = "http"
|
||||
|
||||
if len(headscale.GetCert()) > 0 {
|
||||
loginURL.Scheme = "https"
|
||||
}
|
||||
|
||||
insecureTransport := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // nolint
|
||||
}
|
||||
httpClient := &http.Client{
|
||||
Transport: insecureTransport,
|
||||
}
|
||||
ctx := context.Background()
|
||||
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, loginURL.String(), nil)
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
// see api.go HTML template
|
||||
codeSep := strings.Split(string(body), "</code>")
|
||||
if len(codeSep) != 2 {
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/policy"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/juanfont/headscale/integration/hsic"
|
||||
"github.com/juanfont/headscale/integration/tsic"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -135,8 +136,9 @@ func TestUserCommand(t *testing.T) {
|
||||
slices.SortFunc(listByUsername, sortWithID)
|
||||
want := []*v1.User{
|
||||
{
|
||||
Id: 1,
|
||||
Name: "user1",
|
||||
Id: 1,
|
||||
Name: "user1",
|
||||
Email: "user1@test.no",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -161,8 +163,9 @@ func TestUserCommand(t *testing.T) {
|
||||
slices.SortFunc(listByID, sortWithID)
|
||||
want = []*v1.User{
|
||||
{
|
||||
Id: 1,
|
||||
Name: "user1",
|
||||
Id: 1,
|
||||
Name: "user1",
|
||||
Email: "user1@test.no",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -199,8 +202,9 @@ func TestUserCommand(t *testing.T) {
|
||||
slices.SortFunc(listAfterIDDelete, sortWithID)
|
||||
want = []*v1.User{
|
||||
{
|
||||
Id: 2,
|
||||
Name: "newname",
|
||||
Id: 2,
|
||||
Name: "newname",
|
||||
Email: "user2@test.no",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -541,7 +545,6 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) {
|
||||
hsic.WithTestName("clipak"),
|
||||
hsic.WithEmbeddedDERPServerOnly(),
|
||||
hsic.WithTLS(),
|
||||
hsic.WithHostnameAsServerURL(),
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
@@ -603,22 +606,12 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) {
|
||||
t.Fatalf("expected node to be logged in as userid:2, got: %s", status.Self.UserID.String())
|
||||
}
|
||||
|
||||
var listNodes []v1.Node
|
||||
err = executeAndUnmarshal(
|
||||
headscale,
|
||||
[]string{
|
||||
"headscale",
|
||||
"nodes",
|
||||
"list",
|
||||
"--output",
|
||||
"json",
|
||||
},
|
||||
&listNodes,
|
||||
)
|
||||
listNodes, err := headscale.ListNodes()
|
||||
assert.Nil(t, err)
|
||||
assert.Len(t, listNodes, 1)
|
||||
assert.Len(t, listNodes, 2)
|
||||
|
||||
assert.Equal(t, "user2", listNodes[0].GetUser().GetName())
|
||||
assert.Equal(t, "user1", listNodes[0].GetUser().GetName())
|
||||
assert.Equal(t, "user2", listNodes[1].GetUser().GetName())
|
||||
}
|
||||
|
||||
func TestApiKeyCommand(t *testing.T) {
|
||||
@@ -809,14 +802,14 @@ func TestNodeTagCommand(t *testing.T) {
|
||||
headscale, err := scenario.Headscale()
|
||||
assertNoErr(t, err)
|
||||
|
||||
machineKeys := []string{
|
||||
"mkey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
|
||||
"mkey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
|
||||
regIDs := []string{
|
||||
types.MustRegistrationID().String(),
|
||||
types.MustRegistrationID().String(),
|
||||
}
|
||||
nodes := make([]*v1.Node, len(machineKeys))
|
||||
nodes := make([]*v1.Node, len(regIDs))
|
||||
assert.Nil(t, err)
|
||||
|
||||
for index, machineKey := range machineKeys {
|
||||
for index, regID := range regIDs {
|
||||
_, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
@@ -827,7 +820,7 @@ func TestNodeTagCommand(t *testing.T) {
|
||||
"--user",
|
||||
"user1",
|
||||
"--key",
|
||||
machineKey,
|
||||
regID,
|
||||
"--output",
|
||||
"json",
|
||||
},
|
||||
@@ -844,7 +837,7 @@ func TestNodeTagCommand(t *testing.T) {
|
||||
"user1",
|
||||
"register",
|
||||
"--key",
|
||||
machineKey,
|
||||
regID,
|
||||
"--output",
|
||||
"json",
|
||||
},
|
||||
@@ -854,7 +847,7 @@ func TestNodeTagCommand(t *testing.T) {
|
||||
|
||||
nodes[index] = &node
|
||||
}
|
||||
assert.Len(t, nodes, len(machineKeys))
|
||||
assert.Len(t, nodes, len(regIDs))
|
||||
|
||||
var node v1.Node
|
||||
err = executeAndUnmarshal(
|
||||
@@ -886,7 +879,7 @@ func TestNodeTagCommand(t *testing.T) {
|
||||
assert.ErrorContains(t, err, "tag must start with the string 'tag:'")
|
||||
|
||||
// Test list all nodes after added seconds
|
||||
resultMachines := make([]*v1.Node, len(machineKeys))
|
||||
resultMachines := make([]*v1.Node, len(regIDs))
|
||||
err = executeAndUnmarshal(
|
||||
headscale,
|
||||
[]string{
|
||||
@@ -930,7 +923,23 @@ func TestNodeAdvertiseTagCommand(t *testing.T) {
|
||||
wantTag: false,
|
||||
},
|
||||
{
|
||||
name: "with-policy",
|
||||
name: "with-policy-email",
|
||||
policy: &policy.ACLPolicy{
|
||||
ACLs: []policy.ACL{
|
||||
{
|
||||
Action: "accept",
|
||||
Sources: []string{"*"},
|
||||
Destinations: []string{"*:*"},
|
||||
},
|
||||
},
|
||||
TagOwners: map[string][]string{
|
||||
"tag:test": {"user1@test.no"},
|
||||
},
|
||||
},
|
||||
wantTag: true,
|
||||
},
|
||||
{
|
||||
name: "with-policy-username",
|
||||
policy: &policy.ACLPolicy{
|
||||
ACLs: []policy.ACL{
|
||||
{
|
||||
@@ -945,13 +954,32 @@ func TestNodeAdvertiseTagCommand(t *testing.T) {
|
||||
},
|
||||
wantTag: true,
|
||||
},
|
||||
{
|
||||
name: "with-policy-groups",
|
||||
policy: &policy.ACLPolicy{
|
||||
Groups: policy.Groups{
|
||||
"group:admins": []string{"user1"},
|
||||
},
|
||||
ACLs: []policy.ACL{
|
||||
{
|
||||
Action: "accept",
|
||||
Sources: []string{"*"},
|
||||
Destinations: []string{"*:*"},
|
||||
},
|
||||
},
|
||||
TagOwners: map[string][]string{
|
||||
"tag:test": {"group:admins"},
|
||||
},
|
||||
},
|
||||
wantTag: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
scenario, err := NewScenario(dockertestMaxWait())
|
||||
assertNoErr(t, err)
|
||||
// defer scenario.ShutdownAssertNoPanics(t)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
spec := map[string]int{
|
||||
"user1": 1,
|
||||
@@ -1016,18 +1044,17 @@ func TestNodeCommand(t *testing.T) {
|
||||
headscale, err := scenario.Headscale()
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Pregenerated machine keys
|
||||
machineKeys := []string{
|
||||
"mkey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
|
||||
"mkey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
|
||||
"mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
|
||||
"mkey:8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1",
|
||||
"mkey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
|
||||
regIDs := []string{
|
||||
types.MustRegistrationID().String(),
|
||||
types.MustRegistrationID().String(),
|
||||
types.MustRegistrationID().String(),
|
||||
types.MustRegistrationID().String(),
|
||||
types.MustRegistrationID().String(),
|
||||
}
|
||||
nodes := make([]*v1.Node, len(machineKeys))
|
||||
nodes := make([]*v1.Node, len(regIDs))
|
||||
assert.Nil(t, err)
|
||||
|
||||
for index, machineKey := range machineKeys {
|
||||
for index, regID := range regIDs {
|
||||
_, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
@@ -1038,7 +1065,7 @@ func TestNodeCommand(t *testing.T) {
|
||||
"--user",
|
||||
"node-user",
|
||||
"--key",
|
||||
machineKey,
|
||||
regID,
|
||||
"--output",
|
||||
"json",
|
||||
},
|
||||
@@ -1055,7 +1082,7 @@ func TestNodeCommand(t *testing.T) {
|
||||
"node-user",
|
||||
"register",
|
||||
"--key",
|
||||
machineKey,
|
||||
regID,
|
||||
"--output",
|
||||
"json",
|
||||
},
|
||||
@@ -1066,7 +1093,7 @@ func TestNodeCommand(t *testing.T) {
|
||||
nodes[index] = &node
|
||||
}
|
||||
|
||||
assert.Len(t, nodes, len(machineKeys))
|
||||
assert.Len(t, nodes, len(regIDs))
|
||||
|
||||
// Test list all nodes after added seconds
|
||||
var listAll []v1.Node
|
||||
@@ -1097,14 +1124,14 @@ func TestNodeCommand(t *testing.T) {
|
||||
assert.Equal(t, "node-4", listAll[3].GetName())
|
||||
assert.Equal(t, "node-5", listAll[4].GetName())
|
||||
|
||||
otherUserMachineKeys := []string{
|
||||
"mkey:b5b444774186d4217adcec407563a1223929465ee2c68a4da13af0d0185b4f8e",
|
||||
"mkey:dc721977ac7415aafa87f7d4574cbe07c6b171834a6d37375782bdc1fb6b3584",
|
||||
otherUserRegIDs := []string{
|
||||
types.MustRegistrationID().String(),
|
||||
types.MustRegistrationID().String(),
|
||||
}
|
||||
otherUserMachines := make([]*v1.Node, len(otherUserMachineKeys))
|
||||
otherUserMachines := make([]*v1.Node, len(otherUserRegIDs))
|
||||
assert.Nil(t, err)
|
||||
|
||||
for index, machineKey := range otherUserMachineKeys {
|
||||
for index, regID := range otherUserRegIDs {
|
||||
_, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
@@ -1115,7 +1142,7 @@ func TestNodeCommand(t *testing.T) {
|
||||
"--user",
|
||||
"other-user",
|
||||
"--key",
|
||||
machineKey,
|
||||
regID,
|
||||
"--output",
|
||||
"json",
|
||||
},
|
||||
@@ -1132,7 +1159,7 @@ func TestNodeCommand(t *testing.T) {
|
||||
"other-user",
|
||||
"register",
|
||||
"--key",
|
||||
machineKey,
|
||||
regID,
|
||||
"--output",
|
||||
"json",
|
||||
},
|
||||
@@ -1143,7 +1170,7 @@ func TestNodeCommand(t *testing.T) {
|
||||
otherUserMachines[index] = &node
|
||||
}
|
||||
|
||||
assert.Len(t, otherUserMachines, len(otherUserMachineKeys))
|
||||
assert.Len(t, otherUserMachines, len(otherUserRegIDs))
|
||||
|
||||
// Test list all nodes after added otherUser
|
||||
var listAllWithotherUser []v1.Node
|
||||
@@ -1256,17 +1283,16 @@ func TestNodeExpireCommand(t *testing.T) {
|
||||
headscale, err := scenario.Headscale()
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Pregenerated machine keys
|
||||
machineKeys := []string{
|
||||
"mkey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
|
||||
"mkey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
|
||||
"mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
|
||||
"mkey:8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1",
|
||||
"mkey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
|
||||
regIDs := []string{
|
||||
types.MustRegistrationID().String(),
|
||||
types.MustRegistrationID().String(),
|
||||
types.MustRegistrationID().String(),
|
||||
types.MustRegistrationID().String(),
|
||||
types.MustRegistrationID().String(),
|
||||
}
|
||||
nodes := make([]*v1.Node, len(machineKeys))
|
||||
nodes := make([]*v1.Node, len(regIDs))
|
||||
|
||||
for index, machineKey := range machineKeys {
|
||||
for index, regID := range regIDs {
|
||||
_, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
@@ -1277,7 +1303,7 @@ func TestNodeExpireCommand(t *testing.T) {
|
||||
"--user",
|
||||
"node-expire-user",
|
||||
"--key",
|
||||
machineKey,
|
||||
regID,
|
||||
"--output",
|
||||
"json",
|
||||
},
|
||||
@@ -1294,7 +1320,7 @@ func TestNodeExpireCommand(t *testing.T) {
|
||||
"node-expire-user",
|
||||
"register",
|
||||
"--key",
|
||||
machineKey,
|
||||
regID,
|
||||
"--output",
|
||||
"json",
|
||||
},
|
||||
@@ -1305,7 +1331,7 @@ func TestNodeExpireCommand(t *testing.T) {
|
||||
nodes[index] = &node
|
||||
}
|
||||
|
||||
assert.Len(t, nodes, len(machineKeys))
|
||||
assert.Len(t, nodes, len(regIDs))
|
||||
|
||||
var listAll []v1.Node
|
||||
err = executeAndUnmarshal(
|
||||
@@ -1383,18 +1409,17 @@ func TestNodeRenameCommand(t *testing.T) {
|
||||
headscale, err := scenario.Headscale()
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Pregenerated machine keys
|
||||
machineKeys := []string{
|
||||
"mkey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
|
||||
"mkey:8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1",
|
||||
"mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
|
||||
"mkey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
|
||||
"mkey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
|
||||
regIDs := []string{
|
||||
types.MustRegistrationID().String(),
|
||||
types.MustRegistrationID().String(),
|
||||
types.MustRegistrationID().String(),
|
||||
types.MustRegistrationID().String(),
|
||||
types.MustRegistrationID().String(),
|
||||
}
|
||||
nodes := make([]*v1.Node, len(machineKeys))
|
||||
nodes := make([]*v1.Node, len(regIDs))
|
||||
assert.Nil(t, err)
|
||||
|
||||
for index, machineKey := range machineKeys {
|
||||
for index, regID := range regIDs {
|
||||
_, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
@@ -1405,7 +1430,7 @@ func TestNodeRenameCommand(t *testing.T) {
|
||||
"--user",
|
||||
"node-rename-command",
|
||||
"--key",
|
||||
machineKey,
|
||||
regID,
|
||||
"--output",
|
||||
"json",
|
||||
},
|
||||
@@ -1422,7 +1447,7 @@ func TestNodeRenameCommand(t *testing.T) {
|
||||
"node-rename-command",
|
||||
"register",
|
||||
"--key",
|
||||
machineKey,
|
||||
regID,
|
||||
"--output",
|
||||
"json",
|
||||
},
|
||||
@@ -1433,7 +1458,7 @@ func TestNodeRenameCommand(t *testing.T) {
|
||||
nodes[index] = &node
|
||||
}
|
||||
|
||||
assert.Len(t, nodes, len(machineKeys))
|
||||
assert.Len(t, nodes, len(regIDs))
|
||||
|
||||
var listAll []v1.Node
|
||||
err = executeAndUnmarshal(
|
||||
@@ -1551,7 +1576,7 @@ func TestNodeMoveCommand(t *testing.T) {
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Randomly generated node key
|
||||
machineKey := "mkey:688411b767663479632d44140f08a9fde87383adc7cdeb518f62ce28a17ef0aa"
|
||||
regID := types.MustRegistrationID()
|
||||
|
||||
_, err = headscale.Execute(
|
||||
[]string{
|
||||
@@ -1563,7 +1588,7 @@ func TestNodeMoveCommand(t *testing.T) {
|
||||
"--user",
|
||||
"old-user",
|
||||
"--key",
|
||||
machineKey,
|
||||
regID.String(),
|
||||
"--output",
|
||||
"json",
|
||||
},
|
||||
@@ -1580,7 +1605,7 @@ func TestNodeMoveCommand(t *testing.T) {
|
||||
"old-user",
|
||||
"register",
|
||||
"--key",
|
||||
machineKey,
|
||||
regID.String(),
|
||||
"--output",
|
||||
"json",
|
||||
},
|
||||
@@ -1802,7 +1827,7 @@ func TestPolicyBrokenConfigCommand(t *testing.T) {
|
||||
{
|
||||
// This is an unknown action, so it will return an error
|
||||
// and the config will not be applied.
|
||||
Action: "acccept",
|
||||
Action: "unknown-action",
|
||||
Sources: []string{"*"},
|
||||
Destinations: []string{"*:*"},
|
||||
},
|
||||
|
||||
@@ -17,7 +17,8 @@ type ControlServer interface {
|
||||
WaitForRunning() error
|
||||
CreateUser(user string) error
|
||||
CreateAuthKey(user string, reusable bool, ephemeral bool) (*v1.PreAuthKey, error)
|
||||
ListNodesInUser(user string) ([]*v1.Node, error)
|
||||
ListNodes(users ...string) ([]*v1.Node, error)
|
||||
ListUsers() ([]*v1.User, error)
|
||||
GetCert() []byte
|
||||
GetHostname() string
|
||||
GetIP() string
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user