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/ +
+ MichaelKo +
+ kevinlin/ @@ -473,6 +480,8 @@ make build kevinlin + + Snack/ @@ -480,8 +489,6 @@ make build Snack - - Artem @@ -517,6 +524,8 @@ make build LIU HANCHENG + + Motiejus @@ -524,8 +533,6 @@ make build Motiejus Jakštys - - Pavlos @@ -547,13 +554,6 @@ make build Steven Honson - - - MichaelKo/ -
- MichaelKo -
- Victor @@ -577,6 +577,13 @@ make build thomas + + + Andrei +
+ Andrei Pechkurov +
+ Sean @@ -598,13 +605,6 @@ make build Albert Copeland - - - Andrei -
- Andrei Pechkurov -
- Anoop @@ -658,6 +658,13 @@ make build + + + Azamat +
+ Azamat H. Hackimov +
+ Bryan @@ -693,6 +700,8 @@ make build Felix Kronlage-Dammers + + Felix @@ -700,8 +709,6 @@ make build Felix Yan - - Gabe @@ -723,6 +730,13 @@ make build hrtkpf + + + JesseBot/ +
+ JesseBot +
+ Jim @@ -730,6 +744,8 @@ make build Jim Tittsler + + Johan @@ -744,8 +760,6 @@ make build John Axel Eriksson - - Jonathan @@ -774,6 +788,8 @@ make build Lucalux + + Marc/ @@ -788,8 +804,6 @@ make build Mesar Hameed - - Michael @@ -818,6 +832,8 @@ make build Pontus N + + Rasmus @@ -832,8 +848,6 @@ make build rcursaru - - Mend @@ -850,9 +864,9 @@ make build - Sebastian + Sebastian/
- Sebastian Muszytowski + Sebastian
@@ -862,6 +876,8 @@ make build Shaanan Cohney + + Six/ @@ -876,8 +892,6 @@ make build Stefan VanBuren - - sophware/ @@ -906,6 +920,8 @@ make build The Gitter Badger + + Tianon @@ -920,8 +936,6 @@ make build Till Hoffmann - - Tjerk @@ -950,6 +964,8 @@ make build Zachary Newell + + Zakhar @@ -964,8 +980,6 @@ make build Zhiyuan Zheng - - Ziyuan @@ -994,6 +1008,8 @@ make build dnaq + + henning @@ -1008,8 +1024,6 @@ make build ignoramous - - jimyag/ @@ -1038,6 +1052,8 @@ make build ma6174 + + manju-rn/ @@ -1052,8 +1068,6 @@ make build nicholas-yap - - Tommi @@ -1082,6 +1096,8 @@ make build zy + + Àlex 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