diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 1867a57..1d19ed3 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -26,7 +26,7 @@ jobs:
key: ${{ github.ref }}
path: .cache
- name: Setup dependencies
- run: pip install mkdocs-material pillow cairosvg mkdocs-minify-plugin
+ run: pip install -r docs/requirements.txt
- name: Build docs
run: mkdocs build --strict
- name: Upload artifact
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index 36483bc..c30571c 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -12,10 +12,10 @@ jobs:
steps:
- uses: actions/stale@v5
with:
- days-before-issue-stale: 180
- days-before-issue-close: 14
+ days-before-issue-stale: 90
+ days-before-issue-close: 7
stale-issue-label: "stale"
- stale-issue-message: "This issue is stale because it has been open for 180 days with no activity."
+ stale-issue-message: "This issue is stale because it has been open for 90 days with no activity."
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
days-before-pr-stale: -1
days-before-pr-close: -1
diff --git a/.github/workflows/test-integration-v2-TestACLAllowStarDst.yaml b/.github/workflows/test-integration-v2-TestACLAllowStarDst.yaml
index d26137a..63017ac 100644
--- a/.github/workflows/test-integration-v2-TestACLAllowStarDst.yaml
+++ b/.github/workflows/test-integration-v2-TestACLAllowStarDst.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestACLAllowStarDst
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestACLAllowUser80Dst.yaml b/.github/workflows/test-integration-v2-TestACLAllowUser80Dst.yaml
index 0501811..e3d5d29 100644
--- a/.github/workflows/test-integration-v2-TestACLAllowUser80Dst.yaml
+++ b/.github/workflows/test-integration-v2-TestACLAllowUser80Dst.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestACLAllowUser80Dst
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestACLAllowUserDst.yaml b/.github/workflows/test-integration-v2-TestACLAllowUserDst.yaml
index 93ef061..dc328ed 100644
--- a/.github/workflows/test-integration-v2-TestACLAllowUserDst.yaml
+++ b/.github/workflows/test-integration-v2-TestACLAllowUserDst.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestACLAllowUserDst
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestACLDenyAllPort80.yaml b/.github/workflows/test-integration-v2-TestACLDenyAllPort80.yaml
index ae55984..396994a 100644
--- a/.github/workflows/test-integration-v2-TestACLDenyAllPort80.yaml
+++ b/.github/workflows/test-integration-v2-TestACLDenyAllPort80.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestACLDenyAllPort80
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestACLDevice1CanAccessDevice2.yaml b/.github/workflows/test-integration-v2-TestACLDevice1CanAccessDevice2.yaml
index 7d124b3..9af861f 100644
--- a/.github/workflows/test-integration-v2-TestACLDevice1CanAccessDevice2.yaml
+++ b/.github/workflows/test-integration-v2-TestACLDevice1CanAccessDevice2.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestACLDevice1CanAccessDevice2
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestACLHostsInNetMapTable.yaml b/.github/workflows/test-integration-v2-TestACLHostsInNetMapTable.yaml
index 7d74ed0..cac45ba 100644
--- a/.github/workflows/test-integration-v2-TestACLHostsInNetMapTable.yaml
+++ b/.github/workflows/test-integration-v2-TestACLHostsInNetMapTable.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestACLHostsInNetMapTable
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestACLNamedHostsCanReach.yaml b/.github/workflows/test-integration-v2-TestACLNamedHostsCanReach.yaml
index c4d0fbd..f098522 100644
--- a/.github/workflows/test-integration-v2-TestACLNamedHostsCanReach.yaml
+++ b/.github/workflows/test-integration-v2-TestACLNamedHostsCanReach.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestACLNamedHostsCanReach
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestACLNamedHostsCanReachBySubnet.yaml b/.github/workflows/test-integration-v2-TestACLNamedHostsCanReachBySubnet.yaml
index 8434570..cee0e35 100644
--- a/.github/workflows/test-integration-v2-TestACLNamedHostsCanReachBySubnet.yaml
+++ b/.github/workflows/test-integration-v2-TestACLNamedHostsCanReachBySubnet.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestACLNamedHostsCanReachBySubnet
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestApiKeyCommand.yaml b/.github/workflows/test-integration-v2-TestApiKeyCommand.yaml
index ef16d50..b495b9b 100644
--- a/.github/workflows/test-integration-v2-TestApiKeyCommand.yaml
+++ b/.github/workflows/test-integration-v2-TestApiKeyCommand.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestApiKeyCommand
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestAuthKeyLogoutAndRelogin.yaml b/.github/workflows/test-integration-v2-TestAuthKeyLogoutAndRelogin.yaml
index e5f83ed..fcdceeb 100644
--- a/.github/workflows/test-integration-v2-TestAuthKeyLogoutAndRelogin.yaml
+++ b/.github/workflows/test-integration-v2-TestAuthKeyLogoutAndRelogin.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestAuthKeyLogoutAndRelogin
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestAuthWebFlowAuthenticationPingAll.yaml b/.github/workflows/test-integration-v2-TestAuthWebFlowAuthenticationPingAll.yaml
index f870fae..9e24a7d 100644
--- a/.github/workflows/test-integration-v2-TestAuthWebFlowAuthenticationPingAll.yaml
+++ b/.github/workflows/test-integration-v2-TestAuthWebFlowAuthenticationPingAll.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestAuthWebFlowAuthenticationPingAll
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestAuthWebFlowLogoutAndRelogin.yaml b/.github/workflows/test-integration-v2-TestAuthWebFlowLogoutAndRelogin.yaml
index d30882b..e1ff6c3 100644
--- a/.github/workflows/test-integration-v2-TestAuthWebFlowLogoutAndRelogin.yaml
+++ b/.github/workflows/test-integration-v2-TestAuthWebFlowLogoutAndRelogin.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestAuthWebFlowLogoutAndRelogin
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestCreateTailscale.yaml b/.github/workflows/test-integration-v2-TestCreateTailscale.yaml
index 343337f..eaf829c 100644
--- a/.github/workflows/test-integration-v2-TestCreateTailscale.yaml
+++ b/.github/workflows/test-integration-v2-TestCreateTailscale.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestCreateTailscale
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestDERPServerScenario.yaml b/.github/workflows/test-integration-v2-TestDERPServerScenario.yaml
index 3cf2238..41c7db5 100644
--- a/.github/workflows/test-integration-v2-TestDERPServerScenario.yaml
+++ b/.github/workflows/test-integration-v2-TestDERPServerScenario.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestDERPServerScenario
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestEnablingRoutes.yaml b/.github/workflows/test-integration-v2-TestEnablingRoutes.yaml
index a2e70c1..750ea9f 100644
--- a/.github/workflows/test-integration-v2-TestEnablingRoutes.yaml
+++ b/.github/workflows/test-integration-v2-TestEnablingRoutes.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestEnablingRoutes
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestEphemeral.yaml b/.github/workflows/test-integration-v2-TestEphemeral.yaml
index fbba828..df037ee 100644
--- a/.github/workflows/test-integration-v2-TestEphemeral.yaml
+++ b/.github/workflows/test-integration-v2-TestEphemeral.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestEphemeral
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestExpireNode.yaml b/.github/workflows/test-integration-v2-TestExpireNode.yaml
index f19f9d2..48e5e36 100644
--- a/.github/workflows/test-integration-v2-TestExpireNode.yaml
+++ b/.github/workflows/test-integration-v2-TestExpireNode.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestExpireNode
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestHASubnetRouterFailover.yaml b/.github/workflows/test-integration-v2-TestHASubnetRouterFailover.yaml
new file mode 100644
index 0000000..4ffe464
--- /dev/null
+++ b/.github/workflows/test-integration-v2-TestHASubnetRouterFailover.yaml
@@ -0,0 +1,67 @@
+# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
+# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
+
+name: Integration Test v2 - TestHASubnetRouterFailover
+
+on: [pull_request]
+
+concurrency:
+ group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
+ cancel-in-progress: true
+
+jobs:
+ TestHASubnetRouterFailover:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 2
+
+ - uses: DeterminateSystems/nix-installer-action@main
+ - uses: DeterminateSystems/magic-nix-cache-action@main
+ - uses: satackey/action-docker-layer-caching@main
+ continue-on-error: true
+
+ - name: Get changed files
+ id: changed-files
+ uses: tj-actions/changed-files@v34
+ with:
+ files: |
+ *.nix
+ go.*
+ **/*.go
+ integration_test/
+ config-example.yaml
+
+ - name: Run TestHASubnetRouterFailover
+ uses: Wandalen/wretry.action@master
+ if: steps.changed-files.outputs.any_changed == 'true'
+ with:
+ attempt_limit: 5
+ command: |
+ nix develop --command -- docker run \
+ --tty --rm \
+ --volume ~/.cache/hs-integration-go:/go \
+ --name headscale-test-suite \
+ --volume $PWD:$PWD -w $PWD/integration \
+ --volume /var/run/docker.sock:/var/run/docker.sock \
+ --volume $PWD/control_logs:/tmp/control \
+ golang:1 \
+ go run gotest.tools/gotestsum@latest -- ./... \
+ -failfast \
+ -timeout 120m \
+ -parallel 1 \
+ -run "^TestHASubnetRouterFailover$"
+
+ - uses: actions/upload-artifact@v3
+ if: always() && steps.changed-files.outputs.any_changed == 'true'
+ with:
+ name: logs
+ path: "control_logs/*.log"
+
+ - uses: actions/upload-artifact@v3
+ if: always() && steps.changed-files.outputs.any_changed == 'true'
+ with:
+ name: pprof
+ path: "control_logs/*.pprof.tar"
diff --git a/.github/workflows/test-integration-v2-TestHeadscale.yaml b/.github/workflows/test-integration-v2-TestHeadscale.yaml
index e6ffce2..ff7dbb1 100644
--- a/.github/workflows/test-integration-v2-TestHeadscale.yaml
+++ b/.github/workflows/test-integration-v2-TestHeadscale.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestHeadscale
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestNodeAdvertiseTagNoACLCommand.yaml b/.github/workflows/test-integration-v2-TestNodeAdvertiseTagNoACLCommand.yaml
new file mode 100644
index 0000000..f51fa61
--- /dev/null
+++ b/.github/workflows/test-integration-v2-TestNodeAdvertiseTagNoACLCommand.yaml
@@ -0,0 +1,67 @@
+# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
+# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
+
+name: Integration Test v2 - TestNodeAdvertiseTagNoACLCommand
+
+on: [pull_request]
+
+concurrency:
+ group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
+ cancel-in-progress: true
+
+jobs:
+ TestNodeAdvertiseTagNoACLCommand:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 2
+
+ - uses: DeterminateSystems/nix-installer-action@main
+ - uses: DeterminateSystems/magic-nix-cache-action@main
+ - uses: satackey/action-docker-layer-caching@main
+ continue-on-error: true
+
+ - name: Get changed files
+ id: changed-files
+ uses: tj-actions/changed-files@v34
+ with:
+ files: |
+ *.nix
+ go.*
+ **/*.go
+ integration_test/
+ config-example.yaml
+
+ - name: Run TestNodeAdvertiseTagNoACLCommand
+ uses: Wandalen/wretry.action@master
+ if: steps.changed-files.outputs.any_changed == 'true'
+ with:
+ attempt_limit: 5
+ command: |
+ nix develop --command -- docker run \
+ --tty --rm \
+ --volume ~/.cache/hs-integration-go:/go \
+ --name headscale-test-suite \
+ --volume $PWD:$PWD -w $PWD/integration \
+ --volume /var/run/docker.sock:/var/run/docker.sock \
+ --volume $PWD/control_logs:/tmp/control \
+ golang:1 \
+ go run gotest.tools/gotestsum@latest -- ./... \
+ -failfast \
+ -timeout 120m \
+ -parallel 1 \
+ -run "^TestNodeAdvertiseTagNoACLCommand$"
+
+ - uses: actions/upload-artifact@v3
+ if: always() && steps.changed-files.outputs.any_changed == 'true'
+ with:
+ name: logs
+ path: "control_logs/*.log"
+
+ - uses: actions/upload-artifact@v3
+ if: always() && steps.changed-files.outputs.any_changed == 'true'
+ with:
+ name: pprof
+ path: "control_logs/*.pprof.tar"
diff --git a/.github/workflows/test-integration-v2-TestNodeAdvertiseTagWithACLCommand.yaml b/.github/workflows/test-integration-v2-TestNodeAdvertiseTagWithACLCommand.yaml
new file mode 100644
index 0000000..9e0fcd2
--- /dev/null
+++ b/.github/workflows/test-integration-v2-TestNodeAdvertiseTagWithACLCommand.yaml
@@ -0,0 +1,67 @@
+# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
+# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
+
+name: Integration Test v2 - TestNodeAdvertiseTagWithACLCommand
+
+on: [pull_request]
+
+concurrency:
+ group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
+ cancel-in-progress: true
+
+jobs:
+ TestNodeAdvertiseTagWithACLCommand:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 2
+
+ - uses: DeterminateSystems/nix-installer-action@main
+ - uses: DeterminateSystems/magic-nix-cache-action@main
+ - uses: satackey/action-docker-layer-caching@main
+ continue-on-error: true
+
+ - name: Get changed files
+ id: changed-files
+ uses: tj-actions/changed-files@v34
+ with:
+ files: |
+ *.nix
+ go.*
+ **/*.go
+ integration_test/
+ config-example.yaml
+
+ - name: Run TestNodeAdvertiseTagWithACLCommand
+ uses: Wandalen/wretry.action@master
+ if: steps.changed-files.outputs.any_changed == 'true'
+ with:
+ attempt_limit: 5
+ command: |
+ nix develop --command -- docker run \
+ --tty --rm \
+ --volume ~/.cache/hs-integration-go:/go \
+ --name headscale-test-suite \
+ --volume $PWD:$PWD -w $PWD/integration \
+ --volume /var/run/docker.sock:/var/run/docker.sock \
+ --volume $PWD/control_logs:/tmp/control \
+ golang:1 \
+ go run gotest.tools/gotestsum@latest -- ./... \
+ -failfast \
+ -timeout 120m \
+ -parallel 1 \
+ -run "^TestNodeAdvertiseTagWithACLCommand$"
+
+ - uses: actions/upload-artifact@v3
+ if: always() && steps.changed-files.outputs.any_changed == 'true'
+ with:
+ name: logs
+ path: "control_logs/*.log"
+
+ - uses: actions/upload-artifact@v3
+ if: always() && steps.changed-files.outputs.any_changed == 'true'
+ with:
+ name: pprof
+ path: "control_logs/*.pprof.tar"
diff --git a/.github/workflows/test-integration-v2-TestNodeCommand.yaml b/.github/workflows/test-integration-v2-TestNodeCommand.yaml
index 3d26a1e..4398672 100644
--- a/.github/workflows/test-integration-v2-TestNodeCommand.yaml
+++ b/.github/workflows/test-integration-v2-TestNodeCommand.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestNodeCommand
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestNodeExpireCommand.yaml b/.github/workflows/test-integration-v2-TestNodeExpireCommand.yaml
index af60677..f953a1c 100644
--- a/.github/workflows/test-integration-v2-TestNodeExpireCommand.yaml
+++ b/.github/workflows/test-integration-v2-TestNodeExpireCommand.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestNodeExpireCommand
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestNodeMoveCommand.yaml b/.github/workflows/test-integration-v2-TestNodeMoveCommand.yaml
index 1862e5d..ce5f5b9 100644
--- a/.github/workflows/test-integration-v2-TestNodeMoveCommand.yaml
+++ b/.github/workflows/test-integration-v2-TestNodeMoveCommand.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestNodeMoveCommand
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestNodeOnlineLastSeenStatus.yaml b/.github/workflows/test-integration-v2-TestNodeOnlineLastSeenStatus.yaml
new file mode 100644
index 0000000..e3a30f8
--- /dev/null
+++ b/.github/workflows/test-integration-v2-TestNodeOnlineLastSeenStatus.yaml
@@ -0,0 +1,67 @@
+# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
+# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
+
+name: Integration Test v2 - TestNodeOnlineLastSeenStatus
+
+on: [pull_request]
+
+concurrency:
+ group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
+ cancel-in-progress: true
+
+jobs:
+ TestNodeOnlineLastSeenStatus:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 2
+
+ - uses: DeterminateSystems/nix-installer-action@main
+ - uses: DeterminateSystems/magic-nix-cache-action@main
+ - uses: satackey/action-docker-layer-caching@main
+ continue-on-error: true
+
+ - name: Get changed files
+ id: changed-files
+ uses: tj-actions/changed-files@v34
+ with:
+ files: |
+ *.nix
+ go.*
+ **/*.go
+ integration_test/
+ config-example.yaml
+
+ - name: Run TestNodeOnlineLastSeenStatus
+ uses: Wandalen/wretry.action@master
+ if: steps.changed-files.outputs.any_changed == 'true'
+ with:
+ attempt_limit: 5
+ command: |
+ nix develop --command -- docker run \
+ --tty --rm \
+ --volume ~/.cache/hs-integration-go:/go \
+ --name headscale-test-suite \
+ --volume $PWD:$PWD -w $PWD/integration \
+ --volume /var/run/docker.sock:/var/run/docker.sock \
+ --volume $PWD/control_logs:/tmp/control \
+ golang:1 \
+ go run gotest.tools/gotestsum@latest -- ./... \
+ -failfast \
+ -timeout 120m \
+ -parallel 1 \
+ -run "^TestNodeOnlineLastSeenStatus$"
+
+ - uses: actions/upload-artifact@v3
+ if: always() && steps.changed-files.outputs.any_changed == 'true'
+ with:
+ name: logs
+ path: "control_logs/*.log"
+
+ - uses: actions/upload-artifact@v3
+ if: always() && steps.changed-files.outputs.any_changed == 'true'
+ with:
+ name: pprof
+ path: "control_logs/*.pprof.tar"
diff --git a/.github/workflows/test-integration-v2-TestNodeRenameCommand.yaml b/.github/workflows/test-integration-v2-TestNodeRenameCommand.yaml
index c069948..e3ac56a 100644
--- a/.github/workflows/test-integration-v2-TestNodeRenameCommand.yaml
+++ b/.github/workflows/test-integration-v2-TestNodeRenameCommand.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestNodeRenameCommand
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestNodeTagCommand.yaml b/.github/workflows/test-integration-v2-TestNodeTagCommand.yaml
index 2fc7cc6..5e1e578 100644
--- a/.github/workflows/test-integration-v2-TestNodeTagCommand.yaml
+++ b/.github/workflows/test-integration-v2-TestNodeTagCommand.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestNodeTagCommand
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestOIDCAuthenticationPingAll.yaml b/.github/workflows/test-integration-v2-TestOIDCAuthenticationPingAll.yaml
index f8ece12..e333be2 100644
--- a/.github/workflows/test-integration-v2-TestOIDCAuthenticationPingAll.yaml
+++ b/.github/workflows/test-integration-v2-TestOIDCAuthenticationPingAll.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestOIDCAuthenticationPingAll
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestOIDCExpireNodesBasedOnTokenExpiry.yaml b/.github/workflows/test-integration-v2-TestOIDCExpireNodesBasedOnTokenExpiry.yaml
index 32b4d30..1f148c7 100644
--- a/.github/workflows/test-integration-v2-TestOIDCExpireNodesBasedOnTokenExpiry.yaml
+++ b/.github/workflows/test-integration-v2-TestOIDCExpireNodesBasedOnTokenExpiry.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestOIDCExpireNodesBasedOnTokenExpiry
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestPingAllByHostname.yaml b/.github/workflows/test-integration-v2-TestPingAllByHostname.yaml
index 712b6f4..fe9ad76 100644
--- a/.github/workflows/test-integration-v2-TestPingAllByHostname.yaml
+++ b/.github/workflows/test-integration-v2-TestPingAllByHostname.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestPingAllByHostname
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestPingAllByIP.yaml b/.github/workflows/test-integration-v2-TestPingAllByIP.yaml
index d460700..156ef73 100644
--- a/.github/workflows/test-integration-v2-TestPingAllByIP.yaml
+++ b/.github/workflows/test-integration-v2-TestPingAllByIP.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestPingAllByIP
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestPreAuthKeyCommand.yaml b/.github/workflows/test-integration-v2-TestPreAuthKeyCommand.yaml
index 744e68c..11f10b0 100644
--- a/.github/workflows/test-integration-v2-TestPreAuthKeyCommand.yaml
+++ b/.github/workflows/test-integration-v2-TestPreAuthKeyCommand.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestPreAuthKeyCommand
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestPreAuthKeyCommandReusableEphemeral.yaml b/.github/workflows/test-integration-v2-TestPreAuthKeyCommandReusableEphemeral.yaml
index 0485626..1be71ac 100644
--- a/.github/workflows/test-integration-v2-TestPreAuthKeyCommandReusableEphemeral.yaml
+++ b/.github/workflows/test-integration-v2-TestPreAuthKeyCommandReusableEphemeral.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestPreAuthKeyCommandReusableEphemeral
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestPreAuthKeyCommandWithoutExpiry.yaml b/.github/workflows/test-integration-v2-TestPreAuthKeyCommandWithoutExpiry.yaml
index 9540543..7d290cd 100644
--- a/.github/workflows/test-integration-v2-TestPreAuthKeyCommandWithoutExpiry.yaml
+++ b/.github/workflows/test-integration-v2-TestPreAuthKeyCommandWithoutExpiry.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestPreAuthKeyCommandWithoutExpiry
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestResolveMagicDNS.yaml b/.github/workflows/test-integration-v2-TestResolveMagicDNS.yaml
index 3c28e21..fbcf808 100644
--- a/.github/workflows/test-integration-v2-TestResolveMagicDNS.yaml
+++ b/.github/workflows/test-integration-v2-TestResolveMagicDNS.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestResolveMagicDNS
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestSSHIsBlockedInACL.yaml b/.github/workflows/test-integration-v2-TestSSHIsBlockedInACL.yaml
index ab4e360..bd19c8d 100644
--- a/.github/workflows/test-integration-v2-TestSSHIsBlockedInACL.yaml
+++ b/.github/workflows/test-integration-v2-TestSSHIsBlockedInACL.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestSSHIsBlockedInACL
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestSSHMultipleUsersAllToAll.yaml b/.github/workflows/test-integration-v2-TestSSHMultipleUsersAllToAll.yaml
index b934c23..00748aa 100644
--- a/.github/workflows/test-integration-v2-TestSSHMultipleUsersAllToAll.yaml
+++ b/.github/workflows/test-integration-v2-TestSSHMultipleUsersAllToAll.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestSSHMultipleUsersAllToAll
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestSSHNoSSHConfigured.yaml b/.github/workflows/test-integration-v2-TestSSHNoSSHConfigured.yaml
index 94e1a03..be8f38a 100644
--- a/.github/workflows/test-integration-v2-TestSSHNoSSHConfigured.yaml
+++ b/.github/workflows/test-integration-v2-TestSSHNoSSHConfigured.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestSSHNoSSHConfigured
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestSSHOneUserToAll.yaml b/.github/workflows/test-integration-v2-TestSSHOneUserToAll.yaml
index eda5184..62ab49b 100644
--- a/.github/workflows/test-integration-v2-TestSSHOneUserToAll.yaml
+++ b/.github/workflows/test-integration-v2-TestSSHOneUserToAll.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestSSHOneUserToAll
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestSSHUserOnlyIsolation.yaml b/.github/workflows/test-integration-v2-TestSSHUserOnlyIsolation.yaml
index 381eed5..8626453 100644
--- a/.github/workflows/test-integration-v2-TestSSHUserOnlyIsolation.yaml
+++ b/.github/workflows/test-integration-v2-TestSSHUserOnlyIsolation.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestSSHUserOnlyIsolation
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestTaildrop.yaml b/.github/workflows/test-integration-v2-TestTaildrop.yaml
index 10dbec8..e64eede 100644
--- a/.github/workflows/test-integration-v2-TestTaildrop.yaml
+++ b/.github/workflows/test-integration-v2-TestTaildrop.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestTaildrop
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestTailscaleNodesJoiningHeadcale.yaml b/.github/workflows/test-integration-v2-TestTailscaleNodesJoiningHeadcale.yaml
index 138e707..c406b2b 100644
--- a/.github/workflows/test-integration-v2-TestTailscaleNodesJoiningHeadcale.yaml
+++ b/.github/workflows/test-integration-v2-TestTailscaleNodesJoiningHeadcale.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestTailscaleNodesJoiningHeadcale
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.github/workflows/test-integration-v2-TestUserCommand.yaml b/.github/workflows/test-integration-v2-TestUserCommand.yaml
index 765e37d..667ad43 100644
--- a/.github/workflows/test-integration-v2-TestUserCommand.yaml
+++ b/.github/workflows/test-integration-v2-TestUserCommand.yaml
@@ -35,8 +35,11 @@ jobs:
config-example.yaml
- name: Run TestUserCommand
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -46,7 +49,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/.gitignore b/.gitignore
index 3b85ecb..f6e506b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
ignored/
tailscale/
+.vscode/
# Binaries for programs and plugins
*.exe
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f52c91f..6484bf3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,11 +23,18 @@ after improving the test harness as part of adopting [#1460](https://github.com/
### BREAKING
-Code reorganisation, a lot of code has moved, please review the following PRs accordingly [#1473](https://github.com/juanfont/headscale/pull/1473)
-API: Machine is now Node [#1553](https://github.com/juanfont/headscale/pull/1553)
+- Code reorganisation, a lot of code has moved, please review the following PRs accordingly [#1473](https://github.com/juanfont/headscale/pull/1473)
+- API: Machine is now Node [#1553](https://github.com/juanfont/headscale/pull/1553)
+- Remove support for older Tailscale clients [#1611](https://github.com/juanfont/headscale/pull/1611)
+ - The latest supported client is 1.36
+- Headscale checks that _at least_ one DERP is defined at start [#1564](https://github.com/juanfont/headscale/pull/1564)
+ - If no DERP is configured, the server will fail to start, this can be because it cannot load the DERPMap from file or url.
+- Embedded DERP server requires a private key [#1611](https://github.com/juanfont/headscale/pull/1611)
+ - Add a filepath entry to [`derp.server.private_key_path`](https://github.com/juanfont/headscale/blob/b35993981297e18393706b2c963d6db882bba6aa/config-example.yaml#L95)
### Changes
+Use versioned migrations [#1644](https://github.com/juanfont/headscale/pull/1644)
Make the OIDC callback page better [#1484](https://github.com/juanfont/headscale/pull/1484)
Allow use of the username OIDC claim [#1287](https://github.com/juanfont/headscale/pull/1287)
SSH support [#1487](https://github.com/juanfont/headscale/pull/1487)
@@ -36,6 +43,7 @@ Use error group handling to ensure tests actually pass [#1535](https://github.co
Fix hang on SIGTERM [#1492](https://github.com/juanfont/headscale/pull/1492) taken from [#1480](https://github.com/juanfont/headscale/pull/1480)
Send logs to stderr by default [#1524](https://github.com/juanfont/headscale/pull/1524)
Fix [TS-2023-006](https://tailscale.com/security-bulletins/#ts-2023-006) security UPnP issue [#1563](https://github.com/juanfont/headscale/pull/1563)
+Turn off gRPC logging [#1640](https://github.com/juanfont/headscale/pull/1640) fixes [#1259](https://github.com/juanfont/headscale/issues/1259)
Add `oidc.groups_claim`, `oidc.email_claim`, and `oidc.username_claim` to allow setting those claim names [#1594](https://github.com/juanfont/headscale/pull/1594)
## 0.22.3 (2023-05-12)
diff --git a/Dockerfile b/Dockerfile
index 0e6774d..367afe9 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -9,7 +9,7 @@ RUN go mod download
COPY . .
-RUN CGO_ENABLED=0 GOOS=linux go install -tags ts2019 -ldflags="-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$VERSION" -a ./cmd/headscale
+RUN CGO_ENABLED=0 GOOS=linux go install -ldflags="-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$VERSION" -a ./cmd/headscale
RUN strip /go/bin/headscale
RUN test -e /go/bin/headscale
@@ -17,9 +17,9 @@ RUN test -e /go/bin/headscale
FROM docker.io/debian:bookworm-slim
RUN apt-get update \
- && apt-get install -y ca-certificates \
- && rm -rf /var/lib/apt/lists/* \
- && apt-get clean
+ && apt-get install -y ca-certificates \
+ && rm -rf /var/lib/apt/lists/* \
+ && apt-get clean
COPY --from=build /go/bin/headscale /bin/headscale
ENV TZ UTC
diff --git a/Dockerfile.debug b/Dockerfile.debug
index 5a860f8..8f49d2b 100644
--- a/Dockerfile.debug
+++ b/Dockerfile.debug
@@ -9,7 +9,7 @@ RUN go mod download
COPY . .
-RUN CGO_ENABLED=0 GOOS=linux go install -tags ts2019 -ldflags="-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$VERSION" -a ./cmd/headscale
+RUN CGO_ENABLED=0 GOOS=linux go install -ldflags="-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$VERSION" -a ./cmd/headscale
RUN test -e /go/bin/headscale
# Debug image
@@ -19,9 +19,9 @@ COPY --from=build /go/bin/headscale /bin/headscale
ENV TZ UTC
RUN apt-get update \
- && apt-get install --no-install-recommends --yes less jq \
- && rm -rf /var/lib/apt/lists/* \
- && apt-get clean
+ && apt-get install --no-install-recommends --yes less jq \
+ && rm -rf /var/lib/apt/lists/* \
+ && apt-get clean
RUN mkdir -p /var/run/headscale
# Need to reset the entrypoint or everything will run as a busybox script
diff --git a/Makefile b/Makefile
index 4fdf418..442690e 100644
--- a/Makefile
+++ b/Makefile
@@ -10,8 +10,6 @@ ifeq ($(filter $(GOOS), openbsd netbsd soloaris plan9), )
else
endif
-TAGS = -tags ts2019
-
# GO_SOURCES = $(wildcard *.go)
# PROTO_SOURCES = $(wildcard **/*.proto)
GO_SOURCES = $(call rwildcard,,*.go)
@@ -24,7 +22,7 @@ build:
dev: lint test build
test:
- gotestsum -- $(TAGS) -short -coverprofile=coverage.out ./...
+ gotestsum -- -short -coverprofile=coverage.out ./...
test_integration:
docker run \
@@ -34,7 +32,7 @@ test_integration:
-v $$PWD:$$PWD -w $$PWD/integration \
-v /var/run/docker.sock:/var/run/docker.sock \
golang:1 \
- go run gotest.tools/gotestsum@latest -- $(TAGS) -failfast ./... -timeout 120m -parallel 8
+ go run gotest.tools/gotestsum@latest -- -failfast ./... -timeout 120m -parallel 8
lint:
golangci-lint run --fix --timeout 10m
diff --git a/README.md b/README.md
index 834c0ab..457e56f 100644
--- a/README.md
+++ b/README.md
@@ -466,6 +466,13 @@ make build
unreality
+
+
+
+
+ MichaelKo
+
+ |
@@ -473,6 +480,8 @@ make build
kevinlin
|
+
+
@@ -480,8 +489,6 @@ make build
Snack
|
-
-
@@ -517,6 +524,8 @@ make build
LIU HANCHENG
|
+
+
@@ -524,8 +533,6 @@ make build
Motiejus Jakštys
|
-
-
@@ -547,13 +554,6 @@ make build
Steven Honson
|
-
-
-
-
- MichaelKo
-
- |
@@ -577,6 +577,13 @@ make build
thomas
|
+
+
+
+
+ Andrei Pechkurov
+
+ |
@@ -598,13 +605,6 @@ make build
Albert Copeland
|
-
-
-
-
- Andrei Pechkurov
-
- |
@@ -658,6 +658,13 @@ make build
|
+
+
+
+
+ Azamat H. Hackimov
+
+ |
@@ -693,6 +700,8 @@ make build
Felix Kronlage-Dammers
|
+
+
@@ -700,8 +709,6 @@ make build
Felix Yan
|
-
-
@@ -723,6 +730,13 @@ make build
hrtkpf
|
+
+
+
+
+ JesseBot
+
+ |
@@ -730,6 +744,8 @@ make build
Jim Tittsler
|
+
+
@@ -744,8 +760,6 @@ make build
John Axel Eriksson
|
-
-
@@ -774,6 +788,8 @@ make build
Lucalux
|
+
+
@@ -788,8 +804,6 @@ make build
Mesar Hameed
|
-
-
@@ -818,6 +832,8 @@ make build
Pontus N
|
+
+
@@ -832,8 +848,6 @@ make build
rcursaru
|
-
-
@@ -850,9 +864,9 @@ make build
|
-
+
- Sebastian Muszytowski
+ Sebastian
|
@@ -862,6 +876,8 @@ make build
Shaanan Cohney
|
+
+
@@ -876,8 +892,6 @@ make build
Stefan VanBuren
|
-
-
@@ -906,6 +920,8 @@ make build
The Gitter Badger
|
+
+
@@ -920,8 +936,6 @@ make build
Till Hoffmann
|
-
-
@@ -950,6 +964,8 @@ make build
Zachary Newell
|
+
+
@@ -964,8 +980,6 @@ make build
Zhiyuan Zheng
|
-
-
@@ -994,6 +1008,8 @@ make build
dnaq
|
+
+
@@ -1008,8 +1024,6 @@ make build
ignoramous
|
-
-
@@ -1038,6 +1052,8 @@ make build
ma6174
|
+
+
@@ -1052,8 +1068,6 @@ make build
nicholas-yap
|
-
-
@@ -1082,6 +1096,8 @@ make build
zy
|
+
+
diff --git a/cmd/build-docker-img/main.go b/cmd/build-docker-img/main.go
deleted file mode 100644
index e162aa6..0000000
--- a/cmd/build-docker-img/main.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package main
-
-import (
- "log"
-
- "github.com/juanfont/headscale/integration"
- "github.com/juanfont/headscale/integration/tsic"
- "github.com/ory/dockertest/v3"
-)
-
-func main() {
- log.Printf("creating docker pool")
- pool, err := dockertest.NewPool("")
- if err != nil {
- log.Fatalf("could not connect to docker: %s", err)
- }
-
- log.Printf("creating docker network")
- network, err := pool.CreateNetwork("docker-integration-net")
- if err != nil {
- log.Fatalf("failed to create or get network: %s", err)
- }
-
- for _, version := range integration.AllVersions {
- log.Printf("creating container image for Tailscale (%s)", version)
-
- tsClient, err := tsic.New(
- pool,
- version,
- network,
- )
- if err != nil {
- log.Fatalf("failed to create tailscale node: %s", err)
- }
-
- err = tsClient.Shutdown()
- if err != nil {
- log.Fatalf("failed to shut down container: %s", err)
- }
- }
-
- network.Close()
- err = pool.RemoveNetwork(network)
- if err != nil {
- log.Fatalf("failed to remove network: %s", err)
- }
-}
diff --git a/cmd/gh-action-integration-generator/main.go b/cmd/gh-action-integration-generator/main.go
index 6c2db5b..d5798a9 100644
--- a/cmd/gh-action-integration-generator/main.go
+++ b/cmd/gh-action-integration-generator/main.go
@@ -56,8 +56,11 @@ jobs:
config-example.yaml
- name: Run {{.Name}}
+ uses: Wandalen/wretry.action@master
if: steps.changed-files.outputs.any_changed == 'true'
- run: |
+ with:
+ attempt_limit: 5
+ command: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
@@ -67,7 +70,6 @@ jobs:
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go run gotest.tools/gotestsum@latest -- ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
diff --git a/cmd/headscale/cli/api_key.go b/cmd/headscale/cli/api_key.go
index 37ef423..14293ae 100644
--- a/cmd/headscale/cli/api_key.go
+++ b/cmd/headscale/cli/api_key.go
@@ -67,7 +67,7 @@ var listAPIKeys = &cobra.Command{
}
if output != "" {
- SuccessOutput(response.ApiKeys, "", output)
+ SuccessOutput(response.GetApiKeys(), "", output)
return
}
@@ -75,11 +75,11 @@ var listAPIKeys = &cobra.Command{
tableData := pterm.TableData{
{"ID", "Prefix", "Expiration", "Created"},
}
- for _, key := range response.ApiKeys {
+ for _, key := range response.GetApiKeys() {
expiration := "-"
if key.GetExpiration() != nil {
- expiration = ColourTime(key.Expiration.AsTime())
+ expiration = ColourTime(key.GetExpiration().AsTime())
}
tableData = append(tableData, []string{
@@ -155,7 +155,7 @@ If you loose a key, create a new one and revoke (expire) the old one.`,
return
}
- SuccessOutput(response.ApiKey, response.ApiKey, output)
+ SuccessOutput(response.GetApiKey(), response.GetApiKey(), output)
},
}
diff --git a/cmd/headscale/cli/debug.go b/cmd/headscale/cli/debug.go
index 8250910..054fc07 100644
--- a/cmd/headscale/cli/debug.go
+++ b/cmd/headscale/cli/debug.go
@@ -4,10 +4,10 @@ import (
"fmt"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
- "github.com/juanfont/headscale/hscontrol/util"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"google.golang.org/grpc/status"
+ "tailscale.com/types/key"
)
const (
@@ -93,11 +93,13 @@ var createNodeCmd = &cobra.Command{
return
}
- if !util.NodePublicKeyRegex.Match([]byte(machineKey)) {
- err = errPreAuthKeyMalformed
+
+ var mkey key.MachinePublic
+ err = mkey.UnmarshalText([]byte(machineKey))
+ if err != nil {
ErrorOutput(
err,
- fmt.Sprintf("Error: %s", err),
+ fmt.Sprintf("Failed to parse machine key from flag: %s", err),
output,
)
@@ -133,6 +135,6 @@ var createNodeCmd = &cobra.Command{
return
}
- SuccessOutput(response.Node, "Node created", output)
+ SuccessOutput(response.GetNode(), "Node created", output)
},
}
diff --git a/cmd/headscale/cli/nodes.go b/cmd/headscale/cli/nodes.go
index 055d2e3..ac99624 100644
--- a/cmd/headscale/cli/nodes.go
+++ b/cmd/headscale/cli/nodes.go
@@ -152,8 +152,8 @@ var registerNodeCmd = &cobra.Command{
}
SuccessOutput(
- response.Node,
- fmt.Sprintf("Node %s registered", response.Node.GivenName), output)
+ response.GetNode(),
+ fmt.Sprintf("Node %s registered", response.GetNode().GetGivenName()), output)
},
}
@@ -196,12 +196,12 @@ var listNodesCmd = &cobra.Command{
}
if output != "" {
- SuccessOutput(response.Nodes, "", output)
+ SuccessOutput(response.GetNodes(), "", output)
return
}
- tableData, err := nodesToPtables(user, showTags, response.Nodes)
+ tableData, err := nodesToPtables(user, showTags, response.GetNodes())
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error converting to table: %s", err), output)
@@ -262,7 +262,7 @@ var expireNodeCmd = &cobra.Command{
return
}
- SuccessOutput(response.Node, "Node expired", output)
+ SuccessOutput(response.GetNode(), "Node expired", output)
},
}
@@ -310,7 +310,7 @@ var renameNodeCmd = &cobra.Command{
return
}
- SuccessOutput(response.Node, "Node renamed", output)
+ SuccessOutput(response.GetNode(), "Node renamed", output)
},
}
@@ -364,7 +364,7 @@ var deleteNodeCmd = &cobra.Command{
prompt := &survey.Confirm{
Message: fmt.Sprintf(
"Do you want to remove the node %s?",
- getResponse.GetNode().Name,
+ getResponse.GetNode().GetName(),
),
}
err = survey.AskOne(prompt, &confirm)
@@ -473,7 +473,7 @@ var moveNodeCmd = &cobra.Command{
return
}
- SuccessOutput(moveResponse.Node, "Node moved to another user", output)
+ SuccessOutput(moveResponse.GetNode(), "Node moved to another user", output)
},
}
@@ -493,7 +493,7 @@ func nodesToPtables(
"Ephemeral",
"Last seen",
"Expiration",
- "Online",
+ "Connected",
"Expired",
}
if showTags {
@@ -507,21 +507,21 @@ func nodesToPtables(
for _, node := range nodes {
var ephemeral bool
- if node.PreAuthKey != nil && node.PreAuthKey.Ephemeral {
+ if node.GetPreAuthKey() != nil && node.GetPreAuthKey().GetEphemeral() {
ephemeral = true
}
var lastSeen time.Time
var lastSeenTime string
- if node.LastSeen != nil {
- lastSeen = node.LastSeen.AsTime()
+ if node.GetLastSeen() != nil {
+ lastSeen = node.GetLastSeen().AsTime()
lastSeenTime = lastSeen.Format("2006-01-02 15:04:05")
}
var expiry time.Time
var expiryTime string
- if node.Expiry != nil {
- expiry = node.Expiry.AsTime()
+ if node.GetExpiry() != nil {
+ expiry = node.GetExpiry().AsTime()
expiryTime = expiry.Format("2006-01-02 15:04:05")
} else {
expiryTime = "N/A"
@@ -529,7 +529,7 @@ func nodesToPtables(
var machineKey key.MachinePublic
err := machineKey.UnmarshalText(
- []byte(util.MachinePublicKeyEnsurePrefix(node.MachineKey)),
+ []byte(node.GetMachineKey()),
)
if err != nil {
machineKey = key.MachinePublic{}
@@ -537,14 +537,14 @@ func nodesToPtables(
var nodeKey key.NodePublic
err = nodeKey.UnmarshalText(
- []byte(util.NodePublicKeyEnsurePrefix(node.NodeKey)),
+ []byte(node.GetNodeKey()),
)
if err != nil {
return nil, err
}
var online string
- if node.Online {
+ if node.GetOnline() {
online = pterm.LightGreen("online")
} else {
online = pterm.LightRed("offline")
@@ -558,36 +558,36 @@ func nodesToPtables(
}
var forcedTags string
- for _, tag := range node.ForcedTags {
+ for _, tag := range node.GetForcedTags() {
forcedTags += "," + tag
}
forcedTags = strings.TrimLeft(forcedTags, ",")
var invalidTags string
- for _, tag := range node.InvalidTags {
- if !contains(node.ForcedTags, tag) {
+ for _, tag := range node.GetInvalidTags() {
+ if !contains(node.GetForcedTags(), tag) {
invalidTags += "," + pterm.LightRed(tag)
}
}
invalidTags = strings.TrimLeft(invalidTags, ",")
var validTags string
- for _, tag := range node.ValidTags {
- if !contains(node.ForcedTags, tag) {
+ for _, tag := range node.GetValidTags() {
+ if !contains(node.GetForcedTags(), tag) {
validTags += "," + pterm.LightGreen(tag)
}
}
validTags = strings.TrimLeft(validTags, ",")
var user string
- if currentUser == "" || (currentUser == node.User.Name) {
- user = pterm.LightMagenta(node.User.Name)
+ if currentUser == "" || (currentUser == node.GetUser().GetName()) {
+ user = pterm.LightMagenta(node.GetUser().GetName())
} else {
// Shared into this user
- user = pterm.LightYellow(node.User.Name)
+ user = pterm.LightYellow(node.GetUser().GetName())
}
var IPV4Address string
var IPV6Address string
- for _, addr := range node.IpAddresses {
+ for _, addr := range node.GetIpAddresses() {
if netip.MustParseAddr(addr).Is4() {
IPV4Address = addr
} else {
@@ -596,8 +596,8 @@ func nodesToPtables(
}
nodeData := []string{
- strconv.FormatUint(node.Id, util.Base10),
- node.Name,
+ strconv.FormatUint(node.GetId(), util.Base10),
+ node.GetName(),
node.GetGivenName(),
machineKey.ShortString(),
nodeKey.ShortString(),
diff --git a/cmd/headscale/cli/preauthkeys.go b/cmd/headscale/cli/preauthkeys.go
index 7eb1956..c8dd2ad 100644
--- a/cmd/headscale/cli/preauthkeys.go
+++ b/cmd/headscale/cli/preauthkeys.go
@@ -84,7 +84,7 @@ var listPreAuthKeys = &cobra.Command{
}
if output != "" {
- SuccessOutput(response.PreAuthKeys, "", output)
+ SuccessOutput(response.GetPreAuthKeys(), "", output)
return
}
@@ -101,10 +101,10 @@ var listPreAuthKeys = &cobra.Command{
"Tags",
},
}
- for _, key := range response.PreAuthKeys {
+ for _, key := range response.GetPreAuthKeys() {
expiration := "-"
if key.GetExpiration() != nil {
- expiration = ColourTime(key.Expiration.AsTime())
+ expiration = ColourTime(key.GetExpiration().AsTime())
}
var reusable string
@@ -116,7 +116,7 @@ var listPreAuthKeys = &cobra.Command{
aclTags := ""
- for _, tag := range key.AclTags {
+ for _, tag := range key.GetAclTags() {
aclTags += "," + tag
}
@@ -214,7 +214,7 @@ var createPreAuthKeyCmd = &cobra.Command{
return
}
- SuccessOutput(response.PreAuthKey, response.PreAuthKey.Key, output)
+ SuccessOutput(response.GetPreAuthKey(), response.GetPreAuthKey().GetKey(), output)
},
}
diff --git a/cmd/headscale/cli/routes.go b/cmd/headscale/cli/routes.go
index 1872cbc..86ef295 100644
--- a/cmd/headscale/cli/routes.go
+++ b/cmd/headscale/cli/routes.go
@@ -87,12 +87,12 @@ var listRoutesCmd = &cobra.Command{
}
if output != "" {
- SuccessOutput(response.Routes, "", output)
+ SuccessOutput(response.GetRoutes(), "", output)
return
}
- routes = response.Routes
+ routes = response.GetRoutes()
} else {
response, err := client.GetNodeRoutes(ctx, &v1.GetNodeRoutesRequest{
NodeId: machineID,
@@ -108,12 +108,12 @@ var listRoutesCmd = &cobra.Command{
}
if output != "" {
- SuccessOutput(response.Routes, "", output)
+ SuccessOutput(response.GetRoutes(), "", output)
return
}
- routes = response.Routes
+ routes = response.GetRoutes()
}
tableData := routesToPtables(routes)
@@ -271,25 +271,25 @@ func routesToPtables(routes []*v1.Route) pterm.TableData {
for _, route := range routes {
var isPrimaryStr string
- prefix, err := netip.ParsePrefix(route.Prefix)
+ prefix, err := netip.ParsePrefix(route.GetPrefix())
if err != nil {
- log.Printf("Error parsing prefix %s: %s", route.Prefix, err)
+ log.Printf("Error parsing prefix %s: %s", route.GetPrefix(), err)
continue
}
if prefix == types.ExitRouteV4 || prefix == types.ExitRouteV6 {
isPrimaryStr = "-"
} else {
- isPrimaryStr = strconv.FormatBool(route.IsPrimary)
+ isPrimaryStr = strconv.FormatBool(route.GetIsPrimary())
}
tableData = append(tableData,
[]string{
- strconv.FormatUint(route.Id, Base10),
- route.Node.GivenName,
- route.Prefix,
- strconv.FormatBool(route.Advertised),
- strconv.FormatBool(route.Enabled),
+ strconv.FormatUint(route.GetId(), Base10),
+ route.GetNode().GetGivenName(),
+ route.GetPrefix(),
+ strconv.FormatBool(route.GetAdvertised()),
+ strconv.FormatBool(route.GetEnabled()),
isPrimaryStr,
})
}
diff --git a/cmd/headscale/cli/users.go b/cmd/headscale/cli/users.go
index 3132e99..e6463d6 100644
--- a/cmd/headscale/cli/users.go
+++ b/cmd/headscale/cli/users.go
@@ -67,7 +67,7 @@ var createUserCmd = &cobra.Command{
return
}
- SuccessOutput(response.User, "User created", output)
+ SuccessOutput(response.GetUser(), "User created", output)
},
}
@@ -169,7 +169,7 @@ var listUsersCmd = &cobra.Command{
}
if output != "" {
- SuccessOutput(response.Users, "", output)
+ SuccessOutput(response.GetUsers(), "", output)
return
}
@@ -236,6 +236,6 @@ var renameUserCmd = &cobra.Command{
return
}
- SuccessOutput(response.User, "User renamed", output)
+ SuccessOutput(response.GetUser(), "User renamed", output)
},
}
diff --git a/config-example.yaml b/config-example.yaml
index baf108d..5325840 100644
--- a/config-example.yaml
+++ b/config-example.yaml
@@ -40,19 +40,12 @@ grpc_listen_addr: 127.0.0.1:50443
# are doing.
grpc_allow_insecure: false
-# Private key used to encrypt the traffic between headscale
-# and Tailscale clients.
-# The private key file will be autogenerated if it's missing.
-#
-private_key_path: /var/lib/headscale/private.key
-
# The Noise section includes specific configuration for the
# TS2021 Noise protocol
noise:
# The Noise private key is used to encrypt the
# traffic between headscale and Tailscale clients when
- # using the new Noise-based protocol. It must be different
- # from the legacy private key.
+ # using the new Noise-based protocol.
private_key_path: /var/lib/headscale/noise_private.key
# List of IP prefixes to allocate tailaddresses from.
@@ -95,6 +88,12 @@ derp:
# For more details on how this works, check this great article: https://tailscale.com/blog/how-tailscale-works/
stun_listen_addr: "0.0.0.0:3478"
+ # Private key used to encrypt the traffic between headscale DERP
+ # and Tailscale clients.
+ # The private key file will be autogenerated if it's missing.
+ #
+ private_key_path: /var/lib/headscale/derp_server_private.key
+
# List of externally available DERP maps encoded in JSON
urls:
- https://controlplane.tailscale.com/derpmap/default
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 0000000..32bd08c
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,5 @@
+cairosvg~=2.7.1
+mkdocs-material~=9.4.14
+mkdocs-minify-plugin~=0.7.1
+pillow~=10.1.0
+
diff --git a/docs/running-headscale-container.md b/docs/running-headscale-container.md
index 18dacd9..c266358 100644
--- a/docs/running-headscale-container.md
+++ b/docs/running-headscale-container.md
@@ -28,7 +28,7 @@ cd ./headscale
touch ./config/db.sqlite
```
-3. **(Strongly Recommended)** Download a copy of the [example configuration][config-example.yaml](https://github.com/juanfont/headscale/blob/main/config-example.yaml) from the headscale repository.
+3. **(Strongly Recommended)** Download a copy of the [example configuration](https://github.com/juanfont/headscale/blob/main/config-example.yaml) from the headscale repository.
Using wget:
diff --git a/flake.lock b/flake.lock
index 50c55c6..91bd505 100644
--- a/flake.lock
+++ b/flake.lock
@@ -5,11 +5,11 @@
"systems": "systems"
},
"locked": {
- "lastModified": 1694529238,
- "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
+ "lastModified": 1701680307,
+ "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"owner": "numtide",
"repo": "flake-utils",
- "rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
+ "rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"type": "github"
},
"original": {
@@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1699186365,
- "narHash": "sha256-Pxrw5U8mBsL3NlrJ6q1KK1crzvSUcdfwb9083sKDrcU=",
+ "lastModified": 1701998057,
+ "narHash": "sha256-gAJGhcTO9cso7XDfAScXUlPcva427AUT2q02qrmXPdo=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "a0b3b06b7a82c965ae0bb1d59f6e386fe755001d",
+ "rev": "09dc04054ba2ff1f861357d0e7e76d021b273cd7",
"type": "github"
},
"original": {
diff --git a/flake.nix b/flake.nix
index 65f9d0a..3576e3a 100644
--- a/flake.nix
+++ b/flake.nix
@@ -26,14 +26,12 @@
version = headscaleVersion;
src = pkgs.lib.cleanSource self;
- tags = ["ts2019"];
-
# Only run unit tests when testing a build
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.
- vendorSha256 = "sha256-Q6eySc8lXYhkWka7Y+qOM6viv7QhdjFZDX8PttaLfr4=";
+ vendorHash = "sha256-8x4RKaS8vnBYTPlvQTkDKWIAJOgPF99hvPiuRyTMrA8=";
ldflags = ["-s" "-w" "-X github.com/juanfont/headscale/cmd/headscale/cli.Version=v${version}"];
};
@@ -49,7 +47,7 @@
sha256 = "sha256-2K9KAg8iSubiTbujyFGN3yggrL+EDyeUCs9OOta/19A=";
};
- vendorSha256 = "sha256-rxYuzn4ezAxaeDhxd8qdOzt+CKYIh03A9zKNdzILq18=";
+ vendorHash = "sha256-rxYuzn4ezAxaeDhxd8qdOzt+CKYIh03A9zKNdzILq18=";
nativeBuildInputs = [pkgs.installShellFiles];
};
@@ -71,7 +69,7 @@
sha256 = "sha256-lnNdsDCpeSHtl2lC1IhUw11t3cnGF+37qSM7HDvKLls=";
};
- vendorSha256 = "sha256-dGdnDuRbwg8fU7uB5GaHEWa/zI3w06onqjturvooJQA=";
+ vendorHash = "sha256-dGdnDuRbwg8fU7uB5GaHEWa/zI3w06onqjturvooJQA=";
nativeBuildInputs = [pkgs.installShellFiles];
@@ -129,15 +127,7 @@
buildInputs = devDeps;
shellHook = ''
- export GOFLAGS=-tags="ts2019"
export PATH="$PWD/result/bin:$PATH"
-
- mkdir -p ./ignored
- export HEADSCALE_PRIVATE_KEY_PATH="./ignored/private.key"
- export HEADSCALE_NOISE_PRIVATE_KEY_PATH="./ignored/noise_private.key"
- export HEADSCALE_DB_PATH="./ignored/db.sqlite"
- export HEADSCALE_TLS_LETSENCRYPT_CACHE_DIR="./ignored/cache"
- export HEADSCALE_UNIX_SOCKET="./ignored/headscale.sock"
'';
};
diff --git a/go.mod b/go.mod
index 590947b..66746fc 100644
--- a/go.mod
+++ b/go.mod
@@ -1,137 +1,181 @@
module github.com/juanfont/headscale
-go 1.21
+go 1.21.0
-toolchain go1.21.1
+toolchain go1.21.4
require (
- github.com/AlecAivazis/survey/v2 v2.3.6
- github.com/coreos/go-oidc/v3 v3.5.0
- github.com/davecgh/go-spew v1.1.1
- github.com/deckarep/golang-set/v2 v2.3.0
+ github.com/AlecAivazis/survey/v2 v2.3.7
+ github.com/coreos/go-oidc/v3 v3.8.0
+ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
+ github.com/deckarep/golang-set/v2 v2.4.0
github.com/efekarakus/termcolor v1.0.1
- github.com/glebarez/sqlite v1.7.0
+ github.com/glebarez/sqlite v1.10.0
+ github.com/go-gormigrate/gormigrate/v2 v2.1.1
github.com/gofrs/uuid/v5 v5.0.0
- github.com/google/go-cmp v0.5.9
- github.com/gorilla/mux v1.8.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.15.2
- github.com/klauspost/compress v1.16.7
+ github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1
+ github.com/klauspost/compress v1.17.3
github.com/oauth2-proxy/mockoidc v0.0.0-20220308204021-b9169deeb282
- github.com/ory/dockertest/v3 v3.9.1
+ github.com/ory/dockertest/v3 v3.10.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/philip-bui/grpc-zerolog v1.0.1
github.com/pkg/profile v1.7.0
- github.com/prometheus/client_golang v1.15.1
- github.com/prometheus/common v0.42.0
- github.com/pterm/pterm v0.12.58
- github.com/puzpuzpuz/xsync/v2 v2.4.0
- github.com/rs/zerolog v1.29.0
+ github.com/prometheus/client_golang v1.17.0
+ github.com/prometheus/common v0.45.0
+ github.com/pterm/pterm v0.12.71
+ github.com/puzpuzpuz/xsync/v3 v3.0.2
+ github.com/rs/zerolog v1.31.0
github.com/samber/lo v1.38.1
- github.com/spf13/cobra v1.7.0
- github.com/spf13/viper v1.16.0
+ github.com/spf13/cobra v1.8.0
+ github.com/spf13/viper v1.17.0
github.com/stretchr/testify v1.8.4
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a
+ github.com/tailscale/tailsql v0.0.0-20231216172832-51483e0c711b
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
- go4.org/netipx v0.0.0-20230728180743-ad4cb58a6516
- golang.org/x/crypto v0.12.0
- golang.org/x/net v0.14.0
- golang.org/x/oauth2 v0.7.0
- golang.org/x/sync v0.2.0
- google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1
- google.golang.org/grpc v1.55.0
- google.golang.org/protobuf v1.30.0
+ go4.org/netipx v0.0.0-20230824141953-6213f710f925
+ golang.org/x/crypto v0.16.0
+ golang.org/x/exp v0.0.0-20231127185646-65229373498e
+ golang.org/x/net v0.19.0
+ golang.org/x/oauth2 v0.15.0
+ golang.org/x/sync v0.5.0
+ google.golang.org/genproto/googleapis/api v0.0.0-20231127180814-3a041ad873d4
+ google.golang.org/grpc v1.59.0
+ google.golang.org/protobuf v1.31.0
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
gopkg.in/yaml.v3 v3.0.1
- gorm.io/driver/postgres v1.4.8
- gorm.io/gorm v1.24.6
- tailscale.com v1.50.0
+ gorm.io/driver/postgres v1.5.4
+ gorm.io/gorm v1.25.5
+ tailscale.com v1.56.1
)
require (
- atomicgo.dev/cursor v0.1.1 // indirect
+ atomicgo.dev/cursor v0.2.0 // indirect
atomicgo.dev/keyboard v0.2.9 // indirect
+ atomicgo.dev/schedule v0.1.0 // indirect
filippo.io/edwards25519 v1.0.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/akutz/memconn v0.1.0 // indirect
- github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect
+ github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
+ github.com/aws/aws-sdk-go-v2 v1.21.0 // indirect
+ github.com/aws/aws-sdk-go-v2/config v1.18.42 // indirect
+ github.com/aws/aws-sdk-go-v2/credentials v1.13.40 // indirect
+ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/ini v1.3.43 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ssm v1.38.0 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sso v1.14.1 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.1 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sts v1.22.0 // indirect
+ github.com/aws/smithy-go v1.14.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
- github.com/cenkalti/backoff/v4 v4.2.0 // indirect
+ github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/containerd/console v1.0.3 // indirect
- github.com/containerd/continuity v0.3.0 // indirect
- github.com/coreos/go-iptables v0.6.0 // indirect
- github.com/dblohm7/wingoes v0.0.0-20230821191801-fc76608aecf0 // indirect
- github.com/docker/cli v23.0.5+incompatible // indirect
- github.com/docker/docker v24.0.4+incompatible // indirect
+ github.com/containerd/continuity v0.4.3 // indirect
+ github.com/coreos/go-iptables v0.7.0 // indirect
+ github.com/coreos/go-systemd/v22 v22.5.0 // indirect
+ github.com/dblohm7/wingoes v0.0.0-20231025182615-65d8b4b5428f // indirect
+ github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e // indirect
+ github.com/docker/cli v24.0.7+incompatible // indirect
+ github.com/docker/docker v24.0.7+incompatible // indirect
github.com/docker/go-connections v0.4.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.3 // indirect
- github.com/fsnotify/fsnotify v1.6.0 // indirect
- github.com/fxamacker/cbor/v2 v2.4.0 // indirect
- github.com/glebarez/go-sqlite v1.20.3 // indirect
- github.com/go-jose/go-jose/v3 v3.0.0 // indirect
+ github.com/fsnotify/fsnotify v1.7.0 // indirect
+ github.com/fxamacker/cbor/v2 v2.5.0 // indirect
+ github.com/glebarez/go-sqlite v1.21.2 // indirect
+ github.com/go-jose/go-jose/v3 v3.0.1 // indirect
+ github.com/go-ole/go-ole v1.3.0 // indirect
+ github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
+ github.com/google/btree v1.1.2 // indirect
github.com/google/go-github v17.0.0+incompatible // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/nftables v0.1.1-0.20230115205135-9aa6fdf5a28c // indirect
- github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 // indirect
+ github.com/google/pprof v0.0.0-20231127191134-f3a68a39ae15 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
- github.com/google/uuid v1.3.0 // indirect
- github.com/gookit/color v1.5.3 // indirect
+ github.com/google/uuid v1.4.0 // indirect
+ github.com/gookit/color v1.5.4 // indirect
+ github.com/gorilla/csrf v1.7.1 // indirect
+ github.com/gorilla/securecookie v1.1.1 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hdevalence/ed25519consensus v0.1.0 // indirect
+ github.com/illarion/gonotify v1.0.1 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/insomniacslk/dhcp v0.0.0-20230908212754-65c27093e38a // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
- github.com/jackc/pgx/v5 v5.3.0 // indirect
+ github.com/jackc/pgx/v5 v5.5.0 // indirect
+ github.com/jackc/puddle/v2 v2.2.1 // 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.3.2 // indirect
+ github.com/jsimonetti/rtnetlink v1.4.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
+ github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/lib/pq v1.10.7 // indirect
- github.com/lithammer/fuzzysearch v1.1.5 // indirect
+ github.com/lithammer/fuzzysearch v1.1.8 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
- github.com/mattn/go-isatty v0.0.18 // indirect
- github.com/mattn/go-runewidth v0.0.14 // indirect
- github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-runewidth v0.0.15 // indirect
+ github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
+ github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect
- github.com/mdlayher/socket v0.4.1 // 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
- github.com/miekg/dns v1.1.55 // indirect
+ github.com/miekg/dns v1.1.57 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
- github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect
+ github.com/moby/term v0.5.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
- github.com/opencontainers/image-spec v1.1.0-rc3 // indirect
- github.com/opencontainers/runc v1.1.4 // indirect
- github.com/pelletier/go-toml/v2 v2.0.8 // indirect
+ github.com/opencontainers/image-spec v1.1.0-rc5 // indirect
+ github.com/opencontainers/runc v1.1.10 // indirect
+ github.com/pelletier/go-toml/v2 v2.1.0 // indirect
+ github.com/pierrec/lz4/v4 v4.1.18 // indirect
github.com/pkg/errors v0.9.1 // indirect
- github.com/pmezard/go-difflib v1.0.0 // indirect
- github.com/prometheus/client_model v0.4.0 // indirect
- github.com/prometheus/procfs v0.9.0 // indirect
+ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+ github.com/prometheus/client_model v0.5.0 // indirect
+ github.com/prometheus/procfs v0.12.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.4 // indirect
- github.com/rogpeppe/go-internal v1.10.0 // indirect
- github.com/sirupsen/logrus v1.9.0 // indirect
- github.com/spf13/afero v1.9.5 // indirect
- github.com/spf13/cast v1.5.1 // indirect
- github.com/spf13/jwalterweatherman v1.1.0 // indirect
+ github.com/rogpeppe/go-internal v1.11.0 // indirect
+ github.com/safchain/ethtool v0.3.0 // indirect
+ github.com/sagikazarmark/locafero v0.4.0 // indirect
+ github.com/sagikazarmark/slog-shim v0.1.0 // indirect
+ github.com/sirupsen/logrus v1.9.3 // indirect
+ github.com/sourcegraph/conc v0.3.0 // indirect
+ github.com/spf13/afero v1.11.0 // indirect
+ github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
- github.com/subosito/gotenv v1.4.2 // indirect
+ github.com/subosito/gotenv v1.6.0 // indirect
+ github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect
+ github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
+ github.com/tailscale/golang-x-crypto v0.0.0-20230713185742-f0b76a10a08e // indirect
+ github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 // indirect
+ github.com/tailscale/setec v0.0.0-20230926024544-07dde05889e7 // indirect
+ github.com/tailscale/web-client-prebuilt v0.0.0-20231213172531-a4fa669015b2 // indirect
+ github.com/tailscale/wireguard-go v0.0.0-20231121184858-cc193a0b3272 // indirect
+ github.com/tcnksm/go-httpstat v0.2.0 // indirect
+ github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect
github.com/vishvananda/netlink v1.2.1-beta.2 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
github.com/x448/float16 v0.8.4 // indirect
@@ -139,23 +183,27 @@ require (
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
+ go.uber.org/multierr v1.11.0 // indirect
go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect
- golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 // indirect
- golang.org/x/mod v0.11.0 // indirect
- golang.org/x/sys v0.11.0 // indirect
- golang.org/x/term v0.11.0 // indirect
- golang.org/x/text v0.12.0 // indirect
- golang.org/x/time v0.3.0 // indirect
- golang.org/x/tools v0.9.1 // indirect
+ golang.org/x/mod v0.14.0 // indirect
+ golang.org/x/sys v0.15.0 // indirect
+ golang.org/x/term v0.15.0 // indirect
+ golang.org/x/text v0.14.0 // indirect
+ golang.org/x/time v0.5.0 // indirect
+ golang.org/x/tools v0.16.0 // indirect
+ golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
- google.golang.org/appengine v1.6.7 // indirect
+ google.golang.org/appengine v1.6.8 // indirect
+ google.golang.org/genproto v0.0.0-20231127180814-3a041ad873d4 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
- gotest.tools/v3 v3.4.0 // indirect
- modernc.org/libc v1.22.2 // indirect
- modernc.org/mathutil v1.5.0 // indirect
- modernc.org/memory v1.5.0 // indirect
- modernc.org/sqlite v1.20.3 // indirect
- nhooyr.io/websocket v1.8.7 // indirect
+ gvisor.dev/gvisor v0.0.0-20230928000133-4fe30062272c // indirect
+ inet.af/peercred v0.0.0-20210906144145-0893ea02156a // indirect
+ modernc.org/libc v1.34.11 // indirect
+ modernc.org/mathutil v1.6.0 // indirect
+ modernc.org/memory v1.7.2 // indirect
+ modernc.org/sqlite v1.28.0 // indirect
+ nhooyr.io/websocket v1.8.10 // indirect
)
diff --git a/go.sum b/go.sum
index 7eaf620..380fcb2 100644
--- a/go.sum
+++ b/go.sum
@@ -1,58 +1,23 @@
atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg=
atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ=
-atomicgo.dev/cursor v0.1.1 h1:0t9sxQomCTRh5ug+hAMCs59x/UmC9QL6Ci5uosINKD4=
-atomicgo.dev/cursor v0.1.1/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU=
+atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw=
+atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU=
atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8=
atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ=
+atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs=
+atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
-cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
-cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
-cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
-cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
-cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
-cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
-cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
-cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
-cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
-cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
-cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
-cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
-cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
-cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
-cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
-cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
-cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
-cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
-cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
-cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
-cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
-cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
-cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
-cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
-cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
-cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
-cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
-cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
-cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
-cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
-cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
-cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
-cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
-cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
-cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
-dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek=
filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc=
filippo.io/mkcert v1.4.4/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA=
-github.com/AlecAivazis/survey/v2 v2.3.6 h1:NvTuVHISgTHEHeBFqt6BHOe4Ny/NwGZr7w+F8S9ziyw=
-github.com/AlecAivazis/survey/v2 v2.3.6/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI=
+github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
+github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
+github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs=
github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8=
github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII=
@@ -70,59 +35,95 @@ github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEV
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
-github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA=
-github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
+github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
+github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
+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.21.0 h1:gMT0IW+03wtYJhRqTVYn0wLzwdnK9sRMcxmtfGzRdJc=
+github.com/aws/aws-sdk-go-v2 v1.21.0/go.mod h1:/RfNgGmRxI+iFOB1OeJUyxiU+9s88k3pfHvDagGEp0M=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno=
+github.com/aws/aws-sdk-go-v2/config v1.18.42 h1:28jHROB27xZwU0CB88giDSjz7M1Sba3olb5JBGwina8=
+github.com/aws/aws-sdk-go-v2/config v1.18.42/go.mod h1:4AZM3nMMxwlG+eZlxvBKqwVbkDLlnN2a4UGTL6HjaZI=
+github.com/aws/aws-sdk-go-v2/credentials v1.13.40 h1:s8yOkDh+5b1jUDhMBtngF6zKWLDs84chUk2Vk0c38Og=
+github.com/aws/aws-sdk-go-v2/credentials v1.13.40/go.mod h1:VtEHVAAqDWASwdOqj/1huyT6uHbs5s8FUHfDQdky/Rs=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11 h1:uDZJF1hu0EVT/4bogChk8DyjSF6fof6uL/0Y26Ma7Fg=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11/go.mod h1:TEPP4tENqBGO99KwVpV9MlOX4NSrSLP8u3KRy2CDwA8=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41 h1:22dGT7PneFMx4+b3pz7lMTRyN8ZKH7M2cW4GP9yUS2g=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41/go.mod h1:CrObHAuPneJBlfEJ5T3szXOUkLEThaGfvnhTf33buas=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35 h1:SijA0mgjV8E+8G45ltVHs0fvKpTj8xmZJ3VwhGKtUSI=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35/go.mod h1:SJC1nEVVva1g3pHAIdCp7QsRIkMmLAgoDquQ9Rr8kYw=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.3.43 h1:g+qlObJH4Kn4n21g69DjspU0hKTjWtq7naZ9OLCv0ew=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.3.43/go.mod h1:rzfdUlfA+jdgLDmPKjd3Chq9V7LVLYo1Nz++Wb91aRo=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 h1:AzwRi5OKKwo4QNqPf7TjeO+tK8AyOK3GVSwmRPo7/Cs=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25/go.mod h1:SUbB4wcbSEyCvqBxv/O/IBf93RbEze7U7OnoTlpPB+g=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28 h1:vGWm5vTpMr39tEZfQeDiDAMgk+5qsnvRny3FjLpnH5w=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28/go.mod h1:spfrICMD6wCAhjhzHuy6DOZZ+LAIY10UxhUmLzpJTTs=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35 h1:CdzPW9kKitgIiLV1+MHobfR5Xg25iYnyzWZhyQuSlDI=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35/go.mod h1:QGF2Rs33W5MaN9gYdEQOBBFPLwTZkEhRwI33f7KIG0o=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2 h1:NbWkRxEEIRSCqxhsHQuMiTH7yo+JZW1gp8v3elSVMTQ=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2/go.mod h1:4tfW5l4IAB32VWCDEBxCRtR9T4BWy4I4kr1spr8NgZM=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.33.0 h1:L5h2fymEdVJYvn6hYO8Jx48YmC6xVmjmgHJV3oGKgmc=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.33.0/go.mod h1:J9kLNzEiHSeGMyN7238EjJmBpCniVzFda75Gxl/NqB8=
+github.com/aws/aws-sdk-go-v2/service/ssm v1.38.0 h1:JON9MBvwUlM8HXylfB2caZuH3VXz9RxO4SMp2+TNc3Q=
+github.com/aws/aws-sdk-go-v2/service/ssm v1.38.0/go.mod h1:JjBzoceyKkpQY3v1GPIdg6kHqUFHRJ7SDlwtwoH0Qh8=
+github.com/aws/aws-sdk-go-v2/service/sso v1.14.1 h1:YkNzx1RLS0F5qdf9v1Q8Cuv9NXCL2TkosOxhzlUPV64=
+github.com/aws/aws-sdk-go-v2/service/sso v1.14.1/go.mod h1:fIAwKQKBFu90pBxx07BFOMJLpRUGu8VOzLJakeY+0K4=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.1 h1:8lKOidPkmSmfUtiTgtdXWgaKItCZ/g75/jEk6Ql6GsA=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.1/go.mod h1:yygr8ACQRY2PrEcy3xsUI357stq2AxnFM6DIsR9lij4=
+github.com/aws/aws-sdk-go-v2/service/sts v1.22.0 h1:s4bioTgjSFRwOoyEFzAVCmFmoowBgjTR8gkrF/sQ4wk=
+github.com/aws/aws-sdk-go-v2/service/sts v1.22.0/go.mod h1:VC7JDqsqiwXukYEDjoHh9U0fOJtNWh04FPQz4ct4GGU=
+github.com/aws/smithy-go v1.14.2 h1:MJU9hqBGbvWZdApzpvoF2WAIJDbtjK2NDJSiJP7HblQ=
+github.com/aws/smithy-go v1.14.2/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
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=
-github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4=
-github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
+github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
+github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
-github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA=
-github.com/cilium/ebpf v0.10.0 h1:nk5HPMeoBXtOzbkZBWym+ZWq1GIiHUsBFXxwewXAHLQ=
-github.com/cilium/ebpf v0.10.0/go.mod h1:DPiVdY/kT534dgc9ERmvP8mWA+9gvwgKfRvk4nNWnoE=
+github.com/cilium/ebpf v0.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4=
+github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
-github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
-github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg=
-github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM=
-github.com/coreos/go-iptables v0.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo4jk=
-github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
-github.com/coreos/go-oidc/v3 v3.5.0 h1:VxKtbccHZxs8juq7RdJntSqtXFtde9YpNpGn0yqgEHw=
-github.com/coreos/go-oidc/v3 v3.5.0/go.mod h1:ecXRtV4romGPeO6ieExAsUK9cb/3fp9hXNz1tlv8PIM=
-github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
-github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
-github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
-github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8=
+github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
+github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8=
+github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
+github.com/coreos/go-oidc/v3 v3.8.0 h1:s3e30r6VEl3/M7DTSCEuImmrfu1/1WBgA0cXkdzkrAY=
+github.com/coreos/go-oidc/v3 v3.8.0/go.mod h1:yQzSCqBnK3e6Fs5l+f5i0F8Kwf0zpH9bPEsbY00KanM=
+github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
+github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
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.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
-github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/dblohm7/wingoes v0.0.0-20230821191801-fc76608aecf0 h1:/dgKwHVTI0J+A0zd/BHOF2CTn1deN0735cJrb+w2hbE=
-github.com/dblohm7/wingoes v0.0.0-20230821191801-fc76608aecf0/go.mod h1:6NCrWM5jRefaG7iN0iMShPalLsljHWBh9v1zxM2f8Xs=
-github.com/deckarep/golang-set/v2 v2.3.0 h1:qs18EKUfHm2X9fA50Mr/M5hccg2tNnVqsiBImnyDs0g=
-github.com/deckarep/golang-set/v2 v2.3.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
-github.com/docker/cli v23.0.5+incompatible h1:ufWmAOuD3Vmr7JP2G5K3cyuNC4YZWiAsuDEvFVVDafE=
-github.com/docker/cli v23.0.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
-github.com/docker/docker v24.0.4+incompatible h1:s/LVDftw9hjblvqIeTiGYXBCD95nOEEl7qRsRrIOuQI=
-github.com/docker/docker v24.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dblohm7/wingoes v0.0.0-20231025182615-65d8b4b5428f h1:c5mkOIXbHZVKGQaSEZZyLW9ORD+h4PT2TPF8IQPwyOs=
+github.com/dblohm7/wingoes v0.0.0-20231025182615-65d8b4b5428f/go.mod h1:6NCrWM5jRefaG7iN0iMShPalLsljHWBh9v1zxM2f8Xs=
+github.com/deckarep/golang-set/v2 v2.4.0 h1:DnfgWKdhvHM8Kihdw9fKWXd08EdsPiyoHsk5bfsmkNI=
+github.com/deckarep/golang-set/v2 v2.4.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
+github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
+github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
+github.com/docker/cli v24.0.7+incompatible h1:wa/nIwYFW7BVTGa7SWPVyyXU9lgORqUb1xfI36MSkFg=
+github.com/docker/cli v24.0.7+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM=
+github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
-github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
@@ -132,51 +133,35 @@ github.com/efekarakus/termcolor v1.0.1/go.mod h1:AitrZNrE4nPO538fRsqf+p0WgLdAsGN
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
-github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
-github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
-github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA=
-github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
-github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
-github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
-github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88=
-github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
-github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
-github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
-github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
-github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
-github.com/glebarez/go-sqlite v1.20.3 h1:89BkqGOXR9oRmG58ZrzgoY/Fhy5x0M+/WV48U5zVrZ4=
-github.com/glebarez/go-sqlite v1.20.3/go.mod h1:u3N6D/wftiAzIOJtZl6BmedqxmmkDfH3q+ihjqxC9u0=
-github.com/glebarez/sqlite v1.7.0 h1:A7Xj/KN2Lvie4Z4rrgQHY8MsbebX3NyWsL3n2i82MVI=
-github.com/glebarez/sqlite v1.7.0/go.mod h1:PkeevrRlF/1BhQBCnzcMWzgrIk7IOop+qS2jUYLfHhk=
-github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo=
-github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
+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/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE=
+github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
+github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
+github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=
+github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
+github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
+github.com/glebarez/sqlite v1.10.0 h1:u4gt8y7OND/cCei/NMHmfbLxF6xP2wgKcT/BJf2pYkc=
+github.com/glebarez/sqlite v1.10.0/go.mod h1:IJ+lfSOmiekhQsFTJRx/lHtGYmCdtAiTaf5wI9u5uHA=
+github.com/go-gormigrate/gormigrate/v2 v2.1.1 h1:eGS0WTFRV30r103lU8JNXY27KbviRnqqIDobW3EV3iY=
+github.com/go-gormigrate/gormigrate/v2 v2.1.1/go.mod h1:L7nJ620PFDKei9QOhJzqA8kRCk+E3UbV2f5gv+1ndLc=
+github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
+github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
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-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
-github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
-github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
-github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
-github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
-github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
-github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
+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.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
-github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
-github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
-github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
-github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
-github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
-github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
-github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg=
+github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU=
github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M=
github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
@@ -184,242 +169,194 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE=
-github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ=
-github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
-github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
-github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
-github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
-github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
-github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
-github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
-github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
-github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
-github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
-github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
+github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
-github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
-github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
-github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
-github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
-github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/nftables v0.1.1-0.20230115205135-9aa6fdf5a28c h1:06RMfw+TMMHtRuUOroMeatRCCgSMWXCJQeABvHU69YQ=
github.com/google/nftables v0.1.1-0.20230115205135-9aa6fdf5a28c/go.mod h1:BVIYo3cdnT4qSylnYqcd5YtmXhr51cJPGtnLBe/uLBU=
-github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
-github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
-github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
-github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/pprof v0.0.0-20231127191134-f3a68a39ae15 h1:t2sLhFuGXwoomaKLTuoxFfFqqlG1Gp2DpsupXq3UvZ0=
+github.com/google/pprof v0.0.0-20231127191134-f3a68a39ae15/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
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.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
-github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
-github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
-github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
+github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
+github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo=
-github.com/gookit/color v1.5.3 h1:twfIhZs4QLCtimkP7MOxlF3A0U/5cDPseRT9M/+2SCE=
-github.com/gookit/color v1.5.3/go.mod h1:NUzwzeehUfl7GIb36pqId+UGmRfQcU/WiiyTTeNjHtE=
-github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
-github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
-github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
-github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+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.1 h1:Ir3o2c1/Uzj6FBxMlAUB6SivgVMy1ONXwYgXn+/aHPE=
+github.com/gorilla/csrf v1.7.1/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA=
+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.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
+github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
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.15.2 h1:gDLXvp5S9izjldquuoAhDzccbskOL6tDC5jMSyx3zxE=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2/go.mod h1:7pdNwVWBBHGiCxa9lAszqCJMbfTISJ7oMftp8+UGV08=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 h1:6UKoz5ujsI55KNpsJH3UwCq3T8kKbZwNZBNPuTTje8U=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1/go.mod h1:YvJ2f6MplWDhfxiUC3KpyTy76kYUZA4W3pTv/wdKQ9Y=
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
-github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hdevalence/ed25519consensus v0.1.0 h1:jtBwzzcHuTmFrQN6xQZn6CQEO/V9f7HsjsjeEZ6auqU=
github.com/hdevalence/ed25519consensus v0.1.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
-github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
+github.com/illarion/gonotify v1.0.1 h1:F1d+0Fgbq/sDWjj/r66ekjDG+IDeecQKUFH4wNwsoio=
+github.com/illarion/gonotify v1.0.1/go.mod h1:zt5pmDofZpU1f8aqlK0+95eQhoEAn/d4G4B/FjVW4jE=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/insomniacslk/dhcp v0.0.0-20230908212754-65c27093e38a h1:S33o3djA1nPRd+d/bf7jbbXytXuK/EoXow7+aa76grQ=
+github.com/insomniacslk/dhcp v0.0.0-20230908212754-65c27093e38a/go.mod h1:zmdm3sTSDP3vOOX3CEWRkkRHtKr1DxBx+J1OQFoDQQs=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
-github.com/jackc/pgx/v5 v5.3.0 h1:/NQi8KHMpKWHInxXesC8yD4DhkXPrVhmnwYkjp9AmBA=
-github.com/jackc/pgx/v5 v5.3.0/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
-github.com/jackc/puddle/v2 v2.2.0/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
+github.com/jackc/pgx/v5 v5.5.0 h1:NxstgwndsTRy7eq9/kqYc/BZh5w2hHJV86wjvO+1xPw=
+github.com/jackc/pgx/v5 v5.5.0/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
+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/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
-github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
+github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
+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/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.3.2 h1:dcn0uWkfxycEEyNy0IGfx3GrhQ38LH7odjxAghimsVI=
-github.com/jsimonetti/rtnetlink v1.3.2/go.mod h1:BBu4jZCpTjP6Gk0/wfrO8qcqymnN3g0hoFqObRmUo6U=
-github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
-github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
-github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
-github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I=
+github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E=
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.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
-github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
-github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
+github.com/klauspost/compress v1.17.3 h1:qkRjuerhUU1EmXLYGkSH6EZL+vPSxIrYjLNAK4slzwA=
+github.com/klauspost/compress v1.17.3/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
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=
github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU=
github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ=
+github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=
+github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
-github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
-github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
-github.com/lithammer/fuzzysearch v1.1.5 h1:Ag7aKU08wp0R9QCfF4GoGST9HbmAIeLP7xwMrOBEp1c=
-github.com/lithammer/fuzzysearch v1.1.5/go.mod h1:1R1LRNk7yKid1BaQkmuLQaHruxcC4HmAH30Dh61Ih1Q=
+github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
+github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
-github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
-github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
-github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
-github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
-github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
-github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
-github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
-github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
+github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
+github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
+github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
+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/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
-github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
+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=
+github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
-github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
-github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
+github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
+github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
-github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU=
-github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA=
-github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
-github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
-github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
-github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
+github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
+github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/oauth2-proxy/mockoidc v0.0.0-20220308204021-b9169deeb282 h1:TQMyrpijtkFyXpNI3rY5hsZQZw+paiH+BfAlsb81HBY=
github.com/oauth2-proxy/mockoidc v0.0.0-20220308204021-b9169deeb282/go.mod h1:rW25Kyd08Wdn3UVn0YBsDTSvReu0jqpmJKzxITPSjks=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
-github.com/opencontainers/image-spec v1.1.0-rc3 h1:fzg1mXZFj8YdPeNkRXMg+zb88BFV0Ys52cJydRwBkb8=
-github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
-github.com/opencontainers/runc v1.1.4 h1:nRCz/8sKg6K6jgYAFLDlXzPeITBZJyX28DBVhWD+5dg=
-github.com/opencontainers/runc v1.1.4/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg=
-github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
-github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI=
+github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI=
+github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
+github.com/opencontainers/runc v1.1.10 h1:EaL5WeO9lv9wmS6SASjszOeQdSctvpbu0DdBQBizE40=
+github.com/opencontainers/runc v1.1.10/go.mod h1:+/R6+KmDlh+hOO8NkjmgkG9Qzvypzk0yXxAPYYR65+M=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
-github.com/ory/dockertest/v3 v3.9.1 h1:v4dkG+dlu76goxMiTT2j8zV7s4oPPEppKT8K8p2f1kY=
-github.com/ory/dockertest/v3 v3.9.1/go.mod h1:42Ir9hmvaAPm0Mgibk6mBPi7SFvTXxEcnztDYOJ//uM=
+github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4=
+github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
-github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
-github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
+github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
+github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
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.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
+github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
-github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
+github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI=
-github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
+github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
-github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
-github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
-github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
-github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
-github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
+github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
+github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
+github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
+github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
+github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
+github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI=
github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg=
github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE=
@@ -427,49 +364,48 @@ 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.58 h1:MEImvkbvty8JvoJH64bJ+CvoCkcuRw2iBIJvRwAEgHI=
-github.com/pterm/pterm v0.12.58/go.mod h1:Ro9CV954hiaxt3mcpDx4a8XF5EmRDlIIpPdlfCKF9fE=
-github.com/puzpuzpuz/xsync/v2 v2.4.0 h1:5sXAMHrtx1bg9nbRZTOn8T4MkWe5V+o8yKRH02Eznag=
-github.com/puzpuzpuz/xsync/v2 v2.4.0/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU=
-github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/pterm/pterm v0.12.71 h1:KcEJ98EiVCbzDkFbktJ2gMlr4pn8IzyGb9bwK6ffkuA=
+github.com/pterm/pterm v0.12.71/go.mod h1:SUAcoZjRt+yjPWlWba+/Fd8zJJ2lSXBQWf0Z0HbFiIQ=
+github.com/puzpuzpuz/xsync/v3 v3.0.2 h1:3yESHrRFYr6xzkz61LLkvNiPFXxJEAABanTQpKbAaew=
+github.com/puzpuzpuz/xsync/v3 v3.0.2/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
-github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
-github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
-github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
-github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
-github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
-github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
-github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
-github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
+github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
+github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
+github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
+github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0=
+github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs=
+github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
+github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
+github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
+github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
-github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
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/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
-github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
-github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
-github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
-github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
-github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
-github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
-github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
-github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
-github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
-github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
-github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
+github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
+github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
+github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
+github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
+github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
+github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc=
-github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
+github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI=
+github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
@@ -478,33 +414,46 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
-github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
-github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
+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=
+github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4=
+github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
+github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
+github.com/tailscale/golang-x-crypto v0.0.0-20230713185742-f0b76a10a08e h1:JyeJF/HuSwvxWtsR1c0oKX1lzaSH5Wh4aX+MgiStaGQ=
+github.com/tailscale/golang-x-crypto v0.0.0-20230713185742-f0b76a10a08e/go.mod h1:DjoeCULdP6vTJ/xY+nzzR9LaUHprkbZEpNidX0aqEEk=
+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/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQSPhaUPjUQwozcRdDdSxxqhNgNZ3drZFk=
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
+github.com/tailscale/setec v0.0.0-20230926024544-07dde05889e7 h1:k0DGEB1KO37rE2IKJS1KU0YSlVfA7Zv7keP+vOpTAsk=
+github.com/tailscale/setec v0.0.0-20230926024544-07dde05889e7/go.mod h1:m+fXeYoPtxKq/XHRTjW7BrVbRlbHPh4TOdIFY4x6frY=
+github.com/tailscale/tailsql v0.0.0-20231216172832-51483e0c711b h1:FzqUT8XFn3OJTzTMteYMZlg3EUQMxoq7oJiaVj4SEBA=
+github.com/tailscale/tailsql v0.0.0-20231216172832-51483e0c711b/go.mod h1:Nkao4BDbQqzxxg78ty4ejq+KgX/0Bxj00DxfxScuJoI=
+github.com/tailscale/web-client-prebuilt v0.0.0-20231213172531-a4fa669015b2 h1:lR1voET3dwe3CxacGAiva4k08TXtQ6Dlmult4JILlj4=
+github.com/tailscale/web-client-prebuilt v0.0.0-20231213172531-a4fa669015b2/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
+github.com/tailscale/wireguard-go v0.0.0-20231121184858-cc193a0b3272 h1:zwsem4CaamMdC3tFoTpzrsUSMDPV0K6rhnQdF7kXekQ=
+github.com/tailscale/wireguard-go v0.0.0-20231121184858-cc193a0b3272/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
+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/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
-github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
-github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
-github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
-github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
-github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
+github.com/tink-crypto/tink-go/v2 v2.0.0 h1:LutFJapahsM0i/6hKfOkzSYTVeshmFs+jloZXqe9z9s=
+github.com/tink-crypto/tink-go/v2 v2.0.0/go.mod h1:QAbyq9LZncomYnScxlfaHImbV4ieNIe6bnu/Xcqqox4=
+github.com/u-root/u-root v0.11.0 h1:6gCZLOeRyevw7gbTwMj3fKxnr9+yHFlgF3N7udUVNO8=
+github.com/u-root/u-root v0.11.0/go.mod h1:DBkDtiZyONk9hzVEdB/PWI9B4TxDkElWlVTHseglrZY=
+github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 h1:YcojQL98T/OO+rybuzn2+5KrD5dBwXIvYBvQ2cD3Avg=
+github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs=
github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
-github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
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=
@@ -520,399 +469,172 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
-github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.1.32/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.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
-go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
-go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
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/netipx v0.0.0-20230728180743-ad4cb58a6516 h1:X66ZEoMN2SuaoI/dfZVYobB6E5zjZyyHUMWlCA7MgGE=
-go4.org/netipx v0.0.0-20230728180743-ad4cb58a6516/go.mod h1:TQvodOM+hJTioNQJilmLXu08JNb8i+ccq418+KWu1/Y=
+go4.org/netipx v0.0.0-20230824141953-6213f710f925 h1:eeQDDVKFkx0g4Hyy8pHgmZaK0EqB4SD6rvKbUdN3ziQ=
+go4.org/netipx v0.0.0-20230824141953-6213f710f925/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
-golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
-golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
+golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
+golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
-golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
-golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
-golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
-golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
-golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY=
-golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
-golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
-golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No=
+golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
+golang.org/x/exp/typeparams v0.0.0-20230905200255-921286631fa9 h1:j3D9DvWRpUfIyFfDPws7LoIZ2MAI1OJHdQXtTnYtN+k=
+golang.org/x/exp/typeparams v0.0.0-20230905200255-921286631fa9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
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-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
-golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
-golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
-golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
-golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
-golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
-golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
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.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.1/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.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
-golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
+golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
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-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
-golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
-golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
+golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
+golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk=
-golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
-golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
+golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ=
+golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+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.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
-golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
+golang.org/x/sync v0.5.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=
-golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191113165036-4c7a9d0fe056/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210301091718-77cc2087c03b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/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-20220422013727-9388b58f7150/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-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.3.0/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.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
-golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
+golang.org/x/sys v0.15.0/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-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
-golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
-golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
-golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
+golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+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.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
-golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
-golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+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/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=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
-golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
-golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
-golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
-golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo=
-golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM=
+golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
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=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
+golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
-google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
-google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
-google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
-google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
-google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
-google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
-google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
-google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
-google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
-google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
-google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
+google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
-google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
-google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
-google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
-google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
-google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
-google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
+google.golang.org/genproto v0.0.0-20231127180814-3a041ad873d4 h1:W12Pwm4urIbRdGhMEg2NM9O3TWKjNcxQhs46V0ypf/k=
+google.golang.org/genproto v0.0.0-20231127180814-3a041ad873d4/go.mod h1:5RBcpGRxr25RbDzY5w+dmaqpSEvl8Gwl1x2CICf60ic=
+google.golang.org/genproto/googleapis/api v0.0.0-20231127180814-3a041ad873d4 h1:ZcOkrmX74HbKFYnpPY8Qsw93fC29TbJXspYKaBkSXDQ=
+google.golang.org/genproto/googleapis/api v0.0.0-20231127180814-3a041ad873d4/go.mod h1:k2dtGpRrbsSyKcNPKKI5sstZkrNCZwpU/ns96JoHbGg=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 h1:DC7wcm+i+P1rN3Ff07vL+OndGg5OhNddHyTA+ocPqYE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4/go.mod h1:eJVxU6o+4G1PSczBr85xmyvSNYAKvAYgkub40YGomFM=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
-google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
-google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
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.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
-google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
-google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
-google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag=
-google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
-google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
-google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
-google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
-google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
-google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
-google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
-google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
+google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
-google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
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=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
-gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
@@ -926,36 +648,47 @@ 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.4.8 h1:NDWizaclb7Q2aupT0jkwK8jx1HVCNzt+PQ8v/VnxviA=
-gorm.io/driver/postgres v1.4.8/go.mod h1:O9MruWGNLUBUWVYfWuBClpf3HeGjOoybY0SNmCs3wsw=
-gorm.io/gorm v1.24.2/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
-gorm.io/gorm v1.24.6 h1:wy98aq9oFEetsc4CAbKD2SoBCdMzsbSIvSUUFJuHi5s=
-gorm.io/gorm v1.24.6/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
+gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
+gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
+gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
+gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=
+gvisor.dev/gvisor v0.0.0-20230928000133-4fe30062272c h1:bYb98Ra11fJ8F2xFbZx0zg2VQ28lYqC1JxfaaF53xqY=
+gvisor.dev/gvisor v0.0.0-20230928000133-4fe30062272c/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
-honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+honnef.co/go/tools v0.4.6 h1:oFEHCKeID7to/3autwsWfnuv69j3NsfcXbvJKuIcep8=
+honnef.co/go/tools v0.4.6/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0=
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
-modernc.org/libc v1.22.2 h1:4U7v51GyhlWqQmwCHj28Rdq2Yzwk55ovjFrdPjs8Hb0=
-modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug=
-modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
-modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
-modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
-modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
-modernc.org/sqlite v1.20.3 h1:SqGJMMxjj1PHusLxdYxeQSodg7Jxn9WWkaAQjKrntZs=
-modernc.org/sqlite v1.20.3/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A=
-nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
-nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
-rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
-rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
-rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
-software.sslmate.com/src/go-pkcs12 v0.2.0 h1:nlFkj7bTysH6VkC4fGphtjXRbezREPgrHuJG20hBGPE=
-software.sslmate.com/src/go-pkcs12 v0.2.0/go.mod h1:23rNcYsMabIc1otwLpTkCCPwUq6kQsTyowttG/as0kQ=
-tailscale.com v1.50.0 h1:98infw8rznkdntRgcnlrAC5JuZfDH0bqKLyg8ZKfwMk=
-tailscale.com v1.50.0/go.mod h1:lBw7+Mw2d7rea3kefGjYWN8IJkB5dyaakMNMOinNGDo=
+inet.af/peercred v0.0.0-20210906144145-0893ea02156a h1:qdkS8Q5/i10xU2ArJMKYhVa1DORzBfYS/qA2UK2jheg=
+inet.af/peercred v0.0.0-20210906144145-0893ea02156a/go.mod h1:FjawnflS/udxX+SvpsMgZfdqx2aykOlkISeAsADi5IU=
+inet.af/wf v0.0.0-20221017222439-36129f591884 h1:zg9snq3Cpy50lWuVqDYM7AIRVTtU50y5WXETMFohW/Q=
+inet.af/wf v0.0.0-20221017222439-36129f591884/go.mod h1:bSAQ38BYbY68uwpasXOTZo22dKGy9SNvI6PZFeKomZE=
+lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
+lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
+modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q=
+modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y=
+modernc.org/ccgo/v3 v3.16.15 h1:KbDR3ZAVU+wiLyMESPtbtE/Add4elztFyfsWoNTgxS0=
+modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI=
+modernc.org/libc v1.34.11 h1:hQDcIUlSG4QAOkXCIQKkaAOV5ptXvkOx4ddbXzgW2JU=
+modernc.org/libc v1.34.11/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
+modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
+modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
+modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
+modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
+modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
+modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
+modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
+modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
+modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
+modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
+modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=
+nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
+software.sslmate.com/src/go-pkcs12 v0.2.1 h1:tbT1jjaeFOF230tzOIRJ6U5S1jNqpsSyNjzDd58H3J8=
+software.sslmate.com/src/go-pkcs12 v0.2.1/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
+tailscale.com v1.56.1 h1:V3HBDJai3u7xo22Xlv7ioqKNZQdxOJebLYCNqCXVwZg=
+tailscale.com v1.56.1/go.mod h1:XQk6fCN8oMJ+qbCmW+2WS/VM3jTA9nIHT6O19t0hZeQ=
diff --git a/hscontrol/app.go b/hscontrol/app.go
index 59284cb..5327d6f 100644
--- a/hscontrol/app.go
+++ b/hscontrol/app.go
@@ -48,6 +48,7 @@ import (
"google.golang.org/grpc/peer"
"google.golang.org/grpc/reflection"
"google.golang.org/grpc/status"
+ "tailscale.com/envknob"
"tailscale.com/tailcfg"
"tailscale.com/types/dnstype"
"tailscale.com/types/key"
@@ -59,6 +60,9 @@ var (
errUnsupportedLetsEncryptChallengeType = errors.New(
"unknown value for Lets Encrypt challenge type",
)
+ errEmptyInitialDERPMap = errors.New(
+ "initial DERPMap is empty, Headscale requries at least one entry",
+ )
)
const (
@@ -77,7 +81,6 @@ type Headscale struct {
dbString string
dbType string
dbDebug bool
- privateKey2019 *key.MachinePrivate
noisePrivateKey *key.MachinePrivate
DERPMap *tailcfg.DERPMap
@@ -96,26 +99,23 @@ type Headscale struct {
pollNetMapStreamWG sync.WaitGroup
}
+var (
+ profilingEnabled = envknob.Bool("HEADSCALE_PROFILING_ENABLED")
+ tailsqlEnabled = envknob.Bool("HEADSCALE_DEBUG_TAILSQL_ENABLED")
+ tailsqlStateDir = envknob.String("HEADSCALE_DEBUG_TAILSQL_STATE_DIR")
+ tailsqlTSKey = envknob.String("TS_AUTHKEY")
+)
+
func NewHeadscale(cfg *types.Config) (*Headscale, error) {
- if _, enableProfile := os.LookupEnv("HEADSCALE_PROFILING_ENABLED"); enableProfile {
+ if profilingEnabled {
runtime.SetBlockProfileRate(1)
}
- privateKey, err := readOrCreatePrivateKey(cfg.PrivateKeyPath)
- if err != nil {
- return nil, fmt.Errorf("failed to read or create private key: %w", err)
- }
-
- // TS2021 requires to have a different key from the legacy protocol.
noisePrivateKey, err := readOrCreatePrivateKey(cfg.NoisePrivateKeyPath)
if err != nil {
return nil, fmt.Errorf("failed to read or create Noise protocol private key: %w", err)
}
- if privateKey.Equal(*noisePrivateKey) {
- return nil, fmt.Errorf("private key and noise private key are the same: %w", err)
- }
-
var dbString string
switch cfg.DBtype {
case db.Postgres:
@@ -156,7 +156,6 @@ func NewHeadscale(cfg *types.Config) (*Headscale, error) {
cfg: cfg,
dbType: cfg.DBtype,
dbString: dbString,
- privateKey2019: privateKey,
noisePrivateKey: noisePrivateKey,
registrationCache: registrationCache,
pollNetMapStreamWG: sync.WaitGroup{},
@@ -199,10 +198,21 @@ func NewHeadscale(cfg *types.Config) (*Headscale, error) {
}
if cfg.DERP.ServerEnabled {
- // TODO(kradalby): replace this key with a dedicated DERP key.
+ derpServerKey, err := readOrCreatePrivateKey(cfg.DERP.ServerPrivateKeyPath)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read or create DERP server private key: %w", err)
+ }
+
+ if derpServerKey.Equal(*noisePrivateKey) {
+ return nil, fmt.Errorf(
+ "DERP server private key and noise private key are the same: %w",
+ err,
+ )
+ }
+
embeddedDERPServer, err := derpServer.NewDERPServer(
cfg.ServerURL,
- key.NodePrivate(*privateKey),
+ key.NodePrivate(*derpServerKey),
&cfg.DERP,
)
if err != nil {
@@ -263,20 +273,13 @@ func (h *Headscale) scheduledDERPMapUpdateWorker(cancelChan <-chan struct{}) {
h.DERPMap.Regions[region.RegionID] = ®ion
}
- h.nodeNotifier.NotifyAll(types.StateUpdate{
+ stateUpdate := types.StateUpdate{
Type: types.StateDERPUpdated,
- DERPMap: *h.DERPMap,
- })
- }
- }
-}
-
-func (h *Headscale) failoverSubnetRoutes(milliSeconds int64) {
- ticker := time.NewTicker(time.Duration(milliSeconds) * time.Millisecond)
- for range ticker.C {
- err := h.db.HandlePrimarySubnetFailover()
- if err != nil {
- log.Error().Err(err).Msg("failed to handle primary subnet failover")
+ DERPMap: h.DERPMap,
+ }
+ if stateUpdate.Valid() {
+ h.nodeNotifier.NotifyAll(stateUpdate)
+ }
}
}
}
@@ -449,10 +452,9 @@ 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/{nkey}", h.RegisterWebAPI).Methods(http.MethodGet)
- h.addLegacyHandlers(router)
+ router.HandleFunc("/register/{mkey}", h.RegisterWebAPI).Methods(http.MethodGet)
- router.HandleFunc("/oidc/register/{nkey}", h.RegisterOIDC).Methods(http.MethodGet)
+ router.HandleFunc("/oidc/register/{mkey}", h.RegisterOIDC).Methods(http.MethodGet)
router.HandleFunc("/oidc/callback", h.OIDCCallback).Methods(http.MethodGet)
router.HandleFunc("/apple", h.AppleConfigMessage).Methods(http.MethodGet)
router.HandleFunc("/apple/{platform}", h.ApplePlatformConfig).
@@ -510,13 +512,15 @@ func (h *Headscale) Serve() error {
go h.scheduledDERPMapUpdateWorker(derpMapCancelChannel)
}
+ if len(h.DERPMap.Regions) == 0 {
+ return errEmptyInitialDERPMap
+ }
+
// TODO(kradalby): These should have cancel channels and be cleaned
// up on shutdown.
go h.expireEphemeralNodes(updateInterval)
go h.expireExpiredMachines(updateInterval)
- go h.failoverSubnetRoutes(updateInterval)
-
if zl.GlobalLevel() == zl.TraceLevel {
zerolog.RespLog = true
} else {
@@ -572,7 +576,10 @@ func (h *Headscale) Serve() error {
}
// Start the local gRPC server without TLS and without authentication
- grpcSocket := grpc.NewServer(zerolog.UnaryInterceptor())
+ grpcSocket := grpc.NewServer(
+ // Uncomment to debug grpc communication.
+ // zerolog.UnaryInterceptor(),
+ )
v1.RegisterHeadscaleServiceServer(grpcSocket, newHeadscaleV1APIServer(h))
reflection.Register(grpcSocket)
@@ -612,7 +619,8 @@ func (h *Headscale) Serve() error {
grpc.UnaryInterceptor(
grpcMiddleware.ChainUnaryServer(
h.grpcAuthenticationInterceptor,
- zerolog.NewUnaryServerInterceptor(),
+ // Uncomment to debug grpc communication.
+ // zerolog.NewUnaryServerInterceptor(),
),
),
}
@@ -698,6 +706,18 @@ func (h *Headscale) Serve() error {
log.Info().
Msgf("listening and serving metrics on: %s", h.cfg.MetricsAddr)
+ var tailsqlContext context.Context
+ if tailsqlEnabled {
+ if h.cfg.DBtype != db.Sqlite {
+ log.Fatal().Str("type", h.cfg.DBtype).Msgf("tailsql only support %q", db.Sqlite)
+ }
+ if tailsqlTSKey == "" {
+ log.Fatal().Msg("tailsql requires TS_AUTHKEY to be set")
+ }
+ tailsqlContext = context.Background()
+ go runTailSQLService(ctx, util.TSLogfWrapper(), tailsqlStateDir, h.cfg.DBpath)
+ }
+
// Handle common process-killing signals so we can gracefully shut down:
h.shutdownChan = make(chan struct{})
sigc := make(chan os.Signal, 1)
@@ -763,6 +783,10 @@ func (h *Headscale) Serve() error {
grpcListener.Close()
}
+ if tailsqlContext != nil {
+ tailsqlContext.Done()
+ }
+
// Close network listeners
promHTTPListener.Close()
httpListener.Close()
@@ -900,7 +924,8 @@ func readOrCreatePrivateKey(path string) (*key.MachinePrivate, error) {
err = os.WriteFile(path, machineKeyStr, privateKeyFileMode)
if err != nil {
return nil, fmt.Errorf(
- "failed to save private key to disk: %w",
+ "failed to save private key to disk at path %q: %w",
+ path,
err,
)
}
@@ -911,16 +936,9 @@ func readOrCreatePrivateKey(path string) (*key.MachinePrivate, error) {
}
trimmedPrivateKey := strings.TrimSpace(string(privateKey))
- privateKeyEnsurePrefix := util.PrivateKeyEnsurePrefix(trimmedPrivateKey)
var machineKey key.MachinePrivate
- if err = machineKey.UnmarshalText([]byte(privateKeyEnsurePrefix)); err != nil {
- log.Info().
- Str("path", path).
- Msg("This might be due to a legacy (headscale pre-0.12) private key. " +
- "If the key is in WireGuard format, delete the key and restart headscale. " +
- "A new key will automatically be generated. All Tailscale clients will have to be restarted")
-
+ if err = machineKey.UnmarshalText([]byte(trimmedPrivateKey)); err != nil {
return nil, fmt.Errorf("failed to parse private key: %w", err)
}
diff --git a/hscontrol/auth.go b/hscontrol/auth.go
index b756365..4fe5a16 100644
--- a/hscontrol/auth.go
+++ b/hscontrol/auth.go
@@ -1,13 +1,13 @@
package hscontrol
import (
+ "encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"time"
- "github.com/juanfont/headscale/hscontrol/mapper"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/rs/zerolog/log"
@@ -16,22 +16,62 @@ import (
"tailscale.com/types/key"
)
-// handleRegister is the common logic for registering a client in the legacy and Noise protocols
-//
-// When using Noise, the machineKey is Zero.
+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,
registerRequest tailcfg.RegisterRequest,
machineKey key.MachinePublic,
- isNoise bool,
) {
+ logInfo, logTrace, logErr := logAuthFunc(registerRequest, machineKey)
now := time.Now().UTC()
+ logTrace("handleRegister called, looking up machine in DB")
node, err := h.db.GetNodeByAnyKey(machineKey, registerRequest.NodeKey, registerRequest.OldNodeKey)
+ logTrace("handleRegister database lookup has returned")
if errors.Is(err, gorm.ErrRecordNotFound) {
// If the node has AuthKey set, handle registration via PreAuthKeys
if registerRequest.Auth.AuthKey != "" {
- h.handleAuthKey(writer, registerRequest, machineKey, isNoise)
+ h.handleAuthKey(writer, registerRequest, machineKey)
return
}
@@ -45,49 +85,29 @@ func (h *Headscale) handleRegister(
// is that the client will hammer headscale with requests until it gets a
// successful RegisterResponse.
if registerRequest.Followup != "" {
- if _, ok := h.registrationCache.Get(util.NodePublicKeyStripPrefix(registerRequest.NodeKey)); ok {
- log.Debug().
- Caller().
- Str("node", registerRequest.Hostinfo.Hostname).
- Str("machine_key", machineKey.ShortString()).
- Str("node_key", registerRequest.NodeKey.ShortString()).
- Str("node_key_old", registerRequest.OldNodeKey.ShortString()).
- Str("follow_up", registerRequest.Followup).
- Bool("noise", isNoise).
- Msg("Node is waiting for interactive login")
+ logTrace("register request is a followup")
+ if _, ok := h.registrationCache.Get(machineKey.String()); ok {
+ logTrace("Node is waiting for interactive login")
select {
case <-req.Context().Done():
return
case <-time.After(registrationHoldoff):
- h.handleNewNode(writer, registerRequest, machineKey, isNoise)
+ h.handleNewNode(writer, registerRequest, machineKey)
return
}
}
}
- log.Info().
- Caller().
- Str("node", registerRequest.Hostinfo.Hostname).
- Str("machine_key", machineKey.ShortString()).
- Str("node_key", registerRequest.NodeKey.ShortString()).
- Str("node_key_old", registerRequest.OldNodeKey.ShortString()).
- Str("follow_up", registerRequest.Followup).
- Bool("noise", isNoise).
- Msg("New node not yet in the database")
+ logInfo("Node not found in database, creating new")
givenName, err := h.db.GenerateGivenName(
- machineKey.String(),
+ machineKey,
registerRequest.Hostinfo.Hostname,
)
if err != nil {
- log.Error().
- Caller().
- Str("func", "RegistrationHandler").
- Str("hostinfo.name", registerRequest.Hostinfo.Hostname).
- Err(err).
- Msg("Failed to generate given name for node")
+ logErr(err, "Failed to generate given name for node")
return
}
@@ -97,31 +117,26 @@ func (h *Headscale) handleRegister(
// We create the node and then keep it around until a callback
// happens
newNode := types.Node{
- MachineKey: util.MachinePublicKeyStripPrefix(machineKey),
+ MachineKey: machineKey,
Hostname: registerRequest.Hostinfo.Hostname,
GivenName: givenName,
- NodeKey: util.NodePublicKeyStripPrefix(registerRequest.NodeKey),
+ NodeKey: registerRequest.NodeKey,
LastSeen: &now,
Expiry: &time.Time{},
}
if !registerRequest.Expiry.IsZero() {
- log.Trace().
- Caller().
- Bool("noise", isNoise).
- Str("node", registerRequest.Hostinfo.Hostname).
- Time("expiry", registerRequest.Expiry).
- Msg("Non-zero expiry time requested")
+ logTrace("Non-zero expiry time requested")
newNode.Expiry = ®isterRequest.Expiry
}
h.registrationCache.Set(
- newNode.NodeKey,
+ machineKey.String(),
newNode,
registerCacheExpiration,
)
- h.handleNewNode(writer, registerRequest, machineKey, isNoise)
+ h.handleNewNode(writer, registerRequest, machineKey)
return
}
@@ -134,11 +149,7 @@ func (h *Headscale) handleRegister(
// (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.
- var storedMachineKey key.MachinePublic
- err = storedMachineKey.UnmarshalText(
- []byte(util.MachinePublicKeyEnsurePrefix(node.MachineKey)),
- )
- if err != nil || storedMachineKey.IsZero() {
+ if err != nil || node.MachineKey.IsZero() {
if err := h.db.NodeSetMachineKey(node, machineKey); err != nil {
log.Error().
Caller().
@@ -156,12 +167,12 @@ func (h *Headscale) handleRegister(
// - 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 == util.NodePublicKeyStripPrefix(registerRequest.NodeKey) {
+ if node.NodeKey.String() == registerRequest.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 !registerRequest.Expiry.IsZero() &&
registerRequest.Expiry.UTC().Before(now) {
- h.handleNodeLogOut(writer, *node, machineKey, isNoise)
+ h.handleNodeLogOut(writer, *node, machineKey)
return
}
@@ -169,21 +180,20 @@ func (h *Headscale) handleRegister(
// 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, machineKey, isNoise)
+ h.handleNodeWithValidRegistration(writer, *node, machineKey)
return
}
}
// The NodeKey we have matches OldNodeKey, which means this is a refresh after a key expiration
- if node.NodeKey == util.NodePublicKeyStripPrefix(registerRequest.OldNodeKey) &&
+ if node.NodeKey.String() == registerRequest.OldNodeKey.String() &&
!node.IsExpired() {
h.handleNodeKeyRefresh(
writer,
registerRequest,
*node,
machineKey,
- isNoise,
)
return
@@ -198,7 +208,7 @@ func (h *Headscale) handleRegister(
}
// The node has expired or it is logged out
- h.handleNodeExpiredOrLoggedOut(writer, registerRequest, *node, machineKey, isNoise)
+ h.handleNodeExpiredOrLoggedOut(writer, registerRequest, *node, machineKey)
// TODO(juan): RegisterRequest includes an Expiry time, that we could optionally use
node.Expiry = &time.Time{}
@@ -207,9 +217,9 @@ func (h *Headscale) handleRegister(
// 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 = util.NodePublicKeyStripPrefix(registerRequest.NodeKey)
+ node.NodeKey = registerRequest.NodeKey
h.registrationCache.Set(
- util.NodePublicKeyStripPrefix(registerRequest.NodeKey),
+ machineKey.String(),
*node,
registerCacheExpiration,
)
@@ -219,7 +229,6 @@ func (h *Headscale) handleRegister(
}
// handleAuthKey contains the logic to manage auth key client registration
-// It is used both by the legacy and the new Noise protocol.
// When using Noise, the machineKey is Zero.
//
// TODO: check if any locks are needed around IP allocation.
@@ -227,12 +236,10 @@ func (h *Headscale) handleAuthKey(
writer http.ResponseWriter,
registerRequest tailcfg.RegisterRequest,
machineKey key.MachinePublic,
- isNoise bool,
) {
log.Debug().
Caller().
Str("node", registerRequest.Hostinfo.Hostname).
- Bool("noise", isNoise).
Msgf("Processing auth key for %s", registerRequest.Hostinfo.Hostname)
resp := tailcfg.RegisterResponse{}
@@ -240,17 +247,15 @@ func (h *Headscale) handleAuthKey(
if err != nil {
log.Error().
Caller().
- Bool("noise", isNoise).
Str("node", registerRequest.Hostinfo.Hostname).
Err(err).
Msg("Failed authentication via AuthKey")
resp.MachineAuthorized = false
- respBody, err := mapper.MarshalResponse(resp, isNoise, h.privateKey2019, machineKey)
+ respBody, err := json.Marshal(resp)
if err != nil {
log.Error().
Caller().
- Bool("noise", isNoise).
Str("node", registerRequest.Hostinfo.Hostname).
Err(err).
Msg("Cannot encode message")
@@ -267,14 +272,12 @@ func (h *Headscale) handleAuthKey(
if err != nil {
log.Error().
Caller().
- Bool("noise", isNoise).
Err(err).
Msg("Failed to write response")
}
log.Error().
Caller().
- Bool("noise", isNoise).
Str("node", registerRequest.Hostinfo.Hostname).
Msg("Failed authentication via AuthKey")
@@ -290,11 +293,10 @@ func (h *Headscale) handleAuthKey(
log.Debug().
Caller().
- Bool("noise", isNoise).
Str("node", registerRequest.Hostinfo.Hostname).
Msg("Authentication key was valid, proceeding to acquire IP addresses")
- nodeKey := util.NodePublicKeyStripPrefix(registerRequest.NodeKey)
+ nodeKey := registerRequest.NodeKey
// retrieve node information if it exist
// The error is not important, because if it does not
@@ -304,7 +306,6 @@ func (h *Headscale) handleAuthKey(
if node != nil {
log.Trace().
Caller().
- Bool("noise", isNoise).
Str("node", node.Hostname).
Msg("node was already registered before, refreshing with new auth key")
@@ -314,7 +315,6 @@ func (h *Headscale) handleAuthKey(
if err != nil {
log.Error().
Caller().
- Bool("noise", isNoise).
Str("node", node.Hostname).
Err(err).
Msg("Failed to refresh node")
@@ -322,7 +322,7 @@ func (h *Headscale) handleAuthKey(
return
}
- aclTags := pak.Proto().AclTags
+ 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, aclTags)
@@ -330,7 +330,6 @@ func (h *Headscale) handleAuthKey(
if err != nil {
log.Error().
Caller().
- Bool("noise", isNoise).
Str("node", node.Hostname).
Strs("aclTags", aclTags).
Err(err).
@@ -342,11 +341,10 @@ func (h *Headscale) handleAuthKey(
} else {
now := time.Now().UTC()
- givenName, err := h.db.GenerateGivenName(util.MachinePublicKeyStripPrefix(machineKey), registerRequest.Hostinfo.Hostname)
+ givenName, err := h.db.GenerateGivenName(machineKey, registerRequest.Hostinfo.Hostname)
if err != nil {
log.Error().
Caller().
- Bool("noise", isNoise).
Str("func", "RegistrationHandler").
Str("hostinfo.name", registerRequest.Hostinfo.Hostname).
Err(err).
@@ -359,13 +357,13 @@ func (h *Headscale) handleAuthKey(
Hostname: registerRequest.Hostinfo.Hostname,
GivenName: givenName,
UserID: pak.User.ID,
- MachineKey: util.MachinePublicKeyStripPrefix(machineKey),
+ MachineKey: machineKey,
RegisterMethod: util.RegisterMethodAuthKey,
Expiry: ®isterRequest.Expiry,
NodeKey: nodeKey,
LastSeen: &now,
AuthKeyID: uint(pak.ID),
- ForcedTags: pak.Proto().AclTags,
+ ForcedTags: pak.Proto().GetAclTags(),
}
node, err = h.db.RegisterNode(
@@ -374,7 +372,6 @@ func (h *Headscale) handleAuthKey(
if err != nil {
log.Error().
Caller().
- Bool("noise", isNoise).
Err(err).
Msg("could not register node")
nodeRegistrations.WithLabelValues("new", util.RegisterMethodAuthKey, "error", pak.User.Name).
@@ -389,7 +386,6 @@ func (h *Headscale) handleAuthKey(
if err != nil {
log.Error().
Caller().
- Bool("noise", isNoise).
Err(err).
Msg("Failed to use pre-auth key")
nodeRegistrations.WithLabelValues("new", util.RegisterMethodAuthKey, "error", pak.User.Name).
@@ -405,11 +401,10 @@ func (h *Headscale) handleAuthKey(
// Otherwise it will need to exec `tailscale up` twice to fetch the *LoginName*
resp.Login = *pak.User.TailscaleLogin()
- respBody, err := mapper.MarshalResponse(resp, isNoise, h.privateKey2019, machineKey)
+ respBody, err := json.Marshal(resp)
if err != nil {
log.Error().
Caller().
- Bool("noise", isNoise).
Str("node", registerRequest.Hostinfo.Hostname).
Err(err).
Msg("Cannot encode message")
@@ -427,54 +422,46 @@ func (h *Headscale) handleAuthKey(
if err != nil {
log.Error().
Caller().
- Bool("noise", isNoise).
Err(err).
Msg("Failed to write response")
}
log.Info().
- Bool("noise", isNoise).
Str("node", registerRequest.Hostinfo.Hostname).
Str("ips", strings.Join(node.IPAddresses.StringSlice(), ", ")).
Msg("Successfully authenticated via AuthKey")
}
-// handleNewNode exposes for both legacy and Noise the functionality to get a URL
-// for authorizing the node. This url is then showed to the user by the local Tailscale client.
+// 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,
- isNoise bool,
) {
+ logInfo, logTrace, logErr := logAuthFunc(registerRequest, machineKey)
+
resp := tailcfg.RegisterResponse{}
// The node registration is new, redirect the client to the registration URL
- log.Debug().
- Caller().
- Bool("noise", isNoise).
- Str("node", registerRequest.Hostinfo.Hostname).
- Msg("The node seems to be new, sending auth url")
+ logTrace("The node seems to be new, sending auth url")
if h.oauth2Config != nil {
resp.AuthURL = fmt.Sprintf(
"%s/oidc/register/%s",
strings.TrimSuffix(h.cfg.ServerURL, "/"),
- registerRequest.NodeKey,
+ machineKey.String(),
)
} else {
resp.AuthURL = fmt.Sprintf("%s/register/%s",
strings.TrimSuffix(h.cfg.ServerURL, "/"),
- registerRequest.NodeKey)
+ machineKey.String())
}
- respBody, err := mapper.MarshalResponse(resp, isNoise, h.privateKey2019, machineKey)
+ respBody, err := json.Marshal(resp)
if err != nil {
- log.Error().
- Caller().
- Bool("noise", isNoise).
- Err(err).
- Msg("Cannot encode message")
+ logErr(err, "Cannot encode message")
http.Error(writer, "Internal server error", http.StatusInternalServerError)
return
@@ -484,31 +471,20 @@ func (h *Headscale) handleNewNode(
writer.WriteHeader(http.StatusOK)
_, err = writer.Write(respBody)
if err != nil {
- log.Error().
- Bool("noise", isNoise).
- Caller().
- Err(err).
- Msg("Failed to write response")
+ logErr(err, "Failed to write response")
}
- log.Info().
- Caller().
- Bool("noise", isNoise).
- Str("AuthURL", resp.AuthURL).
- Str("node", registerRequest.Hostinfo.Hostname).
- Msg("Successfully sent auth url")
+ logInfo(fmt.Sprintf("Successfully sent auth url: %s", resp.AuthURL))
}
func (h *Headscale) handleNodeLogOut(
writer http.ResponseWriter,
node types.Node,
machineKey key.MachinePublic,
- isNoise bool,
) {
resp := tailcfg.RegisterResponse{}
log.Info().
- Bool("noise", isNoise).
Str("node", node.Hostname).
Msg("Client requested logout")
@@ -517,7 +493,6 @@ func (h *Headscale) handleNodeLogOut(
if err != nil {
log.Error().
Caller().
- Bool("noise", isNoise).
Err(err).
Msg("Failed to expire node")
http.Error(writer, "Internal server error", http.StatusInternalServerError)
@@ -525,15 +500,27 @@ func (h *Headscale) handleNodeLogOut(
return
}
+ stateUpdate := types.StateUpdate{
+ Type: types.StatePeerChangedPatch,
+ ChangePatches: []*tailcfg.PeerChange{
+ {
+ NodeID: tailcfg.NodeID(node.ID),
+ KeyExpiry: &now,
+ },
+ },
+ }
+ if stateUpdate.Valid() {
+ h.nodeNotifier.NotifyWithIgnore(stateUpdate, node.MachineKey.String())
+ }
+
resp.AuthURL = ""
resp.MachineAuthorized = false
resp.NodeKeyExpired = true
resp.User = *node.User.TailscaleUser()
- respBody, err := mapper.MarshalResponse(resp, isNoise, h.privateKey2019, machineKey)
+ respBody, err := json.Marshal(resp)
if err != nil {
log.Error().
Caller().
- Bool("noise", isNoise).
Err(err).
Msg("Cannot encode message")
http.Error(writer, "Internal server error", http.StatusInternalServerError)
@@ -546,7 +533,6 @@ func (h *Headscale) handleNodeLogOut(
_, err = writer.Write(respBody)
if err != nil {
log.Error().
- Bool("noise", isNoise).
Caller().
Err(err).
Msg("Failed to write response")
@@ -568,7 +554,6 @@ func (h *Headscale) handleNodeLogOut(
log.Info().
Caller().
- Bool("noise", isNoise).
Str("node", node.Hostname).
Msg("Successfully logged out")
}
@@ -577,14 +562,12 @@ func (h *Headscale) handleNodeWithValidRegistration(
writer http.ResponseWriter,
node types.Node,
machineKey key.MachinePublic,
- isNoise bool,
) {
resp := tailcfg.RegisterResponse{}
// The node registration is valid, respond with redirect to /map
log.Debug().
Caller().
- Bool("noise", isNoise).
Str("node", node.Hostname).
Msg("Client is registered and we have the current NodeKey. All clear to /map")
@@ -593,11 +576,10 @@ func (h *Headscale) handleNodeWithValidRegistration(
resp.User = *node.User.TailscaleUser()
resp.Login = *node.User.TailscaleLogin()
- respBody, err := mapper.MarshalResponse(resp, isNoise, h.privateKey2019, machineKey)
+ respBody, err := json.Marshal(resp)
if err != nil {
log.Error().
Caller().
- Bool("noise", isNoise).
Err(err).
Msg("Cannot encode message")
nodeRegistrations.WithLabelValues("update", "web", "error", node.User.Name).
@@ -615,14 +597,12 @@ func (h *Headscale) handleNodeWithValidRegistration(
if err != nil {
log.Error().
Caller().
- Bool("noise", isNoise).
Err(err).
Msg("Failed to write response")
}
log.Info().
Caller().
- Bool("noise", isNoise).
Str("node", node.Hostname).
Msg("Node successfully authorized")
}
@@ -632,13 +612,11 @@ func (h *Headscale) handleNodeKeyRefresh(
registerRequest tailcfg.RegisterRequest,
node types.Node,
machineKey key.MachinePublic,
- isNoise bool,
) {
resp := tailcfg.RegisterResponse{}
log.Info().
Caller().
- Bool("noise", isNoise).
Str("node", node.Hostname).
Msg("We have the OldNodeKey in the database. This is a key refresh")
@@ -655,11 +633,10 @@ func (h *Headscale) handleNodeKeyRefresh(
resp.AuthURL = ""
resp.User = *node.User.TailscaleUser()
- respBody, err := mapper.MarshalResponse(resp, isNoise, h.privateKey2019, machineKey)
+ respBody, err := json.Marshal(resp)
if err != nil {
log.Error().
Caller().
- Bool("noise", isNoise).
Err(err).
Msg("Cannot encode message")
http.Error(writer, "Internal server error", http.StatusInternalServerError)
@@ -673,14 +650,12 @@ func (h *Headscale) handleNodeKeyRefresh(
if err != nil {
log.Error().
Caller().
- Bool("noise", isNoise).
Err(err).
Msg("Failed to write response")
}
log.Info().
Caller().
- Bool("noise", isNoise).
Str("node_key", registerRequest.NodeKey.ShortString()).
Str("old_node_key", registerRequest.OldNodeKey.ShortString()).
Str("node", node.Hostname).
@@ -692,12 +667,11 @@ func (h *Headscale) handleNodeExpiredOrLoggedOut(
registerRequest tailcfg.RegisterRequest,
node types.Node,
machineKey key.MachinePublic,
- isNoise bool,
) {
resp := tailcfg.RegisterResponse{}
if registerRequest.Auth.AuthKey != "" {
- h.handleAuthKey(writer, registerRequest, machineKey, isNoise)
+ h.handleAuthKey(writer, registerRequest, machineKey)
return
}
@@ -705,7 +679,6 @@ func (h *Headscale) handleNodeExpiredOrLoggedOut(
// The client has registered before, but has expired or logged out
log.Trace().
Caller().
- Bool("noise", isNoise).
Str("node", node.Hostname).
Str("machine_key", machineKey.ShortString()).
Str("node_key", registerRequest.NodeKey.ShortString()).
@@ -715,18 +688,17 @@ func (h *Headscale) handleNodeExpiredOrLoggedOut(
if h.oauth2Config != nil {
resp.AuthURL = fmt.Sprintf("%s/oidc/register/%s",
strings.TrimSuffix(h.cfg.ServerURL, "/"),
- registerRequest.NodeKey)
+ machineKey.String())
} else {
resp.AuthURL = fmt.Sprintf("%s/register/%s",
strings.TrimSuffix(h.cfg.ServerURL, "/"),
- registerRequest.NodeKey)
+ machineKey.String())
}
- respBody, err := mapper.MarshalResponse(resp, isNoise, h.privateKey2019, machineKey)
+ respBody, err := json.Marshal(resp)
if err != nil {
log.Error().
Caller().
- Bool("noise", isNoise).
Err(err).
Msg("Cannot encode message")
nodeRegistrations.WithLabelValues("reauth", "web", "error", node.User.Name).
@@ -744,14 +716,12 @@ func (h *Headscale) handleNodeExpiredOrLoggedOut(
if err != nil {
log.Error().
Caller().
- Bool("noise", isNoise).
Err(err).
Msg("Failed to write response")
}
log.Trace().
Caller().
- Bool("noise", isNoise).
Str("machine_key", machineKey.ShortString()).
Str("node_key", registerRequest.NodeKey.ShortString()).
Str("node_key_old", registerRequest.OldNodeKey.ShortString()).
diff --git a/hscontrol/auth_legacy.go b/hscontrol/auth_legacy.go
deleted file mode 100644
index f7e0382..0000000
--- a/hscontrol/auth_legacy.go
+++ /dev/null
@@ -1,61 +0,0 @@
-//go:build ts2019
-
-package hscontrol
-
-import (
- "io"
- "net/http"
-
- "github.com/gorilla/mux"
- "github.com/juanfont/headscale/hscontrol/util"
- "github.com/rs/zerolog/log"
- "tailscale.com/tailcfg"
- "tailscale.com/types/key"
-)
-
-// RegistrationHandler handles the actual registration process of a machine
-// Endpoint /machine/:mkey.
-func (h *Headscale) RegistrationHandler(
- writer http.ResponseWriter,
- req *http.Request,
-) {
- vars := mux.Vars(req)
- machineKeyStr, ok := vars["mkey"]
- if !ok || machineKeyStr == "" {
- log.Error().
- Str("handler", "RegistrationHandler").
- Msg("No machine ID in request")
- http.Error(writer, "No machine ID in request", http.StatusBadRequest)
-
- return
- }
-
- body, _ := io.ReadAll(req.Body)
-
- var machineKey key.MachinePublic
- err := machineKey.UnmarshalText([]byte(util.MachinePublicKeyEnsurePrefix(machineKeyStr)))
- if err != nil {
- log.Error().
- Caller().
- Err(err).
- Msg("Cannot parse machine key")
- nodeRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc()
- http.Error(writer, "Cannot parse machine key", http.StatusBadRequest)
-
- return
- }
- registerRequest := tailcfg.RegisterRequest{}
- err = util.DecodeAndUnmarshalNaCl(body, ®isterRequest, &machineKey, h.privateKey2019)
- if err != nil {
- log.Error().
- Caller().
- Err(err).
- Msg("Cannot decode message")
- nodeRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc()
- http.Error(writer, "Cannot decode message", http.StatusBadRequest)
-
- return
- }
-
- h.handleRegister(writer, req, registerRequest, machineKey, false)
-}
diff --git a/hscontrol/auth_noise.go b/hscontrol/auth_noise.go
index 19a2c65..323a49b 100644
--- a/hscontrol/auth_noise.go
+++ b/hscontrol/auth_noise.go
@@ -39,7 +39,19 @@ func (ns *noiseServer) NoiseRegistrationHandler(
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(), true)
+ ns.headscale.handleRegister(writer, req, registerRequest, ns.conn.Peer())
}
diff --git a/hscontrol/db/addresses_test.go b/hscontrol/db/addresses_test.go
index 781fd89..07059ea 100644
--- a/hscontrol/db/addresses_test.go
+++ b/hscontrol/db/addresses_test.go
@@ -35,9 +35,6 @@ func (s *Suite) TestGetUsedIps(c *check.C) {
node := types.Node{
ID: 0,
- MachineKey: "foo",
- NodeKey: "bar",
- DiscoKey: "faa",
Hostname: "testnode",
UserID: user.ID,
RegisterMethod: util.RegisterMethodAuthKey,
@@ -83,9 +80,6 @@ func (s *Suite) TestGetMultiIp(c *check.C) {
node := types.Node{
ID: uint64(index),
- MachineKey: "foo",
- NodeKey: "bar",
- DiscoKey: "faa",
Hostname: "testnode",
UserID: user.ID,
RegisterMethod: util.RegisterMethodAuthKey,
@@ -173,9 +167,6 @@ func (s *Suite) TestGetAvailableIpNodeWithoutIP(c *check.C) {
node := types.Node{
ID: 0,
- MachineKey: "foo",
- NodeKey: "bar",
- DiscoKey: "faa",
Hostname: "testnode",
UserID: user.ID,
RegisterMethod: util.RegisterMethodAuthKey,
diff --git a/hscontrol/db/db.go b/hscontrol/db/db.go
index 51bb402..030a6f0 100644
--- a/hscontrol/db/db.go
+++ b/hscontrol/db/db.go
@@ -2,13 +2,16 @@ package db
import (
"context"
+ "database/sql"
"errors"
"fmt"
"net/netip"
+ "strings"
"sync"
"time"
"github.com/glebarez/sqlite"
+ "github.com/go-gormigrate/gormigrate/v2"
"github.com/juanfont/headscale/hscontrol/notifier"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util"
@@ -19,15 +22,11 @@ import (
)
const (
- dbVersion = "1"
- Postgres = "postgres"
- Sqlite = "sqlite3"
+ Postgres = "postgres"
+ Sqlite = "sqlite3"
)
-var (
- errValueNotFound = errors.New("not found")
- errDatabaseNotSupported = errors.New("database type not supported")
-)
+var errDatabaseNotSupported = errors.New("database type not supported")
// KV is a key-value store in a psql table. For future use...
// TODO(kradalby): Is this used for anything?
@@ -62,6 +61,261 @@ func NewHeadscaleDatabase(
return nil, err
}
+ migrations := gormigrate.New(dbConn, gormigrate.DefaultOptions, []*gormigrate.Migration{
+ // New migrations should be added as transactions at the end of this list.
+ // The initial commit here is quite messy, completely out of order and
+ // has no versioning and is the tech debt of not having versioned migrations
+ // prior to this point. This first migration is all DB changes to bring a DB
+ // up to 0.23.0.
+ {
+ ID: "202312101416",
+ Migrate: func(tx *gorm.DB) error {
+ if dbType == Postgres {
+ tx.Exec(`create extension if not exists "uuid-ossp";`)
+ }
+
+ _ = tx.Migrator().RenameTable("namespaces", "users")
+
+ // the big rename from Machine to Node
+ _ = tx.Migrator().RenameTable("machines", "nodes")
+ _ = tx.Migrator().RenameColumn(&types.Route{}, "machine_id", "node_id")
+
+ err = tx.AutoMigrate(types.User{})
+ if err != nil {
+ return err
+ }
+
+ _ = tx.Migrator().RenameColumn(&types.Node{}, "namespace_id", "user_id")
+ _ = tx.Migrator().RenameColumn(&types.PreAuthKey{}, "namespace_id", "user_id")
+
+ _ = tx.Migrator().RenameColumn(&types.Node{}, "ip_address", "ip_addresses")
+ _ = tx.Migrator().RenameColumn(&types.Node{}, "name", "hostname")
+
+ // GivenName is used as the primary source of DNS names, make sure
+ // the field is populated and normalized if it was not when the
+ // node was registered.
+ _ = tx.Migrator().RenameColumn(&types.Node{}, "nickname", "given_name")
+
+ // If the Node table has a column for registered,
+ // find all occourences of "false" and drop them. Then
+ // remove the column.
+ if tx.Migrator().HasColumn(&types.Node{}, "registered") {
+ log.Info().
+ Msg(`Database has legacy "registered" column in node, removing...`)
+
+ nodes := types.Nodes{}
+ if err := tx.Not("registered").Find(&nodes).Error; err != nil {
+ log.Error().Err(err).Msg("Error accessing db")
+ }
+
+ for _, node := range nodes {
+ log.Info().
+ Str("node", node.Hostname).
+ Str("machine_key", node.MachineKey.ShortString()).
+ Msg("Deleting unregistered node")
+ if err := tx.Delete(&types.Node{}, node.ID).Error; err != nil {
+ log.Error().
+ Err(err).
+ Str("node", node.Hostname).
+ Str("machine_key", node.MachineKey.ShortString()).
+ Msg("Error deleting unregistered node")
+ }
+ }
+
+ err := tx.Migrator().DropColumn(&types.Node{}, "registered")
+ if err != nil {
+ log.Error().Err(err).Msg("Error dropping registered column")
+ }
+ }
+
+ err = tx.AutoMigrate(&types.Route{})
+ if err != nil {
+ return err
+ }
+
+ err = tx.AutoMigrate(&types.Node{})
+ if err != nil {
+ return err
+ }
+
+ // Ensure all keys have correct prefixes
+ // https://github.com/tailscale/tailscale/blob/main/types/key/node.go#L35
+ type result struct {
+ ID uint64
+ MachineKey string
+ NodeKey string
+ DiscoKey string
+ }
+ var results []result
+ err = tx.Raw("SELECT id, node_key, machine_key, disco_key FROM nodes").Find(&results).Error
+ if err != nil {
+ return err
+ }
+
+ for _, node := range results {
+ mKey := node.MachineKey
+ if !strings.HasPrefix(node.MachineKey, "mkey:") {
+ mKey = "mkey:" + node.MachineKey
+ }
+ nKey := node.NodeKey
+ if !strings.HasPrefix(node.NodeKey, "nodekey:") {
+ nKey = "nodekey:" + node.NodeKey
+ }
+
+ dKey := node.DiscoKey
+ if !strings.HasPrefix(node.DiscoKey, "discokey:") {
+ dKey = "discokey:" + node.DiscoKey
+ }
+
+ err := tx.Exec(
+ "UPDATE nodes SET machine_key = @mKey, node_key = @nKey, disco_key = @dKey WHERE ID = @id",
+ sql.Named("mKey", mKey),
+ sql.Named("nKey", nKey),
+ sql.Named("dKey", dKey),
+ sql.Named("id", node.ID),
+ ).Error
+ if err != nil {
+ return err
+ }
+ }
+
+ if tx.Migrator().HasColumn(&types.Node{}, "enabled_routes") {
+ log.Info().Msgf("Database has legacy enabled_routes column in node, migrating...")
+
+ type NodeAux struct {
+ ID uint64
+ EnabledRoutes types.IPPrefixes
+ }
+
+ nodesAux := []NodeAux{}
+ err := tx.Table("nodes").Select("id, enabled_routes").Scan(&nodesAux).Error
+ if err != nil {
+ log.Fatal().Err(err).Msg("Error accessing db")
+ }
+ for _, node := range nodesAux {
+ for _, prefix := range node.EnabledRoutes {
+ if err != nil {
+ log.Error().
+ Err(err).
+ Str("enabled_route", prefix.String()).
+ Msg("Error parsing enabled_route")
+
+ continue
+ }
+
+ err = tx.Preload("Node").
+ Where("node_id = ? AND prefix = ?", node.ID, types.IPPrefix(prefix)).
+ First(&types.Route{}).
+ Error
+ if err == nil {
+ log.Info().
+ Str("enabled_route", prefix.String()).
+ Msg("Route already migrated to new table, skipping")
+
+ continue
+ }
+
+ route := types.Route{
+ NodeID: node.ID,
+ Advertised: true,
+ Enabled: true,
+ Prefix: types.IPPrefix(prefix),
+ }
+ if err := tx.Create(&route).Error; err != nil {
+ log.Error().Err(err).Msg("Error creating route")
+ } else {
+ log.Info().
+ Uint64("node_id", route.NodeID).
+ Str("prefix", prefix.String()).
+ Msg("Route migrated")
+ }
+ }
+ }
+
+ err = tx.Migrator().DropColumn(&types.Node{}, "enabled_routes")
+ if err != nil {
+ log.Error().Err(err).Msg("Error dropping enabled_routes column")
+ }
+ }
+
+ if tx.Migrator().HasColumn(&types.Node{}, "given_name") {
+ nodes := types.Nodes{}
+ if err := tx.Find(&nodes).Error; err != nil {
+ log.Error().Err(err).Msg("Error accessing db")
+ }
+
+ for item, node := range nodes {
+ if node.GivenName == "" {
+ normalizedHostname, err := util.NormalizeToFQDNRulesConfigFromViper(
+ node.Hostname,
+ )
+ if err != nil {
+ log.Error().
+ Caller().
+ Str("hostname", node.Hostname).
+ Err(err).
+ Msg("Failed to normalize node hostname in DB migration")
+ }
+
+ err = tx.Model(nodes[item]).Updates(types.Node{
+ GivenName: normalizedHostname,
+ }).Error
+ if err != nil {
+ log.Error().
+ Caller().
+ Str("hostname", node.Hostname).
+ Err(err).
+ Msg("Failed to save normalized node name in DB migration")
+ }
+ }
+ }
+ }
+
+ err = tx.AutoMigrate(&KV{})
+ if err != nil {
+ return err
+ }
+
+ err = tx.AutoMigrate(&types.PreAuthKey{})
+ if err != nil {
+ return err
+ }
+
+ err = tx.AutoMigrate(&types.PreAuthKeyACLTag{})
+ if err != nil {
+ return err
+ }
+
+ _ = tx.Migrator().DropTable("shared_machines")
+
+ err = tx.AutoMigrate(&types.APIKey{})
+ if err != nil {
+ return err
+ }
+
+ return nil
+ },
+ Rollback: func(tx *gorm.DB) error {
+ return nil
+ },
+ },
+ {
+ // drop key-value table, it is not used, and has not contained
+ // useful data for a long time or ever.
+ ID: "202312101430",
+ Migrate: func(tx *gorm.DB) error {
+ return tx.Migrator().DropTable("kvs")
+ },
+ Rollback: func(tx *gorm.DB) error {
+ return nil
+ },
+ },
+ })
+
+ if err = migrations.Migrate(); err != nil {
+ log.Fatal().Err(err).Msgf("Migration failed: %v", err)
+ }
+
db := HSDatabase{
db: dbConn,
notifier: notifier,
@@ -70,191 +324,6 @@ func NewHeadscaleDatabase(
baseDomain: baseDomain,
}
- log.Debug().Msgf("database %#v", dbConn)
-
- if dbType == Postgres {
- dbConn.Exec(`create extension if not exists "uuid-ossp";`)
- }
-
- _ = dbConn.Migrator().RenameTable("namespaces", "users")
-
- // the big rename from Machine to Node
- _ = dbConn.Migrator().RenameTable("machines", "nodes")
- _ = dbConn.Migrator().RenameColumn(&types.Route{}, "machine_id", "node_id")
-
- err = dbConn.AutoMigrate(types.User{})
- if err != nil {
- return nil, err
- }
-
- _ = dbConn.Migrator().RenameColumn(&types.Node{}, "namespace_id", "user_id")
- _ = dbConn.Migrator().RenameColumn(&types.PreAuthKey{}, "namespace_id", "user_id")
-
- _ = dbConn.Migrator().RenameColumn(&types.Node{}, "ip_address", "ip_addresses")
- _ = dbConn.Migrator().RenameColumn(&types.Node{}, "name", "hostname")
-
- // GivenName is used as the primary source of DNS names, make sure
- // the field is populated and normalized if it was not when the
- // node was registered.
- _ = dbConn.Migrator().RenameColumn(&types.Node{}, "nickname", "given_name")
-
- // If the MacNodehine table has a column for registered,
- // find all occourences of "false" and drop them. Then
- // remove the column.
- if dbConn.Migrator().HasColumn(&types.Node{}, "registered") {
- log.Info().
- Msg(`Database has legacy "registered" column in node, removing...`)
-
- nodes := types.Nodes{}
- if err := dbConn.Not("registered").Find(&nodes).Error; err != nil {
- log.Error().Err(err).Msg("Error accessing db")
- }
-
- for _, node := range nodes {
- log.Info().
- Str("node", node.Hostname).
- Str("machine_key", node.MachineKey).
- Msg("Deleting unregistered node")
- if err := dbConn.Delete(&types.Node{}, node.ID).Error; err != nil {
- log.Error().
- Err(err).
- Str("node", node.Hostname).
- Str("machine_key", node.MachineKey).
- Msg("Error deleting unregistered node")
- }
- }
-
- err := dbConn.Migrator().DropColumn(&types.Node{}, "registered")
- if err != nil {
- log.Error().Err(err).Msg("Error dropping registered column")
- }
- }
-
- err = dbConn.AutoMigrate(&types.Route{})
- if err != nil {
- return nil, err
- }
-
- if dbConn.Migrator().HasColumn(&types.Node{}, "enabled_routes") {
- log.Info().Msgf("Database has legacy enabled_routes column in node, migrating...")
-
- type NodeAux struct {
- ID uint64
- EnabledRoutes types.IPPrefixes
- }
-
- nodesAux := []NodeAux{}
- err := dbConn.Table("nodes").Select("id, enabled_routes").Scan(&nodesAux).Error
- if err != nil {
- log.Fatal().Err(err).Msg("Error accessing db")
- }
- for _, node := range nodesAux {
- for _, prefix := range node.EnabledRoutes {
- if err != nil {
- log.Error().
- Err(err).
- Str("enabled_route", prefix.String()).
- Msg("Error parsing enabled_route")
-
- continue
- }
-
- err = dbConn.Preload("Node").
- Where("node_id = ? AND prefix = ?", node.ID, types.IPPrefix(prefix)).
- First(&types.Route{}).
- Error
- if err == nil {
- log.Info().
- Str("enabled_route", prefix.String()).
- Msg("Route already migrated to new table, skipping")
-
- continue
- }
-
- route := types.Route{
- NodeID: node.ID,
- Advertised: true,
- Enabled: true,
- Prefix: types.IPPrefix(prefix),
- }
- if err := dbConn.Create(&route).Error; err != nil {
- log.Error().Err(err).Msg("Error creating route")
- } else {
- log.Info().
- Uint64("node_id", route.NodeID).
- Str("prefix", prefix.String()).
- Msg("Route migrated")
- }
- }
- }
-
- err = dbConn.Migrator().DropColumn(&types.Node{}, "enabled_routes")
- if err != nil {
- log.Error().Err(err).Msg("Error dropping enabled_routes column")
- }
- }
-
- err = dbConn.AutoMigrate(&types.Node{})
- if err != nil {
- return nil, err
- }
-
- if dbConn.Migrator().HasColumn(&types.Node{}, "given_name") {
- nodes := types.Nodes{}
- if err := dbConn.Find(&nodes).Error; err != nil {
- log.Error().Err(err).Msg("Error accessing db")
- }
-
- for item, node := range nodes {
- if node.GivenName == "" {
- normalizedHostname, err := util.NormalizeToFQDNRulesConfigFromViper(
- node.Hostname,
- )
- if err != nil {
- log.Error().
- Caller().
- Str("hostname", node.Hostname).
- Err(err).
- Msg("Failed to normalize node hostname in DB migration")
- }
-
- err = db.RenameNode(nodes[item], normalizedHostname)
- if err != nil {
- log.Error().
- Caller().
- Str("hostname", node.Hostname).
- Err(err).
- Msg("Failed to save normalized node name in DB migration")
- }
- }
- }
- }
-
- err = dbConn.AutoMigrate(&KV{})
- if err != nil {
- return nil, err
- }
-
- err = dbConn.AutoMigrate(&types.PreAuthKey{})
- if err != nil {
- return nil, err
- }
-
- err = dbConn.AutoMigrate(&types.PreAuthKeyACLTag{})
- if err != nil {
- return nil, err
- }
-
- _ = dbConn.Migrator().DropTable("shared_machines")
-
- err = dbConn.AutoMigrate(&types.APIKey{})
- if err != nil {
- return nil, err
- }
-
- // TODO(kradalby): is this needed?
- err = db.setValue("db_version", dbVersion)
-
return &db, err
}
@@ -304,39 +373,6 @@ func openDB(dbType, connectionAddr string, debug bool) (*gorm.DB, error) {
)
}
-// getValue returns the value for the given key in KV.
-func (hsdb *HSDatabase) getValue(key string) (string, error) {
- var row KV
- if result := hsdb.db.First(&row, "key = ?", key); errors.Is(
- result.Error,
- gorm.ErrRecordNotFound,
- ) {
- return "", errValueNotFound
- }
-
- return row.Value, nil
-}
-
-// setValue sets value for the given key in KV.
-func (hsdb *HSDatabase) setValue(key string, value string) error {
- keyValue := KV{
- Key: key,
- Value: value,
- }
-
- if _, err := hsdb.getValue(key); err == nil {
- hsdb.db.Model(&keyValue).Where("key = ?", key).Update("value", value)
-
- return nil
- }
-
- if err := hsdb.db.Create(keyValue).Error; err != nil {
- return fmt.Errorf("failed to create key value pair in the database: %w", err)
- }
-
- return nil
-}
-
func (hsdb *HSDatabase) PingDB(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
diff --git a/hscontrol/db/node.go b/hscontrol/db/node.go
index dc4b75d..ce535b9 100644
--- a/hscontrol/db/node.go
+++ b/hscontrol/db/node.go
@@ -55,17 +55,12 @@ func (hsdb *HSDatabase) listPeers(node *types.Node) (types.Nodes, error) {
Preload("User").
Preload("Routes").
Where("node_key <> ?",
- node.NodeKey).Find(&nodes).Error; err != nil {
+ node.NodeKey.String()).Find(&nodes).Error; err != nil {
return types.Nodes{}, err
}
sort.Slice(nodes, func(i, j int) bool { return nodes[i].ID < nodes[j].ID })
- log.Trace().
- Caller().
- Str("node", node.Hostname).
- Msgf("Found peers: %s", nodes.String())
-
return nodes, nil
}
@@ -176,13 +171,19 @@ func (hsdb *HSDatabase) GetNodeByMachineKey(
hsdb.mu.RLock()
defer hsdb.mu.RUnlock()
+ return hsdb.getNodeByMachineKey(machineKey)
+}
+
+func (hsdb *HSDatabase) getNodeByMachineKey(
+ machineKey key.MachinePublic,
+) (*types.Node, error) {
mach := types.Node{}
if result := hsdb.db.
Preload("AuthKey").
Preload("AuthKey.User").
Preload("User").
Preload("Routes").
- First(&mach, "machine_key = ?", util.MachinePublicKeyStripPrefix(machineKey)); result.Error != nil {
+ First(&mach, "machine_key = ?", machineKey.String()); result.Error != nil {
return nil, result.Error
}
@@ -203,7 +204,7 @@ func (hsdb *HSDatabase) GetNodeByNodeKey(
Preload("User").
Preload("Routes").
First(&node, "node_key = ?",
- util.NodePublicKeyStripPrefix(nodeKey)); result.Error != nil {
+ nodeKey.String()); result.Error != nil {
return nil, result.Error
}
@@ -224,9 +225,9 @@ func (hsdb *HSDatabase) GetNodeByAnyKey(
Preload("User").
Preload("Routes").
First(&node, "machine_key = ? OR node_key = ? OR node_key = ?",
- util.MachinePublicKeyStripPrefix(machineKey),
- util.NodePublicKeyStripPrefix(nodeKey),
- util.NodePublicKeyStripPrefix(oldNodeKey)); result.Error != nil {
+ machineKey.String(),
+ nodeKey.String(),
+ oldNodeKey.String()); result.Error != nil {
return nil, result.Error
}
@@ -252,6 +253,10 @@ func (hsdb *HSDatabase) SetTags(
hsdb.mu.Lock()
defer hsdb.mu.Unlock()
+ if len(tags) == 0 {
+ return nil
+ }
+
newTags := []string{}
for _, tag := range tags {
if !util.StringOrPrefixListContains(newTags, tag) {
@@ -265,10 +270,14 @@ func (hsdb *HSDatabase) SetTags(
return fmt.Errorf("failed to update tags for node in the database: %w", err)
}
- hsdb.notifier.NotifyWithIgnore(types.StateUpdate{
- Type: types.StatePeerChanged,
- Changed: types.Nodes{node},
- }, node.MachineKey)
+ stateUpdate := types.StateUpdate{
+ Type: types.StatePeerChanged,
+ ChangeNodes: types.Nodes{node},
+ Message: "called from db.SetTags",
+ }
+ if stateUpdate.Valid() {
+ hsdb.notifier.NotifyWithIgnore(stateUpdate, node.MachineKey.String())
+ }
return nil
}
@@ -301,10 +310,14 @@ func (hsdb *HSDatabase) RenameNode(node *types.Node, newName string) error {
return fmt.Errorf("failed to rename node in the database: %w", err)
}
- hsdb.notifier.NotifyWithIgnore(types.StateUpdate{
- Type: types.StatePeerChanged,
- Changed: types.Nodes{node},
- }, node.MachineKey)
+ stateUpdate := types.StateUpdate{
+ Type: types.StatePeerChanged,
+ ChangeNodes: types.Nodes{node},
+ Message: "called from db.RenameNode",
+ }
+ if stateUpdate.Valid() {
+ hsdb.notifier.NotifyWithIgnore(stateUpdate, node.MachineKey.String())
+ }
return nil
}
@@ -327,10 +340,28 @@ func (hsdb *HSDatabase) nodeSetExpiry(node *types.Node, expiry time.Time) error
)
}
- hsdb.notifier.NotifyWithIgnore(types.StateUpdate{
- Type: types.StatePeerChanged,
- Changed: types.Nodes{node},
- }, node.MachineKey)
+ node.Expiry = &expiry
+
+ stateSelfUpdate := types.StateUpdate{
+ Type: types.StateSelfUpdate,
+ ChangeNodes: types.Nodes{node},
+ }
+ if stateSelfUpdate.Valid() {
+ hsdb.notifier.NotifyByMachineKey(stateSelfUpdate, node.MachineKey)
+ }
+
+ stateUpdate := types.StateUpdate{
+ Type: types.StatePeerChangedPatch,
+ ChangePatches: []*tailcfg.PeerChange{
+ {
+ NodeID: tailcfg.NodeID(node.ID),
+ KeyExpiry: &expiry,
+ },
+ },
+ }
+ if stateUpdate.Valid() {
+ hsdb.notifier.NotifyWithIgnore(stateUpdate, node.MachineKey.String())
+ }
return nil
}
@@ -354,10 +385,13 @@ func (hsdb *HSDatabase) deleteNode(node *types.Node) error {
return err
}
- hsdb.notifier.NotifyAll(types.StateUpdate{
+ stateUpdate := types.StateUpdate{
Type: types.StatePeerRemoved,
Removed: []tailcfg.NodeID{tailcfg.NodeID(node.ID)},
- })
+ }
+ if stateUpdate.Valid() {
+ hsdb.notifier.NotifyAll(stateUpdate)
+ }
return nil
}
@@ -376,7 +410,7 @@ func (hsdb *HSDatabase) UpdateLastSeen(node *types.Node) error {
func (hsdb *HSDatabase) RegisterNodeFromAuthCallback(
cache *cache.Cache,
- nodeKeyStr string,
+ mkey key.MachinePublic,
userName string,
nodeExpiry *time.Time,
registrationMethod string,
@@ -384,20 +418,14 @@ func (hsdb *HSDatabase) RegisterNodeFromAuthCallback(
hsdb.mu.Lock()
defer hsdb.mu.Unlock()
- nodeKey := key.NodePublic{}
- err := nodeKey.UnmarshalText([]byte(nodeKeyStr))
- if err != nil {
- return nil, err
- }
-
log.Debug().
- Str("nodeKey", nodeKey.ShortString()).
+ Str("machine_key", mkey.ShortString()).
Str("userName", userName).
Str("registrationMethod", registrationMethod).
Str("expiresAt", fmt.Sprintf("%v", nodeExpiry)).
Msg("Registering node from API/CLI or auth callback")
- if nodeInterface, ok := cache.Get(util.NodePublicKeyStripPrefix(nodeKey)); ok {
+ if nodeInterface, ok := cache.Get(mkey.String()); ok {
if registrationNode, ok := nodeInterface.(types.Node); ok {
user, err := hsdb.getUser(userName)
if err != nil {
@@ -425,7 +453,7 @@ func (hsdb *HSDatabase) RegisterNodeFromAuthCallback(
)
if err == nil {
- cache.Delete(nodeKeyStr)
+ cache.Delete(mkey.String())
}
return node, err
@@ -448,8 +476,8 @@ func (hsdb *HSDatabase) RegisterNode(node types.Node) (*types.Node, error) {
func (hsdb *HSDatabase) registerNode(node types.Node) (*types.Node, error) {
log.Debug().
Str("node", node.Hostname).
- Str("machine_key", node.MachineKey).
- Str("node_key", node.NodeKey).
+ Str("machine_key", node.MachineKey.ShortString()).
+ Str("node_key", node.NodeKey.ShortString()).
Str("user", node.User.Name).
Msg("Registering node")
@@ -464,8 +492,8 @@ func (hsdb *HSDatabase) registerNode(node types.Node) (*types.Node, error) {
log.Trace().
Caller().
Str("node", node.Hostname).
- Str("machine_key", node.MachineKey).
- Str("node_key", node.NodeKey).
+ Str("machine_key", node.MachineKey.ShortString()).
+ Str("node_key", node.NodeKey.ShortString()).
Str("user", node.User.Name).
Msg("Node authorized again")
@@ -507,7 +535,7 @@ func (hsdb *HSDatabase) NodeSetNodeKey(node *types.Node, nodeKey key.NodePublic)
defer hsdb.mu.Unlock()
if err := hsdb.db.Model(node).Updates(types.Node{
- NodeKey: util.NodePublicKeyStripPrefix(nodeKey),
+ NodeKey: nodeKey,
}).Error; err != nil {
return err
}
@@ -524,7 +552,7 @@ func (hsdb *HSDatabase) NodeSetMachineKey(
defer hsdb.mu.Unlock()
if err := hsdb.db.Model(node).Updates(types.Node{
- MachineKey: util.MachinePublicKeyStripPrefix(machineKey),
+ MachineKey: machineKey,
}).Error; err != nil {
return err
}
@@ -635,20 +663,6 @@ func (hsdb *HSDatabase) IsRoutesEnabled(node *types.Node, routeStr string) bool
return false
}
-func (hsdb *HSDatabase) ListOnlineNodes(
- node *types.Node,
-) (map[tailcfg.NodeID]bool, error) {
- hsdb.mu.RLock()
- defer hsdb.mu.RUnlock()
-
- peers, err := hsdb.listPeers(node)
- if err != nil {
- return nil, err
- }
-
- return peers.OnlineNodeMap(), nil
-}
-
// enableRoutes enables new routes based on a list of new routes.
func (hsdb *HSDatabase) enableRoutes(node *types.Node, routeStrs ...string) error {
newRoutes := make([]netip.Prefix, len(routeStrs))
@@ -700,10 +714,30 @@ func (hsdb *HSDatabase) enableRoutes(node *types.Node, routeStrs ...string) erro
}
}
- hsdb.notifier.NotifyWithIgnore(types.StateUpdate{
- Type: types.StatePeerChanged,
- Changed: types.Nodes{node},
- }, node.MachineKey)
+ // Ensure the node has the latest routes when notifying the other
+ // nodes
+ nRoutes, err := hsdb.getNodeRoutes(node)
+ if err != nil {
+ return fmt.Errorf("failed to read back routes: %w", err)
+ }
+
+ node.Routes = nRoutes
+
+ log.Trace().
+ Caller().
+ Str("node", node.Hostname).
+ Strs("routes", routeStrs).
+ Msg("enabling routes")
+
+ stateUpdate := types.StateUpdate{
+ Type: types.StatePeerChanged,
+ ChangeNodes: types.Nodes{node},
+ Message: "called from db.enableRoutes",
+ }
+ if stateUpdate.Valid() {
+ hsdb.notifier.NotifyWithIgnore(
+ stateUpdate, node.MachineKey.String())
+ }
return nil
}
@@ -734,7 +768,10 @@ func generateGivenName(suppliedName string, randomSuffix bool) (string, error) {
return normalizedHostname, nil
}
-func (hsdb *HSDatabase) GenerateGivenName(machineKey string, suppliedName string) (string, error) {
+func (hsdb *HSDatabase) GenerateGivenName(
+ mkey key.MachinePublic,
+ suppliedName string,
+) (string, error) {
hsdb.mu.RLock()
defer hsdb.mu.RUnlock()
@@ -749,17 +786,22 @@ func (hsdb *HSDatabase) GenerateGivenName(machineKey string, suppliedName string
return "", err
}
- for _, node := range nodes {
- if node.MachineKey != machineKey && node.GivenName == givenName {
- postfixedName, err := generateGivenName(suppliedName, true)
- if err != nil {
- return "", err
- }
-
- givenName = postfixedName
+ var nodeFound *types.Node
+ for idx, node := range nodes {
+ if node.GivenName == givenName {
+ nodeFound = nodes[idx]
}
}
+ if nodeFound != nil && nodeFound.MachineKey.String() != mkey.String() {
+ postfixedName, err := generateGivenName(suppliedName, true)
+ if err != nil {
+ return "", err
+ }
+
+ givenName = postfixedName
+ }
+
return givenName, nil
}
@@ -824,52 +866,69 @@ func (hsdb *HSDatabase) ExpireExpiredNodes(lastCheck time.Time) time.Time {
// checked everything.
started := time.Now()
- users, err := hsdb.listUsers()
+ expiredNodes := make([]*types.Node, 0)
+
+ nodes, err := hsdb.listNodes()
if err != nil {
- log.Error().Err(err).Msg("Error listing users")
+ log.Error().
+ Err(err).
+ Msg("Error listing nodes to find expired nodes")
return time.Unix(0, 0)
}
+ for index, node := range nodes {
+ if node.IsExpired() &&
+ // TODO(kradalby): Replace this, it is very spammy
+ // It will notify about all nodes that has been expired.
+ // It should only notify about expired nodes since _last check_.
+ node.Expiry.After(lastCheck) {
+ expiredNodes = append(expiredNodes, &nodes[index])
- for _, user := range users {
- nodes, err := hsdb.listNodesByUser(user.Name)
- if err != nil {
- log.Error().
- Err(err).
- Str("user", user.Name).
- Msg("Error listing nodes in user")
-
- return time.Unix(0, 0)
- }
-
- expired := make([]tailcfg.NodeID, 0)
- for index, node := range nodes {
- if node.IsExpired() &&
- node.Expiry.After(lastCheck) {
- expired = append(expired, tailcfg.NodeID(node.ID))
-
- now := time.Now()
- err := hsdb.nodeSetExpiry(nodes[index], now)
- if err != nil {
- log.Error().
- Err(err).
- Str("node", node.Hostname).
- Str("name", node.GivenName).
- Msg("🤮 Cannot expire node")
- } else {
- log.Info().
- Str("node", node.Hostname).
- Str("name", node.GivenName).
- Msg("Node successfully expired")
- }
+ // Do not use setNodeExpiry as that has a notifier hook, which
+ // can cause a deadlock, we are updating all changed nodes later
+ // and there is no point in notifiying twice.
+ if err := hsdb.db.Model(nodes[index]).Updates(types.Node{
+ Expiry: &started,
+ }).Error; err != nil {
+ log.Error().
+ Err(err).
+ Str("node", node.Hostname).
+ Str("name", node.GivenName).
+ Msg("🤮 Cannot expire node")
+ } else {
+ log.Info().
+ Str("node", node.Hostname).
+ Str("name", node.GivenName).
+ Msg("Node successfully expired")
}
}
+ }
- if len(expired) > 0 {
- hsdb.notifier.NotifyAll(types.StateUpdate{
- Type: types.StatePeerRemoved,
- Removed: expired,
- })
+ expired := make([]*tailcfg.PeerChange, len(expiredNodes))
+ for idx, node := range expiredNodes {
+ expired[idx] = &tailcfg.PeerChange{
+ NodeID: tailcfg.NodeID(node.ID),
+ KeyExpiry: &started,
+ }
+ }
+
+ // Inform the peers of a node with a lightweight update.
+ stateUpdate := types.StateUpdate{
+ Type: types.StatePeerChangedPatch,
+ ChangePatches: expired,
+ }
+ if stateUpdate.Valid() {
+ hsdb.notifier.NotifyAll(stateUpdate)
+ }
+
+ // Inform the node itself that it has expired.
+ for _, node := range expiredNodes {
+ stateSelfUpdate := types.StateUpdate{
+ Type: types.StateSelfUpdate,
+ ChangeNodes: types.Nodes{node},
+ }
+ if stateSelfUpdate.Valid() {
+ hsdb.notifier.NotifyByMachineKey(stateSelfUpdate, node.MachineKey)
}
}
diff --git a/hscontrol/db/node_test.go b/hscontrol/db/node_test.go
index 54b1cd0..140c264 100644
--- a/hscontrol/db/node_test.go
+++ b/hscontrol/db/node_test.go
@@ -12,6 +12,7 @@ import (
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util"
"gopkg.in/check.v1"
+ "tailscale.com/tailcfg"
"tailscale.com/types/key"
)
@@ -25,11 +26,13 @@ func (s *Suite) TestGetNode(c *check.C) {
_, err = db.GetNode("test", "testnode")
c.Assert(err, check.NotNil)
+ nodeKey := key.NewNode()
+ machineKey := key.NewMachine()
+
node := &types.Node{
ID: 0,
- MachineKey: "foo",
- NodeKey: "bar",
- DiscoKey: "faa",
+ MachineKey: machineKey.Public(),
+ NodeKey: nodeKey.Public(),
Hostname: "testnode",
UserID: user.ID,
RegisterMethod: util.RegisterMethodAuthKey,
@@ -51,11 +54,13 @@ func (s *Suite) TestGetNodeByID(c *check.C) {
_, err = db.GetNodeByID(0)
c.Assert(err, check.NotNil)
+ nodeKey := key.NewNode()
+ machineKey := key.NewMachine()
+
node := types.Node{
ID: 0,
- MachineKey: "foo",
- NodeKey: "bar",
- DiscoKey: "faa",
+ MachineKey: machineKey.Public(),
+ NodeKey: nodeKey.Public(),
Hostname: "testnode",
UserID: user.ID,
RegisterMethod: util.RegisterMethodAuthKey,
@@ -82,9 +87,8 @@ func (s *Suite) TestGetNodeByNodeKey(c *check.C) {
node := types.Node{
ID: 0,
- MachineKey: util.MachinePublicKeyStripPrefix(machineKey.Public()),
- NodeKey: util.NodePublicKeyStripPrefix(nodeKey.Public()),
- DiscoKey: "faa",
+ MachineKey: machineKey.Public(),
+ NodeKey: nodeKey.Public(),
Hostname: "testnode",
UserID: user.ID,
RegisterMethod: util.RegisterMethodAuthKey,
@@ -113,9 +117,8 @@ func (s *Suite) TestGetNodeByAnyNodeKey(c *check.C) {
node := types.Node{
ID: 0,
- MachineKey: util.MachinePublicKeyStripPrefix(machineKey.Public()),
- NodeKey: util.NodePublicKeyStripPrefix(nodeKey.Public()),
- DiscoKey: "faa",
+ MachineKey: machineKey.Public(),
+ NodeKey: nodeKey.Public(),
Hostname: "testnode",
UserID: user.ID,
RegisterMethod: util.RegisterMethodAuthKey,
@@ -130,11 +133,14 @@ func (s *Suite) TestGetNodeByAnyNodeKey(c *check.C) {
func (s *Suite) TestHardDeleteNode(c *check.C) {
user, err := db.CreateUser("test")
c.Assert(err, check.IsNil)
+
+ nodeKey := key.NewNode()
+ machineKey := key.NewMachine()
+
node := types.Node{
ID: 0,
- MachineKey: "foo",
- NodeKey: "bar",
- DiscoKey: "faa",
+ MachineKey: machineKey.Public(),
+ NodeKey: nodeKey.Public(),
Hostname: "testnode3",
UserID: user.ID,
RegisterMethod: util.RegisterMethodAuthKey,
@@ -160,11 +166,13 @@ func (s *Suite) TestListPeers(c *check.C) {
c.Assert(err, check.NotNil)
for index := 0; index <= 10; index++ {
+ nodeKey := key.NewNode()
+ machineKey := key.NewMachine()
+
node := types.Node{
ID: uint64(index),
- MachineKey: "foo" + strconv.Itoa(index),
- NodeKey: "bar" + strconv.Itoa(index),
- DiscoKey: "faa" + strconv.Itoa(index),
+ MachineKey: machineKey.Public(),
+ NodeKey: nodeKey.Public(),
Hostname: "testnode" + strconv.Itoa(index),
UserID: user.ID,
RegisterMethod: util.RegisterMethodAuthKey,
@@ -205,11 +213,13 @@ func (s *Suite) TestGetACLFilteredPeers(c *check.C) {
c.Assert(err, check.NotNil)
for index := 0; index <= 10; index++ {
+ nodeKey := key.NewNode()
+ machineKey := key.NewMachine()
+
node := types.Node{
ID: uint64(index),
- MachineKey: "foo" + strconv.Itoa(index),
- NodeKey: "bar" + strconv.Itoa(index),
- DiscoKey: "faa" + strconv.Itoa(index),
+ MachineKey: machineKey.Public(),
+ NodeKey: nodeKey.Public(),
IPAddresses: types.NodeAddresses{
netip.MustParseAddr(fmt.Sprintf("100.64.0.%v", strconv.Itoa(index+1))),
},
@@ -288,11 +298,13 @@ func (s *Suite) TestExpireNode(c *check.C) {
_, err = db.GetNode("test", "testnode")
c.Assert(err, check.NotNil)
+ nodeKey := key.NewNode()
+ machineKey := key.NewMachine()
+
node := &types.Node{
ID: 0,
- MachineKey: "foo",
- NodeKey: "bar",
- DiscoKey: "faa",
+ MachineKey: machineKey.Public(),
+ NodeKey: nodeKey.Public(),
Hostname: "testnode",
UserID: user.ID,
RegisterMethod: util.RegisterMethodAuthKey,
@@ -345,11 +357,15 @@ func (s *Suite) TestGenerateGivenName(c *check.C) {
_, err = db.GetNode("user-1", "testnode")
c.Assert(err, check.NotNil)
+ nodeKey := key.NewNode()
+ machineKey := key.NewMachine()
+
+ machineKey2 := key.NewMachine()
+
node := &types.Node{
ID: 0,
- MachineKey: "node-key-1",
- NodeKey: "node-key-1",
- DiscoKey: "disco-key-1",
+ MachineKey: machineKey.Public(),
+ NodeKey: nodeKey.Public(),
Hostname: "hostname-1",
GivenName: "hostname-1",
UserID: user1.ID,
@@ -358,25 +374,20 @@ func (s *Suite) TestGenerateGivenName(c *check.C) {
}
db.db.Save(node)
- givenName, err := db.GenerateGivenName("node-key-2", "hostname-2")
+ givenName, err := db.GenerateGivenName(machineKey2.Public(), "hostname-2")
comment := check.Commentf("Same user, unique nodes, unique hostnames, no conflict")
c.Assert(err, check.IsNil, comment)
c.Assert(givenName, check.Equals, "hostname-2", comment)
- givenName, err = db.GenerateGivenName("node-key-1", "hostname-1")
+ givenName, err = db.GenerateGivenName(machineKey.Public(), "hostname-1")
comment = check.Commentf("Same user, same node, same hostname, no conflict")
c.Assert(err, check.IsNil, comment)
c.Assert(givenName, check.Equals, "hostname-1", comment)
- givenName, err = db.GenerateGivenName("node-key-2", "hostname-1")
+ givenName, err = db.GenerateGivenName(machineKey2.Public(), "hostname-1")
comment = check.Commentf("Same user, unique nodes, same hostname, conflict")
c.Assert(err, check.IsNil, comment)
c.Assert(givenName, check.Matches, fmt.Sprintf("^hostname-1-[a-z0-9]{%d}$", NodeGivenNameHashLength), comment)
-
- givenName, err = db.GenerateGivenName("node-key-2", "hostname-1")
- comment = check.Commentf("Unique users, unique nodes, same hostname, conflict")
- c.Assert(err, check.IsNil, comment)
- c.Assert(givenName, check.Matches, fmt.Sprintf("^hostname-1-[a-z0-9]{%d}$", NodeGivenNameHashLength), comment)
}
func (s *Suite) TestSetTags(c *check.C) {
@@ -389,11 +400,13 @@ func (s *Suite) TestSetTags(c *check.C) {
_, err = db.GetNode("test", "testnode")
c.Assert(err, check.NotNil)
+ nodeKey := key.NewNode()
+ machineKey := key.NewMachine()
+
node := &types.Node{
ID: 0,
- MachineKey: "foo",
- NodeKey: "bar",
- DiscoKey: "faa",
+ MachineKey: machineKey.Public(),
+ NodeKey: nodeKey.Public(),
Hostname: "testnode",
UserID: user.ID,
RegisterMethod: util.RegisterMethodAuthKey,
@@ -565,6 +578,7 @@ func (s *Suite) TestAutoApproveRoutes(c *check.C) {
c.Assert(err, check.IsNil)
nodeKey := key.NewNode()
+ machineKey := key.NewMachine()
defaultRouteV4 := netip.MustParsePrefix("0.0.0.0/0")
defaultRouteV6 := netip.MustParsePrefix("::/0")
@@ -574,14 +588,13 @@ func (s *Suite) TestAutoApproveRoutes(c *check.C) {
node := types.Node{
ID: 0,
- MachineKey: "foo",
- NodeKey: util.NodePublicKeyStripPrefix(nodeKey.Public()),
- DiscoKey: "faa",
+ MachineKey: machineKey.Public(),
+ NodeKey: nodeKey.Public(),
Hostname: "test",
UserID: user.ID,
RegisterMethod: util.RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
- HostInfo: types.HostInfo{
+ Hostinfo: &tailcfg.Hostinfo{
RequestTags: []string{"tag:exit"},
RoutableIPs: []netip.Prefix{defaultRouteV4, defaultRouteV6, route1, route2},
},
@@ -590,8 +603,9 @@ func (s *Suite) TestAutoApproveRoutes(c *check.C) {
db.db.Save(&node)
- err = db.SaveNodeRoutes(&node)
+ sendUpdate, err := db.SaveNodeRoutes(&node)
c.Assert(err, check.IsNil)
+ c.Assert(sendUpdate, check.Equals, false)
node0ByID, err := db.GetNodeByID(0)
c.Assert(err, check.IsNil)
diff --git a/hscontrol/db/preauth_keys_test.go b/hscontrol/db/preauth_keys_test.go
index 9bf8c89..df9c2a1 100644
--- a/hscontrol/db/preauth_keys_test.go
+++ b/hscontrol/db/preauth_keys_test.go
@@ -77,9 +77,6 @@ func (*Suite) TestAlreadyUsedKey(c *check.C) {
node := types.Node{
ID: 0,
- MachineKey: "foo",
- NodeKey: "bar",
- DiscoKey: "faa",
Hostname: "testest",
UserID: user.ID,
RegisterMethod: util.RegisterMethodAuthKey,
@@ -101,9 +98,6 @@ func (*Suite) TestReusableBeingUsedKey(c *check.C) {
node := types.Node{
ID: 1,
- MachineKey: "foo",
- NodeKey: "bar",
- DiscoKey: "faa",
Hostname: "testest",
UserID: user.ID,
RegisterMethod: util.RegisterMethodAuthKey,
@@ -138,9 +132,6 @@ func (*Suite) TestEphemeralKey(c *check.C) {
now := time.Now().Add(-time.Second * 30)
node := types.Node{
ID: 0,
- MachineKey: "foo",
- NodeKey: "bar",
- DiscoKey: "faa",
Hostname: "testest",
UserID: user.ID,
RegisterMethod: util.RegisterMethodAuthKey,
@@ -207,5 +198,5 @@ func (*Suite) TestPreAuthKeyACLTags(c *check.C) {
listedPaks, err := db.ListPreAuthKeys("test8")
c.Assert(err, check.IsNil)
- c.Assert(listedPaks[0].Proto().AclTags, check.DeepEquals, tags)
+ c.Assert(listedPaks[0].Proto().GetAclTags(), check.DeepEquals, tags)
}
diff --git a/hscontrol/db/routes.go b/hscontrol/db/routes.go
index d73c3af..51c7f3b 100644
--- a/hscontrol/db/routes.go
+++ b/hscontrol/db/routes.go
@@ -7,7 +7,9 @@ import (
"github.com/juanfont/headscale/hscontrol/policy"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/rs/zerolog/log"
+ "github.com/samber/lo"
"gorm.io/gorm"
+ "tailscale.com/types/key"
)
var ErrRouteIsNotAvailable = errors.New("route is not available")
@@ -21,7 +23,38 @@ func (hsdb *HSDatabase) GetRoutes() (types.Routes, error) {
func (hsdb *HSDatabase) getRoutes() (types.Routes, error) {
var routes types.Routes
- err := hsdb.db.Preload("Node").Find(&routes).Error
+ err := hsdb.db.
+ Preload("Node").
+ Preload("Node.User").
+ Find(&routes).Error
+ if err != nil {
+ return nil, err
+ }
+
+ return routes, nil
+}
+
+func (hsdb *HSDatabase) getAdvertisedAndEnabledRoutes() (types.Routes, error) {
+ var routes types.Routes
+ err := hsdb.db.
+ Preload("Node").
+ Preload("Node.User").
+ Where("advertised = ? AND enabled = ?", true, true).
+ Find(&routes).Error
+ if err != nil {
+ return nil, err
+ }
+
+ return routes, nil
+}
+
+func (hsdb *HSDatabase) getRoutesByPrefix(pref netip.Prefix) (types.Routes, error) {
+ var routes types.Routes
+ err := hsdb.db.
+ Preload("Node").
+ Preload("Node.User").
+ Where("prefix = ?", types.IPPrefix(pref)).
+ Find(&routes).Error
if err != nil {
return nil, err
}
@@ -40,6 +73,7 @@ func (hsdb *HSDatabase) getNodeAdvertisedRoutes(node *types.Node) (types.Routes,
var routes types.Routes
err := hsdb.db.
Preload("Node").
+ Preload("Node.User").
Where("node_id = ? AND advertised = true", node.ID).
Find(&routes).Error
if err != nil {
@@ -60,6 +94,7 @@ func (hsdb *HSDatabase) getNodeRoutes(node *types.Node) (types.Routes, error) {
var routes types.Routes
err := hsdb.db.
Preload("Node").
+ Preload("Node.User").
Where("node_id = ?", node.ID).
Find(&routes).Error
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
@@ -78,7 +113,10 @@ func (hsdb *HSDatabase) GetRoute(id uint64) (*types.Route, error) {
func (hsdb *HSDatabase) getRoute(id uint64) (*types.Route, error) {
var route types.Route
- err := hsdb.db.Preload("Node").First(&route, id).Error
+ err := hsdb.db.
+ Preload("Node").
+ Preload("Node.User").
+ First(&route, id).Error
if err != nil {
return nil, err
}
@@ -122,37 +160,61 @@ func (hsdb *HSDatabase) DisableRoute(id uint64) error {
return err
}
+ var routes types.Routes
+ node := route.Node
+
// Tailscale requires both IPv4 and IPv6 exit routes to
// be enabled at the same time, as per
// https://github.com/juanfont/headscale/issues/804#issuecomment-1399314002
if !route.IsExitRoute() {
+ err = hsdb.failoverRouteWithNotify(route)
+ if err != nil {
+ return err
+ }
+
route.Enabled = false
route.IsPrimary = false
err = hsdb.db.Save(route).Error
if err != nil {
return err
}
+ } else {
+ routes, err = hsdb.getNodeRoutes(&node)
+ if err != nil {
+ return err
+ }
- return hsdb.handlePrimarySubnetFailover()
- }
-
- routes, err := hsdb.getNodeRoutes(&route.Node)
- if err != nil {
- return err
- }
-
- for i := range routes {
- if routes[i].IsExitRoute() {
- routes[i].Enabled = false
- routes[i].IsPrimary = false
- err = hsdb.db.Save(&routes[i]).Error
- if err != nil {
- return err
+ for i := range routes {
+ if routes[i].IsExitRoute() {
+ routes[i].Enabled = false
+ routes[i].IsPrimary = false
+ err = hsdb.db.Save(&routes[i]).Error
+ if err != nil {
+ return err
+ }
}
}
}
- return hsdb.handlePrimarySubnetFailover()
+ if routes == nil {
+ routes, err = hsdb.getNodeRoutes(&node)
+ if err != nil {
+ return err
+ }
+ }
+
+ node.Routes = routes
+
+ stateUpdate := types.StateUpdate{
+ Type: types.StatePeerChanged,
+ ChangeNodes: types.Nodes{&node},
+ Message: "called from db.DisableRoute",
+ }
+ if stateUpdate.Valid() {
+ hsdb.notifier.NotifyAll(stateUpdate)
+ }
+
+ return nil
}
func (hsdb *HSDatabase) DeleteRoute(id uint64) error {
@@ -164,34 +226,58 @@ func (hsdb *HSDatabase) DeleteRoute(id uint64) error {
return err
}
+ var routes types.Routes
+ node := route.Node
+
// Tailscale requires both IPv4 and IPv6 exit routes to
// be enabled at the same time, as per
// https://github.com/juanfont/headscale/issues/804#issuecomment-1399314002
if !route.IsExitRoute() {
+ err := hsdb.failoverRouteWithNotify(route)
+ if err != nil {
+ return nil
+ }
+
if err := hsdb.db.Unscoped().Delete(&route).Error; err != nil {
return err
}
+ } else {
+ routes, err := hsdb.getNodeRoutes(&node)
+ if err != nil {
+ return err
+ }
- return hsdb.handlePrimarySubnetFailover()
- }
+ routesToDelete := types.Routes{}
+ for _, r := range routes {
+ if r.IsExitRoute() {
+ routesToDelete = append(routesToDelete, r)
+ }
+ }
- routes, err := hsdb.getNodeRoutes(&route.Node)
- if err != nil {
- return err
- }
-
- routesToDelete := types.Routes{}
- for _, r := range routes {
- if r.IsExitRoute() {
- routesToDelete = append(routesToDelete, r)
+ if err := hsdb.db.Unscoped().Delete(&routesToDelete).Error; err != nil {
+ return err
}
}
- if err := hsdb.db.Unscoped().Delete(&routesToDelete).Error; err != nil {
- return err
+ if routes == nil {
+ routes, err = hsdb.getNodeRoutes(&node)
+ if err != nil {
+ return err
+ }
}
- return hsdb.handlePrimarySubnetFailover()
+ node.Routes = routes
+
+ stateUpdate := types.StateUpdate{
+ Type: types.StatePeerChanged,
+ ChangeNodes: types.Nodes{&node},
+ Message: "called from db.DeleteRoute",
+ }
+ if stateUpdate.Valid() {
+ hsdb.notifier.NotifyAll(stateUpdate)
+ }
+
+ return nil
}
func (hsdb *HSDatabase) deleteNodeRoutes(node *types.Node) error {
@@ -204,9 +290,13 @@ func (hsdb *HSDatabase) deleteNodeRoutes(node *types.Node) error {
if err := hsdb.db.Unscoped().Delete(&routes[i]).Error; err != nil {
return err
}
+
+ // TODO(kradalby): This is a bit too aggressive, we could probably
+ // figure out which routes needs to be failed over rather than all.
+ hsdb.failoverRouteWithNotify(&routes[i])
}
- return hsdb.handlePrimarySubnetFailover()
+ return nil
}
// isUniquePrefix returns if there is another node providing the same route already.
@@ -259,22 +349,26 @@ func (hsdb *HSDatabase) GetNodePrimaryRoutes(node *types.Node) (types.Routes, er
// SaveNodeRoutes takes a node and updates the database with
// the new routes.
-func (hsdb *HSDatabase) SaveNodeRoutes(node *types.Node) error {
+// It returns a bool wheter an update should be sent as the
+// saved route impacts nodes.
+func (hsdb *HSDatabase) SaveNodeRoutes(node *types.Node) (bool, error) {
hsdb.mu.Lock()
defer hsdb.mu.Unlock()
return hsdb.saveNodeRoutes(node)
}
-func (hsdb *HSDatabase) saveNodeRoutes(node *types.Node) error {
+func (hsdb *HSDatabase) saveNodeRoutes(node *types.Node) (bool, error) {
+ sendUpdate := false
+
currentRoutes := types.Routes{}
err := hsdb.db.Where("node_id = ?", node.ID).Find(¤tRoutes).Error
if err != nil {
- return err
+ return sendUpdate, err
}
advertisedRoutes := map[netip.Prefix]bool{}
- for _, prefix := range node.HostInfo.RoutableIPs {
+ for _, prefix := range node.Hostinfo.RoutableIPs {
advertisedRoutes[prefix] = false
}
@@ -290,7 +384,14 @@ func (hsdb *HSDatabase) saveNodeRoutes(node *types.Node) error {
currentRoutes[pos].Advertised = true
err := hsdb.db.Save(¤tRoutes[pos]).Error
if err != nil {
- return err
+ return sendUpdate, err
+ }
+
+ // If a route that is newly "saved" is already
+ // enabled, set sendUpdate to true as it is now
+ // available.
+ if route.Enabled {
+ sendUpdate = true
}
}
advertisedRoutes[netip.Prefix(route.Prefix)] = true
@@ -299,7 +400,7 @@ func (hsdb *HSDatabase) saveNodeRoutes(node *types.Node) error {
currentRoutes[pos].Enabled = false
err := hsdb.db.Save(¤tRoutes[pos]).Error
if err != nil {
- return err
+ return sendUpdate, err
}
}
}
@@ -314,7 +415,41 @@ func (hsdb *HSDatabase) saveNodeRoutes(node *types.Node) error {
}
err := hsdb.db.Create(&route).Error
if err != nil {
- return err
+ return sendUpdate, err
+ }
+ }
+ }
+
+ return sendUpdate, nil
+}
+
+// EnsureFailoverRouteIsAvailable takes a node and checks if the node's route
+// currently have a functioning host that exposes the network.
+func (hsdb *HSDatabase) EnsureFailoverRouteIsAvailable(node *types.Node) error {
+ nodeRoutes, err := hsdb.getNodeRoutes(node)
+ if err != nil {
+ return nil
+ }
+
+ for _, nodeRoute := range nodeRoutes {
+ routes, err := hsdb.getRoutesByPrefix(netip.Prefix(nodeRoute.Prefix))
+ if err != nil {
+ return err
+ }
+
+ for _, route := range routes {
+ if route.IsPrimary {
+ // if we have a primary route, and the node is connected
+ // nothing needs to be done.
+ if hsdb.notifier.IsConnected(route.Node.MachineKey) {
+ continue
+ }
+
+ // if not, we need to failover the route
+ err := hsdb.failoverRouteWithNotify(&route)
+ if err != nil {
+ return err
+ }
}
}
}
@@ -322,133 +457,181 @@ func (hsdb *HSDatabase) saveNodeRoutes(node *types.Node) error {
return nil
}
-func (hsdb *HSDatabase) HandlePrimarySubnetFailover() error {
- hsdb.mu.Lock()
- defer hsdb.mu.Unlock()
-
- return hsdb.handlePrimarySubnetFailover()
-}
-
-func (hsdb *HSDatabase) handlePrimarySubnetFailover() error {
- // first, get all the enabled routes
- var routes types.Routes
- err := hsdb.db.
- Preload("Node").
- Where("advertised = ? AND enabled = ?", true, true).
- Find(&routes).Error
- if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
- log.Error().Err(err).Msg("error getting routes")
+func (hsdb *HSDatabase) FailoverNodeRoutesWithNotify(node *types.Node) error {
+ routes, err := hsdb.getNodeRoutes(node)
+ if err != nil {
+ return nil
}
- changedNodes := make(types.Nodes, 0)
- for pos, route := range routes {
- if route.IsExitRoute() {
+ var changedKeys []key.MachinePublic
+
+ for _, route := range routes {
+ changed, err := hsdb.failoverRoute(&route)
+ if err != nil {
+ return err
+ }
+
+ changedKeys = append(changedKeys, changed...)
+ }
+
+ changedKeys = lo.Uniq(changedKeys)
+
+ var nodes types.Nodes
+
+ for _, key := range changedKeys {
+ node, err := hsdb.GetNodeByMachineKey(key)
+ if err != nil {
+ return err
+ }
+
+ nodes = append(nodes, node)
+ }
+
+ if nodes != nil {
+ stateUpdate := types.StateUpdate{
+ Type: types.StatePeerChanged,
+ ChangeNodes: nodes,
+ Message: "called from db.FailoverNodeRoutesWithNotify",
+ }
+ if stateUpdate.Valid() {
+ hsdb.notifier.NotifyAll(stateUpdate)
+ }
+ }
+
+ return nil
+}
+
+func (hsdb *HSDatabase) failoverRouteWithNotify(r *types.Route) error {
+ changedKeys, err := hsdb.failoverRoute(r)
+ if err != nil {
+ return err
+ }
+
+ if len(changedKeys) == 0 {
+ return nil
+ }
+
+ var nodes types.Nodes
+
+ log.Trace().
+ Str("hostname", r.Node.Hostname).
+ Msg("loading machines with new primary routes from db")
+
+ for _, key := range changedKeys {
+ node, err := hsdb.getNodeByMachineKey(key)
+ if err != nil {
+ return err
+ }
+
+ nodes = append(nodes, node)
+ }
+
+ log.Trace().
+ Str("hostname", r.Node.Hostname).
+ Msg("notifying peers about primary route change")
+
+ if nodes != nil {
+ stateUpdate := types.StateUpdate{
+ Type: types.StatePeerChanged,
+ ChangeNodes: nodes,
+ Message: "called from db.failoverRouteWithNotify",
+ }
+ if stateUpdate.Valid() {
+ hsdb.notifier.NotifyAll(stateUpdate)
+ }
+ }
+
+ log.Trace().
+ Str("hostname", r.Node.Hostname).
+ Msg("notified peers about primary route change")
+
+ return nil
+}
+
+// failoverRoute takes a route that is no longer available,
+// this can be either from:
+// - being disabled
+// - being deleted
+// - host going offline
+//
+// and tries to find a new route to take over its place.
+// If the given route was not primary, it returns early.
+func (hsdb *HSDatabase) failoverRoute(r *types.Route) ([]key.MachinePublic, error) {
+ if r == nil {
+ return nil, nil
+ }
+
+ // This route is not a primary route, and it isnt
+ // being served to nodes.
+ if !r.IsPrimary {
+ return nil, nil
+ }
+
+ // We do not have to failover exit nodes
+ if r.IsExitRoute() {
+ return nil, nil
+ }
+
+ routes, err := hsdb.getRoutesByPrefix(netip.Prefix(r.Prefix))
+ if err != nil {
+ return nil, err
+ }
+
+ var newPrimary *types.Route
+
+ // Find a new suitable route
+ for idx, route := range routes {
+ if r.ID == route.ID {
continue
}
- node := &route.Node
-
- if !route.IsPrimary {
- _, err := hsdb.getPrimaryRoute(netip.Prefix(route.Prefix))
- if hsdb.isUniquePrefix(route) || errors.Is(err, gorm.ErrRecordNotFound) {
- log.Info().
- Str("prefix", netip.Prefix(route.Prefix).String()).
- Str("node", route.Node.GivenName).
- Msg("Setting primary route")
- routes[pos].IsPrimary = true
- err := hsdb.db.Save(&routes[pos]).Error
- if err != nil {
- log.Error().Err(err).Msg("error marking route as primary")
-
- return err
- }
-
- changedNodes = append(changedNodes, node)
-
- continue
- }
- }
-
- if route.IsPrimary {
- if route.Node.IsOnline() {
- continue
- }
-
- // node offline, find a new primary
- log.Info().
- Str("node", route.Node.Hostname).
- Str("prefix", netip.Prefix(route.Prefix).String()).
- Msgf("node offline, finding a new primary subnet")
-
- // find a new primary route
- var newPrimaryRoutes types.Routes
- err := hsdb.db.
- Preload("Node").
- Where("prefix = ? AND node_id != ? AND advertised = ? AND enabled = ?",
- route.Prefix,
- route.NodeID,
- true, true).
- Find(&newPrimaryRoutes).Error
- if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
- log.Error().Err(err).Msg("error finding new primary route")
-
- return err
- }
-
- var newPrimaryRoute *types.Route
- for pos, r := range newPrimaryRoutes {
- if r.Node.IsOnline() {
- newPrimaryRoute = &newPrimaryRoutes[pos]
-
- break
- }
- }
-
- if newPrimaryRoute == nil {
- log.Warn().
- Str("node", route.Node.Hostname).
- Str("prefix", netip.Prefix(route.Prefix).String()).
- Msgf("no alternative primary route found")
-
- continue
- }
-
- log.Info().
- Str("old_node", route.Node.Hostname).
- Str("prefix", netip.Prefix(route.Prefix).String()).
- Str("new_node", newPrimaryRoute.Node.Hostname).
- Msgf("found new primary route")
-
- // disable the old primary route
- routes[pos].IsPrimary = false
- err = hsdb.db.Save(&routes[pos]).Error
- if err != nil {
- log.Error().Err(err).Msg("error disabling old primary route")
-
- return err
- }
-
- // enable the new primary route
- newPrimaryRoute.IsPrimary = true
- err = hsdb.db.Save(&newPrimaryRoute).Error
- if err != nil {
- log.Error().Err(err).Msg("error enabling new primary route")
-
- return err
- }
-
- changedNodes = append(changedNodes, node)
+ if hsdb.notifier.IsConnected(route.Node.MachineKey) {
+ newPrimary = &routes[idx]
+ break
}
}
- if len(changedNodes) > 0 {
- hsdb.notifier.NotifyAll(types.StateUpdate{
- Type: types.StatePeerChanged,
- Changed: changedNodes,
- })
+ // If a new route was not found/available,
+ // return with an error.
+ // We do not want to update the database as
+ // the one currently marked as primary is the
+ // best we got.
+ if newPrimary == nil {
+ return nil, nil
}
- return nil
+ log.Trace().
+ Str("hostname", newPrimary.Node.Hostname).
+ Msg("found new primary, updating db")
+
+ // Remove primary from the old route
+ r.IsPrimary = false
+ err = hsdb.db.Save(&r).Error
+ if err != nil {
+ log.Error().Err(err).Msg("error disabling new primary route")
+
+ return nil, err
+ }
+
+ log.Trace().
+ Str("hostname", newPrimary.Node.Hostname).
+ Msg("removed primary from old route")
+
+ // Set primary for the new primary
+ newPrimary.IsPrimary = true
+ err = hsdb.db.Save(&newPrimary).Error
+ if err != nil {
+ log.Error().Err(err).Msg("error enabling new primary route")
+
+ return nil, err
+ }
+
+ log.Trace().
+ Str("hostname", newPrimary.Node.Hostname).
+ Msg("set primary to new route")
+
+ // Return a list of the machinekeys of the changed nodes.
+ return []key.MachinePublic{r.Node.MachineKey, newPrimary.Node.MachineKey}, nil
}
// EnableAutoApprovedRoutes enables any routes advertised by a node that match the ACL autoApprovers policy.
diff --git a/hscontrol/db/routes_test.go b/hscontrol/db/routes_test.go
index ba5882b..d491b6a 100644
--- a/hscontrol/db/routes_test.go
+++ b/hscontrol/db/routes_test.go
@@ -2,12 +2,19 @@ package db
import (
"net/netip"
+ "os"
+ "testing"
"time"
+ "github.com/google/go-cmp/cmp"
+ "github.com/juanfont/headscale/hscontrol/notifier"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util"
+ "github.com/stretchr/testify/assert"
"gopkg.in/check.v1"
+ "gorm.io/gorm"
"tailscale.com/tailcfg"
+ "tailscale.com/types/key"
)
func (s *Suite) TestGetRoutes(c *check.C) {
@@ -29,19 +36,17 @@ func (s *Suite) TestGetRoutes(c *check.C) {
node := types.Node{
ID: 0,
- MachineKey: "foo",
- NodeKey: "bar",
- DiscoKey: "faa",
Hostname: "test_get_route_node",
UserID: user.ID,
RegisterMethod: util.RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
- HostInfo: types.HostInfo(hostInfo),
+ Hostinfo: &hostInfo,
}
db.db.Save(&node)
- err = db.SaveNodeRoutes(&node)
+ su, err := db.SaveNodeRoutes(&node)
c.Assert(err, check.IsNil)
+ c.Assert(su, check.Equals, false)
advertisedRoutes, err := db.GetAdvertisedRoutes(&node)
c.Assert(err, check.IsNil)
@@ -80,19 +85,17 @@ func (s *Suite) TestGetEnableRoutes(c *check.C) {
node := types.Node{
ID: 0,
- MachineKey: "foo",
- NodeKey: "bar",
- DiscoKey: "faa",
Hostname: "test_enable_route_node",
UserID: user.ID,
RegisterMethod: util.RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
- HostInfo: types.HostInfo(hostInfo),
+ Hostinfo: &hostInfo,
}
db.db.Save(&node)
- err = db.SaveNodeRoutes(&node)
+ sendUpdate, err := db.SaveNodeRoutes(&node)
c.Assert(err, check.IsNil)
+ c.Assert(sendUpdate, check.Equals, false)
availableRoutes, err := db.GetAdvertisedRoutes(&node)
c.Assert(err, check.IsNil)
@@ -154,19 +157,17 @@ func (s *Suite) TestIsUniquePrefix(c *check.C) {
}
node1 := types.Node{
ID: 1,
- MachineKey: "foo",
- NodeKey: "bar",
- DiscoKey: "faa",
Hostname: "test_enable_route_node",
UserID: user.ID,
RegisterMethod: util.RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
- HostInfo: types.HostInfo(hostInfo1),
+ Hostinfo: &hostInfo1,
}
db.db.Save(&node1)
- err = db.SaveNodeRoutes(&node1)
+ sendUpdate, err := db.SaveNodeRoutes(&node1)
c.Assert(err, check.IsNil)
+ c.Assert(sendUpdate, check.Equals, false)
err = db.enableRoutes(&node1, route.String())
c.Assert(err, check.IsNil)
@@ -179,19 +180,17 @@ func (s *Suite) TestIsUniquePrefix(c *check.C) {
}
node2 := types.Node{
ID: 2,
- MachineKey: "foo",
- NodeKey: "bar",
- DiscoKey: "faa",
Hostname: "test_enable_route_node",
UserID: user.ID,
RegisterMethod: util.RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
- HostInfo: types.HostInfo(hostInfo2),
+ Hostinfo: &hostInfo2,
}
db.db.Save(&node2)
- err = db.SaveNodeRoutes(&node2)
+ sendUpdate, err = db.SaveNodeRoutes(&node2)
c.Assert(err, check.IsNil)
+ c.Assert(sendUpdate, check.Equals, false)
err = db.enableRoutes(&node2, route2.String())
c.Assert(err, check.IsNil)
@@ -213,148 +212,6 @@ func (s *Suite) TestIsUniquePrefix(c *check.C) {
c.Assert(len(routes), check.Equals, 0)
}
-func (s *Suite) TestSubnetFailover(c *check.C) {
- user, err := db.CreateUser("test")
- c.Assert(err, check.IsNil)
-
- pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil)
- c.Assert(err, check.IsNil)
-
- _, err = db.GetNode("test", "test_enable_route_node")
- c.Assert(err, check.NotNil)
-
- prefix, err := netip.ParsePrefix(
- "10.0.0.0/24",
- )
- c.Assert(err, check.IsNil)
-
- prefix2, err := netip.ParsePrefix(
- "150.0.10.0/25",
- )
- c.Assert(err, check.IsNil)
-
- hostInfo1 := tailcfg.Hostinfo{
- RoutableIPs: []netip.Prefix{prefix, prefix2},
- }
-
- now := time.Now()
- node1 := types.Node{
- ID: 1,
- MachineKey: "foo",
- NodeKey: "bar",
- DiscoKey: "faa",
- Hostname: "test_enable_route_node",
- UserID: user.ID,
- RegisterMethod: util.RegisterMethodAuthKey,
- AuthKeyID: uint(pak.ID),
- HostInfo: types.HostInfo(hostInfo1),
- LastSeen: &now,
- }
- db.db.Save(&node1)
-
- err = db.SaveNodeRoutes(&node1)
- c.Assert(err, check.IsNil)
-
- err = db.enableRoutes(&node1, prefix.String())
- c.Assert(err, check.IsNil)
-
- err = db.enableRoutes(&node1, prefix2.String())
- c.Assert(err, check.IsNil)
-
- err = db.HandlePrimarySubnetFailover()
- c.Assert(err, check.IsNil)
-
- enabledRoutes1, err := db.GetEnabledRoutes(&node1)
- c.Assert(err, check.IsNil)
- c.Assert(len(enabledRoutes1), check.Equals, 2)
-
- route, err := db.getPrimaryRoute(prefix)
- c.Assert(err, check.IsNil)
- c.Assert(route.NodeID, check.Equals, node1.ID)
-
- hostInfo2 := tailcfg.Hostinfo{
- RoutableIPs: []netip.Prefix{prefix2},
- }
- node2 := types.Node{
- ID: 2,
- MachineKey: "foo",
- NodeKey: "bar",
- DiscoKey: "faa",
- Hostname: "test_enable_route_node",
- UserID: user.ID,
- RegisterMethod: util.RegisterMethodAuthKey,
- AuthKeyID: uint(pak.ID),
- HostInfo: types.HostInfo(hostInfo2),
- LastSeen: &now,
- }
- db.db.Save(&node2)
-
- err = db.saveNodeRoutes(&node2)
- c.Assert(err, check.IsNil)
-
- err = db.enableRoutes(&node2, prefix2.String())
- c.Assert(err, check.IsNil)
-
- err = db.HandlePrimarySubnetFailover()
- c.Assert(err, check.IsNil)
-
- enabledRoutes1, err = db.GetEnabledRoutes(&node1)
- c.Assert(err, check.IsNil)
- c.Assert(len(enabledRoutes1), check.Equals, 2)
-
- enabledRoutes2, err := db.GetEnabledRoutes(&node2)
- c.Assert(err, check.IsNil)
- c.Assert(len(enabledRoutes2), check.Equals, 1)
-
- routes, err := db.GetNodePrimaryRoutes(&node1)
- c.Assert(err, check.IsNil)
- c.Assert(len(routes), check.Equals, 2)
-
- routes, err = db.GetNodePrimaryRoutes(&node2)
- c.Assert(err, check.IsNil)
- c.Assert(len(routes), check.Equals, 0)
-
- // lets make node1 lastseen 10 mins ago
- before := now.Add(-10 * time.Minute)
- node1.LastSeen = &before
- err = db.db.Save(&node1).Error
- c.Assert(err, check.IsNil)
-
- err = db.HandlePrimarySubnetFailover()
- c.Assert(err, check.IsNil)
-
- routes, err = db.GetNodePrimaryRoutes(&node1)
- c.Assert(err, check.IsNil)
- c.Assert(len(routes), check.Equals, 1)
-
- routes, err = db.GetNodePrimaryRoutes(&node2)
- c.Assert(err, check.IsNil)
- c.Assert(len(routes), check.Equals, 1)
-
- node2.HostInfo = types.HostInfo(tailcfg.Hostinfo{
- RoutableIPs: []netip.Prefix{prefix, prefix2},
- })
- err = db.db.Save(&node2).Error
- c.Assert(err, check.IsNil)
-
- err = db.SaveNodeRoutes(&node2)
- c.Assert(err, check.IsNil)
-
- err = db.enableRoutes(&node2, prefix.String())
- c.Assert(err, check.IsNil)
-
- err = db.HandlePrimarySubnetFailover()
- c.Assert(err, check.IsNil)
-
- routes, err = db.GetNodePrimaryRoutes(&node1)
- c.Assert(err, check.IsNil)
- c.Assert(len(routes), check.Equals, 0)
-
- routes, err = db.GetNodePrimaryRoutes(&node2)
- c.Assert(err, check.IsNil)
- c.Assert(len(routes), check.Equals, 2)
-}
-
func (s *Suite) TestDeleteRoutes(c *check.C) {
user, err := db.CreateUser("test")
c.Assert(err, check.IsNil)
@@ -382,20 +239,18 @@ func (s *Suite) TestDeleteRoutes(c *check.C) {
now := time.Now()
node1 := types.Node{
ID: 1,
- MachineKey: "foo",
- NodeKey: "bar",
- DiscoKey: "faa",
Hostname: "test_enable_route_node",
UserID: user.ID,
RegisterMethod: util.RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
- HostInfo: types.HostInfo(hostInfo1),
+ Hostinfo: &hostInfo1,
LastSeen: &now,
}
db.db.Save(&node1)
- err = db.SaveNodeRoutes(&node1)
+ sendUpdate, err := db.SaveNodeRoutes(&node1)
c.Assert(err, check.IsNil)
+ c.Assert(sendUpdate, check.Equals, false)
err = db.enableRoutes(&node1, prefix.String())
c.Assert(err, check.IsNil)
@@ -413,3 +268,362 @@ func (s *Suite) TestDeleteRoutes(c *check.C) {
c.Assert(err, check.IsNil)
c.Assert(len(enabledRoutes1), check.Equals, 1)
}
+
+func TestFailoverRoute(t *testing.T) {
+ ipp := func(s string) types.IPPrefix { return types.IPPrefix(netip.MustParsePrefix(s)) }
+
+ // TODO(kradalby): Count/verify updates
+ var sink chan types.StateUpdate
+
+ go func() {
+ for range sink {
+ }
+ }()
+
+ machineKeys := []key.MachinePublic{
+ key.NewMachine().Public(),
+ key.NewMachine().Public(),
+ key.NewMachine().Public(),
+ key.NewMachine().Public(),
+ }
+
+ tests := []struct {
+ name string
+ failingRoute types.Route
+ routes types.Routes
+ want []key.MachinePublic
+ wantErr bool
+ }{
+ {
+ name: "no-route",
+ failingRoute: types.Route{},
+ routes: types.Routes{},
+ want: nil,
+ wantErr: false,
+ },
+ {
+ name: "no-prime",
+ failingRoute: types.Route{
+ Model: gorm.Model{
+ ID: 1,
+ },
+ Prefix: ipp("10.0.0.0/24"),
+ Node: types.Node{
+ MachineKey: machineKeys[0],
+ },
+ IsPrimary: false,
+ },
+ routes: types.Routes{},
+ want: nil,
+ wantErr: false,
+ },
+ {
+ name: "exit-node",
+ failingRoute: types.Route{
+ Model: gorm.Model{
+ ID: 1,
+ },
+ Prefix: ipp("0.0.0.0/0"),
+ Node: types.Node{
+ MachineKey: machineKeys[0],
+ },
+ IsPrimary: true,
+ },
+ routes: types.Routes{},
+ want: nil,
+ wantErr: false,
+ },
+ {
+ name: "no-failover-single-route",
+ failingRoute: types.Route{
+ Model: gorm.Model{
+ ID: 1,
+ },
+ Prefix: ipp("10.0.0.0/24"),
+ Node: types.Node{
+ MachineKey: machineKeys[0],
+ },
+ IsPrimary: true,
+ },
+ routes: types.Routes{
+ types.Route{
+ Model: gorm.Model{
+ ID: 1,
+ },
+ Prefix: ipp("10.0.0.0/24"),
+ Node: types.Node{
+ MachineKey: machineKeys[0],
+ },
+ IsPrimary: true,
+ },
+ },
+ want: nil,
+ wantErr: false,
+ },
+ {
+ name: "failover-primary",
+ failingRoute: types.Route{
+ Model: gorm.Model{
+ ID: 1,
+ },
+ Prefix: ipp("10.0.0.0/24"),
+ Node: types.Node{
+ MachineKey: machineKeys[0],
+ },
+ IsPrimary: true,
+ },
+ routes: types.Routes{
+ types.Route{
+ Model: gorm.Model{
+ ID: 1,
+ },
+ Prefix: ipp("10.0.0.0/24"),
+ Node: types.Node{
+ MachineKey: machineKeys[0],
+ },
+ IsPrimary: true,
+ },
+ types.Route{
+ Model: gorm.Model{
+ ID: 2,
+ },
+ Prefix: ipp("10.0.0.0/24"),
+ Node: types.Node{
+ MachineKey: machineKeys[1],
+ },
+ IsPrimary: false,
+ },
+ },
+ want: []key.MachinePublic{
+ machineKeys[0],
+ machineKeys[1],
+ },
+ wantErr: false,
+ },
+ {
+ name: "failover-none-primary",
+ failingRoute: types.Route{
+ Model: gorm.Model{
+ ID: 1,
+ },
+ Prefix: ipp("10.0.0.0/24"),
+ Node: types.Node{
+ MachineKey: machineKeys[0],
+ },
+ IsPrimary: false,
+ },
+ routes: types.Routes{
+ types.Route{
+ Model: gorm.Model{
+ ID: 1,
+ },
+ Prefix: ipp("10.0.0.0/24"),
+ Node: types.Node{
+ MachineKey: machineKeys[0],
+ },
+ IsPrimary: true,
+ },
+ types.Route{
+ Model: gorm.Model{
+ ID: 2,
+ },
+ Prefix: ipp("10.0.0.0/24"),
+ Node: types.Node{
+ MachineKey: machineKeys[1],
+ },
+ IsPrimary: false,
+ },
+ },
+ want: nil,
+ wantErr: false,
+ },
+ {
+ name: "failover-primary-multi-route",
+ failingRoute: types.Route{
+ Model: gorm.Model{
+ ID: 2,
+ },
+ Prefix: ipp("10.0.0.0/24"),
+ Node: types.Node{
+ MachineKey: machineKeys[1],
+ },
+ IsPrimary: true,
+ },
+ routes: types.Routes{
+ types.Route{
+ Model: gorm.Model{
+ ID: 1,
+ },
+ Prefix: ipp("10.0.0.0/24"),
+ Node: types.Node{
+ MachineKey: machineKeys[0],
+ },
+ IsPrimary: false,
+ },
+ types.Route{
+ Model: gorm.Model{
+ ID: 2,
+ },
+ Prefix: ipp("10.0.0.0/24"),
+ Node: types.Node{
+ MachineKey: machineKeys[1],
+ },
+ IsPrimary: true,
+ },
+ types.Route{
+ Model: gorm.Model{
+ ID: 3,
+ },
+ Prefix: ipp("10.0.0.0/24"),
+ Node: types.Node{
+ MachineKey: machineKeys[2],
+ },
+ IsPrimary: false,
+ },
+ },
+ want: []key.MachinePublic{
+ machineKeys[1],
+ machineKeys[0],
+ },
+ wantErr: false,
+ },
+ {
+ name: "failover-primary-no-online",
+ failingRoute: types.Route{
+ Model: gorm.Model{
+ ID: 1,
+ },
+ Prefix: ipp("10.0.0.0/24"),
+ Node: types.Node{
+ MachineKey: machineKeys[0],
+ },
+ IsPrimary: true,
+ },
+ routes: types.Routes{
+ types.Route{
+ Model: gorm.Model{
+ ID: 1,
+ },
+ Prefix: ipp("10.0.0.0/24"),
+ Node: types.Node{
+ MachineKey: machineKeys[0],
+ },
+ IsPrimary: true,
+ },
+ // Offline
+ types.Route{
+ Model: gorm.Model{
+ ID: 2,
+ },
+ Prefix: ipp("10.0.0.0/24"),
+ Node: types.Node{
+ MachineKey: machineKeys[3],
+ },
+ IsPrimary: false,
+ },
+ },
+ want: nil,
+ wantErr: false,
+ },
+ {
+ name: "failover-primary-one-not-online",
+ failingRoute: types.Route{
+ Model: gorm.Model{
+ ID: 1,
+ },
+ Prefix: ipp("10.0.0.0/24"),
+ Node: types.Node{
+ MachineKey: machineKeys[0],
+ },
+ IsPrimary: true,
+ },
+ routes: types.Routes{
+ types.Route{
+ Model: gorm.Model{
+ ID: 1,
+ },
+ Prefix: ipp("10.0.0.0/24"),
+ Node: types.Node{
+ MachineKey: machineKeys[0],
+ },
+ IsPrimary: true,
+ },
+ // Offline
+ types.Route{
+ Model: gorm.Model{
+ ID: 2,
+ },
+ Prefix: ipp("10.0.0.0/24"),
+ Node: types.Node{
+ MachineKey: machineKeys[3],
+ },
+ IsPrimary: false,
+ },
+ types.Route{
+ Model: gorm.Model{
+ ID: 3,
+ },
+ Prefix: ipp("10.0.0.0/24"),
+ Node: types.Node{
+ MachineKey: machineKeys[1],
+ },
+ IsPrimary: true,
+ },
+ },
+ want: []key.MachinePublic{
+ machineKeys[0],
+ machineKeys[1],
+ },
+ wantErr: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tmpDir, err := os.MkdirTemp("", "failover-db-test")
+ assert.NoError(t, err)
+
+ notif := notifier.NewNotifier()
+
+ db, err = NewHeadscaleDatabase(
+ "sqlite3",
+ tmpDir+"/headscale_test.db",
+ false,
+ notif,
+ []netip.Prefix{
+ netip.MustParsePrefix("10.27.0.0/23"),
+ },
+ "",
+ )
+ assert.NoError(t, err)
+
+ // Pretend that all the nodes are connected to control
+ for idx, key := range machineKeys {
+ // Pretend one node is offline
+ if idx == 3 {
+ continue
+ }
+
+ notif.AddNode(key, sink)
+ }
+
+ for _, route := range tt.routes {
+ if err := db.db.Save(&route).Error; err != nil {
+ t.Fatalf("failed to create route: %s", err)
+ }
+ }
+
+ got, err := db.failoverRoute(&tt.failingRoute)
+
+ if (err != nil) != tt.wantErr {
+ t.Errorf("failoverRoute() error = %v, wantErr %v", err, tt.wantErr)
+
+ return
+ }
+
+ if diff := cmp.Diff(tt.want, got, util.Comparers...); diff != "" {
+ t.Errorf("failoverRoute() unexpected result (-want +got):\n%s", diff)
+ }
+ })
+ }
+}
diff --git a/hscontrol/db/suite_test.go b/hscontrol/db/suite_test.go
index ff1a095..1c38491 100644
--- a/hscontrol/db/suite_test.go
+++ b/hscontrol/db/suite_test.go
@@ -1,6 +1,7 @@
package db
import (
+ "log"
"net/netip"
"os"
"testing"
@@ -27,19 +28,22 @@ func (s *Suite) SetUpTest(c *check.C) {
}
func (s *Suite) TearDownTest(c *check.C) {
- os.RemoveAll(tmpDir)
+ // os.RemoveAll(tmpDir)
}
func (s *Suite) ResetDB(c *check.C) {
- if len(tmpDir) != 0 {
- os.RemoveAll(tmpDir)
- }
+ // if len(tmpDir) != 0 {
+ // os.RemoveAll(tmpDir)
+ // }
+
var err error
- tmpDir, err = os.MkdirTemp("", "autoygg-client-test")
+ tmpDir, err = os.MkdirTemp("", "headscale-db-test-*")
if err != nil {
c.Fatal(err)
}
+ log.Printf("database path: %s", tmpDir+"/headscale_test.db")
+
db, err = NewHeadscaleDatabase(
"sqlite3",
tmpDir+"/headscale_test.db",
diff --git a/hscontrol/db/users_test.go b/hscontrol/db/users_test.go
index 0c43b97..1ca3b49 100644
--- a/hscontrol/db/users_test.go
+++ b/hscontrol/db/users_test.go
@@ -48,9 +48,6 @@ func (s *Suite) TestDestroyUserErrors(c *check.C) {
node := types.Node{
ID: 0,
- MachineKey: "foo",
- NodeKey: "bar",
- DiscoKey: "faa",
Hostname: "testnode",
UserID: user.ID,
RegisterMethod: util.RegisterMethodAuthKey,
@@ -103,9 +100,6 @@ func (s *Suite) TestSetMachineUser(c *check.C) {
node := types.Node{
ID: 0,
- MachineKey: "foo",
- NodeKey: "bar",
- DiscoKey: "faa",
Hostname: "testnode",
UserID: oldUser.ID,
RegisterMethod: util.RegisterMethodAuthKey,
diff --git a/hscontrol/derp/server/derp_server.go b/hscontrol/derp/server/derp_server.go
index d59966b..59e4028 100644
--- a/hscontrol/derp/server/derp_server.go
+++ b/hscontrol/derp/server/derp_server.go
@@ -13,6 +13,7 @@ import (
"time"
"github.com/juanfont/headscale/hscontrol/types"
+ "github.com/juanfont/headscale/hscontrol/util"
"github.com/rs/zerolog/log"
"tailscale.com/derp"
"tailscale.com/net/stun"
@@ -39,7 +40,7 @@ func NewDERPServer(
cfg *types.DERPConfig,
) (*DERPServer, error) {
log.Trace().Caller().Msg("Creating new embedded DERP server")
- server := derp.NewServer(derpKey, log.Debug().Msgf) // nolint // zerolinter complains
+ server := derp.NewServer(derpKey, util.TSLogfWrapper()) // nolint // zerolinter complains
return &DERPServer{
serverURL: serverURL,
diff --git a/hscontrol/grpcv1.go b/hscontrol/grpcv1.go
index e04e3b1..ffd3a57 100644
--- a/hscontrol/grpcv1.go
+++ b/hscontrol/grpcv1.go
@@ -172,12 +172,18 @@ func (api headscaleV1APIServer) RegisterNode(
) (*v1.RegisterNodeResponse, error) {
log.Trace().
Str("user", request.GetUser()).
- Str("node_key", request.GetKey()).
+ Str("machine_key", request.GetKey()).
Msg("Registering node")
+ var mkey key.MachinePublic
+ err := mkey.UnmarshalText([]byte(request.GetKey()))
+ if err != nil {
+ return nil, err
+ }
+
node, err := api.h.db.RegisterNodeFromAuthCallback(
api.h.registrationCache,
- request.GetKey(),
+ mkey,
request.GetUser(),
nil,
util.RegisterMethodCLI,
@@ -198,7 +204,13 @@ func (api headscaleV1APIServer) GetNode(
return nil, err
}
- return &v1.GetNodeResponse{Node: node.Proto()}, nil
+ resp := node.Proto()
+
+ // Populate the online field based on
+ // currently connected nodes.
+ resp.Online = api.h.nodeNotifier.IsConnected(node.MachineKey)
+
+ return &v1.GetNodeResponse{Node: resp}, nil
}
func (api headscaleV1APIServer) SetTags(
@@ -327,7 +339,13 @@ func (api headscaleV1APIServer) ListNodes(
response := make([]*v1.Node, len(nodes))
for index, node := range nodes {
- response[index] = node.Proto()
+ resp := node.Proto()
+
+ // Populate the online field based on
+ // currently connected nodes.
+ resp.Online = api.h.nodeNotifier.IsConnected(node.MachineKey)
+
+ response[index] = resp
}
return &v1.ListNodesResponse{Nodes: response}, nil
@@ -340,13 +358,18 @@ func (api headscaleV1APIServer) ListNodes(
response := make([]*v1.Node, len(nodes))
for index, node := range nodes {
- m := node.Proto()
+ resp := node.Proto()
+
+ // Populate the online field based on
+ // currently connected nodes.
+ resp.Online = api.h.nodeNotifier.IsConnected(node.MachineKey)
+
validTags, invalidTags := api.h.ACLPolicy.TagsOfNode(
&node,
)
- m.InvalidTags = invalidTags
- m.ValidTags = validTags
- response[index] = m
+ resp.InvalidTags = invalidTags
+ resp.ValidTags = validTags
+ response[index] = resp
}
return &v1.ListNodesResponse{Nodes: response}, nil
@@ -521,13 +544,22 @@ func (api headscaleV1APIServer) DebugCreateNode(
Hostname: "DebugTestNode",
}
- givenName, err := api.h.db.GenerateGivenName(request.GetKey(), request.GetName())
+ var mkey key.MachinePublic
+ err = mkey.UnmarshalText([]byte(request.GetKey()))
if err != nil {
return nil, err
}
+ givenName, err := api.h.db.GenerateGivenName(mkey, request.GetName())
+ if err != nil {
+ return nil, err
+ }
+
+ nodeKey := key.NewNode()
+
newNode := types.Node{
- MachineKey: request.GetKey(),
+ MachineKey: mkey,
+ NodeKey: nodeKey.Public(),
Hostname: request.GetName(),
GivenName: givenName,
User: *user,
@@ -535,17 +567,15 @@ func (api headscaleV1APIServer) DebugCreateNode(
Expiry: &time.Time{},
LastSeen: &time.Time{},
- HostInfo: types.HostInfo(hostinfo),
+ Hostinfo: &hostinfo,
}
- nodeKey := key.NodePublic{}
- err = nodeKey.UnmarshalText([]byte(request.GetKey()))
- if err != nil {
- log.Panic().Msg("can not add node for debug. invalid node key")
- }
+ log.Debug().
+ Str("machine_key", mkey.ShortString()).
+ Msg("adding debug machine via CLI, appending to registration cache")
api.h.registrationCache.Set(
- util.NodePublicKeyStripPrefix(nodeKey),
+ mkey.String(),
newNode,
registerCacheExpiration,
)
diff --git a/hscontrol/handler_legacy.go b/hscontrol/handler_legacy.go
deleted file mode 100644
index bb94b1e..0000000
--- a/hscontrol/handler_legacy.go
+++ /dev/null
@@ -1,15 +0,0 @@
-//go:build ts2019
-
-package hscontrol
-
-import (
- "net/http"
-
- "github.com/gorilla/mux"
-)
-
-func (h *Headscale) addLegacyHandlers(router *mux.Router) {
- router.HandleFunc("/machine/{mkey}/map", h.PollNetMapHandler).
- Methods(http.MethodPost)
- router.HandleFunc("/machine/{mkey}", h.RegistrationHandler).Methods(http.MethodPost)
-}
diff --git a/hscontrol/handler_placeholder.go b/hscontrol/handler_placeholder.go
deleted file mode 100644
index 73d17c4..0000000
--- a/hscontrol/handler_placeholder.go
+++ /dev/null
@@ -1,8 +0,0 @@
-//go:build !ts2019
-
-package hscontrol
-
-import "github.com/gorilla/mux"
-
-func (h *Headscale) addLegacyHandlers(router *mux.Router) {
-}
diff --git a/hscontrol/handlers.go b/hscontrol/handlers.go
index 7f3b23c..ee67073 100644
--- a/hscontrol/handlers.go
+++ b/hscontrol/handlers.go
@@ -11,7 +11,6 @@ import (
"time"
"github.com/gorilla/mux"
- "github.com/juanfont/headscale/hscontrol/util"
"github.com/rs/zerolog/log"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
@@ -63,26 +62,6 @@ func (h *Headscale) KeyHandler(
// New Tailscale clients send a 'v' parameter to indicate the CurrentCapabilityVersion
capVer, err := parseCabailityVersion(req)
if err != nil {
- if errors.Is(err, ErrNoCapabilityVersion) {
- log.Debug().
- Str("handler", "/key").
- Msg("New legacy client")
- // Old clients don't send a 'v' parameter, so we send the legacy public key
- writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
- writer.WriteHeader(http.StatusOK)
- _, err := writer.Write(
- []byte(util.MachinePublicKeyStripPrefix(h.privateKey2019.Public())),
- )
- if err != nil {
- log.Error().
- Caller().
- Err(err).
- Msg("Failed to write response")
- }
-
- return
- }
-
log.Error().
Caller().
Err(err).
@@ -101,7 +80,7 @@ func (h *Headscale) KeyHandler(
log.Debug().
Str("handler", "/key").
- Int("v", int(capVer)).
+ Int("cap_ver", int(capVer)).
Msg("New noise client")
if err != nil {
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
@@ -120,8 +99,7 @@ func (h *Headscale) KeyHandler(
// TS2021 (Tailscale v2 protocol) requires to have a different key
if capVer >= NoiseCapabilityVersion {
resp := tailcfg.OverTLSPublicKeyResponse{
- LegacyPublicKey: h.privateKey2019.Public(),
- PublicKey: h.noisePrivateKey.Public(),
+ PublicKey: h.noisePrivateKey.Public(),
}
writer.Header().Set("Content-Type", "application/json")
writer.WriteHeader(http.StatusOK)
@@ -206,33 +184,16 @@ func (h *Headscale) RegisterWebAPI(
req *http.Request,
) {
vars := mux.Vars(req)
- nodeKeyStr, ok := vars["nkey"]
-
- if !util.NodePublicKeyRegex.Match([]byte(nodeKeyStr)) {
- log.Warn().Str("node_key", nodeKeyStr).Msg("Invalid node key passed to registration url")
-
- writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
- writer.WriteHeader(http.StatusUnauthorized)
- _, err := writer.Write([]byte("Unauthorized"))
- if err != nil {
- log.Error().
- Caller().
- Err(err).
- Msg("Failed to write response")
- }
-
- return
- }
+ machineKeyStr := vars["mkey"]
// 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 nodeKey key.NodePublic
- err := nodeKey.UnmarshalText(
- []byte(util.NodePublicKeyEnsurePrefix(nodeKeyStr)),
+ var machineKey key.MachinePublic
+ err := machineKey.UnmarshalText(
+ []byte(machineKeyStr),
)
-
- if !ok || nodeKeyStr == "" || err != nil {
+ if err != nil {
log.Warn().Err(err).Msg("Failed to parse incoming nodekey")
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
@@ -250,7 +211,7 @@ func (h *Headscale) RegisterWebAPI(
var content bytes.Buffer
if err := registerWebAPITemplate.Execute(&content, registerWebAPITemplateConfig{
- Key: nodeKeyStr,
+ Key: machineKey.String(),
}); err != nil {
log.Error().
Str("func", "RegisterWebAPI").
diff --git a/hscontrol/mapper/mapper.go b/hscontrol/mapper/mapper.go
index ed997df..d6404ce 100644
--- a/hscontrol/mapper/mapper.go
+++ b/hscontrol/mapper/mapper.go
@@ -8,6 +8,7 @@ import (
"net/url"
"os"
"path"
+ "slices"
"sort"
"strings"
"sync"
@@ -20,12 +21,11 @@ import (
"github.com/juanfont/headscale/hscontrol/util"
"github.com/klauspost/compress/zstd"
"github.com/rs/zerolog/log"
- "github.com/samber/lo"
+ "golang.org/x/exp/maps"
"tailscale.com/envknob"
"tailscale.com/smallzstd"
"tailscale.com/tailcfg"
"tailscale.com/types/dnstype"
- "tailscale.com/types/key"
)
const (
@@ -46,12 +46,9 @@ var debugDumpMapResponsePath = envknob.String("HEADSCALE_DEBUG_DUMP_MAPRESPONSE_
// - Keep information about the previous mapresponse so we can send a diff
// - Store hashes
// - Create a "minifier" that removes info not needed for the node
+// - some sort of batching, wait for 5 or 60 seconds before sending
type Mapper struct {
- privateKey2019 *key.MachinePrivate
- isNoise bool
- capVer tailcfg.CapabilityVersion
-
// Configuration
// TODO(kradalby): figure out if this is the format we want this in
derpMap *tailcfg.DERPMap
@@ -66,16 +63,19 @@ type Mapper struct {
// Map isnt concurrency safe, so we need to ensure
// only one func is accessing it over time.
- mu sync.Mutex
- peers map[uint64]*types.Node
+ mu sync.Mutex
+ peers map[uint64]*types.Node
+ patches map[uint64][]patch
+}
+
+type patch struct {
+ timestamp time.Time
+ change *tailcfg.PeerChange
}
func NewMapper(
node *types.Node,
peers types.Nodes,
- privateKey *key.MachinePrivate,
- isNoise bool,
- capVer tailcfg.CapabilityVersion,
derpMap *tailcfg.DERPMap,
baseDomain string,
dnsCfg *tailcfg.DNSConfig,
@@ -84,17 +84,12 @@ func NewMapper(
) *Mapper {
log.Debug().
Caller().
- Bool("noise", isNoise).
Str("node", node.Hostname).
Msg("creating new mapper")
uid, _ := util.GenerateRandomStringDNSSafe(mapperIDLength)
return &Mapper{
- privateKey2019: privateKey,
- isNoise: isNoise,
- capVer: capVer,
-
derpMap: derpMap,
baseDomain: baseDomain,
dnsCfg: dnsCfg,
@@ -106,7 +101,8 @@ func NewMapper(
seq: 0,
// TODO: populate
- peers: peers.IDMap(),
+ peers: peers.IDMap(),
+ patches: make(map[uint64][]patch),
}
}
@@ -195,7 +191,7 @@ func addNextDNSMetadata(resolvers []*dnstype.Resolver, node *types.Node) {
if strings.HasPrefix(resolver.Addr, nextDNSDoHPrefix) {
attrs := url.Values{
"device_name": []string{node.Hostname},
- "device_model": []string{node.HostInfo.OS},
+ "device_model": []string{node.Hostinfo.OS},
}
if len(node.IPAddresses) > 0 {
@@ -212,10 +208,11 @@ func addNextDNSMetadata(resolvers []*dnstype.Resolver, node *types.Node) {
func (m *Mapper) fullMapResponse(
node *types.Node,
pol *policy.ACLPolicy,
+ capVer tailcfg.CapabilityVersion,
) (*tailcfg.MapResponse, error) {
peers := nodeMapToList(m.peers)
- resp, err := m.baseWithConfigMapResponse(node, pol)
+ resp, err := m.baseWithConfigMapResponse(node, pol, capVer)
if err != nil {
return nil, err
}
@@ -224,7 +221,7 @@ func (m *Mapper) fullMapResponse(
resp,
pol,
node,
- m.capVer,
+ capVer,
peers,
peers,
m.baseDomain,
@@ -247,13 +244,22 @@ func (m *Mapper) FullMapResponse(
m.mu.Lock()
defer m.mu.Unlock()
- resp, err := m.fullMapResponse(node, pol)
- if err != nil {
- return nil, err
+ peers := maps.Keys(m.peers)
+ peersWithPatches := maps.Keys(m.patches)
+ slices.Sort(peers)
+ slices.Sort(peersWithPatches)
+
+ if len(peersWithPatches) > 0 {
+ log.Debug().
+ Str("node", node.Hostname).
+ Uints64("peers", peers).
+ Uints64("pending_patches", peersWithPatches).
+ Msgf("node requested full map response, but has pending patches")
}
- if m.isNoise {
- return m.marshalMapResponse(mapRequest, resp, node, mapRequest.Compress)
+ resp, err := m.fullMapResponse(node, pol, mapRequest.Version)
+ if err != nil {
+ return nil, err
}
return m.marshalMapResponse(mapRequest, resp, node, mapRequest.Compress)
@@ -267,15 +273,11 @@ func (m *Mapper) LiteMapResponse(
node *types.Node,
pol *policy.ACLPolicy,
) ([]byte, error) {
- resp, err := m.baseWithConfigMapResponse(node, pol)
+ resp, err := m.baseWithConfigMapResponse(node, pol, mapRequest.Version)
if err != nil {
return nil, err
}
- if m.isNoise {
- return m.marshalMapResponse(mapRequest, resp, node, mapRequest.Compress)
- }
-
return m.marshalMapResponse(mapRequest, resp, node, mapRequest.Compress)
}
@@ -292,10 +294,12 @@ func (m *Mapper) KeepAliveResponse(
func (m *Mapper) DERPMapResponse(
mapRequest tailcfg.MapRequest,
node *types.Node,
- derpMap tailcfg.DERPMap,
+ derpMap *tailcfg.DERPMap,
) ([]byte, error) {
+ m.derpMap = derpMap
+
resp := m.baseMapResponse()
- resp.DERPMap = &derpMap
+ resp.DERPMap = derpMap
return m.marshalMapResponse(mapRequest, &resp, node, mapRequest.Compress)
}
@@ -305,18 +309,29 @@ func (m *Mapper) PeerChangedResponse(
node *types.Node,
changed types.Nodes,
pol *policy.ACLPolicy,
+ messages ...string,
) ([]byte, error) {
m.mu.Lock()
defer m.mu.Unlock()
- lastSeen := make(map[tailcfg.NodeID]bool)
-
// Update our internal map.
for _, node := range changed {
- m.peers[node.ID] = node
+ if patches, ok := m.patches[node.ID]; ok {
+ // preserve online status in case the patch has an outdated one
+ online := node.IsOnline
- // We have just seen the node, let the peers update their list.
- lastSeen[tailcfg.NodeID(node.ID)] = true
+ for _, p := range patches {
+ // TODO(kradalby): Figure if this needs to be sorted by timestamp
+ node.ApplyPeerChange(p.change)
+ }
+
+ // Ensure the patches are not applied again later
+ delete(m.patches, node.ID)
+
+ node.IsOnline = online
+ }
+
+ m.peers[node.ID] = node
}
resp := m.baseMapResponse()
@@ -325,7 +340,7 @@ func (m *Mapper) PeerChangedResponse(
&resp,
pol,
node,
- m.capVer,
+ mapRequest.Version,
nodeMapToList(m.peers),
changed,
m.baseDomain,
@@ -336,11 +351,55 @@ func (m *Mapper) PeerChangedResponse(
return nil, err
}
- // resp.PeerSeenChange = lastSeen
+ return m.marshalMapResponse(mapRequest, &resp, node, mapRequest.Compress, messages...)
+}
+
+// PeerChangedPatchResponse creates a patch MapResponse with
+// incoming update from a state change.
+func (m *Mapper) PeerChangedPatchResponse(
+ mapRequest tailcfg.MapRequest,
+ node *types.Node,
+ changed []*tailcfg.PeerChange,
+ pol *policy.ACLPolicy,
+) ([]byte, error) {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+
+ sendUpdate := false
+ // patch the internal map
+ for _, change := range changed {
+ if peer, ok := m.peers[uint64(change.NodeID)]; ok {
+ peer.ApplyPeerChange(change)
+ sendUpdate = true
+ } else {
+ log.Trace().Str("node", node.Hostname).Msgf("Node with ID %s is missing from mapper for Node %s, saving patch for when node is available", change.NodeID, node.Hostname)
+
+ p := patch{
+ timestamp: time.Now(),
+ change: change,
+ }
+
+ if patches, ok := m.patches[uint64(change.NodeID)]; ok {
+ patches := append(patches, p)
+
+ m.patches[uint64(change.NodeID)] = patches
+ } else {
+ m.patches[uint64(change.NodeID)] = []patch{p}
+ }
+ }
+ }
+
+ if !sendUpdate {
+ return nil, nil
+ }
+
+ resp := m.baseMapResponse()
+ resp.PeersChangedPatch = changed
return m.marshalMapResponse(mapRequest, &resp, node, mapRequest.Compress)
}
+// TODO(kradalby): We need some integration tests for this.
func (m *Mapper) PeerRemovedResponse(
mapRequest tailcfg.MapRequest,
node *types.Node,
@@ -349,13 +408,23 @@ func (m *Mapper) PeerRemovedResponse(
m.mu.Lock()
defer m.mu.Unlock()
+ // Some nodes might have been removed already
+ // so we dont want to ask downstream to remove
+ // twice, than can cause a panic in tailscaled.
+ notYetRemoved := []tailcfg.NodeID{}
+
// remove from our internal map
for _, id := range removed {
+ if _, ok := m.peers[uint64(id)]; ok {
+ notYetRemoved = append(notYetRemoved, id)
+ }
+
delete(m.peers, uint64(id))
+ delete(m.patches, uint64(id))
}
resp := m.baseMapResponse()
- resp.PeersRemoved = removed
+ resp.PeersRemoved = notYetRemoved
return m.marshalMapResponse(mapRequest, &resp, node, mapRequest.Compress)
}
@@ -365,20 +434,10 @@ func (m *Mapper) marshalMapResponse(
resp *tailcfg.MapResponse,
node *types.Node,
compression string,
+ messages ...string,
) ([]byte, error) {
atomic.AddUint64(&m.seq, 1)
- var machineKey key.MachinePublic
- err := machineKey.UnmarshalText([]byte(util.MachinePublicKeyEnsurePrefix(node.MachineKey)))
- if err != nil {
- log.Error().
- Caller().
- Err(err).
- Msg("Cannot parse client key")
-
- return nil, err
- }
-
jsonBody, err := json.Marshal(resp)
if err != nil {
log.Error().
@@ -389,11 +448,27 @@ func (m *Mapper) marshalMapResponse(
if debugDumpMapResponsePath != "" {
data := map[string]interface{}{
+ "Messages": messages,
"MapRequest": mapRequest,
"MapResponse": resp,
}
- body, err := json.Marshal(data)
+ responseType := "keepalive"
+
+ switch {
+ case resp.Peers != nil && len(resp.Peers) > 0:
+ responseType = "full"
+ case resp.Peers == nil && resp.PeersChanged == nil && resp.PeersChangedPatch == nil:
+ responseType = "lite"
+ case resp.PeersChanged != nil && len(resp.PeersChanged) > 0:
+ responseType = "changed"
+ case resp.PeersChangedPatch != nil && len(resp.PeersChangedPatch) > 0:
+ responseType = "patch"
+ case resp.PeersRemoved != nil && len(resp.PeersRemoved) > 0:
+ responseType = "removed"
+ }
+
+ body, err := json.MarshalIndent(data, "", " ")
if err != nil {
log.Error().
Caller().
@@ -412,7 +487,7 @@ func (m *Mapper) marshalMapResponse(
mapResponsePath := path.Join(
mPath,
- fmt.Sprintf("%d-%s-%d.json", now, m.uid, atomic.LoadUint64(&m.seq)),
+ fmt.Sprintf("%d-%s-%d-%s.json", now, m.uid, atomic.LoadUint64(&m.seq), responseType),
)
log.Trace().Msgf("Writing MapResponse to %s", mapResponsePath)
@@ -425,15 +500,8 @@ func (m *Mapper) marshalMapResponse(
var respBody []byte
if compression == util.ZstdCompression {
respBody = zstdEncode(jsonBody)
- if !m.isNoise { // if legacy protocol
- respBody = m.privateKey2019.SealTo(machineKey, respBody)
- }
} else {
- if !m.isNoise { // if legacy protocol
- respBody = m.privateKey2019.SealTo(machineKey, jsonBody)
- } else {
- respBody = jsonBody
- }
+ respBody = jsonBody
}
data := make([]byte, reservedResponseHeaderSize)
@@ -443,32 +511,6 @@ func (m *Mapper) marshalMapResponse(
return data, nil
}
-// MarshalResponse takes an Tailscale Response, marhsal it to JSON.
-// If isNoise is set, then the JSON body will be returned
-// If !isNoise and privateKey2019 is set, the JSON body will be sealed in a Nacl box.
-func MarshalResponse(
- resp interface{},
- isNoise bool,
- privateKey2019 *key.MachinePrivate,
- machineKey key.MachinePublic,
-) ([]byte, error) {
- jsonBody, err := json.Marshal(resp)
- if err != nil {
- log.Error().
- Caller().
- Err(err).
- Msg("Cannot marshal response")
-
- return nil, err
- }
-
- if !isNoise && privateKey2019 != nil {
- return privateKey2019.SealTo(machineKey, jsonBody), nil
- }
-
- return jsonBody, nil
-}
-
func zstdEncode(in []byte) []byte {
encoder, ok := zstdEncoderPool.Get().(*zstd.Encoder)
if !ok {
@@ -502,6 +544,7 @@ func (m *Mapper) baseMapResponse() tailcfg.MapResponse {
resp := tailcfg.MapResponse{
KeepAlive: false,
ControlTime: &now,
+ // TODO(kradalby): Implement PingRequest?
}
return resp
@@ -514,10 +557,11 @@ func (m *Mapper) baseMapResponse() tailcfg.MapResponse {
func (m *Mapper) baseWithConfigMapResponse(
node *types.Node,
pol *policy.ACLPolicy,
+ capVer tailcfg.CapabilityVersion,
) (*tailcfg.MapResponse, error) {
resp := m.baseMapResponse()
- tailnode, err := tailNode(node, m.capVer, pol, m.dnsCfg, m.baseDomain, m.randomClientPort)
+ tailnode, err := tailNode(node, capVer, pol, m.dnsCfg, m.baseDomain, m.randomClientPort)
if err != nil {
return nil, err
}
@@ -550,15 +594,6 @@ func nodeMapToList(nodes map[uint64]*types.Node) types.Nodes {
return ret
}
-func filterExpiredAndNotReady(peers types.Nodes) types.Nodes {
- return lo.Filter(peers, func(item *types.Node, index int) bool {
- // Filter out nodes that are expired OR
- // nodes that has no endpoints, this typically means they have
- // registered, but are not configured.
- return !item.IsExpired() || len(item.Endpoints) > 0
- })
-}
-
// appendPeerChanges mutates a tailcfg.MapResponse with all the
// necessary changes when peers have changed.
func appendPeerChanges(
@@ -584,9 +619,6 @@ func appendPeerChanges(
return err
}
- // Filter out peers that have expired.
- changed = filterExpiredAndNotReady(changed)
-
// If there are filter rules present, see if there are any nodes that cannot
// access eachother at all and remove them from the peers.
if len(rules) > 0 {
@@ -622,8 +654,5 @@ func appendPeerChanges(
resp.UserProfiles = profiles
resp.SSHPolicy = sshPolicy
- // TODO(kradalby): This currently does not take last seen in keepalives into account
- resp.OnlineChange = peers.OnlineNodeMap()
-
return nil
}
diff --git a/hscontrol/mapper/mapper_test.go b/hscontrol/mapper/mapper_test.go
index 8270fc3..bcc17dd 100644
--- a/hscontrol/mapper/mapper_test.go
+++ b/hscontrol/mapper/mapper_test.go
@@ -166,10 +166,16 @@ func Test_fullMapResponse(t *testing.T) {
expire := time.Date(2500, time.November, 11, 23, 0, 0, 0, time.UTC)
mini := &types.Node{
- ID: 0,
- MachineKey: "mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
- NodeKey: "nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
- DiscoKey: "discokey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
+ ID: 0,
+ MachineKey: mustMK(
+ "mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
+ ),
+ NodeKey: mustNK(
+ "nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
+ ),
+ DiscoKey: mustDK(
+ "discokey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
+ ),
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")},
Hostname: "mini",
GivenName: "mini",
@@ -180,8 +186,7 @@ func Test_fullMapResponse(t *testing.T) {
AuthKey: &types.PreAuthKey{},
LastSeen: &lastSeen,
Expiry: &expire,
- HostInfo: types.HostInfo{},
- Endpoints: []string{},
+ Hostinfo: &tailcfg.Hostinfo{},
Routes: []types.Route{
{
Prefix: types.IPPrefix(netip.MustParsePrefix("0.0.0.0/0")),
@@ -226,14 +231,12 @@ func Test_fullMapResponse(t *testing.T) {
netip.MustParsePrefix("0.0.0.0/0"),
netip.MustParsePrefix("192.168.0.0/24"),
},
- Endpoints: []string{},
DERP: "127.3.3.40:0",
Hostinfo: hiview(tailcfg.Hostinfo{}),
Created: created,
Tags: []string{},
PrimaryRoutes: []netip.Prefix{netip.MustParsePrefix("192.168.0.0/24")},
LastSeen: &lastSeen,
- Online: new(bool),
MachineAuthorized: true,
Capabilities: []tailcfg.NodeCapability{
tailcfg.CapabilityFileSharing,
@@ -244,10 +247,16 @@ func Test_fullMapResponse(t *testing.T) {
}
peer1 := &types.Node{
- ID: 1,
- MachineKey: "mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
- NodeKey: "nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
- DiscoKey: "discokey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
+ ID: 1,
+ MachineKey: mustMK(
+ "mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
+ ),
+ NodeKey: mustNK(
+ "nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
+ ),
+ DiscoKey: mustDK(
+ "discokey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
+ ),
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.2")},
Hostname: "peer1",
GivenName: "peer1",
@@ -256,8 +265,7 @@ func Test_fullMapResponse(t *testing.T) {
ForcedTags: []string{},
LastSeen: &lastSeen,
Expiry: &expire,
- HostInfo: types.HostInfo{},
- Endpoints: []string{},
+ Hostinfo: &tailcfg.Hostinfo{},
Routes: []types.Route{},
CreatedAt: created,
}
@@ -278,14 +286,12 @@ 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")},
- Endpoints: []string{},
DERP: "127.3.3.40:0",
Hostinfo: hiview(tailcfg.Hostinfo{}),
Created: created,
Tags: []string{},
PrimaryRoutes: []netip.Prefix{},
LastSeen: &lastSeen,
- Online: new(bool),
MachineAuthorized: true,
Capabilities: []tailcfg.NodeCapability{
tailcfg.CapabilityFileSharing,
@@ -296,10 +302,16 @@ func Test_fullMapResponse(t *testing.T) {
}
peer2 := &types.Node{
- ID: 2,
- MachineKey: "mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
- NodeKey: "nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
- DiscoKey: "discokey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
+ ID: 2,
+ MachineKey: mustMK(
+ "mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
+ ),
+ NodeKey: mustNK(
+ "nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
+ ),
+ DiscoKey: mustDK(
+ "discokey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
+ ),
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.3")},
Hostname: "peer2",
GivenName: "peer2",
@@ -308,8 +320,7 @@ func Test_fullMapResponse(t *testing.T) {
ForcedTags: []string{},
LastSeen: &lastSeen,
Expiry: &expire,
- HostInfo: types.HostInfo{},
- Endpoints: []string{},
+ Hostinfo: &tailcfg.Hostinfo{},
Routes: []types.Route{},
CreatedAt: created,
}
@@ -387,7 +398,6 @@ func Test_fullMapResponse(t *testing.T) {
DNSConfig: &tailcfg.DNSConfig{},
Domain: "",
CollectServices: "false",
- OnlineChange: map[tailcfg.NodeID]bool{tailPeer1.ID: false},
PacketFilter: []tailcfg.FilterRule{},
UserProfiles: []tailcfg.UserProfile{{LoginName: "mini", DisplayName: "mini"}},
SSHPolicy: &tailcfg.SSHPolicy{Rules: []*tailcfg.SSHRule{}},
@@ -429,10 +439,6 @@ func Test_fullMapResponse(t *testing.T) {
DNSConfig: &tailcfg.DNSConfig{},
Domain: "",
CollectServices: "false",
- OnlineChange: map[tailcfg.NodeID]bool{
- tailPeer1.ID: false,
- tailcfg.NodeID(peer2.ID): false,
- },
PacketFilter: []tailcfg.FilterRule{
{
SrcIPs: []string{"100.64.0.2/32"},
@@ -459,9 +465,6 @@ func Test_fullMapResponse(t *testing.T) {
mappy := NewMapper(
tt.node,
tt.peers,
- nil,
- false,
- 0,
tt.derpMap,
tt.baseDomain,
tt.dnsConfig,
@@ -472,6 +475,7 @@ func Test_fullMapResponse(t *testing.T) {
got, err := mappy.fullMapResponse(
tt.node,
tt.pol,
+ 0,
)
if (err != nil) != tt.wantErr {
diff --git a/hscontrol/mapper/tail.go b/hscontrol/mapper/tail.go
index 13958c9..e213a95 100644
--- a/hscontrol/mapper/tail.go
+++ b/hscontrol/mapper/tail.go
@@ -52,21 +52,6 @@ func tailNode(
baseDomain string,
randomClientPort bool,
) (*tailcfg.Node, error) {
- nodeKey, err := node.NodePublicKey()
- if err != nil {
- return nil, err
- }
-
- machineKey, err := node.MachinePublicKey()
- if err != nil {
- return nil, err
- }
-
- discoKey, err := node.DiscoPublicKey()
- if err != nil {
- return nil, err
- }
-
addrs := node.IPAddresses.Prefixes()
allowedIPs := append(
@@ -87,8 +72,8 @@ func tailNode(
}
var derp string
- if node.HostInfo.NetInfo != nil {
- derp = fmt.Sprintf("127.3.3.40:%d", node.HostInfo.NetInfo.PreferredDERP)
+ if node.Hostinfo.NetInfo != nil {
+ derp = fmt.Sprintf("127.3.3.40:%d", node.Hostinfo.NetInfo.PreferredDERP)
} else {
derp = "127.3.3.40:0" // Zero means disconnected or unknown.
}
@@ -102,13 +87,9 @@ func tailNode(
hostname, err := node.GetFQDN(dnsConfig, baseDomain)
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("tailNode, failed to create FQDN: %s", err)
}
- hostInfo := node.GetHostInfo()
-
- online := node.IsOnline()
-
tags, _ := pol.TagsOfNode(node)
tags = lo.Uniq(append(tags, node.ForcedTags...))
@@ -118,28 +99,30 @@ func tailNode(
strconv.FormatUint(node.ID, util.Base10),
), // in headscale, unlike tailcontrol server, IDs are permanent
Name: hostname,
+ Cap: capVer,
User: tailcfg.UserID(node.UserID),
- Key: nodeKey,
+ Key: node.NodeKey,
KeyExpiry: keyExpiry,
- Machine: machineKey,
- DiscoKey: discoKey,
+ Machine: node.MachineKey,
+ DiscoKey: node.DiscoKey,
Addresses: addrs,
AllowedIPs: allowedIPs,
Endpoints: node.Endpoints,
DERP: derp,
- Hostinfo: hostInfo.View(),
+ Hostinfo: node.Hostinfo.View(),
Created: node.CreatedAt,
+ Online: node.IsOnline,
+
Tags: tags,
PrimaryRoutes: primaryPrefixes,
- LastSeen: node.LastSeen,
- Online: &online,
MachineAuthorized: !node.IsExpired(),
+ Expired: node.IsExpired(),
}
// - 74: 2023-09-18: Client understands NodeCapMap
@@ -170,5 +153,11 @@ func tailNode(
tNode.Capabilities = append(tNode.Capabilities, tailcfg.NodeAttrDisableUPnP)
}
+ if node.IsOnline == nil || !*node.IsOnline {
+ // LastSeen is only set when node is
+ // not connected to the control server.
+ tNode.LastSeen = node.LastSeen
+ }
+
return &tNode, nil
}
diff --git a/hscontrol/mapper/tail_test.go b/hscontrol/mapper/tail_test.go
index ced7537..f6e370c 100644
--- a/hscontrol/mapper/tail_test.go
+++ b/hscontrol/mapper/tail_test.go
@@ -53,21 +53,42 @@ func TestTailNode(t *testing.T) {
wantErr bool
}{
{
- name: "empty-node",
- node: &types.Node{},
+ name: "empty-node",
+ node: &types.Node{
+ Hostinfo: &tailcfg.Hostinfo{},
+ },
pol: &policy.ACLPolicy{},
dnsConfig: &tailcfg.DNSConfig{},
baseDomain: "",
- want: nil,
- wantErr: true,
+ want: &tailcfg.Node{
+ StableID: "0",
+ Addresses: []netip.Prefix{},
+ AllowedIPs: []netip.Prefix{},
+ DERP: "127.3.3.40:0",
+ Hostinfo: hiview(tailcfg.Hostinfo{}),
+ Tags: []string{},
+ PrimaryRoutes: []netip.Prefix{},
+ MachineAuthorized: true,
+ Capabilities: []tailcfg.NodeCapability{
+ "https://tailscale.com/cap/file-sharing", "https://tailscale.com/cap/is-admin",
+ "https://tailscale.com/cap/ssh", "debug-disable-upnp",
+ },
+ },
+ wantErr: false,
},
{
name: "minimal-node",
node: &types.Node{
- ID: 0,
- MachineKey: "mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
- NodeKey: "nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
- DiscoKey: "discokey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
+ ID: 0,
+ MachineKey: mustMK(
+ "mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
+ ),
+ NodeKey: mustNK(
+ "nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
+ ),
+ DiscoKey: mustDK(
+ "discokey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
+ ),
IPAddresses: []netip.Addr{
netip.MustParseAddr("100.64.0.1"),
},
@@ -82,8 +103,7 @@ func TestTailNode(t *testing.T) {
AuthKey: &types.PreAuthKey{},
LastSeen: &lastSeen,
Expiry: &expire,
- HostInfo: types.HostInfo{},
- Endpoints: []string{},
+ Hostinfo: &tailcfg.Hostinfo{},
Routes: []types.Route{
{
Prefix: types.IPPrefix(netip.MustParsePrefix("0.0.0.0/0")),
@@ -133,10 +153,9 @@ func TestTailNode(t *testing.T) {
netip.MustParsePrefix("0.0.0.0/0"),
netip.MustParsePrefix("192.168.0.0/24"),
},
- Endpoints: []string{},
- DERP: "127.3.3.40:0",
- Hostinfo: hiview(tailcfg.Hostinfo{}),
- Created: created,
+ DERP: "127.3.3.40:0",
+ Hostinfo: hiview(tailcfg.Hostinfo{}),
+ Created: created,
Tags: []string{},
@@ -145,7 +164,6 @@ func TestTailNode(t *testing.T) {
},
LastSeen: &lastSeen,
- Online: new(bool),
MachineAuthorized: true,
Capabilities: []tailcfg.NodeCapability{
diff --git a/hscontrol/notifier/notifier.go b/hscontrol/notifier/notifier.go
index 32f426a..77e8b19 100644
--- a/hscontrol/notifier/notifier.go
+++ b/hscontrol/notifier/notifier.go
@@ -1,11 +1,14 @@
package notifier
import (
+ "fmt"
+ "strings"
"sync"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/rs/zerolog/log"
+ "tailscale.com/types/key"
)
type Notifier struct {
@@ -17,9 +20,9 @@ func NewNotifier() *Notifier {
return &Notifier{}
}
-func (n *Notifier) AddNode(machineKey string, c chan<- types.StateUpdate) {
- log.Trace().Caller().Str("key", machineKey).Msg("acquiring lock to add node")
- defer log.Trace().Caller().Str("key", machineKey).Msg("releasing lock to add node")
+func (n *Notifier) AddNode(machineKey key.MachinePublic, c chan<- types.StateUpdate) {
+ log.Trace().Caller().Str("key", machineKey.ShortString()).Msg("acquiring lock to add node")
+ defer log.Trace().Caller().Str("key", machineKey.ShortString()).Msg("releasing lock to add node")
n.l.Lock()
defer n.l.Unlock()
@@ -28,17 +31,17 @@ func (n *Notifier) AddNode(machineKey string, c chan<- types.StateUpdate) {
n.nodes = make(map[string]chan<- types.StateUpdate)
}
- n.nodes[machineKey] = c
+ n.nodes[machineKey.String()] = c
log.Trace().
- Str("machine_key", machineKey).
+ Str("machine_key", machineKey.ShortString()).
Int("open_chans", len(n.nodes)).
Msg("Added new channel")
}
-func (n *Notifier) RemoveNode(machineKey string) {
- log.Trace().Caller().Str("key", machineKey).Msg("acquiring lock to remove node")
- defer log.Trace().Caller().Str("key", machineKey).Msg("releasing lock to remove node")
+func (n *Notifier) RemoveNode(machineKey key.MachinePublic) {
+ log.Trace().Caller().Str("key", machineKey.ShortString()).Msg("acquiring lock to remove node")
+ defer log.Trace().Caller().Str("key", machineKey.ShortString()).Msg("releasing lock to remove node")
n.l.Lock()
defer n.l.Unlock()
@@ -47,14 +50,27 @@ func (n *Notifier) RemoveNode(machineKey string) {
return
}
- delete(n.nodes, machineKey)
+ delete(n.nodes, machineKey.String())
log.Trace().
- Str("machine_key", machineKey).
+ Str("machine_key", machineKey.ShortString()).
Int("open_chans", len(n.nodes)).
Msg("Removed channel")
}
+// IsConnected reports if a node is connected to headscale and has a
+// poll session open.
+func (n *Notifier) IsConnected(machineKey key.MachinePublic) bool {
+ n.l.RLock()
+ defer n.l.RUnlock()
+
+ if _, ok := n.nodes[machineKey.String()]; ok {
+ return true
+ }
+
+ return false
+}
+
func (n *Notifier) NotifyAll(update types.StateUpdate) {
n.NotifyWithIgnore(update)
}
@@ -78,3 +94,31 @@ func (n *Notifier) NotifyWithIgnore(update types.StateUpdate, ignore ...string)
c <- update
}
}
+
+func (n *Notifier) NotifyByMachineKey(update types.StateUpdate, mKey key.MachinePublic) {
+ log.Trace().Caller().Interface("type", update.Type).Msg("acquiring lock to notify")
+ defer log.Trace().
+ Caller().
+ Interface("type", update.Type).
+ Msg("releasing lock, finished notifing")
+
+ n.l.RLock()
+ defer n.l.RUnlock()
+
+ if c, ok := n.nodes[mKey.String()]; ok {
+ c <- update
+ }
+}
+
+func (n *Notifier) String() string {
+ n.l.RLock()
+ defer n.l.RUnlock()
+
+ str := []string{"Notifier, in map:\n"}
+
+ for k, v := range n.nodes {
+ str = append(str, fmt.Sprintf("\t%s: %v\n", k, v))
+ }
+
+ return strings.Join(str, "")
+}
diff --git a/hscontrol/oidc.go b/hscontrol/oidc.go
index 30ef1c8..c678693 100644
--- a/hscontrol/oidc.go
+++ b/hscontrol/oidc.go
@@ -124,42 +124,28 @@ func (h *Headscale) determineTokenExpiration(idTokenExpiration time.Time) time.T
// 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 /oidc/register/:nKey.
+// Listens in /oidc/register/:mKey.
func (h *Headscale) RegisterOIDC(
writer http.ResponseWriter,
req *http.Request,
) {
vars := mux.Vars(req)
- nodeKeyStr, ok := vars["nkey"]
+ machineKeyStr, ok := vars["mkey"]
log.Debug().
Caller().
- Str("node_key", nodeKeyStr).
+ Str("machine_key", machineKeyStr).
Bool("ok", ok).
Msg("Received oidc register call")
- if !util.NodePublicKeyRegex.Match([]byte(nodeKeyStr)) {
- log.Warn().Str("node_key", nodeKeyStr).Msg("Invalid node key passed to registration url")
-
- writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
- writer.WriteHeader(http.StatusUnauthorized)
- _, err := writer.Write([]byte("Unauthorized"))
- if err != nil {
- util.LogErr(err, "Failed to write response")
- }
-
- return
- }
-
// 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 nodeKey key.NodePublic
- err := nodeKey.UnmarshalText(
- []byte(util.NodePublicKeyEnsurePrefix(nodeKeyStr)),
+ var machineKey key.MachinePublic
+ err := machineKey.UnmarshalText(
+ []byte(machineKeyStr),
)
-
- if !ok || nodeKeyStr == "" || err != nil {
+ if err != nil {
log.Warn().
Err(err).
Msg("Failed to parse incoming nodekey in OIDC registration")
@@ -188,7 +174,7 @@ func (h *Headscale) RegisterOIDC(
// place the node key into the state cache, so it can be retrieved later
h.registrationCache.Set(
stateStr,
- util.NodePublicKeyStripPrefix(nodeKey),
+ machineKey,
registerCacheExpiration,
)
@@ -266,7 +252,7 @@ func (h *Headscale) OIDCCallback(
return
}
- nodeKey, nodeExists, err := h.validateNodeForOIDCCallback(
+ machineKey, nodeExists, err := h.validateNodeForOIDCCallback(
writer,
state,
claims,
@@ -294,7 +280,7 @@ func (h *Headscale) OIDCCallback(
return
}
- if err := h.registerNodeForOIDCCallback(writer, user, nodeKey, idTokenExpiry); err != nil {
+ if err := h.registerNodeForOIDCCallback(writer, user, machineKey, idTokenExpiry); err != nil {
return
}
@@ -539,10 +525,10 @@ func (h *Headscale) validateNodeForOIDCCallback(
state string,
claims *IDTokenClaims,
expiry time.Time,
-) (*key.NodePublic, bool, error) {
+) (*key.MachinePublic, bool, error) {
// retrieve nodekey from state cache
- nodeKeyIf, nodeKeyFound := h.registrationCache.Get(state)
- if !nodeKeyFound {
+ machineKeyIf, machineKeyFound := h.registrationCache.Get(state)
+ if !machineKeyFound {
log.Trace().
Msg("requested node state key expired before authorisation completed")
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
@@ -555,11 +541,12 @@ func (h *Headscale) validateNodeForOIDCCallback(
return nil, false, errOIDCNodeKeyMissing
}
- var nodeKey key.NodePublic
- nodeKeyFromCache, nodeKeyOK := nodeKeyIf.(string)
- if !nodeKeyOK {
+ var machineKey key.MachinePublic
+ machineKey, machineKeyOK := machineKeyIf.(key.MachinePublic)
+ if !machineKeyOK {
log.Trace().
- Msg("requested node state key is not a string")
+ Interface("got", machineKeyIf).
+ Msg("requested node state key is not a nodekey")
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusBadRequest)
_, err := writer.Write([]byte("state is invalid"))
@@ -570,29 +557,11 @@ func (h *Headscale) validateNodeForOIDCCallback(
return nil, false, errOIDCInvalidNodeState
}
- err := nodeKey.UnmarshalText(
- []byte(util.NodePublicKeyEnsurePrefix(nodeKeyFromCache)),
- )
- if err != nil {
- log.Error().
- Str("nodeKey", nodeKeyFromCache).
- Bool("nodeKeyOK", nodeKeyOK).
- Msg("could not parse node public key")
- writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
- writer.WriteHeader(http.StatusBadRequest)
- _, werr := writer.Write([]byte("could not parse node public key"))
- if werr != nil {
- util.LogErr(err, "Failed to write response")
- }
-
- return nil, false, err
- }
-
// 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.GetNodeByNodeKey(nodeKey)
+ node, _ := h.db.GetNodeByMachineKey(machineKey)
if node != nil {
log.Trace().
@@ -657,7 +626,7 @@ func (h *Headscale) validateNodeForOIDCCallback(
return nil, true, nil
}
- return &nodeKey, false, nil
+ return &machineKey, false, nil
}
func getUserName(
@@ -740,13 +709,13 @@ func (h *Headscale) findOrCreateNewUserForOIDCCallback(
func (h *Headscale) registerNodeForOIDCCallback(
writer http.ResponseWriter,
user *types.User,
- nodeKey *key.NodePublic,
+ machineKey *key.MachinePublic,
expiry time.Time,
) error {
if _, err := h.db.RegisterNodeFromAuthCallback(
// TODO(kradalby): find a better way to use the cache across modules
h.registrationCache,
- nodeKey.String(),
+ *machineKey,
user.Name,
&expiry,
util.RegisterMethodOIDC,
diff --git a/hscontrol/policy/acls.go b/hscontrol/policy/acls.go
index 08ce800..4798d81 100644
--- a/hscontrol/policy/acls.go
+++ b/hscontrol/policy/acls.go
@@ -596,10 +596,13 @@ func excludeCorrectlyTaggedNodes(
}
// for each node if tag is in tags list, don't append it.
for _, node := range nodes {
- hi := node.GetHostInfo()
-
found := false
- for _, t := range hi.RequestTags {
+
+ if node.Hostinfo == nil {
+ continue
+ }
+
+ for _, t := range node.Hostinfo.RequestTags {
if util.StringOrPrefixListContains(tags, t) {
found = true
@@ -671,14 +674,18 @@ func expandOwnersFromTag(
pol *ACLPolicy,
tag string,
) ([]string, error) {
+ noTagErr := fmt.Errorf(
+ "%w. %v isn't owned by a TagOwner. Please add one first. https://tailscale.com/kb/1018/acls/#tag-owners",
+ ErrInvalidTag,
+ tag,
+ )
+ if pol == nil {
+ return []string{}, noTagErr
+ }
var owners []string
ows, ok := pol.TagOwners[tag]
if !ok {
- return []string{}, fmt.Errorf(
- "%w. %v isn't owned by a TagOwner. Please add one first. https://tailscale.com/kb/1018/acls/#tag-owners",
- ErrInvalidTag,
- tag,
- )
+ return []string{}, noTagErr
}
for _, owner := range ows {
if isGroup(owner) {
@@ -787,8 +794,11 @@ func (pol *ACLPolicy) expandIPsFromTag(
for _, user := range owners {
nodes := filterNodesByUser(nodes, user)
for _, node := range nodes {
- hi := node.GetHostInfo()
- if util.StringOrPrefixListContains(hi.RequestTags, alias) {
+ if node.Hostinfo == nil {
+ continue
+ }
+
+ if util.StringOrPrefixListContains(node.Hostinfo.RequestTags, alias) {
node.IPAddresses.AppendToIPSet(&build)
}
}
@@ -882,7 +892,7 @@ func (pol *ACLPolicy) TagsOfNode(
validTagMap := make(map[string]bool)
invalidTagMap := make(map[string]bool)
- for _, tag := range node.HostInfo.RequestTags {
+ for _, tag := range node.Hostinfo.RequestTags {
owners, err := expandOwnersFromTag(pol, tag)
if errors.Is(err, ErrInvalidTag) {
invalidTagMap[tag] = true
diff --git a/hscontrol/policy/acls_test.go b/hscontrol/policy/acls_test.go
index b2d694b..c048778 100644
--- a/hscontrol/policy/acls_test.go
+++ b/hscontrol/policy/acls_test.go
@@ -16,10 +16,6 @@ import (
"tailscale.com/tailcfg"
)
-var ipComparer = cmp.Comparer(func(x, y netip.Addr) bool {
- return x.Compare(y) == 0
-})
-
func Test(t *testing.T) {
check.TestingT(t)
}
@@ -401,6 +397,7 @@ acls:
User: types.User{
Name: "testuser",
},
+ Hostinfo: &tailcfg.Hostinfo{},
},
})
@@ -951,7 +948,7 @@ func Test_listNodesInUser(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
got := filterNodesByUser(test.args.nodes, test.args.user)
- if diff := cmp.Diff(test.want, got); diff != "" {
+ if diff := cmp.Diff(test.want, got, util.Comparers...); diff != "" {
t.Errorf("listNodesInUser() = (-want +got):\n%s", diff)
}
})
@@ -1247,7 +1244,7 @@ func Test_expandAlias(t *testing.T) {
netip.MustParseAddr("100.64.0.1"),
},
User: types.User{Name: "joe"},
- HostInfo: types.HostInfo{
+ Hostinfo: &tailcfg.Hostinfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:hr-webserver"},
@@ -1258,7 +1255,7 @@ func Test_expandAlias(t *testing.T) {
netip.MustParseAddr("100.64.0.2"),
},
User: types.User{Name: "joe"},
- HostInfo: types.HostInfo{
+ Hostinfo: &tailcfg.Hostinfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:hr-webserver"},
@@ -1388,7 +1385,7 @@ func Test_expandAlias(t *testing.T) {
netip.MustParseAddr("100.64.0.2"),
},
User: types.User{Name: "joe"},
- HostInfo: types.HostInfo{
+ Hostinfo: &tailcfg.Hostinfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:hr-webserver"},
@@ -1426,7 +1423,7 @@ func Test_expandAlias(t *testing.T) {
netip.MustParseAddr("100.64.0.1"),
},
User: types.User{Name: "joe"},
- HostInfo: types.HostInfo{
+ Hostinfo: &tailcfg.Hostinfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:accountant-webserver"},
@@ -1437,7 +1434,7 @@ func Test_expandAlias(t *testing.T) {
netip.MustParseAddr("100.64.0.2"),
},
User: types.User{Name: "joe"},
- HostInfo: types.HostInfo{
+ Hostinfo: &tailcfg.Hostinfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:accountant-webserver"},
@@ -1447,13 +1444,15 @@ func Test_expandAlias(t *testing.T) {
IPAddresses: types.NodeAddresses{
netip.MustParseAddr("100.64.0.3"),
},
- User: types.User{Name: "marc"},
+ User: types.User{Name: "marc"},
+ Hostinfo: &tailcfg.Hostinfo{},
},
&types.Node{
IPAddresses: types.NodeAddresses{
netip.MustParseAddr("100.64.0.4"),
},
- User: types.User{Name: "joe"},
+ User: types.User{Name: "joe"},
+ Hostinfo: &tailcfg.Hostinfo{},
},
},
},
@@ -1503,7 +1502,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
netip.MustParseAddr("100.64.0.1"),
},
User: types.User{Name: "joe"},
- HostInfo: types.HostInfo{
+ Hostinfo: &tailcfg.Hostinfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:accountant-webserver"},
@@ -1514,7 +1513,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
netip.MustParseAddr("100.64.0.2"),
},
User: types.User{Name: "joe"},
- HostInfo: types.HostInfo{
+ Hostinfo: &tailcfg.Hostinfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:accountant-webserver"},
@@ -1524,7 +1523,8 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
IPAddresses: types.NodeAddresses{
netip.MustParseAddr("100.64.0.4"),
},
- User: types.User{Name: "joe"},
+ User: types.User{Name: "joe"},
+ Hostinfo: &tailcfg.Hostinfo{},
},
},
user: "joe",
@@ -1533,6 +1533,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
&types.Node{
IPAddresses: types.NodeAddresses{netip.MustParseAddr("100.64.0.4")},
User: types.User{Name: "joe"},
+ Hostinfo: &tailcfg.Hostinfo{},
},
},
},
@@ -1553,7 +1554,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
netip.MustParseAddr("100.64.0.1"),
},
User: types.User{Name: "joe"},
- HostInfo: types.HostInfo{
+ Hostinfo: &tailcfg.Hostinfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:accountant-webserver"},
@@ -1564,7 +1565,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
netip.MustParseAddr("100.64.0.2"),
},
User: types.User{Name: "joe"},
- HostInfo: types.HostInfo{
+ Hostinfo: &tailcfg.Hostinfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:accountant-webserver"},
@@ -1574,7 +1575,8 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
IPAddresses: types.NodeAddresses{
netip.MustParseAddr("100.64.0.4"),
},
- User: types.User{Name: "joe"},
+ User: types.User{Name: "joe"},
+ Hostinfo: &tailcfg.Hostinfo{},
},
},
user: "joe",
@@ -1583,6 +1585,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
&types.Node{
IPAddresses: types.NodeAddresses{netip.MustParseAddr("100.64.0.4")},
User: types.User{Name: "joe"},
+ Hostinfo: &tailcfg.Hostinfo{},
},
},
},
@@ -1598,7 +1601,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
netip.MustParseAddr("100.64.0.1"),
},
User: types.User{Name: "joe"},
- HostInfo: types.HostInfo{
+ Hostinfo: &tailcfg.Hostinfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:accountant-webserver"},
@@ -1610,12 +1613,14 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
},
User: types.User{Name: "joe"},
ForcedTags: []string{"tag:accountant-webserver"},
+ Hostinfo: &tailcfg.Hostinfo{},
},
&types.Node{
IPAddresses: types.NodeAddresses{
netip.MustParseAddr("100.64.0.4"),
},
- User: types.User{Name: "joe"},
+ User: types.User{Name: "joe"},
+ Hostinfo: &tailcfg.Hostinfo{},
},
},
user: "joe",
@@ -1624,6 +1629,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
&types.Node{
IPAddresses: types.NodeAddresses{netip.MustParseAddr("100.64.0.4")},
User: types.User{Name: "joe"},
+ Hostinfo: &tailcfg.Hostinfo{},
},
},
},
@@ -1639,7 +1645,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
netip.MustParseAddr("100.64.0.1"),
},
User: types.User{Name: "joe"},
- HostInfo: types.HostInfo{
+ Hostinfo: &tailcfg.Hostinfo{
OS: "centos",
Hostname: "hr-web1",
RequestTags: []string{"tag:hr-webserver"},
@@ -1650,7 +1656,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
netip.MustParseAddr("100.64.0.2"),
},
User: types.User{Name: "joe"},
- HostInfo: types.HostInfo{
+ Hostinfo: &tailcfg.Hostinfo{
OS: "centos",
Hostname: "hr-web2",
RequestTags: []string{"tag:hr-webserver"},
@@ -1660,7 +1666,8 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
IPAddresses: types.NodeAddresses{
netip.MustParseAddr("100.64.0.4"),
},
- User: types.User{Name: "joe"},
+ User: types.User{Name: "joe"},
+ Hostinfo: &tailcfg.Hostinfo{},
},
},
user: "joe",
@@ -1671,7 +1678,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
netip.MustParseAddr("100.64.0.1"),
},
User: types.User{Name: "joe"},
- HostInfo: types.HostInfo{
+ Hostinfo: &tailcfg.Hostinfo{
OS: "centos",
Hostname: "hr-web1",
RequestTags: []string{"tag:hr-webserver"},
@@ -1682,7 +1689,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
netip.MustParseAddr("100.64.0.2"),
},
User: types.User{Name: "joe"},
- HostInfo: types.HostInfo{
+ Hostinfo: &tailcfg.Hostinfo{
OS: "centos",
Hostname: "hr-web2",
RequestTags: []string{"tag:hr-webserver"},
@@ -1692,7 +1699,8 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
IPAddresses: types.NodeAddresses{
netip.MustParseAddr("100.64.0.4"),
},
- User: types.User{Name: "joe"},
+ User: types.User{Name: "joe"},
+ Hostinfo: &tailcfg.Hostinfo{},
},
},
},
@@ -1704,7 +1712,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
test.args.nodes,
test.args.user,
)
- if diff := cmp.Diff(test.want, got, ipComparer); diff != "" {
+ if diff := cmp.Diff(test.want, got, util.Comparers...); diff != "" {
t.Errorf("excludeCorrectlyTaggedNodes() (-want +got):\n%s", diff)
}
})
@@ -1935,7 +1943,7 @@ func Test_getTags(t *testing.T) {
User: types.User{
Name: "joe",
},
- HostInfo: types.HostInfo{
+ Hostinfo: &tailcfg.Hostinfo{
RequestTags: []string{"tag:valid"},
},
},
@@ -1955,7 +1963,7 @@ func Test_getTags(t *testing.T) {
User: types.User{
Name: "joe",
},
- HostInfo: types.HostInfo{
+ Hostinfo: &tailcfg.Hostinfo{
RequestTags: []string{"tag:valid", "tag:invalid"},
},
},
@@ -1975,7 +1983,7 @@ func Test_getTags(t *testing.T) {
User: types.User{
Name: "joe",
},
- HostInfo: types.HostInfo{
+ Hostinfo: &tailcfg.Hostinfo{
RequestTags: []string{
"tag:invalid",
"tag:valid",
@@ -1999,7 +2007,7 @@ func Test_getTags(t *testing.T) {
User: types.User{
Name: "joe",
},
- HostInfo: types.HostInfo{
+ Hostinfo: &tailcfg.Hostinfo{
RequestTags: []string{"tag:invalid", "very-invalid"},
},
},
@@ -2015,7 +2023,7 @@ func Test_getTags(t *testing.T) {
User: types.User{
Name: "joe",
},
- HostInfo: types.HostInfo{
+ Hostinfo: &tailcfg.Hostinfo{
RequestTags: []string{"tag:invalid", "very-invalid"},
},
},
@@ -2056,10 +2064,6 @@ func Test_getTags(t *testing.T) {
}
func Test_getFilteredByACLPeers(t *testing.T) {
- ipComparer := cmp.Comparer(func(x, y netip.Addr) bool {
- return x.Compare(y) == 0
- })
-
type args struct {
nodes types.Nodes
rules []tailcfg.FilterRule
@@ -2723,7 +2727,7 @@ func Test_getFilteredByACLPeers(t *testing.T) {
tt.args.nodes,
tt.args.rules,
)
- if diff := cmp.Diff(tt.want, got, ipComparer); diff != "" {
+ if diff := cmp.Diff(tt.want, got, util.Comparers...); diff != "" {
t.Errorf("FilterNodesByACL() unexpected result (-want +got):\n%s", diff)
}
})
@@ -2986,9 +2990,6 @@ func TestValidExpandTagOwnersInSources(t *testing.T) {
node := &types.Node{
ID: 0,
- MachineKey: "foo",
- NodeKey: "bar",
- DiscoKey: "faa",
Hostname: "testnodes",
IPAddresses: types.NodeAddresses{netip.MustParseAddr("100.64.0.1")},
UserID: 0,
@@ -2996,7 +2997,7 @@ func TestValidExpandTagOwnersInSources(t *testing.T) {
Name: "user1",
},
RegisterMethod: util.RegisterMethodAuthKey,
- HostInfo: types.HostInfo(hostInfo),
+ Hostinfo: &hostInfo,
}
pol := &ACLPolicy{
@@ -3041,9 +3042,6 @@ func TestInvalidTagValidUser(t *testing.T) {
node := &types.Node{
ID: 1,
- MachineKey: "12345",
- NodeKey: "bar",
- DiscoKey: "faa",
Hostname: "testnodes",
IPAddresses: types.NodeAddresses{netip.MustParseAddr("100.64.0.1")},
UserID: 1,
@@ -3051,7 +3049,7 @@ func TestInvalidTagValidUser(t *testing.T) {
Name: "user1",
},
RegisterMethod: util.RegisterMethodAuthKey,
- HostInfo: types.HostInfo(hostInfo),
+ Hostinfo: &hostInfo,
}
pol := &ACLPolicy{
@@ -3095,9 +3093,6 @@ func TestValidExpandTagOwnersInDestinations(t *testing.T) {
node := &types.Node{
ID: 1,
- MachineKey: "12345",
- NodeKey: "bar",
- DiscoKey: "faa",
Hostname: "testnodes",
IPAddresses: types.NodeAddresses{netip.MustParseAddr("100.64.0.1")},
UserID: 1,
@@ -3105,7 +3100,7 @@ func TestValidExpandTagOwnersInDestinations(t *testing.T) {
Name: "user1",
},
RegisterMethod: util.RegisterMethodAuthKey,
- HostInfo: types.HostInfo(hostInfo),
+ Hostinfo: &hostInfo,
}
pol := &ACLPolicy{
@@ -3159,9 +3154,6 @@ func TestValidTagInvalidUser(t *testing.T) {
node := &types.Node{
ID: 1,
- MachineKey: "12345",
- NodeKey: "bar",
- DiscoKey: "faa",
Hostname: "webserver",
IPAddresses: types.NodeAddresses{netip.MustParseAddr("100.64.0.1")},
UserID: 1,
@@ -3169,7 +3161,7 @@ func TestValidTagInvalidUser(t *testing.T) {
Name: "user1",
},
RegisterMethod: util.RegisterMethodAuthKey,
- HostInfo: types.HostInfo(hostInfo),
+ Hostinfo: &hostInfo,
}
hostInfo2 := tailcfg.Hostinfo{
@@ -3179,9 +3171,6 @@ func TestValidTagInvalidUser(t *testing.T) {
nodes2 := &types.Node{
ID: 2,
- MachineKey: "56789",
- NodeKey: "bar2",
- DiscoKey: "faab",
Hostname: "user",
IPAddresses: types.NodeAddresses{netip.MustParseAddr("100.64.0.2")},
UserID: 1,
@@ -3189,7 +3178,7 @@ func TestValidTagInvalidUser(t *testing.T) {
Name: "user1",
},
RegisterMethod: util.RegisterMethodAuthKey,
- HostInfo: types.HostInfo(hostInfo2),
+ Hostinfo: &hostInfo2,
}
pol := &ACLPolicy{
diff --git a/hscontrol/poll.go b/hscontrol/poll.go
index 9a14c36..568f209 100644
--- a/hscontrol/poll.go
+++ b/hscontrol/poll.go
@@ -8,8 +8,8 @@ import (
"github.com/juanfont/headscale/hscontrol/mapper"
"github.com/juanfont/headscale/hscontrol/types"
- "github.com/juanfont/headscale/hscontrol/util"
"github.com/rs/zerolog/log"
+ xslices "golang.org/x/exp/slices"
"tailscale.com/tailcfg"
)
@@ -26,35 +26,32 @@ type UpdateNode func()
func logPollFunc(
mapRequest tailcfg.MapRequest,
node *types.Node,
- isNoise bool,
) (func(string), func(error, string)) {
return func(msg string) {
log.Info().
Caller().
- Bool("noise", isNoise).
Bool("readOnly", mapRequest.ReadOnly).
Bool("omitPeers", mapRequest.OmitPeers).
Bool("stream", mapRequest.Stream).
- Str("node_key", node.NodeKey).
+ Str("node_key", node.NodeKey.ShortString()).
Str("node", node.Hostname).
Msg(msg)
},
func(err error, msg string) {
log.Error().
Caller().
- Bool("noise", isNoise).
Bool("readOnly", mapRequest.ReadOnly).
Bool("omitPeers", mapRequest.OmitPeers).
Bool("stream", mapRequest.Stream).
- Str("node_key", node.NodeKey).
+ Str("node_key", node.NodeKey.ShortString()).
Str("node", node.Hostname).
Err(err).
Msg(msg)
}
}
-// handlePoll is the common code for the legacy and Noise protocols to
-// managed the poll loop.
+// handlePoll ensures the node gets the appropriate updates from either
+// polling or immediate responses.
//
//nolint:gocyclo
func (h *Headscale) handlePoll(
@@ -62,12 +59,10 @@ func (h *Headscale) handlePoll(
ctx context.Context,
node *types.Node,
mapRequest tailcfg.MapRequest,
- isNoise bool,
- capVer tailcfg.CapabilityVersion,
) {
- logInfo, logErr := logPollFunc(mapRequest, node, isNoise)
+ logInfo, logErr := logPollFunc(mapRequest, node)
- // This is the mechanism where the node gives us inforamtion about its
+ // This is the mechanism where the node gives us information about its
// current configuration.
//
// If OmitPeers is true, Stream is false, and ReadOnly is false,
@@ -75,24 +70,95 @@ func (h *Headscale) handlePoll(
// 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.
+ // TODO(kradalby): remove ReadOnly when we only support capVer 68+
if mapRequest.OmitPeers && !mapRequest.Stream && !mapRequest.ReadOnly {
log.Info().
Caller().
- Bool("noise", isNoise).
Bool("readOnly", mapRequest.ReadOnly).
Bool("omitPeers", mapRequest.OmitPeers).
Bool("stream", mapRequest.Stream).
- Str("node_key", node.NodeKey).
+ Str("node_key", node.NodeKey.ShortString()).
Str("node", node.Hostname).
- Strs("endpoints", node.Endpoints).
- Msg("Received endpoint update")
+ Int("cap_ver", int(mapRequest.Version)).
+ Msg("Received update")
- now := time.Now().UTC()
- node.LastSeen = &now
- node.Hostname = mapRequest.Hostinfo.Hostname
- node.HostInfo = types.HostInfo(*mapRequest.Hostinfo)
- node.DiscoKey = util.DiscoPublicKeyStripPrefix(mapRequest.DiscoKey)
- node.Endpoints = mapRequest.Endpoints
+ change := node.PeerChangeFromMapRequest(mapRequest)
+
+ online := h.nodeNotifier.IsConnected(node.MachineKey)
+ change.Online = &online
+
+ node.ApplyPeerChange(&change)
+
+ hostInfoChange := node.Hostinfo.Equal(mapRequest.Hostinfo)
+
+ logTracePeerChange(node.Hostname, hostInfoChange, &change)
+
+ // Check if the Hostinfo of the node has changed.
+ // If it has changed, check if there has been a change tod
+ // the routable IPs of the host and update update them in
+ // the database. Then send a Changed update
+ // (containing the whole node object) to peers to inform about
+ // the route change.
+ // If the hostinfo has changed, but not the routes, just update
+ // hostinfo and let the function continue.
+ if !hostInfoChange {
+ oldRoutes := node.Hostinfo.RoutableIPs
+ newRoutes := mapRequest.Hostinfo.RoutableIPs
+
+ oldServicesCount := len(node.Hostinfo.Services)
+ newServicesCount := len(mapRequest.Hostinfo.Services)
+
+ node.Hostinfo = mapRequest.Hostinfo
+
+ sendUpdate := false
+
+ // Route changes come as part of Hostinfo, which means that
+ // when an update comes, the Node Route logic need to run.
+ // This will require a "change" in comparison to a "patch",
+ // which is more costly.
+ if !xslices.Equal(oldRoutes, newRoutes) {
+ var err error
+ sendUpdate, err = h.db.SaveNodeRoutes(node)
+ if err != nil {
+ logErr(err, "Error processing node routes")
+ http.Error(writer, "", http.StatusInternalServerError)
+
+ return
+ }
+ }
+
+ // Services is mostly useful for discovery and not critical,
+ // except for peerapi, which is how nodes talk to eachother.
+ // If peerapi was not part of the initial mapresponse, we
+ // need to make sure its sent out later as it is needed for
+ // Taildrop.
+ // TODO(kradalby): Length comparison is a bit naive, replace.
+ if oldServicesCount != newServicesCount {
+ sendUpdate = true
+ }
+
+ if sendUpdate {
+ if err := h.db.NodeSave(node); err != nil {
+ logErr(err, "Failed to persist/update node in the database")
+ http.Error(writer, "", http.StatusInternalServerError)
+
+ return
+ }
+
+ stateUpdate := types.StateUpdate{
+ Type: types.StatePeerChanged,
+ ChangeNodes: types.Nodes{node},
+ Message: "called from handlePoll -> update -> new hostinfo",
+ }
+ if stateUpdate.Valid() {
+ h.nodeNotifier.NotifyWithIgnore(
+ stateUpdate,
+ node.MachineKey.String())
+ }
+
+ return
+ }
+ }
if err := h.db.NodeSave(node); err != nil {
logErr(err, "Failed to persist/update node in the database")
@@ -101,20 +167,15 @@ func (h *Headscale) handlePoll(
return
}
- err := h.db.SaveNodeRoutes(node)
- if err != nil {
- logErr(err, "Error processing node routes")
- http.Error(writer, "", http.StatusInternalServerError)
-
- return
+ stateUpdate := types.StateUpdate{
+ Type: types.StatePeerChangedPatch,
+ ChangePatches: []*tailcfg.PeerChange{&change},
+ }
+ if stateUpdate.Valid() {
+ h.nodeNotifier.NotifyWithIgnore(
+ stateUpdate,
+ node.MachineKey.String())
}
-
- h.nodeNotifier.NotifyWithIgnore(
- types.StateUpdate{
- Type: types.StatePeerChanged,
- Changed: types.Nodes{node},
- },
- node.MachineKey)
writer.WriteHeader(http.StatusOK)
if f, ok := writer.(http.Flusher); ok {
@@ -122,7 +183,7 @@ func (h *Headscale) handlePoll(
}
return
-
+ } else if mapRequest.OmitPeers && !mapRequest.Stream && mapRequest.ReadOnly {
// ReadOnly is whether the client just wants to fetch the
// MapResponse, without updating their Endpoints. The
// Endpoints field will be ignored and LastSeen will not be
@@ -131,7 +192,7 @@ func (h *Headscale) handlePoll(
// The intended use is for clients to discover the DERP map at
// start-up before their first real endpoint update.
} else if mapRequest.OmitPeers && !mapRequest.Stream && mapRequest.ReadOnly {
- h.handleLiteRequest(writer, node, mapRequest, isNoise, capVer)
+ h.handleLiteRequest(writer, node, mapRequest)
return
} else if mapRequest.OmitPeers && mapRequest.Stream {
@@ -140,12 +201,39 @@ func (h *Headscale) handlePoll(
return
}
- now := time.Now().UTC()
- node.LastSeen = &now
- node.Hostname = mapRequest.Hostinfo.Hostname
- node.HostInfo = types.HostInfo(*mapRequest.Hostinfo)
- node.DiscoKey = util.DiscoPublicKeyStripPrefix(mapRequest.DiscoKey)
- node.Endpoints = mapRequest.Endpoints
+ change := node.PeerChangeFromMapRequest(mapRequest)
+
+ // A stream is being set up, the node is Online
+ online := true
+ change.Online = &online
+
+ node.ApplyPeerChange(&change)
+
+ // Only save HostInfo if changed, update routes if changed
+ // TODO(kradalby): Remove when capver is over 68
+ if !node.Hostinfo.Equal(mapRequest.Hostinfo) {
+ oldRoutes := node.Hostinfo.RoutableIPs
+ newRoutes := mapRequest.Hostinfo.RoutableIPs
+
+ node.Hostinfo = mapRequest.Hostinfo
+
+ if !xslices.Equal(oldRoutes, newRoutes) {
+ _, err := h.db.SaveNodeRoutes(node)
+ if err != nil {
+ logErr(err, "Error processing node routes")
+ http.Error(writer, "", http.StatusInternalServerError)
+
+ return
+ }
+ }
+ }
+
+ if err := h.db.NodeSave(node); err != nil {
+ logErr(err, "Failed to persist/update node in the database")
+ http.Error(writer, "", http.StatusInternalServerError)
+
+ return
+ }
// When a node connects to control, list the peers it has at
// that given point, further updates are kept in memory in
@@ -159,12 +247,14 @@ func (h *Headscale) handlePoll(
return
}
+ for _, peer := range peers {
+ online := h.nodeNotifier.IsConnected(peer.MachineKey)
+ peer.IsOnline = &online
+ }
+
mapp := mapper.NewMapper(
node,
peers,
- h.privateKey2019,
- isNoise,
- capVer,
h.DERPMap,
h.cfg.BaseDomain,
h.cfg.DNSConfig,
@@ -172,11 +262,6 @@ func (h *Headscale) handlePoll(
h.cfg.RandomizeClientPort,
)
- err = h.db.SaveNodeRoutes(node)
- if err != nil {
- logErr(err, "Error processing node routes")
- }
-
// update ACLRules with peer informations (to update server tags if necessary)
if h.ACLPolicy != nil {
// update routes with peer information
@@ -186,14 +271,6 @@ func (h *Headscale) handlePoll(
}
}
- // TODO(kradalby): Save specific stuff, not whole object.
- if err := h.db.NodeSave(node); err != nil {
- logErr(err, "Failed to persist/update node in the database")
- http.Error(writer, "", http.StatusInternalServerError)
-
- return
- }
-
logInfo("Sending initial map")
mapResp, err := mapp.FullMapResponse(mapRequest, node, h.ACLPolicy)
@@ -218,18 +295,26 @@ func (h *Headscale) handlePoll(
return
}
- h.nodeNotifier.NotifyWithIgnore(
- types.StateUpdate{
- Type: types.StatePeerChanged,
- Changed: types.Nodes{node},
- },
- node.MachineKey)
+ stateUpdate := types.StateUpdate{
+ Type: types.StatePeerChanged,
+ ChangeNodes: types.Nodes{node},
+ Message: "called from handlePoll -> new node added",
+ }
+ if stateUpdate.Valid() {
+ h.nodeNotifier.NotifyWithIgnore(
+ stateUpdate,
+ node.MachineKey.String())
+ }
// Set up the client stream
h.pollNetMapStreamWG.Add(1)
defer h.pollNetMapStreamWG.Done()
- updateChan := make(chan types.StateUpdate)
+ // Use a buffered channel in case a node is not fully ready
+ // to receive a message to make sure we dont block the entire
+ // notifier.
+ // 12 is arbitrarily chosen.
+ updateChan := make(chan types.StateUpdate, 12)
defer closeChanWithLog(updateChan, node.Hostname, "updateChan")
// Register the node's update channel
@@ -243,6 +328,10 @@ func (h *Headscale) handlePoll(
ctx, cancel := context.WithCancel(ctx)
defer cancel()
+ if len(node.Routes) > 0 {
+ go h.db.EnsureFailoverRouteIsAvailable(node)
+ }
+
for {
logInfo("Waiting for update on stream channel")
select {
@@ -272,14 +361,7 @@ func (h *Headscale) handlePoll(
// One alternative is to split these different channels into
// goroutines, but then you might have a problem without a lock
// if a keepalive is written at the same time as an update.
- go func() {
- err = h.db.UpdateLastSeen(node)
- if err != nil {
- logErr(err, "Cannot update node LastSeen")
-
- return
- }
- }()
+ go h.updateNodeOnlineStatus(true, node)
case update := <-updateChan:
logInfo("Received update")
@@ -289,18 +371,43 @@ func (h *Headscale) handlePoll(
var err error
switch update.Type {
+ case types.StateFullUpdate:
+ logInfo("Sending Full MapResponse")
+
+ data, err = mapp.FullMapResponse(mapRequest, node, h.ACLPolicy)
case types.StatePeerChanged:
- logInfo("Sending PeerChanged MapResponse")
- data, err = mapp.PeerChangedResponse(mapRequest, node, update.Changed, h.ACLPolicy)
+ logInfo(fmt.Sprintf("Sending Changed MapResponse: %s", update.Message))
+
+ for _, node := range update.ChangeNodes {
+ // If a node is not reported to be online, it might be
+ // because the value is outdated, check with the notifier.
+ // However, if it is set to Online, and not in the notifier,
+ // this might be because it has announced itself, but not
+ // reached the stage to actually create the notifier channel.
+ if node.IsOnline != nil && !*node.IsOnline {
+ isOnline := h.nodeNotifier.IsConnected(node.MachineKey)
+ node.IsOnline = &isOnline
+ }
+ }
+
+ data, err = mapp.PeerChangedResponse(mapRequest, node, update.ChangeNodes, h.ACLPolicy, update.Message)
+ case types.StatePeerChangedPatch:
+ logInfo("Sending PeerChangedPatch MapResponse")
+ data, err = mapp.PeerChangedPatchResponse(mapRequest, node, update.ChangePatches, h.ACLPolicy)
case types.StatePeerRemoved:
logInfo("Sending PeerRemoved MapResponse")
data, err = mapp.PeerRemovedResponse(mapRequest, node, update.Removed)
+ case types.StateSelfUpdate:
+ if len(update.ChangeNodes) == 1 {
+ logInfo("Sending SelfUpdate MapResponse")
+ node = update.ChangeNodes[0]
+ data, err = mapp.LiteMapResponse(mapRequest, node, h.ACLPolicy)
+ } else {
+ logInfo("SelfUpdate contained too many nodes, this is likely a bug in the code, please report.")
+ }
case types.StateDERPUpdated:
logInfo("Sending DERPUpdate MapResponse")
data, err = mapp.DERPMapResponse(mapRequest, node, update.DERPMap)
- case types.StateFullUpdate:
- logInfo("Sending Full MapResponse")
- data, err = mapp.FullMapResponse(mapRequest, node, h.ACLPolicy)
}
if err != nil {
@@ -309,55 +416,45 @@ func (h *Headscale) handlePoll(
return
}
- _, err = writer.Write(data)
- if err != nil {
- logErr(err, "Could not write the map response")
-
- updateRequestsSentToNode.WithLabelValues(node.User.Name, node.Hostname, "failed").
- Inc()
-
- return
- }
-
- if flusher, ok := writer.(http.Flusher); ok {
- flusher.Flush()
- } else {
- log.Error().Msg("Failed to create http flusher")
-
- return
- }
-
- // See comment in keepAliveTicker
- go func() {
- err = h.db.UpdateLastSeen(node)
+ // Only send update if there is change
+ if data != nil {
+ _, err = writer.Write(data)
if err != nil {
- logErr(err, "Cannot update node LastSeen")
+ logErr(err, "Could not write the map response")
+
+ updateRequestsSentToNode.WithLabelValues(node.User.Name, node.Hostname, "failed").
+ Inc()
return
}
- }()
- log.Info().
- Caller().
- Bool("noise", isNoise).
- Bool("readOnly", mapRequest.ReadOnly).
- Bool("omitPeers", mapRequest.OmitPeers).
- Bool("stream", mapRequest.Stream).
- Str("node_key", node.NodeKey).
- Str("node", node.Hostname).
- TimeDiff("timeSpent", time.Now(), now).
- Msg("update sent")
+ if flusher, ok := writer.(http.Flusher); ok {
+ flusher.Flush()
+ } else {
+ log.Error().Msg("Failed to create http flusher")
+
+ return
+ }
+
+ log.Info().
+ Caller().
+ Bool("readOnly", mapRequest.ReadOnly).
+ Bool("omitPeers", mapRequest.OmitPeers).
+ Bool("stream", mapRequest.Stream).
+ Str("node_key", node.NodeKey.ShortString()).
+ Str("machine_key", node.MachineKey.ShortString()).
+ Str("node", node.Hostname).
+ TimeDiff("timeSpent", time.Now(), now).
+ Msg("update sent")
+ }
+
case <-ctx.Done():
logInfo("The client has closed the connection")
- go func() {
- err = h.db.UpdateLastSeen(node)
- if err != nil {
- logErr(err, "Cannot update node LastSeen")
+ go h.updateNodeOnlineStatus(false, node)
- return
- }
- }()
+ // Failover the node's routes if any.
+ go h.db.FailoverNodeRoutesWithNotify(node)
// The connection has been closed, so we can stop polling.
return
@@ -370,6 +467,36 @@ func (h *Headscale) handlePoll(
}
}
+// updateNodeOnlineStatus records the last seen status of a node and notifies peers
+// about change in their online/offline status.
+// It takes a StateUpdateType of either StatePeerOnlineChanged or StatePeerOfflineChanged.
+func (h *Headscale) updateNodeOnlineStatus(online bool, node *types.Node) {
+ now := time.Now()
+
+ node.LastSeen = &now
+
+ statusUpdate := types.StateUpdate{
+ Type: types.StatePeerChangedPatch,
+ ChangePatches: []*tailcfg.PeerChange{
+ {
+ NodeID: tailcfg.NodeID(node.ID),
+ Online: &online,
+ LastSeen: &now,
+ },
+ },
+ }
+ if statusUpdate.Valid() {
+ h.nodeNotifier.NotifyWithIgnore(statusUpdate, node.MachineKey.String())
+ }
+
+ err := h.db.UpdateLastSeen(node)
+ if err != nil {
+ log.Error().Err(err).Msg("Cannot update node LastSeen")
+
+ return
+ }
+}
+
func closeChanWithLog[C chan []byte | chan struct{} | chan types.StateUpdate](channel C, node, name string) {
log.Trace().
Str("handler", "PollNetMap").
@@ -384,19 +511,12 @@ func (h *Headscale) handleLiteRequest(
writer http.ResponseWriter,
node *types.Node,
mapRequest tailcfg.MapRequest,
- isNoise bool,
- capVer tailcfg.CapabilityVersion,
) {
- logInfo, logErr := logPollFunc(mapRequest, node, isNoise)
+ logInfo, logErr := logPollFunc(mapRequest, node)
mapp := mapper.NewMapper(
node,
- // TODO(kradalby): It might not be acceptable to send
- // an empty peer list here.
types.Nodes{},
- h.privateKey2019,
- isNoise,
- capVer,
h.DERPMap,
h.cfg.BaseDomain,
h.cfg.DNSConfig,
@@ -421,3 +541,38 @@ func (h *Headscale) handleLiteRequest(
logErr(err, "Failed to write response")
}
}
+
+func logTracePeerChange(hostname string, hostinfoChange bool, change *tailcfg.PeerChange) {
+ trace := log.Trace().Str("node_id", change.NodeID.String()).Str("hostname", hostname)
+
+ if change.Key != nil {
+ trace = trace.Str("node_key", change.Key.ShortString())
+ }
+
+ if change.DiscoKey != nil {
+ trace = trace.Str("disco_key", change.DiscoKey.ShortString())
+ }
+
+ if change.Online != nil {
+ trace = trace.Bool("online", *change.Online)
+ }
+
+ if change.Endpoints != nil {
+ eps := make([]string, len(change.Endpoints))
+ for idx, ep := range change.Endpoints {
+ eps[idx] = ep.String()
+ }
+
+ trace = trace.Strs("endpoints", eps)
+ }
+
+ if hostinfoChange {
+ trace = trace.Bool("hostinfo_changed", hostinfoChange)
+ }
+
+ if change.DERPRegion != 0 {
+ trace = trace.Int("derp_region", change.DERPRegion)
+ }
+
+ trace.Time("last_seen", *change.LastSeen).Msg("PeerChange received")
+}
diff --git a/hscontrol/poll_legacy.go b/hscontrol/poll_legacy.go
deleted file mode 100644
index 2d269e1..0000000
--- a/hscontrol/poll_legacy.go
+++ /dev/null
@@ -1,108 +0,0 @@
-//go:build ts2019
-
-package hscontrol
-
-import (
- "errors"
- "io"
- "net/http"
-
- "github.com/gorilla/mux"
- "github.com/juanfont/headscale/hscontrol/util"
- "github.com/rs/zerolog/log"
- "gorm.io/gorm"
- "tailscale.com/tailcfg"
- "tailscale.com/types/key"
-)
-
-// PollNetMapHandler takes care of /machine/:id/map
-//
-// This is the busiest endpoint, as it keeps the HTTP long poll that updates
-// the clients when something in the network changes.
-//
-// The clients POST stuff like HostInfo and their Endpoints here, but
-// only after their first request (marked with the ReadOnly field).
-//
-// At this moment the updates are sent in a quite horrendous way, but they kinda work.
-func (h *Headscale) PollNetMapHandler(
- writer http.ResponseWriter,
- req *http.Request,
-) {
- vars := mux.Vars(req)
- machineKeyStr, ok := vars["mkey"]
- if !ok || machineKeyStr == "" {
- log.Error().
- Str("handler", "PollNetMap").
- Msg("No machine key in request")
- http.Error(writer, "No machine key in request", http.StatusBadRequest)
-
- return
- }
- log.Trace().
- Str("handler", "PollNetMap").
- Str("id", machineKeyStr).
- Msg("PollNetMapHandler called")
- body, _ := io.ReadAll(req.Body)
-
- var machineKey key.MachinePublic
- err := machineKey.UnmarshalText([]byte(util.MachinePublicKeyEnsurePrefix(machineKeyStr)))
- if err != nil {
- log.Error().
- Str("handler", "PollNetMap").
- Err(err).
- Msg("Cannot parse client key")
-
- http.Error(writer, "Cannot parse client key", http.StatusBadRequest)
-
- return
- }
- mapRequest := tailcfg.MapRequest{}
- err = util.DecodeAndUnmarshalNaCl(body, &mapRequest, &machineKey, h.privateKey2019)
- if err != nil {
- log.Error().
- Str("handler", "PollNetMap").
- Err(err).
- Msg("Cannot decode message")
- http.Error(writer, "Cannot decode message", http.StatusBadRequest)
-
- return
- }
-
- node, err := h.db.GetNodeByMachineKey(machineKey)
- if err != nil {
- if errors.Is(err, gorm.ErrRecordNotFound) {
- log.Warn().
- Str("handler", "PollNetMap").
- Msgf("Ignoring request, cannot find node with key %s", machineKey.String())
-
- http.Error(writer, "", http.StatusUnauthorized)
-
- return
- }
- log.Error().
- Str("handler", "PollNetMap").
- Msgf("Failed to fetch node from the database with Machine key: %s", machineKey.String())
- http.Error(writer, "", http.StatusInternalServerError)
-
- return
- }
-
- log.Trace().
- Str("handler", "PollNetMap").
- Str("id", machineKeyStr).
- Str("node", node.Hostname).
- Msg("A node is sending a MapRequest via legacy protocol")
-
- capVer, err := parseCabailityVersion(req)
- if err != nil && !errors.Is(err, ErrNoCapabilityVersion) {
- log.Error().
- Caller().
- Err(err).
- Msg("failed to parse capVer")
- http.Error(writer, "Internal error", http.StatusInternalServerError)
-
- return
- }
-
- h.handlePoll(writer, req.Context(), node, mapRequest, false, capVer)
-}
diff --git a/hscontrol/poll_noise.go b/hscontrol/poll_noise.go
index 933350c..675836a 100644
--- a/hscontrol/poll_noise.go
+++ b/hscontrol/poll_noise.go
@@ -12,6 +12,10 @@ import (
"tailscale.com/types/key"
)
+const (
+ MinimumCapVersion tailcfg.CapabilityVersion = 56
+)
+
// NoisePollNetMapHandler takes care of /machine/:id/map using the Noise protocol
//
// This is the busiest endpoint, as it keeps the HTTP long poll that updates
@@ -47,6 +51,18 @@ func (ns *noiseServer) NoisePollNetMapHandler(
return
}
+ // 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)
+
+ return
+ }
+
ns.nodeKey = mapRequest.NodeKey
node, err := ns.headscale.db.GetNodeByAnyKey(
@@ -73,20 +89,8 @@ func (ns *noiseServer) NoisePollNetMapHandler(
log.Debug().
Str("handler", "NoisePollNetMap").
Str("node", node.Hostname).
+ Int("cap_ver", int(mapRequest.Version)).
Msg("A node sending a MapRequest with Noise protocol")
- capVer, err := parseCabailityVersion(req)
- if err != nil && !errors.Is(err, ErrNoCapabilityVersion) {
- log.Error().
- Caller().
- Err(err).
- Msg("failed to parse capVer")
- http.Error(writer, "Internal error", http.StatusInternalServerError)
-
- return
- }
-
- // TODO(kradalby): since we are now passing capVer, we could arguably stop passing
- // isNoise, and rather have a isNoise function that takes capVer
- ns.headscale.handlePoll(writer, req.Context(), node, mapRequest, true, capVer)
+ ns.headscale.handlePoll(writer, req.Context(), node, mapRequest)
}
diff --git a/hscontrol/suite_test.go b/hscontrol/suite_test.go
index efee33e..82bdc79 100644
--- a/hscontrol/suite_test.go
+++ b/hscontrol/suite_test.go
@@ -40,7 +40,6 @@ func (s *Suite) ResetDB(c *check.C) {
c.Fatal(err)
}
cfg := types.Config{
- PrivateKeyPath: tmpDir + "/private.key",
NoisePrivateKeyPath: tmpDir + "/noise_private.key",
DBtype: "sqlite3",
DBpath: tmpDir + "/headscale_test.db",
diff --git a/hscontrol/tailsql.go b/hscontrol/tailsql.go
new file mode 100644
index 0000000..973915d
--- /dev/null
+++ b/hscontrol/tailsql.go
@@ -0,0 +1,99 @@
+package hscontrol
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "os"
+
+ "github.com/tailscale/tailsql/server/tailsql"
+ "tailscale.com/tsnet"
+ "tailscale.com/tsweb"
+ "tailscale.com/types/logger"
+)
+
+func runTailSQLService(ctx context.Context, logf logger.Logf, stateDir, dbPath string) error {
+ opts := tailsql.Options{
+ Hostname: "tailsql-headscale",
+ StateDir: stateDir,
+ Sources: []tailsql.DBSpec{
+ {
+ Source: "headscale",
+ Label: "headscale - sqlite",
+ Driver: "sqlite",
+ URL: fmt.Sprintf("file:%s?mode=ro", dbPath),
+ Named: map[string]string{
+ "schema": `select * from sqlite_schema`,
+ },
+ },
+ },
+ }
+
+ tsNode := &tsnet.Server{
+ Dir: os.ExpandEnv(opts.StateDir),
+ Hostname: opts.Hostname,
+ Logf: logger.Discard,
+ }
+ // if *doDebugLog {
+ // tsNode.Logf = logf
+ // }
+ defer tsNode.Close()
+
+ logf("Starting tailscale (hostname=%q)", opts.Hostname)
+ lc, err := tsNode.LocalClient()
+ if err != nil {
+ return fmt.Errorf("connect local client: %w", err)
+ }
+ opts.LocalClient = lc // for authentication
+
+ // Make sure the Tailscale node starts up. It might not, if it is a new node
+ // and the user did not provide an auth key.
+ if st, err := tsNode.Up(ctx); err != nil {
+ return fmt.Errorf("starting tailscale: %w", err)
+ } else {
+ logf("tailscale started, node state %q", st.BackendState)
+ }
+
+ // Reaching here, we have a running Tailscale node, now we can set up the
+ // HTTP and/or HTTPS plumbing for TailSQL itself.
+ tsql, err := tailsql.NewServer(opts)
+ if err != nil {
+ return fmt.Errorf("creating tailsql server: %w", err)
+ }
+
+ lst, err := tsNode.Listen("tcp", ":80")
+ if err != nil {
+ return fmt.Errorf("listen port 80: %w", err)
+ }
+
+ if opts.ServeHTTPS {
+ // When serving TLS, add a redirect from HTTP on port 80 to HTTPS on 443.
+ certDomains := tsNode.CertDomains()
+ if len(certDomains) == 0 {
+ fmt.Errorf("no cert domains available for HTTPS")
+ }
+ base := "https://" + certDomains[0]
+ go http.Serve(lst, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ target := base + r.RequestURI
+ http.Redirect(w, r, target, http.StatusPermanentRedirect)
+ }))
+ // log.Printf("Redirecting HTTP to HTTPS at %q", base)
+
+ // For the real service, start a separate listener.
+ // Note: Replaces the port 80 listener.
+ var err error
+ lst, err = tsNode.ListenTLS("tcp", ":443")
+ if err != nil {
+ return fmt.Errorf("listen TLS: %w", err)
+ }
+ logf("enabled serving via HTTPS")
+ }
+
+ mux := tsql.NewMux()
+ tsweb.Debugger(mux)
+ go http.Serve(lst, mux)
+ logf("ailSQL started")
+ <-ctx.Done()
+ logf("TailSQL shutting down...")
+ return tsNode.Close()
+}
diff --git a/hscontrol/types/common.go b/hscontrol/types/common.go
index b275fa4..e38d8e3 100644
--- a/hscontrol/types/common.go
+++ b/hscontrol/types/common.go
@@ -12,33 +12,6 @@ import (
var ErrCannotParsePrefix = errors.New("cannot parse prefix")
-// This is a "wrapper" type around tailscales
-// Hostinfo to allow us to add database "serialization"
-// methods. This allows us to use a typed values throughout
-// the code and not have to marshal/unmarshal and error
-// check all over the code.
-type HostInfo tailcfg.Hostinfo
-
-func (hi *HostInfo) Scan(destination interface{}) error {
- switch value := destination.(type) {
- case []byte:
- return json.Unmarshal(value, hi)
-
- case string:
- return json.Unmarshal([]byte(value), hi)
-
- default:
- return fmt.Errorf("%w: unexpected data type %T", ErrNodeAddressesInvalid, destination)
- }
-}
-
-// Value return json value, implement driver.Valuer interface.
-func (hi HostInfo) Value() (driver.Value, error) {
- bytes, err := json.Marshal(hi)
-
- return string(bytes), err
-}
-
type IPPrefix netip.Prefix
func (i *IPPrefix) Scan(destination interface{}) error {
@@ -111,20 +84,37 @@ type StateUpdateType int
const (
StateFullUpdate StateUpdateType = iota
+ // StatePeerChanged is used for updates that needs
+ // to be calculated with all peers and all policy rules.
+ // This would typically be things that include tags, routes
+ // and similar.
StatePeerChanged
+ StatePeerChangedPatch
StatePeerRemoved
+ // StateSelfUpdate is used to indicate that the node
+ // has changed in control, and the client needs to be
+ // informed.
+ // The updated node is inside the ChangeNodes field
+ // which should have a length of one.
+ StateSelfUpdate
StateDERPUpdated
)
// StateUpdate is an internal message containing information about
// a state change that has happened to the network.
+// If type is StateFullUpdate, all fields are ignored.
type StateUpdate struct {
// The type of update
Type StateUpdateType
- // Changed must be set when Type is StatePeerChanged and
- // contain the Node IDs of nodes that have changed.
- Changed Nodes
+ // ChangeNodes must be set when Type is StatePeerAdded
+ // and StatePeerChanged and contains the full node
+ // object for added nodes.
+ ChangeNodes Nodes
+
+ // ChangePatches must be set when Type is StatePeerChangedPatch
+ // and contains a populated PeerChange object.
+ ChangePatches []*tailcfg.PeerChange
// Removed must be set when Type is StatePeerRemoved and
// contain a list of the nodes that has been removed from
@@ -133,5 +123,40 @@ type StateUpdate struct {
// DERPMap must be set when Type is StateDERPUpdated and
// contain the new DERP Map.
- DERPMap tailcfg.DERPMap
+ DERPMap *tailcfg.DERPMap
+
+ // Additional message for tracking origin or what being
+ // updated, useful for ambiguous updates like StatePeerChanged.
+ Message string
+}
+
+// Valid reports if a StateUpdate is correctly filled and
+// panics if the mandatory fields for a type is not
+// filled.
+// Reports true if valid.
+func (su *StateUpdate) Valid() bool {
+ switch su.Type {
+ case StatePeerChanged:
+ if su.ChangeNodes == nil {
+ panic("Mandatory field ChangeNodes is not set on StatePeerChanged update")
+ }
+ case StatePeerChangedPatch:
+ if su.ChangePatches == nil {
+ panic("Mandatory field ChangePatches is not set on StatePeerChangedPatch update")
+ }
+ case StatePeerRemoved:
+ if su.Removed == nil {
+ panic("Mandatory field Removed is not set on StatePeerRemove update")
+ }
+ case StateSelfUpdate:
+ if su.ChangeNodes == nil || len(su.ChangeNodes) != 1 {
+ panic("Mandatory field ChangeNodes is not set for StateSelfUpdate or has more than one node")
+ }
+ case StateDERPUpdated:
+ if su.DERPMap == nil {
+ panic("Mandatory field DERPMap is not set on StateDERPUpdated update")
+ }
+ }
+
+ return true
}
diff --git a/hscontrol/types/config.go b/hscontrol/types/config.go
index 2d7068a..01cb9fd 100644
--- a/hscontrol/types/config.go
+++ b/hscontrol/types/config.go
@@ -41,7 +41,6 @@ type Config struct {
EphemeralNodeInactivityTimeout time.Duration
NodeUpdateCheckInterval time.Duration
IPPrefixes []netip.Prefix
- PrivateKeyPath string
NoisePrivateKeyPath string
BaseDomain string
Log LogConfig
@@ -112,15 +111,16 @@ type OIDCConfig struct {
}
type DERPConfig struct {
- ServerEnabled bool
- ServerRegionID int
- ServerRegionCode string
- ServerRegionName string
- STUNAddr string
- URLs []url.URL
- Paths []string
- AutoUpdate bool
- UpdateFrequency time.Duration
+ ServerEnabled bool
+ ServerRegionID int
+ ServerRegionCode string
+ ServerRegionName string
+ ServerPrivateKeyPath string
+ STUNAddr string
+ URLs []url.URL
+ Paths []string
+ AutoUpdate bool
+ UpdateFrequency time.Duration
}
type LogTailConfig struct {
@@ -294,6 +294,7 @@ func GetDERPConfig() DERPConfig {
serverRegionCode := viper.GetString("derp.server.region_code")
serverRegionName := viper.GetString("derp.server.region_name")
stunAddr := viper.GetString("derp.server.stun_listen_addr")
+ privateKeyPath := util.AbsolutePathFromConfigPath(viper.GetString("derp.server.private_key_path"))
if serverEnabled && stunAddr == "" {
log.Fatal().
@@ -321,15 +322,16 @@ func GetDERPConfig() DERPConfig {
updateFrequency := viper.GetDuration("derp.update_frequency")
return DERPConfig{
- ServerEnabled: serverEnabled,
- ServerRegionID: serverRegionID,
- ServerRegionCode: serverRegionCode,
- ServerRegionName: serverRegionName,
- STUNAddr: stunAddr,
- URLs: urls,
- Paths: paths,
- AutoUpdate: autoUpdate,
- UpdateFrequency: updateFrequency,
+ ServerEnabled: serverEnabled,
+ ServerRegionID: serverRegionID,
+ ServerRegionCode: serverRegionCode,
+ ServerRegionName: serverRegionName,
+ ServerPrivateKeyPath: privateKeyPath,
+ STUNAddr: stunAddr,
+ URLs: urls,
+ Paths: paths,
+ AutoUpdate: autoUpdate,
+ UpdateFrequency: updateFrequency,
}
}
@@ -590,9 +592,6 @@ func GetHeadscaleConfig() (*Config, error) {
DisableUpdateCheck: viper.GetBool("disable_check_updates"),
IPPrefixes: prefixes,
- PrivateKeyPath: util.AbsolutePathFromConfigPath(
- viper.GetString("private_key_path"),
- ),
NoisePrivateKeyPath: util.AbsolutePathFromConfigPath(
viper.GetString("noise.private_key_path"),
),
diff --git a/hscontrol/types/node.go b/hscontrol/types/node.go
index f2b193c..9b2ba76 100644
--- a/hscontrol/types/node.go
+++ b/hscontrol/types/node.go
@@ -2,6 +2,7 @@ package types
import (
"database/sql/driver"
+ "encoding/json"
"errors"
"fmt"
"net/netip"
@@ -11,24 +12,60 @@ import (
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/juanfont/headscale/hscontrol/policy/matcher"
- "github.com/juanfont/headscale/hscontrol/util"
+ "github.com/rs/zerolog/log"
"go4.org/netipx"
"google.golang.org/protobuf/types/known/timestamppb"
+ "gorm.io/gorm"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
var (
ErrNodeAddressesInvalid = errors.New("failed to parse node addresses")
- ErrHostnameTooLong = errors.New("hostname too long")
+ ErrHostnameTooLong = errors.New("hostname too long, cannot except 255 ASCII chars")
+ ErrNodeHasNoGivenName = errors.New("node has no given name")
+ ErrNodeUserHasNoName = errors.New("node user has no name")
)
// Node is a Headscale client.
type Node struct {
- ID uint64 `gorm:"primary_key"`
- MachineKey string `gorm:"type:varchar(64);unique_index"`
- NodeKey string
- DiscoKey string
+ ID uint64 `gorm:"primary_key"`
+
+ // MachineKeyDatabaseField is the string representation of MachineKey
+ // it is _only_ used for reading and writing the key to the
+ // database and should not be used.
+ // Use MachineKey instead.
+ MachineKeyDatabaseField string `gorm:"column:machine_key;unique_index"`
+ MachineKey key.MachinePublic `gorm:"-"`
+
+ // NodeKeyDatabaseField is the string representation of NodeKey
+ // it is _only_ used for reading and writing the key to the
+ // database and should not be used.
+ // Use NodeKey instead.
+ NodeKeyDatabaseField string `gorm:"column:node_key"`
+ NodeKey key.NodePublic `gorm:"-"`
+
+ // DiscoKeyDatabaseField is the string representation of DiscoKey
+ // it is _only_ used for reading and writing the key to the
+ // database and should not be used.
+ // Use DiscoKey instead.
+ DiscoKeyDatabaseField string `gorm:"column:disco_key"`
+ DiscoKey key.DiscoPublic `gorm:"-"`
+
+ // EndpointsDatabaseField is the string list representation of Endpoints
+ // it is _only_ used for reading and writing the key to the
+ // database and should not be used.
+ // Use Endpoints instead.
+ EndpointsDatabaseField StringList `gorm:"column:endpoints"`
+ Endpoints []netip.AddrPort `gorm:"-"`
+
+ // EndpointsDatabaseField is the string list representation of Endpoints
+ // it is _only_ used for reading and writing the key to the
+ // database and should not be used.
+ // Use Endpoints instead.
+ HostinfoDatabaseField string `gorm:"column:host_info"`
+ Hostinfo *tailcfg.Hostinfo `gorm:"-"`
+
IPAddresses NodeAddresses
// Hostname represents the name given by the Tailscale
@@ -56,30 +93,19 @@ type Node struct {
LastSeen *time.Time
Expiry *time.Time
- HostInfo HostInfo
- Endpoints StringList
-
Routes []Route
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
+
+ IsOnline *bool `gorm:"-"`
}
type (
Nodes []*Node
)
-func (nodes Nodes) OnlineNodeMap() map[tailcfg.NodeID]bool {
- ret := make(map[tailcfg.NodeID]bool)
-
- for _, node := range nodes {
- ret[tailcfg.NodeID(node.ID)] = node.IsOnline()
- }
-
- return ret
-}
-
type NodeAddresses []netip.Addr
func (na NodeAddresses) Sort() {
@@ -175,21 +201,6 @@ func (node Node) IsExpired() bool {
return time.Now().UTC().After(*node.Expiry)
}
-// IsOnline returns if the node is connected to Headscale.
-// This is really a naive implementation, as we don't really see
-// if there is a working connection between the client and the server.
-func (node *Node) IsOnline() bool {
- if node.LastSeen == nil {
- return false
- }
-
- if node.IsExpired() {
- return false
- }
-
- return node.LastSeen.After(time.Now().Add(-KeepAliveInterval))
-}
-
// IsEphemeral returns if the node is registered as an Ephemeral node.
// https://tailscale.com/kb/1111/ephemeral-nodes/
func (node *Node) IsEphemeral() bool {
@@ -227,19 +238,89 @@ func (nodes Nodes) FilterByIP(ip netip.Addr) Nodes {
return found
}
+// BeforeSave is a hook that ensures that some values that
+// cannot be directly marshalled into database values are stored
+// correctly in the database.
+// This currently means storing the keys as strings.
+func (node *Node) BeforeSave(tx *gorm.DB) error {
+ node.MachineKeyDatabaseField = node.MachineKey.String()
+ node.NodeKeyDatabaseField = node.NodeKey.String()
+ node.DiscoKeyDatabaseField = node.DiscoKey.String()
+
+ var endpoints StringList
+ for _, addrPort := range node.Endpoints {
+ endpoints = append(endpoints, addrPort.String())
+ }
+
+ node.EndpointsDatabaseField = endpoints
+
+ hi, err := json.Marshal(node.Hostinfo)
+ if err != nil {
+ return fmt.Errorf("failed to marshal Hostinfo to store in db: %w", err)
+ }
+ node.HostinfoDatabaseField = string(hi)
+
+ return nil
+}
+
+// AfterFind is a hook that ensures that Node objects fields that
+// has a different type in the database is unwrapped and populated
+// correctly.
+// This currently unmarshals all the keys, stored as strings, into
+// the proper types.
+func (node *Node) AfterFind(tx *gorm.DB) error {
+ var machineKey key.MachinePublic
+ if err := machineKey.UnmarshalText([]byte(node.MachineKeyDatabaseField)); err != nil {
+ return fmt.Errorf("failed to unmarshal machine key from db: %w", err)
+ }
+ node.MachineKey = machineKey
+
+ var nodeKey key.NodePublic
+ if err := nodeKey.UnmarshalText([]byte(node.NodeKeyDatabaseField)); err != nil {
+ return fmt.Errorf("failed to unmarshal node key from db: %w", err)
+ }
+ node.NodeKey = nodeKey
+
+ var discoKey key.DiscoPublic
+ if err := discoKey.UnmarshalText([]byte(node.DiscoKeyDatabaseField)); err != nil {
+ return fmt.Errorf("failed to unmarshal disco key from db: %w", err)
+ }
+ node.DiscoKey = discoKey
+
+ endpoints := make([]netip.AddrPort, len(node.EndpointsDatabaseField))
+ for idx, ep := range node.EndpointsDatabaseField {
+ addrPort, err := netip.ParseAddrPort(ep)
+ if err != nil {
+ return fmt.Errorf("failed to parse endpoint from db: %w", err)
+ }
+
+ endpoints[idx] = addrPort
+ }
+ node.Endpoints = endpoints
+
+ var hi tailcfg.Hostinfo
+ if err := json.Unmarshal([]byte(node.HostinfoDatabaseField), &hi); err != nil {
+ log.Trace().Err(err).Msgf("Hostinfo content: %s", node.HostinfoDatabaseField)
+
+ return fmt.Errorf("failed to unmarshal Hostinfo from db: %w", err)
+ }
+ node.Hostinfo = &hi
+
+ return nil
+}
+
func (node *Node) Proto() *v1.Node {
nodeProto := &v1.Node{
Id: node.ID,
- MachineKey: node.MachineKey,
+ MachineKey: node.MachineKey.String(),
- NodeKey: node.NodeKey,
- DiscoKey: node.DiscoKey,
+ NodeKey: node.NodeKey.String(),
+ DiscoKey: node.DiscoKey.String(),
IpAddresses: node.IPAddresses.StringSlice(),
Name: node.Hostname,
GivenName: node.GivenName,
User: node.User.Proto(),
ForcedTags: node.ForcedTags,
- Online: node.IsOnline(),
// TODO(kradalby): Implement register method enum converter
// RegisterMethod: ,
@@ -262,14 +343,17 @@ func (node *Node) Proto() *v1.Node {
return nodeProto
}
-// GetHostInfo returns a Hostinfo struct for the node.
-func (node *Node) GetHostInfo() tailcfg.Hostinfo {
- return tailcfg.Hostinfo(node.HostInfo)
-}
-
func (node *Node) GetFQDN(dnsConfig *tailcfg.DNSConfig, baseDomain string) (string, error) {
var hostname string
if dnsConfig != nil && dnsConfig.Proxied { // MagicDNS
+ if node.GivenName == "" {
+ return "", fmt.Errorf("failed to create valid FQDN: %w", ErrNodeHasNoGivenName)
+ }
+
+ if node.User.Name == "" {
+ return "", fmt.Errorf("failed to create valid FQDN: %w", ErrNodeUserHasNoName)
+ }
+
hostname = fmt.Sprintf(
"%s.%s.%s",
node.GivenName,
@@ -278,7 +362,7 @@ func (node *Node) GetFQDN(dnsConfig *tailcfg.DNSConfig, baseDomain string) (stri
)
if len(hostname) > MaxHostnameLength {
return "", fmt.Errorf(
- "hostname %q is too long it cannot except 255 ASCII chars: %w",
+ "failed to create valid FQDN (%s): %w",
hostname,
ErrHostnameTooLong,
)
@@ -290,49 +374,98 @@ func (node *Node) GetFQDN(dnsConfig *tailcfg.DNSConfig, baseDomain string) (stri
return hostname, nil
}
-func (node *Node) MachinePublicKey() (key.MachinePublic, error) {
- var machineKey key.MachinePublic
+// func (node *Node) String() string {
+// return node.Hostname
+// }
- if node.MachineKey != "" {
- err := machineKey.UnmarshalText(
- []byte(util.MachinePublicKeyEnsurePrefix(node.MachineKey)),
- )
- if err != nil {
- return key.MachinePublic{}, fmt.Errorf("failed to parse machine public key: %w", err)
+// PeerChangeFromMapRequest takes a MapRequest and compares it to the node
+// to produce a PeerChange struct that can be used to updated the node and
+// inform peers about smaller changes to the node.
+// When a field is added to this function, remember to also add it to:
+// - node.ApplyPeerChange
+// - logTracePeerChange in poll.go
+func (node *Node) PeerChangeFromMapRequest(req tailcfg.MapRequest) tailcfg.PeerChange {
+ ret := tailcfg.PeerChange{
+ NodeID: tailcfg.NodeID(node.ID),
+ }
+
+ if node.NodeKey.String() != req.NodeKey.String() {
+ ret.Key = &req.NodeKey
+ }
+
+ if node.DiscoKey.String() != req.DiscoKey.String() {
+ ret.DiscoKey = &req.DiscoKey
+ }
+
+ if node.Hostinfo != nil &&
+ node.Hostinfo.NetInfo != nil &&
+ req.Hostinfo != nil &&
+ req.Hostinfo.NetInfo != nil &&
+ node.Hostinfo.NetInfo.PreferredDERP != req.Hostinfo.NetInfo.PreferredDERP {
+ ret.DERPRegion = req.Hostinfo.NetInfo.PreferredDERP
+ }
+
+ if req.Hostinfo != nil && req.Hostinfo.NetInfo != nil {
+ // If there is no stored Hostinfo or NetInfo, use
+ // the new PreferredDERP.
+ if node.Hostinfo == nil {
+ ret.DERPRegion = req.Hostinfo.NetInfo.PreferredDERP
+ } else if node.Hostinfo.NetInfo == nil {
+ ret.DERPRegion = req.Hostinfo.NetInfo.PreferredDERP
+ } else {
+ // If there is a PreferredDERP check if it has changed.
+ if node.Hostinfo.NetInfo.PreferredDERP != req.Hostinfo.NetInfo.PreferredDERP {
+ ret.DERPRegion = req.Hostinfo.NetInfo.PreferredDERP
+ }
}
}
- return machineKey, nil
+ // TODO(kradalby): Find a good way to compare updates
+ ret.Endpoints = req.Endpoints
+
+ now := time.Now()
+ ret.LastSeen = &now
+
+ return ret
}
-func (node *Node) DiscoPublicKey() (key.DiscoPublic, error) {
- var discoKey key.DiscoPublic
- if node.DiscoKey != "" {
- err := discoKey.UnmarshalText(
- []byte(util.DiscoPublicKeyEnsurePrefix(node.DiscoKey)),
- )
- if err != nil {
- return key.DiscoPublic{}, fmt.Errorf("failed to parse disco public key: %w", err)
+// ApplyPeerChange takes a PeerChange struct and updates the node.
+func (node *Node) ApplyPeerChange(change *tailcfg.PeerChange) {
+ if change.Key != nil {
+ node.NodeKey = *change.Key
+ }
+
+ if change.DiscoKey != nil {
+ node.DiscoKey = *change.DiscoKey
+ }
+
+ if change.Online != nil {
+ node.IsOnline = change.Online
+ }
+
+ if change.Endpoints != nil {
+ node.Endpoints = change.Endpoints
+ }
+
+ // This might technically not be useful as we replace
+ // the whole hostinfo blob when it has changed.
+ if change.DERPRegion != 0 {
+ if node.Hostinfo == nil {
+ node.Hostinfo = &tailcfg.Hostinfo{
+ NetInfo: &tailcfg.NetInfo{
+ PreferredDERP: change.DERPRegion,
+ },
+ }
+ } else if node.Hostinfo.NetInfo == nil {
+ node.Hostinfo.NetInfo = &tailcfg.NetInfo{
+ PreferredDERP: change.DERPRegion,
+ }
+ } else {
+ node.Hostinfo.NetInfo.PreferredDERP = change.DERPRegion
}
- } else {
- discoKey = key.DiscoPublic{}
}
- return discoKey, nil
-}
-
-func (node *Node) NodePublicKey() (key.NodePublic, error) {
- var nodeKey key.NodePublic
- err := nodeKey.UnmarshalText([]byte(util.NodePublicKeyEnsurePrefix(node.NodeKey)))
- if err != nil {
- return key.NodePublic{}, fmt.Errorf("failed to parse node public key: %w", err)
- }
-
- return nodeKey, nil
-}
-
-func (node Node) String() string {
- return node.Hostname
+ node.LastSeen = change.LastSeen
}
func (nodes Nodes) String() string {
diff --git a/hscontrol/types/node_test.go b/hscontrol/types/node_test.go
index 85fa79c..7e6c984 100644
--- a/hscontrol/types/node_test.go
+++ b/hscontrol/types/node_test.go
@@ -4,7 +4,10 @@ import (
"net/netip"
"testing"
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
"tailscale.com/tailcfg"
+ "tailscale.com/types/key"
)
func Test_NodeCanAccess(t *testing.T) {
@@ -139,3 +142,227 @@ func TestNodeAddressesOrder(t *testing.T) {
}
}
}
+
+func TestNodeFQDN(t *testing.T) {
+ tests := []struct {
+ name string
+ node Node
+ dns tailcfg.DNSConfig
+ domain string
+ want string
+ wantErr string
+ }{
+ {
+ name: "all-set",
+ node: Node{
+ GivenName: "test",
+ User: User{
+ Name: "user",
+ },
+ },
+ dns: tailcfg.DNSConfig{
+ Proxied: true,
+ },
+ domain: "example.com",
+ want: "test.user.example.com",
+ },
+ {
+ name: "no-given-name",
+ node: Node{
+ User: User{
+ Name: "user",
+ },
+ },
+ dns: tailcfg.DNSConfig{
+ Proxied: true,
+ },
+ domain: "example.com",
+ wantErr: "failed to create valid FQDN: node has no given name",
+ },
+ {
+ name: "no-user-name",
+ node: Node{
+ GivenName: "test",
+ User: User{},
+ },
+ dns: tailcfg.DNSConfig{
+ Proxied: true,
+ },
+ domain: "example.com",
+ wantErr: "failed to create valid FQDN: node user has no name",
+ },
+ {
+ name: "no-magic-dns",
+ node: Node{
+ GivenName: "test",
+ User: User{
+ Name: "user",
+ },
+ },
+ dns: tailcfg.DNSConfig{
+ Proxied: false,
+ },
+ domain: "example.com",
+ want: "test",
+ },
+ {
+ name: "no-dnsconfig",
+ node: Node{
+ GivenName: "test",
+ User: User{
+ Name: "user",
+ },
+ },
+ domain: "example.com",
+ want: "test",
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ got, err := tc.node.GetFQDN(&tc.dns, tc.domain)
+
+ if (err != nil) && (err.Error() != tc.wantErr) {
+ t.Errorf("GetFQDN() error = %s, wantErr %s", err, tc.wantErr)
+
+ return
+ }
+
+ if diff := cmp.Diff(tc.want, got); diff != "" {
+ t.Errorf("GetFQDN unexpected result (-want +got):\n%s", diff)
+ }
+ })
+ }
+}
+
+func TestPeerChangeFromMapRequest(t *testing.T) {
+ nKeys := []key.NodePublic{
+ key.NewNode().Public(),
+ key.NewNode().Public(),
+ key.NewNode().Public(),
+ }
+
+ dKeys := []key.DiscoPublic{
+ key.NewDisco().Public(),
+ key.NewDisco().Public(),
+ key.NewDisco().Public(),
+ }
+
+ tests := []struct {
+ name string
+ node Node
+ mapReq tailcfg.MapRequest
+ want tailcfg.PeerChange
+ }{
+ {
+ name: "preferred-derp-changed",
+ node: Node{
+ ID: 1,
+ NodeKey: nKeys[0],
+ DiscoKey: dKeys[0],
+ Endpoints: []netip.AddrPort{},
+ Hostinfo: &tailcfg.Hostinfo{
+ NetInfo: &tailcfg.NetInfo{
+ PreferredDERP: 998,
+ },
+ },
+ },
+ mapReq: tailcfg.MapRequest{
+ NodeKey: nKeys[0],
+ DiscoKey: dKeys[0],
+ Hostinfo: &tailcfg.Hostinfo{
+ NetInfo: &tailcfg.NetInfo{
+ PreferredDERP: 999,
+ },
+ },
+ },
+ want: tailcfg.PeerChange{
+ NodeID: 1,
+ DERPRegion: 999,
+ },
+ },
+ {
+ name: "preferred-derp-no-changed",
+ node: Node{
+ ID: 1,
+ NodeKey: nKeys[0],
+ DiscoKey: dKeys[0],
+ Endpoints: []netip.AddrPort{},
+ Hostinfo: &tailcfg.Hostinfo{
+ NetInfo: &tailcfg.NetInfo{
+ PreferredDERP: 100,
+ },
+ },
+ },
+ mapReq: tailcfg.MapRequest{
+ NodeKey: nKeys[0],
+ DiscoKey: dKeys[0],
+ Hostinfo: &tailcfg.Hostinfo{
+ NetInfo: &tailcfg.NetInfo{
+ PreferredDERP: 100,
+ },
+ },
+ },
+ want: tailcfg.PeerChange{
+ NodeID: 1,
+ DERPRegion: 0,
+ },
+ },
+ {
+ name: "preferred-derp-no-mapreq-netinfo",
+ node: Node{
+ ID: 1,
+ NodeKey: nKeys[0],
+ DiscoKey: dKeys[0],
+ Endpoints: []netip.AddrPort{},
+ Hostinfo: &tailcfg.Hostinfo{
+ NetInfo: &tailcfg.NetInfo{
+ PreferredDERP: 200,
+ },
+ },
+ },
+ mapReq: tailcfg.MapRequest{
+ NodeKey: nKeys[0],
+ DiscoKey: dKeys[0],
+ Hostinfo: &tailcfg.Hostinfo{},
+ },
+ want: tailcfg.PeerChange{
+ NodeID: 1,
+ DERPRegion: 0,
+ },
+ },
+ {
+ name: "preferred-derp-no-node-netinfo",
+ node: Node{
+ ID: 1,
+ NodeKey: nKeys[0],
+ DiscoKey: dKeys[0],
+ Endpoints: []netip.AddrPort{},
+ Hostinfo: &tailcfg.Hostinfo{},
+ },
+ mapReq: tailcfg.MapRequest{
+ NodeKey: nKeys[0],
+ DiscoKey: dKeys[0],
+ Hostinfo: &tailcfg.Hostinfo{
+ NetInfo: &tailcfg.NetInfo{
+ PreferredDERP: 200,
+ },
+ },
+ },
+ want: tailcfg.PeerChange{
+ NodeID: 1,
+ DERPRegion: 200,
+ },
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ got := tc.node.PeerChangeFromMapRequest(tc.mapReq)
+
+ if diff := cmp.Diff(tc.want, got, cmpopts.IgnoreFields(tailcfg.PeerChange{}, "LastSeen")); diff != "" {
+ t.Errorf("Patch unexpected result (-want +got):\n%s", diff)
+ }
+ })
+ }
+}
diff --git a/hscontrol/types/routes.go b/hscontrol/types/routes.go
index 3fd9670..697cbc3 100644
--- a/hscontrol/types/routes.go
+++ b/hscontrol/types/routes.go
@@ -19,6 +19,8 @@ type Route struct {
NodeID uint64
Node Node
+
+ // TODO(kradalby): change this custom type to netip.Prefix
Prefix IPPrefix
Advertised bool
@@ -29,13 +31,17 @@ type Route struct {
type Routes []Route
func (r *Route) String() string {
- return fmt.Sprintf("%s:%s", r.Node, netip.Prefix(r.Prefix).String())
+ return fmt.Sprintf("%s:%s", r.Node.Hostname, netip.Prefix(r.Prefix).String())
}
func (r *Route) IsExitRoute() bool {
return netip.Prefix(r.Prefix) == ExitRouteV4 || netip.Prefix(r.Prefix) == ExitRouteV6
}
+func (r *Route) IsAnnouncable() bool {
+ return r.Advertised && r.Enabled
+}
+
func (rs Routes) Prefixes() []netip.Prefix {
prefixes := make([]netip.Prefix, len(rs))
for i, r := range rs {
@@ -45,6 +51,32 @@ func (rs Routes) Prefixes() []netip.Prefix {
return prefixes
}
+// Primaries returns Primary routes from a list of routes.
+func (rs Routes) Primaries() Routes {
+ res := make(Routes, 0)
+ for _, route := range rs {
+ if route.IsPrimary {
+ res = append(res, route)
+ }
+ }
+
+ return res
+}
+
+func (rs Routes) PrefixMap() map[IPPrefix][]Route {
+ res := map[IPPrefix][]Route{}
+
+ for _, route := range rs {
+ if _, ok := res[route.Prefix]; ok {
+ res[route.Prefix] = append(res[route.Prefix], route)
+ } else {
+ res[route.Prefix] = []Route{route}
+ }
+ }
+
+ return res
+}
+
func (rs Routes) Proto() []*v1.Route {
protoRoutes := []*v1.Route{}
diff --git a/hscontrol/types/routes_test.go b/hscontrol/types/routes_test.go
new file mode 100644
index 0000000..ead4c59
--- /dev/null
+++ b/hscontrol/types/routes_test.go
@@ -0,0 +1,94 @@
+package types
+
+import (
+ "fmt"
+ "net/netip"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/juanfont/headscale/hscontrol/util"
+)
+
+func TestPrefixMap(t *testing.T) {
+ ipp := func(s string) IPPrefix { return IPPrefix(netip.MustParsePrefix(s)) }
+
+ // TODO(kradalby): Remove when we have gotten rid of IPPrefix type
+ prefixComparer := cmp.Comparer(func(x, y IPPrefix) bool {
+ return x == y
+ })
+
+ tests := []struct {
+ rs Routes
+ want map[IPPrefix][]Route
+ }{
+ {
+ rs: Routes{
+ Route{
+ Prefix: ipp("10.0.0.0/24"),
+ },
+ },
+ want: map[IPPrefix][]Route{
+ ipp("10.0.0.0/24"): Routes{
+ Route{
+ Prefix: ipp("10.0.0.0/24"),
+ },
+ },
+ },
+ },
+ {
+ rs: Routes{
+ Route{
+ Prefix: ipp("10.0.0.0/24"),
+ },
+ Route{
+ Prefix: ipp("10.0.1.0/24"),
+ },
+ },
+ want: map[IPPrefix][]Route{
+ ipp("10.0.0.0/24"): Routes{
+ Route{
+ Prefix: ipp("10.0.0.0/24"),
+ },
+ },
+ ipp("10.0.1.0/24"): Routes{
+ Route{
+ Prefix: ipp("10.0.1.0/24"),
+ },
+ },
+ },
+ },
+ {
+ rs: Routes{
+ Route{
+ Prefix: ipp("10.0.0.0/24"),
+ Enabled: true,
+ },
+ Route{
+ Prefix: ipp("10.0.0.0/24"),
+ Enabled: false,
+ },
+ },
+ want: map[IPPrefix][]Route{
+ ipp("10.0.0.0/24"): Routes{
+ Route{
+ Prefix: ipp("10.0.0.0/24"),
+ Enabled: true,
+ },
+ Route{
+ Prefix: ipp("10.0.0.0/24"),
+ Enabled: false,
+ },
+ },
+ },
+ },
+ }
+
+ for idx, tt := range tests {
+ t.Run(fmt.Sprintf("test-%d", idx), func(t *testing.T) {
+ got := tt.rs.PrefixMap()
+ if diff := cmp.Diff(tt.want, got, prefixComparer, util.MkeyComparer, util.NkeyComparer, util.DkeyComparer); diff != "" {
+ t.Errorf("PrefixMap() unexpected result (-want +got):\n%s", diff)
+ }
+ })
+ }
+}
diff --git a/hscontrol/util/file.go b/hscontrol/util/file.go
index 7b424da..5b8656f 100644
--- a/hscontrol/util/file.go
+++ b/hscontrol/util/file.go
@@ -11,11 +11,12 @@ import (
)
const (
- Base8 = 8
- Base10 = 10
- BitSize16 = 16
- BitSize32 = 32
- BitSize64 = 64
+ Base8 = 8
+ Base10 = 10
+ BitSize16 = 16
+ BitSize32 = 32
+ BitSize64 = 64
+ PermissionFallback = 0o700
)
func AbsolutePathFromConfigPath(path string) string {
diff --git a/hscontrol/util/key.go b/hscontrol/util/key.go
index 4eb1db6..6501dac 100644
--- a/hscontrol/util/key.go
+++ b/hscontrol/util/key.go
@@ -4,106 +4,22 @@ import (
"encoding/json"
"errors"
"regexp"
- "strings"
"tailscale.com/types/key"
)
-const (
-
- // These constants are copied from the upstream tailscale.com/types/key
- // library, because they are not exported.
- // https://github.com/tailscale/tailscale/tree/main/types/key
-
- // nodePublicHexPrefix is the prefix used to identify a
- // hex-encoded node public key.
- //
- // This prefix is used in the control protocol, so cannot be
- // changed.
- nodePublicHexPrefix = "nodekey:"
-
- // machinePublicHexPrefix is the prefix used to identify a
- // hex-encoded machine public key.
- //
- // This prefix is used in the control protocol, so cannot be
- // changed.
- machinePublicHexPrefix = "mkey:"
-
- // discoPublicHexPrefix is the prefix used to identify a
- // hex-encoded disco public key.
- //
- // This prefix is used in the control protocol, so cannot be
- // changed.
- discoPublicHexPrefix = "discokey:"
-
- // privateKey prefix.
- privateHexPrefix = "privkey:"
-
- PermissionFallback = 0o700
-
- ZstdCompression = "zstd"
-)
-
var (
NodePublicKeyRegex = regexp.MustCompile("nodekey:[a-fA-F0-9]+")
ErrCannotDecryptResponse = errors.New("cannot decrypt response")
+ ZstdCompression = "zstd"
)
-func MachinePublicKeyStripPrefix(machineKey key.MachinePublic) string {
- return strings.TrimPrefix(machineKey.String(), machinePublicHexPrefix)
-}
-
-func NodePublicKeyStripPrefix(nodeKey key.NodePublic) string {
- return strings.TrimPrefix(nodeKey.String(), nodePublicHexPrefix)
-}
-
-func DiscoPublicKeyStripPrefix(discoKey key.DiscoPublic) string {
- return strings.TrimPrefix(discoKey.String(), discoPublicHexPrefix)
-}
-
-func MachinePublicKeyEnsurePrefix(machineKey string) string {
- if !strings.HasPrefix(machineKey, machinePublicHexPrefix) {
- return machinePublicHexPrefix + machineKey
- }
-
- return machineKey
-}
-
-func NodePublicKeyEnsurePrefix(nodeKey string) string {
- if !strings.HasPrefix(nodeKey, nodePublicHexPrefix) {
- return nodePublicHexPrefix + nodeKey
- }
-
- return nodeKey
-}
-
-func DiscoPublicKeyEnsurePrefix(discoKey string) string {
- if !strings.HasPrefix(discoKey, discoPublicHexPrefix) {
- return discoPublicHexPrefix + discoKey
- }
-
- return discoKey
-}
-
-func PrivateKeyEnsurePrefix(privateKey string) string {
- if !strings.HasPrefix(privateKey, privateHexPrefix) {
- return privateHexPrefix + privateKey
- }
-
- return privateKey
-}
-
func DecodeAndUnmarshalNaCl(
msg []byte,
output interface{},
pubKey *key.MachinePublic,
privKey *key.MachinePrivate,
) error {
- // log.Trace().
- // Str("pubkey", pubKey.ShortString()).
- // Int("length", len(msg)).
- // Msg("Trying to decrypt")
-
decrypted, ok := privKey.OpenFrom(*pubKey, msg)
if !ok {
return ErrCannotDecryptResponse
diff --git a/hscontrol/util/log.go b/hscontrol/util/log.go
index ebbdb79..41d667d 100644
--- a/hscontrol/util/log.go
+++ b/hscontrol/util/log.go
@@ -1,7 +1,16 @@
package util
-import "github.com/rs/zerolog/log"
+import (
+ "github.com/rs/zerolog/log"
+ "tailscale.com/types/logger"
+)
func LogErr(err error, msg string) {
log.Error().Caller().Err(err).Msg(msg)
}
+
+func TSLogfWrapper() logger.Logf {
+ return func(format string, args ...any) {
+ log.Debug().Caller().Msgf(format, args...)
+ }
+}
diff --git a/hscontrol/util/test.go b/hscontrol/util/test.go
new file mode 100644
index 0000000..6d46542
--- /dev/null
+++ b/hscontrol/util/test.go
@@ -0,0 +1,32 @@
+package util
+
+import (
+ "net/netip"
+
+ "github.com/google/go-cmp/cmp"
+ "tailscale.com/types/key"
+)
+
+var PrefixComparer = cmp.Comparer(func(x, y netip.Prefix) bool {
+ return x == y
+})
+
+var IPComparer = cmp.Comparer(func(x, y netip.Addr) bool {
+ return x.Compare(y) == 0
+})
+
+var MkeyComparer = cmp.Comparer(func(x, y key.MachinePublic) bool {
+ return x.String() == y.String()
+})
+
+var NkeyComparer = cmp.Comparer(func(x, y key.NodePublic) bool {
+ return x.String() == y.String()
+})
+
+var DkeyComparer = cmp.Comparer(func(x, y key.DiscoPublic) bool {
+ return x.String() == y.String()
+})
+
+var Comparers []cmp.Option = []cmp.Option{
+ IPComparer, PrefixComparer, MkeyComparer, NkeyComparer, DkeyComparer,
+}
diff --git a/integration/cli_test.go b/integration/cli_test.go
index 61439c3..d2d741e 100644
--- a/integration/cli_test.go
+++ b/integration/cli_test.go
@@ -4,11 +4,11 @@ import (
"encoding/json"
"fmt"
"sort"
- "strconv"
"testing"
"time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
+ "github.com/juanfont/headscale/hscontrol/policy"
"github.com/juanfont/headscale/integration/hsic"
"github.com/juanfont/headscale/integration/tsic"
"github.com/stretchr/testify/assert"
@@ -22,7 +22,7 @@ func executeAndUnmarshal[T any](headscale ControlServer, command []string, resul
err = json.Unmarshal([]byte(str), result)
if err != nil {
- return err
+ return fmt.Errorf("failed to unmarshal: %s\n command err: %s", err, str)
}
return nil
@@ -60,7 +60,7 @@ func TestUserCommand(t *testing.T) {
)
assertNoErr(t, err)
- result := []string{listUsers[0].Name, listUsers[1].Name}
+ result := []string{listUsers[0].GetName(), listUsers[1].GetName()}
sort.Strings(result)
assert.Equal(
@@ -95,7 +95,7 @@ func TestUserCommand(t *testing.T) {
)
assertNoErr(t, err)
- result = []string{listAfterRenameUsers[0].Name, listAfterRenameUsers[1].Name}
+ result = []string{listAfterRenameUsers[0].GetName(), listAfterRenameUsers[1].GetName()}
sort.Strings(result)
assert.Equal(
@@ -177,29 +177,33 @@ func TestPreAuthKeyCommand(t *testing.T) {
assert.Equal(
t,
- []string{keys[0].Id, keys[1].Id, keys[2].Id},
- []string{listedPreAuthKeys[1].Id, listedPreAuthKeys[2].Id, listedPreAuthKeys[3].Id},
+ []string{keys[0].GetId(), keys[1].GetId(), keys[2].GetId()},
+ []string{
+ listedPreAuthKeys[1].GetId(),
+ listedPreAuthKeys[2].GetId(),
+ listedPreAuthKeys[3].GetId(),
+ },
)
- assert.NotEmpty(t, listedPreAuthKeys[1].Key)
- assert.NotEmpty(t, listedPreAuthKeys[2].Key)
- assert.NotEmpty(t, listedPreAuthKeys[3].Key)
+ assert.NotEmpty(t, listedPreAuthKeys[1].GetKey())
+ assert.NotEmpty(t, listedPreAuthKeys[2].GetKey())
+ assert.NotEmpty(t, listedPreAuthKeys[3].GetKey())
- assert.True(t, listedPreAuthKeys[1].Expiration.AsTime().After(time.Now()))
- assert.True(t, listedPreAuthKeys[2].Expiration.AsTime().After(time.Now()))
- assert.True(t, listedPreAuthKeys[3].Expiration.AsTime().After(time.Now()))
+ assert.True(t, listedPreAuthKeys[1].GetExpiration().AsTime().After(time.Now()))
+ assert.True(t, listedPreAuthKeys[2].GetExpiration().AsTime().After(time.Now()))
+ assert.True(t, listedPreAuthKeys[3].GetExpiration().AsTime().After(time.Now()))
assert.True(
t,
- listedPreAuthKeys[1].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)),
+ listedPreAuthKeys[1].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)),
)
assert.True(
t,
- listedPreAuthKeys[2].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)),
+ listedPreAuthKeys[2].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)),
)
assert.True(
t,
- listedPreAuthKeys[3].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)),
+ listedPreAuthKeys[3].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)),
)
for index := range listedPreAuthKeys {
@@ -207,7 +211,7 @@ func TestPreAuthKeyCommand(t *testing.T) {
continue
}
- assert.Equal(t, listedPreAuthKeys[index].AclTags, []string{"tag:test1", "tag:test2"})
+ assert.Equal(t, listedPreAuthKeys[index].GetAclTags(), []string{"tag:test1", "tag:test2"})
}
// Test key expiry
@@ -218,7 +222,7 @@ func TestPreAuthKeyCommand(t *testing.T) {
"--user",
user,
"expire",
- listedPreAuthKeys[1].Key,
+ listedPreAuthKeys[1].GetKey(),
},
)
assertNoErr(t, err)
@@ -239,9 +243,9 @@ func TestPreAuthKeyCommand(t *testing.T) {
)
assertNoErr(t, err)
- assert.True(t, listedPreAuthKeysAfterExpire[1].Expiration.AsTime().Before(time.Now()))
- assert.True(t, listedPreAuthKeysAfterExpire[2].Expiration.AsTime().After(time.Now()))
- assert.True(t, listedPreAuthKeysAfterExpire[3].Expiration.AsTime().After(time.Now()))
+ assert.True(t, listedPreAuthKeysAfterExpire[1].GetExpiration().AsTime().Before(time.Now()))
+ assert.True(t, listedPreAuthKeysAfterExpire[2].GetExpiration().AsTime().After(time.Now()))
+ assert.True(t, listedPreAuthKeysAfterExpire[3].GetExpiration().AsTime().After(time.Now()))
}
func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) {
@@ -300,10 +304,10 @@ func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) {
// There is one key created by "scenario.CreateHeadscaleEnv"
assert.Len(t, listedPreAuthKeys, 2)
- assert.True(t, listedPreAuthKeys[1].Expiration.AsTime().After(time.Now()))
+ assert.True(t, listedPreAuthKeys[1].GetExpiration().AsTime().After(time.Now()))
assert.True(
t,
- listedPreAuthKeys[1].Expiration.AsTime().Before(time.Now().Add(time.Minute*70)),
+ listedPreAuthKeys[1].GetExpiration().AsTime().Before(time.Now().Add(time.Minute*70)),
)
}
@@ -384,141 +388,6 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
assert.Len(t, listedPreAuthKeys, 3)
}
-func TestEnablingRoutes(t *testing.T) {
- IntegrationSkip(t)
- t.Parallel()
-
- user := "enable-routing"
-
- scenario, err := NewScenario()
- assertNoErrf(t, "failed to create scenario: %s", err)
- defer scenario.Shutdown()
-
- spec := map[string]int{
- user: 3,
- }
-
- err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clienableroute"))
- assertNoErrHeadscaleEnv(t, err)
-
- allClients, err := scenario.ListTailscaleClients()
- assertNoErrListClients(t, err)
-
- err = scenario.WaitForTailscaleSync()
- assertNoErrSync(t, err)
-
- headscale, err := scenario.Headscale()
- assertNoErrGetHeadscale(t, err)
-
- // advertise routes using the up command
- for i, client := range allClients {
- routeStr := fmt.Sprintf("10.0.%d.0/24", i)
- command := []string{
- "tailscale",
- "set",
- "--advertise-routes=" + routeStr,
- }
- _, _, err := client.Execute(command)
- assertNoErrf(t, "failed to advertise route: %s", err)
- }
-
- err = scenario.WaitForTailscaleSync()
- assertNoErrSync(t, err)
-
- var routes []*v1.Route
- err = executeAndUnmarshal(
- headscale,
- []string{
- "headscale",
- "routes",
- "list",
- "--output",
- "json",
- },
- &routes,
- )
-
- assertNoErr(t, err)
- assert.Len(t, routes, 3)
-
- for _, route := range routes {
- assert.Equal(t, route.Advertised, true)
- assert.Equal(t, route.Enabled, false)
- assert.Equal(t, route.IsPrimary, false)
- }
-
- for _, route := range routes {
- _, err = headscale.Execute(
- []string{
- "headscale",
- "routes",
- "enable",
- "--route",
- strconv.Itoa(int(route.Id)),
- })
- assertNoErr(t, err)
- }
-
- var enablingRoutes []*v1.Route
- err = executeAndUnmarshal(
- headscale,
- []string{
- "headscale",
- "routes",
- "list",
- "--output",
- "json",
- },
- &enablingRoutes,
- )
- assertNoErr(t, err)
- assert.Len(t, enablingRoutes, 3)
-
- for _, route := range enablingRoutes {
- assert.Equal(t, route.Advertised, true)
- assert.Equal(t, route.Enabled, true)
- assert.Equal(t, route.IsPrimary, true)
- }
-
- routeIDToBeDisabled := enablingRoutes[0].Id
-
- _, err = headscale.Execute(
- []string{
- "headscale",
- "routes",
- "disable",
- "--route",
- strconv.Itoa(int(routeIDToBeDisabled)),
- })
- assertNoErr(t, err)
-
- var disablingRoutes []*v1.Route
- err = executeAndUnmarshal(
- headscale,
- []string{
- "headscale",
- "routes",
- "list",
- "--output",
- "json",
- },
- &disablingRoutes,
- )
- assertNoErr(t, err)
-
- for _, route := range disablingRoutes {
- assert.Equal(t, true, route.Advertised)
-
- if route.Id == routeIDToBeDisabled {
- assert.Equal(t, route.Enabled, false)
- assert.Equal(t, route.IsPrimary, false)
- } else {
- assert.Equal(t, route.Enabled, true)
- assert.Equal(t, route.IsPrimary, true)
- }
- }
-}
-
func TestApiKeyCommand(t *testing.T) {
IntegrationSkip(t)
t.Parallel()
@@ -577,43 +446,43 @@ func TestApiKeyCommand(t *testing.T) {
assert.Len(t, listedAPIKeys, 5)
- assert.Equal(t, uint64(1), listedAPIKeys[0].Id)
- assert.Equal(t, uint64(2), listedAPIKeys[1].Id)
- assert.Equal(t, uint64(3), listedAPIKeys[2].Id)
- assert.Equal(t, uint64(4), listedAPIKeys[3].Id)
- assert.Equal(t, uint64(5), listedAPIKeys[4].Id)
+ assert.Equal(t, uint64(1), listedAPIKeys[0].GetId())
+ assert.Equal(t, uint64(2), listedAPIKeys[1].GetId())
+ assert.Equal(t, uint64(3), listedAPIKeys[2].GetId())
+ assert.Equal(t, uint64(4), listedAPIKeys[3].GetId())
+ assert.Equal(t, uint64(5), listedAPIKeys[4].GetId())
- assert.NotEmpty(t, listedAPIKeys[0].Prefix)
- assert.NotEmpty(t, listedAPIKeys[1].Prefix)
- assert.NotEmpty(t, listedAPIKeys[2].Prefix)
- assert.NotEmpty(t, listedAPIKeys[3].Prefix)
- assert.NotEmpty(t, listedAPIKeys[4].Prefix)
+ assert.NotEmpty(t, listedAPIKeys[0].GetPrefix())
+ assert.NotEmpty(t, listedAPIKeys[1].GetPrefix())
+ assert.NotEmpty(t, listedAPIKeys[2].GetPrefix())
+ assert.NotEmpty(t, listedAPIKeys[3].GetPrefix())
+ assert.NotEmpty(t, listedAPIKeys[4].GetPrefix())
- assert.True(t, listedAPIKeys[0].Expiration.AsTime().After(time.Now()))
- assert.True(t, listedAPIKeys[1].Expiration.AsTime().After(time.Now()))
- assert.True(t, listedAPIKeys[2].Expiration.AsTime().After(time.Now()))
- assert.True(t, listedAPIKeys[3].Expiration.AsTime().After(time.Now()))
- assert.True(t, listedAPIKeys[4].Expiration.AsTime().After(time.Now()))
+ assert.True(t, listedAPIKeys[0].GetExpiration().AsTime().After(time.Now()))
+ assert.True(t, listedAPIKeys[1].GetExpiration().AsTime().After(time.Now()))
+ assert.True(t, listedAPIKeys[2].GetExpiration().AsTime().After(time.Now()))
+ assert.True(t, listedAPIKeys[3].GetExpiration().AsTime().After(time.Now()))
+ assert.True(t, listedAPIKeys[4].GetExpiration().AsTime().After(time.Now()))
assert.True(
t,
- listedAPIKeys[0].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)),
+ listedAPIKeys[0].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)),
)
assert.True(
t,
- listedAPIKeys[1].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)),
+ listedAPIKeys[1].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)),
)
assert.True(
t,
- listedAPIKeys[2].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)),
+ listedAPIKeys[2].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)),
)
assert.True(
t,
- listedAPIKeys[3].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)),
+ listedAPIKeys[3].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)),
)
assert.True(
t,
- listedAPIKeys[4].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)),
+ listedAPIKeys[4].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)),
)
expiredPrefixes := make(map[string]bool)
@@ -626,12 +495,12 @@ func TestApiKeyCommand(t *testing.T) {
"apikeys",
"expire",
"--prefix",
- listedAPIKeys[idx].Prefix,
+ listedAPIKeys[idx].GetPrefix(),
},
)
assert.Nil(t, err)
- expiredPrefixes[listedAPIKeys[idx].Prefix] = true
+ expiredPrefixes[listedAPIKeys[idx].GetPrefix()] = true
}
var listedAfterExpireAPIKeys []v1.ApiKey
@@ -648,17 +517,17 @@ func TestApiKeyCommand(t *testing.T) {
assert.Nil(t, err)
for index := range listedAfterExpireAPIKeys {
- if _, ok := expiredPrefixes[listedAfterExpireAPIKeys[index].Prefix]; ok {
+ if _, ok := expiredPrefixes[listedAfterExpireAPIKeys[index].GetPrefix()]; ok {
// Expired
assert.True(
t,
- listedAfterExpireAPIKeys[index].Expiration.AsTime().Before(time.Now()),
+ listedAfterExpireAPIKeys[index].GetExpiration().AsTime().Before(time.Now()),
)
} else {
// Not expired
assert.False(
t,
- listedAfterExpireAPIKeys[index].Expiration.AsTime().Before(time.Now()),
+ listedAfterExpireAPIKeys[index].GetExpiration().AsTime().Before(time.Now()),
)
}
}
@@ -683,8 +552,8 @@ func TestNodeTagCommand(t *testing.T) {
assertNoErr(t, err)
machineKeys := []string{
- "nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
- "nodekey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
+ "mkey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
+ "mkey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
}
nodes := make([]*v1.Node, len(machineKeys))
assert.Nil(t, err)
@@ -744,7 +613,7 @@ func TestNodeTagCommand(t *testing.T) {
)
assert.Nil(t, err)
- assert.Equal(t, []string{"tag:test"}, node.ForcedTags)
+ assert.Equal(t, []string{"tag:test"}, node.GetForcedTags())
// try to set a wrong tag and retrieve the error
type errOutput struct {
@@ -781,8 +650,8 @@ func TestNodeTagCommand(t *testing.T) {
assert.Nil(t, err)
found := false
for _, node := range resultMachines {
- if node.ForcedTags != nil {
- for _, tag := range node.ForcedTags {
+ if node.GetForcedTags() != nil {
+ for _, tag := range node.GetForcedTags() {
if tag == "tag:test" {
found = true
}
@@ -797,6 +666,119 @@ func TestNodeTagCommand(t *testing.T) {
)
}
+func TestNodeAdvertiseTagNoACLCommand(t *testing.T) {
+ IntegrationSkip(t)
+ t.Parallel()
+
+ scenario, err := NewScenario()
+ assertNoErr(t, err)
+ defer scenario.Shutdown()
+
+ spec := map[string]int{
+ "user1": 1,
+ }
+
+ err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{tsic.WithTags([]string{"tag:test"})}, hsic.WithTestName("cliadvtags"))
+ assertNoErr(t, err)
+
+ headscale, err := scenario.Headscale()
+ assertNoErr(t, err)
+
+ // Test list all nodes after added seconds
+ resultMachines := make([]*v1.Node, spec["user1"])
+ err = executeAndUnmarshal(
+ headscale,
+ []string{
+ "headscale",
+ "nodes",
+ "list",
+ "--tags",
+ "--output", "json",
+ },
+ &resultMachines,
+ )
+ assert.Nil(t, err)
+ found := false
+ for _, node := range resultMachines {
+ if node.GetInvalidTags() != nil {
+ for _, tag := range node.GetInvalidTags() {
+ if tag == "tag:test" {
+ found = true
+ }
+ }
+ }
+ }
+ assert.Equal(
+ t,
+ true,
+ found,
+ "should not find a node with the tag 'tag:test' in the list of nodes",
+ )
+}
+
+func TestNodeAdvertiseTagWithACLCommand(t *testing.T) {
+ IntegrationSkip(t)
+ t.Parallel()
+
+ scenario, err := NewScenario()
+ assertNoErr(t, err)
+ defer scenario.Shutdown()
+
+ spec := map[string]int{
+ "user1": 1,
+ }
+
+ err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{tsic.WithTags([]string{"tag:exists"})}, hsic.WithTestName("cliadvtags"), hsic.WithACLPolicy(
+ &policy.ACLPolicy{
+ ACLs: []policy.ACL{
+ {
+ Action: "accept",
+ Sources: []string{"*"},
+ Destinations: []string{"*:*"},
+ },
+ },
+ TagOwners: map[string][]string{
+ "tag:exists": {"user1"},
+ },
+ },
+ ))
+ assertNoErr(t, err)
+
+ headscale, err := scenario.Headscale()
+ assertNoErr(t, err)
+
+ // Test list all nodes after added seconds
+ resultMachines := make([]*v1.Node, spec["user1"])
+ err = executeAndUnmarshal(
+ headscale,
+ []string{
+ "headscale",
+ "nodes",
+ "list",
+ "--tags",
+ "--output", "json",
+ },
+ &resultMachines,
+ )
+ assert.Nil(t, err)
+ found := false
+ for _, node := range resultMachines {
+ if node.GetValidTags() != nil {
+ for _, tag := range node.GetValidTags() {
+ if tag == "tag:exists" {
+ found = true
+ }
+ }
+ }
+ }
+ assert.Equal(
+ t,
+ true,
+ found,
+ "should not find a node with the tag 'tag:exists' in the list of nodes",
+ )
+}
+
func TestNodeCommand(t *testing.T) {
IntegrationSkip(t)
t.Parallel()
@@ -816,13 +798,13 @@ func TestNodeCommand(t *testing.T) {
headscale, err := scenario.Headscale()
assertNoErr(t, err)
- // Randomly generated node keys
+ // Pregenerated machine keys
machineKeys := []string{
- "nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
- "nodekey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
- "nodekey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
- "nodekey:8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1",
- "nodekey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
+ "mkey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
+ "mkey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
+ "mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
+ "mkey:8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1",
+ "mkey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
}
nodes := make([]*v1.Node, len(machineKeys))
assert.Nil(t, err)
@@ -885,21 +867,21 @@ func TestNodeCommand(t *testing.T) {
assert.Len(t, listAll, 5)
- assert.Equal(t, uint64(1), listAll[0].Id)
- assert.Equal(t, uint64(2), listAll[1].Id)
- assert.Equal(t, uint64(3), listAll[2].Id)
- assert.Equal(t, uint64(4), listAll[3].Id)
- assert.Equal(t, uint64(5), listAll[4].Id)
+ assert.Equal(t, uint64(1), listAll[0].GetId())
+ assert.Equal(t, uint64(2), listAll[1].GetId())
+ assert.Equal(t, uint64(3), listAll[2].GetId())
+ assert.Equal(t, uint64(4), listAll[3].GetId())
+ assert.Equal(t, uint64(5), listAll[4].GetId())
- assert.Equal(t, "node-1", listAll[0].Name)
- assert.Equal(t, "node-2", listAll[1].Name)
- assert.Equal(t, "node-3", listAll[2].Name)
- assert.Equal(t, "node-4", listAll[3].Name)
- assert.Equal(t, "node-5", listAll[4].Name)
+ assert.Equal(t, "node-1", listAll[0].GetName())
+ assert.Equal(t, "node-2", listAll[1].GetName())
+ assert.Equal(t, "node-3", listAll[2].GetName())
+ assert.Equal(t, "node-4", listAll[3].GetName())
+ assert.Equal(t, "node-5", listAll[4].GetName())
otherUserMachineKeys := []string{
- "nodekey:b5b444774186d4217adcec407563a1223929465ee2c68a4da13af0d0185b4f8e",
- "nodekey:dc721977ac7415aafa87f7d4574cbe07c6b171834a6d37375782bdc1fb6b3584",
+ "mkey:b5b444774186d4217adcec407563a1223929465ee2c68a4da13af0d0185b4f8e",
+ "mkey:dc721977ac7415aafa87f7d4574cbe07c6b171834a6d37375782bdc1fb6b3584",
}
otherUserMachines := make([]*v1.Node, len(otherUserMachineKeys))
assert.Nil(t, err)
@@ -963,11 +945,11 @@ func TestNodeCommand(t *testing.T) {
// All nodes, nodes + otherUser
assert.Len(t, listAllWithotherUser, 7)
- assert.Equal(t, uint64(6), listAllWithotherUser[5].Id)
- assert.Equal(t, uint64(7), listAllWithotherUser[6].Id)
+ assert.Equal(t, uint64(6), listAllWithotherUser[5].GetId())
+ assert.Equal(t, uint64(7), listAllWithotherUser[6].GetId())
- assert.Equal(t, "otherUser-node-1", listAllWithotherUser[5].Name)
- assert.Equal(t, "otherUser-node-2", listAllWithotherUser[6].Name)
+ assert.Equal(t, "otherUser-node-1", listAllWithotherUser[5].GetName())
+ assert.Equal(t, "otherUser-node-2", listAllWithotherUser[6].GetName())
// Test list all nodes after added otherUser
var listOnlyotherUserMachineUser []v1.Node
@@ -988,18 +970,18 @@ func TestNodeCommand(t *testing.T) {
assert.Len(t, listOnlyotherUserMachineUser, 2)
- assert.Equal(t, uint64(6), listOnlyotherUserMachineUser[0].Id)
- assert.Equal(t, uint64(7), listOnlyotherUserMachineUser[1].Id)
+ assert.Equal(t, uint64(6), listOnlyotherUserMachineUser[0].GetId())
+ assert.Equal(t, uint64(7), listOnlyotherUserMachineUser[1].GetId())
assert.Equal(
t,
"otherUser-node-1",
- listOnlyotherUserMachineUser[0].Name,
+ listOnlyotherUserMachineUser[0].GetName(),
)
assert.Equal(
t,
"otherUser-node-2",
- listOnlyotherUserMachineUser[1].Name,
+ listOnlyotherUserMachineUser[1].GetName(),
)
// Delete a nodes
@@ -1056,13 +1038,13 @@ func TestNodeExpireCommand(t *testing.T) {
headscale, err := scenario.Headscale()
assertNoErr(t, err)
- // Randomly generated node keys
+ // Pregenerated machine keys
machineKeys := []string{
- "nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
- "nodekey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
- "nodekey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
- "nodekey:8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1",
- "nodekey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
+ "mkey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
+ "mkey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
+ "mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
+ "mkey:8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1",
+ "mkey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
}
nodes := make([]*v1.Node, len(machineKeys))
@@ -1123,11 +1105,11 @@ func TestNodeExpireCommand(t *testing.T) {
assert.Len(t, listAll, 5)
- assert.True(t, listAll[0].Expiry.AsTime().IsZero())
- assert.True(t, listAll[1].Expiry.AsTime().IsZero())
- assert.True(t, listAll[2].Expiry.AsTime().IsZero())
- assert.True(t, listAll[3].Expiry.AsTime().IsZero())
- assert.True(t, listAll[4].Expiry.AsTime().IsZero())
+ assert.True(t, listAll[0].GetExpiry().AsTime().IsZero())
+ assert.True(t, listAll[1].GetExpiry().AsTime().IsZero())
+ assert.True(t, listAll[2].GetExpiry().AsTime().IsZero())
+ assert.True(t, listAll[3].GetExpiry().AsTime().IsZero())
+ assert.True(t, listAll[4].GetExpiry().AsTime().IsZero())
for idx := 0; idx < 3; idx++ {
_, err := headscale.Execute(
@@ -1136,7 +1118,7 @@ func TestNodeExpireCommand(t *testing.T) {
"nodes",
"expire",
"--identifier",
- fmt.Sprintf("%d", listAll[idx].Id),
+ fmt.Sprintf("%d", listAll[idx].GetId()),
},
)
assert.Nil(t, err)
@@ -1158,11 +1140,11 @@ func TestNodeExpireCommand(t *testing.T) {
assert.Len(t, listAllAfterExpiry, 5)
- assert.True(t, listAllAfterExpiry[0].Expiry.AsTime().Before(time.Now()))
- assert.True(t, listAllAfterExpiry[1].Expiry.AsTime().Before(time.Now()))
- assert.True(t, listAllAfterExpiry[2].Expiry.AsTime().Before(time.Now()))
- assert.True(t, listAllAfterExpiry[3].Expiry.AsTime().IsZero())
- assert.True(t, listAllAfterExpiry[4].Expiry.AsTime().IsZero())
+ assert.True(t, listAllAfterExpiry[0].GetExpiry().AsTime().Before(time.Now()))
+ assert.True(t, listAllAfterExpiry[1].GetExpiry().AsTime().Before(time.Now()))
+ assert.True(t, listAllAfterExpiry[2].GetExpiry().AsTime().Before(time.Now()))
+ assert.True(t, listAllAfterExpiry[3].GetExpiry().AsTime().IsZero())
+ assert.True(t, listAllAfterExpiry[4].GetExpiry().AsTime().IsZero())
}
func TestNodeRenameCommand(t *testing.T) {
@@ -1183,13 +1165,13 @@ func TestNodeRenameCommand(t *testing.T) {
headscale, err := scenario.Headscale()
assertNoErr(t, err)
- // Randomly generated node keys
+ // Pregenerated machine keys
machineKeys := []string{
- "nodekey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
- "nodekey:8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1",
- "nodekey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
- "nodekey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
- "nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
+ "mkey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
+ "mkey:8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1",
+ "mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
+ "mkey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
+ "mkey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
}
nodes := make([]*v1.Node, len(machineKeys))
assert.Nil(t, err)
@@ -1210,7 +1192,7 @@ func TestNodeRenameCommand(t *testing.T) {
"json",
},
)
- assert.Nil(t, err)
+ assertNoErr(t, err)
var node v1.Node
err = executeAndUnmarshal(
@@ -1228,7 +1210,7 @@ func TestNodeRenameCommand(t *testing.T) {
},
&node,
)
- assert.Nil(t, err)
+ assertNoErr(t, err)
nodes[index] = &node
}
@@ -1264,7 +1246,7 @@ func TestNodeRenameCommand(t *testing.T) {
"nodes",
"rename",
"--identifier",
- fmt.Sprintf("%d", listAll[idx].Id),
+ fmt.Sprintf("%d", listAll[idx].GetId()),
fmt.Sprintf("newnode-%d", idx+1),
},
)
@@ -1300,7 +1282,7 @@ func TestNodeRenameCommand(t *testing.T) {
"nodes",
"rename",
"--identifier",
- fmt.Sprintf("%d", listAll[4].Id),
+ fmt.Sprintf("%d", listAll[4].GetId()),
"testmaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaachine12345678901234567890",
},
)
@@ -1350,7 +1332,7 @@ func TestNodeMoveCommand(t *testing.T) {
assertNoErr(t, err)
// Randomly generated node key
- machineKey := "nodekey:688411b767663479632d44140f08a9fde87383adc7cdeb518f62ce28a17ef0aa"
+ machineKey := "mkey:688411b767663479632d44140f08a9fde87383adc7cdeb518f62ce28a17ef0aa"
_, err = headscale.Execute(
[]string{
@@ -1387,11 +1369,11 @@ func TestNodeMoveCommand(t *testing.T) {
)
assert.Nil(t, err)
- assert.Equal(t, uint64(1), node.Id)
- assert.Equal(t, "nomad-node", node.Name)
- assert.Equal(t, node.User.Name, "old-user")
+ assert.Equal(t, uint64(1), node.GetId())
+ assert.Equal(t, "nomad-node", node.GetName())
+ assert.Equal(t, node.GetUser().GetName(), "old-user")
- nodeID := fmt.Sprintf("%d", node.Id)
+ nodeID := fmt.Sprintf("%d", node.GetId())
err = executeAndUnmarshal(
headscale,
@@ -1410,7 +1392,7 @@ func TestNodeMoveCommand(t *testing.T) {
)
assert.Nil(t, err)
- assert.Equal(t, node.User.Name, "new-user")
+ assert.Equal(t, node.GetUser().GetName(), "new-user")
var allNodes []v1.Node
err = executeAndUnmarshal(
@@ -1428,9 +1410,9 @@ func TestNodeMoveCommand(t *testing.T) {
assert.Len(t, allNodes, 1)
- assert.Equal(t, allNodes[0].Id, node.Id)
- assert.Equal(t, allNodes[0].User, node.User)
- assert.Equal(t, allNodes[0].User.Name, "new-user")
+ assert.Equal(t, allNodes[0].GetId(), node.GetId())
+ assert.Equal(t, allNodes[0].GetUser(), node.GetUser())
+ assert.Equal(t, allNodes[0].GetUser().GetName(), "new-user")
moveToNonExistingNSResult, err := headscale.Execute(
[]string{
@@ -1452,7 +1434,7 @@ func TestNodeMoveCommand(t *testing.T) {
moveToNonExistingNSResult,
"user not found",
)
- assert.Equal(t, node.User.Name, "new-user")
+ assert.Equal(t, node.GetUser().GetName(), "new-user")
err = executeAndUnmarshal(
headscale,
@@ -1471,7 +1453,7 @@ func TestNodeMoveCommand(t *testing.T) {
)
assert.Nil(t, err)
- assert.Equal(t, node.User.Name, "old-user")
+ assert.Equal(t, node.GetUser().GetName(), "old-user")
err = executeAndUnmarshal(
headscale,
@@ -1490,5 +1472,5 @@ func TestNodeMoveCommand(t *testing.T) {
)
assert.Nil(t, err)
- assert.Equal(t, node.User.Name, "old-user")
+ assert.Equal(t, node.GetUser().GetName(), "old-user")
}
diff --git a/integration/embedded_derp_test.go b/integration/embedded_derp_test.go
index 669faf5..3a40749 100644
--- a/integration/embedded_derp_test.go
+++ b/integration/embedded_derp_test.go
@@ -43,6 +43,10 @@ func TestDERPServerScenario(t *testing.T) {
headscaleConfig["HEADSCALE_DERP_SERVER_REGION_CODE"] = "headscale"
headscaleConfig["HEADSCALE_DERP_SERVER_REGION_NAME"] = "Headscale Embedded DERP"
headscaleConfig["HEADSCALE_DERP_SERVER_STUN_LISTEN_ADDR"] = "0.0.0.0:3478"
+ headscaleConfig["HEADSCALE_DERP_SERVER_PRIVATE_KEY_PATH"] = "/tmp/derp.key"
+ // Envknob for enabling DERP debug logs
+ headscaleConfig["DERP_DEBUG_LOGS"] = "true"
+ headscaleConfig["DERP_PROBER_DEBUG_LOGS"] = "true"
err = scenario.CreateHeadscaleEnv(
spec,
diff --git a/integration/general_test.go b/integration/general_test.go
index ca2394a..c092844 100644
--- a/integration/general_test.go
+++ b/integration/general_test.go
@@ -14,6 +14,8 @@ import (
"github.com/rs/zerolog/log"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
+ "tailscale.com/client/tailscale/apitype"
+ "tailscale.com/types/key"
)
func TestPingAllByIP(t *testing.T) {
@@ -248,9 +250,8 @@ func TestPingAllByHostname(t *testing.T) {
defer scenario.Shutdown()
spec := map[string]int{
- // Omit 1.16.2 (-1) because it does not have the FQDN field
- "user3": len(MustTestVersions) - 1,
- "user4": len(MustTestVersions) - 1,
+ "user3": len(MustTestVersions),
+ "user4": len(MustTestVersions),
}
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("pingallbyname"))
@@ -296,8 +297,7 @@ func TestTaildrop(t *testing.T) {
defer scenario.Shutdown()
spec := map[string]int{
- // Omit 1.16.2 (-1) because it does not have the FQDN field
- "taildrop": len(MustTestVersions) - 1,
+ "taildrop": len(MustTestVersions),
}
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("taildrop"))
@@ -313,6 +313,42 @@ func TestTaildrop(t *testing.T) {
_, err = scenario.ListTailscaleClientsFQDNs()
assertNoErrListFQDN(t, err)
+ for _, client := range allClients {
+ if !strings.Contains(client.Hostname(), "head") {
+ command := []string{"apk", "add", "curl"}
+ _, _, err := client.Execute(command)
+ if err != nil {
+ t.Fatalf("failed to install curl on %s, err: %s", client.Hostname(), err)
+ }
+
+ }
+ curlCommand := []string{"curl", "--unix-socket", "/var/run/tailscale/tailscaled.sock", "http://local-tailscaled.sock/localapi/v0/file-targets"}
+ err = retry(10, 1*time.Second, func() error {
+ result, _, err := client.Execute(curlCommand)
+ if err != nil {
+ return err
+ }
+ var fts []apitype.FileTarget
+ err = json.Unmarshal([]byte(result), &fts)
+ if err != nil {
+ return err
+ }
+
+ if len(fts) != len(allClients)-1 {
+ ftStr := fmt.Sprintf("FileTargets for %s:\n", client.Hostname())
+ for _, ft := range fts {
+ ftStr += fmt.Sprintf("\t%s\n", ft.Node.Name)
+ }
+ return fmt.Errorf("client %s does not have all its peers as FileTargets, got %d, want: %d\n%s", client.Hostname(), len(fts), len(allClients)-1, ftStr)
+ }
+
+ return err
+ })
+ if err != nil {
+ t.Errorf("failed to query localapi for filetarget on %s, err: %s", client.Hostname(), err)
+ }
+ }
+
for _, client := range allClients {
command := []string{"touch", fmt.Sprintf("/tmp/file_from_%s", client.Hostname())}
@@ -347,8 +383,9 @@ func TestTaildrop(t *testing.T) {
})
if err != nil {
t.Fatalf(
- "failed to send taildrop file on %s, err: %s",
+ "failed to send taildrop file on %s with command %q, err: %s",
client.Hostname(),
+ strings.Join(command, " "),
err,
)
}
@@ -500,7 +537,7 @@ func TestExpireNode(t *testing.T) {
assertNoErr(t, err)
// Assert that we have the original count - self
- assert.Len(t, status.Peers(), len(MustTestVersions)-1)
+ assert.Len(t, status.Peers(), spec["user1"]-1)
}
headscale, err := scenario.Headscale()
@@ -517,25 +554,188 @@ func TestExpireNode(t *testing.T) {
err = json.Unmarshal([]byte(result), &node)
assertNoErr(t, err)
- time.Sleep(30 * time.Second)
+ var expiredNodeKey key.NodePublic
+ err = expiredNodeKey.UnmarshalText([]byte(node.GetNodeKey()))
+ assertNoErr(t, err)
- // Verify that the expired not is no longer present in the Peer list
- // of connected nodes.
+ t.Logf("Node %s with node_key %s has been expired", node.GetName(), expiredNodeKey.String())
+
+ time.Sleep(2 * time.Minute)
+
+ now := time.Now()
+
+ // Verify that the expired node has been marked in all peers list.
for _, client := range allClients {
status, err := client.Status()
assertNoErr(t, err)
- for _, peerKey := range status.Peers() {
- peerStatus := status.Peer[peerKey]
+ if client.Hostname() != node.GetName() {
+ t.Logf("available peers of %s: %v", client.Hostname(), status.Peers())
- peerPublicKey := strings.TrimPrefix(peerStatus.PublicKey.String(), "nodekey:")
+ // Ensures that the node is present, and that it is expired.
+ if peerStatus, ok := status.Peer[expiredNodeKey]; ok {
+ assertNotNil(t, peerStatus.Expired)
+ assert.NotNil(t, peerStatus.KeyExpiry)
- assert.NotEqual(t, node.NodeKey, peerPublicKey)
- }
+ t.Logf("node %q should have a key expire before %s, was %s", peerStatus.HostName, now.String(), peerStatus.KeyExpiry)
+ if peerStatus.KeyExpiry != nil {
+ assert.Truef(t, peerStatus.KeyExpiry.Before(now), "node %q should have a key expire before %s, was %s", peerStatus.HostName, now.String(), peerStatus.KeyExpiry)
+ }
- if client.Hostname() != node.Name {
- // Assert that we have the original count - self - expired node
- assert.Len(t, status.Peers(), len(MustTestVersions)-2)
+ assert.Truef(t, peerStatus.Expired, "node %q should be expired, expired is %v", peerStatus.HostName, peerStatus.Expired)
+
+ _, stderr, _ := client.Execute([]string{"tailscale", "ping", node.GetName()})
+ if !strings.Contains(stderr, "node key has expired") {
+ t.Errorf("expected to be unable to ping expired host %q from %q", node.GetName(), client.Hostname())
+ }
+ } else {
+ t.Errorf("failed to find node %q with nodekey (%s) in mapresponse, should be present even if it is expired", node.GetName(), expiredNodeKey)
+ }
+ } else {
+ if status.Self.KeyExpiry != nil {
+ assert.Truef(t, status.Self.KeyExpiry.Before(now), "node %q should have a key expire before %s, was %s", status.Self.HostName, now.String(), status.Self.KeyExpiry)
+ }
+
+ // NeedsLogin means that the node has understood that it is no longer
+ // valid.
+ assert.Equal(t, "NeedsLogin", status.BackendState)
}
}
}
+
+func TestNodeOnlineLastSeenStatus(t *testing.T) {
+ IntegrationSkip(t)
+ t.Parallel()
+
+ scenario, err := NewScenario()
+ assertNoErr(t, err)
+ defer scenario.Shutdown()
+
+ spec := map[string]int{
+ "user1": len(MustTestVersions),
+ }
+
+ err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("onlinelastseen"))
+ assertNoErrHeadscaleEnv(t, err)
+
+ 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("before expire: %d successful pings out of %d", success, len(allClients)*len(allIps))
+
+ for _, client := range allClients {
+ status, err := client.Status()
+ assertNoErr(t, err)
+
+ // Assert that we have the original count - self
+ assert.Len(t, status.Peers(), len(MustTestVersions)-1)
+ }
+
+ headscale, err := scenario.Headscale()
+ assertNoErr(t, err)
+
+ keepAliveInterval := 60 * time.Second
+
+ // Duration is chosen arbitrarily, 10m is reported in #1561
+ testDuration := 12 * time.Minute
+ start := time.Now()
+ end := start.Add(testDuration)
+
+ log.Printf("Starting online test from %v to %v", start, end)
+
+ for {
+ // Let the test run continuously for X minutes to verify
+ // all nodes stay connected and has the expected status over time.
+ if end.Before(time.Now()) {
+ return
+ }
+
+ result, err := headscale.Execute([]string{
+ "headscale", "nodes", "list", "--output", "json",
+ })
+ assertNoErr(t, err)
+
+ var nodes []*v1.Node
+ err = json.Unmarshal([]byte(result), &nodes)
+ assertNoErr(t, err)
+
+ now := time.Now()
+
+ // Threshold with some leeway
+ lastSeenThreshold := now.Add(-keepAliveInterval - (10 * time.Second))
+
+ // Verify that headscale reports the nodes as online
+ for _, node := range nodes {
+ // All nodes should be online
+ assert.Truef(
+ t,
+ node.GetOnline(),
+ "expected %s to have online status in Headscale, marked as offline %s after start",
+ node.GetName(),
+ time.Since(start),
+ )
+
+ lastSeen := node.GetLastSeen().AsTime()
+ // All nodes should have been last seen between now and the keepAliveInterval
+ assert.Truef(
+ t,
+ lastSeen.After(lastSeenThreshold),
+ "lastSeen (%v) was not %s after the threshold (%v)",
+ lastSeen,
+ keepAliveInterval,
+ lastSeenThreshold,
+ )
+ }
+
+ // Verify that all nodes report all nodes to be online
+ for _, client := range allClients {
+ status, err := client.Status()
+ assertNoErr(t, err)
+
+ for _, peerKey := range status.Peers() {
+ peerStatus := status.Peer[peerKey]
+
+ // .Online is only available from CapVer 16, which
+ // is not present in 1.18 which is the lowest we
+ // test.
+ if strings.Contains(client.Hostname(), "1-18") {
+ continue
+ }
+
+ // All peers of this nodess are reporting to be
+ // connected to the control server
+ assert.Truef(
+ t,
+ peerStatus.Online,
+ "expected node %s to be marked as online in %s peer list, marked as offline %s after start",
+ peerStatus.HostName,
+ client.Hostname(),
+ time.Since(start),
+ )
+
+ // from docs: last seen to tailcontrol; only present if offline
+ // assert.Nilf(
+ // t,
+ // peerStatus.LastSeen,
+ // "expected node %s to not have LastSeen set, got %s",
+ // peerStatus.HostName,
+ // peerStatus.LastSeen,
+ // )
+ }
+ }
+
+ // Check maximum once per second
+ time.Sleep(time.Second)
+ }
+}
diff --git a/integration/hsic/hsic.go b/integration/hsic/hsic.go
index 64aaebb..5019895 100644
--- a/integration/hsic/hsic.go
+++ b/integration/hsic/hsic.go
@@ -348,6 +348,14 @@ func (t *HeadscaleInContainer) Shutdown() error {
)
}
+ err = t.SaveDatabase("/tmp/control")
+ if err != nil {
+ log.Printf(
+ "Failed to save database from control: %s",
+ fmt.Errorf("failed to save database from control: %w", err),
+ )
+ }
+
return t.pool.Purge(t.container)
}
@@ -393,6 +401,24 @@ func (t *HeadscaleInContainer) SaveMapResponses(savePath string) error {
return nil
}
+func (t *HeadscaleInContainer) SaveDatabase(savePath string) error {
+ tarFile, err := t.FetchPath("/tmp/integration_test_db.sqlite3")
+ if err != nil {
+ return err
+ }
+
+ err = os.WriteFile(
+ path.Join(savePath, t.hostname+".db.tar"),
+ tarFile,
+ os.ModePerm,
+ )
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
// Execute runs a command inside the Headscale container and returns the
// result of stdout as a string.
func (t *HeadscaleInContainer) Execute(
diff --git a/integration/route_test.go b/integration/route_test.go
new file mode 100644
index 0000000..489165a
--- /dev/null
+++ b/integration/route_test.go
@@ -0,0 +1,780 @@
+package integration
+
+import (
+ "fmt"
+ "log"
+ "net/netip"
+ "sort"
+ "strconv"
+ "testing"
+ "time"
+
+ v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
+ "github.com/juanfont/headscale/integration/hsic"
+ "github.com/juanfont/headscale/integration/tsic"
+ "github.com/stretchr/testify/assert"
+)
+
+// This test is both testing the routes command and the propagation of
+// routes.
+func TestEnablingRoutes(t *testing.T) {
+ IntegrationSkip(t)
+ t.Parallel()
+
+ user := "enable-routing"
+
+ scenario, err := NewScenario()
+ assertNoErrf(t, "failed to create scenario: %s", err)
+ defer scenario.Shutdown()
+
+ spec := map[string]int{
+ user: 3,
+ }
+
+ err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clienableroute"))
+ assertNoErrHeadscaleEnv(t, err)
+
+ allClients, err := scenario.ListTailscaleClients()
+ assertNoErrListClients(t, err)
+
+ err = scenario.WaitForTailscaleSync()
+ assertNoErrSync(t, err)
+
+ headscale, err := scenario.Headscale()
+ assertNoErrGetHeadscale(t, err)
+
+ expectedRoutes := map[string]string{
+ "1": "10.0.0.0/24",
+ "2": "10.0.1.0/24",
+ "3": "10.0.2.0/24",
+ }
+
+ // advertise routes using the up command
+ for _, client := range allClients {
+ status, err := client.Status()
+ assertNoErr(t, err)
+
+ command := []string{
+ "tailscale",
+ "set",
+ "--advertise-routes=" + expectedRoutes[string(status.Self.ID)],
+ }
+ _, _, err = client.Execute(command)
+ assertNoErrf(t, "failed to advertise route: %s", err)
+ }
+
+ err = scenario.WaitForTailscaleSync()
+ assertNoErrSync(t, err)
+
+ var routes []*v1.Route
+ err = executeAndUnmarshal(
+ headscale,
+ []string{
+ "headscale",
+ "routes",
+ "list",
+ "--output",
+ "json",
+ },
+ &routes,
+ )
+
+ assertNoErr(t, err)
+ assert.Len(t, routes, 3)
+
+ for _, route := range routes {
+ assert.Equal(t, route.GetAdvertised(), true)
+ assert.Equal(t, route.GetEnabled(), false)
+ assert.Equal(t, route.GetIsPrimary(), false)
+ }
+
+ // Verify that no routes has been sent to the client,
+ // they are not yet enabled.
+ for _, client := range allClients {
+ status, err := client.Status()
+ assertNoErr(t, err)
+
+ for _, peerKey := range status.Peers() {
+ peerStatus := status.Peer[peerKey]
+
+ assert.Nil(t, peerStatus.PrimaryRoutes)
+ }
+ }
+
+ // Enable all routes
+ for _, route := range routes {
+ _, err = headscale.Execute(
+ []string{
+ "headscale",
+ "routes",
+ "enable",
+ "--route",
+ strconv.Itoa(int(route.GetId())),
+ })
+ assertNoErr(t, err)
+ }
+
+ var enablingRoutes []*v1.Route
+ err = executeAndUnmarshal(
+ headscale,
+ []string{
+ "headscale",
+ "routes",
+ "list",
+ "--output",
+ "json",
+ },
+ &enablingRoutes,
+ )
+ assertNoErr(t, err)
+ assert.Len(t, enablingRoutes, 3)
+
+ for _, route := range enablingRoutes {
+ assert.Equal(t, route.GetAdvertised(), true)
+ assert.Equal(t, route.GetEnabled(), true)
+ assert.Equal(t, route.GetIsPrimary(), true)
+ }
+
+ time.Sleep(5 * time.Second)
+
+ // Verify that the clients can see the new routes
+ for _, client := range allClients {
+ status, err := client.Status()
+ assertNoErr(t, err)
+
+ for _, peerKey := range status.Peers() {
+ peerStatus := status.Peer[peerKey]
+
+ assert.NotNil(t, peerStatus.PrimaryRoutes)
+ if peerStatus.PrimaryRoutes == nil {
+ continue
+ }
+
+ pRoutes := peerStatus.PrimaryRoutes.AsSlice()
+
+ assert.Len(t, pRoutes, 1)
+
+ if len(pRoutes) > 0 {
+ peerRoute := peerStatus.PrimaryRoutes.AsSlice()[0]
+
+ // id starts at 1, we created routes with 0 index
+ assert.Equalf(
+ t,
+ expectedRoutes[string(peerStatus.ID)],
+ peerRoute.String(),
+ "expected route %s to be present on peer %s (%s) in %s (%s) status",
+ expectedRoutes[string(peerStatus.ID)],
+ peerStatus.HostName,
+ peerStatus.ID,
+ client.Hostname(),
+ client.ID(),
+ )
+ }
+ }
+ }
+
+ routeToBeDisabled := enablingRoutes[0]
+ log.Printf("preparing to disable %v", routeToBeDisabled)
+
+ _, err = headscale.Execute(
+ []string{
+ "headscale",
+ "routes",
+ "disable",
+ "--route",
+ strconv.Itoa(int(routeToBeDisabled.GetId())),
+ })
+ assertNoErr(t, err)
+
+ var disablingRoutes []*v1.Route
+ err = executeAndUnmarshal(
+ headscale,
+ []string{
+ "headscale",
+ "routes",
+ "list",
+ "--output",
+ "json",
+ },
+ &disablingRoutes,
+ )
+ assertNoErr(t, err)
+
+ for _, route := range disablingRoutes {
+ assert.Equal(t, true, route.GetAdvertised())
+
+ if route.GetId() == routeToBeDisabled.GetId() {
+ assert.Equal(t, route.GetEnabled(), false)
+ assert.Equal(t, route.GetIsPrimary(), false)
+ } else {
+ assert.Equal(t, route.GetEnabled(), true)
+ assert.Equal(t, route.GetIsPrimary(), true)
+ }
+ }
+
+ time.Sleep(5 * time.Second)
+
+ // Verify that the clients can see the new routes
+ for _, client := range allClients {
+ status, err := client.Status()
+ assertNoErr(t, err)
+
+ for _, peerKey := range status.Peers() {
+ peerStatus := status.Peer[peerKey]
+
+ if string(peerStatus.ID) == fmt.Sprintf("%d", routeToBeDisabled.GetNode().GetId()) {
+ assert.Nilf(
+ t,
+ peerStatus.PrimaryRoutes,
+ "expected node %s to have no routes, got primary route (%v)",
+ peerStatus.HostName,
+ peerStatus.PrimaryRoutes,
+ )
+ }
+ }
+ }
+}
+
+func TestHASubnetRouterFailover(t *testing.T) {
+ IntegrationSkip(t)
+ t.Parallel()
+
+ user := "enable-routing"
+
+ scenario, err := NewScenario()
+ assertNoErrf(t, "failed to create scenario: %s", err)
+ defer scenario.Shutdown()
+
+ spec := map[string]int{
+ user: 3,
+ }
+
+ err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clienableroute"))
+ assertNoErrHeadscaleEnv(t, err)
+
+ allClients, err := scenario.ListTailscaleClients()
+ assertNoErrListClients(t, err)
+
+ err = scenario.WaitForTailscaleSync()
+ assertNoErrSync(t, err)
+
+ headscale, err := scenario.Headscale()
+ assertNoErrGetHeadscale(t, err)
+
+ expectedRoutes := map[string]string{
+ "1": "10.0.0.0/24",
+ "2": "10.0.0.0/24",
+ }
+
+ // Sort nodes by ID
+ sort.SliceStable(allClients, func(i, j int) bool {
+ statusI, err := allClients[i].Status()
+ if err != nil {
+ return false
+ }
+
+ statusJ, err := allClients[j].Status()
+ if err != nil {
+ return false
+ }
+
+ return statusI.Self.ID < statusJ.Self.ID
+ })
+
+ subRouter1 := allClients[0]
+ subRouter2 := allClients[1]
+
+ client := allClients[2]
+
+ // advertise HA route on node 1 and 2
+ // ID 1 will be primary
+ // ID 2 will be secondary
+ for _, client := range allClients {
+ status, err := client.Status()
+ assertNoErr(t, err)
+
+ if route, ok := expectedRoutes[string(status.Self.ID)]; ok {
+ command := []string{
+ "tailscale",
+ "set",
+ "--advertise-routes=" + route,
+ }
+ _, _, err = client.Execute(command)
+ assertNoErrf(t, "failed to advertise route: %s", err)
+ }
+ }
+
+ err = scenario.WaitForTailscaleSync()
+ assertNoErrSync(t, err)
+
+ var routes []*v1.Route
+ err = executeAndUnmarshal(
+ headscale,
+ []string{
+ "headscale",
+ "routes",
+ "list",
+ "--output",
+ "json",
+ },
+ &routes,
+ )
+
+ assertNoErr(t, err)
+ assert.Len(t, routes, 2)
+
+ for _, route := range routes {
+ assert.Equal(t, true, route.GetAdvertised())
+ assert.Equal(t, false, route.GetEnabled())
+ assert.Equal(t, false, route.GetIsPrimary())
+ }
+
+ // Verify that no routes has been sent to the client,
+ // they are not yet enabled.
+ for _, client := range allClients {
+ status, err := client.Status()
+ assertNoErr(t, err)
+
+ for _, peerKey := range status.Peers() {
+ peerStatus := status.Peer[peerKey]
+
+ assert.Nil(t, peerStatus.PrimaryRoutes)
+ }
+ }
+
+ // Enable all routes
+ for _, route := range routes {
+ _, err = headscale.Execute(
+ []string{
+ "headscale",
+ "routes",
+ "enable",
+ "--route",
+ strconv.Itoa(int(route.GetId())),
+ })
+ assertNoErr(t, err)
+
+ time.Sleep(time.Second)
+ }
+
+ var enablingRoutes []*v1.Route
+ err = executeAndUnmarshal(
+ headscale,
+ []string{
+ "headscale",
+ "routes",
+ "list",
+ "--output",
+ "json",
+ },
+ &enablingRoutes,
+ )
+ assertNoErr(t, err)
+ assert.Len(t, enablingRoutes, 2)
+
+ // Node 1 is primary
+ assert.Equal(t, true, enablingRoutes[0].GetAdvertised())
+ assert.Equal(t, true, enablingRoutes[0].GetEnabled())
+ assert.Equal(t, true, enablingRoutes[0].GetIsPrimary())
+
+ // Node 2 is not primary
+ assert.Equal(t, true, enablingRoutes[1].GetAdvertised())
+ assert.Equal(t, true, enablingRoutes[1].GetEnabled())
+ assert.Equal(t, false, enablingRoutes[1].GetIsPrimary())
+
+ // Verify that the client has routes from the primary machine
+ srs1, err := subRouter1.Status()
+ srs2, err := subRouter2.Status()
+
+ clientStatus, err := client.Status()
+ assertNoErr(t, err)
+
+ srs1PeerStatus := clientStatus.Peer[srs1.Self.PublicKey]
+ srs2PeerStatus := clientStatus.Peer[srs2.Self.PublicKey]
+
+ assertNotNil(t, srs1PeerStatus.PrimaryRoutes)
+ assert.Nil(t, srs2PeerStatus.PrimaryRoutes)
+
+ assert.Contains(
+ t,
+ srs1PeerStatus.PrimaryRoutes.AsSlice(),
+ netip.MustParsePrefix(expectedRoutes[string(srs1.Self.ID)]),
+ )
+
+ // Take down the current primary
+ t.Logf("taking down subnet router 1 (%s)", subRouter1.Hostname())
+ err = subRouter1.Down()
+ assertNoErr(t, err)
+
+ time.Sleep(5 * time.Second)
+
+ var routesAfterMove []*v1.Route
+ err = executeAndUnmarshal(
+ headscale,
+ []string{
+ "headscale",
+ "routes",
+ "list",
+ "--output",
+ "json",
+ },
+ &routesAfterMove,
+ )
+ assertNoErr(t, err)
+ assert.Len(t, routesAfterMove, 2)
+
+ // Node 1 is not primary
+ assert.Equal(t, true, routesAfterMove[0].GetAdvertised())
+ assert.Equal(t, true, routesAfterMove[0].GetEnabled())
+ assert.Equal(t, false, routesAfterMove[0].GetIsPrimary())
+
+ // Node 2 is primary
+ assert.Equal(t, true, routesAfterMove[1].GetAdvertised())
+ assert.Equal(t, true, routesAfterMove[1].GetEnabled())
+ assert.Equal(t, true, routesAfterMove[1].GetIsPrimary())
+
+ // TODO(kradalby): Check client status
+ // Route is expected to be on SR2
+
+ srs2, err = subRouter2.Status()
+
+ clientStatus, err = client.Status()
+ assertNoErr(t, err)
+
+ srs1PeerStatus = clientStatus.Peer[srs1.Self.PublicKey]
+ srs2PeerStatus = clientStatus.Peer[srs2.Self.PublicKey]
+
+ assert.Nil(t, srs1PeerStatus.PrimaryRoutes)
+ assertNotNil(t, srs2PeerStatus.PrimaryRoutes)
+
+ if srs2PeerStatus.PrimaryRoutes != nil {
+ assert.Contains(
+ t,
+ srs2PeerStatus.PrimaryRoutes.AsSlice(),
+ netip.MustParsePrefix(expectedRoutes[string(srs2.Self.ID)]),
+ )
+ }
+
+ // Take down subnet router 2, leaving none available
+ t.Logf("taking down subnet router 2 (%s)", subRouter2.Hostname())
+ err = subRouter2.Down()
+ assertNoErr(t, err)
+
+ time.Sleep(5 * time.Second)
+
+ var routesAfterBothDown []*v1.Route
+ err = executeAndUnmarshal(
+ headscale,
+ []string{
+ "headscale",
+ "routes",
+ "list",
+ "--output",
+ "json",
+ },
+ &routesAfterBothDown,
+ )
+ assertNoErr(t, err)
+ assert.Len(t, routesAfterBothDown, 2)
+
+ // Node 1 is not primary
+ assert.Equal(t, true, routesAfterBothDown[0].GetAdvertised())
+ assert.Equal(t, true, routesAfterBothDown[0].GetEnabled())
+ assert.Equal(t, false, routesAfterBothDown[0].GetIsPrimary())
+
+ // Node 2 is primary
+ // if the node goes down, but no other suitable route is
+ // available, keep the last known good route.
+ assert.Equal(t, true, routesAfterBothDown[1].GetAdvertised())
+ assert.Equal(t, true, routesAfterBothDown[1].GetEnabled())
+ assert.Equal(t, true, routesAfterBothDown[1].GetIsPrimary())
+
+ // TODO(kradalby): Check client status
+ // Both are expected to be down
+
+ // Verify that the route is not presented from either router
+ clientStatus, err = client.Status()
+ assertNoErr(t, err)
+
+ srs1PeerStatus = clientStatus.Peer[srs1.Self.PublicKey]
+ srs2PeerStatus = clientStatus.Peer[srs2.Self.PublicKey]
+
+ assert.Nil(t, srs1PeerStatus.PrimaryRoutes)
+ assertNotNil(t, srs2PeerStatus.PrimaryRoutes)
+
+ if srs2PeerStatus.PrimaryRoutes != nil {
+ assert.Contains(
+ t,
+ srs2PeerStatus.PrimaryRoutes.AsSlice(),
+ netip.MustParsePrefix(expectedRoutes[string(srs2.Self.ID)]),
+ )
+ }
+
+ // Bring up subnet router 1, making the route available from there.
+ t.Logf("bringing up subnet router 1 (%s)", subRouter1.Hostname())
+ err = subRouter1.Up()
+ assertNoErr(t, err)
+
+ time.Sleep(5 * time.Second)
+
+ var routesAfter1Up []*v1.Route
+ err = executeAndUnmarshal(
+ headscale,
+ []string{
+ "headscale",
+ "routes",
+ "list",
+ "--output",
+ "json",
+ },
+ &routesAfter1Up,
+ )
+ assertNoErr(t, err)
+ assert.Len(t, routesAfter1Up, 2)
+
+ // Node 1 is primary
+ assert.Equal(t, true, routesAfter1Up[0].GetAdvertised())
+ assert.Equal(t, true, routesAfter1Up[0].GetEnabled())
+ assert.Equal(t, true, routesAfter1Up[0].GetIsPrimary())
+
+ // Node 2 is not primary
+ assert.Equal(t, true, routesAfter1Up[1].GetAdvertised())
+ assert.Equal(t, true, routesAfter1Up[1].GetEnabled())
+ assert.Equal(t, false, routesAfter1Up[1].GetIsPrimary())
+
+ // Verify that the route is announced from subnet router 1
+ clientStatus, err = client.Status()
+ assertNoErr(t, err)
+
+ srs1PeerStatus = clientStatus.Peer[srs1.Self.PublicKey]
+ srs2PeerStatus = clientStatus.Peer[srs2.Self.PublicKey]
+
+ assert.NotNil(t, srs1PeerStatus.PrimaryRoutes)
+ assert.Nil(t, srs2PeerStatus.PrimaryRoutes)
+
+ if srs1PeerStatus.PrimaryRoutes != nil {
+ assert.Contains(
+ t,
+ srs1PeerStatus.PrimaryRoutes.AsSlice(),
+ netip.MustParsePrefix(expectedRoutes[string(srs1.Self.ID)]),
+ )
+ }
+
+ // Bring up subnet router 2, should result in no change.
+ t.Logf("bringing up subnet router 2 (%s)", subRouter2.Hostname())
+ err = subRouter2.Up()
+ assertNoErr(t, err)
+
+ time.Sleep(5 * time.Second)
+
+ var routesAfter2Up []*v1.Route
+ err = executeAndUnmarshal(
+ headscale,
+ []string{
+ "headscale",
+ "routes",
+ "list",
+ "--output",
+ "json",
+ },
+ &routesAfter2Up,
+ )
+ assertNoErr(t, err)
+ assert.Len(t, routesAfter2Up, 2)
+
+ // Node 1 is not primary
+ assert.Equal(t, true, routesAfter2Up[0].GetAdvertised())
+ assert.Equal(t, true, routesAfter2Up[0].GetEnabled())
+ assert.Equal(t, true, routesAfter2Up[0].GetIsPrimary())
+
+ // Node 2 is primary
+ assert.Equal(t, true, routesAfter2Up[1].GetAdvertised())
+ assert.Equal(t, true, routesAfter2Up[1].GetEnabled())
+ assert.Equal(t, false, routesAfter2Up[1].GetIsPrimary())
+
+ // Verify that the route is announced from subnet router 1
+ clientStatus, err = client.Status()
+ assertNoErr(t, err)
+
+ srs1PeerStatus = clientStatus.Peer[srs1.Self.PublicKey]
+ srs2PeerStatus = clientStatus.Peer[srs2.Self.PublicKey]
+
+ assert.NotNil(t, srs1PeerStatus.PrimaryRoutes)
+ assert.Nil(t, srs2PeerStatus.PrimaryRoutes)
+
+ if srs1PeerStatus.PrimaryRoutes != nil {
+ assert.Contains(
+ t,
+ srs1PeerStatus.PrimaryRoutes.AsSlice(),
+ netip.MustParsePrefix(expectedRoutes[string(srs1.Self.ID)]),
+ )
+ }
+
+ // Disable the route of subnet router 1, making it failover to 2
+ t.Logf("disabling route in subnet router 1 (%s)", subRouter1.Hostname())
+ _, err = headscale.Execute(
+ []string{
+ "headscale",
+ "routes",
+ "disable",
+ "--route",
+ fmt.Sprintf("%d", routesAfter2Up[0].GetId()),
+ })
+ assertNoErr(t, err)
+
+ time.Sleep(5 * time.Second)
+
+ var routesAfterDisabling1 []*v1.Route
+ err = executeAndUnmarshal(
+ headscale,
+ []string{
+ "headscale",
+ "routes",
+ "list",
+ "--output",
+ "json",
+ },
+ &routesAfterDisabling1,
+ )
+ assertNoErr(t, err)
+ assert.Len(t, routesAfterDisabling1, 2)
+
+ // Node 1 is not primary
+ assert.Equal(t, true, routesAfterDisabling1[0].GetAdvertised())
+ assert.Equal(t, false, routesAfterDisabling1[0].GetEnabled())
+ assert.Equal(t, false, routesAfterDisabling1[0].GetIsPrimary())
+
+ // Node 2 is primary
+ assert.Equal(t, true, routesAfterDisabling1[1].GetAdvertised())
+ assert.Equal(t, true, routesAfterDisabling1[1].GetEnabled())
+ assert.Equal(t, true, routesAfterDisabling1[1].GetIsPrimary())
+
+ // Verify that the route is announced from subnet router 1
+ clientStatus, err = client.Status()
+ assertNoErr(t, err)
+
+ srs1PeerStatus = clientStatus.Peer[srs1.Self.PublicKey]
+ srs2PeerStatus = clientStatus.Peer[srs2.Self.PublicKey]
+
+ assert.Nil(t, srs1PeerStatus.PrimaryRoutes)
+ assert.NotNil(t, srs2PeerStatus.PrimaryRoutes)
+
+ if srs2PeerStatus.PrimaryRoutes != nil {
+ assert.Contains(
+ t,
+ srs2PeerStatus.PrimaryRoutes.AsSlice(),
+ netip.MustParsePrefix(expectedRoutes[string(srs2.Self.ID)]),
+ )
+ }
+
+ // enable the route of subnet router 1, no change expected
+ t.Logf("enabling route in subnet router 1 (%s)", subRouter1.Hostname())
+ _, err = headscale.Execute(
+ []string{
+ "headscale",
+ "routes",
+ "enable",
+ "--route",
+ fmt.Sprintf("%d", routesAfter2Up[0].GetId()),
+ })
+ assertNoErr(t, err)
+
+ time.Sleep(5 * time.Second)
+
+ var routesAfterEnabling1 []*v1.Route
+ err = executeAndUnmarshal(
+ headscale,
+ []string{
+ "headscale",
+ "routes",
+ "list",
+ "--output",
+ "json",
+ },
+ &routesAfterEnabling1,
+ )
+ assertNoErr(t, err)
+ assert.Len(t, routesAfterEnabling1, 2)
+
+ // Node 1 is not primary
+ assert.Equal(t, true, routesAfterEnabling1[0].GetAdvertised())
+ assert.Equal(t, true, routesAfterEnabling1[0].GetEnabled())
+ assert.Equal(t, false, routesAfterEnabling1[0].GetIsPrimary())
+
+ // Node 2 is primary
+ assert.Equal(t, true, routesAfterEnabling1[1].GetAdvertised())
+ assert.Equal(t, true, routesAfterEnabling1[1].GetEnabled())
+ assert.Equal(t, true, routesAfterEnabling1[1].GetIsPrimary())
+
+ // Verify that the route is announced from subnet router 1
+ clientStatus, err = client.Status()
+ assertNoErr(t, err)
+
+ srs1PeerStatus = clientStatus.Peer[srs1.Self.PublicKey]
+ srs2PeerStatus = clientStatus.Peer[srs2.Self.PublicKey]
+
+ assert.Nil(t, srs1PeerStatus.PrimaryRoutes)
+ assert.NotNil(t, srs2PeerStatus.PrimaryRoutes)
+
+ if srs2PeerStatus.PrimaryRoutes != nil {
+ assert.Contains(
+ t,
+ srs2PeerStatus.PrimaryRoutes.AsSlice(),
+ netip.MustParsePrefix(expectedRoutes[string(srs2.Self.ID)]),
+ )
+ }
+
+ // delete the route of subnet router 2, failover to one expected
+ t.Logf("deleting route in subnet router 2 (%s)", subRouter2.Hostname())
+ _, err = headscale.Execute(
+ []string{
+ "headscale",
+ "routes",
+ "delete",
+ "--route",
+ fmt.Sprintf("%d", routesAfterEnabling1[1].GetId()),
+ })
+ assertNoErr(t, err)
+
+ time.Sleep(5 * time.Second)
+
+ var routesAfterDeleting2 []*v1.Route
+ err = executeAndUnmarshal(
+ headscale,
+ []string{
+ "headscale",
+ "routes",
+ "list",
+ "--output",
+ "json",
+ },
+ &routesAfterDeleting2,
+ )
+ assertNoErr(t, err)
+ assert.Len(t, routesAfterDeleting2, 1)
+
+ t.Logf("routes after deleting2 %#v", routesAfterDeleting2)
+
+ // Node 1 is primary
+ assert.Equal(t, true, routesAfterDeleting2[0].GetAdvertised())
+ assert.Equal(t, true, routesAfterDeleting2[0].GetEnabled())
+ assert.Equal(t, true, routesAfterDeleting2[0].GetIsPrimary())
+
+ // Verify that the route is announced from subnet router 1
+ clientStatus, err = client.Status()
+ assertNoErr(t, err)
+
+ srs1PeerStatus = clientStatus.Peer[srs1.Self.PublicKey]
+ srs2PeerStatus = clientStatus.Peer[srs2.Self.PublicKey]
+
+ assertNotNil(t, srs1PeerStatus.PrimaryRoutes)
+ assert.Nil(t, srs2PeerStatus.PrimaryRoutes)
+
+ if srs1PeerStatus.PrimaryRoutes != nil {
+ assert.Contains(
+ t,
+ srs1PeerStatus.PrimaryRoutes.AsSlice(),
+ netip.MustParsePrefix(expectedRoutes[string(srs1.Self.ID)]),
+ )
+ }
+}
diff --git a/integration/run.sh b/integration/run.sh
index 97c7956..8cad3f0 100755
--- a/integration/run.sh
+++ b/integration/run.sh
@@ -13,8 +13,10 @@ run_tests() {
for ((i = 1; i <= num_tests; i++)); do
docker network prune -f >/dev/null 2>&1
- docker rm headscale-test-suite || true
- docker kill "$(docker ps -q)" || true
+ docker rm headscale-test-suite >/dev/null 2>&1 || true
+ docker kill "$(docker ps -q)" >/dev/null 2>&1 || true
+
+ echo "Run $i"
start=$(date +%s)
docker run \
@@ -26,11 +28,10 @@ run_tests() {
--volume "$PWD"/control_logs:/tmp/control \
golang:1 \
go test ./... \
- -tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
- -run "^$test_name\$" >/dev/null 2>&1
+ -run "^$test_name\$" >./control_logs/"$test_name"_"$i".log 2>&1
status=$?
end=$(date +%s)
diff --git a/integration/scenario.go b/integration/scenario.go
index 4178b78..c11af72 100644
--- a/integration/scenario.go
+++ b/integration/scenario.go
@@ -6,6 +6,7 @@ import (
"log"
"net/netip"
"os"
+ "sort"
"sync"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
@@ -14,7 +15,8 @@ import (
"github.com/juanfont/headscale/integration/hsic"
"github.com/juanfont/headscale/integration/tsic"
"github.com/ory/dockertest/v3"
- "github.com/puzpuzpuz/xsync/v2"
+ "github.com/puzpuzpuz/xsync/v3"
+ "github.com/samber/lo"
"golang.org/x/sync/errgroup"
)
@@ -22,6 +24,19 @@ const (
scenarioHashLength = 6
)
+func enabledVersions(vs map[string]bool) []string {
+ var ret []string
+ for version, enabled := range vs {
+ if enabled {
+ ret = append(ret, version)
+ }
+ }
+
+ sort.Sort(sort.Reverse(sort.StringSlice(ret)))
+
+ return ret
+}
+
var (
errNoHeadscaleAvailable = errors.New("no headscale available")
errNoUserAvailable = errors.New("no user available")
@@ -29,29 +44,32 @@ var (
// Tailscale started adding TS2021 support in CapabilityVersion>=28 (v1.24.0), but
// proper support in Headscale was only added for CapabilityVersion>=39 clients (v1.30.0).
- tailscaleVersions2021 = []string{
- "head",
- "unstable",
- "1.50",
- "1.48",
- "1.46",
- "1.44",
- "1.42",
- "1.40",
- "1.38",
- "1.36",
- "1.34",
- "1.32",
- "1.30",
+ tailscaleVersions2021 = map[string]bool{
+ "head": true,
+ "unstable": true,
+ "1.56": true, // CapVer: 82
+ "1.54": true, // CapVer: 79
+ "1.52": true, // CapVer: 79
+ "1.50": true, // CapVer: 74
+ "1.48": true, // CapVer: 68
+ "1.46": true, // CapVer: 65
+ "1.44": true, // CapVer: 63
+ "1.42": true, // CapVer: 61
+ "1.40": true, // CapVer: 61
+ "1.38": true, // CapVer: 58
+ "1.36": true, // Oldest supported version, CapVer: 56
+ "1.34": false, // CapVer: 51
+ "1.32": false, // CapVer: 46
+ "1.30": false,
}
- tailscaleVersions2019 = []string{
- "1.28",
- "1.26",
- "1.24", // Tailscale SSH
- "1.22",
- "1.20",
- "1.18",
+ tailscaleVersions2019 = map[string]bool{
+ "1.28": false,
+ "1.26": false,
+ "1.24": false, // Tailscale SSH
+ "1.22": false,
+ "1.20": false,
+ "1.18": false,
}
// tailscaleVersionsUnavailable = []string{
@@ -72,8 +90,8 @@ var (
// The rest of the version represents Tailscale versions that can be
// found in Tailscale's apt repository.
AllVersions = append(
- tailscaleVersions2021,
- tailscaleVersions2019...,
+ enabledVersions(tailscaleVersions2021),
+ enabledVersions(tailscaleVersions2019)...,
)
// MustTestVersions is the minimum set of versions we should test.
@@ -81,10 +99,10 @@ var (
//
// - Two unstable (HEAD and unstable)
// - Two latest versions
- // - Two oldest versions.
+ // - Two oldest supported version.
MustTestVersions = append(
- tailscaleVersions2021[0:4],
- tailscaleVersions2019[len(tailscaleVersions2019)-2:]...,
+ AllVersions[0:4],
+ AllVersions[len(AllVersions)-2:]...,
)
)
@@ -150,7 +168,7 @@ func NewScenario() (*Scenario, error) {
}
return &Scenario{
- controlServers: xsync.NewMapOf[ControlServer](),
+ controlServers: xsync.NewMapOf[string, ControlServer](),
users: make(map[string]*User),
pool: pool,
@@ -284,11 +302,13 @@ func (s *Scenario) CreateTailscaleNodesInUser(
opts ...tsic.Option,
) error {
if user, ok := s.users[userStr]; ok {
+ var versions []string
for i := 0; i < count; i++ {
version := requestedVersion
if requestedVersion == "all" {
version = MustTestVersions[i%len(MustTestVersions)]
}
+ versions = append(versions, version)
headscale, err := s.Headscale()
if err != nil {
@@ -338,6 +358,8 @@ func (s *Scenario) CreateTailscaleNodesInUser(
return err
}
+ log.Printf("testing versions %v, MustTestVersions %v", lo.Uniq(versions), MustTestVersions)
+
return nil
}
@@ -391,7 +413,17 @@ func (s *Scenario) CountTailscale() int {
func (s *Scenario) WaitForTailscaleSync() error {
tsCount := s.CountTailscale()
- return s.WaitForTailscaleSyncWithPeerCount(tsCount - 1)
+ err := s.WaitForTailscaleSyncWithPeerCount(tsCount - 1)
+ if err != nil {
+ for _, user := range s.users {
+ for _, client := range user.Clients {
+ peers, _ := client.PrettyPeers()
+ log.Println(peers)
+ }
+ }
+ }
+
+ return err
}
// WaitForTailscaleSyncWithPeerCount blocks execution until all the TailscaleClient reports
diff --git a/integration/scenario_test.go b/integration/scenario_test.go
index 59b6a33..cc9810a 100644
--- a/integration/scenario_test.go
+++ b/integration/scenario_test.go
@@ -142,7 +142,7 @@ func TestTailscaleNodesJoiningHeadcale(t *testing.T) {
})
t.Run("create-tailscale", func(t *testing.T) {
- err := scenario.CreateTailscaleNodesInUser(user, "1.30.2", count)
+ err := scenario.CreateTailscaleNodesInUser(user, "unstable", count)
if err != nil {
t.Fatalf("failed to add tailscale nodes: %s", err)
}
diff --git a/integration/ssh_test.go b/integration/ssh_test.go
index 88e62e9..587190e 100644
--- a/integration/ssh_test.go
+++ b/integration/ssh_test.go
@@ -109,7 +109,7 @@ func TestSSHOneUserToAll(t *testing.T) {
},
},
},
- len(MustTestVersions)-2,
+ len(MustTestVersions),
)
defer scenario.Shutdown()
@@ -174,7 +174,7 @@ func TestSSHMultipleUsersAllToAll(t *testing.T) {
},
},
},
- len(MustTestVersions)-2,
+ len(MustTestVersions),
)
defer scenario.Shutdown()
@@ -220,7 +220,7 @@ func TestSSHNoSSHConfigured(t *testing.T) {
},
SSHs: []policy.SSH{},
},
- len(MustTestVersions)-2,
+ len(MustTestVersions),
)
defer scenario.Shutdown()
@@ -269,7 +269,7 @@ func TestSSHIsBlockedInACL(t *testing.T) {
},
},
},
- len(MustTestVersions)-2,
+ len(MustTestVersions),
)
defer scenario.Shutdown()
@@ -325,7 +325,7 @@ func TestSSHUserOnlyIsolation(t *testing.T) {
},
},
},
- len(MustTestVersions)-2,
+ len(MustTestVersions),
)
defer scenario.Shutdown()
diff --git a/integration/tailscale.go b/integration/tailscale.go
index ba63e7d..e7bf71b 100644
--- a/integration/tailscale.go
+++ b/integration/tailscale.go
@@ -21,6 +21,8 @@ type TailscaleClient interface {
Login(loginServer, authKey string) error
LoginWithURL(loginServer string) (*url.URL, error)
Logout() error
+ Up() error
+ Down() error
IPs() ([]netip.Addr, error)
FQDN() (string, error)
Status() (*ipnstate.Status, error)
@@ -30,4 +32,5 @@ type TailscaleClient interface {
Ping(hostnameOrIP string, opts ...tsic.PingOption) error
Curl(url string, opts ...tsic.CurlOption) (string, error)
ID() string
+ PrettyPeers() (string, error)
}
diff --git a/integration/tsic/tsic.go b/integration/tsic/tsic.go
index efe9c90..7404f6e 100644
--- a/integration/tsic/tsic.go
+++ b/integration/tsic/tsic.go
@@ -285,6 +285,15 @@ func (t *TailscaleInContainer) hasTLS() bool {
// Shutdown stops and cleans up the Tailscale container.
func (t *TailscaleInContainer) Shutdown() error {
+ err := t.SaveLog("/tmp/control")
+ if err != nil {
+ log.Printf(
+ "Failed to save log from %s: %s",
+ t.hostname,
+ fmt.Errorf("failed to save log: %w", err),
+ )
+ }
+
return t.pool.Purge(t.container)
}
@@ -417,6 +426,44 @@ func (t *TailscaleInContainer) Logout() error {
return nil
}
+// Helper that runs `tailscale up` with no arguments.
+func (t *TailscaleInContainer) Up() error {
+ command := []string{
+ "tailscale",
+ "up",
+ }
+
+ if _, _, err := t.Execute(command, dockertestutil.ExecuteCommandTimeout(dockerExecuteTimeout)); err != nil {
+ return fmt.Errorf(
+ "%s failed to bring tailscale client up (%s): %w",
+ t.hostname,
+ strings.Join(command, " "),
+ err,
+ )
+ }
+
+ return nil
+}
+
+// Helper that runs `tailscale down` with no arguments.
+func (t *TailscaleInContainer) Down() error {
+ command := []string{
+ "tailscale",
+ "down",
+ }
+
+ if _, _, err := t.Execute(command, dockertestutil.ExecuteCommandTimeout(dockerExecuteTimeout)); err != nil {
+ return fmt.Errorf(
+ "%s failed to bring tailscale client down (%s): %w",
+ t.hostname,
+ strings.Join(command, " "),
+ err,
+ )
+ }
+
+ return nil
+}
+
// IPs returns the netip.Addr of the Tailscale instance.
func (t *TailscaleInContainer) IPs() ([]netip.Addr, error) {
if t.ips != nil && len(t.ips) != 0 {
@@ -486,6 +533,34 @@ func (t *TailscaleInContainer) FQDN() (string, error) {
return status.Self.DNSName, nil
}
+// PrettyPeers returns a formatted-ish table of peers in the client.
+func (t *TailscaleInContainer) PrettyPeers() (string, error) {
+ status, err := t.Status()
+ if err != nil {
+ return "", fmt.Errorf("failed to get FQDN: %w", err)
+ }
+
+ str := fmt.Sprintf("Peers of %s\n", t.hostname)
+ str += "Hostname\tOnline\tLastSeen\n"
+
+ peerCount := len(status.Peers())
+ onlineCount := 0
+
+ for _, peerKey := range status.Peers() {
+ peer := status.Peer[peerKey]
+
+ if peer.Online {
+ onlineCount++
+ }
+
+ str += fmt.Sprintf("%s\t%t\t%s\n", peer.HostName, peer.Online, peer.LastSeen)
+ }
+
+ str += fmt.Sprintf("Peer Count: %d, Online Count: %d\n\n", peerCount, onlineCount)
+
+ return str, nil
+}
+
// WaitForNeedsLogin blocks until the Tailscale (tailscaled) instance has
// started and needs to be logged into.
func (t *TailscaleInContainer) WaitForNeedsLogin() error {
@@ -531,7 +606,7 @@ func (t *TailscaleInContainer) WaitForRunning() error {
}
// WaitForPeers blocks until N number of peers is present in the
-// Peer list of the Tailscale instance.
+// Peer list of the Tailscale instance and is reporting Online.
func (t *TailscaleInContainer) WaitForPeers(expected int) error {
return t.pool.Retry(func() error {
status, err := t.Status()
@@ -547,6 +622,14 @@ func (t *TailscaleInContainer) WaitForPeers(expected int) error {
expected,
len(peers),
)
+ } else {
+ for _, peerKey := range peers {
+ peer := status.Peer[peerKey]
+
+ if !peer.Online {
+ return fmt.Errorf("[%s] peer count correct, but %s is not online", t.hostname, peer.HostName)
+ }
+ }
}
return nil
@@ -738,3 +821,9 @@ func (t *TailscaleInContainer) Curl(url string, opts ...CurlOption) (string, err
func (t *TailscaleInContainer) WriteFile(path string, data []byte) error {
return integrationutil.WriteFileToContainer(t.pool, t.container, path, data)
}
+
+// SaveLog saves the current stdout log of the container to a path
+// on the host system.
+func (t *TailscaleInContainer) SaveLog(path string) error {
+ return dockertestutil.SaveLog(t.pool, t.container, path)
+}
diff --git a/integration/utils.go b/integration/utils.go
index 91e274b..e17e18a 100644
--- a/integration/utils.go
+++ b/integration/utils.go
@@ -26,6 +26,13 @@ func assertNoErrf(t *testing.T, msg string, err error) {
}
}
+func assertNotNil(t *testing.T, thing interface{}) {
+ t.Helper()
+ if thing == nil {
+ t.Fatal("got unexpected nil")
+ }
+}
+
func assertNoErrHeadscaleEnv(t *testing.T, err error) {
t.Helper()
assertNoErrf(t, "failed to create headscale environment: %s", err)
@@ -68,13 +75,13 @@ func assertContains(t *testing.T, str, subStr string) {
}
}
-func pingAllHelper(t *testing.T, clients []TailscaleClient, addrs []string) int {
+func pingAllHelper(t *testing.T, clients []TailscaleClient, addrs []string, opts ...tsic.PingOption) int {
t.Helper()
success := 0
for _, client := range clients {
for _, addr := range addrs {
- err := client.Ping(addr)
+ err := client.Ping(addr, opts...)
if err != nil {
t.Fatalf("failed to ping %s from %s: %s", addr, client.Hostname(), err)
} else {
diff --git a/mkdocs.yml b/mkdocs.yml
index 75abcdd..86a1546 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -94,8 +94,8 @@ markdown_extensions:
- pymdownx.caret
- pymdownx.details
- pymdownx.emoji:
- emoji_generator: !!python/name:materialx.emoji.to_svg
- emoji_index: !!python/name:materialx.emoji.twemoji
+ emoji_generator: !!python/name:material.extensions.emoji.to_svg
+ emoji_index: !!python/name:material.extensions.emoji.twemoji
- pymdownx.highlight:
anchor_linenums: true
line_spans: __span
|