From 1d3eae8861270ed51ae9f2741e9fc130f9fef64e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 10 Mar 2024 12:40:31 +0100 Subject: [PATCH 001/145] Update flake.lock (#1657) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'flake-utils': 'github:numtide/flake-utils/1ef2e671c3b0c19053962c07dbda38332dcebf26' (2024-01-15) → 'github:numtide/flake-utils/d465f4819400de7c8d874d50b982301f28a84605' (2024-02-28) • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/442d407992384ed9c0e6d352de75b69079904e4e' (2024-02-09) → 'github:NixOS/nixpkgs/0e7f98a5f30166cbed344569426850b21e4091d4' (2024-03-09) Co-authored-by: github-actions[bot] --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 307d6339..020648b6 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1705309234, - "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", + "lastModified": 1709126324, + "narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=", "owner": "numtide", "repo": "flake-utils", - "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", + "rev": "d465f4819400de7c8d874d50b982301f28a84605", "type": "github" }, "original": { @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1707451808, - "narHash": "sha256-UwDBUNHNRsYKFJzyTMVMTF5qS4xeJlWoeyJf+6vvamU=", + "lastModified": 1709968316, + "narHash": "sha256-4rZEtEDT6jcgRaqxsatBeds7x1PoEiEjb6QNGb4mNrk=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "442d407992384ed9c0e6d352de75b69079904e4e", + "rev": "0e7f98a5f30166cbed344569426850b21e4091d4", "type": "github" }, "original": { From ef26f5808549b4a1d59d0b7e5f628633bcb16330 Mon Sep 17 00:00:00 2001 From: Vitalij Dovhanyc <45185420+vdovhanych@users.noreply.github.com> Date: Wed, 13 Mar 2024 13:43:06 +0100 Subject: [PATCH 002/145] update gh workflow actions (#1809) --- .github/workflows/build.yml | 30 +++++------ .github/workflows/check-tests.yaml | 22 ++++---- .github/workflows/contributors.yml | 3 +- .github/workflows/docs.yml | 6 ++- .github/workflows/gh-actions-updater.yaml | 5 +- .github/workflows/lint.yml | 62 +++++++++++------------ .github/workflows/release.yml | 6 +-- .github/workflows/stale.yml | 3 +- .github/workflows/test-integration.yaml | 32 +++++++----- .github/workflows/test.yml | 23 +++++---- .github/workflows/update-flake.yml | 2 +- 11 files changed, 100 insertions(+), 94 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9d4b9925..09c5cd34 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,31 +16,29 @@ jobs: build: runs-on: ubuntu-latest permissions: write-all - steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 2 - - name: Get changed files id: changed-files - uses: tj-actions/changed-files@v34 + uses: dorny/paths-filter@v3 with: - files: | - *.nix - go.* - **/*.go - integration_test/ - config-example.yaml - + filters: | + files: + - '*.nix' + - 'go.*' + - '**/*.go' + - 'integration_test/' + - 'config-example.yaml' - uses: DeterminateSystems/nix-installer-action@main - if: steps.changed-files.outputs.any_changed == 'true' + if: steps.changed-files.outputs.files == 'true' - uses: DeterminateSystems/magic-nix-cache-action@main - if: steps.changed-files.outputs.any_changed == 'true' + if: steps.changed-files.outputs.files == 'true' - name: Run build id: build - if: steps.changed-files.outputs.any_changed == 'true' + if: steps.changed-files.outputs.files == 'true' run: | nix build |& tee build-result BUILD_STATUS="${PIPESTATUS[0]}" @@ -66,8 +64,8 @@ jobs: body: 'Nix build failed with wrong gosum, please update "vendorSha256" (${{ steps.build.outputs.OLD_HASH }}) for the "headscale" package in flake.nix with the new SHA: ${{ steps.build.outputs.NEW_HASH }}' }) - - uses: actions/upload-artifact@v3 - if: steps.changed-files.outputs.any_changed == 'true' + - uses: actions/upload-artifact@v4 + if: steps.changed-files.outputs.files == 'true' with: name: headscale-linux path: result/bin/headscale diff --git a/.github/workflows/check-tests.yaml b/.github/workflows/check-tests.yaml index c085f178..b1b94532 100644 --- a/.github/workflows/check-tests.yaml +++ b/.github/workflows/check-tests.yaml @@ -15,22 +15,22 @@ jobs: fetch-depth: 2 - name: Get changed files id: changed-files - uses: tj-actions/changed-files@v34 + uses: dorny/paths-filter@v3 with: - files: | - *.nix - go.* - **/*.go - integration_test/ - config-example.yaml - + filters: | + files: + - '*.nix' + - 'go.*' + - '**/*.go' + - 'integration_test/' + - 'config-example.yaml' - uses: DeterminateSystems/nix-installer-action@main - if: steps.changed-files.outputs.any_changed == 'true' + if: steps.changed-files.outputs.files == 'true' - uses: DeterminateSystems/magic-nix-cache-action@main - if: steps.changed-files.outputs.any_changed == 'true' + if: steps.changed-files.outputs.files == 'true' - name: Generate and check integration tests - if: steps.changed-files.outputs.any_changed == 'true' + if: steps.changed-files.outputs.files == 'true' run: | nix develop --command bash -c "cd cmd/gh-action-integration-generator/ && go generate" git diff --exit-code .github/workflows/test-integration.yaml diff --git a/.github/workflows/contributors.yml b/.github/workflows/contributors.yml index 4b05ffd2..2c55c002 100644 --- a/.github/workflows/contributors.yml +++ b/.github/workflows/contributors.yml @@ -5,11 +5,12 @@ on: branches: - main workflow_dispatch: + jobs: add-contributors: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Delete upstream contributor branch # Allow continue on failure to account for when the # upstream branch is deleted or does not exist. diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 1d19ed3d..0269965e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,4 +1,5 @@ name: Build documentation + on: push: branches: @@ -15,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install python uses: actions/setup-python@v4 with: @@ -33,6 +34,7 @@ jobs: uses: actions/upload-pages-artifact@v1 with: path: ./site + deploy: environment: name: github-pages @@ -42,4 +44,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v1 + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/gh-actions-updater.yaml b/.github/workflows/gh-actions-updater.yaml index 6b44051a..48d0fabd 100644 --- a/.github/workflows/gh-actions-updater.yaml +++ b/.github/workflows/gh-actions-updater.yaml @@ -1,6 +1,5 @@ name: GitHub Actions Version Updater -# Controls when the action will run. on: schedule: # Automatically run on every Sunday @@ -11,13 +10,13 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: # [Required] Access token with `workflow` scope. token: ${{ secrets.WORKFLOW_SECRET }} - name: Run GitHub Actions Version Updater - uses: saadmk11/github-actions-version-updater@v0.7.1 + uses: saadmk11/github-actions-version-updater@v0.8.1 with: # [Required] Access token with `workflow` scope. token: ${{ secrets.WORKFLOW_SECRET }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ade5ffc0..8f38f9d7 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,7 +1,6 @@ ---- name: Lint -on: [push, pull_request] +on: [pull_request] concurrency: group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }} @@ -11,63 +10,64 @@ jobs: golangci-lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 2 - - name: Get changed files id: changed-files - uses: tj-actions/changed-files@v34 + uses: dorny/paths-filter@v3 with: - files: | - *.nix - go.* - **/*.go - integration_test/ - config-example.yaml - + filters: | + files: + - '*.nix' + - 'go.*' + - '**/*.go' + - 'integration_test/' + - 'config-example.yaml' - uses: DeterminateSystems/nix-installer-action@main + if: steps.changed-files.outputs.files == 'true' - uses: DeterminateSystems/magic-nix-cache-action@main + if: steps.changed-files.outputs.files == 'true' - name: golangci-lint - if: steps.changed-files.outputs.any_changed == 'true' + if: steps.changed-files.outputs.files == 'true' run: nix develop --command -- golangci-lint run --new-from-rev=${{github.event.pull_request.base.sha}} --out-format=github-actions . prettier-lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: fetch-depth: 2 - - name: Get changed files id: changed-files - uses: tj-actions/changed-files@v14.1 + uses: dorny/paths-filter@v3 with: - files: | - *.nix - **/*.md - **/*.yml - **/*.yaml - **/*.ts - **/*.js - **/*.sass - **/*.css - **/*.scss - **/*.html - + filters: | + files: + - '*.nix' + - '**/*.md' + - '**/*.yml' + - '**/*.yaml' + - '**/*.ts' + - '**/*.js' + - '**/*.sass' + - '**/*.css' + - '**/*.scss' + - '**/*.html' - uses: DeterminateSystems/nix-installer-action@main + if: steps.changed-files.outputs.files == 'true' - uses: DeterminateSystems/magic-nix-cache-action@main + if: steps.changed-files.outputs.files == 'true' - name: Prettify code - if: steps.changed-files.outputs.any_changed == 'true' + if: steps.changed-files.outputs.files == 'true' run: nix develop --command -- prettier --no-error-on-unmatched-pattern --ignore-unknown --check **/*.{ts,js,md,yaml,yml,sass,css,scss,html} proto-lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - + - uses: actions/checkout@v4 - uses: DeterminateSystems/nix-installer-action@main - uses: DeterminateSystems/magic-nix-cache-action@main diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7929ac56..3554677f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,18 +12,18 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GHCR - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index c30571c4..f7c4ae75 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -1,4 +1,5 @@ name: Close inactive issues + on: schedule: - cron: "30 1 * * *" @@ -10,7 +11,7 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/stale@v5 + - uses: actions/stale@v9 with: days-before-issue-stale: 90 days-before-issue-close: 7 diff --git a/.github/workflows/test-integration.yaml b/.github/workflows/test-integration.yaml index bad05bce..06a99db4 100644 --- a/.github/workflows/test-integration.yaml +++ b/.github/workflows/test-integration.yaml @@ -61,23 +61,27 @@ jobs: - uses: actions/checkout@v4 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 + uses: dorny/paths-filter@v3 with: - files: | - *.nix - go.* - **/*.go - integration_test/ - config-example.yaml + filters: | + files: + - '*.nix' + - 'go.*' + - '**/*.go' + - 'integration_test/' + - 'config-example.yaml' + - uses: DeterminateSystems/nix-installer-action@main + if: steps.changed-files.outputs.files == 'true' + - uses: DeterminateSystems/magic-nix-cache-action@main + if: steps.changed-files.outputs.files == 'true' + - uses: satackey/action-docker-layer-caching@main + if: steps.changed-files.outputs.files == 'true' + continue-on-error: true - name: Run Integration Test uses: Wandalen/wretry.action@master - if: steps.changed-files.outputs.any_changed == 'true' + if: steps.changed-files.outputs.files == 'true' env: USE_POSTGRES: ${{ matrix.database == 'postgres' && '1' || '0' }} with: @@ -98,12 +102,12 @@ jobs: -parallel 1 \ -run "^${{ matrix.test }}$" - uses: actions/upload-artifact@v4 - if: always() && steps.changed-files.outputs.any_changed == 'true' + if: always() && steps.changed-files.outputs.files == 'true' with: name: ${{ matrix.test }}-${{matrix.database}}-logs path: "control_logs/*.log" - uses: actions/upload-artifact@v4 - if: always() && steps.changed-files.outputs.any_changed == 'true' + if: always() && steps.changed-files.outputs.files == 'true' with: name: ${{ matrix.test }}-${{matrix.database}}-pprof path: "control_logs/*.pprof.tar" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c2700d17..b03fc434 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,26 +11,27 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 2 - name: Get changed files id: changed-files - uses: tj-actions/changed-files@v34 + uses: dorny/paths-filter@v3 with: - files: | - *.nix - go.* - **/*.go - integration_test/ - config-example.yaml + filters: | + files: + - '*.nix' + - 'go.*' + - '**/*.go' + - 'integration_test/' + - 'config-example.yaml' - uses: DeterminateSystems/nix-installer-action@main - if: steps.changed-files.outputs.any_changed == 'true' + if: steps.changed-files.outputs.files == 'true' - uses: DeterminateSystems/magic-nix-cache-action@main - if: steps.changed-files.outputs.any_changed == 'true' + if: steps.changed-files.outputs.files == 'true' - name: Run tests - if: steps.changed-files.outputs.any_changed == 'true' + if: steps.changed-files.outputs.files == 'true' run: nix develop --check diff --git a/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml index 6fcea23e..c04bb9cc 100644 --- a/.github/workflows/update-flake.yml +++ b/.github/workflows/update-flake.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Nix uses: DeterminateSystems/nix-installer-action@main - name: Update flake.lock From 95004de5e8c7a09429dc1fecdafb23b50f920f1a Mon Sep 17 00:00:00 2001 From: Sandro Date: Thu, 14 Mar 2024 22:56:55 +0100 Subject: [PATCH 003/145] docs/reverse-proxy: use standard map, correct X-Forwarded-Proto variable for nginx (#1790) --- docs/reverse-proxy.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/reverse-proxy.md b/docs/reverse-proxy.md index aab9f848..1f417c9b 100644 --- a/docs/reverse-proxy.md +++ b/docs/reverse-proxy.md @@ -33,8 +33,7 @@ The following example configuration can be used in your nginx setup, substitutin ```Nginx map $http_upgrade $connection_upgrade { - default keep-alive; - 'websocket' upgrade; + default upgrade; '' close; } @@ -61,7 +60,7 @@ server { proxy_buffering off; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; + proxy_set_header X-Forwarded-Proto $scheme; add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always; } } From b477e5f3667905e0154da864273f461bf0d41ebd Mon Sep 17 00:00:00 2001 From: zx Date: Fri, 15 Mar 2024 06:01:33 +0800 Subject: [PATCH 004/145] Update docker related doc (#1421) --- docs/running-headscale-container.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/running-headscale-container.md b/docs/running-headscale-container.md index 862ba032..6a582ba9 100644 --- a/docs/running-headscale-container.md +++ b/docs/running-headscale-container.md @@ -92,6 +92,26 @@ Note: use `0.0.0.0:8080:8080` instead of `127.0.0.1:8080:8080` if you want to ex This command will mount `config/` under `/etc/headscale`, forward port 8080 out of the container so the `headscale` instance becomes available and then detach so headscale runs in the background. +Example `docker-compose.yaml` + +```yaml +version: "3.7" + +services: + headscale: + image: headscale/headscale:0.22.3 + restart: unless-stopped + container_name: headscale + ports: + - "127.0.0.1:8080:8080" + - "127.0.0.1:9090:9090" + volumes: + # pls change [config_path] to the fullpath of the config folder just created + - [config_path]:/etc/headscale + command: headscale serve + +``` + 5. Verify `headscale` is running: Follow the container logs: From c29eddded370f715d2f4c106899d6ca015542b90 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 17 Mar 2024 09:01:57 +0100 Subject: [PATCH 005/145] flake.lock: Update (#1833) --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 020648b6..89d88b38 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1709126324, - "narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=", + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", "owner": "numtide", "repo": "flake-utils", - "rev": "d465f4819400de7c8d874d50b982301f28a84605", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", "type": "github" }, "original": { @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1709968316, - "narHash": "sha256-4rZEtEDT6jcgRaqxsatBeds7x1PoEiEjb6QNGb4mNrk=", + "lastModified": 1710534455, + "narHash": "sha256-huQT4Xs0y4EeFKn2BTBVYgEwJSv8SDlm82uWgMnCMmI=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "0e7f98a5f30166cbed344569426850b21e4091d4", + "rev": "9af9c1c87ed3e3ed271934cb896e0cdd33dae212", "type": "github" }, "original": { From 20bf3777d3a4c71066eabf7b48ece311bba872b1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 19 Mar 2024 22:39:43 +0100 Subject: [PATCH 006/145] docs(README): update contributors (#1834) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- README.md | 1057 +++++++++++++++++++++++++++++------------------------ 1 file changed, 576 insertions(+), 481 deletions(-) diff --git a/README.md b/README.md index 457e56ff..4c7ccacb 100644 --- a/README.md +++ b/README.md @@ -225,13 +225,6 @@ make build Nico - - - Even -
- Even Holthe -
- e-zk/ @@ -239,6 +232,13 @@ make build e-zk + + + Even +
+ Even Holthe +
+ Justin @@ -283,13 +283,6 @@ make build GrigoriyMikhalkin - - - Christian -
- Christian Heusel -
- Mike @@ -297,6 +290,13 @@ make build Mike Lloyd + + + Christian +
+ Christian Heusel +
+ Anton @@ -321,7 +321,7 @@ make build - + Azz/
Azz @@ -358,17 +358,10 @@ make build
- - Fernando + + bravechamp/
- Fernando De Lucchi -
- - - - Orville -
- Orville Q. Song + bravechamp
@@ -379,54 +372,61 @@ make build - - bravechamp/ + + Orville
- bravechamp + Orville Q. Song
- - Deon + + Fernando
- Deon Thomas + Fernando De Lucchi +
+ + + + MichaelKo/ +
+ MichaelKo
- - Jamie + + kevinlin/
- Jamie Greeff + kevinlin
- - Jonathan + + unreality/
- Jonathan Wright + unreality
- - ChibangLW/ + + loprima-l/
- ChibangLW + loprima-l
- - Maja + + Samuel
- Maja Bojarska + Samuel Lock
- - Mevan + + Paul
- Mevan Samaratunga + Paul Tötterman
@@ -439,49 +439,114 @@ make build - - Paul + + Mevan
- Paul Tötterman + Mevan Samaratunga
- - Samuel + + Maja
- Samuel Lock + Maja Bojarska
- - loprima-l/ + + ChibangLW/
- loprima-l + ChibangLW
- - unreality/ + + Jonathan
- unreality + Jonathan Wright
- - MichaelKo/ + + Jamie
- MichaelKo + Jamie Greeff
- - kevinlin/ + + Deon
- kevinlin + Deon Thomas
+ + + Sean +
+ Sean Reifschneider +
+ + + + derelm/ +
+ derelm +
+ + + + Andrei +
+ Andrei Pechkurov +
+ + + + thomas/ +
+ thomas +
+ + + + Zhenyu +
+ Zhenyu Qi +
+ + + + Vitalij +
+ Vitalij Dovhanyc +
+ + + + + + Victor +
+ Victor Freire +
+ + + + Steven +
+ Steven Honson +
+ + + + Silver +
+ Silver Bullet +
+ Snack/ @@ -503,6 +568,15 @@ make build Casey Marshall + + + + + DeveloperDragon/ +
+ DeveloperDragon +
+ dbevacqua/ @@ -510,6 +584,27 @@ make build dbevacqua + + + Sandro/ +
+ Sandro +
+ + + + Pavlos +
+ Pavlos Vinieratos +
+ + + + Pallab +
+ Pallab Pain +
+ Josh @@ -517,13 +612,6 @@ make build Josh Taylor - - - LIU -
- LIU HANCHENG -
- @@ -534,346 +622,126 @@ make build - - Pavlos + + LIU
- Pavlos Vinieratos + LIU HANCHENG
- - Silver + + caelansar/
- Silver Bullet + caelansar
- - Steven + + Ziyuan
- Steven Honson + Ziyuan Han
- - Victor + + Zhiyuan
- Victor Freire + Zhiyuan Zheng
- - Zhenyu + + Zakhar
- Zhenyu Qi + Zakhar Bessarab
- - thomas/ + + Zachary
- thomas + Zachary Newell
- - Andrei + + Yujie
- Andrei Pechkurov + Yujie Xia
- - Sean + + 杨斌
- Sean Reifschneider + 杨斌 Aben
- - Abraham + + Tjerk
- Abraham Ingersoll + Tjerk Woudsma
- - Albert + + Till
- Albert Copeland + Till Hoffmann
- - Anoop + + Tianon
- Anoop Sundaresh + Tianon Gravi
- - Antoine + + The
- Antoine POPINEAU + The Gitter Badger
- - Antonio + + Teteros/
- Antonio Fernandez + Teteros
- - Aofei + + Tanner/
- Aofei Sheng + Tanner
- - Arnar/ + + sophware/
- Arnar + sophware
- - Arthur + + Stepan/
- Arthur Woimbée + Stepan
- - Avirut + + Stefan
- Avirut Mehta -
- - - - - - Azamat -
- Azamat H. Hackimov -
- - - - Bryan -
- Bryan Stenson -
- - - - Carson -
- Carson Yang -
- - - - Darrell -
- Darrell Kundel -
- - - - fatih-acar/ -
- fatih-acar -
- - - - Felix -
- Felix Kronlage-Dammers -
- - - - - - Felix -
- Felix Yan -
- - - - Gabe -
- Gabe Cook -
- - - - JJGadgets/ -
- JJGadgets -
- - - - hrtkpf/ -
- hrtkpf -
- - - - JesseBot/ -
- JesseBot -
- - - - Jim -
- Jim Tittsler -
- - - - - - Johan -
- Johan Siebens -
- - - - John -
- John Axel Eriksson -
- - - - Jonathan -
- Jonathan de Jong -
- - - - Julien -
- Julien Zweverink -
- - - - Kurnia -
- Kurnia D Win -
- - - - Lucalux/ -
- Lucalux -
- - - - - - Marc/ -
- Marc -
- - - - Mesar -
- Mesar Hameed -
- - - - Michael -
- Michael Savage -
- - - - Philipp -
- Philipp Krivanec -
- - - - Pierre -
- Pierre Carru -
- - - - Pontus -
- Pontus N -
- - - - - - Rasmus -
- Rasmus Moorats -
- - - - rcursaru/ -
- rcursaru -
- - - - Mend -
- Mend Renovate -
- - - - Ryan -
- Ryan Fowler -
- - - - Sebastian/ -
- Sebastian -
- - - - Shaanan -
- Shaanan Cohney + Stefan VanBuren
@@ -886,163 +754,75 @@ make build - - Stefan + + Shaanan
- Stefan VanBuren + Shaanan Cohney
- - sophware/ + + Sebastian/
- sophware + Sebastian
- - Tanner/ + + Àlex
- Tanner + Àlex Torregrosa
- - Teteros/ + + zy/
- Teteros + zy
- - The + + zx/
- The Gitter Badger + zx
- - Tianon + + Wakeful
- Tianon Gravi + Wakeful Cloud
- - Till + + phpmalik/
- Till Hoffmann + phpmalik
- - Tjerk + + Tommi
- Tjerk Woudsma + Tommi Pernila
- - 杨斌 + + nicholas-yap/
- 杨斌 Aben + nicholas-yap
- - Yujie + + manju-rn/
- Yujie Xia -
- - - - Zachary -
- Zachary Newell -
- - - - - - Zakhar -
- Zakhar Bessarab -
- - - - Zhiyuan -
- Zhiyuan Zheng -
- - - - Ziyuan -
- Ziyuan Han -
- - - - caelansar/ -
- caelansar -
- - - - derelm/ -
- derelm -
- - - - dnaq/ -
- dnaq -
- - - - - - henning -
- henning mueller -
- - - - ignoramous/ -
- ignoramous -
- - - - jimyag/ -
- jimyag -
- - - - suhelen/ -
- suhelen -
- - - - sharkonet/ -
- sharkonet + manju-rn
@@ -1055,54 +835,369 @@ make build - - manju-rn/ + + lööps/
- manju-rn + lööps
- - nicholas-yap/ + + lionel.codes/
- nicholas-yap + lionel.codes
- - Tommi + + suhelen/
- Tommi Pernila + suhelen
- - phpmalik/ + + jimyag/
- phpmalik + jimyag
- - Wakeful + + ignoramous/
- Wakeful Cloud + ignoramous
- - zy/ + + henning
- zy + henning mueller
- - Àlex + + foosinn/
- Àlex Torregrosa + foosinn +
+ + + + dyz/ +
+ dyz +
+ + + + dnaq/ +
+ dnaq +
+ + + + danielalvsaaker/ +
+ danielalvsaaker +
+ + + + JJGadgets/ +
+ JJGadgets +
+ + + + Gabe +
+ Gabe Cook +
+ + + + + + Felix +
+ Felix Yan +
+ + + + Felix +
+ Felix Kronlage-Dammers +
+ + + + fatih-acar/ +
+ fatih-acar +
+ + + + Darrell +
+ Darrell Kundel +
+ + + + Carson +
+ Carson Yang +
+ + + + Calvin +
+ Calvin Figuereo-Supraner +
+ + + + + + Bryan +
+ Bryan Stenson +
+ + + + Azamat +
+ Azamat H. Hackimov +
+ + + + Avirut +
+ Avirut Mehta +
+ + + + Arthur +
+ Arthur Woimbée +
+ + + + Arnar/ +
+ Arnar +
+ + + + Aofei +
+ Aofei Sheng +
+ + + + + + Antonio +
+ Antonio Fernandez +
+ + + + Antoine +
+ Antoine POPINEAU +
+ + + + Anoop +
+ Anoop Sundaresh +
+ + + + Alexander +
+ Alexander Halbarth +
+ + + + Albert +
+ Albert Copeland +
+ + + + Abraham +
+ Abraham Ingersoll +
+ + + + + + Ryan +
+ Ryan Fowler +
+ + + + Mend +
+ Mend Renovate +
+ + + + rcursaru/ +
+ rcursaru +
+ + + + Rasmus +
+ Rasmus Moorats +
+ + + + Pontus +
+ Pontus N +
+ + + + Pierre +
+ Pierre Carru +
+ + + + + + Philipp +
+ Philipp Krivanec +
+ + + + Michael +
+ Michael Savage +
+ + + + Mesar +
+ Mesar Hameed +
+ + + + Marc/ +
+ Marc +
+ + + + Lucalux/ +
+ Lucalux +
+ + + + Linus/ +
+ Linus +
+ + + + + + Kurnia +
+ Kurnia D Win +
+ + + + Julien +
+ Julien Zweverink +
+ + + + Jonathan +
+ Jonathan de Jong +
+ + + + John +
+ John Axel Eriksson +
+ + + + Johan +
+ Johan Siebens +
+ + + + Jim +
+ Jim Tittsler +
+ + + + + + JesseBot/ +
+ JesseBot +
+ + + + hrtkpf/ +
+ hrtkpf
From 785b150467126c547c482afbb3a4528d2d6bde9a Mon Sep 17 00:00:00 2001 From: George Pchelkin Date: Tue, 19 Mar 2024 21:43:43 +0000 Subject: [PATCH 007/145] Fix typo in docs - DB file path (#1546) --- docs/running-headscale-linux.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/running-headscale-linux.md b/docs/running-headscale-linux.md index 66ccc3d3..282cd5ee 100644 --- a/docs/running-headscale-linux.md +++ b/docs/running-headscale-linux.md @@ -15,7 +15,7 @@ This includes running Headscale with SystemD. If you are migrating from the old manual install, the best thing would be to remove the files installed by following [the guide in reverse](./running-headscale-linux-manual.md). -You should _not_ delete the database (`/var/headscale/db.sqlite`) and the +You should _not_ delete the database (`/var/lib/headscale/db.sqlite`) and the configuration (`/etc/headscale/config.yaml`). ## Installation From c1d4fef1945ee506b24dfda58b28667467472f40 Mon Sep 17 00:00:00 2001 From: Sandro Date: Fri, 22 Mar 2024 10:28:10 +0100 Subject: [PATCH 008/145] Downgrade update sent to debug (#1843) --- hscontrol/poll.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hscontrol/poll.go b/hscontrol/poll.go index bf48cc05..22dd78ff 100644 --- a/hscontrol/poll.go +++ b/hscontrol/poll.go @@ -507,7 +507,7 @@ func (h *Headscale) handlePoll( } log.Trace().Str("node", node.Hostname).TimeDiff("timeSpent", time.Now(), startWrite).Str("mkey", node.MachineKey.String()).Int("type", int(update.Type)).Msg("finished writing mapresp to node") - log.Info(). + log.Debug(). Caller(). Bool("readOnly", mapRequest.ReadOnly). Bool("omitPeers", mapRequest.OmitPeers). From 74ff14eb3087ed560b4a64e23d00e881c630ccec Mon Sep 17 00:00:00 2001 From: ohdearaugustin Date: Fri, 22 Mar 2024 19:50:35 +0100 Subject: [PATCH 009/145] update docs workflow (#1832) --- .github/workflows/docs.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 0269965e..c5cddef7 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -31,7 +31,7 @@ jobs: - name: Build docs run: mkdocs build --strict - name: Upload artifact - uses: actions/upload-pages-artifact@v1 + uses: actions/upload-pages-artifact@v3 with: path: ./site @@ -39,9 +39,14 @@ jobs: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} + permissions: + pages: write + id-token: write runs-on: ubuntu-latest needs: build steps: + - name: Configure Pages + uses: actions/configure-pages@v4 - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 From 4d9021047fa5421189efa8cab281d4873062d55a Mon Sep 17 00:00:00 2001 From: Maja Bojarska Date: Fri, 22 Mar 2024 19:55:20 +0100 Subject: [PATCH 010/145] Fix/improve documentation formatting (#1575) * Fix docs formatting * .prettierignore add docu --------- Co-authored-by: ohdearaugustin --- .prettierignore | 5 + docs/dns-records.md | 30 ++-- docs/running-headscale-container.md | 187 +++++++++++----------- docs/running-headscale-linux-manual.md | 207 ++++++++++++------------- docs/running-headscale-linux.md | 48 +++--- docs/running-headscale-openbsd.md | 201 ++++++++++++------------ 6 files changed, 341 insertions(+), 337 deletions(-) diff --git a/.prettierignore b/.prettierignore index 146ae4dd..d455d02c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1,6 @@ .github/workflows/test-integration-v2* +docs/dns-records.md +docs/running-headscale-container.md +docs/running-headscale-linux-manual.md +docs/running-headscale-linux.md +docs/running-headscale-openbsd.md diff --git a/docs/dns-records.md b/docs/dns-records.md index c5a07fe9..d049c554 100644 --- a/docs/dns-records.md +++ b/docs/dns-records.md @@ -18,23 +18,25 @@ An example use case is to serve apps on the same host via a reverse proxy like N 1. Change the `config.yaml` to contain the desired records like so: -```yaml -dns_config: - ... - extra_records: - - name: "prometheus.myvpn.example.com" - type: "A" - value: "100.64.0.3" + ```yaml + dns_config: + ... + extra_records: + - name: "prometheus.myvpn.example.com" + type: "A" + value: "100.64.0.3" - - name: "grafana.myvpn.example.com" - type: "A" - value: "100.64.0.3" - ... -``` + - name: "grafana.myvpn.example.com" + type: "A" + value: "100.64.0.3" + ... + ``` -2. Restart your headscale instance. +1. Restart your headscale instance. -Beware of the limitations listed later on! + !!! warning + + Beware of the limitations listed later on! ### 2. Verify that the records are set diff --git a/docs/running-headscale-container.md b/docs/running-headscale-container.md index 6a582ba9..73c1107e 100644 --- a/docs/running-headscale-container.md +++ b/docs/running-headscale-container.md @@ -17,127 +17,124 @@ not work with alternatives like [Podman](https://podman.io). The Docker image ca 1. Prepare a directory on the host Docker node in your directory of choice, used to hold `headscale` configuration and the [SQLite](https://www.sqlite.org/) database: -```shell -mkdir -p ./headscale/config -cd ./headscale -``` + ```shell + mkdir -p ./headscale/config + cd ./headscale + ``` -2. Create an empty SQlite datebase in the headscale directory: +1. Create an empty SQlite datebase in the headscale directory: -```shell -touch ./config/db.sqlite -``` + ```shell + touch ./config/db.sqlite + ``` -3. **(Strongly Recommended)** Download a copy of the [example configuration](https://github.com/juanfont/headscale/blob/main/config-example.yaml) from the headscale repository. +1. **(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: + - Using `wget`: -```shell -wget -O ./config/config.yaml https://raw.githubusercontent.com/juanfont/headscale/main/config-example.yaml -``` + ```shell + wget -O ./config/config.yaml https://raw.githubusercontent.com/juanfont/headscale/main/config-example.yaml + ``` -Using curl: + - Using `curl`: -```shell -curl https://raw.githubusercontent.com/juanfont/headscale/main/config-example.yaml -o ./config/config.yaml -``` + ```shell + curl https://raw.githubusercontent.com/juanfont/headscale/main/config-example.yaml -o ./config/config.yaml + ``` -**(Advanced)** If you would like to hand craft a config file **instead** of downloading the example config file, create a blank `headscale` configuration in the headscale directory to edit: + - **(Advanced)** If you would like to hand craft a config file **instead** of downloading the example config file, create a blank `headscale` configuration in the headscale directory to edit: -```shell -touch ./config/config.yaml -``` + ```shell + touch ./config/config.yaml + ``` -Modify the config file to your preferences before launching Docker container. -Here are some settings that you likely want: + Modify the config file to your preferences before launching Docker container. + Here are some settings that you likely want: -```yaml -# Change to your hostname or host IP -server_url: http://your-host-name:8080 -# Listen to 0.0.0.0 so it's accessible outside the container -metrics_listen_addr: 0.0.0.0:9090 -# The default /var/lib/headscale path is not writable in the container -noise: - private_key_path: /etc/headscale/noise_private.key -# The default /var/lib/headscale path is not writable in the container -derp: - private_key_path: /etc/headscale/private.key -# The default /var/run/headscale path is not writable in the container -unix_socket: /etc/headscale/headscale.sock -# The default /var/lib/headscale path is not writable in the container -database.type: sqlite3 -database.sqlite.path: /etc/headscale/db.sqlite -``` + ```yaml + # Change to your hostname or host IP + server_url: http://your-host-name:8080 + # Listen to 0.0.0.0 so it's accessible outside the container + metrics_listen_addr: 0.0.0.0:9090 + # The default /var/lib/headscale path is not writable in the container + noise: + private_key_path: /etc/headscale/noise_private.key + # The default /var/lib/headscale path is not writable in the container + derp: + private_key_path: /etc/headscale/private.key + # The default /var/run/headscale path is not writable in the container + unix_socket: /etc/headscale/headscale.sock + # The default /var/lib/headscale path is not writable in the container + database.type: sqlite3 + database.sqlite.path: /etc/headscale/db.sqlite + ``` -Alternatively, you can mount `/var/lib` and `/var/run` from your host system by adding -`--volume $(pwd)/lib:/var/lib/headscale` and `--volume $(pwd)/run:/var/run/headscale` -in the next step. + Alternatively, you can mount `/var/lib` and `/var/run` from your host system by adding + `--volume $(pwd)/lib:/var/lib/headscale` and `--volume $(pwd)/run:/var/run/headscale` + in the next step. -4. Start the headscale server while working in the host headscale directory: +1. Start the headscale server while working in the host headscale directory: -```shell -docker run \ - --name headscale \ - --detach \ - --volume $(pwd)/config:/etc/headscale/ \ - --publish 127.0.0.1:8080:8080 \ - --publish 127.0.0.1:9090:9090 \ - headscale/headscale: \ - headscale serve + ```shell + docker run \ + --name headscale \ + --detach \ + --volume $(pwd)/config:/etc/headscale/ \ + --publish 127.0.0.1:8080:8080 \ + --publish 127.0.0.1:9090:9090 \ + headscale/headscale: \ + headscale serve + ``` -``` + Note: use `0.0.0.0:8080:8080` instead of `127.0.0.1:8080:8080` if you want to expose the container externally. -Note: use `0.0.0.0:8080:8080` instead of `127.0.0.1:8080:8080` if you want to expose the container externally. + This command will mount `config/` under `/etc/headscale`, forward port 8080 out of the container so the + `headscale` instance becomes available and then detach so headscale runs in the background. -This command will mount `config/` under `/etc/headscale`, forward port 8080 out of the container so the -`headscale` instance becomes available and then detach so headscale runs in the background. + Example `docker-compose.yaml` -Example `docker-compose.yaml` + ```yaml + version: "3.7" + + services: + headscale: + image: headscale/headscale:0.22.3 + restart: unless-stopped + container_name: headscale + ports: + - "127.0.0.1:8080:8080" + - "127.0.0.1:9090:9090" + volumes: + # pls change [config_path] to the fullpath of the config folder just created + - [config_path]:/etc/headscale + command: headscale serve + ``` -```yaml -version: "3.7" +1. Verify `headscale` is running: + Follow the container logs: -services: - headscale: - image: headscale/headscale:0.22.3 - restart: unless-stopped - container_name: headscale - ports: - - "127.0.0.1:8080:8080" - - "127.0.0.1:9090:9090" - volumes: - # pls change [config_path] to the fullpath of the config folder just created - - [config_path]:/etc/headscale - command: headscale serve + ```shell + docker logs --follow headscale + ``` -``` + Verify running containers: -5. Verify `headscale` is running: + ```shell + docker ps + ``` -Follow the container logs: + Verify `headscale` is available: -```shell -docker logs --follow headscale -``` + ```shell + curl http://127.0.0.1:9090/metrics + ``` -Verify running containers: +1. Create a user ([tailnet](https://tailscale.com/kb/1136/tailnet/)): -```shell -docker ps -``` - -Verify `headscale` is available: - -```shell -curl http://127.0.0.1:9090/metrics -``` - -6. Create a user ([tailnet](https://tailscale.com/kb/1136/tailnet/)): - -```shell -docker exec headscale \ - headscale users create myfirstuser -``` + ```shell + docker exec headscale \ + headscale users create myfirstuser + ``` ### Register a machine (normal login) diff --git a/docs/running-headscale-linux-manual.md b/docs/running-headscale-linux-manual.md index 20e88d54..4108208f 100644 --- a/docs/running-headscale-linux-manual.md +++ b/docs/running-headscale-linux-manual.md @@ -15,78 +15,77 @@ describing how to make `headscale` run properly in a server environment. 1. Download the latest [`headscale` binary from GitHub's release page](https://github.com/juanfont/headscale/releases): -```shell -wget --output-document=/usr/local/bin/headscale \ - https://github.com/juanfont/headscale/releases/download/v/headscale__linux_ -``` + ```shell + wget --output-document=/usr/local/bin/headscale \ + https://github.com/juanfont/headscale/releases/download/v/headscale__linux_ + ``` -2. Make `headscale` executable: +1. Make `headscale` executable: -```shell -chmod +x /usr/local/bin/headscale -``` + ```shell + chmod +x /usr/local/bin/headscale + ``` -3. Prepare a directory to hold `headscale` configuration and the [SQLite](https://www.sqlite.org/) database: +1. Prepare a directory to hold `headscale` configuration and the [SQLite](https://www.sqlite.org/) database: -```shell -# Directory for configuration + ```shell + # Directory for configuration -mkdir -p /etc/headscale + mkdir -p /etc/headscale -# Directory for Database, and other variable data (like certificates) -mkdir -p /var/lib/headscale -# or if you create a headscale user: -useradd \ - --create-home \ - --home-dir /var/lib/headscale/ \ - --system \ - --user-group \ - --shell /usr/sbin/nologin \ - headscale -``` + # Directory for Database, and other variable data (like certificates) + mkdir -p /var/lib/headscale + # or if you create a headscale user: + useradd \ + --create-home \ + --home-dir /var/lib/headscale/ \ + --system \ + --user-group \ + --shell /usr/sbin/nologin \ + headscale + ``` -4. Create an empty SQLite database: +1. Create an empty SQLite database: -```shell -touch /var/lib/headscale/db.sqlite -``` + ```shell + touch /var/lib/headscale/db.sqlite + ``` -5. Create a `headscale` configuration: +1. Create a `headscale` configuration: -```shell -touch /etc/headscale/config.yaml -``` + ```shell + touch /etc/headscale/config.yaml + ``` -**(Strongly Recommended)** Download a copy of the [example configuration][config-example.yaml](https://github.com/juanfont/headscale/blob/main/config-example.yaml) from the headscale repository. + **(Strongly Recommended)** Download a copy of the [example configuration][config-example.yaml](https://github.com/juanfont/headscale/blob/main/config-example.yaml) from the headscale repository. -6. Start the headscale server: +1. Start the headscale server: -```shell -headscale serve -``` + ```shell + headscale serve + ``` -This command will start `headscale` in the current terminal session. + This command will start `headscale` in the current terminal session. ---- + --- -To continue the tutorial, open a new terminal and let it run in the background. -Alternatively use terminal emulators like [tmux](https://github.com/tmux/tmux) or [screen](https://www.gnu.org/software/screen/). + To continue the tutorial, open a new terminal and let it run in the background. + Alternatively use terminal emulators like [tmux](https://github.com/tmux/tmux) or [screen](https://www.gnu.org/software/screen/). -To run `headscale` in the background, please follow the steps in the [SystemD section](#running-headscale-in-the-background-with-systemd) before continuing. + To run `headscale` in the background, please follow the steps in the [SystemD section](#running-headscale-in-the-background-with-systemd) before continuing. -7. Verify `headscale` is running: +1. Verify `headscale` is running: + Verify `headscale` is available: -Verify `headscale` is available: + ```shell + curl http://127.0.0.1:9090/metrics + ``` -```shell -curl http://127.0.0.1:9090/metrics -``` +1. Create a user ([tailnet](https://tailscale.com/kb/1136/tailnet/)): -8. Create a user ([tailnet](https://tailscale.com/kb/1136/tailnet/)): - -```shell -headscale users create myfirstuser -``` + ```shell + headscale users create myfirstuser + ``` ### Register a machine (normal login) @@ -118,81 +117,81 @@ tailscale up --login-server --authkey ## Running `headscale` in the background with SystemD -:warning: **Deprecated**: This part is very outdated and you should use the [pre-packaged Headscale for this](./running-headscale-linux.md +:warning: **Deprecated**: This part is very outdated and you should use the [pre-packaged Headscale for this](./running-headscale-linux.md) This section demonstrates how to run `headscale` as a service in the background with [SystemD](https://www.freedesktop.org/wiki/Software/systemd/). This should work on most modern Linux distributions. 1. Create a SystemD service configuration at `/etc/systemd/system/headscale.service` containing: -```systemd -[Unit] -Description=headscale controller -After=syslog.target -After=network.target + ```systemd + [Unit] + Description=headscale controller + After=syslog.target + After=network.target -[Service] -Type=simple -User=headscale -Group=headscale -ExecStart=/usr/local/bin/headscale serve -Restart=always -RestartSec=5 + [Service] + Type=simple + User=headscale + Group=headscale + ExecStart=/usr/local/bin/headscale serve + Restart=always + RestartSec=5 -# Optional security enhancements -NoNewPrivileges=yes -PrivateTmp=yes -ProtectSystem=strict -ProtectHome=yes -WorkingDirectory=/var/lib/headscale -ReadWritePaths=/var/lib/headscale /var/run/headscale -AmbientCapabilities=CAP_NET_BIND_SERVICE -RuntimeDirectory=headscale + # Optional security enhancements + NoNewPrivileges=yes + PrivateTmp=yes + ProtectSystem=strict + ProtectHome=yes + WorkingDirectory=/var/lib/headscale + ReadWritePaths=/var/lib/headscale /var/run/headscale + AmbientCapabilities=CAP_NET_BIND_SERVICE + RuntimeDirectory=headscale -[Install] -WantedBy=multi-user.target -``` + [Install] + WantedBy=multi-user.target + ``` -Note that when running as the headscale user ensure that, either you add your current user to the headscale group: + Note that when running as the headscale user ensure that, either you add your current user to the headscale group: -```shell -usermod -a -G headscale current_user -``` + ```shell + usermod -a -G headscale current_user + ``` -or run all headscale commands as the headscale user: + or run all headscale commands as the headscale user: -```shell -su - headscale -``` + ```shell + su - headscale + ``` -2. In `/etc/headscale/config.yaml`, override the default `headscale` unix socket with path that is writable by the `headscale` user or group: +1. In `/etc/headscale/config.yaml`, override the default `headscale` unix socket with path that is writable by the `headscale` user or group: -```yaml -unix_socket: /var/run/headscale/headscale.sock -``` + ```yaml + unix_socket: /var/run/headscale/headscale.sock + ``` -3. Reload SystemD to load the new configuration file: +1. Reload SystemD to load the new configuration file: -```shell -systemctl daemon-reload -``` + ```shell + systemctl daemon-reload + ``` -4. Enable and start the new `headscale` service: +1. Enable and start the new `headscale` service: -```shell -systemctl enable --now headscale -``` + ```shell + systemctl enable --now headscale + ``` -5. Verify the headscale service: +1. Verify the headscale service: -```shell -systemctl status headscale -``` + ```shell + systemctl status headscale + ``` -Verify `headscale` is available: + Verify `headscale` is available: -```shell -curl http://127.0.0.1:9090/metrics -``` + ```shell + curl http://127.0.0.1:9090/metrics + ``` `headscale` will now run in the background and start at boot. diff --git a/docs/running-headscale-linux.md b/docs/running-headscale-linux.md index 282cd5ee..aa92cb2b 100644 --- a/docs/running-headscale-linux.md +++ b/docs/running-headscale-linux.md @@ -22,40 +22,40 @@ configuration (`/etc/headscale/config.yaml`). 1. Download the lastest Headscale package for your platform (`.deb` for Ubuntu and Debian) from [Headscale's releases page](https://github.com/juanfont/headscale/releases): -```shell -wget --output-document=headscale.deb \ - https://github.com/juanfont/headscale/releases/download/v/headscale__linux_.deb -``` + ```shell + wget --output-document=headscale.deb \ + https://github.com/juanfont/headscale/releases/download/v/headscale__linux_.deb + ``` -2. Install Headscale: +1. Install Headscale: -```shell -sudo dpkg --install headscale.deb -``` + ```shell + sudo dpkg --install headscale.deb + ``` -3. Enable Headscale service, this will start Headscale at boot: +1. Enable Headscale service, this will start Headscale at boot: -```shell -sudo systemctl enable headscale -``` + ```shell + sudo systemctl enable headscale + ``` -4. Configure Headscale by editing the configuration file: +1. Configure Headscale by editing the configuration file: -```shell -nano /etc/headscale/config.yaml -``` + ```shell + nano /etc/headscale/config.yaml + ``` -5. Start Headscale: +1. Start Headscale: -```shell -sudo systemctl start headscale -``` + ```shell + sudo systemctl start headscale + ``` -6. Check that Headscale is running as intended: +1. Check that Headscale is running as intended: -```shell -systemctl status headscale -``` + ```shell + systemctl status headscale + ``` ## Using Headscale diff --git a/docs/running-headscale-openbsd.md b/docs/running-headscale-openbsd.md index b76c9135..29e340fc 100644 --- a/docs/running-headscale-openbsd.md +++ b/docs/running-headscale-openbsd.md @@ -15,115 +15,116 @@ describing how to make `headscale` run properly in a server environment. ## Install `headscale` -1. Install from ports (Not Recommend) +1. Install from ports (not recommended) - As of OpenBSD 7.2, there's a headscale in ports collection, however, it's severely outdated(v0.12.4). - You can install it via `pkg_add headscale`. + !!! info -2. Install from source on OpenBSD 7.2 + As of OpenBSD 7.2, there's a headscale in ports collection, however, it's severely outdated(v0.12.4). You can install it via `pkg_add headscale`. -```shell -# Install prerequistes -pkg_add go +1. Install from source on OpenBSD 7.2 -git clone https://github.com/juanfont/headscale.git + ```shell + # Install prerequistes + pkg_add go -cd headscale + git clone https://github.com/juanfont/headscale.git -# optionally checkout a release -# option a. you can find offical relase at https://github.com/juanfont/headscale/releases/latest -# option b. get latest tag, this may be a beta release -latestTag=$(git describe --tags `git rev-list --tags --max-count=1`) + cd headscale -git checkout $latestTag + # optionally checkout a release + # option a. you can find offical relase at https://github.com/juanfont/headscale/releases/latest + # option b. get latest tag, this may be a beta release + latestTag=$(git describe --tags `git rev-list --tags --max-count=1`) -go build -ldflags="-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$latestTag" github.com/juanfont/headscale + git checkout $latestTag -# make it executable -chmod a+x headscale + go build -ldflags="-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$latestTag" github.com/juanfont/headscale -# copy it to /usr/local/sbin -cp headscale /usr/local/sbin -``` + # make it executable + chmod a+x headscale -3. Install from source via cross compile + # copy it to /usr/local/sbin + cp headscale /usr/local/sbin + ``` -```shell -# Install prerequistes -# 1. go v1.20+: headscale newer than 0.21 needs go 1.20+ to compile -# 2. gmake: Makefile in the headscale repo is written in GNU make syntax +1. Install from source via cross compile -git clone https://github.com/juanfont/headscale.git + ```shell + # Install prerequistes + # 1. go v1.20+: headscale newer than 0.21 needs go 1.20+ to compile + # 2. gmake: Makefile in the headscale repo is written in GNU make syntax -cd headscale + git clone https://github.com/juanfont/headscale.git -# optionally checkout a release -# option a. you can find offical relase at https://github.com/juanfont/headscale/releases/latest -# option b. get latest tag, this may be a beta release -latestTag=$(git describe --tags `git rev-list --tags --max-count=1`) + cd headscale -git checkout $latestTag + # optionally checkout a release + # option a. you can find offical relase at https://github.com/juanfont/headscale/releases/latest + # option b. get latest tag, this may be a beta release + latestTag=$(git describe --tags `git rev-list --tags --max-count=1`) -make build GOOS=openbsd + git checkout $latestTag -# copy headscale to openbsd machine and put it in /usr/local/sbin -``` + make build GOOS=openbsd + + # copy headscale to openbsd machine and put it in /usr/local/sbin + ``` ## Configure and run `headscale` 1. Prepare a directory to hold `headscale` configuration and the [SQLite](https://www.sqlite.org/) database: -```shell -# Directory for configuration + ```shell + # Directory for configuration -mkdir -p /etc/headscale + mkdir -p /etc/headscale -# Directory for Database, and other variable data (like certificates) -mkdir -p /var/lib/headscale -``` + # Directory for Database, and other variable data (like certificates) + mkdir -p /var/lib/headscale + ``` -2. Create an empty SQLite database: +1. Create an empty SQLite database: -```shell -touch /var/lib/headscale/db.sqlite -``` + ```shell + touch /var/lib/headscale/db.sqlite + ``` -3. Create a `headscale` configuration: +1. Create a `headscale` configuration: -```shell -touch /etc/headscale/config.yaml -``` + ```shell + touch /etc/headscale/config.yaml + ``` **(Strongly Recommended)** Download a copy of the [example configuration][config-example.yaml](https://github.com/juanfont/headscale/blob/main/config-example.yaml) from the headscale repository. -4. Start the headscale server: +1. Start the headscale server: -```shell -headscale serve -``` + ```shell + headscale serve + ``` -This command will start `headscale` in the current terminal session. + This command will start `headscale` in the current terminal session. ---- + *** -To continue the tutorial, open a new terminal and let it run in the background. -Alternatively use terminal emulators like [tmux](https://github.com/tmux/tmux). + To continue the tutorial, open a new terminal and let it run in the background. + Alternatively use terminal emulators like [tmux](https://github.com/tmux/tmux). -To run `headscale` in the background, please follow the steps in the [rc.d section](#running-headscale-in-the-background-with-rcd) before continuing. + To run `headscale` in the background, please follow the steps in the [rc.d section](#running-headscale-in-the-background-with-rcd) before continuing. -5. Verify `headscale` is running: +1. Verify `headscale` is running: -Verify `headscale` is available: + Verify `headscale` is available: -```shell -curl http://127.0.0.1:9090/metrics -``` + ```shell + curl http://127.0.0.1:9090/metrics + ``` -6. Create a user ([tailnet](https://tailscale.com/kb/1136/tailnet/)): +1. Create a user ([tailnet](https://tailscale.com/kb/1136/tailnet/)): -```shell -headscale users create myfirstuser -``` + ```shell + headscale users create myfirstuser + ``` ### Register a machine (normal login) @@ -159,51 +160,51 @@ This section demonstrates how to run `headscale` as a service in the background 1. Create a rc.d service at `/etc/rc.d/headscale` containing: -```shell -#!/bin/ksh + ```shell + #!/bin/ksh -daemon="/usr/local/sbin/headscale" -daemon_logger="daemon.info" -daemon_user="root" -daemon_flags="serve" -daemon_timeout=60 + daemon="/usr/local/sbin/headscale" + daemon_logger="daemon.info" + daemon_user="root" + daemon_flags="serve" + daemon_timeout=60 -. /etc/rc.d/rc.subr + . /etc/rc.d/rc.subr -rc_bg=YES -rc_reload=NO + rc_bg=YES + rc_reload=NO -rc_cmd $1 -``` + rc_cmd $1 + ``` -2. `/etc/rc.d/headscale` needs execute permission: +1. `/etc/rc.d/headscale` needs execute permission: -```shell -chmod a+x /etc/rc.d/headscale -``` + ```shell + chmod a+x /etc/rc.d/headscale + ``` -3. Start `headscale` service: +1. Start `headscale` service: -```shell -rcctl start headscale -``` + ```shell + rcctl start headscale + ``` -4. Make `headscale` service start at boot: +1. Make `headscale` service start at boot: -```shell -rcctl enable headscale -``` + ```shell + rcctl enable headscale + ``` -5. Verify the headscale service: +1. Verify the headscale service: -```shell -rcctl check headscale -``` + ```shell + rcctl check headscale + ``` -Verify `headscale` is available: + Verify `headscale` is available: -```shell -curl http://127.0.0.1:9090/metrics -``` + ```shell + curl http://127.0.0.1:9090/metrics + ``` -`headscale` will now run in the background and start at boot. + `headscale` will now run in the background and start at boot. From 8a8e25a8d1e6bc5fa27b7f72f99bbf24b290e0a6 Mon Sep 17 00:00:00 2001 From: Daniel Lo Nigro Date: Fri, 22 Mar 2024 12:32:01 -0700 Subject: [PATCH 011/145] [docs] Use modern Apt command to install package (#1420) --- docs/running-headscale-linux.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/running-headscale-linux.md b/docs/running-headscale-linux.md index aa92cb2b..5f906009 100644 --- a/docs/running-headscale-linux.md +++ b/docs/running-headscale-linux.md @@ -20,7 +20,7 @@ configuration (`/etc/headscale/config.yaml`). ## Installation -1. Download the lastest Headscale package for your platform (`.deb` for Ubuntu and Debian) from [Headscale's releases page](https://github.com/juanfont/headscale/releases): +1. Download the latest Headscale package for your platform (`.deb` for Ubuntu and Debian) from [Headscale's releases page](https://github.com/juanfont/headscale/releases): ```shell wget --output-document=headscale.deb \ @@ -30,7 +30,7 @@ configuration (`/etc/headscale/config.yaml`). 1. Install Headscale: ```shell - sudo dpkg --install headscale.deb + sudo apt install headscale.deb ``` 1. Enable Headscale service, this will start Headscale at boot: From 2fb7428ba9d57a7116d23a81a17a522baa611853 Mon Sep 17 00:00:00 2001 From: Riezebos <22647971+Riezebos@users.noreply.github.com> Date: Sun, 31 Mar 2024 23:50:25 +0200 Subject: [PATCH 012/145] Add FAQ question about using the same machine as server and client --- docs/faq.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/faq.md b/docs/faq.md index 6331c54a..e33261a3 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -51,3 +51,6 @@ For convenience, we also build Docker images with `headscale`. But **please be a ## Why is my reverse proxy not working with Headscale? We don't know. We don't use reverse proxies with `headscale` ourselves, so we don't have any experience with them. We have [community documentation](https://headscale.net/reverse-proxy/) on how to configure various reverse proxies, and a dedicated [Discord channel](https://discord.com/channels/896711691637780480/1070619818346164324) where you can ask for help to the community. + +## Can/Should I use the same machine as both server and client? +Yes, this is no problem. Install Headscale and Tailscale according to the instructions and use `tailscale up --login-server localhost`. From dbe32829a1b14c2012b09eeb17cca0fa2d3c3481 Mon Sep 17 00:00:00 2001 From: Riezebos <22647971+Riezebos@users.noreply.github.com> Date: Tue, 2 Apr 2024 10:48:16 +0200 Subject: [PATCH 013/145] Update docs/faq.md Co-authored-by: ohdearaugustin --- docs/faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index e33261a3..14ff9e20 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -52,5 +52,5 @@ For convenience, we also build Docker images with `headscale`. But **please be a We don't know. We don't use reverse proxies with `headscale` ourselves, so we don't have any experience with them. We have [community documentation](https://headscale.net/reverse-proxy/) on how to configure various reverse proxies, and a dedicated [Discord channel](https://discord.com/channels/896711691637780480/1070619818346164324) where you can ask for help to the community. -## Can/Should I use the same machine as both server and client? +## Can I use headscale and tailscale on the same machine? Yes, this is no problem. Install Headscale and Tailscale according to the instructions and use `tailscale up --login-server localhost`. From bdf54e802e6e6d29db7465211c40148874105a52 Mon Sep 17 00:00:00 2001 From: Riezebos <22647971+Riezebos@users.noreply.github.com> Date: Tue, 2 Apr 2024 10:49:50 +0200 Subject: [PATCH 014/145] Update answer based on comment --- docs/faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index 14ff9e20..5e603c05 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -53,4 +53,4 @@ For convenience, we also build Docker images with `headscale`. But **please be a We don't know. We don't use reverse proxies with `headscale` ourselves, so we don't have any experience with them. We have [community documentation](https://headscale.net/reverse-proxy/) on how to configure various reverse proxies, and a dedicated [Discord channel](https://discord.com/channels/896711691637780480/1070619818346164324) where you can ask for help to the community. ## Can I use headscale and tailscale on the same machine? -Yes, this is no problem. Install Headscale and Tailscale according to the instructions and use `tailscale up --login-server localhost`. +Running headscale on a machine that is also in the tailnet might cause problems with routing and MagicDNS - at very least. It should be extremely discouraged. \ No newline at end of file From 0fcfd643fa13513fc9a4b31b6b9365583130f470 Mon Sep 17 00:00:00 2001 From: Riezebos <22647971+Riezebos@users.noreply.github.com> Date: Tue, 2 Apr 2024 10:53:17 +0200 Subject: [PATCH 015/145] More concise --- docs/faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index 5e603c05..76712f2c 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -53,4 +53,4 @@ For convenience, we also build Docker images with `headscale`. But **please be a We don't know. We don't use reverse proxies with `headscale` ourselves, so we don't have any experience with them. We have [community documentation](https://headscale.net/reverse-proxy/) on how to configure various reverse proxies, and a dedicated [Discord channel](https://discord.com/channels/896711691637780480/1070619818346164324) where you can ask for help to the community. ## Can I use headscale and tailscale on the same machine? -Running headscale on a machine that is also in the tailnet might cause problems with routing and MagicDNS - at very least. It should be extremely discouraged. \ No newline at end of file +Running headscale on a machine that is also in the tailnet might cause problems with, at least, routing and MagicDNS. It should be extremely discouraged. \ No newline at end of file From 6efc50789d0c5ce3ac53e7b1240df563f8b258f5 Mon Sep 17 00:00:00 2001 From: Riezebos <22647971+Riezebos@users.noreply.github.com> Date: Tue, 2 Apr 2024 14:21:11 +0200 Subject: [PATCH 016/145] Update docs/faq.md Co-authored-by: Juan Font --- docs/faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index 76712f2c..2d602e7f 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -53,4 +53,4 @@ For convenience, we also build Docker images with `headscale`. But **please be a We don't know. We don't use reverse proxies with `headscale` ourselves, so we don't have any experience with them. We have [community documentation](https://headscale.net/reverse-proxy/) on how to configure various reverse proxies, and a dedicated [Discord channel](https://discord.com/channels/896711691637780480/1070619818346164324) where you can ask for help to the community. ## Can I use headscale and tailscale on the same machine? -Running headscale on a machine that is also in the tailnet might cause problems with, at least, routing and MagicDNS. It should be extremely discouraged. \ No newline at end of file +Running headscale on a machine that is also in the tailnet can cause problems with subnet routers, traffic relay nodes, and MagicDNS. It might work, but it is not supported. \ No newline at end of file From 84de1854f8ec1cd641ca6a954ac464c16ca41825 Mon Sep 17 00:00:00 2001 From: Riezebos <22647971+Riezebos@users.noreply.github.com> Date: Fri, 5 Apr 2024 10:01:46 +0200 Subject: [PATCH 017/145] Run prettier --- docs/faq.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index 2d602e7f..fff96132 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -53,4 +53,5 @@ For convenience, we also build Docker images with `headscale`. But **please be a We don't know. We don't use reverse proxies with `headscale` ourselves, so we don't have any experience with them. We have [community documentation](https://headscale.net/reverse-proxy/) on how to configure various reverse proxies, and a dedicated [Discord channel](https://discord.com/channels/896711691637780480/1070619818346164324) where you can ask for help to the community. ## Can I use headscale and tailscale on the same machine? -Running headscale on a machine that is also in the tailnet can cause problems with subnet routers, traffic relay nodes, and MagicDNS. It might work, but it is not supported. \ No newline at end of file + +Running headscale on a machine that is also in the tailnet can cause problems with subnet routers, traffic relay nodes, and MagicDNS. It might work, but it is not supported. From 7bea885b8cfdb9ce313fbdf42849bd9ab1bcb880 Mon Sep 17 00:00:00 2001 From: Brandon Schneider Date: Wed, 10 Apr 2024 13:07:34 -0500 Subject: [PATCH 018/145] Updated dependencies, fixing segfault for OpenBSD Fixes #1857 --- go.mod | 16 ++++++++-------- go.sum | 46 ++++++++++++++++++++++++++++------------------ 2 files changed, 36 insertions(+), 26 deletions(-) diff --git a/go.mod b/go.mod index be7be536..20bd86bd 100644 --- a/go.mod +++ b/go.mod @@ -36,9 +36,9 @@ require ( 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-20231129151722-fdeea329fbba - golang.org/x/crypto v0.19.0 + golang.org/x/crypto v0.21.0 golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 - golang.org/x/net v0.21.0 + golang.org/x/net v0.22.0 golang.org/x/oauth2 v0.17.0 golang.org/x/sync v0.6.0 google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014 @@ -187,12 +187,12 @@ require ( github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.uber.org/multierr v1.11.0 // indirect go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect - golang.org/x/mod v0.15.0 // indirect - golang.org/x/sys v0.17.0 // indirect - golang.org/x/term v0.17.0 // indirect + golang.org/x/mod v0.16.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/term v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.17.0 // indirect + golang.org/x/tools v0.19.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.8 // indirect @@ -203,9 +203,9 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect gvisor.dev/gvisor v0.0.0-20230928000133-4fe30062272c // indirect inet.af/peercred v0.0.0-20210906144145-0893ea02156a // indirect - modernc.org/libc v1.41.0 // indirect + modernc.org/libc v1.49.3 // indirect modernc.org/mathutil v1.6.0 // indirect - modernc.org/memory v1.7.2 // indirect + modernc.org/memory v1.8.0 // indirect modernc.org/sqlite v1.28.0 // indirect nhooyr.io/websocket v1.8.10 // indirect ) diff --git a/go.sum b/go.sum index d654db11..63876d19 100644 --- a/go.sum +++ b/go.sum @@ -496,8 +496,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 h1:/RIbNt/Zr7rVhIkQhooTxCxFcdWLGIKnZA4IXNFSrvo= golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= @@ -513,8 +513,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= +golang.org/x/mod v0.16.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-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -527,8 +527,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v 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.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ= golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= @@ -574,15 +574,15 @@ golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepC 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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.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-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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -606,8 +606,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= -golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= +golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= 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= @@ -680,16 +680,26 @@ lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo= lukechampine.com/uint128 v1.3.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.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk= -modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY= +modernc.org/cc/v4 v4.20.0 h1:45Or8mQfbUqJOG9WaxvlFYOAQO0lQ5RvqBcFCXngjxk= +modernc.org/cc/v4 v4.20.0/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v3 v3.17.0 h1:o3OmOqx4/OFnl4Vm3G8Bgmqxnvxnh0nbxeT5p/dWChA= +modernc.org/ccgo/v3 v3.17.0/go.mod h1:Sg3fwVpmLvCUTaqEUjiBDAvshIaKDB0RXaf+zgqFu8I= +modernc.org/ccgo/v4 v4.16.0 h1:ofwORa6vx2FMm0916/CkZjpFPSR70VwTjUCe2Eg5BnA= +modernc.org/ccgo/v4 v4.16.0/go.mod h1:dkNyWIjFrVIZ68DTo36vHK+6/ShBn4ysU61So6PIqCI= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= +modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= +modernc.org/libc v1.49.3 h1:j2MRCRdwJI2ls/sGbeSk0t2bypOG/uvPZUsGQFDulqg= +modernc.org/libc v1.49.3/go.mod h1:yMZuGkn7pXbKfoT/M35gFJOAEdSKdxL0q64sF7KqCDo= 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/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= +modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ= modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0= modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= From 2858ab402a347f951b8b3a438c62b49aca8b1d87 Mon Sep 17 00:00:00 2001 From: Brandon Schneider Date: Wed, 10 Apr 2024 19:03:05 -0500 Subject: [PATCH 019/145] Add new shasum for flake.nix --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 4eae1d52..79b75a9a 100644 --- a/flake.nix +++ b/flake.nix @@ -31,7 +31,7 @@ # When updating go.mod or go.sum, a new sha will need to be calculated, # update this if you have a mismatch after doing a change to thos files. - vendorHash = "sha256-Yb5WaN0abPLZ4mPnuJGZoj6EMfoZjaZZ0f344KWva3o="; + vendorHash = "sha256-z3IXmr8SK8oUJTnw7gTok6zpLf15kE89q6zYKbMA5AI="; subPackages = ["cmd/headscale"]; From dd693c444c4ad24a8e76f42a0912e21ef3ac7f87 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 14 Apr 2024 06:46:44 +0000 Subject: [PATCH 020/145] flake.lock: Update (#1848) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 89d88b38..13f9133e 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1710534455, - "narHash": "sha256-huQT4Xs0y4EeFKn2BTBVYgEwJSv8SDlm82uWgMnCMmI=", + "lastModified": 1712883908, + "narHash": "sha256-icE1IJE9fHcbDfJ0+qWoDdcBXUoZCcIJxME4lMHwvSM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9af9c1c87ed3e3ed271934cb896e0cdd33dae212", + "rev": "a0c9e3aee1000ac2bfb0e5b98c94c946a5d180a9", "type": "github" }, "original": { From 58c94d2bd33d0fd03336b8f2b56f63953c44abaa Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 23 Feb 2024 10:59:24 +0100 Subject: [PATCH 021/145] Rework map session This commit restructures the map session in to a struct holding the state of what is needed during its lifetime. For streaming sessions, the event loop is structured a bit differently not hammering the clients with updates but rather batching them over a short, configurable time which should significantly improve cpu usage, and potentially flakyness. The use of Patch updates has been dialed back a little as it does not look like its a 100% ready for prime time. Nodes are now updated with full changes, except for a few things like online status. Signed-off-by: Kristoffer Dalby --- .github/workflows/test-integration.yaml | 3 +- go.mod | 2 + go.sum | 4 + hscontrol/app.go | 61 +- hscontrol/auth.go | 33 +- hscontrol/db/node.go | 101 +- hscontrol/db/node_test.go | 18 +- hscontrol/db/preauth_keys.go | 4 - hscontrol/db/preauth_keys_test.go | 4 +- hscontrol/db/routes.go | 276 +++--- hscontrol/db/routes_test.go | 506 +++++------ hscontrol/grpcv1.go | 113 ++- hscontrol/handlers.go | 19 - hscontrol/mapper/mapper.go | 306 +++---- hscontrol/mapper/mapper_test.go | 65 +- hscontrol/mapper/tail.go | 30 +- hscontrol/mapper/tail_test.go | 9 +- hscontrol/noise.go | 134 +++ hscontrol/notifier/notifier.go | 87 +- hscontrol/oidc.go | 7 +- hscontrol/policy/acls.go | 41 +- hscontrol/policy/acls_test.go | 73 +- hscontrol/poll.go | 1112 +++++++++++++---------- hscontrol/poll_noise.go | 96 -- hscontrol/types/common.go | 60 +- hscontrol/types/config.go | 16 + hscontrol/types/node.go | 27 +- integration/auth_oidc_test.go | 4 +- integration/auth_web_flow_test.go | 4 +- integration/general_test.go | 132 ++- integration/route_test.go | 68 +- integration/scenario.go | 2 + integration/tailscale.go | 2 +- integration/tsic/tsic.go | 83 +- integration/utils.go | 17 +- 35 files changed, 1803 insertions(+), 1716 deletions(-) delete mode 100644 hscontrol/poll_noise.go diff --git a/.github/workflows/test-integration.yaml b/.github/workflows/test-integration.yaml index 06a99db4..d93aaca2 100644 --- a/.github/workflows/test-integration.yaml +++ b/.github/workflows/test-integration.yaml @@ -43,7 +43,8 @@ jobs: - TestTaildrop - TestResolveMagicDNS - TestExpireNode - - TestNodeOnlineLastSeenStatus + - TestNodeOnlineStatus + - TestPingAllByIPManyUpDown - TestEnablingRoutes - TestHASubnetRouterFailover - TestEnableDisableAutoApprovedRoute diff --git a/go.mod b/go.mod index 20bd86bd..bf7e61b7 100644 --- a/go.mod +++ b/go.mod @@ -150,6 +150,7 @@ require ( github.com/opencontainers/image-spec v1.1.0-rc6 // indirect github.com/opencontainers/runc v1.1.12 // indirect github.com/pelletier/go-toml/v2 v2.1.1 // indirect + github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect @@ -161,6 +162,7 @@ require ( 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/sasha-s/go-deadlock v0.3.1 // 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 diff --git a/go.sum b/go.sum index 63876d19..703fa08c 100644 --- a/go.sum +++ b/go.sum @@ -336,6 +336,8 @@ github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaR github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ= +github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= 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= @@ -392,6 +394,8 @@ github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6g github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= +github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0= +github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM= 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= diff --git a/hscontrol/app.go b/hscontrol/app.go index a29e53dc..bdb5c1d9 100644 --- a/hscontrol/app.go +++ b/hscontrol/app.go @@ -28,6 +28,7 @@ import ( "github.com/juanfont/headscale/hscontrol/db" "github.com/juanfont/headscale/hscontrol/derp" derpServer "github.com/juanfont/headscale/hscontrol/derp/server" + "github.com/juanfont/headscale/hscontrol/mapper" "github.com/juanfont/headscale/hscontrol/notifier" "github.com/juanfont/headscale/hscontrol/policy" "github.com/juanfont/headscale/hscontrol/types" @@ -38,6 +39,7 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" zl "github.com/rs/zerolog" "github.com/rs/zerolog/log" + "github.com/sasha-s/go-deadlock" "golang.org/x/crypto/acme" "golang.org/x/crypto/acme/autocert" "golang.org/x/oauth2" @@ -77,6 +79,11 @@ const ( registerCacheCleanup = time.Minute * 20 ) +func init() { + deadlock.Opts.DeadlockTimeout = 15 * time.Second + deadlock.Opts.PrintAllCurrentGoroutines = true +} + // Headscale represents the base app of the service. type Headscale struct { cfg *types.Config @@ -89,6 +96,7 @@ type Headscale struct { ACLPolicy *policy.ACLPolicy + mapper *mapper.Mapper nodeNotifier *notifier.Notifier oidcProvider *oidc.Provider @@ -96,8 +104,10 @@ type Headscale struct { registrationCache *cache.Cache - shutdownChan chan struct{} pollNetMapStreamWG sync.WaitGroup + + mapSessions map[types.NodeID]*mapSession + mapSessionMu deadlock.Mutex } var ( @@ -129,6 +139,7 @@ func NewHeadscale(cfg *types.Config) (*Headscale, error) { registrationCache: registrationCache, pollNetMapStreamWG: sync.WaitGroup{}, nodeNotifier: notifier.NewNotifier(), + mapSessions: make(map[types.NodeID]*mapSession), } app.db, err = db.NewHeadscaleDatabase( @@ -199,16 +210,16 @@ func (h *Headscale) redirect(w http.ResponseWriter, req *http.Request) { http.Redirect(w, req, target, http.StatusFound) } -// expireEphemeralNodes deletes ephemeral node records that have not been +// deleteExpireEphemeralNodes deletes ephemeral node records that have not been // seen for longer than h.cfg.EphemeralNodeInactivityTimeout. -func (h *Headscale) expireEphemeralNodes(milliSeconds int64) { +func (h *Headscale) deleteExpireEphemeralNodes(milliSeconds int64) { ticker := time.NewTicker(time.Duration(milliSeconds) * time.Millisecond) - var update types.StateUpdate - var changed bool for range ticker.C { + var removed []types.NodeID + var changed []types.NodeID if err := h.db.DB.Transaction(func(tx *gorm.DB) error { - update, changed = db.ExpireEphemeralNodes(tx, h.cfg.EphemeralNodeInactivityTimeout) + removed, changed = db.DeleteExpiredEphemeralNodes(tx, h.cfg.EphemeralNodeInactivityTimeout) return nil }); err != nil { @@ -216,9 +227,20 @@ func (h *Headscale) expireEphemeralNodes(milliSeconds int64) { continue } - if changed && update.Valid() { + if removed != nil { ctx := types.NotifyCtx(context.Background(), "expire-ephemeral", "na") - h.nodeNotifier.NotifyAll(ctx, update) + h.nodeNotifier.NotifyAll(ctx, types.StateUpdate{ + Type: types.StatePeerRemoved, + Removed: removed, + }) + } + + if changed != nil { + ctx := types.NotifyCtx(context.Background(), "expire-ephemeral", "na") + h.nodeNotifier.NotifyAll(ctx, types.StateUpdate{ + Type: types.StatePeerChanged, + ChangeNodes: changed, + }) } } } @@ -243,8 +265,9 @@ func (h *Headscale) expireExpiredMachines(intervalMs int64) { continue } - log.Trace().Str("nodes", update.ChangeNodes.String()).Msgf("expiring nodes") - if changed && update.Valid() { + if changed { + log.Trace().Interface("nodes", update.ChangePatches).Msgf("expiring nodes") + ctx := types.NotifyCtx(context.Background(), "expire-expired", "na") h.nodeNotifier.NotifyAll(ctx, update) } @@ -272,14 +295,11 @@ func (h *Headscale) scheduledDERPMapUpdateWorker(cancelChan <-chan struct{}) { h.DERPMap.Regions[region.RegionID] = ®ion } - stateUpdate := types.StateUpdate{ + ctx := types.NotifyCtx(context.Background(), "derpmap-update", "na") + h.nodeNotifier.NotifyAll(ctx, types.StateUpdate{ Type: types.StateDERPUpdated, DERPMap: h.DERPMap, - } - if stateUpdate.Valid() { - ctx := types.NotifyCtx(context.Background(), "derpmap-update", "na") - h.nodeNotifier.NotifyAll(ctx, stateUpdate) - } + }) } } } @@ -502,6 +522,7 @@ func (h *Headscale) Serve() error { // Fetch an initial DERP Map before we start serving h.DERPMap = derp.GetDERPMap(h.cfg.DERP) + h.mapper = mapper.NewMapper(h.db, h.cfg, h.DERPMap, h.nodeNotifier.ConnectedMap()) if h.cfg.DERP.ServerEnabled { // When embedded DERP is enabled we always need a STUN server @@ -533,7 +554,7 @@ func (h *Headscale) Serve() error { // TODO(kradalby): These should have cancel channels and be cleaned // up on shutdown. - go h.expireEphemeralNodes(updateInterval) + go h.deleteExpireEphemeralNodes(updateInterval) go h.expireExpiredMachines(updateInterval) if zl.GlobalLevel() == zl.TraceLevel { @@ -686,6 +707,9 @@ func (h *Headscale) Serve() error { // no good way to handle streaming timeouts, therefore we need to // keep this at unlimited and be careful to clean up connections // https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/#aboutstreaming + // TODO(kradalby): this timeout can now be set per handler with http.ResponseController: + // https://www.alexedwards.net/blog/how-to-use-the-http-responsecontroller-type + // replace this so only the longpoller has no timeout. WriteTimeout: 0, } @@ -742,7 +766,6 @@ func (h *Headscale) Serve() error { } // Handle common process-killing signals so we can gracefully shut down: - h.shutdownChan = make(chan struct{}) sigc := make(chan os.Signal, 1) signal.Notify(sigc, syscall.SIGHUP, @@ -785,8 +808,6 @@ func (h *Headscale) Serve() error { Str("signal", sig.String()). Msg("Received signal to stop, shutting down gracefully") - close(h.shutdownChan) - h.pollNetMapStreamWG.Wait() // Gracefully shut down servers diff --git a/hscontrol/auth.go b/hscontrol/auth.go index b199fa55..8271038c 100644 --- a/hscontrol/auth.go +++ b/hscontrol/auth.go @@ -352,13 +352,8 @@ func (h *Headscale) handleAuthKey( } } - mkey := node.MachineKey - update := types.StateUpdateExpire(node.ID, registerRequest.Expiry) - - if update.Valid() { - ctx := types.NotifyCtx(context.Background(), "handle-authkey", "na") - h.nodeNotifier.NotifyWithIgnore(ctx, update, mkey.String()) - } + ctx := types.NotifyCtx(context.Background(), "handle-authkey", "na") + h.nodeNotifier.NotifyWithIgnore(ctx, types.StateUpdateExpire(node.ID, registerRequest.Expiry), node.ID) } else { now := time.Now().UTC() @@ -538,11 +533,8 @@ func (h *Headscale) handleNodeLogOut( return } - stateUpdate := types.StateUpdateExpire(node.ID, now) - if stateUpdate.Valid() { - ctx := types.NotifyCtx(context.Background(), "logout-expiry", "na") - h.nodeNotifier.NotifyWithIgnore(ctx, stateUpdate, node.MachineKey.String()) - } + ctx := types.NotifyCtx(context.Background(), "logout-expiry", "na") + h.nodeNotifier.NotifyWithIgnore(ctx, types.StateUpdateExpire(node.ID, now), node.ID) resp.AuthURL = "" resp.MachineAuthorized = false @@ -572,7 +564,7 @@ func (h *Headscale) handleNodeLogOut( } if node.IsEphemeral() { - err = h.db.DeleteNode(&node, h.nodeNotifier.ConnectedMap()) + changedNodes, err := h.db.DeleteNode(&node, h.nodeNotifier.ConnectedMap()) if err != nil { log.Error(). Err(err). @@ -580,13 +572,16 @@ func (h *Headscale) handleNodeLogOut( Msg("Cannot delete ephemeral node from the database") } - stateUpdate := types.StateUpdate{ + ctx := types.NotifyCtx(context.Background(), "logout-ephemeral", "na") + h.nodeNotifier.NotifyAll(ctx, types.StateUpdate{ Type: types.StatePeerRemoved, - Removed: []tailcfg.NodeID{tailcfg.NodeID(node.ID)}, - } - if stateUpdate.Valid() { - ctx := types.NotifyCtx(context.Background(), "logout-ephemeral", "na") - h.nodeNotifier.NotifyAll(ctx, stateUpdate) + Removed: []types.NodeID{node.ID}, + }) + if changedNodes != nil { + h.nodeNotifier.NotifyAll(ctx, types.StateUpdate{ + Type: types.StatePeerChanged, + ChangeNodes: changedNodes, + }) } return diff --git a/hscontrol/db/node.go b/hscontrol/db/node.go index d02c2d39..61c952a0 100644 --- a/hscontrol/db/node.go +++ b/hscontrol/db/node.go @@ -34,27 +34,22 @@ var ( ) ) -func (hsdb *HSDatabase) ListPeers(node *types.Node) (types.Nodes, error) { +func (hsdb *HSDatabase) ListPeers(nodeID types.NodeID) (types.Nodes, error) { return Read(hsdb.DB, func(rx *gorm.DB) (types.Nodes, error) { - return ListPeers(rx, node) + return ListPeers(rx, nodeID) }) } // ListPeers returns all peers of node, regardless of any Policy or if the node is expired. -func ListPeers(tx *gorm.DB, node *types.Node) (types.Nodes, error) { - log.Trace(). - Caller(). - Str("node", node.Hostname). - Msg("Finding direct peers") - +func ListPeers(tx *gorm.DB, nodeID types.NodeID) (types.Nodes, error) { nodes := types.Nodes{} if err := tx. Preload("AuthKey"). Preload("AuthKey.User"). Preload("User"). Preload("Routes"). - Where("node_key <> ?", - node.NodeKey.String()).Find(&nodes).Error; err != nil { + Where("id <> ?", + nodeID).Find(&nodes).Error; err != nil { return types.Nodes{}, err } @@ -119,14 +114,14 @@ func getNode(tx *gorm.DB, user string, name string) (*types.Node, error) { return nil, ErrNodeNotFound } -func (hsdb *HSDatabase) GetNodeByID(id uint64) (*types.Node, error) { +func (hsdb *HSDatabase) GetNodeByID(id types.NodeID) (*types.Node, error) { return Read(hsdb.DB, func(rx *gorm.DB) (*types.Node, error) { return GetNodeByID(rx, id) }) } // GetNodeByID finds a Node by ID and returns the Node struct. -func GetNodeByID(tx *gorm.DB, id uint64) (*types.Node, error) { +func GetNodeByID(tx *gorm.DB, id types.NodeID) (*types.Node, error) { mach := types.Node{} if result := tx. Preload("AuthKey"). @@ -197,7 +192,7 @@ func GetNodeByAnyKey( } func (hsdb *HSDatabase) SetTags( - nodeID uint64, + nodeID types.NodeID, tags []string, ) error { return hsdb.Write(func(tx *gorm.DB) error { @@ -208,7 +203,7 @@ func (hsdb *HSDatabase) SetTags( // SetTags takes a Node struct pointer and update the forced tags. func SetTags( tx *gorm.DB, - nodeID uint64, + nodeID types.NodeID, tags []string, ) error { if len(tags) == 0 { @@ -256,7 +251,7 @@ func RenameNode(tx *gorm.DB, return nil } -func (hsdb *HSDatabase) NodeSetExpiry(nodeID uint64, expiry time.Time) error { +func (hsdb *HSDatabase) NodeSetExpiry(nodeID types.NodeID, expiry time.Time) error { return hsdb.Write(func(tx *gorm.DB) error { return NodeSetExpiry(tx, nodeID, expiry) }) @@ -264,13 +259,13 @@ func (hsdb *HSDatabase) NodeSetExpiry(nodeID uint64, expiry time.Time) error { // NodeSetExpiry takes a Node struct and a new expiry time. func NodeSetExpiry(tx *gorm.DB, - nodeID uint64, expiry time.Time, + nodeID types.NodeID, expiry time.Time, ) error { return tx.Model(&types.Node{}).Where("id = ?", nodeID).Update("expiry", expiry).Error } -func (hsdb *HSDatabase) DeleteNode(node *types.Node, isConnected map[key.MachinePublic]bool) error { - return hsdb.Write(func(tx *gorm.DB) error { +func (hsdb *HSDatabase) DeleteNode(node *types.Node, isConnected types.NodeConnectedMap) ([]types.NodeID, error) { + return Write(hsdb.DB, func(tx *gorm.DB) ([]types.NodeID, error) { return DeleteNode(tx, node, isConnected) }) } @@ -279,24 +274,24 @@ func (hsdb *HSDatabase) DeleteNode(node *types.Node, isConnected map[key.Machine // Caller is responsible for notifying all of change. func DeleteNode(tx *gorm.DB, node *types.Node, - isConnected map[key.MachinePublic]bool, -) error { - err := deleteNodeRoutes(tx, node, map[key.MachinePublic]bool{}) + isConnected types.NodeConnectedMap, +) ([]types.NodeID, error) { + changed, err := deleteNodeRoutes(tx, node, isConnected) if err != nil { - return err + return changed, err } // Unscoped causes the node to be fully removed from the database. if err := tx.Unscoped().Delete(&node).Error; err != nil { - return err + return changed, err } - return nil + return changed, nil } -// UpdateLastSeen sets a node's last seen field indicating that we +// SetLastSeen sets a node's last seen field indicating that we // have recently communicating with this node. -func UpdateLastSeen(tx *gorm.DB, nodeID uint64, lastSeen time.Time) error { +func SetLastSeen(tx *gorm.DB, nodeID types.NodeID, lastSeen time.Time) error { return tx.Model(&types.Node{}).Where("id = ?", nodeID).Update("last_seen", lastSeen).Error } @@ -606,7 +601,7 @@ func enableRoutes(tx *gorm.DB, return &types.StateUpdate{ Type: types.StatePeerChanged, - ChangeNodes: types.Nodes{node}, + ChangeNodes: []types.NodeID{node.ID}, Message: "created in db.enableRoutes", }, nil } @@ -681,17 +676,18 @@ func GenerateGivenName( return givenName, nil } -func ExpireEphemeralNodes(tx *gorm.DB, +func DeleteExpiredEphemeralNodes(tx *gorm.DB, inactivityThreshhold time.Duration, -) (types.StateUpdate, bool) { +) ([]types.NodeID, []types.NodeID) { users, err := ListUsers(tx) if err != nil { log.Error().Err(err).Msg("Error listing users") - return types.StateUpdate{}, false + return nil, nil } - expired := make([]tailcfg.NodeID, 0) + var expired []types.NodeID + var changedNodes []types.NodeID for _, user := range users { nodes, err := ListNodesByUser(tx, user.Name) if err != nil { @@ -700,40 +696,36 @@ func ExpireEphemeralNodes(tx *gorm.DB, Str("user", user.Name). Msg("Error listing nodes in user") - return types.StateUpdate{}, false + return nil, nil } for idx, node := range nodes { if node.IsEphemeral() && node.LastSeen != nil && time.Now(). After(node.LastSeen.Add(inactivityThreshhold)) { - expired = append(expired, tailcfg.NodeID(node.ID)) + expired = append(expired, node.ID) log.Info(). Str("node", node.Hostname). Msg("Ephemeral client removed from database") // empty isConnected map as ephemeral nodes are not routes - err = DeleteNode(tx, nodes[idx], map[key.MachinePublic]bool{}) + changed, err := DeleteNode(tx, nodes[idx], nil) if err != nil { log.Error(). Err(err). Str("node", node.Hostname). Msg("🤮 Cannot delete ephemeral node from the database") } + + changedNodes = append(changedNodes, changed...) } } // TODO(kradalby): needs to be moved out of transaction } - if len(expired) > 0 { - return types.StateUpdate{ - Type: types.StatePeerRemoved, - Removed: expired, - }, true - } - return types.StateUpdate{}, false + return expired, changedNodes } func ExpireExpiredNodes(tx *gorm.DB, @@ -754,35 +746,12 @@ func ExpireExpiredNodes(tx *gorm.DB, return time.Unix(0, 0), types.StateUpdate{}, false } - 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) { + for _, node := range nodes { + if node.IsExpired() && node.Expiry.After(lastCheck) { expired = append(expired, &tailcfg.PeerChange{ NodeID: tailcfg.NodeID(node.ID), KeyExpiry: node.Expiry, }) - - now := time.Now() - // 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 := tx.Model(&nodes[index]).Updates(types.Node{ - Expiry: &now, - }).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") - } } } diff --git a/hscontrol/db/node_test.go b/hscontrol/db/node_test.go index 5e8eb294..0dbe7688 100644 --- a/hscontrol/db/node_test.go +++ b/hscontrol/db/node_test.go @@ -120,7 +120,7 @@ func (s *Suite) TestHardDeleteNode(c *check.C) { } db.DB.Save(&node) - err = db.DeleteNode(&node, map[key.MachinePublic]bool{}) + _, err = db.DeleteNode(&node, types.NodeConnectedMap{}) c.Assert(err, check.IsNil) _, err = db.getNode(user.Name, "testnode3") @@ -142,7 +142,7 @@ func (s *Suite) TestListPeers(c *check.C) { machineKey := key.NewMachine() node := types.Node{ - ID: uint64(index), + ID: types.NodeID(index), MachineKey: machineKey.Public(), NodeKey: nodeKey.Public(), Hostname: "testnode" + strconv.Itoa(index), @@ -156,7 +156,7 @@ func (s *Suite) TestListPeers(c *check.C) { node0ByID, err := db.GetNodeByID(0) c.Assert(err, check.IsNil) - peersOfNode0, err := db.ListPeers(node0ByID) + peersOfNode0, err := db.ListPeers(node0ByID.ID) c.Assert(err, check.IsNil) c.Assert(len(peersOfNode0), check.Equals, 9) @@ -189,7 +189,7 @@ func (s *Suite) TestGetACLFilteredPeers(c *check.C) { machineKey := key.NewMachine() node := types.Node{ - ID: uint64(index), + ID: types.NodeID(index), MachineKey: machineKey.Public(), NodeKey: nodeKey.Public(), IPAddresses: types.NodeAddresses{ @@ -232,16 +232,16 @@ func (s *Suite) TestGetACLFilteredPeers(c *check.C) { c.Logf("Node(%v), user: %v", testNode.Hostname, testNode.User) c.Assert(err, check.IsNil) - adminPeers, err := db.ListPeers(adminNode) + adminPeers, err := db.ListPeers(adminNode.ID) c.Assert(err, check.IsNil) - testPeers, err := db.ListPeers(testNode) + testPeers, err := db.ListPeers(testNode.ID) c.Assert(err, check.IsNil) - adminRules, _, err := policy.GenerateFilterAndSSHRules(aclPolicy, adminNode, adminPeers) + adminRules, _, err := policy.GenerateFilterAndSSHRulesForTests(aclPolicy, adminNode, adminPeers) c.Assert(err, check.IsNil) - testRules, _, err := policy.GenerateFilterAndSSHRules(aclPolicy, testNode, testPeers) + testRules, _, err := policy.GenerateFilterAndSSHRulesForTests(aclPolicy, testNode, testPeers) c.Assert(err, check.IsNil) peersOfAdminNode := policy.FilterNodesByACL(adminNode, adminPeers, adminRules) @@ -586,7 +586,7 @@ func (s *Suite) TestAutoApproveRoutes(c *check.C) { c.Assert(err, check.IsNil) // TODO(kradalby): Check state update - _, err = db.EnableAutoApprovedRoutes(pol, node0ByID) + err = db.EnableAutoApprovedRoutes(pol, node0ByID) c.Assert(err, check.IsNil) enabledRoutes, err := db.GetEnabledRoutes(node0ByID) diff --git a/hscontrol/db/preauth_keys.go b/hscontrol/db/preauth_keys.go index d1d94bbe..5d38de29 100644 --- a/hscontrol/db/preauth_keys.go +++ b/hscontrol/db/preauth_keys.go @@ -92,10 +92,6 @@ func CreatePreAuthKey( } } - if err != nil { - return nil, err - } - return &key, nil } diff --git a/hscontrol/db/preauth_keys_test.go b/hscontrol/db/preauth_keys_test.go index 53cf37c4..2cd59c40 100644 --- a/hscontrol/db/preauth_keys_test.go +++ b/hscontrol/db/preauth_keys_test.go @@ -148,7 +148,7 @@ func (*Suite) TestEphemeralKeyReusable(c *check.C) { c.Assert(err, check.IsNil) db.DB.Transaction(func(tx *gorm.DB) error { - ExpireEphemeralNodes(tx, time.Second*20) + DeleteExpiredEphemeralNodes(tx, time.Second*20) return nil }) @@ -182,7 +182,7 @@ func (*Suite) TestEphemeralKeyNotReusable(c *check.C) { c.Assert(err, check.IsNil) db.DB.Transaction(func(tx *gorm.DB) error { - ExpireEphemeralNodes(tx, time.Second*20) + DeleteExpiredEphemeralNodes(tx, time.Second*20) return nil }) diff --git a/hscontrol/db/routes.go b/hscontrol/db/routes.go index 1ee144a7..9498bc65 100644 --- a/hscontrol/db/routes.go +++ b/hscontrol/db/routes.go @@ -8,7 +8,6 @@ import ( "github.com/juanfont/headscale/hscontrol/types" "github.com/rs/zerolog/log" "gorm.io/gorm" - "tailscale.com/types/key" ) var ErrRouteIsNotAvailable = errors.New("route is not available") @@ -124,8 +123,8 @@ func EnableRoute(tx *gorm.DB, id uint64) (*types.StateUpdate, error) { func DisableRoute(tx *gorm.DB, id uint64, - isConnected map[key.MachinePublic]bool, -) (*types.StateUpdate, error) { + isConnected types.NodeConnectedMap, +) ([]types.NodeID, error) { route, err := GetRoute(tx, id) if err != nil { return nil, err @@ -137,16 +136,15 @@ func DisableRoute(tx *gorm.DB, // 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 - var update *types.StateUpdate + var update []types.NodeID if !route.IsExitRoute() { - update, err = failoverRouteReturnUpdate(tx, isConnected, route) + route.Enabled = false + err = tx.Save(route).Error if err != nil { return nil, err } - route.Enabled = false - route.IsPrimary = false - err = tx.Save(route).Error + update, err = failoverRouteTx(tx, isConnected, route) if err != nil { return nil, err } @@ -160,6 +158,7 @@ func DisableRoute(tx *gorm.DB, if routes[i].IsExitRoute() { routes[i].Enabled = false routes[i].IsPrimary = false + err = tx.Save(&routes[i]).Error if err != nil { return nil, err @@ -168,26 +167,11 @@ func DisableRoute(tx *gorm.DB, } } - if routes == nil { - routes, err = GetNodeRoutes(tx, &node) - if err != nil { - return nil, err - } - } - - node.Routes = routes - // If update is empty, it means that one was not created // by failover (as a failover was not necessary), create // one and return to the caller. if update == nil { - update = &types.StateUpdate{ - Type: types.StatePeerChanged, - ChangeNodes: types.Nodes{ - &node, - }, - Message: "called from db.DisableRoute", - } + update = []types.NodeID{node.ID} } return update, nil @@ -195,9 +179,9 @@ func DisableRoute(tx *gorm.DB, func (hsdb *HSDatabase) DeleteRoute( id uint64, - isConnected map[key.MachinePublic]bool, -) (*types.StateUpdate, error) { - return Write(hsdb.DB, func(tx *gorm.DB) (*types.StateUpdate, error) { + isConnected types.NodeConnectedMap, +) ([]types.NodeID, error) { + return Write(hsdb.DB, func(tx *gorm.DB) ([]types.NodeID, error) { return DeleteRoute(tx, id, isConnected) }) } @@ -205,8 +189,8 @@ func (hsdb *HSDatabase) DeleteRoute( func DeleteRoute( tx *gorm.DB, id uint64, - isConnected map[key.MachinePublic]bool, -) (*types.StateUpdate, error) { + isConnected types.NodeConnectedMap, +) ([]types.NodeID, error) { route, err := GetRoute(tx, id) if err != nil { return nil, err @@ -218,9 +202,9 @@ func DeleteRoute( // 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 - var update *types.StateUpdate + var update []types.NodeID if !route.IsExitRoute() { - update, err = failoverRouteReturnUpdate(tx, isConnected, route) + update, err = failoverRouteTx(tx, isConnected, route) if err != nil { return nil, nil } @@ -229,7 +213,7 @@ func DeleteRoute( return nil, err } } else { - routes, err := GetNodeRoutes(tx, &node) + routes, err = GetNodeRoutes(tx, &node) if err != nil { return nil, err } @@ -259,35 +243,37 @@ func DeleteRoute( node.Routes = routes if update == nil { - update = &types.StateUpdate{ - Type: types.StatePeerChanged, - ChangeNodes: types.Nodes{ - &node, - }, - Message: "called from db.DeleteRoute", - } + update = []types.NodeID{node.ID} } return update, nil } -func deleteNodeRoutes(tx *gorm.DB, node *types.Node, isConnected map[key.MachinePublic]bool) error { +func deleteNodeRoutes(tx *gorm.DB, node *types.Node, isConnected types.NodeConnectedMap) ([]types.NodeID, error) { routes, err := GetNodeRoutes(tx, node) if err != nil { - return err + return nil, err } + var changed []types.NodeID for i := range routes { if err := tx.Unscoped().Delete(&routes[i]).Error; err != nil { - return err + return nil, err } // TODO(kradalby): This is a bit too aggressive, we could probably // figure out which routes needs to be failed over rather than all. - failoverRouteReturnUpdate(tx, isConnected, &routes[i]) + chn, err := failoverRouteTx(tx, isConnected, &routes[i]) + if err != nil { + return changed, err + } + + if chn != nil { + changed = append(changed, chn...) + } } - return nil + return changed, nil } // isUniquePrefix returns if there is another node providing the same route already. @@ -400,7 +386,7 @@ func SaveNodeRoutes(tx *gorm.DB, node *types.Node) (bool, error) { for prefix, exists := range advertisedRoutes { if !exists { route := types.Route{ - NodeID: node.ID, + NodeID: node.ID.Uint64(), Prefix: types.IPPrefix(prefix), Advertised: true, Enabled: false, @@ -415,19 +401,23 @@ func SaveNodeRoutes(tx *gorm.DB, node *types.Node) (bool, error) { return sendUpdate, nil } -// EnsureFailoverRouteIsAvailable takes a node and checks if the node's route +// FailoverRouteIfAvailable takes a node and checks if the node's route // currently have a functioning host that exposes the network. -func EnsureFailoverRouteIsAvailable( +// If it does not, it is failed over to another suitable route if there +// is one. +func FailoverRouteIfAvailable( tx *gorm.DB, - isConnected map[key.MachinePublic]bool, + isConnected types.NodeConnectedMap, node *types.Node, ) (*types.StateUpdate, error) { + log.Debug().Caller().Uint64("node.id", node.ID.Uint64()).Msgf("ROUTE DEBUG ENTERED FAILOVER") nodeRoutes, err := GetNodeRoutes(tx, node) if err != nil { + log.Debug().Caller().Uint64("node.id", node.ID.Uint64()).Interface("nodeRoutes", nodeRoutes).Msgf("ROUTE DEBUG NO ROUTES") return nil, nil } - var changedNodes types.Nodes + var changedNodes []types.NodeID for _, nodeRoute := range nodeRoutes { routes, err := getRoutesByPrefix(tx, netip.Prefix(nodeRoute.Prefix)) if err != nil { @@ -438,71 +428,39 @@ func EnsureFailoverRouteIsAvailable( if route.IsPrimary { // if we have a primary route, and the node is connected // nothing needs to be done. - if isConnected[route.Node.MachineKey] { - continue + log.Debug().Caller().Uint64("node.id", node.ID.Uint64()).Uint64("route.node.id", route.Node.ID.Uint64()).Msgf("ROUTE DEBUG CHECKING IF ONLINE") + if isConnected[route.Node.ID] { + log.Debug().Caller().Uint64("node.id", node.ID.Uint64()).Uint64("route.node.id", route.Node.ID.Uint64()).Msgf("ROUTE DEBUG IS ONLINE") + return nil, nil } + log.Debug().Caller().Uint64("node.id", node.ID.Uint64()).Uint64("route.node.id", route.Node.ID.Uint64()).Msgf("ROUTE DEBUG NOT ONLINE, FAILING OVER") // if not, we need to failover the route - update, err := failoverRouteReturnUpdate(tx, isConnected, &route) + changedIDs, err := failoverRouteTx(tx, isConnected, &route) if err != nil { return nil, err } - if update != nil { - changedNodes = append(changedNodes, update.ChangeNodes...) + if changedIDs != nil { + changedNodes = append(changedNodes, changedIDs...) } } } } + log.Debug().Caller().Uint64("node.id", node.ID.Uint64()).Interface("changedNodes", changedNodes).Msgf("ROUTE DEBUG") if len(changedNodes) != 0 { return &types.StateUpdate{ Type: types.StatePeerChanged, ChangeNodes: changedNodes, - Message: "called from db.EnsureFailoverRouteIsAvailable", + Message: "called from db.FailoverRouteIfAvailable", }, nil } return nil, nil } -func failoverRouteReturnUpdate( - tx *gorm.DB, - isConnected map[key.MachinePublic]bool, - r *types.Route, -) (*types.StateUpdate, error) { - changedKeys, err := failoverRoute(tx, isConnected, r) - if err != nil { - return nil, err - } - - log.Trace(). - Interface("isConnected", isConnected). - Interface("changedKeys", changedKeys). - Msg("building route failover") - - if len(changedKeys) == 0 { - return nil, nil - } - - var nodes types.Nodes - for _, key := range changedKeys { - node, err := GetNodeByMachineKey(tx, key) - if err != nil { - return nil, err - } - - nodes = append(nodes, node) - } - - return &types.StateUpdate{ - Type: types.StatePeerChanged, - ChangeNodes: nodes, - Message: "called from db.failoverRouteReturnUpdate", - }, nil -} - -// failoverRoute takes a route that is no longer available, +// failoverRouteTx takes a route that is no longer available, // this can be either from: // - being disabled // - being deleted @@ -510,11 +468,11 @@ func failoverRouteReturnUpdate( // // and tries to find a new route to take over its place. // If the given route was not primary, it returns early. -func failoverRoute( +func failoverRouteTx( tx *gorm.DB, - isConnected map[key.MachinePublic]bool, + isConnected types.NodeConnectedMap, r *types.Route, -) ([]key.MachinePublic, error) { +) ([]types.NodeID, error) { if r == nil { return nil, nil } @@ -535,11 +493,64 @@ func failoverRoute( return nil, err } + fo := failoverRoute(isConnected, r, routes) + if fo == nil { + return nil, nil + } + + err = tx.Save(fo.old).Error + if err != nil { + log.Error().Err(err).Msg("disabling old primary route") + + return nil, err + } + + err = tx.Save(fo.new).Error + if err != nil { + log.Error().Err(err).Msg("saving new primary route") + + return nil, err + } + + log.Trace(). + Str("hostname", fo.new.Node.Hostname). + Msgf("set primary to new route, was: id(%d), host(%s), now: id(%d), host(%s)", fo.old.ID, fo.old.Node.Hostname, fo.new.ID, fo.new.Node.Hostname) + + // Return a list of the machinekeys of the changed nodes. + return []types.NodeID{fo.old.Node.ID, fo.new.Node.ID}, nil +} + +type failover struct { + old *types.Route + new *types.Route +} + +func failoverRoute( + isConnected types.NodeConnectedMap, + routeToReplace *types.Route, + altRoutes types.Routes, + +) *failover { + if routeToReplace == nil { + return nil + } + + // This route is not a primary route, and it is not + // being served to nodes. + if !routeToReplace.IsPrimary { + return nil + } + + // We do not have to failover exit nodes + if routeToReplace.IsExitRoute() { + return nil + } + var newPrimary *types.Route // Find a new suitable route - for idx, route := range routes { - if r.ID == route.ID { + for idx, route := range altRoutes { + if routeToReplace.ID == route.ID { continue } @@ -547,8 +558,8 @@ func failoverRoute( continue } - if isConnected[route.Node.MachineKey] { - newPrimary = &routes[idx] + if isConnected != nil && isConnected[route.Node.ID] { + newPrimary = &altRoutes[idx] break } } @@ -559,48 +570,23 @@ func failoverRoute( // 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 = tx.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 + routeToReplace.IsPrimary = false newPrimary.IsPrimary = true - err = tx.Save(&newPrimary).Error - if err != nil { - log.Error().Err(err).Msg("error enabling new primary route") - return nil, err + return &failover{ + old: routeToReplace, + new: newPrimary, } - - 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 } func (hsdb *HSDatabase) EnableAutoApprovedRoutes( aclPolicy *policy.ACLPolicy, node *types.Node, -) (*types.StateUpdate, error) { - return Write(hsdb.DB, func(tx *gorm.DB) (*types.StateUpdate, error) { +) error { + return hsdb.Write(func(tx *gorm.DB) error { return EnableAutoApprovedRoutes(tx, aclPolicy, node) }) } @@ -610,9 +596,9 @@ func EnableAutoApprovedRoutes( tx *gorm.DB, aclPolicy *policy.ACLPolicy, node *types.Node, -) (*types.StateUpdate, error) { +) error { if len(node.IPAddresses) == 0 { - return nil, nil // This node has no IPAddresses, so can't possibly match any autoApprovers ACLs + return nil // This node has no IPAddresses, so can't possibly match any autoApprovers ACLs } routes, err := GetNodeAdvertisedRoutes(tx, node) @@ -623,7 +609,7 @@ func EnableAutoApprovedRoutes( Str("node", node.Hostname). Msg("Could not get advertised routes for node") - return nil, err + return err } log.Trace().Interface("routes", routes).Msg("routes for autoapproving") @@ -641,10 +627,10 @@ func EnableAutoApprovedRoutes( if err != nil { log.Err(err). Str("advertisedRoute", advertisedRoute.String()). - Uint64("nodeId", node.ID). + Uint64("nodeId", node.ID.Uint64()). Msg("Failed to resolve autoApprovers for advertised route") - return nil, err + return err } log.Trace(). @@ -665,7 +651,7 @@ func EnableAutoApprovedRoutes( Str("alias", approvedAlias). Msg("Failed to expand alias when processing autoApprovers policy") - return nil, err + return err } // approvedIPs should contain all of node's IPs if it matches the rule, so check for first @@ -676,25 +662,17 @@ func EnableAutoApprovedRoutes( } } - update := &types.StateUpdate{ - Type: types.StatePeerChanged, - ChangeNodes: types.Nodes{}, - Message: "created in db.EnableAutoApprovedRoutes", - } - for _, approvedRoute := range approvedRoutes { - perHostUpdate, err := EnableRoute(tx, uint64(approvedRoute.ID)) + _, err := EnableRoute(tx, uint64(approvedRoute.ID)) if err != nil { log.Err(err). Str("approvedRoute", approvedRoute.String()). - Uint64("nodeId", node.ID). + Uint64("nodeId", node.ID.Uint64()). Msg("Failed to enable approved route") - return nil, err + return err } - - update.ChangeNodes = append(update.ChangeNodes, perHostUpdate.ChangeNodes...) } - return update, nil + return nil } diff --git a/hscontrol/db/routes_test.go b/hscontrol/db/routes_test.go index f3357e2a..390cf700 100644 --- a/hscontrol/db/routes_test.go +++ b/hscontrol/db/routes_test.go @@ -13,7 +13,6 @@ import ( "gopkg.in/check.v1" "gorm.io/gorm" "tailscale.com/tailcfg" - "tailscale.com/types/key" ) func (s *Suite) TestGetRoutes(c *check.C) { @@ -262,7 +261,7 @@ func (s *Suite) TestDeleteRoutes(c *check.C) { c.Assert(err, check.IsNil) // TODO(kradalby): check stateupdate - _, err = db.DeleteRoute(uint64(routes[0].ID), map[key.MachinePublic]bool{}) + _, err = db.DeleteRoute(uint64(routes[0].ID), nil) c.Assert(err, check.IsNil) enabledRoutes1, err := db.GetEnabledRoutes(&node1) @@ -272,20 +271,13 @@ func (s *Suite) TestDeleteRoutes(c *check.C) { var ipp = func(s string) types.IPPrefix { return types.IPPrefix(netip.MustParsePrefix(s)) } -func TestFailoverRoute(t *testing.T) { - machineKeys := []key.MachinePublic{ - key.NewMachine().Public(), - key.NewMachine().Public(), - key.NewMachine().Public(), - key.NewMachine().Public(), - } - +func TestFailoverRouteTx(t *testing.T) { tests := []struct { name string failingRoute types.Route routes types.Routes - isConnected map[key.MachinePublic]bool - want []key.MachinePublic + isConnected types.NodeConnectedMap + want []types.NodeID wantErr bool }{ { @@ -301,10 +293,8 @@ func TestFailoverRoute(t *testing.T) { Model: gorm.Model{ ID: 1, }, - Prefix: ipp("10.0.0.0/24"), - Node: types.Node{ - MachineKey: machineKeys[0], - }, + Prefix: ipp("10.0.0.0/24"), + Node: types.Node{}, IsPrimary: false, }, routes: types.Routes{}, @@ -317,10 +307,8 @@ func TestFailoverRoute(t *testing.T) { Model: gorm.Model{ ID: 1, }, - Prefix: ipp("0.0.0.0/0"), - Node: types.Node{ - MachineKey: machineKeys[0], - }, + Prefix: ipp("0.0.0.0/0"), + Node: types.Node{}, IsPrimary: true, }, routes: types.Routes{}, @@ -335,7 +323,7 @@ func TestFailoverRoute(t *testing.T) { }, Prefix: ipp("10.0.0.0/24"), Node: types.Node{ - MachineKey: machineKeys[0], + ID: 1, }, IsPrimary: true, }, @@ -346,7 +334,7 @@ func TestFailoverRoute(t *testing.T) { }, Prefix: ipp("10.0.0.0/24"), Node: types.Node{ - MachineKey: machineKeys[0], + ID: 1, }, IsPrimary: true, }, @@ -362,7 +350,7 @@ func TestFailoverRoute(t *testing.T) { }, Prefix: ipp("10.0.0.0/24"), Node: types.Node{ - MachineKey: machineKeys[0], + ID: 1, }, IsPrimary: true, Enabled: true, @@ -374,7 +362,7 @@ func TestFailoverRoute(t *testing.T) { }, Prefix: ipp("10.0.0.0/24"), Node: types.Node{ - MachineKey: machineKeys[0], + ID: 1, }, IsPrimary: true, Enabled: true, @@ -385,19 +373,19 @@ func TestFailoverRoute(t *testing.T) { }, Prefix: ipp("10.0.0.0/24"), Node: types.Node{ - MachineKey: machineKeys[1], + ID: 2, }, IsPrimary: false, Enabled: true, }, }, - isConnected: map[key.MachinePublic]bool{ - machineKeys[0]: false, - machineKeys[1]: true, + isConnected: types.NodeConnectedMap{ + 1: false, + 2: true, }, - want: []key.MachinePublic{ - machineKeys[0], - machineKeys[1], + want: []types.NodeID{ + 1, + 2, }, wantErr: false, }, @@ -409,7 +397,7 @@ func TestFailoverRoute(t *testing.T) { }, Prefix: ipp("10.0.0.0/24"), Node: types.Node{ - MachineKey: machineKeys[0], + ID: 1, }, IsPrimary: false, Enabled: true, @@ -421,7 +409,7 @@ func TestFailoverRoute(t *testing.T) { }, Prefix: ipp("10.0.0.0/24"), Node: types.Node{ - MachineKey: machineKeys[0], + ID: 1, }, IsPrimary: true, Enabled: true, @@ -432,7 +420,7 @@ func TestFailoverRoute(t *testing.T) { }, Prefix: ipp("10.0.0.0/24"), Node: types.Node{ - MachineKey: machineKeys[1], + ID: 2, }, IsPrimary: false, Enabled: true, @@ -449,7 +437,7 @@ func TestFailoverRoute(t *testing.T) { }, Prefix: ipp("10.0.0.0/24"), Node: types.Node{ - MachineKey: machineKeys[1], + ID: 2, }, IsPrimary: true, Enabled: true, @@ -461,7 +449,7 @@ func TestFailoverRoute(t *testing.T) { }, Prefix: ipp("10.0.0.0/24"), Node: types.Node{ - MachineKey: machineKeys[0], + ID: 1, }, IsPrimary: false, Enabled: true, @@ -472,7 +460,7 @@ func TestFailoverRoute(t *testing.T) { }, Prefix: ipp("10.0.0.0/24"), Node: types.Node{ - MachineKey: machineKeys[1], + ID: 2, }, IsPrimary: true, Enabled: true, @@ -483,20 +471,19 @@ func TestFailoverRoute(t *testing.T) { }, Prefix: ipp("10.0.0.0/24"), Node: types.Node{ - MachineKey: machineKeys[2], + ID: 3, }, IsPrimary: false, Enabled: true, }, }, - isConnected: map[key.MachinePublic]bool{ - machineKeys[0]: true, - machineKeys[1]: true, - machineKeys[2]: true, + isConnected: types.NodeConnectedMap{ + 1: true, + 2: true, + 3: true, }, - want: []key.MachinePublic{ - machineKeys[1], - machineKeys[0], + want: []types.NodeID{ + 2, 1, }, wantErr: false, }, @@ -508,7 +495,7 @@ func TestFailoverRoute(t *testing.T) { }, Prefix: ipp("10.0.0.0/24"), Node: types.Node{ - MachineKey: machineKeys[0], + ID: 1, }, IsPrimary: true, Enabled: true, @@ -520,7 +507,7 @@ func TestFailoverRoute(t *testing.T) { }, Prefix: ipp("10.0.0.0/24"), Node: types.Node{ - MachineKey: machineKeys[0], + ID: 1, }, IsPrimary: true, Enabled: true, @@ -532,15 +519,15 @@ func TestFailoverRoute(t *testing.T) { }, Prefix: ipp("10.0.0.0/24"), Node: types.Node{ - MachineKey: machineKeys[3], + ID: 4, }, IsPrimary: false, Enabled: true, }, }, - isConnected: map[key.MachinePublic]bool{ - machineKeys[0]: true, - machineKeys[3]: false, + isConnected: types.NodeConnectedMap{ + 1: true, + 4: false, }, want: nil, wantErr: false, @@ -553,7 +540,7 @@ func TestFailoverRoute(t *testing.T) { }, Prefix: ipp("10.0.0.0/24"), Node: types.Node{ - MachineKey: machineKeys[0], + ID: 1, }, IsPrimary: true, Enabled: true, @@ -565,7 +552,7 @@ func TestFailoverRoute(t *testing.T) { }, Prefix: ipp("10.0.0.0/24"), Node: types.Node{ - MachineKey: machineKeys[0], + ID: 1, }, IsPrimary: true, Enabled: true, @@ -577,7 +564,7 @@ func TestFailoverRoute(t *testing.T) { }, Prefix: ipp("10.0.0.0/24"), Node: types.Node{ - MachineKey: machineKeys[3], + ID: 4, }, IsPrimary: false, Enabled: true, @@ -588,20 +575,20 @@ func TestFailoverRoute(t *testing.T) { }, Prefix: ipp("10.0.0.0/24"), Node: types.Node{ - MachineKey: machineKeys[1], + ID: 2, }, IsPrimary: true, Enabled: true, }, }, - isConnected: map[key.MachinePublic]bool{ - machineKeys[0]: false, - machineKeys[1]: true, - machineKeys[3]: false, + isConnected: types.NodeConnectedMap{ + 1: false, + 2: true, + 4: false, }, - want: []key.MachinePublic{ - machineKeys[0], - machineKeys[1], + want: []types.NodeID{ + 1, + 2, }, wantErr: false, }, @@ -613,7 +600,7 @@ func TestFailoverRoute(t *testing.T) { }, Prefix: ipp("10.0.0.0/24"), Node: types.Node{ - MachineKey: machineKeys[0], + ID: 1, }, IsPrimary: true, Enabled: true, @@ -625,7 +612,7 @@ func TestFailoverRoute(t *testing.T) { }, Prefix: ipp("10.0.0.0/24"), Node: types.Node{ - MachineKey: machineKeys[0], + ID: 1, }, IsPrimary: true, Enabled: true, @@ -637,7 +624,7 @@ func TestFailoverRoute(t *testing.T) { }, Prefix: ipp("10.0.0.0/24"), Node: types.Node{ - MachineKey: machineKeys[1], + ID: 2, }, IsPrimary: false, Enabled: false, @@ -670,8 +657,8 @@ func TestFailoverRoute(t *testing.T) { } } - got, err := Write(db.DB, func(tx *gorm.DB) ([]key.MachinePublic, error) { - return failoverRoute(tx, tt.isConnected, &tt.failingRoute) + got, err := Write(db.DB, func(tx *gorm.DB) ([]types.NodeID, error) { + return failoverRouteTx(tx, tt.isConnected, &tt.failingRoute) }) if (err != nil) != tt.wantErr { @@ -687,230 +674,177 @@ func TestFailoverRoute(t *testing.T) { } } -// func TestDisableRouteFailover(t *testing.T) { -// machineKeys := []key.MachinePublic{ -// key.NewMachine().Public(), -// key.NewMachine().Public(), -// key.NewMachine().Public(), -// key.NewMachine().Public(), -// } +func TestFailoverRoute(t *testing.T) { + r := func(id uint, nid types.NodeID, prefix types.IPPrefix, enabled, primary bool) types.Route { + return types.Route{ + Model: gorm.Model{ + ID: id, + }, + Node: types.Node{ + ID: nid, + }, + Prefix: prefix, + Enabled: enabled, + IsPrimary: primary, + } + } + rp := func(id uint, nid types.NodeID, prefix types.IPPrefix, enabled, primary bool) *types.Route { + ro := r(id, nid, prefix, enabled, primary) + return &ro + } + tests := []struct { + name string + failingRoute types.Route + routes types.Routes + isConnected types.NodeConnectedMap + want *failover + }{ + { + name: "no-route", + failingRoute: types.Route{}, + routes: types.Routes{}, + want: nil, + }, + { + name: "no-prime", + failingRoute: r(1, 1, ipp("10.0.0.0/24"), false, false), -// tests := []struct { -// name string -// nodes types.Nodes + routes: types.Routes{}, + want: nil, + }, + { + name: "exit-node", + failingRoute: r(1, 1, ipp("0.0.0.0/0"), false, true), + routes: types.Routes{}, + want: nil, + }, + { + name: "no-failover-single-route", + failingRoute: r(1, 1, ipp("10.0.0.0/24"), false, true), + routes: types.Routes{ + r(1, 1, ipp("10.0.0.0/24"), false, true), + }, + want: nil, + }, + { + name: "failover-primary", + failingRoute: r(1, 1, ipp("10.0.0.0/24"), true, true), + routes: types.Routes{ + r(1, 1, ipp("10.0.0.0/24"), true, true), + r(2, 2, ipp("10.0.0.0/24"), true, false), + }, + isConnected: types.NodeConnectedMap{ + 1: false, + 2: true, + }, + want: &failover{ + old: rp(1, 1, ipp("10.0.0.0/24"), true, false), + new: rp(2, 2, ipp("10.0.0.0/24"), true, true), + }, + }, + { + name: "failover-none-primary", + failingRoute: r(1, 1, ipp("10.0.0.0/24"), true, false), + routes: types.Routes{ + r(1, 1, ipp("10.0.0.0/24"), true, true), + r(2, 2, ipp("10.0.0.0/24"), true, false), + }, + want: nil, + }, + { + name: "failover-primary-multi-route", + failingRoute: r(2, 2, ipp("10.0.0.0/24"), true, true), + routes: types.Routes{ + r(1, 1, ipp("10.0.0.0/24"), true, false), + r(2, 2, ipp("10.0.0.0/24"), true, true), + r(3, 3, ipp("10.0.0.0/24"), true, false), + }, + isConnected: types.NodeConnectedMap{ + 1: true, + 2: true, + 3: true, + }, + want: &failover{ + old: rp(2, 2, ipp("10.0.0.0/24"), true, false), + new: rp(1, 1, ipp("10.0.0.0/24"), true, true), + }, + }, + { + name: "failover-primary-no-online", + failingRoute: r(1, 1, ipp("10.0.0.0/24"), true, true), + routes: types.Routes{ + r(1, 1, ipp("10.0.0.0/24"), true, true), + r(2, 4, ipp("10.0.0.0/24"), true, false), + }, + isConnected: types.NodeConnectedMap{ + 1: true, + 4: false, + }, + want: nil, + }, + { + name: "failover-primary-one-not-online", + failingRoute: r(1, 1, ipp("10.0.0.0/24"), true, true), + routes: types.Routes{ + r(1, 1, ipp("10.0.0.0/24"), true, true), + r(2, 4, ipp("10.0.0.0/24"), true, false), + r(3, 2, ipp("10.0.0.0/24"), true, false), + }, + isConnected: types.NodeConnectedMap{ + 1: false, + 2: true, + 4: false, + }, + want: &failover{ + old: rp(1, 1, ipp("10.0.0.0/24"), true, false), + new: rp(3, 2, ipp("10.0.0.0/24"), true, true), + }, + }, + { + name: "failover-primary-none-enabled", + failingRoute: r(1, 1, ipp("10.0.0.0/24"), true, true), + routes: types.Routes{ + r(1, 1, ipp("10.0.0.0/24"), true, false), + r(2, 2, ipp("10.0.0.0/24"), false, true), + }, + want: nil, + }, + } -// routeID uint64 -// isConnected map[key.MachinePublic]bool + cmps := append( + util.Comparers, + cmp.Comparer(func(x, y types.IPPrefix) bool { + return netip.Prefix(x) == netip.Prefix(y) + }), + ) -// wantMachineKey key.MachinePublic -// wantErr string -// }{ -// { -// name: "single-route", -// nodes: types.Nodes{ -// &types.Node{ -// ID: 0, -// MachineKey: machineKeys[0], -// Routes: []types.Route{ -// { -// Model: gorm.Model{ -// ID: 1, -// }, -// Prefix: ipp("10.0.0.0/24"), -// Node: types.Node{ -// MachineKey: machineKeys[0], -// }, -// IsPrimary: true, -// }, -// }, -// Hostinfo: &tailcfg.Hostinfo{ -// RoutableIPs: []netip.Prefix{ -// netip.MustParsePrefix("10.0.0.0/24"), -// }, -// }, -// }, -// }, -// routeID: 1, -// wantMachineKey: machineKeys[0], -// }, -// { -// name: "failover-simple", -// nodes: types.Nodes{ -// &types.Node{ -// ID: 0, -// MachineKey: machineKeys[0], -// Routes: []types.Route{ -// { -// Model: gorm.Model{ -// ID: 1, -// }, -// Prefix: ipp("10.0.0.0/24"), -// IsPrimary: true, -// }, -// }, -// Hostinfo: &tailcfg.Hostinfo{ -// RoutableIPs: []netip.Prefix{ -// netip.MustParsePrefix("10.0.0.0/24"), -// }, -// }, -// }, -// &types.Node{ -// ID: 1, -// MachineKey: machineKeys[1], -// Routes: []types.Route{ -// { -// Model: gorm.Model{ -// ID: 2, -// }, -// Prefix: ipp("10.0.0.0/24"), -// IsPrimary: false, -// }, -// }, -// Hostinfo: &tailcfg.Hostinfo{ -// RoutableIPs: []netip.Prefix{ -// netip.MustParsePrefix("10.0.0.0/24"), -// }, -// }, -// }, -// }, -// routeID: 1, -// wantMachineKey: machineKeys[1], -// }, -// { -// name: "no-failover-offline", -// nodes: types.Nodes{ -// &types.Node{ -// ID: 0, -// MachineKey: machineKeys[0], -// Routes: []types.Route{ -// { -// Model: gorm.Model{ -// ID: 1, -// }, -// Prefix: ipp("10.0.0.0/24"), -// IsPrimary: true, -// }, -// }, -// Hostinfo: &tailcfg.Hostinfo{ -// RoutableIPs: []netip.Prefix{ -// netip.MustParsePrefix("10.0.0.0/24"), -// }, -// }, -// }, -// &types.Node{ -// ID: 1, -// MachineKey: machineKeys[1], -// Routes: []types.Route{ -// { -// Model: gorm.Model{ -// ID: 2, -// }, -// Prefix: ipp("10.0.0.0/24"), -// IsPrimary: false, -// }, -// }, -// Hostinfo: &tailcfg.Hostinfo{ -// RoutableIPs: []netip.Prefix{ -// netip.MustParsePrefix("10.0.0.0/24"), -// }, -// }, -// }, -// }, -// isConnected: map[key.MachinePublic]bool{ -// machineKeys[0]: true, -// machineKeys[1]: false, -// }, -// routeID: 1, -// wantMachineKey: machineKeys[1], -// }, -// { -// name: "failover-to-online", -// nodes: types.Nodes{ -// &types.Node{ -// ID: 0, -// MachineKey: machineKeys[0], -// Routes: []types.Route{ -// { -// Model: gorm.Model{ -// ID: 1, -// }, -// Prefix: ipp("10.0.0.0/24"), -// IsPrimary: true, -// }, -// }, -// Hostinfo: &tailcfg.Hostinfo{ -// RoutableIPs: []netip.Prefix{ -// netip.MustParsePrefix("10.0.0.0/24"), -// }, -// }, -// }, -// &types.Node{ -// ID: 1, -// MachineKey: machineKeys[1], -// Routes: []types.Route{ -// { -// Model: gorm.Model{ -// ID: 2, -// }, -// Prefix: ipp("10.0.0.0/24"), -// IsPrimary: false, -// }, -// }, -// Hostinfo: &tailcfg.Hostinfo{ -// RoutableIPs: []netip.Prefix{ -// netip.MustParsePrefix("10.0.0.0/24"), -// }, -// }, -// }, -// }, -// isConnected: map[key.MachinePublic]bool{ -// machineKeys[0]: true, -// machineKeys[1]: true, -// }, -// routeID: 1, -// wantMachineKey: machineKeys[1], -// }, -// } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotf := failoverRoute(tt.isConnected, &tt.failingRoute, tt.routes) -// for _, tt := range tests { -// t.Run(tt.name, func(t *testing.T) { -// datab, err := NewHeadscaleDatabase("sqlite3", ":memory:", false, []netip.Prefix{}, "") -// assert.NoError(t, err) + if tt.want == nil && gotf != nil { + t.Fatalf("expected nil, got %+v", gotf) + } -// // bootstrap db -// datab.DB.Transaction(func(tx *gorm.DB) error { -// for _, node := range tt.nodes { -// err := tx.Save(node).Error -// if err != nil { -// return err -// } + if gotf == nil && tt.want != nil { + t.Fatalf("expected %+v, got nil", tt.want) + } -// _, err = SaveNodeRoutes(tx, node) -// if err != nil { -// return err -// } -// } + if tt.want != nil && gotf != nil { + want := map[string]*types.Route{ + "new": tt.want.new, + "old": tt.want.old, + } -// return nil -// }) + got := map[string]*types.Route{ + "new": gotf.new, + "old": gotf.old, + } -// got, err := Write(datab.DB, func(tx *gorm.DB) (*types.StateUpdate, error) { -// return DisableRoute(tx, tt.routeID, tt.isConnected) -// }) - -// // if (err.Error() != "") != tt.wantErr { -// // t.Errorf("failoverRoute() error = %v, wantErr %v", err, tt.wantErr) - -// // return -// // } - -// if len(got.ChangeNodes) != 1 { -// t.Errorf("expected update with one machine, got %d", len(got.ChangeNodes)) -// } - -// if diff := cmp.Diff(tt.wantMachineKey, got.ChangeNodes[0].MachineKey, util.Comparers...); diff != "" { -// t.Errorf("DisableRoute() unexpected result (-want +got):\n%s", diff) -// } -// }) -// } -// } + if diff := cmp.Diff(want, got, cmps...); diff != "" { + t.Fatalf("failoverRoute unexpected result (-want +got):\n%s", diff) + } + } + }) + } +} diff --git a/hscontrol/grpcv1.go b/hscontrol/grpcv1.go index 379502c7..d5a1854e 100644 --- a/hscontrol/grpcv1.go +++ b/hscontrol/grpcv1.go @@ -222,7 +222,7 @@ func (api headscaleV1APIServer) GetNode( ctx context.Context, request *v1.GetNodeRequest, ) (*v1.GetNodeResponse, error) { - node, err := api.h.db.GetNodeByID(request.GetNodeId()) + node, err := api.h.db.GetNodeByID(types.NodeID(request.GetNodeId())) if err != nil { return nil, err } @@ -231,7 +231,7 @@ func (api headscaleV1APIServer) GetNode( // Populate the online field based on // currently connected nodes. - resp.Online = api.h.nodeNotifier.IsConnected(node.MachineKey) + resp.Online = api.h.nodeNotifier.IsConnected(node.ID) return &v1.GetNodeResponse{Node: resp}, nil } @@ -248,12 +248,12 @@ func (api headscaleV1APIServer) SetTags( } node, err := db.Write(api.h.db.DB, func(tx *gorm.DB) (*types.Node, error) { - err := db.SetTags(tx, request.GetNodeId(), request.GetTags()) + err := db.SetTags(tx, types.NodeID(request.GetNodeId()), request.GetTags()) if err != nil { return nil, err } - return db.GetNodeByID(tx, request.GetNodeId()) + return db.GetNodeByID(tx, types.NodeID(request.GetNodeId())) }) if err != nil { return &v1.SetTagsResponse{ @@ -261,15 +261,12 @@ func (api headscaleV1APIServer) SetTags( }, status.Error(codes.InvalidArgument, err.Error()) } - stateUpdate := types.StateUpdate{ + ctx = types.NotifyCtx(ctx, "cli-settags", node.Hostname) + api.h.nodeNotifier.NotifyWithIgnore(ctx, types.StateUpdate{ Type: types.StatePeerChanged, - ChangeNodes: types.Nodes{node}, + ChangeNodes: []types.NodeID{node.ID}, Message: "called from api.SetTags", - } - if stateUpdate.Valid() { - ctx := types.NotifyCtx(ctx, "cli-settags", node.Hostname) - api.h.nodeNotifier.NotifyWithIgnore(ctx, stateUpdate, node.MachineKey.String()) - } + }, node.ID) log.Trace(). Str("node", node.Hostname). @@ -296,12 +293,12 @@ func (api headscaleV1APIServer) DeleteNode( ctx context.Context, request *v1.DeleteNodeRequest, ) (*v1.DeleteNodeResponse, error) { - node, err := api.h.db.GetNodeByID(request.GetNodeId()) + node, err := api.h.db.GetNodeByID(types.NodeID(request.GetNodeId())) if err != nil { return nil, err } - err = api.h.db.DeleteNode( + changedNodes, err := api.h.db.DeleteNode( node, api.h.nodeNotifier.ConnectedMap(), ) @@ -309,13 +306,17 @@ func (api headscaleV1APIServer) DeleteNode( return nil, err } - stateUpdate := types.StateUpdate{ + ctx = types.NotifyCtx(ctx, "cli-deletenode", node.Hostname) + api.h.nodeNotifier.NotifyAll(ctx, types.StateUpdate{ Type: types.StatePeerRemoved, - Removed: []tailcfg.NodeID{tailcfg.NodeID(node.ID)}, - } - if stateUpdate.Valid() { - ctx := types.NotifyCtx(ctx, "cli-deletenode", node.Hostname) - api.h.nodeNotifier.NotifyAll(ctx, stateUpdate) + Removed: []types.NodeID{node.ID}, + }) + + if changedNodes != nil { + api.h.nodeNotifier.NotifyAll(ctx, types.StateUpdate{ + Type: types.StatePeerChanged, + ChangeNodes: changedNodes, + }) } return &v1.DeleteNodeResponse{}, nil @@ -330,33 +331,27 @@ func (api headscaleV1APIServer) ExpireNode( node, err := db.Write(api.h.db.DB, func(tx *gorm.DB) (*types.Node, error) { db.NodeSetExpiry( tx, - request.GetNodeId(), + types.NodeID(request.GetNodeId()), now, ) - return db.GetNodeByID(tx, request.GetNodeId()) + return db.GetNodeByID(tx, types.NodeID(request.GetNodeId())) }) if err != nil { return nil, err } - selfUpdate := types.StateUpdate{ - Type: types.StateSelfUpdate, - ChangeNodes: types.Nodes{node}, - } - if selfUpdate.Valid() { - ctx := types.NotifyCtx(ctx, "cli-expirenode-self", node.Hostname) - api.h.nodeNotifier.NotifyByMachineKey( - ctx, - selfUpdate, - node.MachineKey) - } + ctx = types.NotifyCtx(ctx, "cli-expirenode-self", node.Hostname) + api.h.nodeNotifier.NotifyByMachineKey( + ctx, + types.StateUpdate{ + Type: types.StateSelfUpdate, + ChangeNodes: []types.NodeID{node.ID}, + }, + node.ID) - stateUpdate := types.StateUpdateExpire(node.ID, now) - if stateUpdate.Valid() { - ctx := types.NotifyCtx(ctx, "cli-expirenode-peers", node.Hostname) - api.h.nodeNotifier.NotifyWithIgnore(ctx, stateUpdate, node.MachineKey.String()) - } + ctx = types.NotifyCtx(ctx, "cli-expirenode-peers", node.Hostname) + api.h.nodeNotifier.NotifyWithIgnore(ctx, types.StateUpdateExpire(node.ID, now), node.ID) log.Trace(). Str("node", node.Hostname). @@ -380,21 +375,18 @@ func (api headscaleV1APIServer) RenameNode( return nil, err } - return db.GetNodeByID(tx, request.GetNodeId()) + return db.GetNodeByID(tx, types.NodeID(request.GetNodeId())) }) if err != nil { return nil, err } - stateUpdate := types.StateUpdate{ + ctx = types.NotifyCtx(ctx, "cli-renamenode", node.Hostname) + api.h.nodeNotifier.NotifyWithIgnore(ctx, types.StateUpdate{ Type: types.StatePeerChanged, - ChangeNodes: types.Nodes{node}, + ChangeNodes: []types.NodeID{node.ID}, Message: "called from api.RenameNode", - } - if stateUpdate.Valid() { - ctx := types.NotifyCtx(ctx, "cli-renamenode", node.Hostname) - api.h.nodeNotifier.NotifyWithIgnore(ctx, stateUpdate, node.MachineKey.String()) - } + }, node.ID) log.Trace(). Str("node", node.Hostname). @@ -423,7 +415,7 @@ func (api headscaleV1APIServer) ListNodes( // Populate the online field based on // currently connected nodes. - resp.Online = isConnected[node.MachineKey] + resp.Online = isConnected[node.ID] response[index] = resp } @@ -446,7 +438,7 @@ func (api headscaleV1APIServer) ListNodes( // Populate the online field based on // currently connected nodes. - resp.Online = isConnected[node.MachineKey] + resp.Online = isConnected[node.ID] validTags, invalidTags := api.h.ACLPolicy.TagsOfNode( node, @@ -463,7 +455,7 @@ func (api headscaleV1APIServer) MoveNode( ctx context.Context, request *v1.MoveNodeRequest, ) (*v1.MoveNodeResponse, error) { - node, err := api.h.db.GetNodeByID(request.GetNodeId()) + node, err := api.h.db.GetNodeByID(types.NodeID(request.GetNodeId())) if err != nil { return nil, err } @@ -503,7 +495,7 @@ func (api headscaleV1APIServer) EnableRoute( return nil, err } - if update != nil && update.Valid() { + if update != nil { ctx := types.NotifyCtx(ctx, "cli-enableroute", "unknown") api.h.nodeNotifier.NotifyAll( ctx, *update) @@ -516,17 +508,19 @@ func (api headscaleV1APIServer) DisableRoute( ctx context.Context, request *v1.DisableRouteRequest, ) (*v1.DisableRouteResponse, error) { - isConnected := api.h.nodeNotifier.ConnectedMap() - update, err := db.Write(api.h.db.DB, func(tx *gorm.DB) (*types.StateUpdate, error) { - return db.DisableRoute(tx, request.GetRouteId(), isConnected) + update, err := db.Write(api.h.db.DB, func(tx *gorm.DB) ([]types.NodeID, error) { + return db.DisableRoute(tx, request.GetRouteId(), api.h.nodeNotifier.ConnectedMap()) }) if err != nil { return nil, err } - if update != nil && update.Valid() { + if update != nil { ctx := types.NotifyCtx(ctx, "cli-disableroute", "unknown") - api.h.nodeNotifier.NotifyAll(ctx, *update) + api.h.nodeNotifier.NotifyAll(ctx, types.StateUpdate{ + Type: types.StatePeerChanged, + ChangeNodes: update, + }) } return &v1.DisableRouteResponse{}, nil @@ -536,7 +530,7 @@ func (api headscaleV1APIServer) GetNodeRoutes( ctx context.Context, request *v1.GetNodeRoutesRequest, ) (*v1.GetNodeRoutesResponse, error) { - node, err := api.h.db.GetNodeByID(request.GetNodeId()) + node, err := api.h.db.GetNodeByID(types.NodeID(request.GetNodeId())) if err != nil { return nil, err } @@ -556,16 +550,19 @@ func (api headscaleV1APIServer) DeleteRoute( request *v1.DeleteRouteRequest, ) (*v1.DeleteRouteResponse, error) { isConnected := api.h.nodeNotifier.ConnectedMap() - update, err := db.Write(api.h.db.DB, func(tx *gorm.DB) (*types.StateUpdate, error) { + update, err := db.Write(api.h.db.DB, func(tx *gorm.DB) ([]types.NodeID, error) { return db.DeleteRoute(tx, request.GetRouteId(), isConnected) }) if err != nil { return nil, err } - if update != nil && update.Valid() { + if update != nil { ctx := types.NotifyCtx(ctx, "cli-deleteroute", "unknown") - api.h.nodeNotifier.NotifyWithIgnore(ctx, *update) + api.h.nodeNotifier.NotifyAll(ctx, types.StateUpdate{ + Type: types.StatePeerChanged, + ChangeNodes: update, + }) } return &v1.DeleteRouteResponse{}, nil diff --git a/hscontrol/handlers.go b/hscontrol/handlers.go index ee670733..a6bbd1b8 100644 --- a/hscontrol/handlers.go +++ b/hscontrol/handlers.go @@ -68,12 +68,6 @@ func (h *Headscale) KeyHandler( Msg("could not get capability version") writer.Header().Set("Content-Type", "text/plain; charset=utf-8") writer.WriteHeader(http.StatusInternalServerError) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Failed to write response") - } return } @@ -82,19 +76,6 @@ func (h *Headscale) KeyHandler( Str("handler", "/key"). Int("cap_ver", int(capVer)). Msg("New noise client") - if err != nil { - writer.Header().Set("Content-Type", "text/plain; charset=utf-8") - writer.WriteHeader(http.StatusBadRequest) - _, err := writer.Write([]byte("Wrong params")) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Failed to write response") - } - - return - } // TS2021 (Tailscale v2 protocol) requires to have a different key if capVer >= NoiseCapabilityVersion { diff --git a/hscontrol/mapper/mapper.go b/hscontrol/mapper/mapper.go index df0f4d9c..3a92cae6 100644 --- a/hscontrol/mapper/mapper.go +++ b/hscontrol/mapper/mapper.go @@ -16,12 +16,12 @@ import ( "time" mapset "github.com/deckarep/golang-set/v2" + "github.com/juanfont/headscale/hscontrol/db" "github.com/juanfont/headscale/hscontrol/policy" "github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/util" "github.com/klauspost/compress/zstd" "github.com/rs/zerolog/log" - "golang.org/x/exp/maps" "tailscale.com/envknob" "tailscale.com/smallzstd" "tailscale.com/tailcfg" @@ -51,21 +51,14 @@ var debugDumpMapResponsePath = envknob.String("HEADSCALE_DEBUG_DUMP_MAPRESPONSE_ type Mapper struct { // Configuration // TODO(kradalby): figure out if this is the format we want this in - derpMap *tailcfg.DERPMap - baseDomain string - dnsCfg *tailcfg.DNSConfig - logtail bool - randomClientPort bool + db *db.HSDatabase + cfg *types.Config + derpMap *tailcfg.DERPMap + isLikelyConnected types.NodeConnectedMap uid string created time.Time seq uint64 - - // 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 - patches map[uint64][]patch } type patch struct { @@ -74,35 +67,22 @@ type patch struct { } func NewMapper( - node *types.Node, - peers types.Nodes, + db *db.HSDatabase, + cfg *types.Config, derpMap *tailcfg.DERPMap, - baseDomain string, - dnsCfg *tailcfg.DNSConfig, - logtail bool, - randomClientPort bool, + isLikelyConnected types.NodeConnectedMap, ) *Mapper { - log.Debug(). - Caller(). - Str("node", node.Hostname). - Msg("creating new mapper") - uid, _ := util.GenerateRandomStringDNSSafe(mapperIDLength) return &Mapper{ - derpMap: derpMap, - baseDomain: baseDomain, - dnsCfg: dnsCfg, - logtail: logtail, - randomClientPort: randomClientPort, + db: db, + cfg: cfg, + derpMap: derpMap, + isLikelyConnected: isLikelyConnected, uid: uid, created: time.Now(), seq: 0, - - // TODO: populate - peers: peers.IDMap(), - patches: make(map[uint64][]patch), } } @@ -207,11 +187,10 @@ func addNextDNSMetadata(resolvers []*dnstype.Resolver, node *types.Node) { // It is a separate function to make testing easier. func (m *Mapper) fullMapResponse( node *types.Node, + peers types.Nodes, pol *policy.ACLPolicy, capVer tailcfg.CapabilityVersion, ) (*tailcfg.MapResponse, error) { - peers := nodeMapToList(m.peers) - resp, err := m.baseWithConfigMapResponse(node, pol, capVer) if err != nil { return nil, err @@ -219,14 +198,13 @@ func (m *Mapper) fullMapResponse( err = appendPeerChanges( resp, + true, // full change pol, node, capVer, peers, peers, - m.baseDomain, - m.dnsCfg, - m.randomClientPort, + m.cfg, ) if err != nil { return nil, err @@ -240,35 +218,25 @@ func (m *Mapper) FullMapResponse( mapRequest tailcfg.MapRequest, node *types.Node, pol *policy.ACLPolicy, + messages ...string, ) ([]byte, error) { - m.mu.Lock() - defer m.mu.Unlock() - - 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") - } - - resp, err := m.fullMapResponse(node, pol, mapRequest.Version) + peers, err := m.ListPeers(node.ID) if err != nil { return nil, err } - return m.marshalMapResponse(mapRequest, resp, node, mapRequest.Compress) + resp, err := m.fullMapResponse(node, peers, pol, mapRequest.Version) + if err != nil { + return nil, err + } + + return m.marshalMapResponse(mapRequest, resp, node, mapRequest.Compress, messages...) } -// LiteMapResponse returns a MapResponse for the given node. +// ReadOnlyResponse returns a MapResponse for the given node. // Lite means that the peers has been omitted, this is intended // to be used to answer MapRequests with OmitPeers set to true. -func (m *Mapper) LiteMapResponse( +func (m *Mapper) ReadOnlyMapResponse( mapRequest tailcfg.MapRequest, node *types.Node, pol *policy.ACLPolicy, @@ -279,18 +247,6 @@ func (m *Mapper) LiteMapResponse( return nil, err } - rules, sshPolicy, err := policy.GenerateFilterAndSSHRules( - pol, - node, - nodeMapToList(m.peers), - ) - if err != nil { - return nil, err - } - - resp.PacketFilter = policy.ReduceFilterRules(node, rules) - resp.SSHPolicy = sshPolicy - return m.marshalMapResponse(mapRequest, resp, node, mapRequest.Compress, messages...) } @@ -320,50 +276,74 @@ func (m *Mapper) DERPMapResponse( func (m *Mapper) PeerChangedResponse( mapRequest tailcfg.MapRequest, node *types.Node, - changed types.Nodes, + changed map[types.NodeID]bool, + patches []*tailcfg.PeerChange, pol *policy.ACLPolicy, messages ...string, ) ([]byte, error) { - m.mu.Lock() - defer m.mu.Unlock() - - // Update our internal map. - for _, node := range changed { - if patches, ok := m.patches[node.ID]; ok { - // preserve online status in case the patch has an outdated one - online := node.IsOnline - - 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() - err := appendPeerChanges( + peers, err := m.ListPeers(node.ID) + if err != nil { + return nil, err + } + + var removedIDs []tailcfg.NodeID + var changedIDs []types.NodeID + for nodeID, nodeChanged := range changed { + if nodeChanged { + changedIDs = append(changedIDs, nodeID) + } else { + removedIDs = append(removedIDs, nodeID.NodeID()) + } + } + + changedNodes := make(types.Nodes, 0, len(changedIDs)) + for _, peer := range peers { + if slices.Contains(changedIDs, peer.ID) { + changedNodes = append(changedNodes, peer) + } + } + + err = appendPeerChanges( &resp, + false, // partial change pol, node, mapRequest.Version, - nodeMapToList(m.peers), - changed, - m.baseDomain, - m.dnsCfg, - m.randomClientPort, + peers, + changedNodes, + m.cfg, ) if err != nil { return nil, err } + resp.PeersRemoved = removedIDs + + // Sending patches as a part of a PeersChanged response + // is technically not suppose to be done, but they are + // applied after the PeersChanged. The patch list + // should _only_ contain Nodes that are not in the + // PeersChanged or PeersRemoved list and the caller + // should filter them out. + // + // From tailcfg docs: + // These are applied after Peers* above, but in practice the + // control server should only send these on their own, without + // the Peers* fields also set. + if patches != nil { + resp.PeersChangedPatch = patches + } + + // Add the node itself, it might have changed, and particularly + // if there are no patches or changes, this is a self update. + tailnode, err := tailNode(node, mapRequest.Version, pol, m.cfg) + if err != nil { + return nil, err + } + resp.Node = tailnode + return m.marshalMapResponse(mapRequest, &resp, node, mapRequest.Compress, messages...) } @@ -375,71 +355,12 @@ func (m *Mapper) PeerChangedPatchResponse( 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 { - m.patches[uint64(change.NodeID)] = append(patches, p) - } 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, - removed []tailcfg.NodeID, -) ([]byte, error) { - 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 = notYetRemoved - - return m.marshalMapResponse(mapRequest, &resp, node, mapRequest.Compress) -} - func (m *Mapper) marshalMapResponse( mapRequest tailcfg.MapRequest, resp *tailcfg.MapResponse, @@ -469,10 +390,8 @@ func (m *Mapper) marshalMapResponse( switch { case resp.Peers != nil && len(resp.Peers) > 0: responseType = "full" - case isSelfUpdate(messages...): + case resp.Peers == nil && resp.PeersChanged == nil && resp.PeersChangedPatch == nil && resp.DERPMap == nil && !resp.KeepAlive: responseType = "self" - 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: @@ -496,11 +415,11 @@ func (m *Mapper) marshalMapResponse( panic(err) } - now := time.Now().UnixNano() + now := time.Now().Format("2006-01-02T15-04-05.999999999") mapResponsePath := path.Join( mPath, - fmt.Sprintf("%d-%s-%d-%s.json", now, m.uid, atomic.LoadUint64(&m.seq), responseType), + fmt.Sprintf("%s-%s-%d-%s.json", now, m.uid, atomic.LoadUint64(&m.seq), responseType), ) log.Trace().Msgf("Writing MapResponse to %s", mapResponsePath) @@ -574,7 +493,7 @@ func (m *Mapper) baseWithConfigMapResponse( ) (*tailcfg.MapResponse, error) { resp := m.baseMapResponse() - tailnode, err := tailNode(node, capVer, pol, m.dnsCfg, m.baseDomain, m.randomClientPort) + tailnode, err := tailNode(node, capVer, pol, m.cfg) if err != nil { return nil, err } @@ -582,7 +501,7 @@ func (m *Mapper) baseWithConfigMapResponse( resp.DERPMap = m.derpMap - resp.Domain = m.baseDomain + resp.Domain = m.cfg.BaseDomain // Do not instruct clients to collect services we do not // support or do anything with them @@ -591,12 +510,26 @@ func (m *Mapper) baseWithConfigMapResponse( resp.KeepAlive = false resp.Debug = &tailcfg.Debug{ - DisableLogTail: !m.logtail, + DisableLogTail: !m.cfg.LogTail.Enabled, } return &resp, nil } +func (m *Mapper) ListPeers(nodeID types.NodeID) (types.Nodes, error) { + peers, err := m.db.ListPeers(nodeID) + if err != nil { + return nil, err + } + + for _, peer := range peers { + online := m.isLikelyConnected[peer.ID] + peer.IsOnline = &online + } + + return peers, nil +} + func nodeMapToList(nodes map[uint64]*types.Node) types.Nodes { ret := make(types.Nodes, 0) @@ -612,42 +545,41 @@ func nodeMapToList(nodes map[uint64]*types.Node) types.Nodes { func appendPeerChanges( resp *tailcfg.MapResponse, + fullChange bool, pol *policy.ACLPolicy, node *types.Node, capVer tailcfg.CapabilityVersion, peers types.Nodes, changed types.Nodes, - baseDomain string, - dnsCfg *tailcfg.DNSConfig, - randomClientPort bool, + cfg *types.Config, ) error { - fullChange := len(peers) == len(changed) - rules, sshPolicy, err := policy.GenerateFilterAndSSHRules( - pol, - node, - peers, - ) + packetFilter, err := pol.CompileFilterRules(append(peers, node)) + if err != nil { + return err + } + + sshPolicy, err := pol.CompileSSHPolicy(node, peers) if err != nil { return err } // 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 { - changed = policy.FilterNodesByACL(node, changed, rules) + if len(packetFilter) > 0 { + changed = policy.FilterNodesByACL(node, changed, packetFilter) } - profiles := generateUserProfiles(node, changed, baseDomain) + profiles := generateUserProfiles(node, changed, cfg.BaseDomain) dnsConfig := generateDNSConfig( - dnsCfg, - baseDomain, + cfg.DNSConfig, + cfg.BaseDomain, node, peers, ) - tailPeers, err := tailNodes(changed, capVer, pol, dnsCfg, baseDomain, randomClientPort) + tailPeers, err := tailNodes(changed, capVer, pol, cfg) if err != nil { return err } @@ -663,19 +595,9 @@ func appendPeerChanges( resp.PeersChanged = tailPeers } resp.DNSConfig = dnsConfig - resp.PacketFilter = policy.ReduceFilterRules(node, rules) + resp.PacketFilter = policy.ReduceFilterRules(node, packetFilter) resp.UserProfiles = profiles resp.SSHPolicy = sshPolicy return nil } - -func isSelfUpdate(messages ...string) bool { - for _, message := range messages { - if strings.Contains(message, types.SelfUpdateIdentifier) { - return true - } - } - - return false -} diff --git a/hscontrol/mapper/mapper_test.go b/hscontrol/mapper/mapper_test.go index bcc17dd4..3f4d6892 100644 --- a/hscontrol/mapper/mapper_test.go +++ b/hscontrol/mapper/mapper_test.go @@ -331,13 +331,10 @@ func Test_fullMapResponse(t *testing.T) { node *types.Node peers types.Nodes - baseDomain string - dnsConfig *tailcfg.DNSConfig - derpMap *tailcfg.DERPMap - logtail bool - randomClientPort bool - want *tailcfg.MapResponse - wantErr bool + derpMap *tailcfg.DERPMap + cfg *types.Config + want *tailcfg.MapResponse + wantErr bool }{ // { // name: "empty-node", @@ -349,15 +346,17 @@ func Test_fullMapResponse(t *testing.T) { // wantErr: true, // }, { - name: "no-pol-no-peers-map-response", - pol: &policy.ACLPolicy{}, - node: mini, - peers: types.Nodes{}, - baseDomain: "", - dnsConfig: &tailcfg.DNSConfig{}, - derpMap: &tailcfg.DERPMap{}, - logtail: false, - randomClientPort: false, + name: "no-pol-no-peers-map-response", + pol: &policy.ACLPolicy{}, + node: mini, + peers: types.Nodes{}, + derpMap: &tailcfg.DERPMap{}, + cfg: &types.Config{ + BaseDomain: "", + DNSConfig: &tailcfg.DNSConfig{}, + LogTail: types.LogTailConfig{Enabled: false}, + RandomizeClientPort: false, + }, want: &tailcfg.MapResponse{ Node: tailMini, KeepAlive: false, @@ -383,11 +382,13 @@ func Test_fullMapResponse(t *testing.T) { peers: types.Nodes{ peer1, }, - baseDomain: "", - dnsConfig: &tailcfg.DNSConfig{}, - derpMap: &tailcfg.DERPMap{}, - logtail: false, - randomClientPort: false, + derpMap: &tailcfg.DERPMap{}, + cfg: &types.Config{ + BaseDomain: "", + DNSConfig: &tailcfg.DNSConfig{}, + LogTail: types.LogTailConfig{Enabled: false}, + RandomizeClientPort: false, + }, want: &tailcfg.MapResponse{ KeepAlive: false, Node: tailMini, @@ -424,11 +425,13 @@ func Test_fullMapResponse(t *testing.T) { peer1, peer2, }, - baseDomain: "", - dnsConfig: &tailcfg.DNSConfig{}, - derpMap: &tailcfg.DERPMap{}, - logtail: false, - randomClientPort: false, + derpMap: &tailcfg.DERPMap{}, + cfg: &types.Config{ + BaseDomain: "", + DNSConfig: &tailcfg.DNSConfig{}, + LogTail: types.LogTailConfig{Enabled: false}, + RandomizeClientPort: false, + }, want: &tailcfg.MapResponse{ KeepAlive: false, Node: tailMini, @@ -463,17 +466,15 @@ func Test_fullMapResponse(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mappy := NewMapper( - tt.node, - tt.peers, + nil, + tt.cfg, tt.derpMap, - tt.baseDomain, - tt.dnsConfig, - tt.logtail, - tt.randomClientPort, + nil, ) got, err := mappy.fullMapResponse( tt.node, + tt.peers, tt.pol, 0, ) diff --git a/hscontrol/mapper/tail.go b/hscontrol/mapper/tail.go index c10da4de..97d12e86 100644 --- a/hscontrol/mapper/tail.go +++ b/hscontrol/mapper/tail.go @@ -3,12 +3,10 @@ package mapper import ( "fmt" "net/netip" - "strconv" "time" "github.com/juanfont/headscale/hscontrol/policy" "github.com/juanfont/headscale/hscontrol/types" - "github.com/juanfont/headscale/hscontrol/util" "github.com/samber/lo" "tailscale.com/tailcfg" ) @@ -17,9 +15,7 @@ func tailNodes( nodes types.Nodes, capVer tailcfg.CapabilityVersion, pol *policy.ACLPolicy, - dnsConfig *tailcfg.DNSConfig, - baseDomain string, - randomClientPort bool, + cfg *types.Config, ) ([]*tailcfg.Node, error) { tNodes := make([]*tailcfg.Node, len(nodes)) @@ -28,9 +24,7 @@ func tailNodes( node, capVer, pol, - dnsConfig, - baseDomain, - randomClientPort, + cfg, ) if err != nil { return nil, err @@ -48,9 +42,7 @@ func tailNode( node *types.Node, capVer tailcfg.CapabilityVersion, pol *policy.ACLPolicy, - dnsConfig *tailcfg.DNSConfig, - baseDomain string, - randomClientPort bool, + cfg *types.Config, ) (*tailcfg.Node, error) { addrs := node.IPAddresses.Prefixes() @@ -85,7 +77,7 @@ func tailNode( keyExpiry = time.Time{} } - hostname, err := node.GetFQDN(dnsConfig, baseDomain) + hostname, err := node.GetFQDN(cfg.DNSConfig, cfg.BaseDomain) if err != nil { return nil, fmt.Errorf("tailNode, failed to create FQDN: %s", err) } @@ -94,12 +86,10 @@ func tailNode( tags = lo.Uniq(append(tags, node.ForcedTags...)) tNode := tailcfg.Node{ - ID: tailcfg.NodeID(node.ID), // this is the actual ID - StableID: tailcfg.StableNodeID( - strconv.FormatUint(node.ID, util.Base10), - ), // in headscale, unlike tailcontrol server, IDs are permanent - Name: hostname, - Cap: capVer, + ID: tailcfg.NodeID(node.ID), // this is the actual ID + StableID: node.ID.StableID(), + Name: hostname, + Cap: capVer, User: tailcfg.UserID(node.UserID), @@ -133,7 +123,7 @@ func tailNode( tailcfg.CapabilitySSH: []tailcfg.RawMessage{}, } - if randomClientPort { + if cfg.RandomizeClientPort { tNode.CapMap[tailcfg.NodeAttrRandomizeClientPort] = []tailcfg.RawMessage{} } } else { @@ -143,7 +133,7 @@ func tailNode( tailcfg.CapabilitySSH, } - if randomClientPort { + if cfg.RandomizeClientPort { tNode.Capabilities = append(tNode.Capabilities, tailcfg.NodeAttrRandomizeClientPort) } } diff --git a/hscontrol/mapper/tail_test.go b/hscontrol/mapper/tail_test.go index f6e370c4..e79d9dc5 100644 --- a/hscontrol/mapper/tail_test.go +++ b/hscontrol/mapper/tail_test.go @@ -182,13 +182,16 @@ func TestTailNode(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + cfg := &types.Config{ + BaseDomain: tt.baseDomain, + DNSConfig: tt.dnsConfig, + RandomizeClientPort: false, + } got, err := tailNode( tt.node, 0, tt.pol, - tt.dnsConfig, - tt.baseDomain, - false, + cfg, ) if (err != nil) != tt.wantErr { diff --git a/hscontrol/noise.go b/hscontrol/noise.go index 0fa28d19..3debd378 100644 --- a/hscontrol/noise.go +++ b/hscontrol/noise.go @@ -3,6 +3,7 @@ package hscontrol import ( "encoding/binary" "encoding/json" + "errors" "io" "net/http" @@ -11,6 +12,7 @@ import ( "github.com/rs/zerolog/log" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" + "gorm.io/gorm" "tailscale.com/control/controlbase" "tailscale.com/control/controlhttp" "tailscale.com/tailcfg" @@ -163,3 +165,135 @@ func (ns *noiseServer) earlyNoise(protocolVersion int, writer io.Writer) error { return nil } + +const ( + MinimumCapVersion tailcfg.CapabilityVersion = 58 +) + +// 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 +// 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 (ns *noiseServer) NoisePollNetMapHandler( + writer http.ResponseWriter, + req *http.Request, +) { + log.Trace(). + Str("handler", "NoisePollNetMap"). + Msg("PollNetMapHandler called") + + log.Trace(). + Any("headers", req.Header). + Caller(). + Msg("Headers") + + body, _ := io.ReadAll(req.Body) + + mapRequest := tailcfg.MapRequest{} + if err := json.Unmarshal(body, &mapRequest); err != nil { + log.Error(). + Caller(). + Err(err). + Msg("Cannot parse MapRequest") + http.Error(writer, "Internal error", http.StatusInternalServerError) + + 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( + ns.conn.Peer(), + mapRequest.NodeKey, + key.NodePublic{}, + ) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + log.Warn(). + Str("handler", "NoisePollNetMap"). + Uint64("node.id", node.ID.Uint64()). + Msgf("Ignoring request, cannot find node with key %s", mapRequest.NodeKey.String()) + http.Error(writer, "Internal error", http.StatusNotFound) + + return + } + log.Error(). + Str("handler", "NoisePollNetMap"). + Uint64("node.id", node.ID.Uint64()). + Msgf("Failed to fetch node from the database with node key: %s", mapRequest.NodeKey.String()) + http.Error(writer, "Internal error", http.StatusInternalServerError) + + return + } + log.Debug(). + Str("handler", "NoisePollNetMap"). + Str("node", node.Hostname). + Int("cap_ver", int(mapRequest.Version)). + Uint64("node.id", node.ID.Uint64()). + Msg("A node sending a MapRequest with Noise protocol") + + session := ns.headscale.newMapSession(req.Context(), mapRequest, writer, node) + + // If a streaming mapSession exists for this node, close it + // and start a new one. + if session.isStreaming() { + log.Debug(). + Caller(). + Uint64("node.id", node.ID.Uint64()). + Int("cap_ver", int(mapRequest.Version)). + Msg("Aquiring lock to check stream") + ns.headscale.mapSessionMu.Lock() + if oldSession, ok := ns.headscale.mapSessions[node.ID]; ok { + log.Info(). + Caller(). + Uint64("node.id", node.ID.Uint64()). + Msg("Node has an open streaming session, replacing") + oldSession.close() + } + + ns.headscale.mapSessions[node.ID] = session + ns.headscale.mapSessionMu.Unlock() + log.Debug(). + Caller(). + Uint64("node.id", node.ID.Uint64()). + Int("cap_ver", int(mapRequest.Version)). + Msg("Releasing lock to check stream") + } + + session.serve() + + if session.isStreaming() { + log.Debug(). + Caller(). + Uint64("node.id", node.ID.Uint64()). + Int("cap_ver", int(mapRequest.Version)). + Msg("Aquiring lock to remove stream") + ns.headscale.mapSessionMu.Lock() + + delete(ns.headscale.mapSessions, node.ID) + + ns.headscale.mapSessionMu.Unlock() + log.Debug(). + Caller(). + Uint64("node.id", node.ID.Uint64()). + Int("cap_ver", int(mapRequest.Version)). + Msg("Releasing lock to remove stream") + } +} diff --git a/hscontrol/notifier/notifier.go b/hscontrol/notifier/notifier.go index 2384a40f..4ead615b 100644 --- a/hscontrol/notifier/notifier.go +++ b/hscontrol/notifier/notifier.go @@ -3,52 +3,51 @@ package notifier import ( "context" "fmt" + "slices" "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 { l sync.RWMutex - nodes map[string]chan<- types.StateUpdate - connected map[key.MachinePublic]bool + nodes map[types.NodeID]chan<- types.StateUpdate + connected types.NodeConnectedMap } func NewNotifier() *Notifier { return &Notifier{ - nodes: make(map[string]chan<- types.StateUpdate), - connected: make(map[key.MachinePublic]bool), + nodes: make(map[types.NodeID]chan<- types.StateUpdate), + connected: make(types.NodeConnectedMap), } } -func (n *Notifier) AddNode(machineKey key.MachinePublic, c chan<- types.StateUpdate) { - log.Trace().Caller().Str("key", machineKey.ShortString()).Msg("acquiring lock to add node") +func (n *Notifier) AddNode(nodeID types.NodeID, c chan<- types.StateUpdate) { + log.Trace().Caller().Uint64("node.id", nodeID.Uint64()).Msg("acquiring lock to add node") defer log.Trace(). Caller(). - Str("key", machineKey.ShortString()). + Uint64("node.id", nodeID.Uint64()). Msg("releasing lock to add node") n.l.Lock() defer n.l.Unlock() - n.nodes[machineKey.String()] = c - n.connected[machineKey] = true + n.nodes[nodeID] = c + n.connected[nodeID] = true log.Trace(). - Str("machine_key", machineKey.ShortString()). + Uint64("node.id", nodeID.Uint64()). Int("open_chans", len(n.nodes)). Msg("Added new channel") } -func (n *Notifier) RemoveNode(machineKey key.MachinePublic) { - log.Trace().Caller().Str("key", machineKey.ShortString()).Msg("acquiring lock to remove node") +func (n *Notifier) RemoveNode(nodeID types.NodeID) { + log.Trace().Caller().Uint64("node.id", nodeID.Uint64()).Msg("acquiring lock to remove node") defer log.Trace(). Caller(). - Str("key", machineKey.ShortString()). + Uint64("node.id", nodeID.Uint64()). Msg("releasing lock to remove node") n.l.Lock() @@ -58,26 +57,32 @@ func (n *Notifier) RemoveNode(machineKey key.MachinePublic) { return } - delete(n.nodes, machineKey.String()) - n.connected[machineKey] = false + delete(n.nodes, nodeID) + n.connected[nodeID] = false log.Trace(). - Str("machine_key", machineKey.ShortString()). + Uint64("node.id", nodeID.Uint64()). 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 { +func (n *Notifier) IsConnected(nodeID types.NodeID) bool { n.l.RLock() defer n.l.RUnlock() - return n.connected[machineKey] + return n.connected[nodeID] +} + +// IsLikelyConnected reports if a node is connected to headscale and has a +// poll session open, but doesnt lock, so might be wrong. +func (n *Notifier) IsLikelyConnected(nodeID types.NodeID) bool { + return n.connected[nodeID] } // TODO(kradalby): This returns a pointer and can be dangerous. -func (n *Notifier) ConnectedMap() map[key.MachinePublic]bool { +func (n *Notifier) ConnectedMap() types.NodeConnectedMap { return n.connected } @@ -88,19 +93,23 @@ func (n *Notifier) NotifyAll(ctx context.Context, update types.StateUpdate) { func (n *Notifier) NotifyWithIgnore( ctx context.Context, update types.StateUpdate, - ignore ...string, + ignoreNodeIDs ...types.NodeID, ) { - log.Trace().Caller().Interface("type", update.Type).Msg("acquiring lock to notify") + log.Trace().Caller().Str("type", update.Type.String()).Msg("acquiring lock to notify") defer log.Trace(). Caller(). - Interface("type", update.Type). + Str("type", update.Type.String()). Msg("releasing lock, finished notifying") n.l.RLock() defer n.l.RUnlock() - for key, c := range n.nodes { - if util.IsStringInSlice(ignore, key) { + if update.Type == types.StatePeerChangedPatch { + log.Trace().Interface("update", update).Interface("online", n.connected).Msg("PATCH UPDATE SENT") + } + + for nodeID, c := range n.nodes { + if slices.Contains(ignoreNodeIDs, nodeID) { continue } @@ -108,17 +117,17 @@ func (n *Notifier) NotifyWithIgnore( case <-ctx.Done(): log.Error(). Err(ctx.Err()). - Str("mkey", key). + Uint64("node.id", nodeID.Uint64()). Any("origin", ctx.Value("origin")). - Any("hostname", ctx.Value("hostname")). + Any("origin-hostname", ctx.Value("hostname")). Msgf("update not sent, context cancelled") return case c <- update: log.Trace(). - Str("mkey", key). + Uint64("node.id", nodeID.Uint64()). Any("origin", ctx.Value("origin")). - Any("hostname", ctx.Value("hostname")). + Any("origin-hostname", ctx.Value("hostname")). Msgf("update successfully sent on chan") } } @@ -127,33 +136,33 @@ func (n *Notifier) NotifyWithIgnore( func (n *Notifier) NotifyByMachineKey( ctx context.Context, update types.StateUpdate, - mKey key.MachinePublic, + nodeID types.NodeID, ) { - log.Trace().Caller().Interface("type", update.Type).Msg("acquiring lock to notify") + log.Trace().Caller().Str("type", update.Type.String()).Msg("acquiring lock to notify") defer log.Trace(). Caller(). - Interface("type", update.Type). + Str("type", update.Type.String()). Msg("releasing lock, finished notifying") n.l.RLock() defer n.l.RUnlock() - if c, ok := n.nodes[mKey.String()]; ok { + if c, ok := n.nodes[nodeID]; ok { select { case <-ctx.Done(): log.Error(). Err(ctx.Err()). - Str("mkey", mKey.String()). + Uint64("node.id", nodeID.Uint64()). Any("origin", ctx.Value("origin")). - Any("hostname", ctx.Value("hostname")). + Any("origin-hostname", ctx.Value("hostname")). Msgf("update not sent, context cancelled") return case c <- update: log.Trace(). - Str("mkey", mKey.String()). + Uint64("node.id", nodeID.Uint64()). Any("origin", ctx.Value("origin")). - Any("hostname", ctx.Value("hostname")). + Any("origin-hostname", ctx.Value("hostname")). Msgf("update successfully sent on chan") } } @@ -166,7 +175,7 @@ func (n *Notifier) String() string { str := []string{"Notifier, in map:\n"} for k, v := range n.nodes { - str = append(str, fmt.Sprintf("\t%s: %v\n", k, v)) + str = append(str, fmt.Sprintf("\t%d: %v\n", k, v)) } return strings.Join(str, "") diff --git a/hscontrol/oidc.go b/hscontrol/oidc.go index 318aadae..d669a922 100644 --- a/hscontrol/oidc.go +++ b/hscontrol/oidc.go @@ -537,11 +537,8 @@ func (h *Headscale) validateNodeForOIDCCallback( util.LogErr(err, "Failed to write response") } - stateUpdate := types.StateUpdateExpire(node.ID, expiry) - if stateUpdate.Valid() { - ctx := types.NotifyCtx(context.Background(), "oidc-expiry", "na") - h.nodeNotifier.NotifyWithIgnore(ctx, stateUpdate, node.MachineKey.String()) - } + ctx := types.NotifyCtx(context.Background(), "oidc-expiry", "na") + h.nodeNotifier.NotifyWithIgnore(ctx, types.StateUpdateExpire(node.ID, expiry), node.ID) return nil, true, nil } diff --git a/hscontrol/policy/acls.go b/hscontrol/policy/acls.go index 2ccc56b4..b4095781 100644 --- a/hscontrol/policy/acls.go +++ b/hscontrol/policy/acls.go @@ -114,7 +114,7 @@ func LoadACLPolicyFromBytes(acl []byte, format string) (*ACLPolicy, error) { return &policy, nil } -func GenerateFilterAndSSHRules( +func GenerateFilterAndSSHRulesForTests( policy *ACLPolicy, node *types.Node, peers types.Nodes, @@ -124,40 +124,31 @@ func GenerateFilterAndSSHRules( return tailcfg.FilterAllowAll, &tailcfg.SSHPolicy{}, nil } - rules, err := policy.generateFilterRules(node, peers) + rules, err := policy.CompileFilterRules(append(peers, node)) if err != nil { return []tailcfg.FilterRule{}, &tailcfg.SSHPolicy{}, err } log.Trace().Interface("ACL", rules).Str("node", node.GivenName).Msg("ACL rules") - var sshPolicy *tailcfg.SSHPolicy - sshRules, err := policy.generateSSHRules(node, peers) + sshPolicy, err := policy.CompileSSHPolicy(node, peers) if err != nil { return []tailcfg.FilterRule{}, &tailcfg.SSHPolicy{}, err } - log.Trace(). - Interface("SSH", sshRules). - Str("node", node.GivenName). - Msg("SSH rules") - - if sshPolicy == nil { - sshPolicy = &tailcfg.SSHPolicy{} - } - sshPolicy.Rules = sshRules - return rules, sshPolicy, nil } -// generateFilterRules takes a set of nodes and an ACLPolicy and generates a +// CompileFilterRules takes a set of nodes and an ACLPolicy and generates a // set of Tailscale compatible FilterRules used to allow traffic on clients. -func (pol *ACLPolicy) generateFilterRules( - node *types.Node, - peers types.Nodes, +func (pol *ACLPolicy) CompileFilterRules( + nodes types.Nodes, ) ([]tailcfg.FilterRule, error) { + if pol == nil { + return tailcfg.FilterAllowAll, nil + } + rules := []tailcfg.FilterRule{} - nodes := append(peers, node) for index, acl := range pol.ACLs { if acl.Action != "accept" { @@ -279,10 +270,14 @@ func ReduceFilterRules(node *types.Node, rules []tailcfg.FilterRule) []tailcfg.F return ret } -func (pol *ACLPolicy) generateSSHRules( +func (pol *ACLPolicy) CompileSSHPolicy( node *types.Node, peers types.Nodes, -) ([]*tailcfg.SSHRule, error) { +) (*tailcfg.SSHPolicy, error) { + if pol == nil { + return nil, nil + } + rules := []*tailcfg.SSHRule{} acceptAction := tailcfg.SSHAction{ @@ -393,7 +388,9 @@ func (pol *ACLPolicy) generateSSHRules( }) } - return rules, nil + return &tailcfg.SSHPolicy{ + Rules: rules, + }, nil } func sshCheckAction(duration string) (*tailcfg.SSHAction, error) { diff --git a/hscontrol/policy/acls_test.go b/hscontrol/policy/acls_test.go index ff18dd05..db1a0dd3 100644 --- a/hscontrol/policy/acls_test.go +++ b/hscontrol/policy/acls_test.go @@ -385,11 +385,12 @@ acls: return } - rules, err := pol.generateFilterRules(&types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.100.100.100"), + rules, err := pol.CompileFilterRules(types.Nodes{ + &types.Node{ + IPAddresses: types.NodeAddresses{ + netip.MustParseAddr("100.100.100.100"), + }, }, - }, types.Nodes{ &types.Node{ IPAddresses: types.NodeAddresses{ netip.MustParseAddr("200.200.200.200"), @@ -546,7 +547,7 @@ func (s *Suite) TestRuleInvalidGeneration(c *check.C) { c.Assert(pol.ACLs, check.HasLen, 6) c.Assert(err, check.IsNil) - rules, err := pol.generateFilterRules(&types.Node{}, types.Nodes{}) + rules, err := pol.CompileFilterRules(types.Nodes{}) c.Assert(err, check.NotNil) c.Assert(rules, check.IsNil) } @@ -562,7 +563,7 @@ func (s *Suite) TestInvalidAction(c *check.C) { }, }, } - _, _, err := GenerateFilterAndSSHRules(pol, &types.Node{}, types.Nodes{}) + _, _, err := GenerateFilterAndSSHRulesForTests(pol, &types.Node{}, types.Nodes{}) c.Assert(errors.Is(err, ErrInvalidAction), check.Equals, true) } @@ -581,7 +582,7 @@ func (s *Suite) TestInvalidGroupInGroup(c *check.C) { }, }, } - _, _, err := GenerateFilterAndSSHRules(pol, &types.Node{}, types.Nodes{}) + _, _, err := GenerateFilterAndSSHRulesForTests(pol, &types.Node{}, types.Nodes{}) c.Assert(errors.Is(err, ErrInvalidGroup), check.Equals, true) } @@ -597,7 +598,7 @@ func (s *Suite) TestInvalidTagOwners(c *check.C) { }, } - _, _, err := GenerateFilterAndSSHRules(pol, &types.Node{}, types.Nodes{}) + _, _, err := GenerateFilterAndSSHRulesForTests(pol, &types.Node{}, types.Nodes{}) c.Assert(errors.Is(err, ErrInvalidTag), check.Equals, true) } @@ -1724,8 +1725,7 @@ func TestACLPolicy_generateFilterRules(t *testing.T) { pol ACLPolicy } type args struct { - node *types.Node - peers types.Nodes + nodes types.Nodes } tests := []struct { name string @@ -1755,13 +1755,14 @@ func TestACLPolicy_generateFilterRules(t *testing.T) { }, }, args: args{ - node: &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.1"), - netip.MustParseAddr("fd7a:115c:a1e0:ab12:4843:2222:6273:2221"), + nodes: types.Nodes{ + &types.Node{ + IPAddresses: types.NodeAddresses{ + netip.MustParseAddr("100.64.0.1"), + netip.MustParseAddr("fd7a:115c:a1e0:ab12:4843:2222:6273:2221"), + }, }, }, - peers: types.Nodes{}, }, want: []tailcfg.FilterRule{ { @@ -1800,14 +1801,14 @@ func TestACLPolicy_generateFilterRules(t *testing.T) { }, }, args: args{ - node: &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.1"), - netip.MustParseAddr("fd7a:115c:a1e0:ab12:4843:2222:6273:2221"), + nodes: types.Nodes{ + &types.Node{ + IPAddresses: types.NodeAddresses{ + netip.MustParseAddr("100.64.0.1"), + netip.MustParseAddr("fd7a:115c:a1e0:ab12:4843:2222:6273:2221"), + }, + User: types.User{Name: "mickael"}, }, - User: types.User{Name: "mickael"}, - }, - peers: types.Nodes{ &types.Node{ IPAddresses: types.NodeAddresses{ netip.MustParseAddr("100.64.0.2"), @@ -1846,9 +1847,8 @@ func TestACLPolicy_generateFilterRules(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := tt.field.pol.generateFilterRules( - tt.args.node, - tt.args.peers, + got, err := tt.field.pol.CompileFilterRules( + tt.args.nodes, ) if (err != nil) != tt.wantErr { t.Errorf("ACLgenerateFilterRules() error = %v, wantErr %v", err, tt.wantErr) @@ -1980,9 +1980,8 @@ func TestReduceFilterRules(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - rules, _ := tt.pol.generateFilterRules( - tt.node, - tt.peers, + rules, _ := tt.pol.CompileFilterRules( + append(tt.peers, tt.node), ) got := ReduceFilterRules(tt.node, rules) @@ -2883,7 +2882,7 @@ func TestSSHRules(t *testing.T) { node types.Node peers types.Nodes pol ACLPolicy - want []*tailcfg.SSHRule + want *tailcfg.SSHPolicy }{ { name: "peers-can-connect", @@ -2946,7 +2945,7 @@ func TestSSHRules(t *testing.T) { }, }, }, - want: []*tailcfg.SSHRule{ + want: &tailcfg.SSHPolicy{Rules: []*tailcfg.SSHRule{ { Principals: []*tailcfg.SSHPrincipal{ { @@ -2991,7 +2990,7 @@ func TestSSHRules(t *testing.T) { }, Action: &tailcfg.SSHAction{Accept: true, AllowLocalPortForwarding: true}, }, - }, + }}, }, { name: "peers-cannot-connect", @@ -3042,13 +3041,13 @@ func TestSSHRules(t *testing.T) { }, }, }, - want: []*tailcfg.SSHRule{}, + want: &tailcfg.SSHPolicy{Rules: []*tailcfg.SSHRule{}}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := tt.pol.generateSSHRules(&tt.node, tt.peers) + got, err := tt.pol.CompileSSHPolicy(&tt.node, tt.peers) assert.NoError(t, err) if diff := cmp.Diff(tt.want, got); diff != "" { @@ -3155,7 +3154,7 @@ func TestValidExpandTagOwnersInSources(t *testing.T) { }, } - got, _, err := GenerateFilterAndSSHRules(pol, node, types.Nodes{}) + got, _, err := GenerateFilterAndSSHRulesForTests(pol, node, types.Nodes{}) assert.NoError(t, err) want := []tailcfg.FilterRule{ @@ -3206,7 +3205,7 @@ func TestInvalidTagValidUser(t *testing.T) { }, } - got, _, err := GenerateFilterAndSSHRules(pol, node, types.Nodes{}) + got, _, err := GenerateFilterAndSSHRulesForTests(pol, node, types.Nodes{}) assert.NoError(t, err) want := []tailcfg.FilterRule{ @@ -3265,7 +3264,7 @@ func TestValidExpandTagOwnersInDestinations(t *testing.T) { // c.Assert(rules[0].DstPorts, check.HasLen, 1) // c.Assert(rules[0].DstPorts[0].IP, check.Equals, "100.64.0.1/32") - got, _, err := GenerateFilterAndSSHRules(pol, node, types.Nodes{}) + got, _, err := GenerateFilterAndSSHRulesForTests(pol, node, types.Nodes{}) assert.NoError(t, err) want := []tailcfg.FilterRule{ @@ -3335,7 +3334,7 @@ func TestValidTagInvalidUser(t *testing.T) { }, } - got, _, err := GenerateFilterAndSSHRules(pol, node, types.Nodes{nodes2}) + got, _, err := GenerateFilterAndSSHRulesForTests(pol, node, types.Nodes{nodes2}) assert.NoError(t, err) want := []tailcfg.FilterRule{ diff --git a/hscontrol/poll.go b/hscontrol/poll.go index 22dd78ff..2b65f6d9 100644 --- a/hscontrol/poll.go +++ b/hscontrol/poll.go @@ -1,80 +1,166 @@ package hscontrol import ( + "cmp" "context" "fmt" + "math/rand/v2" "net/http" + "net/netip" + "sort" "strings" + "sync" "time" "github.com/juanfont/headscale/hscontrol/db" "github.com/juanfont/headscale/hscontrol/mapper" "github.com/juanfont/headscale/hscontrol/types" "github.com/rs/zerolog/log" + "github.com/sasha-s/go-deadlock" xslices "golang.org/x/exp/slices" "gorm.io/gorm" - "tailscale.com/envknob" "tailscale.com/tailcfg" ) const ( - keepAliveInterval = 60 * time.Second + keepAliveInterval = 50 * time.Second ) type contextKey string const nodeNameContextKey = contextKey("nodeName") -type UpdateNode func() +type sessionManager struct { + mu sync.RWMutex + sess map[types.NodeID]*mapSession +} -func logPollFunc( - mapRequest tailcfg.MapRequest, +type mapSession struct { + h *Headscale + req tailcfg.MapRequest + ctx context.Context + capVer tailcfg.CapabilityVersion + mapper *mapper.Mapper + + serving bool + servingMu deadlock.Mutex + + ch chan types.StateUpdate + cancelCh chan struct{} + + node *types.Node + w http.ResponseWriter + + warnf func(string, ...any) + infof func(string, ...any) + tracef func(string, ...any) + errf func(error, string, ...any) +} + +func (h *Headscale) newMapSession( + ctx context.Context, + req tailcfg.MapRequest, + w http.ResponseWriter, node *types.Node, -) (func(string), func(string), func(error, string)) { - return func(msg string) { - log.Trace(). - Caller(). - Bool("readOnly", mapRequest.ReadOnly). - Bool("omitPeers", mapRequest.OmitPeers). - Bool("stream", mapRequest.Stream). - Str("node_key", node.NodeKey.ShortString()). - Str("node", node.Hostname). - Msg(msg) - }, - func(msg string) { - log.Warn(). - Caller(). - Bool("readOnly", mapRequest.ReadOnly). - Bool("omitPeers", mapRequest.OmitPeers). - Bool("stream", mapRequest.Stream). - Str("node_key", node.NodeKey.ShortString()). - Str("node", node.Hostname). - Msg(msg) - }, - func(err error, msg string) { - log.Error(). - Caller(). - Bool("readOnly", mapRequest.ReadOnly). - Bool("omitPeers", mapRequest.OmitPeers). - Bool("stream", mapRequest.Stream). - Str("node_key", node.NodeKey.ShortString()). - Str("node", node.Hostname). - Err(err). - Msg(msg) - } +) *mapSession { + warnf, tracef, infof, errf := logPollFunc(req, node) + + // 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. + updateChan := make(chan types.StateUpdate, h.cfg.Tuning.NodeMapSessionBufferedChanSize) + + return &mapSession{ + h: h, + ctx: ctx, + req: req, + w: w, + node: node, + capVer: req.Version, + mapper: h.mapper, + + // serving indicates if a client is being served. + serving: false, + + ch: updateChan, + cancelCh: make(chan struct{}), + + // Loggers + warnf: warnf, + infof: infof, + tracef: tracef, + errf: errf, + } +} + +func (m *mapSession) close() { + m.servingMu.Lock() + defer m.servingMu.Unlock() + if !m.serving { + return + } + + select { + case m.cancelCh <- struct{}{}: + default: + } +} + +func (m *mapSession) isStreaming() bool { + return m.req.Stream && !m.req.ReadOnly +} + +func (m *mapSession) isEndpointUpdate() bool { + return !m.req.Stream && !m.req.ReadOnly && m.req.OmitPeers +} + +func (m *mapSession) isReadOnlyUpdate() bool { + return !m.req.Stream && m.req.OmitPeers && m.req.ReadOnly +} + +func (m *mapSession) flush200() { + m.w.WriteHeader(http.StatusOK) + if f, ok := m.w.(http.Flusher); ok { + f.Flush() + } } // handlePoll ensures the node gets the appropriate updates from either // polling or immediate responses. // //nolint:gocyclo -func (h *Headscale) handlePoll( - writer http.ResponseWriter, - ctx context.Context, - node *types.Node, - mapRequest tailcfg.MapRequest, -) { - logTrace, logWarn, logErr := logPollFunc(mapRequest, node) +func (m *mapSession) serve() { + // Register with the notifier if this is a streaming + // session + if m.isStreaming() { + // defers are called in reverse order, + // so top one is executed last. + + // Failover the node's routes if any. + defer m.infof("node has disconnected, mapSession: %p", m) + defer m.pollFailoverRoutes("node closing connection", m.node) + + defer m.h.updateNodeOnlineStatus(false, m.node) + defer m.h.nodeNotifier.RemoveNode(m.node.ID) + + defer func() { + m.servingMu.Lock() + defer m.servingMu.Unlock() + + m.serving = false + close(m.cancelCh) + }() + + m.serving = true + + m.h.nodeNotifier.AddNode(m.node.ID, m.ch) + m.h.updateNodeOnlineStatus(true, m.node) + + m.infof("node has connected, mapSession: %p", m) + } + + // TODO(kradalby): A set todos to harden: + // - func to tell the stream to die, readonly -> false, !stream && omitpeers -> false, true // This is the mechanism where the node gives us information about its // current configuration. @@ -84,473 +170,275 @@ 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. + // + // This is what Tailscale calls a Lite update, the client ignores + // the response and just wants a 200. + // !req.stream && !req.ReadOnly && req.OmitPeers + // // TODO(kradalby): remove ReadOnly when we only support capVer 68+ - if mapRequest.OmitPeers && !mapRequest.Stream && !mapRequest.ReadOnly { - log.Info(). - Caller(). - Bool("readOnly", mapRequest.ReadOnly). - Bool("omitPeers", mapRequest.OmitPeers). - Bool("stream", mapRequest.Stream). - Str("node_key", node.NodeKey.ShortString()). - Str("node", node.Hostname). - Int("cap_ver", int(mapRequest.Version)). - Msg("Received update") + if m.isEndpointUpdate() { + m.handleEndpointUpdate() - change := node.PeerChangeFromMapRequest(mapRequest) + return + } - online := h.nodeNotifier.IsConnected(node.MachineKey) - change.Online = &online + // 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 + // updated and peers will not be notified of changes. + // + // The intended use is for clients to discover the DERP map at + // start-up before their first real endpoint update. + if m.isReadOnlyUpdate() { + m.handleReadOnlyRequest() - 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 - } - - if h.ACLPolicy != nil { - // update routes with peer information - update, err := h.db.EnableAutoApprovedRoutes(h.ACLPolicy, node) - if err != nil { - logErr(err, "Error running auto approved routes") - } - - if update != nil { - sendUpdate = true - } - } - } - - // 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.DB.Save(node).Error; err != nil { - logErr(err, "Failed to persist/update node in the database") - http.Error(writer, "", http.StatusInternalServerError) - - return - } - - // Send an update to all peers to propagate the new routes - // available. - stateUpdate := types.StateUpdate{ - Type: types.StatePeerChanged, - ChangeNodes: types.Nodes{node}, - Message: "called from handlePoll -> update -> new hostinfo", - } - if stateUpdate.Valid() { - ctx := types.NotifyCtx(context.Background(), "poll-nodeupdate-peers-hostinfochange", node.Hostname) - h.nodeNotifier.NotifyWithIgnore( - ctx, - stateUpdate, - node.MachineKey.String()) - } - - // Send an update to the node itself with to ensure it - // has an updated packetfilter allowing the new route - // if it is defined in the ACL. - selfUpdate := types.StateUpdate{ - Type: types.StateSelfUpdate, - ChangeNodes: types.Nodes{node}, - } - if selfUpdate.Valid() { - ctx := types.NotifyCtx(context.Background(), "poll-nodeupdate-self-hostinfochange", node.Hostname) - h.nodeNotifier.NotifyByMachineKey( - ctx, - selfUpdate, - node.MachineKey) - } - - return - } - } - - if err := h.db.DB.Save(node).Error; err != nil { - logErr(err, "Failed to persist/update node in the database") - http.Error(writer, "", http.StatusInternalServerError) + return + } + // From version 68, all streaming requests can be treated as read only. + if m.capVer < 68 { + // Error has been handled/written to client in the func + // return + err := m.handleSaveNode() + if err != nil { return } - - stateUpdate := types.StateUpdate{ - Type: types.StatePeerChangedPatch, - ChangePatches: []*tailcfg.PeerChange{&change}, - } - if stateUpdate.Valid() { - ctx := types.NotifyCtx(context.Background(), "poll-nodeupdate-peers-patch", node.Hostname) - h.nodeNotifier.NotifyWithIgnore( - ctx, - stateUpdate, - node.MachineKey.String()) - } - - writer.WriteHeader(http.StatusOK) - if f, ok := writer.(http.Flusher); ok { - f.Flush() - } - - 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 - // updated and peers will not be notified of changes. - // - // 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) - - return - } else if mapRequest.OmitPeers && mapRequest.Stream { - logErr(nil, "Ignoring request, don't know how to handle it") - - return - } - - 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.DB.Save(node).Error; err != nil { - logErr(err, "Failed to persist/update node in the database") - http.Error(writer, "", http.StatusInternalServerError) - - return } // Set up the client stream - h.pollNetMapStreamWG.Add(1) - defer h.pollNetMapStreamWG.Done() + m.h.pollNetMapStreamWG.Add(1) + defer m.h.pollNetMapStreamWG.Done() - // 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. - chanSize := 3 - if size, ok := envknob.LookupInt("HEADSCALE_TUNING_POLL_QUEUE_SIZE"); ok { - chanSize = size - } - updateChan := make(chan types.StateUpdate, chanSize) - defer closeChanWithLog(updateChan, node.Hostname, "updateChan") + m.pollFailoverRoutes("node connected", m.node) - // Register the node's update channel - h.nodeNotifier.AddNode(node.MachineKey, updateChan) - defer h.nodeNotifier.RemoveNode(node.MachineKey) + keepAliveTicker := time.NewTicker(keepAliveInterval + (time.Duration(rand.IntN(9000)) * time.Millisecond)) - // When a node connects to control, list the peers it has at - // that given point, further updates are kept in memory in - // the Mapper, which lives for the duration of the polling - // session. - peers, err := h.db.ListPeers(node) - if err != nil { - logErr(err, "Failed to list peers when opening poller") - http.Error(writer, "", http.StatusInternalServerError) - - return - } - - isConnected := h.nodeNotifier.ConnectedMap() - for _, peer := range peers { - online := isConnected[peer.MachineKey] - peer.IsOnline = &online - } - - mapp := mapper.NewMapper( - node, - peers, - h.DERPMap, - h.cfg.BaseDomain, - h.cfg.DNSConfig, - h.cfg.LogTail.Enabled, - h.cfg.RandomizeClientPort, - ) - - // update ACLRules with peer informations (to update server tags if necessary) - if h.ACLPolicy != nil { - // update routes with peer information - // This state update is ignored as it will be sent - // as part of the whole node - // TODO(kradalby): figure out if that is actually correct - _, err = h.db.EnableAutoApprovedRoutes(h.ACLPolicy, node) - if err != nil { - logErr(err, "Error running auto approved routes") - } - } - - logTrace("Sending initial map") - - mapResp, err := mapp.FullMapResponse(mapRequest, node, h.ACLPolicy) - if err != nil { - logErr(err, "Failed to create MapResponse") - http.Error(writer, "", http.StatusInternalServerError) - - return - } - - // Send the client an update to make sure we send an initial mapresponse - _, err = writer.Write(mapResp) - if err != nil { - logErr(err, "Could not write the map response") - - return - } - - if flusher, ok := writer.(http.Flusher); ok { - flusher.Flush() - } else { - return - } - - stateUpdate := types.StateUpdate{ - Type: types.StatePeerChanged, - ChangeNodes: types.Nodes{node}, - Message: "called from handlePoll -> new node added", - } - if stateUpdate.Valid() { - ctx := types.NotifyCtx(context.Background(), "poll-newnode-peers", node.Hostname) - h.nodeNotifier.NotifyWithIgnore( - ctx, - stateUpdate, - node.MachineKey.String()) - } - - if len(node.Routes) > 0 { - go h.pollFailoverRoutes(logErr, "new node", node) - } - - keepAliveTicker := time.NewTicker(keepAliveInterval) - - ctx, cancel := context.WithCancel(context.WithValue(ctx, nodeNameContextKey, node.Hostname)) + ctx, cancel := context.WithCancel(context.WithValue(m.ctx, nodeNameContextKey, m.node.Hostname)) defer cancel() + // TODO(kradalby): Make this available through a tuning envvar + wait := time.Second + + // Add a circuit breaker, if the loop is not interrupted + // inbetween listening for the channels, some updates + // might get stale and stucked in the "changed" map + // defined below. + blockBreaker := time.NewTicker(wait) + + // true means changed, false means removed + var changed map[types.NodeID]bool + var patches []*tailcfg.PeerChange + var derp bool + + // Set full to true to immediatly send a full mapresponse + full := true + prev := time.Now() + lastMessage := "" + + // Loop through updates and continuously send them to the + // client. for { - logTrace("Waiting for update on stream channel") - select { - case <-keepAliveTicker.C: - data, err := mapp.KeepAliveResponse(mapRequest, node) - if err != nil { - logErr(err, "Error generating the keep alive msg") - - return - } - _, err = writer.Write(data) - if err != nil { - logErr(err, "Cannot write keep alive message") - - return - } - if flusher, ok := writer.(http.Flusher); ok { - flusher.Flush() - } else { - log.Error().Msg("Failed to create http flusher") - - return - } - - // This goroutine is not ideal, but we have a potential issue here - // where it blocks too long and that holds up updates. - // 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 h.updateNodeOnlineStatus(true, node) - - case update := <-updateChan: - logTrace("Received update") - now := time.Now() - + // If a full update has been requested or there are patches, then send it immediately + // otherwise wait for the "batching" of changes or patches + if full || patches != nil || (changed != nil && time.Since(prev) > wait) { var data []byte var err error // Ensure the node object is updated, for example, there // might have been a hostinfo update in a sidechannel // which contains data needed to generate a map response. - node, err = h.db.GetNodeByMachineKey(node.MachineKey) + m.node, err = m.h.db.GetNodeByID(m.node.ID) if err != nil { - logErr(err, "Could not get machine from db") + m.errf(err, "Could not get machine from db") return } - startMapResp := time.Now() - switch update.Type { - case types.StateFullUpdate: - logTrace("Sending Full MapResponse") + // If there are patches _and_ fully changed nodes, filter the + // patches and remove all patches that are present for the full + // changes updates. This allows us to send them as part of the + // PeerChange update, but only for nodes that are not fully changed. + // The fully changed nodes will be updated from the database and + // have all the updates needed. + // This means that the patches left are for nodes that has no + // updates that requires a full update. + // Patches are not suppose to be mixed in, but can be. + // + // From tailcfg docs: + // These are applied after Peers* above, but in practice the + // control server should only send these on their own, without + // + // Currently, there is no effort to merge patch updates, they + // are all sent, and the client will apply them in order. + // TODO(kradalby): Merge Patches for the same IDs to send less + // data and give the client less work. + if patches != nil && changed != nil { + var filteredPatches []*tailcfg.PeerChange - data, err = mapp.FullMapResponse(mapRequest, node, h.ACLPolicy) - case types.StatePeerChanged: - logTrace(fmt.Sprintf("Sending Changed MapResponse: %s", update.Message)) - - isConnectedMap := h.nodeNotifier.ConnectedMap() - 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 := isConnectedMap[node.MachineKey] - node.IsOnline = &isOnline + for _, patch := range patches { + if _, ok := changed[types.NodeID(patch.NodeID)]; !ok { + filteredPatches = append(filteredPatches, patch) } } - data, err = mapp.PeerChangedResponse(mapRequest, node, update.ChangeNodes, h.ACLPolicy, update.Message) - case types.StatePeerChangedPatch: - logTrace("Sending PeerChangedPatch MapResponse") - data, err = mapp.PeerChangedPatchResponse(mapRequest, node, update.ChangePatches, h.ACLPolicy) - case types.StatePeerRemoved: - logTrace("Sending PeerRemoved MapResponse") - data, err = mapp.PeerRemovedResponse(mapRequest, node, update.Removed) - case types.StateSelfUpdate: - if len(update.ChangeNodes) == 1 { - logTrace("Sending SelfUpdate MapResponse") - node = update.ChangeNodes[0] - data, err = mapp.LiteMapResponse(mapRequest, node, h.ACLPolicy, types.SelfUpdateIdentifier) - } else { - logWarn("SelfUpdate contained too many nodes, this is likely a bug in the code, please report.") - } - case types.StateDERPUpdated: - logTrace("Sending DERPUpdate MapResponse") - data, err = mapp.DERPMapResponse(mapRequest, node, update.DERPMap) + patches = filteredPatches + } + + // When deciding what update to send, the following is considered, + // Full is a superset of all updates, when a full update is requested, + // send only that and move on, all other updates will be present in + // a full map response. + // + // If a map of changed nodes exists, prefer sending that as it will + // contain all the updates for the node, including patches, as it + // is fetched freshly from the database when building the response. + // + // If there is full changes registered, but we have patches for individual + // nodes, send them. + // + // Finally, if a DERP map is the only request, send that alone. + if full { + m.tracef("Sending Full MapResponse") + data, err = m.mapper.FullMapResponse(m.req, m.node, m.h.ACLPolicy, fmt.Sprintf("from mapSession: %p, stream: %t", m, m.isStreaming())) + } else if changed != nil { + m.tracef(fmt.Sprintf("Sending Changed MapResponse: %v", lastMessage)) + data, err = m.mapper.PeerChangedResponse(m.req, m.node, changed, patches, m.h.ACLPolicy, lastMessage) + } else if patches != nil { + m.tracef(fmt.Sprintf("Sending Changed Patch MapResponse: %v", lastMessage)) + data, err = m.mapper.PeerChangedPatchResponse(m.req, m.node, patches, m.h.ACLPolicy) + } else if derp { + m.tracef("Sending DERPUpdate MapResponse") + data, err = m.mapper.DERPMapResponse(m.req, m.node, m.h.DERPMap) } if err != nil { - logErr(err, "Could not get the create map update") + m.errf(err, "Could not get the create map update") return } - log.Trace().Str("node", node.Hostname).TimeDiff("timeSpent", time.Now(), startMapResp).Str("mkey", node.MachineKey.String()).Int("type", int(update.Type)).Msg("finished making map response") + // log.Trace().Str("node", m.node.Hostname).TimeDiff("timeSpent", time.Now(), startMapResp).Str("mkey", m.node.MachineKey.String()).Int("type", int(update.Type)).Msg("finished making map response") // Only send update if there is change if data != nil { startWrite := time.Now() - _, err = writer.Write(data) + _, err = m.w.Write(data) if err != nil { - logErr(err, "Could not write the map response") - - updateRequestsSentToNode.WithLabelValues(node.User.Name, node.Hostname, "failed"). - Inc() + m.errf(err, "Could not write the map response, for mapSession: %p, stream: %t", m, m.isStreaming()) return } - if flusher, ok := writer.(http.Flusher); ok { + if flusher, ok := m.w.(http.Flusher); ok { flusher.Flush() } else { log.Error().Msg("Failed to create http flusher") return } - log.Trace().Str("node", node.Hostname).TimeDiff("timeSpent", time.Now(), startWrite).Str("mkey", node.MachineKey.String()).Int("type", int(update.Type)).Msg("finished writing mapresp to node") + log.Trace().Str("node", m.node.Hostname).TimeDiff("timeSpent", time.Now(), startWrite).Str("mkey", m.node.MachineKey.String()).Msg("finished writing mapresp to node") - log.Debug(). - 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") + m.infof("update sent") } + // reset + changed = nil + patches = nil + lastMessage = "" + full = false + derp = false + prev = time.Now() + } + + // consume channels with update, keep alives or "batch" blocking signals + select { + case <-m.cancelCh: + m.tracef("poll cancelled received") + return case <-ctx.Done(): - logTrace("The client has closed the connection") - - go h.updateNodeOnlineStatus(false, node) - - // Failover the node's routes if any. - go h.pollFailoverRoutes(logErr, "node closing connection", node) - - // The connection has been closed, so we can stop polling. + m.tracef("poll context done") return - case <-h.shutdownChan: - logTrace("The long-poll handler is shutting down") + // Avoid infinite block that would potentially leave + // some updates in the changed map. + case <-blockBreaker.C: + continue - return + // Consume all updates sent to node + case update := <-m.ch: + m.tracef("received stream update: %d %s", update.Type, update.Message) + + switch update.Type { + case types.StateFullUpdate: + full = true + case types.StatePeerChanged: + if changed == nil { + changed = make(map[types.NodeID]bool) + } + + for _, nodeID := range update.ChangeNodes { + changed[nodeID] = true + } + + lastMessage = update.Message + case types.StatePeerChangedPatch: + patches = append(patches, update.ChangePatches...) + case types.StatePeerRemoved: + if changed == nil { + changed = make(map[types.NodeID]bool) + } + + for _, nodeID := range update.Removed { + changed[nodeID] = false + } + case types.StateSelfUpdate: + // create the map so an empty (self) update is sent + if changed == nil { + changed = make(map[types.NodeID]bool) + } + + lastMessage = update.Message + case types.StateDERPUpdated: + derp = true + } + + case <-keepAliveTicker.C: + data, err := m.mapper.KeepAliveResponse(m.req, m.node) + if err != nil { + m.errf(err, "Error generating the keep alive msg") + + return + } + _, err = m.w.Write(data) + if err != nil { + m.errf(err, "Cannot write keep alive message") + + return + } + if flusher, ok := m.w.(http.Flusher); ok { + flusher.Flush() + } else { + log.Error().Msg("Failed to create http flusher") + + return + } } } } -func (h *Headscale) pollFailoverRoutes(logErr func(error, string), where string, node *types.Node) { - update, err := db.Write(h.db.DB, func(tx *gorm.DB) (*types.StateUpdate, error) { - return db.EnsureFailoverRouteIsAvailable(tx, h.nodeNotifier.ConnectedMap(), node) +func (m *mapSession) pollFailoverRoutes(where string, node *types.Node) { + update, err := db.Write(m.h.db.DB, func(tx *gorm.DB) (*types.StateUpdate, error) { + return db.FailoverRouteIfAvailable(tx, m.h.nodeNotifier.ConnectedMap(), node) }) if err != nil { - logErr(err, fmt.Sprintf("failed to ensure failover routes, %s", where)) + m.errf(err, fmt.Sprintf("failed to ensure failover routes, %s", where)) return } - if update != nil && !update.Empty() && update.Valid() { + if update != nil && !update.Empty() { ctx := types.NotifyCtx(context.Background(), fmt.Sprintf("poll-%s-routes-ensurefailover", strings.ReplaceAll(where, " ", "-")), node.Hostname) - h.nodeNotifier.NotifyWithIgnore(ctx, *update, node.MachineKey.String()) + m.h.nodeNotifier.NotifyWithIgnore(ctx, *update, node.ID) } } @@ -558,33 +446,35 @@ func (h *Headscale) pollFailoverRoutes(logErr func(error, string), where string, // 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() + change := &tailcfg.PeerChange{ + NodeID: tailcfg.NodeID(node.ID), + Online: &online, + } - node.LastSeen = &now + if !online { + now := time.Now() - statusUpdate := types.StateUpdate{ + // lastSeen is only relevant if the node is disconnected. + node.LastSeen = &now + change.LastSeen = &now + + err := h.db.DB.Transaction(func(tx *gorm.DB) error { + return db.SetLastSeen(tx, node.ID, *node.LastSeen) + }) + if err != nil { + log.Error().Err(err).Msg("Cannot update node LastSeen") + + return + } + } + + ctx := types.NotifyCtx(context.Background(), "poll-nodeupdate-onlinestatus", node.Hostname) + h.nodeNotifier.NotifyWithIgnore(ctx, types.StateUpdate{ Type: types.StatePeerChangedPatch, ChangePatches: []*tailcfg.PeerChange{ - { - NodeID: tailcfg.NodeID(node.ID), - Online: &online, - LastSeen: &now, - }, + change, }, - } - if statusUpdate.Valid() { - ctx := types.NotifyCtx(context.Background(), "poll-nodeupdate-onlinestatus", node.Hostname) - h.nodeNotifier.NotifyWithIgnore(ctx, statusUpdate, node.MachineKey.String()) - } - - err := h.db.DB.Transaction(func(tx *gorm.DB) error { - return db.UpdateLastSeen(tx, node.ID, *node.LastSeen) - }) - if err != nil { - log.Error().Err(err).Msg("Cannot update node LastSeen") - - return - } + }, node.ID) } func closeChanWithLog[C chan []byte | chan struct{} | chan types.StateUpdate](channel C, node, name string) { @@ -597,43 +487,178 @@ func closeChanWithLog[C chan []byte | chan struct{} | chan types.StateUpdate](ch close(channel) } -func (h *Headscale) handleLiteRequest( - writer http.ResponseWriter, - node *types.Node, - mapRequest tailcfg.MapRequest, -) { - logTrace, _, logErr := logPollFunc(mapRequest, node) +func (m *mapSession) handleEndpointUpdate() { + m.tracef("received endpoint update") - mapp := mapper.NewMapper( - node, - types.Nodes{}, - h.DERPMap, - h.cfg.BaseDomain, - h.cfg.DNSConfig, - h.cfg.LogTail.Enabled, - h.cfg.RandomizeClientPort, - ) + change := m.node.PeerChangeFromMapRequest(m.req) - logTrace("Client asked for a lite update, responding without peers") + online := m.h.nodeNotifier.IsLikelyConnected(m.node.ID) + change.Online = &online - mapResp, err := mapp.LiteMapResponse(mapRequest, node, h.ACLPolicy) - if err != nil { - logErr(err, "Failed to create MapResponse") - http.Error(writer, "", http.StatusInternalServerError) + m.node.ApplyPeerChange(&change) + + sendUpdate, routesChanged := hostInfoChanged(m.node.Hostinfo, m.req.Hostinfo) + m.node.Hostinfo = m.req.Hostinfo + + logTracePeerChange(m.node.Hostname, sendUpdate, &change) + + // If there is no changes and nothing to save, + // return early. + if peerChangeEmpty(change) && !sendUpdate { + return + } + + // Check if the Hostinfo of the node has changed. + // If it has changed, check if there has been a change to + // 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 routesChanged { + var err error + _, err = m.h.db.SaveNodeRoutes(m.node) + if err != nil { + m.errf(err, "Error processing node routes") + http.Error(m.w, "", http.StatusInternalServerError) + + return + } + + if m.h.ACLPolicy != nil { + // update routes with peer information + err := m.h.db.EnableAutoApprovedRoutes(m.h.ACLPolicy, m.node) + if err != nil { + m.errf(err, "Error running auto approved routes") + } + } + + // Send an update to the node itself with to ensure it + // has an updated packetfilter allowing the new route + // if it is defined in the ACL. + ctx := types.NotifyCtx(context.Background(), "poll-nodeupdate-self-hostinfochange", m.node.Hostname) + m.h.nodeNotifier.NotifyByMachineKey( + ctx, + types.StateUpdate{ + Type: types.StateSelfUpdate, + ChangeNodes: []types.NodeID{m.node.ID}, + }, + m.node.ID) + + } + + if err := m.h.db.DB.Save(m.node).Error; err != nil { + m.errf(err, "Failed to persist/update node in the database") + http.Error(m.w, "", http.StatusInternalServerError) return } - writer.Header().Set("Content-Type", "application/json; charset=utf-8") - writer.WriteHeader(http.StatusOK) - _, err = writer.Write(mapResp) - if err != nil { - logErr(err, "Failed to write response") + ctx := types.NotifyCtx(context.Background(), "poll-nodeupdate-peers-patch", m.node.Hostname) + m.h.nodeNotifier.NotifyWithIgnore( + ctx, + types.StateUpdate{ + Type: types.StatePeerChanged, + ChangeNodes: []types.NodeID{m.node.ID}, + Message: "called from handlePoll -> update", + }, + m.node.ID) + + m.flush200() + + return +} + +// handleSaveNode saves node updates in the maprequest _streaming_ +// path and is mostly the same code as in handleEndpointUpdate. +// It is not attempted to be deduplicated since it will go away +// when we stop supporting older than 68 which removes updates +// when the node is streaming. +func (m *mapSession) handleSaveNode() error { + m.tracef("saving node update from stream session") + + change := m.node.PeerChangeFromMapRequest(m.req) + + // A stream is being set up, the node is Online + online := true + change.Online = &online + + m.node.ApplyPeerChange(&change) + + sendUpdate, routesChanged := hostInfoChanged(m.node.Hostinfo, m.req.Hostinfo) + m.node.Hostinfo = m.req.Hostinfo + + // If there is no changes and nothing to save, + // return early. + if peerChangeEmpty(change) || !sendUpdate { + return nil } + + // Check if the Hostinfo of the node has changed. + // If it has changed, check if there has been a change to + // 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 routesChanged { + var err error + _, err = m.h.db.SaveNodeRoutes(m.node) + if err != nil { + return err + } + + if m.h.ACLPolicy != nil { + // update routes with peer information + err := m.h.db.EnableAutoApprovedRoutes(m.h.ACLPolicy, m.node) + if err != nil { + return err + } + } + } + + if err := m.h.db.DB.Save(m.node).Error; err != nil { + return err + } + + ctx := types.NotifyCtx(context.Background(), "pre-68-update-while-stream", m.node.Hostname) + m.h.nodeNotifier.NotifyWithIgnore( + ctx, + types.StateUpdate{ + Type: types.StatePeerChanged, + ChangeNodes: []types.NodeID{m.node.ID}, + Message: "called from handlePoll -> pre-68-update-while-stream", + }, + m.node.ID) + + return nil +} + +func (m *mapSession) handleReadOnlyRequest() { + m.tracef("Client asked for a lite update, responding without peers") + + mapResp, err := m.mapper.ReadOnlyMapResponse(m.req, m.node, m.h.ACLPolicy) + if err != nil { + m.errf(err, "Failed to create MapResponse") + http.Error(m.w, "", http.StatusInternalServerError) + + return + } + + m.w.Header().Set("Content-Type", "application/json; charset=utf-8") + m.w.WriteHeader(http.StatusOK) + _, err = m.w.Write(mapResp) + if err != nil { + m.errf(err, "Failed to write response") + } + + m.flush200() } func logTracePeerChange(hostname string, hostinfoChange bool, change *tailcfg.PeerChange) { - trace := log.Trace().Str("node_id", change.NodeID.String()).Str("hostname", hostname) + trace := log.Trace().Uint64("node.id", uint64(change.NodeID)).Str("hostname", hostname) if change.Key != nil { trace = trace.Str("node_key", change.Key.ShortString()) @@ -666,3 +691,114 @@ func logTracePeerChange(hostname string, hostinfoChange bool, change *tailcfg.Pe trace.Time("last_seen", *change.LastSeen).Msg("PeerChange received") } + +func peerChangeEmpty(chng tailcfg.PeerChange) bool { + return chng.Key == nil && + chng.DiscoKey == nil && + chng.Online == nil && + chng.Endpoints == nil && + chng.DERPRegion == 0 && + chng.LastSeen == nil && + chng.KeyExpiry == nil +} + +func logPollFunc( + mapRequest tailcfg.MapRequest, + node *types.Node, +) (func(string, ...any), func(string, ...any), func(string, ...any), func(error, string, ...any)) { + return func(msg string, a ...any) { + log.Warn(). + Caller(). + Bool("readOnly", mapRequest.ReadOnly). + Bool("omitPeers", mapRequest.OmitPeers). + Bool("stream", mapRequest.Stream). + Uint64("node.id", node.ID.Uint64()). + Str("node", node.Hostname). + Msgf(msg, a...) + }, + func(msg string, a ...any) { + log.Info(). + Caller(). + Bool("readOnly", mapRequest.ReadOnly). + Bool("omitPeers", mapRequest.OmitPeers). + Bool("stream", mapRequest.Stream). + Uint64("node.id", node.ID.Uint64()). + Str("node", node.Hostname). + Msgf(msg, a...) + }, + func(msg string, a ...any) { + log.Trace(). + Caller(). + Bool("readOnly", mapRequest.ReadOnly). + Bool("omitPeers", mapRequest.OmitPeers). + Bool("stream", mapRequest.Stream). + Uint64("node.id", node.ID.Uint64()). + Str("node", node.Hostname). + Msgf(msg, a...) + }, + func(err error, msg string, a ...any) { + log.Error(). + Caller(). + Bool("readOnly", mapRequest.ReadOnly). + Bool("omitPeers", mapRequest.OmitPeers). + Bool("stream", mapRequest.Stream). + Uint64("node.id", node.ID.Uint64()). + Str("node", node.Hostname). + Err(err). + Msgf(msg, a...) + } +} + +// hostInfoChanged reports if hostInfo has changed in two ways, +// - first bool reports if an update needs to be sent to nodes +// - second reports if there has been changes to routes +// the caller can then use this info to save and update nodes +// and routes as needed. +func hostInfoChanged(old, new *tailcfg.Hostinfo) (bool, bool) { + if old.Equal(new) { + return false, false + } + + // Routes + oldRoutes := old.RoutableIPs + newRoutes := new.RoutableIPs + + sort.Slice(oldRoutes, func(i, j int) bool { + return comparePrefix(oldRoutes[i], oldRoutes[j]) > 0 + }) + sort.Slice(newRoutes, func(i, j int) bool { + return comparePrefix(newRoutes[i], newRoutes[j]) > 0 + }) + + if !xslices.Equal(oldRoutes, newRoutes) { + return true, true + } + + // 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 len(old.Services) != len(new.Services) { + return true, false + } + + return false, false +} + +// TODO(kradalby): Remove after go 1.23, will be in stdlib. +// Compare returns an integer comparing two prefixes. +// The result will be 0 if p == p2, -1 if p < p2, and +1 if p > p2. +// Prefixes sort first by validity (invalid before valid), then +// address family (IPv4 before IPv6), then prefix length, then +// address. +func comparePrefix(p, p2 netip.Prefix) int { + if c := cmp.Compare(p.Addr().BitLen(), p2.Addr().BitLen()); c != 0 { + return c + } + if c := cmp.Compare(p.Bits(), p2.Bits()); c != 0 { + return c + } + return p.Addr().Compare(p2.Addr()) +} diff --git a/hscontrol/poll_noise.go b/hscontrol/poll_noise.go deleted file mode 100644 index 53b1d47e..00000000 --- a/hscontrol/poll_noise.go +++ /dev/null @@ -1,96 +0,0 @@ -package hscontrol - -import ( - "encoding/json" - "errors" - "io" - "net/http" - - "github.com/rs/zerolog/log" - "gorm.io/gorm" - "tailscale.com/tailcfg" - "tailscale.com/types/key" -) - -const ( - MinimumCapVersion tailcfg.CapabilityVersion = 58 -) - -// 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 -// 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 (ns *noiseServer) NoisePollNetMapHandler( - writer http.ResponseWriter, - req *http.Request, -) { - log.Trace(). - Str("handler", "NoisePollNetMap"). - Msg("PollNetMapHandler called") - - log.Trace(). - Any("headers", req.Header). - Caller(). - Msg("Headers") - - body, _ := io.ReadAll(req.Body) - - mapRequest := tailcfg.MapRequest{} - if err := json.Unmarshal(body, &mapRequest); err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Cannot parse MapRequest") - http.Error(writer, "Internal error", http.StatusInternalServerError) - - 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( - ns.conn.Peer(), - mapRequest.NodeKey, - key.NodePublic{}, - ) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - log.Warn(). - Str("handler", "NoisePollNetMap"). - Msgf("Ignoring request, cannot find node with key %s", mapRequest.NodeKey.String()) - http.Error(writer, "Internal error", http.StatusNotFound) - - return - } - log.Error(). - Str("handler", "NoisePollNetMap"). - Msgf("Failed to fetch node from the database with node key: %s", mapRequest.NodeKey.String()) - http.Error(writer, "Internal error", http.StatusInternalServerError) - - return - } - log.Debug(). - Str("handler", "NoisePollNetMap"). - Str("node", node.Hostname). - Int("cap_ver", int(mapRequest.Version)). - Msg("A node sending a MapRequest with Noise protocol") - - ns.headscale.handlePoll(writer, req.Context(), node, mapRequest) -} diff --git a/hscontrol/types/common.go b/hscontrol/types/common.go index ceeceea0..6d63f301 100644 --- a/hscontrol/types/common.go +++ b/hscontrol/types/common.go @@ -90,6 +90,25 @@ func (i StringList) Value() (driver.Value, error) { type StateUpdateType int +func (su StateUpdateType) String() string { + switch su { + case StateFullUpdate: + return "StateFullUpdate" + case StatePeerChanged: + return "StatePeerChanged" + case StatePeerChangedPatch: + return "StatePeerChangedPatch" + case StatePeerRemoved: + return "StatePeerRemoved" + case StateSelfUpdate: + return "StateSelfUpdate" + case StateDERPUpdated: + return "StateDERPUpdated" + } + + return "unknown state update type" +} + const ( StateFullUpdate StateUpdateType = iota // StatePeerChanged is used for updates that needs @@ -118,7 +137,7 @@ type StateUpdate struct { // ChangeNodes must be set when Type is StatePeerAdded // and StatePeerChanged and contains the full node // object for added nodes. - ChangeNodes Nodes + ChangeNodes []NodeID // ChangePatches must be set when Type is StatePeerChangedPatch // and contains a populated PeerChange object. @@ -127,7 +146,7 @@ type StateUpdate struct { // Removed must be set when Type is StatePeerRemoved and // contain a list of the nodes that has been removed from // the network. - Removed []tailcfg.NodeID + Removed []NodeID // DERPMap must be set when Type is StateDERPUpdated and // contain the new DERP Map. @@ -138,39 +157,6 @@ type StateUpdate struct { 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 -} - // Empty reports if there are any updates in the StateUpdate. func (su *StateUpdate) Empty() bool { switch su.Type { @@ -185,12 +171,12 @@ func (su *StateUpdate) Empty() bool { return false } -func StateUpdateExpire(nodeID uint64, expiry time.Time) StateUpdate { +func StateUpdateExpire(nodeID NodeID, expiry time.Time) StateUpdate { return StateUpdate{ Type: StatePeerChangedPatch, ChangePatches: []*tailcfg.PeerChange{ { - NodeID: tailcfg.NodeID(nodeID), + NodeID: nodeID.NodeID(), KeyExpiry: &expiry, }, }, diff --git a/hscontrol/types/config.go b/hscontrol/types/config.go index 022d1279..4e4b9a61 100644 --- a/hscontrol/types/config.go +++ b/hscontrol/types/config.go @@ -69,6 +69,8 @@ type Config struct { CLI CLIConfig ACL ACLConfig + + Tuning Tuning } type SqliteConfig struct { @@ -161,6 +163,11 @@ type LogConfig struct { Level zerolog.Level } +type Tuning struct { + BatchChangeDelay time.Duration + NodeMapSessionBufferedChanSize int +} + func LoadConfig(path string, isFile bool) error { if isFile { viper.SetConfigFile(path) @@ -220,6 +227,9 @@ func LoadConfig(path string, isFile bool) error { viper.SetDefault("node_update_check_interval", "10s") + viper.SetDefault("tuning.batch_change_delay", "800ms") + viper.SetDefault("tuning.node_mapsession_buffered_chan_size", 30) + if IsCLIConfigured() { return nil } @@ -719,6 +729,12 @@ func GetHeadscaleConfig() (*Config, error) { }, Log: GetLogConfig(), + + // TODO(kradalby): Document these settings when more stable + Tuning: Tuning{ + BatchChangeDelay: viper.GetDuration("tuning.batch_change_delay"), + NodeMapSessionBufferedChanSize: viper.GetInt("tuning.node_mapsession_buffered_chan_size"), + }, }, nil } diff --git a/hscontrol/types/node.go b/hscontrol/types/node.go index 69004bfd..2d6c6310 100644 --- a/hscontrol/types/node.go +++ b/hscontrol/types/node.go @@ -7,11 +7,13 @@ import ( "fmt" "net/netip" "sort" + "strconv" "strings" "time" 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" @@ -27,9 +29,24 @@ var ( ErrNodeUserHasNoName = errors.New("node user has no name") ) +type NodeID uint64 +type NodeConnectedMap map[NodeID]bool + +func (id NodeID) StableID() tailcfg.StableNodeID { + return tailcfg.StableNodeID(strconv.FormatUint(uint64(id), util.Base10)) +} + +func (id NodeID) NodeID() tailcfg.NodeID { + return tailcfg.NodeID(id) +} + +func (id NodeID) Uint64() uint64 { + return uint64(id) +} + // Node is a Headscale client. type Node struct { - ID uint64 `gorm:"primary_key"` + ID NodeID `gorm:"primary_key"` // MachineKeyDatabaseField is the string representation of MachineKey // it is _only_ used for reading and writing the key to the @@ -198,7 +215,7 @@ func (node Node) IsExpired() bool { return false } - return time.Now().UTC().After(*node.Expiry) + return time.Since(*node.Expiry) > 0 } // IsEphemeral returns if the node is registered as an Ephemeral node. @@ -319,7 +336,7 @@ func (node *Node) AfterFind(tx *gorm.DB) error { func (node *Node) Proto() *v1.Node { nodeProto := &v1.Node{ - Id: node.ID, + Id: uint64(node.ID), MachineKey: node.MachineKey.String(), NodeKey: node.NodeKey.String(), @@ -486,8 +503,8 @@ func (nodes Nodes) String() string { return fmt.Sprintf("[ %s ](%d)", strings.Join(temp, ", "), len(temp)) } -func (nodes Nodes) IDMap() map[uint64]*Node { - ret := map[uint64]*Node{} +func (nodes Nodes) IDMap() map[NodeID]*Node { + ret := map[NodeID]*Node{} for _, node := range nodes { ret[node.ID] = node diff --git a/integration/auth_oidc_test.go b/integration/auth_oidc_test.go index 36e74a8d..347dbcc1 100644 --- a/integration/auth_oidc_test.go +++ b/integration/auth_oidc_test.go @@ -83,7 +83,7 @@ func TestOIDCAuthenticationPingAll(t *testing.T) { err = scenario.WaitForTailscaleSync() assertNoErrSync(t, err) - assertClientsState(t, allClients) + // assertClientsState(t, allClients) allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string { return x.String() @@ -142,7 +142,7 @@ func TestOIDCExpireNodesBasedOnTokenExpiry(t *testing.T) { err = scenario.WaitForTailscaleSync() assertNoErrSync(t, err) - assertClientsState(t, allClients) + // assertClientsState(t, allClients) allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string { return x.String() diff --git a/integration/auth_web_flow_test.go b/integration/auth_web_flow_test.go index aa589fac..6d981bc1 100644 --- a/integration/auth_web_flow_test.go +++ b/integration/auth_web_flow_test.go @@ -53,7 +53,7 @@ func TestAuthWebFlowAuthenticationPingAll(t *testing.T) { err = scenario.WaitForTailscaleSync() assertNoErrSync(t, err) - assertClientsState(t, allClients) + // assertClientsState(t, allClients) allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string { return x.String() @@ -92,7 +92,7 @@ func TestAuthWebFlowLogoutAndRelogin(t *testing.T) { err = scenario.WaitForTailscaleSync() assertNoErrSync(t, err) - assertClientsState(t, allClients) + // assertClientsState(t, allClients) allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string { return x.String() diff --git a/integration/general_test.go b/integration/general_test.go index 9aae26fc..975b4c21 100644 --- a/integration/general_test.go +++ b/integration/general_test.go @@ -65,7 +65,7 @@ func TestPingAllByIP(t *testing.T) { err = scenario.WaitForTailscaleSync() assertNoErrSync(t, err) - assertClientsState(t, allClients) + // assertClientsState(t, allClients) allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string { return x.String() @@ -103,7 +103,7 @@ func TestPingAllByIPPublicDERP(t *testing.T) { err = scenario.WaitForTailscaleSync() assertNoErrSync(t, err) - assertClientsState(t, allClients) + // assertClientsState(t, allClients) allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string { return x.String() @@ -135,7 +135,7 @@ func TestAuthKeyLogoutAndRelogin(t *testing.T) { err = scenario.WaitForTailscaleSync() assertNoErrSync(t, err) - assertClientsState(t, allClients) + // assertClientsState(t, allClients) clientIPs := make(map[TailscaleClient][]netip.Addr) for _, client := range allClients { @@ -176,7 +176,7 @@ func TestAuthKeyLogoutAndRelogin(t *testing.T) { err = scenario.WaitForTailscaleSync() assertNoErrSync(t, err) - assertClientsState(t, allClients) + // assertClientsState(t, allClients) allClients, err = scenario.ListTailscaleClients() assertNoErrListClients(t, err) @@ -329,7 +329,7 @@ func TestPingAllByHostname(t *testing.T) { err = scenario.WaitForTailscaleSync() assertNoErrSync(t, err) - assertClientsState(t, allClients) + // assertClientsState(t, allClients) allHostnames, err := scenario.ListTailscaleClientsFQDNs() assertNoErrListFQDN(t, err) @@ -539,7 +539,7 @@ func TestResolveMagicDNS(t *testing.T) { err = scenario.WaitForTailscaleSync() assertNoErrSync(t, err) - assertClientsState(t, allClients) + // assertClientsState(t, allClients) // Poor mans cache _, err = scenario.ListTailscaleClientsFQDNs() @@ -609,7 +609,7 @@ func TestExpireNode(t *testing.T) { err = scenario.WaitForTailscaleSync() assertNoErrSync(t, err) - assertClientsState(t, allClients) + // assertClientsState(t, allClients) allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string { return x.String() @@ -711,7 +711,7 @@ func TestExpireNode(t *testing.T) { } } -func TestNodeOnlineLastSeenStatus(t *testing.T) { +func TestNodeOnlineStatus(t *testing.T) { IntegrationSkip(t) t.Parallel() @@ -723,7 +723,7 @@ func TestNodeOnlineLastSeenStatus(t *testing.T) { "user1": len(MustTestVersions), } - err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("onlinelastseen")) + err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("online")) assertNoErrHeadscaleEnv(t, err) allClients, err := scenario.ListTailscaleClients() @@ -735,7 +735,7 @@ func TestNodeOnlineLastSeenStatus(t *testing.T) { err = scenario.WaitForTailscaleSync() assertNoErrSync(t, err) - assertClientsState(t, allClients) + // assertClientsState(t, allClients) allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string { return x.String() @@ -755,8 +755,6 @@ func TestNodeOnlineLastSeenStatus(t *testing.T) { 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() @@ -780,11 +778,6 @@ func TestNodeOnlineLastSeenStatus(t *testing.T) { 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 @@ -795,18 +788,6 @@ func TestNodeOnlineLastSeenStatus(t *testing.T) { 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), - "node (%s) lastSeen (%v) was not %s after the threshold (%v)", - node.GetName(), - lastSeen, - keepAliveInterval, - lastSeenThreshold, - ) } // Verify that all nodes report all nodes to be online @@ -834,15 +815,6 @@ func TestNodeOnlineLastSeenStatus(t *testing.T) { 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, - // ) } } @@ -850,3 +822,87 @@ func TestNodeOnlineLastSeenStatus(t *testing.T) { time.Sleep(time.Second) } } + +// TestPingAllByIPManyUpDown is a variant of the PingAll +// test which will take the tailscale node up and down +// five times ensuring they are able to restablish connectivity. +func TestPingAllByIPManyUpDown(t *testing.T) { + IntegrationSkip(t) + t.Parallel() + + scenario, err := NewScenario() + assertNoErr(t, err) + defer scenario.Shutdown() + + // TODO(kradalby): it does not look like the user thing works, only second + // get created? maybe only when many? + spec := map[string]int{ + "user1": len(MustTestVersions), + "user2": len(MustTestVersions), + } + + headscaleConfig := map[string]string{ + "HEADSCALE_DERP_URLS": "", + "HEADSCALE_DERP_SERVER_ENABLED": "true", + "HEADSCALE_DERP_SERVER_REGION_ID": "999", + "HEADSCALE_DERP_SERVER_REGION_CODE": "headscale", + "HEADSCALE_DERP_SERVER_REGION_NAME": "Headscale Embedded DERP", + "HEADSCALE_DERP_SERVER_STUN_LISTEN_ADDR": "0.0.0.0:3478", + "HEADSCALE_DERP_SERVER_PRIVATE_KEY_PATH": "/tmp/derp.key", + + // Envknob for enabling DERP debug logs + "DERP_DEBUG_LOGS": "true", + "DERP_PROBER_DEBUG_LOGS": "true", + } + + err = scenario.CreateHeadscaleEnv(spec, + []tsic.Option{}, + hsic.WithTestName("pingallbyip"), + hsic.WithConfigEnv(headscaleConfig), + hsic.WithTLS(), + hsic.WithHostnameAsServerURL(), + ) + assertNoErrHeadscaleEnv(t, err) + + allClients, err := scenario.ListTailscaleClients() + assertNoErrListClients(t, err) + + allIps, err := scenario.ListTailscaleClientsIPs() + assertNoErrListClientIPs(t, err) + + err = scenario.WaitForTailscaleSync() + assertNoErrSync(t, err) + + // assertClientsState(t, allClients) + + allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string { + return x.String() + }) + + success := pingAllHelper(t, allClients, allAddrs) + t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps)) + + for run := range 3 { + t.Logf("Starting DownUpPing run %d", run+1) + + for _, client := range allClients { + t.Logf("taking down %q", client.Hostname()) + client.Down() + } + + time.Sleep(5 * time.Second) + + for _, client := range allClients { + t.Logf("bringing up %q", client.Hostname()) + client.Up() + } + + time.Sleep(5 * time.Second) + + err = scenario.WaitForTailscaleSync() + assertNoErrSync(t, err) + + success := pingAllHelper(t, allClients, allAddrs) + t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps)) + } +} diff --git a/integration/route_test.go b/integration/route_test.go index 75296fd5..d185acff 100644 --- a/integration/route_test.go +++ b/integration/route_test.go @@ -212,7 +212,11 @@ func TestEnablingRoutes(t *testing.T) { if route.GetId() == routeToBeDisabled.GetId() { assert.Equal(t, false, route.GetEnabled()) - assert.Equal(t, false, route.GetIsPrimary()) + + // since this is the only route of this cidr, + // it will not failover, and remain Primary + // until something can replace it. + assert.Equal(t, true, route.GetIsPrimary()) } else { assert.Equal(t, true, route.GetEnabled()) assert.Equal(t, true, route.GetIsPrimary()) @@ -291,6 +295,7 @@ func TestHASubnetRouterFailover(t *testing.T) { client := allClients[2] + t.Logf("Advertise route from r1 (%s) and r2 (%s), making it HA, n1 is primary", subRouter1.Hostname(), subRouter2.Hostname()) // advertise HA route on node 1 and 2 // ID 1 will be primary // ID 2 will be secondary @@ -384,12 +389,12 @@ func TestHASubnetRouterFailover(t *testing.T) { // 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()) + assert.Equal(t, true, enablingRoutes[0].GetIsPrimary(), "both subnet routers are up, expected r1 to be primary") // 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()) + assert.Equal(t, false, enablingRoutes[1].GetIsPrimary(), "both subnet routers are up, expected r2 to be non-primary") // Verify that the client has routes from the primary machine srs1, err := subRouter1.Status() @@ -401,6 +406,9 @@ func TestHASubnetRouterFailover(t *testing.T) { srs1PeerStatus := clientStatus.Peer[srs1.Self.PublicKey] srs2PeerStatus := clientStatus.Peer[srs2.Self.PublicKey] + assert.True(t, srs1PeerStatus.Online, "r1 up, r2 up") + assert.True(t, srs2PeerStatus.Online, "r1 up, r2 up") + assertNotNil(t, srs1PeerStatus.PrimaryRoutes) assert.Nil(t, srs2PeerStatus.PrimaryRoutes) @@ -411,7 +419,8 @@ func TestHASubnetRouterFailover(t *testing.T) { ) // Take down the current primary - t.Logf("taking down subnet router 1 (%s)", subRouter1.Hostname()) + t.Logf("taking down subnet router r1 (%s)", subRouter1.Hostname()) + t.Logf("expecting r2 (%s) to take over as primary", subRouter2.Hostname()) err = subRouter1.Down() assertNoErr(t, err) @@ -435,15 +444,12 @@ func TestHASubnetRouterFailover(t *testing.T) { // 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()) + assert.Equal(t, false, routesAfterMove[0].GetIsPrimary(), "r1 is down, expected r2 to be primary") // 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 + assert.Equal(t, true, routesAfterMove[1].GetIsPrimary(), "r1 is down, expected r2 to be primary") srs2, err = subRouter2.Status() @@ -453,6 +459,9 @@ func TestHASubnetRouterFailover(t *testing.T) { srs1PeerStatus = clientStatus.Peer[srs1.Self.PublicKey] srs2PeerStatus = clientStatus.Peer[srs2.Self.PublicKey] + assert.False(t, srs1PeerStatus.Online, "r1 down, r2 down") + assert.True(t, srs2PeerStatus.Online, "r1 down, r2 up") + assert.Nil(t, srs1PeerStatus.PrimaryRoutes) assertNotNil(t, srs2PeerStatus.PrimaryRoutes) @@ -465,7 +474,8 @@ func TestHASubnetRouterFailover(t *testing.T) { } // Take down subnet router 2, leaving none available - t.Logf("taking down subnet router 2 (%s)", subRouter2.Hostname()) + t.Logf("taking down subnet router r2 (%s)", subRouter2.Hostname()) + t.Logf("expecting r2 (%s) to remain primary, no other available", subRouter2.Hostname()) err = subRouter2.Down() assertNoErr(t, err) @@ -489,14 +499,14 @@ func TestHASubnetRouterFailover(t *testing.T) { // 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()) + assert.Equal(t, false, routesAfterBothDown[0].GetIsPrimary(), "r1 and r2 is down, expected r2 to _still_ be primary") // 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()) + assert.Equal(t, true, routesAfterBothDown[1].GetIsPrimary(), "r1 and r2 is down, expected r2 to _still_ be primary") // TODO(kradalby): Check client status // Both are expected to be down @@ -508,6 +518,9 @@ func TestHASubnetRouterFailover(t *testing.T) { srs1PeerStatus = clientStatus.Peer[srs1.Self.PublicKey] srs2PeerStatus = clientStatus.Peer[srs2.Self.PublicKey] + assert.False(t, srs1PeerStatus.Online, "r1 down, r2 down") + assert.False(t, srs2PeerStatus.Online, "r1 down, r2 down") + assert.Nil(t, srs1PeerStatus.PrimaryRoutes) assertNotNil(t, srs2PeerStatus.PrimaryRoutes) @@ -520,7 +533,8 @@ func TestHASubnetRouterFailover(t *testing.T) { } // Bring up subnet router 1, making the route available from there. - t.Logf("bringing up subnet router 1 (%s)", subRouter1.Hostname()) + t.Logf("bringing up subnet router r1 (%s)", subRouter1.Hostname()) + t.Logf("expecting r1 (%s) to take over as primary (only one online)", subRouter1.Hostname()) err = subRouter1.Up() assertNoErr(t, err) @@ -544,12 +558,12 @@ func TestHASubnetRouterFailover(t *testing.T) { // 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()) + assert.Equal(t, true, routesAfter1Up[0].GetIsPrimary(), "r1 is back up, expected r1 to become be primary") // 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()) + assert.Equal(t, false, routesAfter1Up[1].GetIsPrimary(), "r1 is back up, expected r1 to become be primary") // Verify that the route is announced from subnet router 1 clientStatus, err = client.Status() @@ -558,6 +572,9 @@ func TestHASubnetRouterFailover(t *testing.T) { srs1PeerStatus = clientStatus.Peer[srs1.Self.PublicKey] srs2PeerStatus = clientStatus.Peer[srs2.Self.PublicKey] + assert.True(t, srs1PeerStatus.Online, "r1 is back up, r2 down") + assert.False(t, srs2PeerStatus.Online, "r1 is back up, r2 down") + assert.NotNil(t, srs1PeerStatus.PrimaryRoutes) assert.Nil(t, srs2PeerStatus.PrimaryRoutes) @@ -570,7 +587,8 @@ func TestHASubnetRouterFailover(t *testing.T) { } // Bring up subnet router 2, should result in no change. - t.Logf("bringing up subnet router 2 (%s)", subRouter2.Hostname()) + t.Logf("bringing up subnet router r2 (%s)", subRouter2.Hostname()) + t.Logf("both online, expecting r1 (%s) to still be primary (no flapping)", subRouter1.Hostname()) err = subRouter2.Up() assertNoErr(t, err) @@ -594,12 +612,12 @@ func TestHASubnetRouterFailover(t *testing.T) { // 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()) + assert.Equal(t, true, routesAfter2Up[0].GetIsPrimary(), "r1 and r2 is back up, expected r1 to _still_ be primary") // 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()) + assert.Equal(t, false, routesAfter2Up[1].GetIsPrimary(), "r1 and r2 is back up, expected r1 to _still_ be primary") // Verify that the route is announced from subnet router 1 clientStatus, err = client.Status() @@ -608,6 +626,9 @@ func TestHASubnetRouterFailover(t *testing.T) { srs1PeerStatus = clientStatus.Peer[srs1.Self.PublicKey] srs2PeerStatus = clientStatus.Peer[srs2.Self.PublicKey] + assert.True(t, srs1PeerStatus.Online, "r1 up, r2 up") + assert.True(t, srs2PeerStatus.Online, "r1 up, r2 up") + assert.NotNil(t, srs1PeerStatus.PrimaryRoutes) assert.Nil(t, srs2PeerStatus.PrimaryRoutes) @@ -620,7 +641,8 @@ func TestHASubnetRouterFailover(t *testing.T) { } // Disable the route of subnet router 1, making it failover to 2 - t.Logf("disabling route in subnet router 1 (%s)", subRouter1.Hostname()) + t.Logf("disabling route in subnet router r1 (%s)", subRouter1.Hostname()) + t.Logf("expecting route to failover to r2 (%s), which is still available", subRouter2.Hostname()) _, err = headscale.Execute( []string{ "headscale", @@ -648,7 +670,7 @@ func TestHASubnetRouterFailover(t *testing.T) { assertNoErr(t, err) assert.Len(t, routesAfterDisabling1, 2) - t.Logf("routes after disabling1 %#v", routesAfterDisabling1) + t.Logf("routes after disabling r1 %#v", routesAfterDisabling1) // Node 1 is not primary assert.Equal(t, true, routesAfterDisabling1[0].GetAdvertised()) @@ -680,6 +702,7 @@ func TestHASubnetRouterFailover(t *testing.T) { // enable the route of subnet router 1, no change expected t.Logf("enabling route in subnet router 1 (%s)", subRouter1.Hostname()) + t.Logf("both online, expecting r2 (%s) to still be primary (no flapping)", subRouter2.Hostname()) _, err = headscale.Execute( []string{ "headscale", @@ -736,7 +759,8 @@ func TestHASubnetRouterFailover(t *testing.T) { } // delete the route of subnet router 2, failover to one expected - t.Logf("deleting route in subnet router 2 (%s)", subRouter2.Hostname()) + t.Logf("deleting route in subnet router r2 (%s)", subRouter2.Hostname()) + t.Logf("expecting route to failover to r1 (%s)", subRouter1.Hostname()) _, err = headscale.Execute( []string{ "headscale", @@ -764,7 +788,7 @@ func TestHASubnetRouterFailover(t *testing.T) { assertNoErr(t, err) assert.Len(t, routesAfterDeleting2, 1) - t.Logf("routes after deleting2 %#v", routesAfterDeleting2) + t.Logf("routes after deleting r2 %#v", routesAfterDeleting2) // Node 1 is primary assert.Equal(t, true, routesAfterDeleting2[0].GetAdvertised()) diff --git a/integration/scenario.go b/integration/scenario.go index a2c63e6f..ebd12bca 100644 --- a/integration/scenario.go +++ b/integration/scenario.go @@ -50,6 +50,8 @@ var ( tailscaleVersions2021 = map[string]bool{ "head": true, "unstable": true, + "1.60": true, // CapVer: 82 + "1.58": true, // CapVer: 82 "1.56": true, // CapVer: 82 "1.54": true, // CapVer: 79 "1.52": true, // CapVer: 79 diff --git a/integration/tailscale.go b/integration/tailscale.go index 9d6796bd..6bcf6073 100644 --- a/integration/tailscale.go +++ b/integration/tailscale.go @@ -27,7 +27,7 @@ type TailscaleClient interface { Down() error IPs() ([]netip.Addr, error) FQDN() (string, error) - Status() (*ipnstate.Status, error) + Status(...bool) (*ipnstate.Status, error) Netmap() (*netmap.NetworkMap, error) Netcheck() (*netcheck.Report, error) WaitForNeedsLogin() error diff --git a/integration/tsic/tsic.go b/integration/tsic/tsic.go index 320ae0d5..6ae0226a 100644 --- a/integration/tsic/tsic.go +++ b/integration/tsic/tsic.go @@ -9,6 +9,7 @@ import ( "log" "net/netip" "net/url" + "os" "strconv" "strings" "time" @@ -503,7 +504,7 @@ func (t *TailscaleInContainer) IPs() ([]netip.Addr, error) { } // Status returns the ipnstate.Status of the Tailscale instance. -func (t *TailscaleInContainer) Status() (*ipnstate.Status, error) { +func (t *TailscaleInContainer) Status(save ...bool) (*ipnstate.Status, error) { command := []string{ "tailscale", "status", @@ -521,60 +522,70 @@ func (t *TailscaleInContainer) Status() (*ipnstate.Status, error) { return nil, fmt.Errorf("failed to unmarshal tailscale status: %w", err) } + err = os.WriteFile(fmt.Sprintf("/tmp/control/%s_status.json", t.hostname), []byte(result), 0o755) + if err != nil { + return nil, fmt.Errorf("status netmap to /tmp/control: %w", err) + } + return &status, err } // Netmap returns the current Netmap (netmap.NetworkMap) of the Tailscale instance. // Only works with Tailscale 1.56 and newer. // Panics if version is lower then minimum. -// func (t *TailscaleInContainer) Netmap() (*netmap.NetworkMap, error) { -// if !util.TailscaleVersionNewerOrEqual("1.56", t.version) { -// panic(fmt.Sprintf("tsic.Netmap() called with unsupported version: %s", t.version)) -// } +func (t *TailscaleInContainer) Netmap() (*netmap.NetworkMap, error) { + if !util.TailscaleVersionNewerOrEqual("1.56", t.version) { + panic(fmt.Sprintf("tsic.Netmap() called with unsupported version: %s", t.version)) + } -// command := []string{ -// "tailscale", -// "debug", -// "netmap", -// } + command := []string{ + "tailscale", + "debug", + "netmap", + } -// result, stderr, err := t.Execute(command) -// if err != nil { -// fmt.Printf("stderr: %s\n", stderr) -// return nil, fmt.Errorf("failed to execute tailscale debug netmap command: %w", err) -// } + result, stderr, err := t.Execute(command) + if err != nil { + fmt.Printf("stderr: %s\n", stderr) + return nil, fmt.Errorf("failed to execute tailscale debug netmap command: %w", err) + } -// var nm netmap.NetworkMap -// err = json.Unmarshal([]byte(result), &nm) -// if err != nil { -// return nil, fmt.Errorf("failed to unmarshal tailscale netmap: %w", err) -// } + var nm netmap.NetworkMap + err = json.Unmarshal([]byte(result), &nm) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal tailscale netmap: %w", err) + } -// return &nm, err -// } + err = os.WriteFile(fmt.Sprintf("/tmp/control/%s_netmap.json", t.hostname), []byte(result), 0o755) + if err != nil { + return nil, fmt.Errorf("saving netmap to /tmp/control: %w", err) + } + + return &nm, err +} // Netmap returns the current Netmap (netmap.NetworkMap) of the Tailscale instance. // This implementation is based on getting the netmap from `tailscale debug watch-ipn` // as there seem to be some weirdness omitting endpoint and DERP info if we use // Patch updates. // This implementation works on all supported versions. -func (t *TailscaleInContainer) Netmap() (*netmap.NetworkMap, error) { - // watch-ipn will only give an update if something is happening, - // since we send keep alives, the worst case for this should be - // 1 minute, but set a slightly more conservative time. - ctx, _ := context.WithTimeout(context.Background(), 3*time.Minute) +// func (t *TailscaleInContainer) Netmap() (*netmap.NetworkMap, error) { +// // watch-ipn will only give an update if something is happening, +// // since we send keep alives, the worst case for this should be +// // 1 minute, but set a slightly more conservative time. +// ctx, _ := context.WithTimeout(context.Background(), 3*time.Minute) - notify, err := t.watchIPN(ctx) - if err != nil { - return nil, err - } +// notify, err := t.watchIPN(ctx) +// if err != nil { +// return nil, err +// } - if notify.NetMap == nil { - return nil, fmt.Errorf("no netmap present in ipn.Notify") - } +// if notify.NetMap == nil { +// return nil, fmt.Errorf("no netmap present in ipn.Notify") +// } - return notify.NetMap, nil -} +// return notify.NetMap, nil +// } // watchIPN watches `tailscale debug watch-ipn` for a ipn.Notify object until // it gets one that has a netmap.NetworkMap. diff --git a/integration/utils.go b/integration/utils.go index b9e25be6..1e2cfd2c 100644 --- a/integration/utils.go +++ b/integration/utils.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/juanfont/headscale/hscontrol/util" "github.com/juanfont/headscale/integration/tsic" "github.com/stretchr/testify/assert" ) @@ -154,11 +155,11 @@ func assertClientsState(t *testing.T, clients []TailscaleClient) { func assertValidNetmap(t *testing.T, client TailscaleClient) { t.Helper() - // if !util.TailscaleVersionNewerOrEqual("1.56", client.Version()) { - // t.Logf("%q has version %q, skipping netmap check...", client.Hostname(), client.Version()) + if !util.TailscaleVersionNewerOrEqual("1.56", client.Version()) { + t.Logf("%q has version %q, skipping netmap check...", client.Hostname(), client.Version()) - // return - // } + return + } t.Logf("Checking netmap of %q", client.Hostname()) @@ -175,7 +176,11 @@ func assertValidNetmap(t *testing.T, client TailscaleClient) { assert.NotEmptyf(t, netmap.SelfNode.AllowedIPs(), "%q does not have any allowed IPs", client.Hostname()) assert.NotEmptyf(t, netmap.SelfNode.Addresses(), "%q does not have any addresses", client.Hostname()) - assert.Truef(t, *netmap.SelfNode.Online(), "%q is not online", client.Hostname()) + if netmap.SelfNode.Online() != nil { + assert.Truef(t, *netmap.SelfNode.Online(), "%q is not online", client.Hostname()) + } else { + t.Errorf("Online should not be nil for %s", client.Hostname()) + } assert.Falsef(t, netmap.SelfNode.Key().IsZero(), "%q does not have a valid NodeKey", client.Hostname()) assert.Falsef(t, netmap.SelfNode.Machine().IsZero(), "%q does not have a valid MachineKey", client.Hostname()) @@ -213,7 +218,7 @@ func assertValidNetmap(t *testing.T, client TailscaleClient) { // This test is not suitable for ACL/partial connection tests. func assertValidStatus(t *testing.T, client TailscaleClient) { t.Helper() - status, err := client.Status() + status, err := client.Status(true) if err != nil { t.Fatalf("getting status for %q: %s", client.Hostname(), err) } From bf4fd078fc9bab3c9ab34e5dac91e56f33bf5852 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 10 Apr 2024 14:49:34 +0200 Subject: [PATCH 022/145] clean up use of log.Error where errors could be wrapped Replace a lot of occurences of log.Error with fmt.Errorf, bubbling the error up the chain instead. --- hscontrol/db/routes.go | 85 ++++++++++++++++-------------------------- 1 file changed, 33 insertions(+), 52 deletions(-) diff --git a/hscontrol/db/routes.go b/hscontrol/db/routes.go index 9498bc65..6d778775 100644 --- a/hscontrol/db/routes.go +++ b/hscontrol/db/routes.go @@ -2,6 +2,7 @@ package db import ( "errors" + "fmt" "net/netip" "github.com/juanfont/headscale/hscontrol/policy" @@ -252,20 +253,20 @@ func DeleteRoute( func deleteNodeRoutes(tx *gorm.DB, node *types.Node, isConnected types.NodeConnectedMap) ([]types.NodeID, error) { routes, err := GetNodeRoutes(tx, node) if err != nil { - return nil, err + return nil, fmt.Errorf("getting node routes: %w", err) } var changed []types.NodeID for i := range routes { if err := tx.Unscoped().Delete(&routes[i]).Error; err != nil { - return nil, err + return nil, fmt.Errorf("deleting route(%d): %w", &routes[i].ID, err) } // TODO(kradalby): This is a bit too aggressive, we could probably // figure out which routes needs to be failed over rather than all. chn, err := failoverRouteTx(tx, isConnected, &routes[i]) if err != nil { - return changed, err + return changed, fmt.Errorf("failing over route after delete: %w", err) } if chn != nil { @@ -410,10 +411,8 @@ func FailoverRouteIfAvailable( isConnected types.NodeConnectedMap, node *types.Node, ) (*types.StateUpdate, error) { - log.Debug().Caller().Uint64("node.id", node.ID.Uint64()).Msgf("ROUTE DEBUG ENTERED FAILOVER") nodeRoutes, err := GetNodeRoutes(tx, node) if err != nil { - log.Debug().Caller().Uint64("node.id", node.ID.Uint64()).Interface("nodeRoutes", nodeRoutes).Msgf("ROUTE DEBUG NO ROUTES") return nil, nil } @@ -421,34 +420,31 @@ func FailoverRouteIfAvailable( for _, nodeRoute := range nodeRoutes { routes, err := getRoutesByPrefix(tx, netip.Prefix(nodeRoute.Prefix)) if err != nil { - return nil, err + return nil, fmt.Errorf("getting routes by prefix: %w", err) } for _, route := range routes { if route.IsPrimary { // if we have a primary route, and the node is connected // nothing needs to be done. - log.Debug().Caller().Uint64("node.id", node.ID.Uint64()).Uint64("route.node.id", route.Node.ID.Uint64()).Msgf("ROUTE DEBUG CHECKING IF ONLINE") if isConnected[route.Node.ID] { - log.Debug().Caller().Uint64("node.id", node.ID.Uint64()).Uint64("route.node.id", route.Node.ID.Uint64()).Msgf("ROUTE DEBUG IS ONLINE") return nil, nil } - log.Debug().Caller().Uint64("node.id", node.ID.Uint64()).Uint64("route.node.id", route.Node.ID.Uint64()).Msgf("ROUTE DEBUG NOT ONLINE, FAILING OVER") // if not, we need to failover the route - changedIDs, err := failoverRouteTx(tx, isConnected, &route) - if err != nil { - return nil, err - } + failover := failoverRoute(isConnected, &route, routes) + if failover != nil { + failover.save(tx) + if err != nil { + return nil, fmt.Errorf("saving failover routes: %w", err) + } - if changedIDs != nil { - changedNodes = append(changedNodes, changedIDs...) + changedNodes = append(changedNodes, failover.old.Node.ID, failover.new.Node.ID) } } } } - log.Debug().Caller().Uint64("node.id", node.ID.Uint64()).Interface("changedNodes", changedNodes).Msgf("ROUTE DEBUG") if len(changedNodes) != 0 { return &types.StateUpdate{ Type: types.StatePeerChanged, @@ -490,7 +486,7 @@ func failoverRouteTx( routes, err := getRoutesByPrefix(tx, netip.Prefix(r.Prefix)) if err != nil { - return nil, err + return nil, fmt.Errorf("getting routes by prefix: %w", err) } fo := failoverRoute(isConnected, r, routes) @@ -498,18 +494,9 @@ func failoverRouteTx( return nil, nil } - err = tx.Save(fo.old).Error + err = fo.save(tx) if err != nil { - log.Error().Err(err).Msg("disabling old primary route") - - return nil, err - } - - err = tx.Save(fo.new).Error - if err != nil { - log.Error().Err(err).Msg("saving new primary route") - - return nil, err + return nil, fmt.Errorf("saving failover route: %w", err) } log.Trace(). @@ -525,6 +512,20 @@ type failover struct { new *types.Route } +func (f *failover) save(tx *gorm.DB) error { + err := tx.Save(f.old).Error + if err != nil { + return fmt.Errorf("saving old primary: %w", err) + } + + err = tx.Save(f.new).Error + if err != nil { + return fmt.Errorf("saving new primary: %w", err) + } + + return nil +} + func failoverRoute( isConnected types.NodeConnectedMap, routeToReplace *types.Route, @@ -603,13 +604,7 @@ func EnableAutoApprovedRoutes( routes, err := GetNodeAdvertisedRoutes(tx, node) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - log.Error(). - Caller(). - Err(err). - Str("node", node.Hostname). - Msg("Could not get advertised routes for node") - - return err + return fmt.Errorf("getting advertised routes for node(%s %d): %w", node.Hostname, node.ID, err) } log.Trace().Interface("routes", routes).Msg("routes for autoapproving") @@ -625,12 +620,7 @@ func EnableAutoApprovedRoutes( netip.Prefix(advertisedRoute.Prefix), ) if err != nil { - log.Err(err). - Str("advertisedRoute", advertisedRoute.String()). - Uint64("nodeId", node.ID.Uint64()). - Msg("Failed to resolve autoApprovers for advertised route") - - return err + return fmt.Errorf("failed to resolve autoApprovers for route(%d) for node(%s %d): %w", advertisedRoute.ID, node.Hostname, node.ID, err) } log.Trace(). @@ -647,11 +637,7 @@ func EnableAutoApprovedRoutes( // TODO(kradalby): figure out how to get this to depend on less stuff approvedIps, err := aclPolicy.ExpandAlias(types.Nodes{node}, approvedAlias) if err != nil { - log.Err(err). - Str("alias", approvedAlias). - Msg("Failed to expand alias when processing autoApprovers policy") - - return err + return fmt.Errorf("expanding alias %q for autoApprovers: %w", approvedAlias, err) } // approvedIPs should contain all of node's IPs if it matches the rule, so check for first @@ -665,12 +651,7 @@ func EnableAutoApprovedRoutes( for _, approvedRoute := range approvedRoutes { _, err := EnableRoute(tx, uint64(approvedRoute.ID)) if err != nil { - log.Err(err). - Str("approvedRoute", approvedRoute.String()). - Uint64("nodeId", node.ID.Uint64()). - Msg("Failed to enable approved route") - - return err + return fmt.Errorf("enabling approved route(%d): %w", approvedRoute.ID, err) } } From 1704977e764a74bb8121d3aa10d5c4a67544ef2b Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 10 Apr 2024 15:35:09 +0200 Subject: [PATCH 023/145] improve testing of route failover logic Signed-off-by: Kristoffer Dalby --- hscontrol/app.go | 71 +++--- hscontrol/db/routes.go | 37 ++-- hscontrol/db/routes_test.go | 380 +++++++++++++++++++++++++++++++-- hscontrol/derp/derp.go | 4 +- hscontrol/noise.go | 92 ++++---- hscontrol/notifier/notifier.go | 14 +- hscontrol/poll.go | 55 +++-- hscontrol/types/const.go | 2 +- integration/hsic/config.go | 2 +- integration/hsic/hsic.go | 2 +- integration/route_test.go | 2 +- 11 files changed, 518 insertions(+), 143 deletions(-) diff --git a/hscontrol/app.go b/hscontrol/app.go index bdb5c1d9..7eb15495 100644 --- a/hscontrol/app.go +++ b/hscontrol/app.go @@ -503,7 +503,7 @@ func (h *Headscale) createRouter(grpcMux *grpcRuntime.ServeMux) *mux.Router { return router } -// Serve launches a GIN server with the Headscale API. +// Serve launches the HTTP and gRPC server service Headscale and the API. func (h *Headscale) Serve() error { if _, enableProfile := os.LookupEnv("HEADSCALE_PROFILING_ENABLED"); enableProfile { if profilePath, ok := os.LookupEnv("HEADSCALE_PROFILING_PATH"); ok { @@ -532,7 +532,7 @@ func (h *Headscale) Serve() error { region, err := h.DERPServer.GenerateRegion() if err != nil { - return err + return fmt.Errorf("generating DERP region for embedded server: %w", err) } if h.cfg.DERP.AutomaticallyAddEmbeddedDerpRegion { @@ -607,14 +607,14 @@ func (h *Headscale) Serve() error { }..., ) if err != nil { - return err + return fmt.Errorf("setting up gRPC gateway via socket: %w", err) } // Connect to the gRPC server over localhost to skip // the authentication. err = v1.RegisterHeadscaleServiceHandler(ctx, grpcGatewayMux, grpcGatewayConn) if err != nil { - return err + return fmt.Errorf("registering Headscale API service to gRPC: %w", err) } // Start the local gRPC server without TLS and without authentication @@ -635,9 +635,7 @@ func (h *Headscale) Serve() error { tlsConfig, err := h.getTLSSettings() if err != nil { - log.Error().Err(err).Msg("Failed to set up TLS configuration") - - return err + return fmt.Errorf("configuring TLS settings: %w", err) } // @@ -702,15 +700,11 @@ func (h *Headscale) Serve() error { httpServer := &http.Server{ Addr: h.cfg.Addr, Handler: router, - ReadTimeout: types.HTTPReadTimeout, - // Go does not handle timeouts in HTTP very well, and there is - // no good way to handle streaming timeouts, therefore we need to - // keep this at unlimited and be careful to clean up connections - // https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/#aboutstreaming - // TODO(kradalby): this timeout can now be set per handler with http.ResponseController: - // https://www.alexedwards.net/blog/how-to-use-the-http-responsecontroller-type - // replace this so only the longpoller has no timeout. - WriteTimeout: 0, + ReadTimeout: types.HTTPTimeout, + + // Long polling should not have any timeout, this is overriden + // further down the chain + WriteTimeout: types.HTTPTimeout, } var httpListener net.Listener @@ -729,27 +723,46 @@ func (h *Headscale) Serve() error { log.Info(). Msgf("listening and serving HTTP on: %s", h.cfg.Addr) - promMux := http.NewServeMux() - promMux.Handle("/metrics", promhttp.Handler()) + debugMux := http.NewServeMux() + debugMux.HandleFunc("/debug/notifier", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(h.nodeNotifier.String())) - promHTTPServer := &http.Server{ + return + }) + debugMux.HandleFunc("/debug/mapresp", func(w http.ResponseWriter, r *http.Request) { + h.mapSessionMu.Lock() + defer h.mapSessionMu.Unlock() + + var b strings.Builder + b.WriteString("mapresponders:\n") + for k, v := range h.mapSessions { + fmt.Fprintf(&b, "\t%d: %p\n", k, v) + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte(b.String())) + + return + }) + debugMux.Handle("/metrics", promhttp.Handler()) + + debugHTTPServer := &http.Server{ Addr: h.cfg.MetricsAddr, - Handler: promMux, - ReadTimeout: types.HTTPReadTimeout, + Handler: debugMux, + ReadTimeout: types.HTTPTimeout, WriteTimeout: 0, } - var promHTTPListener net.Listener - promHTTPListener, err = net.Listen("tcp", h.cfg.MetricsAddr) - + debugHTTPListener, err := net.Listen("tcp", h.cfg.MetricsAddr) if err != nil { return fmt.Errorf("failed to bind to TCP address: %w", err) } - errorGroup.Go(func() error { return promHTTPServer.Serve(promHTTPListener) }) + errorGroup.Go(func() error { return debugHTTPServer.Serve(debugHTTPListener) }) log.Info(). - Msgf("listening and serving metrics on: %s", h.cfg.MetricsAddr) + Msgf("listening and serving debug and metrics on: %s", h.cfg.MetricsAddr) var tailsqlContext context.Context if tailsqlEnabled { @@ -815,7 +828,7 @@ func (h *Headscale) Serve() error { context.Background(), types.HTTPShutdownTimeout, ) - if err := promHTTPServer.Shutdown(ctx); err != nil { + if err := debugHTTPServer.Shutdown(ctx); err != nil { log.Error().Err(err).Msg("Failed to shutdown prometheus http") } if err := httpServer.Shutdown(ctx); err != nil { @@ -833,7 +846,7 @@ func (h *Headscale) Serve() error { } // Close network listeners - promHTTPListener.Close() + debugHTTPListener.Close() httpListener.Close() grpcGatewayConn.Close() @@ -898,7 +911,7 @@ func (h *Headscale) getTLSSettings() (*tls.Config, error) { server := &http.Server{ Addr: h.cfg.TLS.LetsEncrypt.Listen, Handler: certManager.HTTPHandler(http.HandlerFunc(h.redirect)), - ReadTimeout: types.HTTPReadTimeout, + ReadTimeout: types.HTTPTimeout, } go func() { diff --git a/hscontrol/db/routes.go b/hscontrol/db/routes.go index 6d778775..9cc78af8 100644 --- a/hscontrol/db/routes.go +++ b/hscontrol/db/routes.go @@ -4,11 +4,13 @@ import ( "errors" "fmt" "net/netip" + "sort" "github.com/juanfont/headscale/hscontrol/policy" "github.com/juanfont/headscale/hscontrol/types" "github.com/rs/zerolog/log" "gorm.io/gorm" + "tailscale.com/util/set" ) var ErrRouteIsNotAvailable = errors.New("route is not available") @@ -402,11 +404,10 @@ func SaveNodeRoutes(tx *gorm.DB, node *types.Node) (bool, error) { return sendUpdate, nil } -// FailoverRouteIfAvailable takes a node and checks if the node's route -// currently have a functioning host that exposes the network. -// If it does not, it is failed over to another suitable route if there -// is one. -func FailoverRouteIfAvailable( +// FailoverNodeRoutesIfNeccessary takes a node and checks if the node's route +// need to be failed over to another host. +// If needed, the failover will be attempted. +func FailoverNodeRoutesIfNeccessary( tx *gorm.DB, isConnected types.NodeConnectedMap, node *types.Node, @@ -416,8 +417,12 @@ func FailoverRouteIfAvailable( return nil, nil } - var changedNodes []types.NodeID + log.Trace().Msgf("NODE ROUTES: %d", len(nodeRoutes)) + changedNodes := make(set.Set[types.NodeID]) + +nodeRouteLoop: for _, nodeRoute := range nodeRoutes { + log.Trace().Msgf("NODE ROUTE: %d", nodeRoute.ID) routes, err := getRoutesByPrefix(tx, netip.Prefix(nodeRoute.Prefix)) if err != nil { return nil, fmt.Errorf("getting routes by prefix: %w", err) @@ -427,29 +432,37 @@ func FailoverRouteIfAvailable( if route.IsPrimary { // if we have a primary route, and the node is connected // nothing needs to be done. - if isConnected[route.Node.ID] { - return nil, nil + if conn, ok := isConnected[route.Node.ID]; conn && ok { + continue nodeRouteLoop } // if not, we need to failover the route failover := failoverRoute(isConnected, &route, routes) if failover != nil { - failover.save(tx) + err := failover.save(tx) if err != nil { return nil, fmt.Errorf("saving failover routes: %w", err) } - changedNodes = append(changedNodes, failover.old.Node.ID, failover.new.Node.ID) + changedNodes.Add(failover.old.Node.ID) + changedNodes.Add(failover.new.Node.ID) + + continue nodeRouteLoop } } } } + chng := changedNodes.Slice() + sort.SliceStable(chng, func(i, j int) bool { + return chng[i] < chng[j] + }) + if len(changedNodes) != 0 { return &types.StateUpdate{ Type: types.StatePeerChanged, - ChangeNodes: changedNodes, - Message: "called from db.FailoverRouteIfAvailable", + ChangeNodes: chng, + Message: "called from db.FailoverNodeRoutesIfNeccessary", }, nil } diff --git a/hscontrol/db/routes_test.go b/hscontrol/db/routes_test.go index 390cf700..453a7503 100644 --- a/hscontrol/db/routes_test.go +++ b/hscontrol/db/routes_test.go @@ -7,9 +7,9 @@ import ( "time" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/util" - "github.com/stretchr/testify/assert" "gopkg.in/check.v1" "gorm.io/gorm" "tailscale.com/tailcfg" @@ -270,6 +270,370 @@ func (s *Suite) TestDeleteRoutes(c *check.C) { } var ipp = func(s string) types.IPPrefix { return types.IPPrefix(netip.MustParsePrefix(s)) } +var n = func(nid types.NodeID) types.Node { + return types.Node{ID: nid} +} +var np = func(nid types.NodeID) *types.Node { + no := n(nid) + return &no +} +var r = func(id uint, nid types.NodeID, prefix types.IPPrefix, enabled, primary bool) types.Route { + return types.Route{ + Model: gorm.Model{ + ID: id, + }, + Node: n(nid), + Prefix: prefix, + Enabled: enabled, + IsPrimary: primary, + } +} +var rp = func(id uint, nid types.NodeID, prefix types.IPPrefix, enabled, primary bool) *types.Route { + ro := r(id, nid, prefix, enabled, primary) + return &ro +} + +func dbForTest(t *testing.T, testName string) *HSDatabase { + t.Helper() + + tmpDir, err := os.MkdirTemp("", testName) + if err != nil { + t.Fatalf("creating tempdir: %s", err) + } + + dbPath := tmpDir + "/headscale_test.db" + + db, err = NewHeadscaleDatabase( + types.DatabaseConfig{ + Type: "sqlite3", + Sqlite: types.SqliteConfig{ + Path: dbPath, + }, + }, + "", + ) + if err != nil { + t.Fatalf("setting up database: %s", err) + } + + t.Logf("database set up at: %s", dbPath) + + return db +} + +func TestFailoverNodeRoutesIfNeccessary(t *testing.T) { + su := func(nids ...types.NodeID) *types.StateUpdate { + return &types.StateUpdate{ + ChangeNodes: nids, + } + } + tests := []struct { + name string + nodes types.Nodes + routes types.Routes + isConnected []types.NodeConnectedMap + want []*types.StateUpdate + wantErr bool + }{ + { + name: "n1-down-n2-down-n1-up", + nodes: types.Nodes{ + np(1), + np(2), + np(1), + }, + routes: types.Routes{ + r(1, 1, ipp("10.0.0.0/24"), true, true), + r(2, 2, ipp("10.0.0.0/24"), true, false), + }, + isConnected: []types.NodeConnectedMap{ + // n1 goes down + { + 1: false, + 2: true, + }, + // n2 goes down + { + 1: false, + 2: false, + }, + // n1 comes up + { + 1: true, + 2: false, + }, + }, + want: []*types.StateUpdate{ + // route changes from 1 -> 2 + su(1, 2), + // both down, no change + nil, + // route changes from 2 -> 1 + su(1, 2), + }, + }, + { + name: "n1-recon-n2-down-n1-recon-n2-up", + nodes: types.Nodes{ + np(1), + np(2), + np(1), + np(2), + }, + routes: types.Routes{ + r(1, 1, ipp("10.0.0.0/24"), true, true), + r(2, 2, ipp("10.0.0.0/24"), true, false), + }, + isConnected: []types.NodeConnectedMap{ + // n1 up recon = noop + { + 1: true, + 2: true, + }, + // n2 goes down + { + 1: true, + 2: false, + }, + // n1 up recon = noop + { + 1: true, + 2: false, + }, + // n2 comes back up + { + 1: true, + 2: false, + }, + }, + want: []*types.StateUpdate{ + nil, + nil, + nil, + nil, + }, + }, + { + name: "n1-recon-n2-down-n1-recon-n2-up", + nodes: types.Nodes{ + np(1), + np(1), + np(3), + np(3), + np(2), + np(1), + }, + routes: types.Routes{ + r(1, 1, ipp("10.0.0.0/24"), true, true), + r(2, 2, ipp("10.0.0.0/24"), true, false), + r(3, 3, ipp("10.0.0.0/24"), true, false), + }, + isConnected: []types.NodeConnectedMap{ + // n1 goes down + { + 1: false, + 2: false, + 3: true, + }, + // n1 comes up + { + 1: true, + 2: false, + 3: true, + }, + // n3 goes down + { + 1: true, + 2: false, + 3: false, + }, + // n3 comes up + { + 1: true, + 2: false, + 3: true, + }, + // n2 comes up + { + 1: true, + 2: true, + 3: true, + }, + // n1 goes down + { + 1: false, + 2: true, + 3: true, + }, + }, + want: []*types.StateUpdate{ + su(1, 3), // n1 -> n3 + nil, + su(1, 3), // n3 -> n1 + nil, + nil, + su(1, 2), // n1 -> n2 + }, + }, + { + name: "n1-recon-n2-dis-n3-take", + nodes: types.Nodes{ + np(1), + np(3), + }, + routes: types.Routes{ + r(1, 1, ipp("10.0.0.0/24"), true, true), + r(2, 2, ipp("10.0.0.0/24"), false, false), + r(3, 3, ipp("10.0.0.0/24"), true, false), + }, + isConnected: []types.NodeConnectedMap{ + // n1 goes down + { + 1: false, + 2: true, + 3: true, + }, + // n3 goes down + { + 1: false, + 2: true, + 3: false, + }, + }, + want: []*types.StateUpdate{ + su(1, 3), // n1 -> n3 + nil, + }, + }, + { + name: "multi-n1-oneforeach-n2-n3", + nodes: types.Nodes{ + np(1), + }, + routes: types.Routes{ + r(1, 1, ipp("10.0.0.0/24"), true, true), + r(4, 1, ipp("10.1.0.0/24"), true, true), + r(2, 2, ipp("10.0.0.0/24"), true, false), + r(3, 3, ipp("10.1.0.0/24"), true, false), + }, + isConnected: []types.NodeConnectedMap{ + // n1 goes down + { + 1: false, + 2: true, + 3: true, + }, + }, + want: []*types.StateUpdate{ + su(1, 2, 3), // n1 -> n2,n3 + }, + }, + { + name: "multi-n1-onefor-n2-disabled-n3", + nodes: types.Nodes{ + np(1), + }, + routes: types.Routes{ + r(1, 1, ipp("10.0.0.0/24"), true, true), + r(4, 1, ipp("10.1.0.0/24"), true, true), + r(2, 2, ipp("10.0.0.0/24"), true, false), + r(3, 3, ipp("10.1.0.0/24"), false, false), + }, + isConnected: []types.NodeConnectedMap{ + // n1 goes down + { + 1: false, + 2: true, + 3: true, + }, + }, + want: []*types.StateUpdate{ + su(1, 2), // n1 -> n2, n3 is not enabled + }, + }, + { + name: "multi-n1-onefor-n2-offline-n3", + nodes: types.Nodes{ + np(1), + }, + routes: types.Routes{ + r(1, 1, ipp("10.0.0.0/24"), true, true), + r(4, 1, ipp("10.1.0.0/24"), true, true), + r(2, 2, ipp("10.0.0.0/24"), true, false), + r(3, 3, ipp("10.1.0.0/24"), true, false), + }, + isConnected: []types.NodeConnectedMap{ + // n1 goes down + { + 1: false, + 2: true, + 3: false, + }, + }, + want: []*types.StateUpdate{ + su(1, 2), // n1 -> n2, n3 is offline + }, + }, + { + name: "multi-n2-back-to-multi-n1", + nodes: types.Nodes{ + np(1), + }, + routes: types.Routes{ + r(1, 1, ipp("10.0.0.0/24"), true, false), + r(4, 1, ipp("10.1.0.0/24"), true, true), + r(2, 2, ipp("10.0.0.0/24"), true, true), + r(3, 3, ipp("10.1.0.0/24"), true, false), + }, + isConnected: []types.NodeConnectedMap{ + // n1 goes down + { + 1: true, + 2: false, + 3: true, + }, + }, + want: []*types.StateUpdate{ + su(1, 2), // n2 -> n1 + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if (len(tt.isConnected) != len(tt.want)) && len(tt.want) != len(tt.nodes) { + t.Fatalf("nodes (%d), isConnected updates (%d), wants (%d) must be equal", len(tt.nodes), len(tt.isConnected), len(tt.want)) + } + + db := dbForTest(t, tt.name) + + for _, route := range tt.routes { + if err := db.DB.Save(&route).Error; err != nil { + t.Fatalf("failed to create route: %s", err) + } + } + + for step := range len(tt.isConnected) { + node := tt.nodes[step] + isConnected := tt.isConnected[step] + want := tt.want[step] + + got, err := Write(db.DB, func(tx *gorm.DB) (*types.StateUpdate, error) { + return FailoverNodeRoutesIfNeccessary(tx, isConnected, node) + }) + + if (err != nil) != tt.wantErr { + t.Errorf("failoverRoute() error = %v, wantErr %v", err, tt.wantErr) + + return + } + + if diff := cmp.Diff(want, got, cmpopts.IgnoreFields(types.StateUpdate{}, "Type", "Message")); diff != "" { + t.Errorf("failoverRoute() unexpected result (-want +got):\n%s", diff) + } + } + }) + } +} func TestFailoverRouteTx(t *testing.T) { tests := []struct { @@ -637,19 +1001,7 @@ func TestFailoverRouteTx(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "failover-db-test") - assert.NoError(t, err) - - db, err = NewHeadscaleDatabase( - types.DatabaseConfig{ - Type: "sqlite3", - Sqlite: types.SqliteConfig{ - Path: tmpDir + "/headscale_test.db", - }, - }, - "", - ) - assert.NoError(t, err) + db := dbForTest(t, tt.name) for _, route := range tt.routes { if err := db.DB.Save(&route).Error; err != nil { diff --git a/hscontrol/derp/derp.go b/hscontrol/derp/derp.go index 83c200a2..80ec520d 100644 --- a/hscontrol/derp/derp.go +++ b/hscontrol/derp/derp.go @@ -31,7 +31,7 @@ func loadDERPMapFromPath(path string) (*tailcfg.DERPMap, error) { } func loadDERPMapFromURL(addr url.URL) (*tailcfg.DERPMap, error) { - ctx, cancel := context.WithTimeout(context.Background(), types.HTTPReadTimeout) + ctx, cancel := context.WithTimeout(context.Background(), types.HTTPTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodGet, addr.String(), nil) @@ -40,7 +40,7 @@ func loadDERPMapFromURL(addr url.URL) (*tailcfg.DERPMap, error) { } client := http.Client{ - Timeout: types.HTTPReadTimeout, + Timeout: types.HTTPTimeout, } resp, err := client.Do(req) diff --git a/hscontrol/noise.go b/hscontrol/noise.go index 3debd378..92a89d0f 100644 --- a/hscontrol/noise.go +++ b/hscontrol/noise.go @@ -3,7 +3,6 @@ package hscontrol import ( "encoding/binary" "encoding/json" - "errors" "io" "net/http" @@ -12,7 +11,6 @@ import ( "github.com/rs/zerolog/log" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" - "gorm.io/gorm" "tailscale.com/control/controlbase" "tailscale.com/control/controlhttp" "tailscale.com/tailcfg" @@ -103,12 +101,12 @@ func (h *Headscale) NoiseUpgradeHandler( router.HandleFunc("/machine/map", noiseServer.NoisePollNetMapHandler) server := http.Server{ - ReadTimeout: types.HTTPReadTimeout, + ReadTimeout: types.HTTPTimeout, } noiseServer.httpBaseConfig = &http.Server{ Handler: router, - ReadHeaderTimeout: types.HTTPReadTimeout, + ReadHeaderTimeout: types.HTTPTimeout, } noiseServer.http2Server = &http2.Server{} @@ -225,15 +223,6 @@ func (ns *noiseServer) NoisePollNetMapHandler( key.NodePublic{}, ) if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - log.Warn(). - Str("handler", "NoisePollNetMap"). - Uint64("node.id", node.ID.Uint64()). - Msgf("Ignoring request, cannot find node with key %s", mapRequest.NodeKey.String()) - http.Error(writer, "Internal error", http.StatusNotFound) - - return - } log.Error(). Str("handler", "NoisePollNetMap"). Uint64("node.id", node.ID.Uint64()). @@ -242,58 +231,59 @@ func (ns *noiseServer) NoisePollNetMapHandler( return } - log.Debug(). - Str("handler", "NoisePollNetMap"). - Str("node", node.Hostname). - Int("cap_ver", int(mapRequest.Version)). - Uint64("node.id", node.ID.Uint64()). - Msg("A node sending a MapRequest with Noise protocol") + sess := ns.headscale.newMapSession(req.Context(), mapRequest, writer, node) - session := ns.headscale.newMapSession(req.Context(), mapRequest, writer, node) + sess.tracef("a node sending a MapRequest with Noise protocol") // If a streaming mapSession exists for this node, close it // and start a new one. - if session.isStreaming() { - log.Debug(). - Caller(). - Uint64("node.id", node.ID.Uint64()). - Int("cap_ver", int(mapRequest.Version)). - Msg("Aquiring lock to check stream") + if sess.isStreaming() { + sess.tracef("aquiring lock to check stream") + ns.headscale.mapSessionMu.Lock() - if oldSession, ok := ns.headscale.mapSessions[node.ID]; ok { - log.Info(). - Caller(). - Uint64("node.id", node.ID.Uint64()). - Msg("Node has an open streaming session, replacing") - oldSession.close() + if _, ok := ns.headscale.mapSessions[node.ID]; ok { + // NOTE/TODO(kradalby): From how I understand the protocol, when + // a client connects with stream=true, and already has a streaming + // connection open, the correct way is to close the current channel + // and replace it. However, I cannot manage to get that working with + // some sort of lock/block happening on the cancelCh in the streaming + // session. + // Not closing the channel and replacing it puts us in a weird state + // which keeps a ghost stream open, receiving keep alives, but no updates. + // + // Typically a new connection is opened when one exists as a client which + // is already authenticated reconnects (e.g. down, then up). The client will + // start auth and streaming at the same time, and then cancel the streaming + // when the auth has finished successfully, opening a new connection. + // + // As a work-around to not replacing, abusing the clients "resilience" + // by reject the new connection which will cause the client to immediately + // reconnect and "fix" the issue, as the other connection typically has been + // closed, meaning there is nothing to replace. + // + // sess.infof("node has an open stream(%p), replacing with %p", oldSession, sess) + // oldSession.close() + + defer ns.headscale.mapSessionMu.Unlock() + + sess.infof("node has an open stream(%p), rejecting new stream", sess) + return } - ns.headscale.mapSessions[node.ID] = session + ns.headscale.mapSessions[node.ID] = sess ns.headscale.mapSessionMu.Unlock() - log.Debug(). - Caller(). - Uint64("node.id", node.ID.Uint64()). - Int("cap_ver", int(mapRequest.Version)). - Msg("Releasing lock to check stream") + sess.tracef("releasing lock to check stream") } - session.serve() + sess.serve() - if session.isStreaming() { - log.Debug(). - Caller(). - Uint64("node.id", node.ID.Uint64()). - Int("cap_ver", int(mapRequest.Version)). - Msg("Aquiring lock to remove stream") + if sess.isStreaming() { + sess.tracef("aquiring lock to remove stream") ns.headscale.mapSessionMu.Lock() + defer ns.headscale.mapSessionMu.Unlock() delete(ns.headscale.mapSessions, node.ID) - ns.headscale.mapSessionMu.Unlock() - log.Debug(). - Caller(). - Uint64("node.id", node.ID.Uint64()). - Int("cap_ver", int(mapRequest.Version)). - Msg("Releasing lock to remove stream") + sess.tracef("releasing lock to remove stream") } } diff --git a/hscontrol/notifier/notifier.go b/hscontrol/notifier/notifier.go index 4ead615b..6c34af57 100644 --- a/hscontrol/notifier/notifier.go +++ b/hscontrol/notifier/notifier.go @@ -172,11 +172,19 @@ func (n *Notifier) String() string { n.l.RLock() defer n.l.RUnlock() - str := []string{"Notifier, in map:\n"} + var b strings.Builder + b.WriteString("chans:\n") for k, v := range n.nodes { - str = append(str, fmt.Sprintf("\t%d: %v\n", k, v)) + fmt.Fprintf(&b, "\t%d: %p\n", k, v) } - return strings.Join(str, "") + b.WriteString("\n") + b.WriteString("connected:\n") + + for k, v := range n.connected { + fmt.Fprintf(&b, "\t%d: %t\n", k, v) + } + + return b.String() } diff --git a/hscontrol/poll.go b/hscontrol/poll.go index 2b65f6d9..7b554f30 100644 --- a/hscontrol/poll.go +++ b/hscontrol/poll.go @@ -48,6 +48,8 @@ type mapSession struct { ch chan types.StateUpdate cancelCh chan struct{} + keepAliveTicker *time.Ticker + node *types.Node w http.ResponseWriter @@ -85,6 +87,8 @@ func (h *Headscale) newMapSession( ch: updateChan, cancelCh: make(chan struct{}), + keepAliveTicker: time.NewTicker(keepAliveInterval + (time.Duration(rand.IntN(9000)) * time.Millisecond)), + // Loggers warnf: warnf, infof: infof, @@ -100,10 +104,9 @@ func (m *mapSession) close() { return } - select { - case m.cancelCh <- struct{}{}: - default: - } + m.tracef("mapSession (%p) sending message on cancel chan") + m.cancelCh <- struct{}{} + m.tracef("mapSession (%p) sent message on cancel chan") } func (m *mapSession) isStreaming() bool { @@ -118,13 +121,6 @@ func (m *mapSession) isReadOnlyUpdate() bool { return !m.req.Stream && m.req.OmitPeers && m.req.ReadOnly } -func (m *mapSession) flush200() { - m.w.WriteHeader(http.StatusOK) - if f, ok := m.w.(http.Flusher); ok { - f.Flush() - } -} - // handlePoll ensures the node gets the appropriate updates from either // polling or immediate responses. // @@ -211,7 +207,12 @@ func (m *mapSession) serve() { m.pollFailoverRoutes("node connected", m.node) - keepAliveTicker := time.NewTicker(keepAliveInterval + (time.Duration(rand.IntN(9000)) * time.Millisecond)) + // Upgrade the writer to a ResponseController + rc := http.NewResponseController(m.w) + + // Longpolling will break if there is a write timeout, + // so it needs to be disabled. + rc.SetWriteDeadline(time.Time{}) ctx, cancel := context.WithCancel(context.WithValue(m.ctx, nodeNameContextKey, m.node.Hostname)) defer cancel() @@ -324,18 +325,16 @@ func (m *mapSession) serve() { startWrite := time.Now() _, err = m.w.Write(data) if err != nil { - m.errf(err, "Could not write the map response, for mapSession: %p, stream: %t", m, m.isStreaming()) - + m.errf(err, "Could not write the map response, for mapSession: %p", m) return } - if flusher, ok := m.w.(http.Flusher); ok { - flusher.Flush() - } else { - log.Error().Msg("Failed to create http flusher") - + err = rc.Flush() + if err != nil { + m.errf(err, "flushing the map response to client, for mapSession: %p", m) return } + log.Trace().Str("node", m.node.Hostname).TimeDiff("timeSpent", time.Now(), startWrite).Str("mkey", m.node.MachineKey.String()).Msg("finished writing mapresp to node") m.infof("update sent") @@ -402,7 +401,7 @@ func (m *mapSession) serve() { derp = true } - case <-keepAliveTicker.C: + case <-m.keepAliveTicker.C: data, err := m.mapper.KeepAliveResponse(m.req, m.node) if err != nil { m.errf(err, "Error generating the keep alive msg") @@ -415,11 +414,9 @@ func (m *mapSession) serve() { return } - if flusher, ok := m.w.(http.Flusher); ok { - flusher.Flush() - } else { - log.Error().Msg("Failed to create http flusher") - + err = rc.Flush() + if err != nil { + m.errf(err, "flushing keep alive to client, for mapSession: %p", m) return } } @@ -428,7 +425,7 @@ func (m *mapSession) serve() { func (m *mapSession) pollFailoverRoutes(where string, node *types.Node) { update, err := db.Write(m.h.db.DB, func(tx *gorm.DB) (*types.StateUpdate, error) { - return db.FailoverRouteIfAvailable(tx, m.h.nodeNotifier.ConnectedMap(), node) + return db.FailoverNodeRoutesIfNeccessary(tx, m.h.nodeNotifier.ConnectedMap(), node) }) if err != nil { m.errf(err, fmt.Sprintf("failed to ensure failover routes, %s", where)) @@ -565,7 +562,7 @@ func (m *mapSession) handleEndpointUpdate() { }, m.node.ID) - m.flush200() + m.w.WriteHeader(http.StatusOK) return } @@ -654,7 +651,9 @@ func (m *mapSession) handleReadOnlyRequest() { m.errf(err, "Failed to write response") } - m.flush200() + m.w.WriteHeader(http.StatusOK) + + return } func logTracePeerChange(hostname string, hostinfoChange bool, change *tailcfg.PeerChange) { diff --git a/hscontrol/types/const.go b/hscontrol/types/const.go index e718eb2e..019c14b6 100644 --- a/hscontrol/types/const.go +++ b/hscontrol/types/const.go @@ -3,7 +3,7 @@ package types import "time" const ( - HTTPReadTimeout = 30 * time.Second + HTTPTimeout = 30 * time.Second HTTPShutdownTimeout = 3 * time.Second TLSALPN01ChallengeType = "TLS-ALPN-01" HTTP01ChallengeType = "HTTP-01" diff --git a/integration/hsic/config.go b/integration/hsic/config.go index 606718c7..64e6e6eb 100644 --- a/integration/hsic/config.go +++ b/integration/hsic/config.go @@ -124,7 +124,7 @@ func DefaultConfigEnv() map[string]string { "HEADSCALE_PRIVATE_KEY_PATH": "/tmp/private.key", "HEADSCALE_NOISE_PRIVATE_KEY_PATH": "/tmp/noise_private.key", "HEADSCALE_LISTEN_ADDR": "0.0.0.0:8080", - "HEADSCALE_METRICS_LISTEN_ADDR": "127.0.0.1:9090", + "HEADSCALE_METRICS_LISTEN_ADDR": "0.0.0.0:9090", "HEADSCALE_SERVER_URL": "http://headscale:8080", "HEADSCALE_DERP_URLS": "https://controlplane.tailscale.com/derpmap/default", "HEADSCALE_DERP_AUTO_UPDATE_ENABLED": "false", diff --git a/integration/hsic/hsic.go b/integration/hsic/hsic.go index b61827ac..f867a5e2 100644 --- a/integration/hsic/hsic.go +++ b/integration/hsic/hsic.go @@ -260,7 +260,7 @@ func New( runOptions := &dockertest.RunOptions{ Name: hsic.hostname, - ExposedPorts: append([]string{portProto}, hsic.extraPorts...), + ExposedPorts: append([]string{portProto, "9090/tcp"}, hsic.extraPorts...), Networks: []*dockertest.Network{network}, // Cmd: []string{"headscale", "serve"}, // TODO(kradalby): Get rid of this hack, we currently need to give us some diff --git a/integration/route_test.go b/integration/route_test.go index d185acff..150dbd27 100644 --- a/integration/route_test.go +++ b/integration/route_test.go @@ -252,7 +252,7 @@ func TestHASubnetRouterFailover(t *testing.T) { scenario, err := NewScenario() assertNoErrf(t, "failed to create scenario: %s", err) - defer scenario.Shutdown() + // defer scenario.Shutdown() spec := map[string]int{ user: 3, From 60f0cf908ca630a98e087002e3c5e5a10e95d126 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 12 Apr 2024 15:57:43 +0200 Subject: [PATCH 024/145] more log.Error -> fmt.Errorf cleanup --- go.mod | 2 -- go.sum | 4 ---- hscontrol/app.go | 32 +++++-------------------------- hscontrol/db/node.go | 39 +++----------------------------------- hscontrol/db/routes.go | 2 -- hscontrol/db/users.go | 9 ++------- hscontrol/mapper/mapper.go | 10 ++-------- hscontrol/oidc.go | 36 +++++------------------------------ hscontrol/policy/acls.go | 33 +++++++------------------------- hscontrol/poll.go | 3 +-- 10 files changed, 25 insertions(+), 145 deletions(-) diff --git a/go.mod b/go.mod index bf7e61b7..20bd86bd 100644 --- a/go.mod +++ b/go.mod @@ -150,7 +150,6 @@ require ( github.com/opencontainers/image-spec v1.1.0-rc6 // indirect github.com/opencontainers/runc v1.1.12 // indirect github.com/pelletier/go-toml/v2 v2.1.1 // indirect - github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect @@ -162,7 +161,6 @@ require ( 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/sasha-s/go-deadlock v0.3.1 // 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 diff --git a/go.sum b/go.sum index 703fa08c..63876d19 100644 --- a/go.sum +++ b/go.sum @@ -336,8 +336,6 @@ github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaR github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= -github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ= -github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= 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= @@ -394,8 +392,6 @@ github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6g github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= -github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0= -github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM= 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= diff --git a/hscontrol/app.go b/hscontrol/app.go index 7eb15495..6d727001 100644 --- a/hscontrol/app.go +++ b/hscontrol/app.go @@ -39,7 +39,6 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" zl "github.com/rs/zerolog" "github.com/rs/zerolog/log" - "github.com/sasha-s/go-deadlock" "golang.org/x/crypto/acme" "golang.org/x/crypto/acme/autocert" "golang.org/x/oauth2" @@ -79,10 +78,10 @@ const ( registerCacheCleanup = time.Minute * 20 ) -func init() { - deadlock.Opts.DeadlockTimeout = 15 * time.Second - deadlock.Opts.PrintAllCurrentGoroutines = true -} +// func init() { +// deadlock.Opts.DeadlockTimeout = 15 * time.Second +// deadlock.Opts.PrintAllCurrentGoroutines = true +// } // Headscale represents the base app of the service. type Headscale struct { @@ -107,7 +106,7 @@ type Headscale struct { pollNetMapStreamWG sync.WaitGroup mapSessions map[types.NodeID]*mapSession - mapSessionMu deadlock.Mutex + mapSessionMu sync.Mutex } var ( @@ -323,11 +322,6 @@ func (h *Headscale) grpcAuthenticationInterceptor(ctx context.Context, meta, ok := metadata.FromIncomingContext(ctx) if !ok { - log.Error(). - Caller(). - Str("client_address", client.Addr.String()). - Msg("Retrieving metadata is failed") - return ctx, status.Errorf( codes.InvalidArgument, "Retrieving metadata is failed", @@ -336,11 +330,6 @@ func (h *Headscale) grpcAuthenticationInterceptor(ctx context.Context, authHeader, ok := meta["authorization"] if !ok { - log.Error(). - Caller(). - Str("client_address", client.Addr.String()). - Msg("Authorization token is not supplied") - return ctx, status.Errorf( codes.Unauthenticated, "Authorization token is not supplied", @@ -350,11 +339,6 @@ func (h *Headscale) grpcAuthenticationInterceptor(ctx context.Context, token := authHeader[0] if !strings.HasPrefix(token, AuthPrefix) { - log.Error(). - Caller(). - Str("client_address", client.Addr.String()). - Msg(`missing "Bearer " prefix in "Authorization" header`) - return ctx, status.Error( codes.Unauthenticated, `missing "Bearer " prefix in "Authorization" header`, @@ -363,12 +347,6 @@ func (h *Headscale) grpcAuthenticationInterceptor(ctx context.Context, valid, err := h.db.ValidateAPIKey(strings.TrimPrefix(token, AuthPrefix)) if err != nil { - log.Error(). - Caller(). - Err(err). - Str("client_address", client.Addr.String()). - Msg("failed to validate token") - return ctx, status.Error(codes.Internal, "failed to validate token") } diff --git a/hscontrol/db/node.go b/hscontrol/db/node.go index 61c952a0..4f8688c1 100644 --- a/hscontrol/db/node.go +++ b/hscontrol/db/node.go @@ -233,15 +233,7 @@ func RenameNode(tx *gorm.DB, newName, ) if err != nil { - log.Error(). - Caller(). - Str("func", "RenameNode"). - Uint64("nodeID", nodeID). - Str("newName", newName). - Err(err). - Msg("failed to rename node") - - return err + return fmt.Errorf("renaming node: %w", err) } if err := tx.Model(&types.Node{}).Where("id = ?", nodeID).Update("given_name", newName).Error; err != nil { @@ -451,13 +443,7 @@ func GetAdvertisedRoutes(tx *gorm.DB, node *types.Node) ([]netip.Prefix, error) Preload("Node"). Where("node_id = ? AND advertised = ?", node.ID, true).Find(&routes).Error if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - log.Error(). - Caller(). - Err(err). - Str("node", node.Hostname). - Msg("Could not get advertised routes for node") - - return nil, err + return nil, fmt.Errorf("getting advertised routes for node(%d): %w", node.ID, err) } prefixes := []netip.Prefix{} @@ -483,13 +469,7 @@ func GetEnabledRoutes(tx *gorm.DB, node *types.Node) ([]netip.Prefix, error) { Where("node_id = ? AND advertised = ? AND enabled = ?", node.ID, true, true). Find(&routes).Error if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - log.Error(). - Caller(). - Err(err). - Str("node", node.Hostname). - Msg("Could not get enabled routes for node") - - return nil, err + return nil, fmt.Errorf("getting enabled routes for node(%d): %w", node.ID, err) } prefixes := []netip.Prefix{} @@ -508,8 +488,6 @@ func IsRoutesEnabled(tx *gorm.DB, node *types.Node, routeStr string) bool { enabledRoutes, err := GetEnabledRoutes(tx, node) if err != nil { - log.Error().Err(err).Msg("Could not get enabled routes") - return false } @@ -681,8 +659,6 @@ func DeleteExpiredEphemeralNodes(tx *gorm.DB, ) ([]types.NodeID, []types.NodeID) { users, err := ListUsers(tx) if err != nil { - log.Error().Err(err).Msg("Error listing users") - return nil, nil } @@ -691,11 +667,6 @@ func DeleteExpiredEphemeralNodes(tx *gorm.DB, for _, user := range users { nodes, err := ListNodesByUser(tx, user.Name) if err != nil { - log.Error(). - Err(err). - Str("user", user.Name). - Msg("Error listing nodes in user") - return nil, nil } @@ -740,10 +711,6 @@ func ExpireExpiredNodes(tx *gorm.DB, nodes, err := ListNodes(tx) if err != nil { - log.Error(). - Err(err). - Msg("Error listing nodes to find expired nodes") - return time.Unix(0, 0), types.StateUpdate{}, false } for _, node := range nodes { diff --git a/hscontrol/db/routes.go b/hscontrol/db/routes.go index 9cc78af8..a94e1a88 100644 --- a/hscontrol/db/routes.go +++ b/hscontrol/db/routes.go @@ -417,12 +417,10 @@ func FailoverNodeRoutesIfNeccessary( return nil, nil } - log.Trace().Msgf("NODE ROUTES: %d", len(nodeRoutes)) changedNodes := make(set.Set[types.NodeID]) nodeRouteLoop: for _, nodeRoute := range nodeRoutes { - log.Trace().Msgf("NODE ROUTE: %d", nodeRoute.ID) routes, err := getRoutesByPrefix(tx, netip.Prefix(nodeRoute.Prefix)) if err != nil { return nil, fmt.Errorf("getting routes by prefix: %w", err) diff --git a/hscontrol/db/users.go b/hscontrol/db/users.go index 99e93393..1cf8e92f 100644 --- a/hscontrol/db/users.go +++ b/hscontrol/db/users.go @@ -2,10 +2,10 @@ package db import ( "errors" + "fmt" "github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/util" - "github.com/rs/zerolog/log" "gorm.io/gorm" ) @@ -34,12 +34,7 @@ func CreateUser(tx *gorm.DB, name string) (*types.User, error) { } user.Name = name if err := tx.Create(&user).Error; err != nil { - log.Error(). - Str("func", "CreateUser"). - Err(err). - Msg("Could not create row") - - return nil, err + return nil, fmt.Errorf("creating user: %w", err) } return &user, nil diff --git a/hscontrol/mapper/mapper.go b/hscontrol/mapper/mapper.go index 3a92cae6..93ab1f71 100644 --- a/hscontrol/mapper/mapper.go +++ b/hscontrol/mapper/mapper.go @@ -372,10 +372,7 @@ func (m *Mapper) marshalMapResponse( jsonBody, err := json.Marshal(resp) if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Cannot marshal map response") + return nil, fmt.Errorf("marshalling map response: %w", err) } if debugDumpMapResponsePath != "" { @@ -402,10 +399,7 @@ func (m *Mapper) marshalMapResponse( body, err := json.MarshalIndent(data, "", " ") if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Cannot marshal map response") + return nil, fmt.Errorf("marshalling map response: %w", err) } perms := fs.FileMode(debugMapResponsePerm) diff --git a/hscontrol/oidc.go b/hscontrol/oidc.go index d669a922..2ac1b56c 100644 --- a/hscontrol/oidc.go +++ b/hscontrol/oidc.go @@ -58,12 +58,7 @@ func (h *Headscale) initOIDC() error { h.oidcProvider, err = oidc.NewProvider(context.Background(), h.cfg.OIDC.Issuer) if err != nil { - log.Error(). - Err(err). - Caller(). - Msgf("Could not retrieve OIDC Config: %s", err.Error()) - - return err + return fmt.Errorf("creating OIDC provider from issuer config: %w", err) } h.oauth2Config = &oauth2.Config{ @@ -514,12 +509,6 @@ func (h *Headscale) validateNodeForOIDCCallback( User: claims.Email, Verb: "Reauthenticated", }); err != nil { - log.Error(). - Str("func", "OIDCCallback"). - Str("type", "reauthenticate"). - Err(err). - Msg("Could not render OIDC callback template") - writer.Header().Set("Content-Type", "text/plain; charset=utf-8") writer.WriteHeader(http.StatusInternalServerError) _, werr := writer.Write([]byte("Could not render OIDC callback template")) @@ -527,7 +516,7 @@ func (h *Headscale) validateNodeForOIDCCallback( util.LogErr(err, "Failed to write response") } - return nil, true, err + return nil, true, fmt.Errorf("rendering OIDC callback template: %w", err) } writer.Header().Set("Content-Type", "text/html; charset=utf-8") @@ -579,10 +568,6 @@ func (h *Headscale) findOrCreateNewUserForOIDCCallback( if errors.Is(err, db.ErrUserNotFound) { user, err = h.db.CreateUser(userName) if err != nil { - log.Error(). - Err(err). - Caller(). - Msgf("could not create new user '%s'", userName) writer.Header().Set("Content-Type", "text/plain; charset=utf-8") writer.WriteHeader(http.StatusInternalServerError) _, werr := writer.Write([]byte("could not create user")) @@ -590,14 +575,9 @@ func (h *Headscale) findOrCreateNewUserForOIDCCallback( util.LogErr(err, "Failed to write response") } - return nil, err + return nil, fmt.Errorf("creating new user: %w", err) } } else if err != nil { - log.Error(). - Caller(). - Err(err). - Str("user", userName). - Msg("could not find or create user") writer.Header().Set("Content-Type", "text/plain; charset=utf-8") writer.WriteHeader(http.StatusInternalServerError) _, werr := writer.Write([]byte("could not find or create user")) @@ -605,7 +585,7 @@ func (h *Headscale) findOrCreateNewUserForOIDCCallback( util.LogErr(err, "Failed to write response") } - return nil, err + return nil, fmt.Errorf("find or create user: %w", err) } return user, nil @@ -661,12 +641,6 @@ func renderOIDCCallbackTemplate( User: claims.Email, Verb: "Authenticated", }); err != nil { - log.Error(). - Str("func", "OIDCCallback"). - Str("type", "authenticate"). - Err(err). - Msg("Could not render OIDC callback template") - writer.Header().Set("Content-Type", "text/plain; charset=utf-8") writer.WriteHeader(http.StatusInternalServerError) _, werr := writer.Write([]byte("Could not render OIDC callback template")) @@ -674,7 +648,7 @@ func renderOIDCCallbackTemplate( util.LogErr(err, "Failed to write response") } - return nil, err + return nil, fmt.Errorf("rendering OIDC callback template: %w", err) } return &content, nil diff --git a/hscontrol/policy/acls.go b/hscontrol/policy/acls.go index b4095781..a4eee01e 100644 --- a/hscontrol/policy/acls.go +++ b/hscontrol/policy/acls.go @@ -159,23 +159,14 @@ func (pol *ACLPolicy) CompileFilterRules( for srcIndex, src := range acl.Sources { srcs, err := pol.expandSource(src, nodes) if err != nil { - log.Error(). - Interface("src", src). - Int("ACL index", index). - Int("Src index", srcIndex). - Msgf("Error parsing ACL") - - return nil, err + return nil, fmt.Errorf("parsing policy, acl index: %d->%d: %w", index, srcIndex, err) } srcIPs = append(srcIPs, srcs...) } protocols, isWildcard, err := parseProtocol(acl.Protocol) if err != nil { - log.Error(). - Msgf("Error parsing ACL %d. protocol unknown %s", index, acl.Protocol) - - return nil, err + return nil, fmt.Errorf("parsing policy, protocol err: %w ", err) } destPorts := []tailcfg.NetPortRange{} @@ -326,16 +317,12 @@ func (pol *ACLPolicy) CompileSSHPolicy( case "check": checkAction, err := sshCheckAction(sshACL.CheckPeriod) if err != nil { - log.Error(). - Msgf("Error parsing SSH %d, check action with unparsable duration '%s'", index, sshACL.CheckPeriod) + return nil, fmt.Errorf("parsing SSH policy, parsing check duration, index: %d: %w", index, err) } else { action = *checkAction } default: - log.Error(). - Msgf("Error parsing SSH %d, unknown action '%s', skipping", index, sshACL.Action) - - continue + return nil, fmt.Errorf("parsing SSH policy, unknown action %q, index: %d: %w", sshACL.Action, index, err) } principals := make([]*tailcfg.SSHPrincipal, 0, len(sshACL.Sources)) @@ -347,10 +334,7 @@ func (pol *ACLPolicy) CompileSSHPolicy( } else if isGroup(rawSrc) { users, err := pol.expandUsersFromGroup(rawSrc) if err != nil { - log.Error(). - Msgf("Error parsing SSH %d, Source %d", index, innerIndex) - - return nil, err + return nil, fmt.Errorf("parsing SSH policy, expanding user from group, index: %d->%d: %w", index, innerIndex, err) } for _, user := range users { @@ -364,10 +348,7 @@ func (pol *ACLPolicy) CompileSSHPolicy( rawSrc, ) if err != nil { - log.Error(). - Msgf("Error parsing SSH %d, Source %d", index, innerIndex) - - return nil, err + return nil, fmt.Errorf("parsing SSH policy, expanding alias, index: %d->%d: %w", index, innerIndex, err) } for _, expandedSrc := range expandedSrcs.Prefixes() { principals = append(principals, &tailcfg.SSHPrincipal{ @@ -499,7 +480,7 @@ func parseProtocol(protocol string) ([]int, bool, error) { default: protocolNumber, err := strconv.Atoi(protocol) if err != nil { - return nil, false, err + return nil, false, fmt.Errorf("parsing protocol number: %w", err) } needsWildcard := protocolNumber != protocolTCP && protocolNumber != protocolUDP && diff --git a/hscontrol/poll.go b/hscontrol/poll.go index 7b554f30..c38c65e2 100644 --- a/hscontrol/poll.go +++ b/hscontrol/poll.go @@ -16,7 +16,6 @@ import ( "github.com/juanfont/headscale/hscontrol/mapper" "github.com/juanfont/headscale/hscontrol/types" "github.com/rs/zerolog/log" - "github.com/sasha-s/go-deadlock" xslices "golang.org/x/exp/slices" "gorm.io/gorm" "tailscale.com/tailcfg" @@ -43,7 +42,7 @@ type mapSession struct { mapper *mapper.Mapper serving bool - servingMu deadlock.Mutex + servingMu sync.Mutex ch chan types.StateUpdate cancelCh chan struct{} From 7d62e9fce5d0f7c414033fc3bb2847f884d063cd Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Tue, 16 Apr 2024 21:37:25 +0200 Subject: [PATCH 025/145] move "embedded derp" settings into With options for integration tests (#1872) --- integration/embedded_derp_test.go | 16 +--------------- integration/general_test.go | 16 +--------------- integration/hsic/hsic.go | 20 ++++++++++++++++++++ 3 files changed, 22 insertions(+), 30 deletions(-) diff --git a/integration/embedded_derp_test.go b/integration/embedded_derp_test.go index e4f76ec4..b6a62e5f 100644 --- a/integration/embedded_derp_test.go +++ b/integration/embedded_derp_test.go @@ -37,25 +37,11 @@ func TestDERPServerScenario(t *testing.T) { // "user1": len(MustTestVersions), } - headscaleConfig := map[string]string{ - "HEADSCALE_DERP_URLS": "", - "HEADSCALE_DERP_SERVER_ENABLED": "true", - "HEADSCALE_DERP_SERVER_REGION_ID": "999", - "HEADSCALE_DERP_SERVER_REGION_CODE": "headscale", - "HEADSCALE_DERP_SERVER_REGION_NAME": "Headscale Embedded DERP", - "HEADSCALE_DERP_SERVER_STUN_LISTEN_ADDR": "0.0.0.0:3478", - "HEADSCALE_DERP_SERVER_PRIVATE_KEY_PATH": "/tmp/derp.key", - - // Envknob for enabling DERP debug logs - "DERP_DEBUG_LOGS": "true", - "DERP_PROBER_DEBUG_LOGS": "true", - } - err = scenario.CreateHeadscaleEnv( spec, - hsic.WithConfigEnv(headscaleConfig), hsic.WithTestName("derpserver"), hsic.WithExtraPorts([]string{"3478/udp"}), + hsic.WithEmbeddedDERPServerOnly(), hsic.WithTLS(), hsic.WithHostnameAsServerURL(), ) diff --git a/integration/general_test.go b/integration/general_test.go index 975b4c21..e9f9abea 100644 --- a/integration/general_test.go +++ b/integration/general_test.go @@ -33,24 +33,10 @@ func TestPingAllByIP(t *testing.T) { "user2": len(MustTestVersions), } - headscaleConfig := map[string]string{ - "HEADSCALE_DERP_URLS": "", - "HEADSCALE_DERP_SERVER_ENABLED": "true", - "HEADSCALE_DERP_SERVER_REGION_ID": "999", - "HEADSCALE_DERP_SERVER_REGION_CODE": "headscale", - "HEADSCALE_DERP_SERVER_REGION_NAME": "Headscale Embedded DERP", - "HEADSCALE_DERP_SERVER_STUN_LISTEN_ADDR": "0.0.0.0:3478", - "HEADSCALE_DERP_SERVER_PRIVATE_KEY_PATH": "/tmp/derp.key", - - // Envknob for enabling DERP debug logs - "DERP_DEBUG_LOGS": "true", - "DERP_PROBER_DEBUG_LOGS": "true", - } - err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("pingallbyip"), - hsic.WithConfigEnv(headscaleConfig), + hsic.WithEmbeddedDERPServerOnly(), hsic.WithTLS(), hsic.WithHostnameAsServerURL(), ) diff --git a/integration/hsic/hsic.go b/integration/hsic/hsic.go index f867a5e2..2bd60954 100644 --- a/integration/hsic/hsic.go +++ b/integration/hsic/hsic.go @@ -173,6 +173,26 @@ func WithPostgres() Option { } } +// WithEmbeddedDERPServerOnly configures Headscale to start +// and only use the embedded DERP server. +// It requires WithTLS and WithHostnameAsServerURL to be +// set. +func WithEmbeddedDERPServerOnly() Option { + return func(hsic *HeadscaleInContainer) { + hsic.env["HEADSCALE_DERP_URLS"] = "" + hsic.env["HEADSCALE_DERP_SERVER_ENABLED"] = "true" + hsic.env["HEADSCALE_DERP_SERVER_REGION_ID"] = "999" + hsic.env["HEADSCALE_DERP_SERVER_REGION_CODE"] = "headscale" + hsic.env["HEADSCALE_DERP_SERVER_REGION_NAME"] = "Headscale Embedded DERP" + hsic.env["HEADSCALE_DERP_SERVER_STUN_LISTEN_ADDR"] = "0.0.0.0:3478" + hsic.env["HEADSCALE_DERP_SERVER_PRIVATE_KEY_PATH"] = "/tmp/derp.key" + + // Envknob for enabling DERP debug logs + hsic.env["DERP_DEBUG_LOGS"] = "true" + hsic.env["DERP_PROBER_DEBUG_LOGS"] = "true" + } +} + // New returns a new HeadscaleInContainer instance. func New( pool *dockertest.Pool, From 85cef84e17950103b78749ebc781828264be9359 Mon Sep 17 00:00:00 2001 From: Sergey Aksenov Date: Sun, 31 Mar 2024 00:51:14 +0100 Subject: [PATCH 026/145] use newer fork of termcolor (#1842) jagottsicher's fork fixed a bug in Windows implementation. While Windows may be not intended as a target platform, some contributors may prefer it for development. Also ran go mod tidy, thus two more unnecessary packages are removed from go.sum --- cmd/headscale/headscale.go | 2 +- flake.nix | 2 +- go.mod | 2 +- go.sum | 7 ++----- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/cmd/headscale/headscale.go b/cmd/headscale/headscale.go index 3f3322e2..fa17bf6d 100644 --- a/cmd/headscale/headscale.go +++ b/cmd/headscale/headscale.go @@ -4,7 +4,7 @@ import ( "os" "time" - "github.com/efekarakus/termcolor" + "github.com/jagottsicher/termcolor" "github.com/juanfont/headscale/cmd/headscale/cli" "github.com/rs/zerolog" "github.com/rs/zerolog/log" diff --git a/flake.nix b/flake.nix index 79b75a9a..bf11c898 100644 --- a/flake.nix +++ b/flake.nix @@ -31,7 +31,7 @@ # When updating go.mod or go.sum, a new sha will need to be calculated, # update this if you have a mismatch after doing a change to thos files. - vendorHash = "sha256-z3IXmr8SK8oUJTnw7gTok6zpLf15kE89q6zYKbMA5AI="; + vendorHash = "sha256-HGu/OCtjzPeBki5FSL6v1XivCJ30eqj9rL0x7ZVv1TM="; subPackages = ["cmd/headscale"]; diff --git a/go.mod b/go.mod index 20bd86bd..488b60f8 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,6 @@ require ( github.com/coreos/go-oidc/v3 v3.9.0 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/deckarep/golang-set/v2 v2.6.0 - github.com/efekarakus/termcolor v1.0.1 github.com/glebarez/sqlite v1.10.0 github.com/go-gormigrate/gormigrate/v2 v2.1.1 github.com/gofrs/uuid/v5 v5.0.0 @@ -17,6 +16,7 @@ require ( 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.19.1 + github.com/jagottsicher/termcolor v1.0.2 github.com/klauspost/compress v1.17.6 github.com/oauth2-proxy/mockoidc v0.0.0-20220308204021-b9169deeb282 github.com/ory/dockertest/v3 v3.10.0 diff --git a/go.sum b/go.sum index 63876d19..7f6cad4e 100644 --- a/go.sum +++ b/go.sum @@ -131,8 +131,6 @@ 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= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/efekarakus/termcolor v1.0.1 h1:YAKFO3bnLrqZGTWyNLcYoSIAQFKVOmbqmDnwsU/znzg= -github.com/efekarakus/termcolor v1.0.1/go.mod h1:AitrZNrE4nPO538fRsqf+p0WgLdAsGN5pUNrHEPsEMM= 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= @@ -243,6 +241,8 @@ github.com/jackc/pgx/v5 v5.5.3 h1:Ces6/M3wbDXYpM8JyyPD57ivTtJACFZJd885pdIaV2s= github.com/jackc/pgx/v5 v5.5.3/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= 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/jagottsicher/termcolor v1.0.2 h1:fo0c51pQSuLBN1+yVX2ZE+hE+P7ULb/TY8eRowJnrsM= +github.com/jagottsicher/termcolor v1.0.2/go.mod h1:RcH8uFwF/0wbEdQmi83rjmlJ+QOKdMSE9Rc1BEB7zFo= github.com/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.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= @@ -290,7 +290,6 @@ github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc 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.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 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= @@ -547,8 +546,6 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191113165036-4c7a9d0fe056/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= From 2ce23df45a09422bacf14d4ed9b72bc1208c2ce0 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 17 Apr 2024 07:03:06 +0200 Subject: [PATCH 027/145] Migrate IP fields in database to dedicated columns (#1869) --- CHANGELOG.md | 2 + cmd/headscale/cli/nodes.go | 53 ++ config-example.yaml | 5 + gen/go/headscale/v1/apikey.pb.go | 2 +- gen/go/headscale/v1/device.pb.go | 2 +- gen/go/headscale/v1/headscale.pb.go | 294 +++---- gen/go/headscale/v1/headscale.pb.gw.go | 87 ++ gen/go/headscale/v1/headscale_grpc.pb.go | 37 + gen/go/headscale/v1/node.pb.go | 173 +++- gen/go/headscale/v1/preauthkey.pb.go | 2 +- gen/go/headscale/v1/routes.pb.go | 2 +- gen/go/headscale/v1/user.pb.go | 2 +- .../headscale/v1/headscale.swagger.json | 41 + hscontrol/app.go | 14 +- hscontrol/auth.go | 5 +- hscontrol/db/db.go | 61 ++ hscontrol/db/ip.go | 274 +++++- hscontrol/db/ip_test.go | 467 +++++++++-- hscontrol/db/node.go | 20 +- hscontrol/db/node_test.go | 35 +- hscontrol/db/routes.go | 4 +- hscontrol/grpcv1.go | 23 +- hscontrol/mapper/mapper.go | 4 +- hscontrol/mapper/mapper_test.go | 71 +- hscontrol/mapper/tail.go | 2 +- hscontrol/mapper/tail_test.go | 4 +- hscontrol/oidc.go | 4 +- hscontrol/policy/acls.go | 18 +- hscontrol/policy/acls_test.go | 785 +++++++----------- hscontrol/types/config.go | 69 +- hscontrol/types/node.go | 227 ++--- hscontrol/types/node_test.go | 50 +- hscontrol/util/dns.go | 50 +- hscontrol/util/dns_test.go | 20 +- integration/general_test.go | 2 + integration/hsic/config.go | 6 + integration/hsic/hsic.go | 8 + proto/headscale/v1/headscale.proto | 7 + proto/headscale/v1/node.proto | 8 + 39 files changed, 1885 insertions(+), 1055 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0186961..54ad9a55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ after improving the test harness as part of adopting [#1460](https://github.com/ - `/var/lib/headscale` and `/var/run/headscale` is no longer created automatically, see [container docs](./docs/running-headscale-container.md) - Prefixes are now defined per v4 and v6 range. [#1756](https://github.com/juanfont/headscale/pull/1756) - `ip_prefixes` option is now `prefixes.v4` and `prefixes.v6` + - `prefixes.allocation` can be set to assign IPs at `sequential` or `random`. [#1869](https://github.com/juanfont/headscale/pull/1869) ### Changes @@ -53,6 +54,7 @@ after improving the test harness as part of adopting [#1460](https://github.com/ - Turn off gRPC logging [#1640](https://github.com/juanfont/headscale/pull/1640) fixes [#1259](https://github.com/juanfont/headscale/issues/1259) - Added the possibility to manually create a DERP-map entry which can be customized, instead of automatically creating it. [#1565](https://github.com/juanfont/headscale/pull/1565) - Add support for deleting api keys [#1702](https://github.com/juanfont/headscale/pull/1702) +- Add command to backfill IP addresses for nodes missing IPs from configured prefixes. [#1869](https://github.com/juanfont/headscale/pull/1869) ## 0.22.3 (2023-05-12) diff --git a/cmd/headscale/cli/nodes.go b/cmd/headscale/cli/nodes.go index ac996245..58890cb0 100644 --- a/cmd/headscale/cli/nodes.go +++ b/cmd/headscale/cli/nodes.go @@ -97,6 +97,8 @@ func init() { tagCmd.Flags(). StringSliceP("tags", "t", []string{}, "List of tags to add to the node") nodeCmd.AddCommand(tagCmd) + + nodeCmd.AddCommand(backfillNodeIPsCmd) } var nodeCmd = &cobra.Command{ @@ -477,6 +479,57 @@ var moveNodeCmd = &cobra.Command{ }, } +var backfillNodeIPsCmd = &cobra.Command{ + Use: "backfillips", + Short: "Backfill IPs missing from nodes", + Long: ` +Backfill IPs can be used to add/remove IPs from nodes +based on the current configuration of Headscale. + +If there are nodes that does not have IPv4 or IPv6 +even if prefixes for both are configured in the config, +this command can be used to assign IPs of the sort to +all nodes that are missing. + +If you remove IPv4 or IPv6 prefixes from the config, +it can be run to remove the IPs that should no longer +be assigned to nodes.`, + Run: func(cmd *cobra.Command, args []string) { + var err error + output, _ := cmd.Flags().GetString("output") + + confirm := false + prompt := &survey.Confirm{ + Message: "Are you sure that you want to assign/remove IPs to/from nodes?", + } + err = survey.AskOne(prompt, &confirm) + if err != nil { + return + } + if confirm { + ctx, client, conn, cancel := getHeadscaleCLIClient() + defer cancel() + defer conn.Close() + + changes, err := client.BackfillNodeIPs(ctx, &v1.BackfillNodeIPsRequest{Confirmed: confirm}) + if err != nil { + ErrorOutput( + err, + fmt.Sprintf( + "Error backfilling IPs: %s", + status.Convert(err).Message(), + ), + output, + ) + + return + } + + SuccessOutput(changes, "Node IPs backfilled successfully", output) + } + }, +} + func nodesToPtables( currentUser string, showTags bool, diff --git a/config-example.yaml b/config-example.yaml index ba81ba5d..ac0a5eb8 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -61,6 +61,11 @@ prefixes: v6: fd7a:115c:a1e0::/48 v4: 100.64.0.0/10 + # Strategy used for allocation of IPs to nodes, available options: + # - sequential (default): assigns the next free IP from the previous given IP. + # - random: assigns the next free IP from a pseudo-random IP generator (crypto/rand). + allocation: sequential + # DERP is a relay system that Tailscale uses when a direct # connection cannot be established. # https://tailscale.com/blog/how-tailscale-works/#encrypted-tcp-relays-derp diff --git a/gen/go/headscale/v1/apikey.pb.go b/gen/go/headscale/v1/apikey.pb.go index d1a5f555..c4377e48 100644 --- a/gen/go/headscale/v1/apikey.pb.go +++ b/gen/go/headscale/v1/apikey.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 +// protoc-gen-go v1.33.0 // protoc (unknown) // source: headscale/v1/apikey.proto diff --git a/gen/go/headscale/v1/device.pb.go b/gen/go/headscale/v1/device.pb.go index 40e2e24f..7a382dd6 100644 --- a/gen/go/headscale/v1/device.pb.go +++ b/gen/go/headscale/v1/device.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 +// protoc-gen-go v1.33.0 // protoc (unknown) // source: headscale/v1/device.proto diff --git a/gen/go/headscale/v1/headscale.pb.go b/gen/go/headscale/v1/headscale.pb.go index b1af2fa5..9de6b060 100644 --- a/gen/go/headscale/v1/headscale.pb.go +++ b/gen/go/headscale/v1/headscale.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 +// protoc-gen-go v1.33.0 // protoc (unknown) // source: headscale/v1/headscale.proto @@ -36,7 +36,7 @@ var file_headscale_v1_headscale_proto_rawDesc = []byte{ 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x19, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x69, 0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x32, 0xfd, 0x17, 0x0a, 0x10, 0x48, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x53, + 0x6f, 0x32, 0x80, 0x19, 0x0a, 0x10, 0x48, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x63, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, @@ -161,77 +161,85 @@ var file_headscale_v1_headscale_proto_rawDesc = []byte{ 0x76, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x23, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1d, 0x22, 0x1b, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x7b, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x75, - 0x73, 0x65, 0x72, 0x12, 0x64, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, - 0x12, 0x1e, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, - 0x47, 0x65, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1f, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, - 0x47, 0x65, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x16, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x12, 0x0e, 0x2f, 0x61, 0x70, 0x69, 0x2f, - 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x7c, 0x0a, 0x0b, 0x45, 0x6e, 0x61, - 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x20, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, - 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, - 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x68, 0x65, 0x61, - 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, - 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x28, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x22, 0x22, 0x20, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x72, - 0x6f, 0x75, 0x74, 0x65, 0x73, 0x2f, 0x7b, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x7d, - 0x2f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x80, 0x01, 0x0a, 0x0c, 0x44, 0x69, 0x73, 0x61, - 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, - 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x52, - 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x65, + 0x73, 0x65, 0x72, 0x12, 0x80, 0x01, 0x0a, 0x0f, 0x42, 0x61, 0x63, 0x6b, 0x66, 0x69, 0x6c, 0x6c, + 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x50, 0x73, 0x12, 0x24, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, + 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x63, 0x6b, 0x66, 0x69, 0x6c, 0x6c, 0x4e, + 0x6f, 0x64, 0x65, 0x49, 0x50, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, + 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x63, + 0x6b, 0x66, 0x69, 0x6c, 0x6c, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x50, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x22, 0x18, 0x2f, 0x61, + 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x66, + 0x69, 0x6c, 0x6c, 0x69, 0x70, 0x73, 0x12, 0x64, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x75, + 0x74, 0x65, 0x73, 0x12, 0x1e, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, + 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, + 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x16, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x12, 0x0e, 0x2f, 0x61, + 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x7c, 0x0a, 0x0b, + 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x20, 0x2e, 0x68, 0x65, + 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, + 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x61, + 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x28, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x22, 0x22, 0x20, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, + 0x31, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x2f, 0x7b, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x5f, + 0x69, 0x64, 0x7d, 0x2f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x80, 0x01, 0x0a, 0x0c, 0x44, + 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x69, 0x73, 0x61, 0x62, - 0x6c, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x29, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x23, 0x22, 0x21, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, - 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x2f, 0x7b, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x5f, 0x69, - 0x64, 0x7d, 0x2f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x7f, 0x0a, 0x0d, 0x47, 0x65, - 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x22, 0x2e, 0x68, 0x65, - 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4e, 0x6f, - 0x64, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x23, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, - 0x65, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x25, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1f, 0x12, 0x1d, 0x2f, 0x61, - 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x7b, 0x6e, 0x6f, 0x64, 0x65, - 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x75, 0x0a, 0x0b, 0x44, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x20, 0x2e, 0x68, 0x65, 0x61, - 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x68, - 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x21, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1b, 0x2a, 0x19, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, - 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x2f, 0x7b, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x5f, 0x69, - 0x64, 0x7d, 0x12, 0x70, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x70, 0x69, 0x4b, - 0x65, 0x79, 0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, - 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, + 0x6c, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, + 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x69, + 0x73, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x29, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x23, 0x22, 0x21, 0x2f, 0x61, 0x70, 0x69, + 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x2f, 0x7b, 0x72, 0x6f, 0x75, 0x74, + 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x7f, 0x0a, + 0x0d, 0x47, 0x65, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x22, + 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, + 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, + 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x25, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1f, 0x12, + 0x1d, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x7b, 0x6e, + 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x75, + 0x0a, 0x0b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x20, 0x2e, + 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x21, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1b, 0x2a, 0x19, 0x2f, 0x61, 0x70, 0x69, + 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x2f, 0x7b, 0x72, 0x6f, 0x75, 0x74, + 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x12, 0x70, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, + 0x70, 0x69, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, - 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, - 0x13, 0x3a, 0x01, 0x2a, 0x22, 0x0e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, - 0x69, 0x6b, 0x65, 0x79, 0x12, 0x77, 0x0a, 0x0c, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x41, 0x70, - 0x69, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, - 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, + 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, + 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x70, + 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x19, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x22, 0x0e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, + 0x2f, 0x61, 0x70, 0x69, 0x6b, 0x65, 0x79, 0x12, 0x77, 0x0a, 0x0c, 0x45, 0x78, 0x70, 0x69, 0x72, + 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x41, 0x70, 0x69, - 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x20, 0x82, 0xd3, 0xe4, - 0x93, 0x02, 0x1a, 0x3a, 0x01, 0x2a, 0x22, 0x15, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, - 0x61, 0x70, 0x69, 0x6b, 0x65, 0x79, 0x2f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x12, 0x6a, 0x0a, - 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x20, 0x2e, 0x68, - 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, - 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x16, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x12, 0x0e, 0x2f, 0x61, 0x70, 0x69, 0x2f, - 0x76, 0x31, 0x2f, 0x61, 0x70, 0x69, 0x6b, 0x65, 0x79, 0x12, 0x76, 0x0a, 0x0c, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, - 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, - 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, + 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x65, 0x61, + 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, + 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x20, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x3a, 0x01, 0x2a, 0x22, 0x15, 0x2f, 0x61, 0x70, 0x69, 0x2f, + 0x76, 0x31, 0x2f, 0x61, 0x70, 0x69, 0x6b, 0x65, 0x79, 0x2f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, + 0x12, 0x6a, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x73, 0x12, + 0x20, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, + 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x16, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x12, 0x0e, 0x2f, 0x61, + 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x69, 0x6b, 0x65, 0x79, 0x12, 0x76, 0x0a, 0x0c, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x2a, 0x17, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, - 0x31, 0x2f, 0x61, 0x70, 0x69, 0x6b, 0x65, 0x79, 0x2f, 0x7b, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, - 0x7d, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, - 0x6a, 0x75, 0x61, 0x6e, 0x66, 0x6f, 0x6e, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, - 0x6c, 0x65, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x22, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x2a, 0x17, 0x2f, 0x61, 0x70, + 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x69, 0x6b, 0x65, 0x79, 0x2f, 0x7b, 0x70, 0x72, 0x65, + 0x66, 0x69, 0x78, 0x7d, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x6a, 0x75, 0x61, 0x6e, 0x66, 0x6f, 0x6e, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, + 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var file_headscale_v1_headscale_proto_goTypes = []interface{}{ @@ -252,41 +260,43 @@ var file_headscale_v1_headscale_proto_goTypes = []interface{}{ (*RenameNodeRequest)(nil), // 14: headscale.v1.RenameNodeRequest (*ListNodesRequest)(nil), // 15: headscale.v1.ListNodesRequest (*MoveNodeRequest)(nil), // 16: headscale.v1.MoveNodeRequest - (*GetRoutesRequest)(nil), // 17: headscale.v1.GetRoutesRequest - (*EnableRouteRequest)(nil), // 18: headscale.v1.EnableRouteRequest - (*DisableRouteRequest)(nil), // 19: headscale.v1.DisableRouteRequest - (*GetNodeRoutesRequest)(nil), // 20: headscale.v1.GetNodeRoutesRequest - (*DeleteRouteRequest)(nil), // 21: headscale.v1.DeleteRouteRequest - (*CreateApiKeyRequest)(nil), // 22: headscale.v1.CreateApiKeyRequest - (*ExpireApiKeyRequest)(nil), // 23: headscale.v1.ExpireApiKeyRequest - (*ListApiKeysRequest)(nil), // 24: headscale.v1.ListApiKeysRequest - (*DeleteApiKeyRequest)(nil), // 25: headscale.v1.DeleteApiKeyRequest - (*GetUserResponse)(nil), // 26: headscale.v1.GetUserResponse - (*CreateUserResponse)(nil), // 27: headscale.v1.CreateUserResponse - (*RenameUserResponse)(nil), // 28: headscale.v1.RenameUserResponse - (*DeleteUserResponse)(nil), // 29: headscale.v1.DeleteUserResponse - (*ListUsersResponse)(nil), // 30: headscale.v1.ListUsersResponse - (*CreatePreAuthKeyResponse)(nil), // 31: headscale.v1.CreatePreAuthKeyResponse - (*ExpirePreAuthKeyResponse)(nil), // 32: headscale.v1.ExpirePreAuthKeyResponse - (*ListPreAuthKeysResponse)(nil), // 33: headscale.v1.ListPreAuthKeysResponse - (*DebugCreateNodeResponse)(nil), // 34: headscale.v1.DebugCreateNodeResponse - (*GetNodeResponse)(nil), // 35: headscale.v1.GetNodeResponse - (*SetTagsResponse)(nil), // 36: headscale.v1.SetTagsResponse - (*RegisterNodeResponse)(nil), // 37: headscale.v1.RegisterNodeResponse - (*DeleteNodeResponse)(nil), // 38: headscale.v1.DeleteNodeResponse - (*ExpireNodeResponse)(nil), // 39: headscale.v1.ExpireNodeResponse - (*RenameNodeResponse)(nil), // 40: headscale.v1.RenameNodeResponse - (*ListNodesResponse)(nil), // 41: headscale.v1.ListNodesResponse - (*MoveNodeResponse)(nil), // 42: headscale.v1.MoveNodeResponse - (*GetRoutesResponse)(nil), // 43: headscale.v1.GetRoutesResponse - (*EnableRouteResponse)(nil), // 44: headscale.v1.EnableRouteResponse - (*DisableRouteResponse)(nil), // 45: headscale.v1.DisableRouteResponse - (*GetNodeRoutesResponse)(nil), // 46: headscale.v1.GetNodeRoutesResponse - (*DeleteRouteResponse)(nil), // 47: headscale.v1.DeleteRouteResponse - (*CreateApiKeyResponse)(nil), // 48: headscale.v1.CreateApiKeyResponse - (*ExpireApiKeyResponse)(nil), // 49: headscale.v1.ExpireApiKeyResponse - (*ListApiKeysResponse)(nil), // 50: headscale.v1.ListApiKeysResponse - (*DeleteApiKeyResponse)(nil), // 51: headscale.v1.DeleteApiKeyResponse + (*BackfillNodeIPsRequest)(nil), // 17: headscale.v1.BackfillNodeIPsRequest + (*GetRoutesRequest)(nil), // 18: headscale.v1.GetRoutesRequest + (*EnableRouteRequest)(nil), // 19: headscale.v1.EnableRouteRequest + (*DisableRouteRequest)(nil), // 20: headscale.v1.DisableRouteRequest + (*GetNodeRoutesRequest)(nil), // 21: headscale.v1.GetNodeRoutesRequest + (*DeleteRouteRequest)(nil), // 22: headscale.v1.DeleteRouteRequest + (*CreateApiKeyRequest)(nil), // 23: headscale.v1.CreateApiKeyRequest + (*ExpireApiKeyRequest)(nil), // 24: headscale.v1.ExpireApiKeyRequest + (*ListApiKeysRequest)(nil), // 25: headscale.v1.ListApiKeysRequest + (*DeleteApiKeyRequest)(nil), // 26: headscale.v1.DeleteApiKeyRequest + (*GetUserResponse)(nil), // 27: headscale.v1.GetUserResponse + (*CreateUserResponse)(nil), // 28: headscale.v1.CreateUserResponse + (*RenameUserResponse)(nil), // 29: headscale.v1.RenameUserResponse + (*DeleteUserResponse)(nil), // 30: headscale.v1.DeleteUserResponse + (*ListUsersResponse)(nil), // 31: headscale.v1.ListUsersResponse + (*CreatePreAuthKeyResponse)(nil), // 32: headscale.v1.CreatePreAuthKeyResponse + (*ExpirePreAuthKeyResponse)(nil), // 33: headscale.v1.ExpirePreAuthKeyResponse + (*ListPreAuthKeysResponse)(nil), // 34: headscale.v1.ListPreAuthKeysResponse + (*DebugCreateNodeResponse)(nil), // 35: headscale.v1.DebugCreateNodeResponse + (*GetNodeResponse)(nil), // 36: headscale.v1.GetNodeResponse + (*SetTagsResponse)(nil), // 37: headscale.v1.SetTagsResponse + (*RegisterNodeResponse)(nil), // 38: headscale.v1.RegisterNodeResponse + (*DeleteNodeResponse)(nil), // 39: headscale.v1.DeleteNodeResponse + (*ExpireNodeResponse)(nil), // 40: headscale.v1.ExpireNodeResponse + (*RenameNodeResponse)(nil), // 41: headscale.v1.RenameNodeResponse + (*ListNodesResponse)(nil), // 42: headscale.v1.ListNodesResponse + (*MoveNodeResponse)(nil), // 43: headscale.v1.MoveNodeResponse + (*BackfillNodeIPsResponse)(nil), // 44: headscale.v1.BackfillNodeIPsResponse + (*GetRoutesResponse)(nil), // 45: headscale.v1.GetRoutesResponse + (*EnableRouteResponse)(nil), // 46: headscale.v1.EnableRouteResponse + (*DisableRouteResponse)(nil), // 47: headscale.v1.DisableRouteResponse + (*GetNodeRoutesResponse)(nil), // 48: headscale.v1.GetNodeRoutesResponse + (*DeleteRouteResponse)(nil), // 49: headscale.v1.DeleteRouteResponse + (*CreateApiKeyResponse)(nil), // 50: headscale.v1.CreateApiKeyResponse + (*ExpireApiKeyResponse)(nil), // 51: headscale.v1.ExpireApiKeyResponse + (*ListApiKeysResponse)(nil), // 52: headscale.v1.ListApiKeysResponse + (*DeleteApiKeyResponse)(nil), // 53: headscale.v1.DeleteApiKeyResponse } var file_headscale_v1_headscale_proto_depIdxs = []int32{ 0, // 0: headscale.v1.HeadscaleService.GetUser:input_type -> headscale.v1.GetUserRequest @@ -306,43 +316,45 @@ var file_headscale_v1_headscale_proto_depIdxs = []int32{ 14, // 14: headscale.v1.HeadscaleService.RenameNode:input_type -> headscale.v1.RenameNodeRequest 15, // 15: headscale.v1.HeadscaleService.ListNodes:input_type -> headscale.v1.ListNodesRequest 16, // 16: headscale.v1.HeadscaleService.MoveNode:input_type -> headscale.v1.MoveNodeRequest - 17, // 17: headscale.v1.HeadscaleService.GetRoutes:input_type -> headscale.v1.GetRoutesRequest - 18, // 18: headscale.v1.HeadscaleService.EnableRoute:input_type -> headscale.v1.EnableRouteRequest - 19, // 19: headscale.v1.HeadscaleService.DisableRoute:input_type -> headscale.v1.DisableRouteRequest - 20, // 20: headscale.v1.HeadscaleService.GetNodeRoutes:input_type -> headscale.v1.GetNodeRoutesRequest - 21, // 21: headscale.v1.HeadscaleService.DeleteRoute:input_type -> headscale.v1.DeleteRouteRequest - 22, // 22: headscale.v1.HeadscaleService.CreateApiKey:input_type -> headscale.v1.CreateApiKeyRequest - 23, // 23: headscale.v1.HeadscaleService.ExpireApiKey:input_type -> headscale.v1.ExpireApiKeyRequest - 24, // 24: headscale.v1.HeadscaleService.ListApiKeys:input_type -> headscale.v1.ListApiKeysRequest - 25, // 25: headscale.v1.HeadscaleService.DeleteApiKey:input_type -> headscale.v1.DeleteApiKeyRequest - 26, // 26: headscale.v1.HeadscaleService.GetUser:output_type -> headscale.v1.GetUserResponse - 27, // 27: headscale.v1.HeadscaleService.CreateUser:output_type -> headscale.v1.CreateUserResponse - 28, // 28: headscale.v1.HeadscaleService.RenameUser:output_type -> headscale.v1.RenameUserResponse - 29, // 29: headscale.v1.HeadscaleService.DeleteUser:output_type -> headscale.v1.DeleteUserResponse - 30, // 30: headscale.v1.HeadscaleService.ListUsers:output_type -> headscale.v1.ListUsersResponse - 31, // 31: headscale.v1.HeadscaleService.CreatePreAuthKey:output_type -> headscale.v1.CreatePreAuthKeyResponse - 32, // 32: headscale.v1.HeadscaleService.ExpirePreAuthKey:output_type -> headscale.v1.ExpirePreAuthKeyResponse - 33, // 33: headscale.v1.HeadscaleService.ListPreAuthKeys:output_type -> headscale.v1.ListPreAuthKeysResponse - 34, // 34: headscale.v1.HeadscaleService.DebugCreateNode:output_type -> headscale.v1.DebugCreateNodeResponse - 35, // 35: headscale.v1.HeadscaleService.GetNode:output_type -> headscale.v1.GetNodeResponse - 36, // 36: headscale.v1.HeadscaleService.SetTags:output_type -> headscale.v1.SetTagsResponse - 37, // 37: headscale.v1.HeadscaleService.RegisterNode:output_type -> headscale.v1.RegisterNodeResponse - 38, // 38: headscale.v1.HeadscaleService.DeleteNode:output_type -> headscale.v1.DeleteNodeResponse - 39, // 39: headscale.v1.HeadscaleService.ExpireNode:output_type -> headscale.v1.ExpireNodeResponse - 40, // 40: headscale.v1.HeadscaleService.RenameNode:output_type -> headscale.v1.RenameNodeResponse - 41, // 41: headscale.v1.HeadscaleService.ListNodes:output_type -> headscale.v1.ListNodesResponse - 42, // 42: headscale.v1.HeadscaleService.MoveNode:output_type -> headscale.v1.MoveNodeResponse - 43, // 43: headscale.v1.HeadscaleService.GetRoutes:output_type -> headscale.v1.GetRoutesResponse - 44, // 44: headscale.v1.HeadscaleService.EnableRoute:output_type -> headscale.v1.EnableRouteResponse - 45, // 45: headscale.v1.HeadscaleService.DisableRoute:output_type -> headscale.v1.DisableRouteResponse - 46, // 46: headscale.v1.HeadscaleService.GetNodeRoutes:output_type -> headscale.v1.GetNodeRoutesResponse - 47, // 47: headscale.v1.HeadscaleService.DeleteRoute:output_type -> headscale.v1.DeleteRouteResponse - 48, // 48: headscale.v1.HeadscaleService.CreateApiKey:output_type -> headscale.v1.CreateApiKeyResponse - 49, // 49: headscale.v1.HeadscaleService.ExpireApiKey:output_type -> headscale.v1.ExpireApiKeyResponse - 50, // 50: headscale.v1.HeadscaleService.ListApiKeys:output_type -> headscale.v1.ListApiKeysResponse - 51, // 51: headscale.v1.HeadscaleService.DeleteApiKey:output_type -> headscale.v1.DeleteApiKeyResponse - 26, // [26:52] is the sub-list for method output_type - 0, // [0:26] is the sub-list for method input_type + 17, // 17: headscale.v1.HeadscaleService.BackfillNodeIPs:input_type -> headscale.v1.BackfillNodeIPsRequest + 18, // 18: headscale.v1.HeadscaleService.GetRoutes:input_type -> headscale.v1.GetRoutesRequest + 19, // 19: headscale.v1.HeadscaleService.EnableRoute:input_type -> headscale.v1.EnableRouteRequest + 20, // 20: headscale.v1.HeadscaleService.DisableRoute:input_type -> headscale.v1.DisableRouteRequest + 21, // 21: headscale.v1.HeadscaleService.GetNodeRoutes:input_type -> headscale.v1.GetNodeRoutesRequest + 22, // 22: headscale.v1.HeadscaleService.DeleteRoute:input_type -> headscale.v1.DeleteRouteRequest + 23, // 23: headscale.v1.HeadscaleService.CreateApiKey:input_type -> headscale.v1.CreateApiKeyRequest + 24, // 24: headscale.v1.HeadscaleService.ExpireApiKey:input_type -> headscale.v1.ExpireApiKeyRequest + 25, // 25: headscale.v1.HeadscaleService.ListApiKeys:input_type -> headscale.v1.ListApiKeysRequest + 26, // 26: headscale.v1.HeadscaleService.DeleteApiKey:input_type -> headscale.v1.DeleteApiKeyRequest + 27, // 27: headscale.v1.HeadscaleService.GetUser:output_type -> headscale.v1.GetUserResponse + 28, // 28: headscale.v1.HeadscaleService.CreateUser:output_type -> headscale.v1.CreateUserResponse + 29, // 29: headscale.v1.HeadscaleService.RenameUser:output_type -> headscale.v1.RenameUserResponse + 30, // 30: headscale.v1.HeadscaleService.DeleteUser:output_type -> headscale.v1.DeleteUserResponse + 31, // 31: headscale.v1.HeadscaleService.ListUsers:output_type -> headscale.v1.ListUsersResponse + 32, // 32: headscale.v1.HeadscaleService.CreatePreAuthKey:output_type -> headscale.v1.CreatePreAuthKeyResponse + 33, // 33: headscale.v1.HeadscaleService.ExpirePreAuthKey:output_type -> headscale.v1.ExpirePreAuthKeyResponse + 34, // 34: headscale.v1.HeadscaleService.ListPreAuthKeys:output_type -> headscale.v1.ListPreAuthKeysResponse + 35, // 35: headscale.v1.HeadscaleService.DebugCreateNode:output_type -> headscale.v1.DebugCreateNodeResponse + 36, // 36: headscale.v1.HeadscaleService.GetNode:output_type -> headscale.v1.GetNodeResponse + 37, // 37: headscale.v1.HeadscaleService.SetTags:output_type -> headscale.v1.SetTagsResponse + 38, // 38: headscale.v1.HeadscaleService.RegisterNode:output_type -> headscale.v1.RegisterNodeResponse + 39, // 39: headscale.v1.HeadscaleService.DeleteNode:output_type -> headscale.v1.DeleteNodeResponse + 40, // 40: headscale.v1.HeadscaleService.ExpireNode:output_type -> headscale.v1.ExpireNodeResponse + 41, // 41: headscale.v1.HeadscaleService.RenameNode:output_type -> headscale.v1.RenameNodeResponse + 42, // 42: headscale.v1.HeadscaleService.ListNodes:output_type -> headscale.v1.ListNodesResponse + 43, // 43: headscale.v1.HeadscaleService.MoveNode:output_type -> headscale.v1.MoveNodeResponse + 44, // 44: headscale.v1.HeadscaleService.BackfillNodeIPs:output_type -> headscale.v1.BackfillNodeIPsResponse + 45, // 45: headscale.v1.HeadscaleService.GetRoutes:output_type -> headscale.v1.GetRoutesResponse + 46, // 46: headscale.v1.HeadscaleService.EnableRoute:output_type -> headscale.v1.EnableRouteResponse + 47, // 47: headscale.v1.HeadscaleService.DisableRoute:output_type -> headscale.v1.DisableRouteResponse + 48, // 48: headscale.v1.HeadscaleService.GetNodeRoutes:output_type -> headscale.v1.GetNodeRoutesResponse + 49, // 49: headscale.v1.HeadscaleService.DeleteRoute:output_type -> headscale.v1.DeleteRouteResponse + 50, // 50: headscale.v1.HeadscaleService.CreateApiKey:output_type -> headscale.v1.CreateApiKeyResponse + 51, // 51: headscale.v1.HeadscaleService.ExpireApiKey:output_type -> headscale.v1.ExpireApiKeyResponse + 52, // 52: headscale.v1.HeadscaleService.ListApiKeys:output_type -> headscale.v1.ListApiKeysResponse + 53, // 53: headscale.v1.HeadscaleService.DeleteApiKey:output_type -> headscale.v1.DeleteApiKeyResponse + 27, // [27:54] is the sub-list for method output_type + 0, // [0:27] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name diff --git a/gen/go/headscale/v1/headscale.pb.gw.go b/gen/go/headscale/v1/headscale.pb.gw.go index b46f383b..adc7beeb 100644 --- a/gen/go/headscale/v1/headscale.pb.gw.go +++ b/gen/go/headscale/v1/headscale.pb.gw.go @@ -795,6 +795,42 @@ func local_request_HeadscaleService_MoveNode_0(ctx context.Context, marshaler ru } +var ( + filter_HeadscaleService_BackfillNodeIPs_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_HeadscaleService_BackfillNodeIPs_0(ctx context.Context, marshaler runtime.Marshaler, client HeadscaleServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq BackfillNodeIPsRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_HeadscaleService_BackfillNodeIPs_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.BackfillNodeIPs(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_HeadscaleService_BackfillNodeIPs_0(ctx context.Context, marshaler runtime.Marshaler, server HeadscaleServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq BackfillNodeIPsRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_HeadscaleService_BackfillNodeIPs_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.BackfillNodeIPs(ctx, &protoReq) + return msg, metadata, err + +} + func request_HeadscaleService_GetRoutes_0(ctx context.Context, marshaler runtime.Marshaler, client HeadscaleServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq GetRoutesRequest var metadata runtime.ServerMetadata @@ -1574,6 +1610,31 @@ func RegisterHeadscaleServiceHandlerServer(ctx context.Context, mux *runtime.Ser }) + mux.Handle("POST", pattern_HeadscaleService_BackfillNodeIPs_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/headscale.v1.HeadscaleService/BackfillNodeIPs", runtime.WithHTTPPathPattern("/api/v1/node/backfillips")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_HeadscaleService_BackfillNodeIPs_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_HeadscaleService_BackfillNodeIPs_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("GET", pattern_HeadscaleService_GetRoutes_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -2214,6 +2275,28 @@ func RegisterHeadscaleServiceHandlerClient(ctx context.Context, mux *runtime.Ser }) + mux.Handle("POST", pattern_HeadscaleService_BackfillNodeIPs_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/headscale.v1.HeadscaleService/BackfillNodeIPs", runtime.WithHTTPPathPattern("/api/v1/node/backfillips")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_HeadscaleService_BackfillNodeIPs_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_HeadscaleService_BackfillNodeIPs_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("GET", pattern_HeadscaleService_GetRoutes_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -2450,6 +2533,8 @@ var ( pattern_HeadscaleService_MoveNode_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4}, []string{"api", "v1", "node", "node_id", "user"}, "")) + pattern_HeadscaleService_BackfillNodeIPs_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v1", "node", "backfillips"}, "")) + pattern_HeadscaleService_GetRoutes_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "routes"}, "")) pattern_HeadscaleService_EnableRoute_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4}, []string{"api", "v1", "routes", "route_id", "enable"}, "")) @@ -2504,6 +2589,8 @@ var ( forward_HeadscaleService_MoveNode_0 = runtime.ForwardResponseMessage + forward_HeadscaleService_BackfillNodeIPs_0 = runtime.ForwardResponseMessage + forward_HeadscaleService_GetRoutes_0 = runtime.ForwardResponseMessage forward_HeadscaleService_EnableRoute_0 = runtime.ForwardResponseMessage diff --git a/gen/go/headscale/v1/headscale_grpc.pb.go b/gen/go/headscale/v1/headscale_grpc.pb.go index 0d731adc..6557f880 100644 --- a/gen/go/headscale/v1/headscale_grpc.pb.go +++ b/gen/go/headscale/v1/headscale_grpc.pb.go @@ -36,6 +36,7 @@ const ( HeadscaleService_RenameNode_FullMethodName = "/headscale.v1.HeadscaleService/RenameNode" HeadscaleService_ListNodes_FullMethodName = "/headscale.v1.HeadscaleService/ListNodes" HeadscaleService_MoveNode_FullMethodName = "/headscale.v1.HeadscaleService/MoveNode" + HeadscaleService_BackfillNodeIPs_FullMethodName = "/headscale.v1.HeadscaleService/BackfillNodeIPs" HeadscaleService_GetRoutes_FullMethodName = "/headscale.v1.HeadscaleService/GetRoutes" HeadscaleService_EnableRoute_FullMethodName = "/headscale.v1.HeadscaleService/EnableRoute" HeadscaleService_DisableRoute_FullMethodName = "/headscale.v1.HeadscaleService/DisableRoute" @@ -71,6 +72,7 @@ type HeadscaleServiceClient interface { RenameNode(ctx context.Context, in *RenameNodeRequest, opts ...grpc.CallOption) (*RenameNodeResponse, error) ListNodes(ctx context.Context, in *ListNodesRequest, opts ...grpc.CallOption) (*ListNodesResponse, error) MoveNode(ctx context.Context, in *MoveNodeRequest, opts ...grpc.CallOption) (*MoveNodeResponse, error) + BackfillNodeIPs(ctx context.Context, in *BackfillNodeIPsRequest, opts ...grpc.CallOption) (*BackfillNodeIPsResponse, error) // --- Route start --- GetRoutes(ctx context.Context, in *GetRoutesRequest, opts ...grpc.CallOption) (*GetRoutesResponse, error) EnableRoute(ctx context.Context, in *EnableRouteRequest, opts ...grpc.CallOption) (*EnableRouteResponse, error) @@ -245,6 +247,15 @@ func (c *headscaleServiceClient) MoveNode(ctx context.Context, in *MoveNodeReque return out, nil } +func (c *headscaleServiceClient) BackfillNodeIPs(ctx context.Context, in *BackfillNodeIPsRequest, opts ...grpc.CallOption) (*BackfillNodeIPsResponse, error) { + out := new(BackfillNodeIPsResponse) + err := c.cc.Invoke(ctx, HeadscaleService_BackfillNodeIPs_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *headscaleServiceClient) GetRoutes(ctx context.Context, in *GetRoutesRequest, opts ...grpc.CallOption) (*GetRoutesResponse, error) { out := new(GetRoutesResponse) err := c.cc.Invoke(ctx, HeadscaleService_GetRoutes_FullMethodName, in, out, opts...) @@ -350,6 +361,7 @@ type HeadscaleServiceServer interface { RenameNode(context.Context, *RenameNodeRequest) (*RenameNodeResponse, error) ListNodes(context.Context, *ListNodesRequest) (*ListNodesResponse, error) MoveNode(context.Context, *MoveNodeRequest) (*MoveNodeResponse, error) + BackfillNodeIPs(context.Context, *BackfillNodeIPsRequest) (*BackfillNodeIPsResponse, error) // --- Route start --- GetRoutes(context.Context, *GetRoutesRequest) (*GetRoutesResponse, error) EnableRoute(context.Context, *EnableRouteRequest) (*EnableRouteResponse, error) @@ -419,6 +431,9 @@ func (UnimplementedHeadscaleServiceServer) ListNodes(context.Context, *ListNodes func (UnimplementedHeadscaleServiceServer) MoveNode(context.Context, *MoveNodeRequest) (*MoveNodeResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method MoveNode not implemented") } +func (UnimplementedHeadscaleServiceServer) BackfillNodeIPs(context.Context, *BackfillNodeIPsRequest) (*BackfillNodeIPsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method BackfillNodeIPs not implemented") +} func (UnimplementedHeadscaleServiceServer) GetRoutes(context.Context, *GetRoutesRequest) (*GetRoutesResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetRoutes not implemented") } @@ -765,6 +780,24 @@ func _HeadscaleService_MoveNode_Handler(srv interface{}, ctx context.Context, de return interceptor(ctx, in, info, handler) } +func _HeadscaleService_BackfillNodeIPs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(BackfillNodeIPsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(HeadscaleServiceServer).BackfillNodeIPs(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: HeadscaleService_BackfillNodeIPs_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(HeadscaleServiceServer).BackfillNodeIPs(ctx, req.(*BackfillNodeIPsRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _HeadscaleService_GetRoutes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetRoutesRequest) if err := dec(in); err != nil { @@ -1002,6 +1035,10 @@ var HeadscaleService_ServiceDesc = grpc.ServiceDesc{ MethodName: "MoveNode", Handler: _HeadscaleService_MoveNode_Handler, }, + { + MethodName: "BackfillNodeIPs", + Handler: _HeadscaleService_BackfillNodeIPs_Handler, + }, { MethodName: "GetRoutes", Handler: _HeadscaleService_GetRoutes_Handler, diff --git a/gen/go/headscale/v1/node.pb.go b/gen/go/headscale/v1/node.pb.go index ee031566..93d2c6b0 100644 --- a/gen/go/headscale/v1/node.pb.go +++ b/gen/go/headscale/v1/node.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 +// protoc-gen-go v1.33.0 // protoc (unknown) // source: headscale/v1/node.proto @@ -1141,6 +1141,100 @@ func (x *DebugCreateNodeResponse) GetNode() *Node { return nil } +type BackfillNodeIPsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Confirmed bool `protobuf:"varint,1,opt,name=confirmed,proto3" json:"confirmed,omitempty"` +} + +func (x *BackfillNodeIPsRequest) Reset() { + *x = BackfillNodeIPsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_headscale_v1_node_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *BackfillNodeIPsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BackfillNodeIPsRequest) ProtoMessage() {} + +func (x *BackfillNodeIPsRequest) ProtoReflect() protoreflect.Message { + mi := &file_headscale_v1_node_proto_msgTypes[19] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BackfillNodeIPsRequest.ProtoReflect.Descriptor instead. +func (*BackfillNodeIPsRequest) Descriptor() ([]byte, []int) { + return file_headscale_v1_node_proto_rawDescGZIP(), []int{19} +} + +func (x *BackfillNodeIPsRequest) GetConfirmed() bool { + if x != nil { + return x.Confirmed + } + return false +} + +type BackfillNodeIPsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Changes []string `protobuf:"bytes,1,rep,name=changes,proto3" json:"changes,omitempty"` +} + +func (x *BackfillNodeIPsResponse) Reset() { + *x = BackfillNodeIPsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_headscale_v1_node_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *BackfillNodeIPsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BackfillNodeIPsResponse) ProtoMessage() {} + +func (x *BackfillNodeIPsResponse) ProtoReflect() protoreflect.Message { + mi := &file_headscale_v1_node_proto_msgTypes[20] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BackfillNodeIPsResponse.ProtoReflect.Descriptor instead. +func (*BackfillNodeIPsResponse) Descriptor() ([]byte, []int) { + return file_headscale_v1_node_proto_rawDescGZIP(), []int{20} +} + +func (x *BackfillNodeIPsResponse) GetChanges() []string { + if x != nil { + return x.Changes + } + return nil +} + var File_headscale_v1_node_proto protoreflect.FileDescriptor var file_headscale_v1_node_proto_rawDesc = []byte{ @@ -1260,18 +1354,25 @@ var file_headscale_v1_node_proto_rawDesc = []byte{ 0x65, 0x61, 0x74, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x04, 0x6e, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x6f, - 0x64, 0x65, 0x52, 0x04, 0x6e, 0x6f, 0x64, 0x65, 0x2a, 0x82, 0x01, 0x0a, 0x0e, 0x52, 0x65, 0x67, - 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x1f, 0x0a, 0x1b, 0x52, - 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x55, - 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1c, 0x0a, 0x18, - 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, - 0x41, 0x55, 0x54, 0x48, 0x5f, 0x4b, 0x45, 0x59, 0x10, 0x01, 0x12, 0x17, 0x0a, 0x13, 0x52, 0x45, - 0x47, 0x49, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x43, 0x4c, - 0x49, 0x10, 0x02, 0x12, 0x18, 0x0a, 0x14, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52, 0x5f, - 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x4f, 0x49, 0x44, 0x43, 0x10, 0x03, 0x42, 0x29, 0x5a, - 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x75, 0x61, 0x6e, - 0x66, 0x6f, 0x6e, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x67, - 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x64, 0x65, 0x52, 0x04, 0x6e, 0x6f, 0x64, 0x65, 0x22, 0x36, 0x0a, 0x16, 0x42, 0x61, 0x63, 0x6b, + 0x66, 0x69, 0x6c, 0x6c, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x50, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x65, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x65, 0x64, + 0x22, 0x33, 0x0a, 0x17, 0x42, 0x61, 0x63, 0x6b, 0x66, 0x69, 0x6c, 0x6c, 0x4e, 0x6f, 0x64, 0x65, + 0x49, 0x50, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, + 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x63, 0x68, + 0x61, 0x6e, 0x67, 0x65, 0x73, 0x2a, 0x82, 0x01, 0x0a, 0x0e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x65, 0x72, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x1f, 0x0a, 0x1b, 0x52, 0x45, 0x47, 0x49, + 0x53, 0x54, 0x45, 0x52, 0x5f, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x55, 0x4e, 0x53, 0x50, + 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1c, 0x0a, 0x18, 0x52, 0x45, 0x47, + 0x49, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x41, 0x55, 0x54, + 0x48, 0x5f, 0x4b, 0x45, 0x59, 0x10, 0x01, 0x12, 0x17, 0x0a, 0x13, 0x52, 0x45, 0x47, 0x49, 0x53, + 0x54, 0x45, 0x52, 0x5f, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x43, 0x4c, 0x49, 0x10, 0x02, + 0x12, 0x18, 0x0a, 0x14, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x4d, 0x45, 0x54, + 0x48, 0x4f, 0x44, 0x5f, 0x4f, 0x49, 0x44, 0x43, 0x10, 0x03, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x75, 0x61, 0x6e, 0x66, 0x6f, 0x6e, + 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x67, 0x65, 0x6e, 0x2f, + 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1287,7 +1388,7 @@ func file_headscale_v1_node_proto_rawDescGZIP() []byte { } var file_headscale_v1_node_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_headscale_v1_node_proto_msgTypes = make([]protoimpl.MessageInfo, 19) +var file_headscale_v1_node_proto_msgTypes = make([]protoimpl.MessageInfo, 21) var file_headscale_v1_node_proto_goTypes = []interface{}{ (RegisterMethod)(0), // 0: headscale.v1.RegisterMethod (*Node)(nil), // 1: headscale.v1.Node @@ -1309,16 +1410,18 @@ var file_headscale_v1_node_proto_goTypes = []interface{}{ (*MoveNodeResponse)(nil), // 17: headscale.v1.MoveNodeResponse (*DebugCreateNodeRequest)(nil), // 18: headscale.v1.DebugCreateNodeRequest (*DebugCreateNodeResponse)(nil), // 19: headscale.v1.DebugCreateNodeResponse - (*User)(nil), // 20: headscale.v1.User - (*timestamppb.Timestamp)(nil), // 21: google.protobuf.Timestamp - (*PreAuthKey)(nil), // 22: headscale.v1.PreAuthKey + (*BackfillNodeIPsRequest)(nil), // 20: headscale.v1.BackfillNodeIPsRequest + (*BackfillNodeIPsResponse)(nil), // 21: headscale.v1.BackfillNodeIPsResponse + (*User)(nil), // 22: headscale.v1.User + (*timestamppb.Timestamp)(nil), // 23: google.protobuf.Timestamp + (*PreAuthKey)(nil), // 24: headscale.v1.PreAuthKey } var file_headscale_v1_node_proto_depIdxs = []int32{ - 20, // 0: headscale.v1.Node.user:type_name -> headscale.v1.User - 21, // 1: headscale.v1.Node.last_seen:type_name -> google.protobuf.Timestamp - 21, // 2: headscale.v1.Node.expiry:type_name -> google.protobuf.Timestamp - 22, // 3: headscale.v1.Node.pre_auth_key:type_name -> headscale.v1.PreAuthKey - 21, // 4: headscale.v1.Node.created_at:type_name -> google.protobuf.Timestamp + 22, // 0: headscale.v1.Node.user:type_name -> headscale.v1.User + 23, // 1: headscale.v1.Node.last_seen:type_name -> google.protobuf.Timestamp + 23, // 2: headscale.v1.Node.expiry:type_name -> google.protobuf.Timestamp + 24, // 3: headscale.v1.Node.pre_auth_key:type_name -> headscale.v1.PreAuthKey + 23, // 4: headscale.v1.Node.created_at:type_name -> google.protobuf.Timestamp 0, // 5: headscale.v1.Node.register_method:type_name -> headscale.v1.RegisterMethod 1, // 6: headscale.v1.RegisterNodeResponse.node:type_name -> headscale.v1.Node 1, // 7: headscale.v1.GetNodeResponse.node:type_name -> headscale.v1.Node @@ -1571,6 +1674,30 @@ func file_headscale_v1_node_proto_init() { return nil } } + file_headscale_v1_node_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*BackfillNodeIPsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_headscale_v1_node_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*BackfillNodeIPsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -1578,7 +1705,7 @@ func file_headscale_v1_node_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_headscale_v1_node_proto_rawDesc, NumEnums: 1, - NumMessages: 19, + NumMessages: 21, NumExtensions: 0, NumServices: 0, }, diff --git a/gen/go/headscale/v1/preauthkey.pb.go b/gen/go/headscale/v1/preauthkey.pb.go index 35a0dfe0..c3ae2818 100644 --- a/gen/go/headscale/v1/preauthkey.pb.go +++ b/gen/go/headscale/v1/preauthkey.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 +// protoc-gen-go v1.33.0 // protoc (unknown) // source: headscale/v1/preauthkey.proto diff --git a/gen/go/headscale/v1/routes.pb.go b/gen/go/headscale/v1/routes.pb.go index d2273047..9c7475b4 100644 --- a/gen/go/headscale/v1/routes.pb.go +++ b/gen/go/headscale/v1/routes.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 +// protoc-gen-go v1.33.0 // protoc (unknown) // source: headscale/v1/routes.proto diff --git a/gen/go/headscale/v1/user.pb.go b/gen/go/headscale/v1/user.pb.go index 17cb4b54..3fcd12bf 100644 --- a/gen/go/headscale/v1/user.pb.go +++ b/gen/go/headscale/v1/user.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 +// protoc-gen-go v1.33.0 // protoc (unknown) // source: headscale/v1/user.proto diff --git a/gen/openapiv2/headscale/v1/headscale.swagger.json b/gen/openapiv2/headscale/v1/headscale.swagger.json index 7fe0b696..51b4ad22 100644 --- a/gen/openapiv2/headscale/v1/headscale.swagger.json +++ b/gen/openapiv2/headscale/v1/headscale.swagger.json @@ -194,6 +194,36 @@ ] } }, + "/api/v1/node/backfillips": { + "post": { + "operationId": "HeadscaleService_BackfillNodeIPs", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1BackfillNodeIPsResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "confirmed", + "in": "query", + "required": false, + "type": "boolean" + } + ], + "tags": [ + "HeadscaleService" + ] + } + }, "/api/v1/node/register": { "post": { "operationId": "HeadscaleService_RegisterNode", @@ -886,6 +916,17 @@ } } }, + "v1BackfillNodeIPsResponse": { + "type": "object", + "properties": { + "changes": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "v1CreateApiKeyRequest": { "type": "object", "properties": { diff --git a/hscontrol/app.go b/hscontrol/app.go index 6d727001..64d40ed1 100644 --- a/hscontrol/app.go +++ b/hscontrol/app.go @@ -9,7 +9,6 @@ import ( "net" "net/http" _ "net/http/pprof" //nolint - "net/netip" "os" "os/signal" "path/filepath" @@ -56,6 +55,7 @@ import ( "tailscale.com/tailcfg" "tailscale.com/types/dnstype" "tailscale.com/types/key" + "tailscale.com/util/dnsname" ) var ( @@ -148,7 +148,7 @@ func NewHeadscale(cfg *types.Config) (*Headscale, error) { return nil, err } - app.ipAlloc, err = db.NewIPAllocator(app.db, *cfg.PrefixV4, *cfg.PrefixV6) + app.ipAlloc, err = db.NewIPAllocator(app.db, cfg.PrefixV4, cfg.PrefixV6, cfg.IPAllocation) if err != nil { return nil, err } @@ -166,7 +166,15 @@ func NewHeadscale(cfg *types.Config) (*Headscale, error) { if app.cfg.DNSConfig != nil && app.cfg.DNSConfig.Proxied { // if MagicDNS // TODO(kradalby): revisit why this takes a list. - magicDNSDomains := util.GenerateMagicDNSRootDomains([]netip.Prefix{*cfg.PrefixV4, *cfg.PrefixV6}) + + var magicDNSDomains []dnsname.FQDN + if cfg.PrefixV4 != nil { + magicDNSDomains = append(magicDNSDomains, util.GenerateIPv4DNSRootDomain(*cfg.PrefixV4)...) + } + if cfg.PrefixV6 != nil { + magicDNSDomains = append(magicDNSDomains, util.GenerateIPv6DNSRootDomain(*cfg.PrefixV6)...) + } + // we might have routes already from Split DNS if app.cfg.DNSConfig.Routes == nil { app.cfg.DNSConfig.Routes = make(map[string][]*dnstype.Resolver) diff --git a/hscontrol/auth.go b/hscontrol/auth.go index 8271038c..8307d314 100644 --- a/hscontrol/auth.go +++ b/hscontrol/auth.go @@ -383,7 +383,7 @@ func (h *Headscale) handleAuthKey( ForcedTags: pak.Proto().GetAclTags(), } - addrs, err := h.ipAlloc.Next() + ipv4, ipv6, err := h.ipAlloc.Next() if err != nil { log.Error(). Caller(). @@ -397,7 +397,7 @@ func (h *Headscale) handleAuthKey( node, err = h.db.RegisterNode( nodeToRegister, - addrs, + ipv4, ipv6, ) if err != nil { log.Error(). @@ -461,7 +461,6 @@ func (h *Headscale) handleAuthKey( log.Info(). Str("node", registerRequest.Hostinfo.Hostname). - Str("ips", strings.Join(node.IPAddresses.StringSlice(), ", ")). Msg("Successfully authenticated via AuthKey") } diff --git a/hscontrol/db/db.go b/hscontrol/db/db.go index 870ad599..b219ffe1 100644 --- a/hscontrol/db/db.go +++ b/hscontrol/db/db.go @@ -5,6 +5,7 @@ import ( "database/sql" "errors" "fmt" + "net/netip" "path/filepath" "strconv" "strings" @@ -330,6 +331,66 @@ func NewHeadscaleDatabase( return nil }, }, + { + // Replace column with IP address list with dedicated + // IP v4 and v6 column. + // Note that previously, the list _could_ contain more + // than two addresses, which should not really happen. + // In that case, the first occurence of each type will + // be kept. + ID: "2024041121742", + Migrate: func(tx *gorm.DB) error { + _ = tx.Migrator().AddColumn(&types.Node{}, "ipv4") + _ = tx.Migrator().AddColumn(&types.Node{}, "ipv6") + + type node struct { + ID uint64 `gorm:"column:id"` + Addresses string `gorm:"column:ip_addresses"` + } + + var nodes []node + + _ = tx.Raw("SELECT id, ip_addresses FROM nodes").Scan(&nodes).Error + + for _, node := range nodes { + addrs := strings.Split(node.Addresses, ",") + + if len(addrs) == 0 { + fmt.Errorf("no addresses found for node(%d)", node.ID) + } + + var v4 *netip.Addr + var v6 *netip.Addr + + for _, addrStr := range addrs { + addr, err := netip.ParseAddr(addrStr) + if err != nil { + return fmt.Errorf("parsing IP for node(%d) from database: %w", node.ID, err) + } + + if addr.Is4() && v4 == nil { + v4 = &addr + } + + if addr.Is6() && v6 == nil { + v6 = &addr + } + } + + err = tx.Save(&types.Node{ID: types.NodeID(node.ID), IPv4: v4, IPv6: v6}).Error + if err != nil { + return fmt.Errorf("saving ip addresses to new columns: %w", err) + } + } + + _ = tx.Migrator().DropColumn(&types.Node{}, "ip_addresses") + + return nil + }, + Rollback: func(tx *gorm.DB) error { + return nil + }, + }, }, ) diff --git a/hscontrol/db/ip.go b/hscontrol/db/ip.go index dc49f8af..7d06e2e8 100644 --- a/hscontrol/db/ip.go +++ b/hscontrol/db/ip.go @@ -1,13 +1,17 @@ package db import ( + "crypto/rand" + "database/sql" "errors" "fmt" + "math/big" "net/netip" "sync" "github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/util" + "github.com/rs/zerolog/log" "go4.org/netipx" "gorm.io/gorm" ) @@ -20,13 +24,16 @@ import ( type IPAllocator struct { mu sync.Mutex - prefix4 netip.Prefix - prefix6 netip.Prefix + prefix4 *netip.Prefix + prefix6 *netip.Prefix // Previous IPs handed out prev4 netip.Addr prev6 netip.Addr + // strategy used for handing out IP addresses. + strategy types.IPAllocationStrategy + // Set of all IPs handed out. // This might not be in sync with the database, // but it is more conservative. If saves to the @@ -40,40 +47,71 @@ type IPAllocator struct { // provided IPv4 and IPv6 prefix. It needs to be created // when headscale starts and needs to finish its read // transaction before any writes to the database occur. -func NewIPAllocator(db *HSDatabase, prefix4, prefix6 netip.Prefix) (*IPAllocator, error) { - var addressesSlices []string +func NewIPAllocator( + db *HSDatabase, + prefix4, prefix6 *netip.Prefix, + strategy types.IPAllocationStrategy, +) (*IPAllocator, error) { + ret := IPAllocator{ + prefix4: prefix4, + prefix6: prefix6, + + strategy: strategy, + } + + var v4s []sql.NullString + var v6s []sql.NullString if db != nil { - db.Read(func(rx *gorm.DB) error { - return rx.Model(&types.Node{}).Pluck("ip_addresses", &addressesSlices).Error + err := db.Read(func(rx *gorm.DB) error { + return rx.Model(&types.Node{}).Pluck("ipv4", &v4s).Error }) + if err != nil { + return nil, fmt.Errorf("reading IPv4 addresses from database: %w", err) + } + + err = db.Read(func(rx *gorm.DB) error { + return rx.Model(&types.Node{}).Pluck("ipv6", &v6s).Error + }) + if err != nil { + return nil, fmt.Errorf("reading IPv6 addresses from database: %w", err) + } + } var ips netipx.IPSetBuilder // Add network and broadcast addrs to used pool so they // are not handed out to nodes. - network4, broadcast4 := util.GetIPPrefixEndpoints(prefix4) - network6, broadcast6 := util.GetIPPrefixEndpoints(prefix6) - ips.Add(network4) - ips.Add(broadcast4) - ips.Add(network6) - ips.Add(broadcast6) + if prefix4 != nil { + network4, broadcast4 := util.GetIPPrefixEndpoints(*prefix4) + ips.Add(network4) + ips.Add(broadcast4) + + // Use network as starting point, it will be used to call .Next() + // TODO(kradalby): Could potentially take all the IPs loaded from + // the database into account to start at a more "educated" location. + ret.prev4 = network4 + } + + if prefix6 != nil { + network6, broadcast6 := util.GetIPPrefixEndpoints(*prefix6) + ips.Add(network6) + ips.Add(broadcast6) + + ret.prev6 = network6 + } // Fetch all the IP Addresses currently handed out from the Database // and add them to the used IP set. - for _, slice := range addressesSlices { - var machineAddresses types.NodeAddresses - err := machineAddresses.Scan(slice) - if err != nil { - return nil, fmt.Errorf( - "parsing IPs from database %v: %w", machineAddresses, - err, - ) - } + for _, addrStr := range append(v4s, v6s...) { + if addrStr.Valid { + addr, err := netip.ParseAddr(addrStr.String) + if err != nil { + return nil, fmt.Errorf("parsing IP address from database: %w", err) + } - for _, ip := range machineAddresses { - ips.Add(ip) + ips.Add(addr) } } @@ -86,42 +124,61 @@ func NewIPAllocator(db *HSDatabase, prefix4, prefix6 netip.Prefix) (*IPAllocator ) } - return &IPAllocator{ - usedIPs: ips, + ret.usedIPs = ips - prefix4: prefix4, - prefix6: prefix6, - - // Use network as starting point, it will be used to call .Next() - // TODO(kradalby): Could potentially take all the IPs loaded from - // the database into account to start at a more "educated" location. - prev4: network4, - prev6: network6, - }, nil + return &ret, nil } -func (i *IPAllocator) Next() (types.NodeAddresses, error) { +func (i *IPAllocator) Next() (*netip.Addr, *netip.Addr, error) { i.mu.Lock() defer i.mu.Unlock() - v4, err := i.next(i.prev4, i.prefix4) - if err != nil { - return nil, fmt.Errorf("allocating IPv4 address: %w", err) + var err error + var ret4 *netip.Addr + var ret6 *netip.Addr + + if i.prefix4 != nil { + ret4, err = i.next(i.prev4, i.prefix4) + if err != nil { + return nil, nil, fmt.Errorf("allocating IPv4 address: %w", err) + } + i.prev4 = *ret4 } - v6, err := i.next(i.prev6, i.prefix6) - if err != nil { - return nil, fmt.Errorf("allocating IPv6 address: %w", err) + if i.prefix6 != nil { + ret6, err = i.next(i.prev6, i.prefix6) + if err != nil { + return nil, nil, fmt.Errorf("allocating IPv6 address: %w", err) + } + i.prev6 = *ret6 } - return types.NodeAddresses{*v4, *v6}, nil + return ret4, ret6, nil } var ErrCouldNotAllocateIP = errors.New("failed to allocate IP") -func (i *IPAllocator) next(prev netip.Addr, prefix netip.Prefix) (*netip.Addr, error) { - // Get the first IP in our prefix - ip := prev.Next() +func (i *IPAllocator) nextLocked(prev netip.Addr, prefix *netip.Prefix) (*netip.Addr, error) { + i.mu.Lock() + defer i.mu.Unlock() + + return i.next(prev, prefix) +} + +func (i *IPAllocator) next(prev netip.Addr, prefix *netip.Prefix) (*netip.Addr, error) { + var err error + var ip netip.Addr + + switch i.strategy { + case types.IPAllocationStrategySequential: + // Get the first IP in our prefix + ip = prev.Next() + case types.IPAllocationStrategyRandom: + ip, err = randomNext(*prefix) + if err != nil { + return nil, fmt.Errorf("getting random IP: %w", err) + } + } // TODO(kradalby): maybe this can be done less often. set, err := i.usedIPs.IPSet() @@ -136,7 +193,15 @@ func (i *IPAllocator) next(prev netip.Addr, prefix netip.Prefix) (*netip.Addr, e // Check if the IP has already been allocated. if set.Contains(ip) { - ip = ip.Next() + switch i.strategy { + case types.IPAllocationStrategySequential: + ip = ip.Next() + case types.IPAllocationStrategyRandom: + ip, err = randomNext(*prefix) + if err != nil { + return nil, fmt.Errorf("getting random IP: %w", err) + } + } continue } @@ -146,3 +211,120 @@ func (i *IPAllocator) next(prev netip.Addr, prefix netip.Prefix) (*netip.Addr, e return &ip, nil } } + +func randomNext(pfx netip.Prefix) (netip.Addr, error) { + rang := netipx.RangeOfPrefix(pfx) + fromIP, toIP := rang.From(), rang.To() + + var from, to big.Int + + from.SetBytes(fromIP.AsSlice()) + to.SetBytes(toIP.AsSlice()) + + // Find the max, this is how we can do "random range", + // get the "max" as 0 -> to - from and then add back from + // after. + tempMax := big.NewInt(0).Sub(&to, &from) + + out, err := rand.Int(rand.Reader, tempMax) + if err != nil { + return netip.Addr{}, fmt.Errorf("generating random IP: %w", err) + } + + valInRange := big.NewInt(0).Add(&from, out) + + ip, ok := netip.AddrFromSlice(valInRange.Bytes()) + if !ok { + return netip.Addr{}, fmt.Errorf("generated ip bytes are invalid ip") + } + + if !pfx.Contains(ip) { + return netip.Addr{}, fmt.Errorf( + "generated ip(%s) not in prefix(%s)", + ip.String(), + pfx.String(), + ) + } + + return ip, nil +} + +// BackfillNodeIPs will take a database transaction, and +// iterate through all of the current nodes in headscale +// and ensure it has IP addresses according to the current +// configuration. +// This means that if both IPv4 and IPv6 is set in the +// config, and some nodes are missing that type of IP, +// it will be added. +// If a prefix type has been removed (IPv4 or IPv6), it +// will remove the IPs in that family from the node. +func (db *HSDatabase) BackfillNodeIPs(i *IPAllocator) ([]string, error) { + var err error + var ret []string + err = db.Write(func(tx *gorm.DB) error { + if i == nil { + return errors.New("backfilling IPs: ip allocator was nil") + } + + log.Trace().Msgf("starting to backfill IPs") + + nodes, err := ListNodes(tx) + if err != nil { + return fmt.Errorf("listing nodes to backfill IPs: %w", err) + } + + for _, node := range nodes { + log.Trace().Uint64("node.id", node.ID.Uint64()).Msg("checking if need backfill") + + changed := false + // IPv4 prefix is set, but node ip is missing, alloc + if i.prefix4 != nil && node.IPv4 == nil { + ret4, err := i.nextLocked(i.prev4, i.prefix4) + if err != nil { + return fmt.Errorf("failed to allocate ipv4 for node(%d): %w", node.ID, err) + } + + node.IPv4 = ret4 + changed = true + ret = append(ret, fmt.Sprintf("assigned IPv4 %q to Node(%d) %q", ret4.String(), node.ID, node.Hostname)) + } + + // IPv6 prefix is set, but node ip is missing, alloc + if i.prefix6 != nil && node.IPv6 == nil { + ret6, err := i.nextLocked(i.prev6, i.prefix6) + if err != nil { + return fmt.Errorf("failed to allocate ipv6 for node(%d): %w", node.ID, err) + } + + node.IPv6 = ret6 + changed = true + ret = append(ret, fmt.Sprintf("assigned IPv6 %q to Node(%d) %q", ret6.String(), node.ID, node.Hostname)) + } + + // IPv4 prefix is not set, but node has IP, remove + if i.prefix4 == nil && node.IPv4 != nil { + ret = append(ret, fmt.Sprintf("removing IPv4 %q from Node(%d) %q", node.IPv4.String(), node.ID, node.Hostname)) + node.IPv4 = nil + changed = true + } + + // IPv6 prefix is not set, but node has IP, remove + if i.prefix6 == nil && node.IPv6 != nil { + ret = append(ret, fmt.Sprintf("removing IPv6 %q from Node(%d) %q", node.IPv6.String(), node.ID, node.Hostname)) + node.IPv6 = nil + changed = true + } + + if changed { + err := tx.Save(node).Error + if err != nil { + return fmt.Errorf("saving node(%d) after adding IPs: %w", node.ID, err) + } + } + } + + return nil + }) + + return ret, err +} diff --git a/hscontrol/db/ip_test.go b/hscontrol/db/ip_test.go index 17f39c81..a651476c 100644 --- a/hscontrol/db/ip_test.go +++ b/hscontrol/db/ip_test.go @@ -1,49 +1,41 @@ package db import ( + "database/sql" + "fmt" "net/netip" - "os" + "strings" "testing" "github.com/davecgh/go-spew/spew" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/util" ) -func TestIPAllocator(t *testing.T) { - mpp := func(pref string) netip.Prefix { - return netip.MustParsePrefix(pref) - } - na := func(pref string) netip.Addr { - return netip.MustParseAddr(pref) - } - newDb := func() *HSDatabase { - tmpDir, err := os.MkdirTemp("", "headscale-db-test-*") - if err != nil { - t.Fatalf("creating temp dir: %s", err) - } - db, _ = NewHeadscaleDatabase( - types.DatabaseConfig{ - Type: "sqlite3", - Sqlite: types.SqliteConfig{ - Path: tmpDir + "/headscale_test.db", - }, - }, - "", - ) - - return db - } +var mpp = func(pref string) *netip.Prefix { + p := netip.MustParsePrefix(pref) + return &p +} +var na = func(pref string) netip.Addr { + return netip.MustParseAddr(pref) +} +var nap = func(pref string) *netip.Addr { + n := na(pref) + return &n +} +func TestIPAllocatorSequential(t *testing.T) { tests := []struct { name string dbFunc func() *HSDatabase - prefix4 netip.Prefix - prefix6 netip.Prefix + prefix4 *netip.Prefix + prefix6 *netip.Prefix getCount int - want []types.NodeAddresses + want4 []netip.Addr + want6 []netip.Addr }{ { name: "simple", @@ -56,23 +48,49 @@ func TestIPAllocator(t *testing.T) { getCount: 1, - want: []types.NodeAddresses{ - { - na("100.64.0.1"), - na("fd7a:115c:a1e0::1"), - }, + want4: []netip.Addr{ + na("100.64.0.1"), + }, + want6: []netip.Addr{ + na("fd7a:115c:a1e0::1"), + }, + }, + { + name: "simple-v4", + dbFunc: func() *HSDatabase { + return nil + }, + + prefix4: mpp("100.64.0.0/10"), + + getCount: 1, + + want4: []netip.Addr{ + na("100.64.0.1"), + }, + }, + { + name: "simple-v6", + dbFunc: func() *HSDatabase { + return nil + }, + + prefix6: mpp("fd7a:115c:a1e0::/48"), + + getCount: 1, + + want6: []netip.Addr{ + na("fd7a:115c:a1e0::1"), }, }, { name: "simple-with-db", dbFunc: func() *HSDatabase { - db := newDb() + db := dbForTest(t, "simple-with-db") db.DB.Save(&types.Node{ - IPAddresses: types.NodeAddresses{ - na("100.64.0.1"), - na("fd7a:115c:a1e0::1"), - }, + IPv4: nap("100.64.0.1"), + IPv6: nap("fd7a:115c:a1e0::1"), }) return db @@ -83,23 +101,21 @@ func TestIPAllocator(t *testing.T) { getCount: 1, - want: []types.NodeAddresses{ - { - na("100.64.0.2"), - na("fd7a:115c:a1e0::2"), - }, + want4: []netip.Addr{ + na("100.64.0.2"), + }, + want6: []netip.Addr{ + na("fd7a:115c:a1e0::2"), }, }, { name: "before-after-free-middle-in-db", dbFunc: func() *HSDatabase { - db := newDb() + db := dbForTest(t, "before-after-free-middle-in-db") db.DB.Save(&types.Node{ - IPAddresses: types.NodeAddresses{ - na("100.64.0.2"), - na("fd7a:115c:a1e0::2"), - }, + IPv4: nap("100.64.0.2"), + IPv6: nap("fd7a:115c:a1e0::2"), }) return db @@ -110,15 +126,13 @@ func TestIPAllocator(t *testing.T) { getCount: 2, - want: []types.NodeAddresses{ - { - na("100.64.0.1"), - na("fd7a:115c:a1e0::1"), - }, - { - na("100.64.0.3"), - na("fd7a:115c:a1e0::3"), - }, + want4: []netip.Addr{ + na("100.64.0.1"), + na("100.64.0.3"), + }, + want6: []netip.Addr{ + na("fd7a:115c:a1e0::1"), + na("fd7a:115c:a1e0::3"), }, }, } @@ -127,24 +141,347 @@ func TestIPAllocator(t *testing.T) { t.Run(tt.name, func(t *testing.T) { db := tt.dbFunc() - alloc, _ := NewIPAllocator(db, tt.prefix4, tt.prefix6) + alloc, _ := NewIPAllocator( + db, + tt.prefix4, + tt.prefix6, + types.IPAllocationStrategySequential, + ) spew.Dump(alloc) - t.Logf("prefixes: %q, %q", tt.prefix4.String(), tt.prefix6.String()) - - var got []types.NodeAddresses + var got4s []netip.Addr + var got6s []netip.Addr for range tt.getCount { - gotSet, err := alloc.Next() + got4, got6, err := alloc.Next() if err != nil { t.Fatalf("allocating next IP: %s", err) } - got = append(got, gotSet) + if got4 != nil { + got4s = append(got4s, *got4) + } + + if got6 != nil { + got6s = append(got6s, *got6) + } } - if diff := cmp.Diff(tt.want, got, util.Comparers...); diff != "" { - t.Errorf("IPAllocator unexpected result (-want +got):\n%s", diff) + if diff := cmp.Diff(tt.want4, got4s, util.Comparers...); diff != "" { + t.Errorf("IPAllocator 4s unexpected result (-want +got):\n%s", diff) + } + + if diff := cmp.Diff(tt.want6, got6s, util.Comparers...); diff != "" { + t.Errorf("IPAllocator 6s unexpected result (-want +got):\n%s", diff) + } + }) + } +} + +func TestIPAllocatorRandom(t *testing.T) { + tests := []struct { + name string + dbFunc func() *HSDatabase + + getCount int + + prefix4 *netip.Prefix + prefix6 *netip.Prefix + want4 bool + want6 bool + }{ + { + name: "simple", + dbFunc: func() *HSDatabase { + return nil + }, + + prefix4: mpp("100.64.0.0/10"), + prefix6: mpp("fd7a:115c:a1e0::/48"), + + getCount: 1, + + want4: true, + want6: true, + }, + { + name: "simple-v4", + dbFunc: func() *HSDatabase { + return nil + }, + + prefix4: mpp("100.64.0.0/10"), + + getCount: 1, + + want4: true, + want6: false, + }, + { + name: "simple-v6", + dbFunc: func() *HSDatabase { + return nil + }, + + prefix6: mpp("fd7a:115c:a1e0::/48"), + + getCount: 1, + + want4: false, + want6: true, + }, + { + name: "generate-lots-of-random", + dbFunc: func() *HSDatabase { + return nil + }, + + prefix4: mpp("100.64.0.0/10"), + prefix6: mpp("fd7a:115c:a1e0::/48"), + + getCount: 1000, + + want4: true, + want6: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db := tt.dbFunc() + + alloc, _ := NewIPAllocator(db, tt.prefix4, tt.prefix6, types.IPAllocationStrategyRandom) + + spew.Dump(alloc) + + for range tt.getCount { + got4, got6, err := alloc.Next() + if err != nil { + t.Fatalf("allocating next IP: %s", err) + } + + t.Logf("addrs ipv4: %v, ipv6: %v", got4, got6) + + if tt.want4 { + if got4 == nil { + t.Fatalf("expected ipv4 addr, got nil") + } + } + + if tt.want6 { + if got6 == nil { + t.Fatalf("expected ipv4 addr, got nil") + } + } + } + }) + } +} + +func TestBackfillIPAddresses(t *testing.T) { + fullNodeP := func(i int) *types.Node { + v4 := fmt.Sprintf("100.64.0.%d", i) + v6 := fmt.Sprintf("fd7a:115c:a1e0::%d", i) + return &types.Node{ + IPv4DatabaseField: sql.NullString{ + Valid: true, + String: v4, + }, + IPv4: nap(v4), + IPv6DatabaseField: sql.NullString{ + Valid: true, + String: v6, + }, + IPv6: nap(v6), + } + } + tests := []struct { + name string + dbFunc func() *HSDatabase + + prefix4 *netip.Prefix + prefix6 *netip.Prefix + want types.Nodes + }{ + { + name: "simple-backfill-ipv6", + dbFunc: func() *HSDatabase { + db := dbForTest(t, "simple-backfill-ipv6") + + db.DB.Save(&types.Node{ + IPv4: nap("100.64.0.1"), + }) + + return db + }, + + prefix4: mpp("100.64.0.0/10"), + prefix6: mpp("fd7a:115c:a1e0::/48"), + + want: types.Nodes{ + &types.Node{ + IPv4DatabaseField: sql.NullString{ + Valid: true, + String: "100.64.0.1", + }, + IPv4: nap("100.64.0.1"), + IPv6DatabaseField: sql.NullString{ + Valid: true, + String: "fd7a:115c:a1e0::1", + }, + IPv6: nap("fd7a:115c:a1e0::1"), + }, + }, + }, + { + name: "simple-backfill-ipv4", + dbFunc: func() *HSDatabase { + db := dbForTest(t, "simple-backfill-ipv4") + + db.DB.Save(&types.Node{ + IPv6: nap("fd7a:115c:a1e0::1"), + }) + + return db + }, + + prefix4: mpp("100.64.0.0/10"), + prefix6: mpp("fd7a:115c:a1e0::/48"), + + want: types.Nodes{ + &types.Node{ + IPv4DatabaseField: sql.NullString{ + Valid: true, + String: "100.64.0.1", + }, + IPv4: nap("100.64.0.1"), + IPv6DatabaseField: sql.NullString{ + Valid: true, + String: "fd7a:115c:a1e0::1", + }, + IPv6: nap("fd7a:115c:a1e0::1"), + }, + }, + }, + { + name: "simple-backfill-remove-ipv6", + dbFunc: func() *HSDatabase { + db := dbForTest(t, "simple-backfill-remove-ipv6") + + db.DB.Save(&types.Node{ + IPv4: nap("100.64.0.1"), + IPv6: nap("fd7a:115c:a1e0::1"), + }) + + return db + }, + + prefix4: mpp("100.64.0.0/10"), + + want: types.Nodes{ + &types.Node{ + IPv4DatabaseField: sql.NullString{ + Valid: true, + String: "100.64.0.1", + }, + IPv4: nap("100.64.0.1"), + }, + }, + }, + { + name: "simple-backfill-remove-ipv4", + dbFunc: func() *HSDatabase { + db := dbForTest(t, "simple-backfill-remove-ipv4") + + db.DB.Save(&types.Node{ + IPv4: nap("100.64.0.1"), + IPv6: nap("fd7a:115c:a1e0::1"), + }) + + return db + }, + + prefix6: mpp("fd7a:115c:a1e0::/48"), + + want: types.Nodes{ + &types.Node{ + IPv6DatabaseField: sql.NullString{ + Valid: true, + String: "fd7a:115c:a1e0::1", + }, + IPv6: nap("fd7a:115c:a1e0::1"), + }, + }, + }, + { + name: "multi-backfill-ipv6", + dbFunc: func() *HSDatabase { + db := dbForTest(t, "simple-backfill-ipv6") + + db.DB.Save(&types.Node{ + IPv4: nap("100.64.0.1"), + }) + db.DB.Save(&types.Node{ + IPv4: nap("100.64.0.2"), + }) + db.DB.Save(&types.Node{ + IPv4: nap("100.64.0.3"), + }) + db.DB.Save(&types.Node{ + IPv4: nap("100.64.0.4"), + }) + + return db + }, + + prefix4: mpp("100.64.0.0/10"), + prefix6: mpp("fd7a:115c:a1e0::/48"), + + want: types.Nodes{ + fullNodeP(1), + fullNodeP(2), + fullNodeP(3), + fullNodeP(4), + }, + }, + } + + comps := append(util.Comparers, cmpopts.IgnoreFields(types.Node{}, + "ID", + "MachineKeyDatabaseField", + "NodeKeyDatabaseField", + "DiscoKeyDatabaseField", + "Endpoints", + "HostinfoDatabaseField", + "Hostinfo", + "Routes", + "CreatedAt", + "UpdatedAt", + )) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db := tt.dbFunc() + + alloc, err := NewIPAllocator(db, tt.prefix4, tt.prefix6, types.IPAllocationStrategySequential) + if err != nil { + t.Fatalf("failed to set up ip alloc: %s", err) + } + + logs, err := db.BackfillNodeIPs(alloc) + if err != nil { + t.Fatalf("failed to backfill: %s", err) + } + + t.Logf("backfill log: \n%s", strings.Join(logs, "\n")) + + got, err := db.ListNodes() + if err != nil { + t.Fatalf("failed to get nodes: %s", err) + } + + if diff := cmp.Diff(tt.want, got, comps...); diff != "" { + t.Errorf("Backfill unexpected result (-want +got):\n%s", diff) } }) } diff --git a/hscontrol/db/node.go b/hscontrol/db/node.go index 4f8688c1..b3977214 100644 --- a/hscontrol/db/node.go +++ b/hscontrol/db/node.go @@ -5,7 +5,6 @@ import ( "fmt" "net/netip" "sort" - "strings" "time" "github.com/juanfont/headscale/hscontrol/types" @@ -294,7 +293,8 @@ func RegisterNodeFromAuthCallback( userName string, nodeExpiry *time.Time, registrationMethod string, - addrs types.NodeAddresses, + ipv4 *netip.Addr, + ipv6 *netip.Addr, ) (*types.Node, error) { log.Debug(). Str("machine_key", mkey.ShortString()). @@ -330,7 +330,7 @@ func RegisterNodeFromAuthCallback( node, err := RegisterNode( tx, registrationNode, - addrs, + ipv4, ipv6, ) if err == nil { @@ -346,14 +346,14 @@ func RegisterNodeFromAuthCallback( return nil, ErrNodeNotFoundRegistrationCache } -func (hsdb *HSDatabase) RegisterNode(node types.Node, addrs types.NodeAddresses) (*types.Node, error) { +func (hsdb *HSDatabase) RegisterNode(node types.Node, ipv4 *netip.Addr, ipv6 *netip.Addr) (*types.Node, error) { return Write(hsdb.DB, func(tx *gorm.DB) (*types.Node, error) { - return RegisterNode(tx, node, addrs) + return RegisterNode(tx, node, ipv4, ipv6) }) } // RegisterNode is executed from the CLI to register a new Node using its MachineKey. -func RegisterNode(tx *gorm.DB, node types.Node, addrs types.NodeAddresses) (*types.Node, error) { +func RegisterNode(tx *gorm.DB, node types.Node, ipv4 *netip.Addr, ipv6 *netip.Addr) (*types.Node, error) { log.Debug(). Str("node", node.Hostname). Str("machine_key", node.MachineKey.ShortString()). @@ -361,10 +361,10 @@ func RegisterNode(tx *gorm.DB, node types.Node, addrs types.NodeAddresses) (*typ Str("user", node.User.Name). Msg("Registering node") - // If the node exists and we had already IPs for it, we just save it + // If the node exists and it already has IP(s), we just save it // so we store the node.Expire and node.Nodekey that has been set when // adding it to the registrationCache - if len(node.IPAddresses) > 0 { + if node.IPv4 != nil || node.IPv6 != nil { if err := tx.Save(&node).Error; err != nil { return nil, fmt.Errorf("failed register existing node in the database: %w", err) } @@ -380,7 +380,8 @@ func RegisterNode(tx *gorm.DB, node types.Node, addrs types.NodeAddresses) (*typ return &node, nil } - node.IPAddresses = addrs + node.IPv4 = ipv4 + node.IPv6 = ipv6 if err := tx.Save(&node).Error; err != nil { return nil, fmt.Errorf("failed register(save) node in the database: %w", err) @@ -389,7 +390,6 @@ func RegisterNode(tx *gorm.DB, node types.Node, addrs types.NodeAddresses) (*typ log.Trace(). Caller(). Str("node", node.Hostname). - Str("ip", strings.Join(addrs.StringSlice(), ",")). Msg("Node registered with the database") return &node, nil diff --git a/hscontrol/db/node_test.go b/hscontrol/db/node_test.go index 0dbe7688..84116f25 100644 --- a/hscontrol/db/node_test.go +++ b/hscontrol/db/node_test.go @@ -188,13 +188,12 @@ func (s *Suite) TestGetACLFilteredPeers(c *check.C) { nodeKey := key.NewNode() machineKey := key.NewMachine() + v4 := netip.MustParseAddr(fmt.Sprintf("100.64.0.%v", strconv.Itoa(index+1))) node := types.Node{ - ID: types.NodeID(index), - MachineKey: machineKey.Public(), - NodeKey: nodeKey.Public(), - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr(fmt.Sprintf("100.64.0.%v", strconv.Itoa(index+1))), - }, + ID: types.NodeID(index), + MachineKey: machineKey.Public(), + NodeKey: nodeKey.Public(), + IPv4: &v4, Hostname: "testnode" + strconv.Itoa(index), UserID: stor[index%2].user.ID, RegisterMethod: util.RegisterMethodAuthKey, @@ -301,27 +300,6 @@ func (s *Suite) TestExpireNode(c *check.C) { c.Assert(nodeFromDB.IsExpired(), check.Equals, true) } -func (s *Suite) TestSerdeAddressStrignSlice(c *check.C) { - input := types.NodeAddresses([]netip.Addr{ - netip.MustParseAddr("192.0.2.1"), - netip.MustParseAddr("2001:db8::1"), - }) - serialized, err := input.Value() - c.Assert(err, check.IsNil) - if serial, ok := serialized.(string); ok { - c.Assert(serial, check.Equals, "192.0.2.1,2001:db8::1") - } - - var deserialized types.NodeAddresses - err = deserialized.Scan(serialized) - c.Assert(err, check.IsNil) - - c.Assert(len(deserialized), check.Equals, len(input)) - for i := range deserialized { - c.Assert(deserialized[i], check.Equals, input[i]) - } -} - func (s *Suite) TestGenerateGivenName(c *check.C) { user1, err := db.CreateUser("user-1") c.Assert(err, check.IsNil) @@ -561,6 +539,7 @@ func (s *Suite) TestAutoApproveRoutes(c *check.C) { // Check if a subprefix of an autoapproved route is approved route2 := netip.MustParsePrefix("10.11.0.0/24") + v4 := netip.MustParseAddr("100.64.0.1") node := types.Node{ ID: 0, MachineKey: machineKey.Public(), @@ -573,7 +552,7 @@ func (s *Suite) TestAutoApproveRoutes(c *check.C) { RequestTags: []string{"tag:exit"}, RoutableIPs: []netip.Prefix{defaultRouteV4, defaultRouteV6, route1, route2}, }, - IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")}, + IPv4: &v4, } db.DB.Save(&node) diff --git a/hscontrol/db/routes.go b/hscontrol/db/routes.go index a94e1a88..bc3f88a5 100644 --- a/hscontrol/db/routes.go +++ b/hscontrol/db/routes.go @@ -609,7 +609,7 @@ func EnableAutoApprovedRoutes( aclPolicy *policy.ACLPolicy, node *types.Node, ) error { - if len(node.IPAddresses) == 0 { + if node.IPv4 == nil && node.IPv6 == nil { return nil // This node has no IPAddresses, so can't possibly match any autoApprovers ACLs } @@ -652,7 +652,7 @@ func EnableAutoApprovedRoutes( } // approvedIPs should contain all of node's IPs if it matches the rule, so check for first - if approvedIps.Contains(node.IPAddresses[0]) { + if approvedIps.Contains(*node.IPv4) { approvedRoutes = append(approvedRoutes, advertisedRoute) } } diff --git a/hscontrol/grpcv1.go b/hscontrol/grpcv1.go index d5a1854e..a24dcead 100644 --- a/hscontrol/grpcv1.go +++ b/hscontrol/grpcv1.go @@ -3,6 +3,7 @@ package hscontrol import ( "context" + "errors" "fmt" "sort" "strings" @@ -195,7 +196,7 @@ func (api headscaleV1APIServer) RegisterNode( return nil, err } - addrs, err := api.h.ipAlloc.Next() + ipv4, ipv6, err := api.h.ipAlloc.Next() if err != nil { return nil, err } @@ -208,7 +209,7 @@ func (api headscaleV1APIServer) RegisterNode( request.GetUser(), nil, util.RegisterMethodCLI, - addrs, + ipv4, ipv6, ) }) if err != nil { @@ -468,6 +469,24 @@ func (api headscaleV1APIServer) MoveNode( return &v1.MoveNodeResponse{Node: node.Proto()}, nil } +func (api headscaleV1APIServer) BackfillNodeIPs( + ctx context.Context, + request *v1.BackfillNodeIPsRequest, +) (*v1.BackfillNodeIPsResponse, error) { + log.Trace().Msg("Backfill called") + + if !request.Confirmed { + return nil, errors.New("not confirmed, aborting") + } + + changes, err := api.h.db.BackfillNodeIPs(api.h.ipAlloc) + if err != nil { + return nil, err + } + + return &v1.BackfillNodeIPsResponse{Changes: changes}, nil +} + func (api headscaleV1APIServer) GetRoutes( ctx context.Context, request *v1.GetRoutesRequest, diff --git a/hscontrol/mapper/mapper.go b/hscontrol/mapper/mapper.go index 93ab1f71..fe8af4d3 100644 --- a/hscontrol/mapper/mapper.go +++ b/hscontrol/mapper/mapper.go @@ -174,8 +174,8 @@ func addNextDNSMetadata(resolvers []*dnstype.Resolver, node *types.Node) { "device_model": []string{node.Hostinfo.OS}, } - if len(node.IPAddresses) > 0 { - attrs.Add("device_ip", node.IPAddresses[0].String()) + if len(node.IPs()) > 0 { + attrs.Add("device_ip", node.IPs()[0].String()) } resolver.Addr = fmt.Sprintf("%s?%s", resolver.Addr, attrs.Encode()) diff --git a/hscontrol/mapper/mapper_test.go b/hscontrol/mapper/mapper_test.go index 3f4d6892..f6248470 100644 --- a/hscontrol/mapper/mapper_test.go +++ b/hscontrol/mapper/mapper_test.go @@ -17,6 +17,11 @@ import ( "tailscale.com/types/key" ) +var iap = func(ipStr string) *netip.Addr { + ip := netip.MustParseAddr(ipStr) + return &ip +} + func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) { mach := func(hostname, username string, userid uint) *types.Node { return &types.Node{ @@ -176,17 +181,17 @@ func Test_fullMapResponse(t *testing.T) { DiscoKey: mustDK( "discokey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084", ), - IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")}, - Hostname: "mini", - GivenName: "mini", - UserID: 0, - User: types.User{Name: "mini"}, - ForcedTags: []string{}, - AuthKeyID: 0, - AuthKey: &types.PreAuthKey{}, - LastSeen: &lastSeen, - Expiry: &expire, - Hostinfo: &tailcfg.Hostinfo{}, + IPv4: iap("100.64.0.1"), + Hostname: "mini", + GivenName: "mini", + UserID: 0, + User: types.User{Name: "mini"}, + ForcedTags: []string{}, + AuthKeyID: 0, + AuthKey: &types.PreAuthKey{}, + LastSeen: &lastSeen, + Expiry: &expire, + Hostinfo: &tailcfg.Hostinfo{}, Routes: []types.Route{ { Prefix: types.IPPrefix(netip.MustParsePrefix("0.0.0.0/0")), @@ -257,17 +262,17 @@ func Test_fullMapResponse(t *testing.T) { DiscoKey: mustDK( "discokey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084", ), - IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.2")}, - Hostname: "peer1", - GivenName: "peer1", - UserID: 0, - User: types.User{Name: "mini"}, - ForcedTags: []string{}, - LastSeen: &lastSeen, - Expiry: &expire, - Hostinfo: &tailcfg.Hostinfo{}, - Routes: []types.Route{}, - CreatedAt: created, + IPv4: iap("100.64.0.2"), + Hostname: "peer1", + GivenName: "peer1", + UserID: 0, + User: types.User{Name: "mini"}, + ForcedTags: []string{}, + LastSeen: &lastSeen, + Expiry: &expire, + Hostinfo: &tailcfg.Hostinfo{}, + Routes: []types.Route{}, + CreatedAt: created, } tailPeer1 := &tailcfg.Node{ @@ -312,17 +317,17 @@ func Test_fullMapResponse(t *testing.T) { DiscoKey: mustDK( "discokey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084", ), - IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.3")}, - Hostname: "peer2", - GivenName: "peer2", - UserID: 1, - User: types.User{Name: "peer2"}, - ForcedTags: []string{}, - LastSeen: &lastSeen, - Expiry: &expire, - Hostinfo: &tailcfg.Hostinfo{}, - Routes: []types.Route{}, - CreatedAt: created, + IPv4: iap("100.64.0.3"), + Hostname: "peer2", + GivenName: "peer2", + UserID: 1, + User: types.User{Name: "peer2"}, + ForcedTags: []string{}, + LastSeen: &lastSeen, + Expiry: &expire, + Hostinfo: &tailcfg.Hostinfo{}, + Routes: []types.Route{}, + CreatedAt: created, } tests := []struct { diff --git a/hscontrol/mapper/tail.go b/hscontrol/mapper/tail.go index 97d12e86..ac39d35e 100644 --- a/hscontrol/mapper/tail.go +++ b/hscontrol/mapper/tail.go @@ -44,7 +44,7 @@ func tailNode( pol *policy.ACLPolicy, cfg *types.Config, ) (*tailcfg.Node, error) { - addrs := node.IPAddresses.Prefixes() + addrs := node.Prefixes() allowedIPs := append( []netip.Prefix{}, diff --git a/hscontrol/mapper/tail_test.go b/hscontrol/mapper/tail_test.go index e79d9dc5..229f0f88 100644 --- a/hscontrol/mapper/tail_test.go +++ b/hscontrol/mapper/tail_test.go @@ -89,9 +89,7 @@ func TestTailNode(t *testing.T) { DiscoKey: mustDK( "discokey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084", ), - IPAddresses: []netip.Addr{ - netip.MustParseAddr("100.64.0.1"), - }, + IPv4: iap("100.64.0.1"), Hostname: "mini", GivenName: "mini", UserID: 0, diff --git a/hscontrol/oidc.go b/hscontrol/oidc.go index 2ac1b56c..0680ce2f 100644 --- a/hscontrol/oidc.go +++ b/hscontrol/oidc.go @@ -597,7 +597,7 @@ func (h *Headscale) registerNodeForOIDCCallback( machineKey *key.MachinePublic, expiry time.Time, ) error { - addrs, err := h.ipAlloc.Next() + ipv4, ipv6, err := h.ipAlloc.Next() if err != nil { return err } @@ -611,7 +611,7 @@ func (h *Headscale) registerNodeForOIDCCallback( user.Name, &expiry, util.RegisterMethodOIDC, - addrs, + ipv4, ipv6, ); err != nil { return err } diff --git a/hscontrol/policy/acls.go b/hscontrol/policy/acls.go index a4eee01e..0f6158c6 100644 --- a/hscontrol/policy/acls.go +++ b/hscontrol/policy/acls.go @@ -229,7 +229,7 @@ func ReduceFilterRules(node *types.Node, rules []tailcfg.FilterRule) []tailcfg.F continue } - if node.IPAddresses.InIPSet(expanded) { + if node.InIPSet(expanded) { dests = append(dests, dest) } @@ -306,7 +306,7 @@ func (pol *ACLPolicy) CompileSSHPolicy( return nil, err } - if !node.IPAddresses.InIPSet(destSet) { + if !node.InIPSet(destSet) { continue } @@ -744,7 +744,7 @@ func (pol *ACLPolicy) expandIPsFromGroup( for _, user := range users { filteredNodes := filterNodesByUser(nodes, user) for _, node := range filteredNodes { - node.IPAddresses.AppendToIPSet(&build) + node.AppendToIPSet(&build) } } @@ -760,7 +760,7 @@ func (pol *ACLPolicy) expandIPsFromTag( // check for forced tags for _, node := range nodes { if util.StringOrPrefixListContains(node.ForcedTags, alias) { - node.IPAddresses.AppendToIPSet(&build) + node.AppendToIPSet(&build) } } @@ -792,7 +792,7 @@ func (pol *ACLPolicy) expandIPsFromTag( } if util.StringOrPrefixListContains(node.Hostinfo.RequestTags, alias) { - node.IPAddresses.AppendToIPSet(&build) + node.AppendToIPSet(&build) } } } @@ -815,7 +815,7 @@ func (pol *ACLPolicy) expandIPsFromUser( } for _, node := range filteredNodes { - node.IPAddresses.AppendToIPSet(&build) + node.AppendToIPSet(&build) } return build.IPSet() @@ -833,7 +833,7 @@ func (pol *ACLPolicy) expandIPsFromSingleIP( build.Add(ip) for _, node := range matches { - node.IPAddresses.AppendToIPSet(&build) + node.AppendToIPSet(&build) } return build.IPSet() @@ -850,11 +850,11 @@ func (pol *ACLPolicy) expandIPsFromIPPrefix( // This is suboptimal and quite expensive, but if we only add the prefix, we will miss all the relevant IPv6 // addresses for the hosts that belong to tailscale. This doesnt really affect stuff like subnet routers. for _, node := range nodes { - for _, ip := range node.IPAddresses { + for _, ip := range node.IPs() { // log.Trace(). // Msgf("checking if node ip (%s) is part of prefix (%s): %v, is single ip prefix (%v), addr: %s", ip.String(), prefix.String(), prefix.Contains(ip), prefix.IsSingleIP(), prefix.Addr().String()) if prefix.Contains(ip) { - node.IPAddresses.AppendToIPSet(&build) + node.AppendToIPSet(&build) } } } diff --git a/hscontrol/policy/acls_test.go b/hscontrol/policy/acls_test.go index db1a0dd3..417ed1d1 100644 --- a/hscontrol/policy/acls_test.go +++ b/hscontrol/policy/acls_test.go @@ -16,6 +16,11 @@ import ( "tailscale.com/tailcfg" ) +var iap = func(ipStr string) *netip.Addr { + ip := netip.MustParseAddr(ipStr) + return &ip +} + func Test(t *testing.T) { check.TestingT(t) } @@ -387,14 +392,10 @@ acls: rules, err := pol.CompileFilterRules(types.Nodes{ &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.100.100.100"), - }, + IPv4: iap("100.100.100.100"), }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("200.200.200.200"), - }, + IPv4: iap("200.200.200.200"), User: types.User{ Name: "testuser", }, @@ -997,12 +998,10 @@ func Test_expandAlias(t *testing.T) { alias: "*", nodes: types.Nodes{ &types.Node{ - IPAddresses: types.NodeAddresses{netip.MustParseAddr("100.64.0.1")}, + IPv4: iap("100.64.0.1"), }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.78.84.227"), - }, + IPv4: iap("100.78.84.227"), }, }, }, @@ -1023,27 +1022,19 @@ func Test_expandAlias(t *testing.T) { alias: "group:accountant", nodes: types.Nodes{ &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.1"), - }, + IPv4: iap("100.64.0.1"), User: types.User{Name: "joe"}, }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.2"), - }, + IPv4: iap("100.64.0.2"), User: types.User{Name: "joe"}, }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.3"), - }, + IPv4: iap("100.64.0.3"), User: types.User{Name: "marc"}, }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.4"), - }, + IPv4: iap("100.64.0.4"), User: types.User{Name: "mickael"}, }, }, @@ -1064,27 +1055,19 @@ func Test_expandAlias(t *testing.T) { alias: "group:hr", nodes: types.Nodes{ &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.1"), - }, + IPv4: iap("100.64.0.1"), User: types.User{Name: "joe"}, }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.2"), - }, + IPv4: iap("100.64.0.2"), User: types.User{Name: "joe"}, }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.3"), - }, + IPv4: iap("100.64.0.3"), User: types.User{Name: "marc"}, }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.4"), - }, + IPv4: iap("100.64.0.4"), User: types.User{Name: "mickael"}, }, }, @@ -1129,9 +1112,7 @@ func Test_expandAlias(t *testing.T) { alias: "10.0.0.1", nodes: types.Nodes{ &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("10.0.0.1"), - }, + IPv4: iap("10.0.0.1"), User: types.User{Name: "mickael"}, }, }, @@ -1150,10 +1131,8 @@ func Test_expandAlias(t *testing.T) { alias: "10.0.0.1", nodes: types.Nodes{ &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("10.0.0.1"), - netip.MustParseAddr("fd7a:115c:a1e0:ab12:4843:2222:6273:2222"), - }, + IPv4: iap("10.0.0.1"), + IPv6: iap("fd7a:115c:a1e0:ab12:4843:2222:6273:2222"), User: types.User{Name: "mickael"}, }, }, @@ -1172,10 +1151,8 @@ func Test_expandAlias(t *testing.T) { alias: "fd7a:115c:a1e0:ab12:4843:2222:6273:2222", nodes: types.Nodes{ &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("10.0.0.1"), - netip.MustParseAddr("fd7a:115c:a1e0:ab12:4843:2222:6273:2222"), - }, + IPv4: iap("10.0.0.1"), + IPv6: iap("fd7a:115c:a1e0:ab12:4843:2222:6273:2222"), User: types.User{Name: "mickael"}, }, }, @@ -1241,9 +1218,7 @@ func Test_expandAlias(t *testing.T) { alias: "tag:hr-webserver", nodes: types.Nodes{ &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.1"), - }, + IPv4: iap("100.64.0.1"), User: types.User{Name: "joe"}, Hostinfo: &tailcfg.Hostinfo{ OS: "centos", @@ -1252,9 +1227,7 @@ func Test_expandAlias(t *testing.T) { }, }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.2"), - }, + IPv4: iap("100.64.0.2"), User: types.User{Name: "joe"}, Hostinfo: &tailcfg.Hostinfo{ OS: "centos", @@ -1263,15 +1236,11 @@ func Test_expandAlias(t *testing.T) { }, }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.3"), - }, + IPv4: iap("100.64.0.3"), User: types.User{Name: "marc"}, }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.4"), - }, + IPv4: iap("100.64.0.4"), User: types.User{Name: "joe"}, }, }, @@ -1295,27 +1264,19 @@ func Test_expandAlias(t *testing.T) { alias: "tag:hr-webserver", nodes: types.Nodes{ &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.1"), - }, + IPv4: iap("100.64.0.1"), User: types.User{Name: "joe"}, }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.2"), - }, + IPv4: iap("100.64.0.2"), User: types.User{Name: "joe"}, }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.3"), - }, + IPv4: iap("100.64.0.3"), User: types.User{Name: "marc"}, }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.4"), - }, + IPv4: iap("100.64.0.4"), User: types.User{Name: "mickael"}, }, }, @@ -1332,29 +1293,21 @@ func Test_expandAlias(t *testing.T) { alias: "tag:hr-webserver", nodes: types.Nodes{ &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.1"), - }, + IPv4: iap("100.64.0.1"), User: types.User{Name: "joe"}, ForcedTags: []string{"tag:hr-webserver"}, }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.2"), - }, + IPv4: iap("100.64.0.2"), User: types.User{Name: "joe"}, ForcedTags: []string{"tag:hr-webserver"}, }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.3"), - }, + IPv4: iap("100.64.0.3"), User: types.User{Name: "marc"}, }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.4"), - }, + IPv4: iap("100.64.0.4"), User: types.User{Name: "mickael"}, }, }, @@ -1375,16 +1328,12 @@ func Test_expandAlias(t *testing.T) { alias: "tag:hr-webserver", nodes: types.Nodes{ &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.1"), - }, + IPv4: iap("100.64.0.1"), User: types.User{Name: "joe"}, ForcedTags: []string{"tag:hr-webserver"}, }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.2"), - }, + IPv4: iap("100.64.0.2"), User: types.User{Name: "joe"}, Hostinfo: &tailcfg.Hostinfo{ OS: "centos", @@ -1393,15 +1342,11 @@ func Test_expandAlias(t *testing.T) { }, }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.3"), - }, + IPv4: iap("100.64.0.3"), User: types.User{Name: "marc"}, }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.4"), - }, + IPv4: iap("100.64.0.4"), User: types.User{Name: "mickael"}, }, }, @@ -1420,9 +1365,7 @@ func Test_expandAlias(t *testing.T) { alias: "joe", nodes: types.Nodes{ &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.1"), - }, + IPv4: iap("100.64.0.1"), User: types.User{Name: "joe"}, Hostinfo: &tailcfg.Hostinfo{ OS: "centos", @@ -1431,9 +1374,7 @@ func Test_expandAlias(t *testing.T) { }, }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.2"), - }, + IPv4: iap("100.64.0.2"), User: types.User{Name: "joe"}, Hostinfo: &tailcfg.Hostinfo{ OS: "centos", @@ -1442,16 +1383,12 @@ func Test_expandAlias(t *testing.T) { }, }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.3"), - }, + IPv4: iap("100.64.0.3"), User: types.User{Name: "marc"}, Hostinfo: &tailcfg.Hostinfo{}, }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.4"), - }, + IPv4: iap("100.64.0.4"), User: types.User{Name: "joe"}, Hostinfo: &tailcfg.Hostinfo{}, }, @@ -1499,9 +1436,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) { }, nodes: types.Nodes{ &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.1"), - }, + IPv4: iap("100.64.0.1"), User: types.User{Name: "joe"}, Hostinfo: &tailcfg.Hostinfo{ OS: "centos", @@ -1510,9 +1445,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) { }, }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.2"), - }, + IPv4: iap("100.64.0.2"), User: types.User{Name: "joe"}, Hostinfo: &tailcfg.Hostinfo{ OS: "centos", @@ -1521,9 +1454,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) { }, }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.4"), - }, + IPv4: iap("100.64.0.4"), User: types.User{Name: "joe"}, Hostinfo: &tailcfg.Hostinfo{}, }, @@ -1532,9 +1463,9 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) { }, want: types.Nodes{ &types.Node{ - IPAddresses: types.NodeAddresses{netip.MustParseAddr("100.64.0.4")}, - User: types.User{Name: "joe"}, - Hostinfo: &tailcfg.Hostinfo{}, + IPv4: iap("100.64.0.4"), + User: types.User{Name: "joe"}, + Hostinfo: &tailcfg.Hostinfo{}, }, }, }, @@ -1551,9 +1482,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) { }, nodes: types.Nodes{ &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.1"), - }, + IPv4: iap("100.64.0.1"), User: types.User{Name: "joe"}, Hostinfo: &tailcfg.Hostinfo{ OS: "centos", @@ -1562,9 +1491,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) { }, }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.2"), - }, + IPv4: iap("100.64.0.2"), User: types.User{Name: "joe"}, Hostinfo: &tailcfg.Hostinfo{ OS: "centos", @@ -1573,9 +1500,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) { }, }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.4"), - }, + IPv4: iap("100.64.0.4"), User: types.User{Name: "joe"}, Hostinfo: &tailcfg.Hostinfo{}, }, @@ -1584,9 +1509,9 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) { }, want: types.Nodes{ &types.Node{ - IPAddresses: types.NodeAddresses{netip.MustParseAddr("100.64.0.4")}, - User: types.User{Name: "joe"}, - Hostinfo: &tailcfg.Hostinfo{}, + IPv4: iap("100.64.0.4"), + User: types.User{Name: "joe"}, + Hostinfo: &tailcfg.Hostinfo{}, }, }, }, @@ -1598,9 +1523,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) { }, nodes: types.Nodes{ &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.1"), - }, + IPv4: iap("100.64.0.1"), User: types.User{Name: "joe"}, Hostinfo: &tailcfg.Hostinfo{ OS: "centos", @@ -1609,17 +1532,13 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) { }, }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.2"), - }, + IPv4: iap("100.64.0.2"), User: types.User{Name: "joe"}, ForcedTags: []string{"tag:accountant-webserver"}, Hostinfo: &tailcfg.Hostinfo{}, }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.4"), - }, + IPv4: iap("100.64.0.4"), User: types.User{Name: "joe"}, Hostinfo: &tailcfg.Hostinfo{}, }, @@ -1628,9 +1547,9 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) { }, want: types.Nodes{ &types.Node{ - IPAddresses: types.NodeAddresses{netip.MustParseAddr("100.64.0.4")}, - User: types.User{Name: "joe"}, - Hostinfo: &tailcfg.Hostinfo{}, + IPv4: iap("100.64.0.4"), + User: types.User{Name: "joe"}, + Hostinfo: &tailcfg.Hostinfo{}, }, }, }, @@ -1642,9 +1561,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) { }, nodes: types.Nodes{ &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.1"), - }, + IPv4: iap("100.64.0.1"), User: types.User{Name: "joe"}, Hostinfo: &tailcfg.Hostinfo{ OS: "centos", @@ -1653,9 +1570,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) { }, }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.2"), - }, + IPv4: iap("100.64.0.2"), User: types.User{Name: "joe"}, Hostinfo: &tailcfg.Hostinfo{ OS: "centos", @@ -1664,9 +1579,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) { }, }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.4"), - }, + IPv4: iap("100.64.0.4"), User: types.User{Name: "joe"}, Hostinfo: &tailcfg.Hostinfo{}, }, @@ -1675,9 +1588,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) { }, want: types.Nodes{ &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.1"), - }, + IPv4: iap("100.64.0.1"), User: types.User{Name: "joe"}, Hostinfo: &tailcfg.Hostinfo{ OS: "centos", @@ -1686,9 +1597,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) { }, }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.2"), - }, + IPv4: iap("100.64.0.2"), User: types.User{Name: "joe"}, Hostinfo: &tailcfg.Hostinfo{ OS: "centos", @@ -1697,9 +1606,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) { }, }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.4"), - }, + IPv4: iap("100.64.0.4"), User: types.User{Name: "joe"}, Hostinfo: &tailcfg.Hostinfo{}, }, @@ -1757,10 +1664,8 @@ func TestACLPolicy_generateFilterRules(t *testing.T) { args: args{ nodes: types.Nodes{ &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.1"), - netip.MustParseAddr("fd7a:115c:a1e0:ab12:4843:2222:6273:2221"), - }, + IPv4: iap("100.64.0.1"), + IPv6: iap("fd7a:115c:a1e0:ab12:4843:2222:6273:2221"), }, }, }, @@ -1803,17 +1708,13 @@ func TestACLPolicy_generateFilterRules(t *testing.T) { args: args{ nodes: types.Nodes{ &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.1"), - netip.MustParseAddr("fd7a:115c:a1e0:ab12:4843:2222:6273:2221"), - }, + IPv4: iap("100.64.0.1"), + IPv6: iap("fd7a:115c:a1e0:ab12:4843:2222:6273:2221"), User: types.User{Name: "mickael"}, }, &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.2"), - netip.MustParseAddr("fd7a:115c:a1e0:ab12:4843:2222:6273:2222"), - }, + IPv4: iap("100.64.0.2"), + IPv6: iap("fd7a:115c:a1e0:ab12:4843:2222:6273:2222"), User: types.User{Name: "mickael"}, }, }, @@ -1884,18 +1785,14 @@ func TestReduceFilterRules(t *testing.T) { }, }, node: &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.1"), - netip.MustParseAddr("fd7a:115c:a1e0:ab12:4843:2222:6273:2221"), - }, + IPv4: iap("100.64.0.1"), + IPv6: iap("fd7a:115c:a1e0:ab12:4843:2222:6273:2221"), User: types.User{Name: "mickael"}, }, peers: types.Nodes{ &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.2"), - netip.MustParseAddr("fd7a:115c:a1e0:ab12:4843:2222:6273:2222"), - }, + IPv4: iap("100.64.0.2"), + IPv6: iap("fd7a:115c:a1e0:ab12:4843:2222:6273:2222"), User: types.User{Name: "mickael"}, }, }, @@ -1921,10 +1818,8 @@ func TestReduceFilterRules(t *testing.T) { }, }, node: &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.1"), - netip.MustParseAddr("fd7a:115c:a1e0::1"), - }, + IPv4: iap("100.64.0.1"), + IPv6: iap("fd7a:115c:a1e0::1"), User: types.User{Name: "user1"}, Hostinfo: &tailcfg.Hostinfo{ RoutableIPs: []netip.Prefix{ @@ -1934,10 +1829,8 @@ func TestReduceFilterRules(t *testing.T) { }, peers: types.Nodes{ &types.Node{ - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.2"), - netip.MustParseAddr("fd7a:115c:a1e0::2"), - }, + IPv4: iap("100.64.0.2"), + IPv6: iap("fd7a:115c:a1e0::2"), User: types.User{Name: "user1"}, }, }, @@ -2153,24 +2046,18 @@ func Test_getFilteredByACLPeers(t *testing.T) { args: args{ nodes: types.Nodes{ // list of all nodess in the database &types.Node{ - ID: 1, - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.1"), - }, + ID: 1, + IPv4: iap("100.64.0.1"), User: types.User{Name: "joe"}, }, &types.Node{ - ID: 2, - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.2"), - }, + ID: 2, + IPv4: iap("100.64.0.2"), User: types.User{Name: "marc"}, }, &types.Node{ - ID: 3, - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.3"), - }, + ID: 3, + IPv4: iap("100.64.0.3"), User: types.User{Name: "mickael"}, }, }, @@ -2183,21 +2070,21 @@ func Test_getFilteredByACLPeers(t *testing.T) { }, }, node: &types.Node{ // current nodes - ID: 1, - IPAddresses: types.NodeAddresses{netip.MustParseAddr("100.64.0.1")}, - User: types.User{Name: "joe"}, + ID: 1, + IPv4: iap("100.64.0.1"), + User: types.User{Name: "joe"}, }, }, want: types.Nodes{ &types.Node{ - ID: 2, - IPAddresses: types.NodeAddresses{netip.MustParseAddr("100.64.0.2")}, - User: types.User{Name: "marc"}, + ID: 2, + IPv4: iap("100.64.0.2"), + User: types.User{Name: "marc"}, }, &types.Node{ - ID: 3, - IPAddresses: types.NodeAddresses{netip.MustParseAddr("100.64.0.3")}, - User: types.User{Name: "mickael"}, + ID: 3, + IPv4: iap("100.64.0.3"), + User: types.User{Name: "mickael"}, }, }, }, @@ -2206,24 +2093,18 @@ func Test_getFilteredByACLPeers(t *testing.T) { args: args{ nodes: types.Nodes{ // list of all nodess in the database &types.Node{ - ID: 1, - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.1"), - }, + ID: 1, + IPv4: iap("100.64.0.1"), User: types.User{Name: "joe"}, }, &types.Node{ - ID: 2, - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.2"), - }, + ID: 2, + IPv4: iap("100.64.0.2"), User: types.User{Name: "marc"}, }, &types.Node{ - ID: 3, - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.3"), - }, + ID: 3, + IPv4: iap("100.64.0.3"), User: types.User{Name: "mickael"}, }, }, @@ -2236,16 +2117,16 @@ func Test_getFilteredByACLPeers(t *testing.T) { }, }, node: &types.Node{ // current nodes - ID: 1, - IPAddresses: types.NodeAddresses{netip.MustParseAddr("100.64.0.1")}, - User: types.User{Name: "joe"}, + ID: 1, + IPv4: iap("100.64.0.1"), + User: types.User{Name: "joe"}, }, }, want: types.Nodes{ &types.Node{ - ID: 2, - IPAddresses: types.NodeAddresses{netip.MustParseAddr("100.64.0.2")}, - User: types.User{Name: "marc"}, + ID: 2, + IPv4: iap("100.64.0.2"), + User: types.User{Name: "marc"}, }, }, }, @@ -2254,24 +2135,18 @@ func Test_getFilteredByACLPeers(t *testing.T) { args: args{ nodes: types.Nodes{ // list of all nodess in the database &types.Node{ - ID: 1, - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.1"), - }, + ID: 1, + IPv4: iap("100.64.0.1"), User: types.User{Name: "joe"}, }, &types.Node{ - ID: 2, - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.2"), - }, + ID: 2, + IPv4: iap("100.64.0.2"), User: types.User{Name: "marc"}, }, &types.Node{ - ID: 3, - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.3"), - }, + ID: 3, + IPv4: iap("100.64.0.3"), User: types.User{Name: "mickael"}, }, }, @@ -2284,16 +2159,16 @@ func Test_getFilteredByACLPeers(t *testing.T) { }, }, node: &types.Node{ // current nodes - ID: 2, - IPAddresses: types.NodeAddresses{netip.MustParseAddr("100.64.0.2")}, - User: types.User{Name: "marc"}, + ID: 2, + IPv4: iap("100.64.0.2"), + User: types.User{Name: "marc"}, }, }, want: types.Nodes{ &types.Node{ - ID: 3, - IPAddresses: types.NodeAddresses{netip.MustParseAddr("100.64.0.3")}, - User: types.User{Name: "mickael"}, + ID: 3, + IPv4: iap("100.64.0.3"), + User: types.User{Name: "mickael"}, }, }, }, @@ -2302,24 +2177,18 @@ func Test_getFilteredByACLPeers(t *testing.T) { args: args{ nodes: types.Nodes{ // list of all nodess in the database &types.Node{ - ID: 1, - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.1"), - }, + ID: 1, + IPv4: iap("100.64.0.1"), User: types.User{Name: "joe"}, }, &types.Node{ - ID: 2, - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.2"), - }, + ID: 2, + IPv4: iap("100.64.0.2"), User: types.User{Name: "marc"}, }, &types.Node{ - ID: 3, - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.3"), - }, + ID: 3, + IPv4: iap("100.64.0.3"), User: types.User{Name: "mickael"}, }, }, @@ -2332,19 +2201,15 @@ func Test_getFilteredByACLPeers(t *testing.T) { }, }, node: &types.Node{ // current nodes - ID: 1, - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.1"), - }, + ID: 1, + IPv4: iap("100.64.0.1"), User: types.User{Name: "joe"}, }, }, want: types.Nodes{ &types.Node{ - ID: 2, - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.2"), - }, + ID: 2, + IPv4: iap("100.64.0.2"), User: types.User{Name: "marc"}, }, }, @@ -2354,24 +2219,18 @@ func Test_getFilteredByACLPeers(t *testing.T) { args: args{ nodes: types.Nodes{ // list of all nodess in the database &types.Node{ - ID: 1, - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.1"), - }, + ID: 1, + IPv4: iap("100.64.0.1"), User: types.User{Name: "joe"}, }, &types.Node{ - ID: 2, - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.2"), - }, + ID: 2, + IPv4: iap("100.64.0.2"), User: types.User{Name: "marc"}, }, &types.Node{ - ID: 3, - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.3"), - }, + ID: 3, + IPv4: iap("100.64.0.3"), User: types.User{Name: "mickael"}, }, }, @@ -2384,26 +2243,20 @@ func Test_getFilteredByACLPeers(t *testing.T) { }, }, node: &types.Node{ // current nodes - ID: 2, - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.2"), - }, + ID: 2, + IPv4: iap("100.64.0.2"), User: types.User{Name: "marc"}, }, }, want: types.Nodes{ &types.Node{ - ID: 1, - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.1"), - }, + ID: 1, + IPv4: iap("100.64.0.1"), User: types.User{Name: "joe"}, }, &types.Node{ - ID: 3, - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.3"), - }, + ID: 3, + IPv4: iap("100.64.0.3"), User: types.User{Name: "mickael"}, }, }, @@ -2413,24 +2266,18 @@ func Test_getFilteredByACLPeers(t *testing.T) { args: args{ nodes: types.Nodes{ // list of all nodess in the database &types.Node{ - ID: 1, - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.1"), - }, + ID: 1, + IPv4: iap("100.64.0.1"), User: types.User{Name: "joe"}, }, &types.Node{ - ID: 2, - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.2"), - }, + ID: 2, + IPv4: iap("100.64.0.2"), User: types.User{Name: "marc"}, }, &types.Node{ - ID: 3, - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.3"), - }, + ID: 3, + IPv4: iap("100.64.0.3"), User: types.User{Name: "mickael"}, }, }, @@ -2443,23 +2290,21 @@ func Test_getFilteredByACLPeers(t *testing.T) { }, }, node: &types.Node{ // current nodes - ID: 2, - IPAddresses: types.NodeAddresses{netip.MustParseAddr("100.64.0.2")}, - User: types.User{Name: "marc"}, + ID: 2, + IPv4: iap("100.64.0.2"), + User: types.User{Name: "marc"}, }, }, want: types.Nodes{ &types.Node{ - ID: 1, - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.1"), - }, + ID: 1, + IPv4: iap("100.64.0.1"), User: types.User{Name: "joe"}, }, &types.Node{ - ID: 3, - IPAddresses: types.NodeAddresses{netip.MustParseAddr("100.64.0.3")}, - User: types.User{Name: "mickael"}, + ID: 3, + IPv4: iap("100.64.0.3"), + User: types.User{Name: "mickael"}, }, }, }, @@ -2468,33 +2313,27 @@ func Test_getFilteredByACLPeers(t *testing.T) { args: args{ nodes: types.Nodes{ // list of all nodess in the database &types.Node{ - ID: 1, - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.1"), - }, + ID: 1, + IPv4: iap("100.64.0.1"), User: types.User{Name: "joe"}, }, &types.Node{ - ID: 2, - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.2"), - }, + ID: 2, + IPv4: iap("100.64.0.2"), User: types.User{Name: "marc"}, }, &types.Node{ - ID: 3, - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.3"), - }, + ID: 3, + IPv4: iap("100.64.0.3"), User: types.User{Name: "mickael"}, }, }, rules: []tailcfg.FilterRule{ // list of all ACLRules registered }, node: &types.Node{ // current nodes - ID: 2, - IPAddresses: types.NodeAddresses{netip.MustParseAddr("100.64.0.2")}, - User: types.User{Name: "marc"}, + ID: 2, + IPv4: iap("100.64.0.2"), + User: types.User{Name: "marc"}, }, }, want: types.Nodes{}, @@ -2510,38 +2349,30 @@ func Test_getFilteredByACLPeers(t *testing.T) { &types.Node{ ID: 1, Hostname: "ts-head-upcrmb", - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.3"), - netip.MustParseAddr("fd7a:115c:a1e0::3"), - }, - User: types.User{Name: "user1"}, + IPv4: iap("100.64.0.3"), + IPv6: iap("fd7a:115c:a1e0::3"), + User: types.User{Name: "user1"}, }, &types.Node{ ID: 2, Hostname: "ts-unstable-rlwpvr", - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.4"), - netip.MustParseAddr("fd7a:115c:a1e0::4"), - }, - User: types.User{Name: "user1"}, + IPv4: iap("100.64.0.4"), + IPv6: iap("fd7a:115c:a1e0::4"), + User: types.User{Name: "user1"}, }, &types.Node{ ID: 3, Hostname: "ts-head-8w6paa", - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.1"), - netip.MustParseAddr("fd7a:115c:a1e0::1"), - }, - User: types.User{Name: "user2"}, + IPv4: iap("100.64.0.1"), + IPv6: iap("fd7a:115c:a1e0::1"), + User: types.User{Name: "user2"}, }, &types.Node{ ID: 4, Hostname: "ts-unstable-lys2ib", - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.2"), - netip.MustParseAddr("fd7a:115c:a1e0::2"), - }, - User: types.User{Name: "user2"}, + IPv4: iap("100.64.0.2"), + IPv6: iap("fd7a:115c:a1e0::2"), + User: types.User{Name: "user2"}, }, }, rules: []tailcfg.FilterRule{ // list of all ACLRules registered @@ -2561,31 +2392,25 @@ func Test_getFilteredByACLPeers(t *testing.T) { node: &types.Node{ // current nodes ID: 3, Hostname: "ts-head-8w6paa", - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.1"), - netip.MustParseAddr("fd7a:115c:a1e0::1"), - }, - User: types.User{Name: "user2"}, + IPv4: iap("100.64.0.1"), + IPv6: iap("fd7a:115c:a1e0::1"), + User: types.User{Name: "user2"}, }, }, want: types.Nodes{ &types.Node{ ID: 1, Hostname: "ts-head-upcrmb", - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.3"), - netip.MustParseAddr("fd7a:115c:a1e0::3"), - }, - User: types.User{Name: "user1"}, + IPv4: iap("100.64.0.3"), + IPv6: iap("fd7a:115c:a1e0::3"), + User: types.User{Name: "user1"}, }, &types.Node{ ID: 2, Hostname: "ts-unstable-rlwpvr", - IPAddresses: types.NodeAddresses{ - netip.MustParseAddr("100.64.0.4"), - netip.MustParseAddr("fd7a:115c:a1e0::4"), - }, - User: types.User{Name: "user1"}, + IPv4: iap("100.64.0.4"), + IPv6: iap("fd7a:115c:a1e0::4"), + User: types.User{Name: "user1"}, }, }, }, @@ -2594,16 +2419,16 @@ func Test_getFilteredByACLPeers(t *testing.T) { args: args{ nodes: []*types.Node{ { - ID: 1, - IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.2")}, - Hostname: "peer1", - User: types.User{Name: "mini"}, + ID: 1, + IPv4: iap("100.64.0.2"), + Hostname: "peer1", + User: types.User{Name: "mini"}, }, { - ID: 2, - IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.3")}, - Hostname: "peer2", - User: types.User{Name: "peer2"}, + ID: 2, + IPv4: iap("100.64.0.3"), + Hostname: "peer2", + User: types.User{Name: "peer2"}, }, }, rules: []tailcfg.FilterRule{ @@ -2616,18 +2441,18 @@ func Test_getFilteredByACLPeers(t *testing.T) { }, }, node: &types.Node{ - ID: 0, - IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")}, - Hostname: "mini", - User: types.User{Name: "mini"}, + ID: 0, + IPv4: iap("100.64.0.1"), + Hostname: "mini", + User: types.User{Name: "mini"}, }, }, want: []*types.Node{ { - ID: 2, - IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.3")}, - Hostname: "peer2", - User: types.User{Name: "peer2"}, + ID: 2, + IPv4: iap("100.64.0.3"), + Hostname: "peer2", + User: types.User{Name: "peer2"}, }, }, }, @@ -2636,22 +2461,22 @@ func Test_getFilteredByACLPeers(t *testing.T) { args: args{ nodes: []*types.Node{ { - ID: 1, - IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.2")}, - Hostname: "user1-2", - User: types.User{Name: "user1"}, + ID: 1, + IPv4: iap("100.64.0.2"), + Hostname: "user1-2", + User: types.User{Name: "user1"}, }, { - ID: 0, - IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")}, - Hostname: "user1-1", - User: types.User{Name: "user1"}, + ID: 0, + IPv4: iap("100.64.0.1"), + Hostname: "user1-1", + User: types.User{Name: "user1"}, }, { - ID: 3, - IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.4")}, - Hostname: "user2-2", - User: types.User{Name: "user2"}, + ID: 3, + IPv4: iap("100.64.0.4"), + Hostname: "user2-2", + User: types.User{Name: "user2"}, }, }, rules: []tailcfg.FilterRule{ @@ -2685,30 +2510,30 @@ func Test_getFilteredByACLPeers(t *testing.T) { }, }, node: &types.Node{ - ID: 2, - IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.3")}, - Hostname: "user-2-1", - User: types.User{Name: "user2"}, + ID: 2, + IPv4: iap("100.64.0.3"), + Hostname: "user-2-1", + User: types.User{Name: "user2"}, }, }, want: []*types.Node{ { - ID: 1, - IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.2")}, - Hostname: "user1-2", - User: types.User{Name: "user1"}, + ID: 1, + IPv4: iap("100.64.0.2"), + Hostname: "user1-2", + User: types.User{Name: "user1"}, }, { - ID: 0, - IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")}, - Hostname: "user1-1", - User: types.User{Name: "user1"}, + ID: 0, + IPv4: iap("100.64.0.1"), + Hostname: "user1-1", + User: types.User{Name: "user1"}, }, { - ID: 3, - IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.4")}, - Hostname: "user2-2", - User: types.User{Name: "user2"}, + ID: 3, + IPv4: iap("100.64.0.4"), + Hostname: "user2-2", + User: types.User{Name: "user2"}, }, }, }, @@ -2717,22 +2542,22 @@ func Test_getFilteredByACLPeers(t *testing.T) { args: args{ nodes: []*types.Node{ { - ID: 1, - IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.2")}, - Hostname: "user1-2", - User: types.User{Name: "user1"}, + ID: 1, + IPv4: iap("100.64.0.2"), + Hostname: "user1-2", + User: types.User{Name: "user1"}, }, { - ID: 2, - IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.3")}, - Hostname: "user-2-1", - User: types.User{Name: "user2"}, + ID: 2, + IPv4: iap("100.64.0.3"), + Hostname: "user-2-1", + User: types.User{Name: "user2"}, }, { - ID: 3, - IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.4")}, - Hostname: "user2-2", - User: types.User{Name: "user2"}, + ID: 3, + IPv4: iap("100.64.0.4"), + Hostname: "user2-2", + User: types.User{Name: "user2"}, }, }, rules: []tailcfg.FilterRule{ @@ -2766,30 +2591,30 @@ func Test_getFilteredByACLPeers(t *testing.T) { }, }, node: &types.Node{ - ID: 0, - IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")}, - Hostname: "user1-1", - User: types.User{Name: "user1"}, + ID: 0, + IPv4: iap("100.64.0.1"), + Hostname: "user1-1", + User: types.User{Name: "user1"}, }, }, want: []*types.Node{ { - ID: 1, - IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.2")}, - Hostname: "user1-2", - User: types.User{Name: "user1"}, + ID: 1, + IPv4: iap("100.64.0.2"), + Hostname: "user1-2", + User: types.User{Name: "user1"}, }, { - ID: 2, - IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.3")}, - Hostname: "user-2-1", - User: types.User{Name: "user2"}, + ID: 2, + IPv4: iap("100.64.0.3"), + Hostname: "user-2-1", + User: types.User{Name: "user2"}, }, { - ID: 3, - IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.4")}, - Hostname: "user2-2", - User: types.User{Name: "user2"}, + ID: 3, + IPv4: iap("100.64.0.4"), + Hostname: "user2-2", + User: types.User{Name: "user2"}, }, }, }, @@ -2799,16 +2624,16 @@ func Test_getFilteredByACLPeers(t *testing.T) { args: args{ nodes: []*types.Node{ { - ID: 1, - IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")}, - Hostname: "user1", - User: types.User{Name: "user1"}, + ID: 1, + IPv4: iap("100.64.0.1"), + Hostname: "user1", + User: types.User{Name: "user1"}, }, { - ID: 2, - IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.2")}, - Hostname: "router", - User: types.User{Name: "router"}, + ID: 2, + IPv4: iap("100.64.0.2"), + Hostname: "router", + User: types.User{Name: "router"}, Routes: types.Routes{ types.Route{ NodeID: 2, @@ -2830,18 +2655,18 @@ func Test_getFilteredByACLPeers(t *testing.T) { }, }, node: &types.Node{ - ID: 1, - IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")}, - Hostname: "user1", - User: types.User{Name: "user1"}, + ID: 1, + IPv4: iap("100.64.0.1"), + Hostname: "user1", + User: types.User{Name: "user1"}, }, }, want: []*types.Node{ { - ID: 2, - IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.2")}, - Hostname: "router", - User: types.User{Name: "router"}, + ID: 2, + IPv4: iap("100.64.0.2"), + Hostname: "router", + User: types.User{Name: "router"}, Routes: types.Routes{ types.Route{ NodeID: 2, @@ -2887,18 +2712,18 @@ func TestSSHRules(t *testing.T) { { name: "peers-can-connect", node: types.Node{ - Hostname: "testnodes", - IPAddresses: types.NodeAddresses{netip.MustParseAddr("100.64.99.42")}, - UserID: 0, + Hostname: "testnodes", + IPv4: iap("100.64.99.42"), + UserID: 0, User: types.User{ Name: "user1", }, }, peers: types.Nodes{ &types.Node{ - Hostname: "testnodes2", - IPAddresses: types.NodeAddresses{netip.MustParseAddr("100.64.0.1")}, - UserID: 0, + Hostname: "testnodes2", + IPv4: iap("100.64.0.1"), + UserID: 0, User: types.User{ Name: "user1", }, @@ -2995,18 +2820,18 @@ func TestSSHRules(t *testing.T) { { name: "peers-cannot-connect", node: types.Node{ - Hostname: "testnodes", - IPAddresses: types.NodeAddresses{netip.MustParseAddr("100.64.0.1")}, - UserID: 0, + Hostname: "testnodes", + IPv4: iap("100.64.0.1"), + UserID: 0, User: types.User{ Name: "user1", }, }, peers: types.Nodes{ &types.Node{ - Hostname: "testnodes2", - IPAddresses: types.NodeAddresses{netip.MustParseAddr("100.64.99.42")}, - UserID: 0, + Hostname: "testnodes2", + IPv4: iap("100.64.99.42"), + UserID: 0, User: types.User{ Name: "user1", }, @@ -3131,10 +2956,10 @@ func TestValidExpandTagOwnersInSources(t *testing.T) { } node := &types.Node{ - ID: 0, - Hostname: "testnodes", - IPAddresses: types.NodeAddresses{netip.MustParseAddr("100.64.0.1")}, - UserID: 0, + ID: 0, + Hostname: "testnodes", + IPv4: iap("100.64.0.1"), + UserID: 0, User: types.User{ Name: "user1", }, @@ -3183,10 +3008,10 @@ func TestInvalidTagValidUser(t *testing.T) { } node := &types.Node{ - ID: 1, - Hostname: "testnodes", - IPAddresses: types.NodeAddresses{netip.MustParseAddr("100.64.0.1")}, - UserID: 1, + ID: 1, + Hostname: "testnodes", + IPv4: iap("100.64.0.1"), + UserID: 1, User: types.User{ Name: "user1", }, @@ -3234,10 +3059,10 @@ func TestValidExpandTagOwnersInDestinations(t *testing.T) { } node := &types.Node{ - ID: 1, - Hostname: "testnodes", - IPAddresses: types.NodeAddresses{netip.MustParseAddr("100.64.0.1")}, - UserID: 1, + ID: 1, + Hostname: "testnodes", + IPv4: iap("100.64.0.1"), + UserID: 1, User: types.User{ Name: "user1", }, @@ -3295,10 +3120,10 @@ func TestValidTagInvalidUser(t *testing.T) { } node := &types.Node{ - ID: 1, - Hostname: "webserver", - IPAddresses: types.NodeAddresses{netip.MustParseAddr("100.64.0.1")}, - UserID: 1, + ID: 1, + Hostname: "webserver", + IPv4: iap("100.64.0.1"), + UserID: 1, User: types.User{ Name: "user1", }, @@ -3312,10 +3137,10 @@ func TestValidTagInvalidUser(t *testing.T) { } nodes2 := &types.Node{ - ID: 2, - Hostname: "user", - IPAddresses: types.NodeAddresses{netip.MustParseAddr("100.64.0.2")}, - UserID: 1, + ID: 2, + Hostname: "user", + IPv4: iap("100.64.0.2"), + UserID: 1, User: types.User{ Name: "user1", }, diff --git a/hscontrol/types/config.go b/hscontrol/types/config.go index 4e4b9a61..5cc11668 100644 --- a/hscontrol/types/config.go +++ b/hscontrol/types/config.go @@ -31,6 +31,13 @@ var errOidcMutuallyExclusive = errors.New( "oidc_client_secret and oidc_client_secret_path are mutually exclusive", ) +type IPAllocationStrategy string + +const ( + IPAllocationStrategySequential IPAllocationStrategy = "sequential" + IPAllocationStrategyRandom IPAllocationStrategy = "random" +) + // Config contains the initial Headscale configuration. type Config struct { ServerURL string @@ -42,6 +49,7 @@ type Config struct { NodeUpdateCheckInterval time.Duration PrefixV4 *netip.Prefix PrefixV6 *netip.Prefix + IPAllocation IPAllocationStrategy NoisePrivateKeyPath string BaseDomain string Log LogConfig @@ -230,6 +238,8 @@ func LoadConfig(path string, isFile bool) error { viper.SetDefault("tuning.batch_change_delay", "800ms") viper.SetDefault("tuning.node_mapsession_buffered_chan_size", 30) + viper.SetDefault("prefixes.allocation", IPAllocationStrategySequential) + if IsCLIConfigured() { return nil } @@ -579,18 +589,16 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) { return nil, "" } -func Prefixes() (*netip.Prefix, *netip.Prefix, error) { +func PrefixV4() (*netip.Prefix, error) { prefixV4Str := viper.GetString("prefixes.v4") - prefixV6Str := viper.GetString("prefixes.v6") + + if prefixV4Str == "" { + return nil, nil + } prefixV4, err := netip.ParsePrefix(prefixV4Str) if err != nil { - return nil, nil, err - } - - prefixV6, err := netip.ParsePrefix(prefixV6Str) - if err != nil { - return nil, nil, err + return nil, fmt.Errorf("parsing IPv4 prefix from config: %w", err) } builder := netipx.IPSetBuilder{} @@ -603,13 +611,33 @@ func Prefixes() (*netip.Prefix, *netip.Prefix, error) { prefixV4Str, tsaddr.CGNATRange()) } + return &prefixV4, nil +} + +func PrefixV6() (*netip.Prefix, error) { + prefixV6Str := viper.GetString("prefixes.v6") + + if prefixV6Str == "" { + return nil, nil + } + + prefixV6, err := netip.ParsePrefix(prefixV6Str) + if err != nil { + return nil, fmt.Errorf("parsing IPv6 prefix from config: %w", err) + } + + builder := netipx.IPSetBuilder{} + builder.AddPrefix(tsaddr.CGNATRange()) + builder.AddPrefix(tsaddr.TailscaleULARange()) + ipSet, _ := builder.IPSet() + if !ipSet.ContainsPrefix(prefixV6) { log.Warn(). Msgf("Prefix %s is not in the %s range. This is an unsupported configuration.", prefixV6Str, tsaddr.TailscaleULARange()) } - return &prefixV4, &prefixV6, nil + return &prefixV6, nil } func GetHeadscaleConfig() (*Config, error) { @@ -624,11 +652,27 @@ func GetHeadscaleConfig() (*Config, error) { }, nil } - prefix4, prefix6, err := Prefixes() + prefix4, err := PrefixV4() if err != nil { return nil, err } + prefix6, err := PrefixV6() + if err != nil { + return nil, err + } + + allocStr := viper.GetString("prefixes.allocation") + var alloc IPAllocationStrategy + switch allocStr { + case string(IPAllocationStrategySequential): + alloc = IPAllocationStrategySequential + case string(IPAllocationStrategyRandom): + alloc = IPAllocationStrategyRandom + default: + log.Fatal().Msgf("config error, prefixes.allocation is set to %s, which is not a valid strategy, allowed options: %s, %s", allocStr, IPAllocationStrategySequential, IPAllocationStrategyRandom) + } + dnsConfig, baseDomain := GetDNSConfig() derpConfig := GetDERPConfig() logConfig := GetLogTailConfig() @@ -655,8 +699,9 @@ func GetHeadscaleConfig() (*Config, error) { GRPCAllowInsecure: viper.GetBool("grpc_allow_insecure"), DisableUpdateCheck: viper.GetBool("disable_check_updates"), - PrefixV4: prefix4, - PrefixV6: prefix6, + PrefixV4: prefix4, + PrefixV6: prefix6, + IPAllocation: IPAllocationStrategy(alloc), NoisePrivateKeyPath: util.AbsolutePathFromConfigPath( viper.GetString("noise.private_key_path"), diff --git a/hscontrol/types/node.go b/hscontrol/types/node.go index 2d6c6310..0e30bd9e 100644 --- a/hscontrol/types/node.go +++ b/hscontrol/types/node.go @@ -1,12 +1,11 @@ package types import ( - "database/sql/driver" + "database/sql" "encoding/json" "errors" "fmt" "net/netip" - "sort" "strconv" "strings" "time" @@ -14,7 +13,6 @@ 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" @@ -83,7 +81,19 @@ type Node struct { HostinfoDatabaseField string `gorm:"column:host_info"` Hostinfo *tailcfg.Hostinfo `gorm:"-"` - IPAddresses NodeAddresses + // IPv4DatabaseField is the string representation of v4 address, + // it is _only_ used for reading and writing the key to the + // database and should not be used. + // Use V4 instead. + IPv4DatabaseField sql.NullString `gorm:"column:ipv4"` + IPv4 *netip.Addr `gorm:"-"` + + // IPv6DatabaseField is the string representation of v4 address, + // it is _only_ used for reading and writing the key to the + // database and should not be used. + // Use V6 instead. + IPv6DatabaseField sql.NullString `gorm:"column:ipv6"` + IPv6 *netip.Addr `gorm:"-"` // Hostname represents the name given by the Tailscale // client during registration @@ -123,89 +133,6 @@ type ( Nodes []*Node ) -type NodeAddresses []netip.Addr - -func (na NodeAddresses) Sort() { - sort.Slice(na, func(index1, index2 int) bool { - if na[index1].Is4() && na[index2].Is6() { - return true - } - if na[index1].Is6() && na[index2].Is4() { - return false - } - - return na[index1].Compare(na[index2]) < 0 - }) -} - -func (na NodeAddresses) StringSlice() []string { - na.Sort() - strSlice := make([]string, 0, len(na)) - for _, addr := range na { - strSlice = append(strSlice, addr.String()) - } - - return strSlice -} - -func (na NodeAddresses) Prefixes() []netip.Prefix { - addrs := []netip.Prefix{} - for _, nodeAddress := range na { - ip := netip.PrefixFrom(nodeAddress, nodeAddress.BitLen()) - addrs = append(addrs, ip) - } - - return addrs -} - -func (na NodeAddresses) InIPSet(set *netipx.IPSet) bool { - for _, nodeAddr := range na { - if set.Contains(nodeAddr) { - return true - } - } - - return false -} - -// AppendToIPSet adds the individual ips in NodeAddresses to a -// given netipx.IPSetBuilder. -func (na NodeAddresses) AppendToIPSet(build *netipx.IPSetBuilder) { - for _, ip := range na { - build.Add(ip) - } -} - -func (na *NodeAddresses) Scan(destination interface{}) error { - switch value := destination.(type) { - case string: - addresses := strings.Split(value, ",") - *na = (*na)[:0] - for _, addr := range addresses { - if len(addr) < 1 { - continue - } - parsed, err := netip.ParseAddr(addr) - if err != nil { - return err - } - *na = append(*na, parsed) - } - - return nil - - default: - return fmt.Errorf("%w: unexpected data type %T", ErrNodeAddressesInvalid, destination) - } -} - -// Value return json value, implement driver.Valuer interface. -func (na NodeAddresses) Value() (driver.Value, error) { - addresses := strings.Join(na.StringSlice(), ",") - - return addresses, nil -} - // IsExpired returns whether the node registration has expired. func (node Node) IsExpired() bool { // If Expiry is not set, the client has not indicated that @@ -224,8 +151,65 @@ func (node *Node) IsEphemeral() bool { return node.AuthKey != nil && node.AuthKey.Ephemeral } +func (node *Node) IPs() []netip.Addr { + var ret []netip.Addr + + if node.IPv4 != nil { + ret = append(ret, *node.IPv4) + } + + if node.IPv6 != nil { + ret = append(ret, *node.IPv6) + } + + return ret +} + +func (node *Node) Prefixes() []netip.Prefix { + addrs := []netip.Prefix{} + for _, nodeAddress := range node.IPs() { + ip := netip.PrefixFrom(nodeAddress, nodeAddress.BitLen()) + addrs = append(addrs, ip) + } + + return addrs +} + +func (node *Node) IPsAsString() []string { + var ret []string + + if node.IPv4 != nil { + ret = append(ret, node.IPv4.String()) + } + + if node.IPv6 != nil { + ret = append(ret, node.IPv6.String()) + } + + return ret +} + +func (node *Node) InIPSet(set *netipx.IPSet) bool { + for _, nodeAddr := range node.IPs() { + if set.Contains(nodeAddr) { + return true + } + } + + return false +} + +// AppendToIPSet adds the individual ips in NodeAddresses to a +// given netipx.IPSetBuilder. +func (node *Node) AppendToIPSet(build *netipx.IPSetBuilder) { + for _, ip := range node.IPs() { + build.Add(ip) + } +} + func (node *Node) CanAccess(filter []tailcfg.FilterRule, node2 *Node) bool { - allowedIPs := append([]netip.Addr{}, node2.IPAddresses...) + src := node.IPs() + allowedIPs := node2.IPs() for _, route := range node2.Routes { if route.Enabled { @@ -237,7 +221,7 @@ func (node *Node) CanAccess(filter []tailcfg.FilterRule, node2 *Node) bool { // TODO(kradalby): Cache or pregen this matcher := matcher.MatchFromFilterRule(rule) - if !matcher.SrcsContainsIPs([]netip.Addr(node.IPAddresses)) { + if !matcher.SrcsContainsIPs(src) { continue } @@ -250,13 +234,16 @@ func (node *Node) CanAccess(filter []tailcfg.FilterRule, node2 *Node) bool { } func (nodes Nodes) FilterByIP(ip netip.Addr) Nodes { - found := make(Nodes, 0) + var found Nodes for _, node := range nodes { - for _, mIP := range node.IPAddresses { - if ip == mIP { - found = append(found, node) - } + if node.IPv4 != nil && ip == *node.IPv4 { + found = append(found, node) + continue + } + + if node.IPv6 != nil && ip == *node.IPv6 { + found = append(found, node) } } @@ -281,10 +268,22 @@ func (node *Node) BeforeSave(tx *gorm.DB) error { hi, err := json.Marshal(node.Hostinfo) if err != nil { - return fmt.Errorf("failed to marshal Hostinfo to store in db: %w", err) + return fmt.Errorf("marshalling Hostinfo to store in db: %w", err) } node.HostinfoDatabaseField = string(hi) + if node.IPv4 != nil { + node.IPv4DatabaseField.String, node.IPv4DatabaseField.Valid = node.IPv4.String(), true + } else { + node.IPv4DatabaseField.String, node.IPv4DatabaseField.Valid = "", false + } + + if node.IPv6 != nil { + node.IPv6DatabaseField.String, node.IPv6DatabaseField.Valid = node.IPv6.String(), true + } else { + node.IPv6DatabaseField.String, node.IPv6DatabaseField.Valid = "", false + } + return nil } @@ -296,19 +295,19 @@ func (node *Node) BeforeSave(tx *gorm.DB) error { 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) + return fmt.Errorf("unmarshalling 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) + return fmt.Errorf("unmarshalling 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) + return fmt.Errorf("unmarshalling disco key from db: %w", err) } node.DiscoKey = discoKey @@ -316,7 +315,7 @@ func (node *Node) AfterFind(tx *gorm.DB) error { 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) + return fmt.Errorf("parsing endpoint from db: %w", err) } endpoints[idx] = addrPort @@ -325,12 +324,28 @@ func (node *Node) AfterFind(tx *gorm.DB) error { 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) + return fmt.Errorf("unmarshalling hostinfo from database: %w", err) } node.Hostinfo = &hi + if node.IPv4DatabaseField.Valid { + ip, err := netip.ParseAddr(node.IPv4DatabaseField.String) + if err != nil { + return fmt.Errorf("parsing IPv4 from database: %w", err) + } + + node.IPv4 = &ip + } + + if node.IPv6DatabaseField.Valid { + ip, err := netip.ParseAddr(node.IPv6DatabaseField.String) + if err != nil { + return fmt.Errorf("parsing IPv6 from database: %w", err) + } + + node.IPv6 = &ip + } + return nil } @@ -339,9 +354,11 @@ func (node *Node) Proto() *v1.Node { Id: uint64(node.ID), MachineKey: node.MachineKey.String(), - NodeKey: node.NodeKey.String(), - DiscoKey: node.DiscoKey.String(), - IpAddresses: node.IPAddresses.StringSlice(), + NodeKey: node.NodeKey.String(), + DiscoKey: node.DiscoKey.String(), + + // TODO(kradalby): replace list with v4, v6 field? + IpAddresses: node.IPsAsString(), Name: node.Hostname, GivenName: node.GivenName, User: node.User.Proto(), diff --git a/hscontrol/types/node_test.go b/hscontrol/types/node_test.go index 712a839e..157be89e 100644 --- a/hscontrol/types/node_test.go +++ b/hscontrol/types/node_test.go @@ -12,6 +12,10 @@ import ( ) func Test_NodeCanAccess(t *testing.T) { + iap := func(ipStr string) *netip.Addr { + ip := netip.MustParseAddr(ipStr) + return &ip + } tests := []struct { name string node1 Node @@ -22,10 +26,10 @@ func Test_NodeCanAccess(t *testing.T) { { name: "no-rules", node1: Node{ - IPAddresses: []netip.Addr{netip.MustParseAddr("10.0.0.1")}, + IPv4: iap("10.0.0.1"), }, node2: Node{ - IPAddresses: []netip.Addr{netip.MustParseAddr("10.0.0.2")}, + IPv4: iap("10.0.0.2"), }, rules: []tailcfg.FilterRule{}, want: false, @@ -33,10 +37,10 @@ func Test_NodeCanAccess(t *testing.T) { { name: "wildcard", node1: Node{ - IPAddresses: []netip.Addr{netip.MustParseAddr("10.0.0.1")}, + IPv4: iap("10.0.0.1"), }, node2: Node{ - IPAddresses: []netip.Addr{netip.MustParseAddr("10.0.0.2")}, + IPv4: iap("10.0.0.2"), }, rules: []tailcfg.FilterRule{ { @@ -54,10 +58,10 @@ func Test_NodeCanAccess(t *testing.T) { { name: "other-cant-access-src", node1: Node{ - IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")}, + IPv4: iap("100.64.0.1"), }, node2: Node{ - IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.3")}, + IPv4: iap("100.64.0.3"), }, rules: []tailcfg.FilterRule{ { @@ -72,10 +76,10 @@ func Test_NodeCanAccess(t *testing.T) { { name: "dest-cant-access-src", node1: Node{ - IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.3")}, + IPv4: iap("100.64.0.3"), }, node2: Node{ - IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.2")}, + IPv4: iap("100.64.0.2"), }, rules: []tailcfg.FilterRule{ { @@ -90,10 +94,10 @@ func Test_NodeCanAccess(t *testing.T) { { name: "src-can-access-dest", node1: Node{ - IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.2")}, + IPv4: iap("100.64.0.2"), }, node2: Node{ - IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.3")}, + IPv4: iap("100.64.0.3"), }, rules: []tailcfg.FilterRule{ { @@ -118,32 +122,6 @@ func Test_NodeCanAccess(t *testing.T) { } } -func TestNodeAddressesOrder(t *testing.T) { - machineAddresses := NodeAddresses{ - netip.MustParseAddr("2001:db8::2"), - netip.MustParseAddr("100.64.0.2"), - netip.MustParseAddr("2001:db8::1"), - netip.MustParseAddr("100.64.0.1"), - } - - strSlice := machineAddresses.StringSlice() - expected := []string{ - "100.64.0.1", - "100.64.0.2", - "2001:db8::1", - "2001:db8::2", - } - - if len(strSlice) != len(expected) { - t.Fatalf("unexpected slice length: got %v, want %v", len(strSlice), len(expected)) - } - for i, addr := range strSlice { - if addr != expected[i] { - t.Errorf("unexpected address at index %v: got %v, want %v", i, addr, expected[i]) - } - } -} - func TestNodeFQDN(t *testing.T) { tests := []struct { name string diff --git a/hscontrol/util/dns.go b/hscontrol/util/dns.go index c6bd2b69..ab3c90b7 100644 --- a/hscontrol/util/dns.go +++ b/hscontrol/util/dns.go @@ -103,33 +103,7 @@ func CheckForFQDNRules(name string) error { // From the netmask we can find out the wildcard bits (the bits that are not set in the netmask). // This allows us to then calculate the subnets included in the subsequent class block and generate the entries. -func GenerateMagicDNSRootDomains(ipPrefixes []netip.Prefix) []dnsname.FQDN { - fqdns := make([]dnsname.FQDN, 0, len(ipPrefixes)) - for _, ipPrefix := range ipPrefixes { - var generateDNSRoot func(netip.Prefix) []dnsname.FQDN - switch ipPrefix.Addr().BitLen() { - case ipv4AddressLength: - generateDNSRoot = generateIPv4DNSRootDomain - - case ipv6AddressLength: - generateDNSRoot = generateIPv6DNSRootDomain - - default: - panic( - fmt.Sprintf( - "unsupported IP version with address length %d", - ipPrefix.Addr().BitLen(), - ), - ) - } - - fqdns = append(fqdns, generateDNSRoot(ipPrefix)...) - } - - return fqdns -} - -func generateIPv4DNSRootDomain(ipPrefix netip.Prefix) []dnsname.FQDN { +func GenerateIPv4DNSRootDomain(ipPrefix netip.Prefix) []dnsname.FQDN { // Conversion to the std lib net.IPnet, a bit easier to operate netRange := netipx.PrefixIPNet(ipPrefix) maskBits, _ := netRange.Mask.Size() @@ -165,7 +139,27 @@ func generateIPv4DNSRootDomain(ipPrefix netip.Prefix) []dnsname.FQDN { return fqdns } -func generateIPv6DNSRootDomain(ipPrefix netip.Prefix) []dnsname.FQDN { +// generateMagicDNSRootDomains generates a list of DNS entries to be included in `Routes` in `MapResponse`. +// This list of reverse DNS entries instructs the OS on what subnets and domains the Tailscale embedded DNS +// server (listening in 100.100.100.100 udp/53) should be used for. +// +// Tailscale.com includes in the list: +// - the `BaseDomain` of the user +// - the reverse DNS entry for IPv6 (0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa., see below more on IPv6) +// - the reverse DNS entries for the IPv4 subnets covered by the user's `IPPrefix`. +// In the public SaaS this is [64-127].100.in-addr.arpa. +// +// The main purpose of this function is then generating the list of IPv4 entries. For the 100.64.0.0/10, this +// is clear, and could be hardcoded. But we are allowing any range as `IPPrefix`, so we need to find out the +// subnets when we have 172.16.0.0/16 (i.e., [0-255].16.172.in-addr.arpa.), or any other subnet. +// +// How IN-ADDR.ARPA domains work is defined in RFC1035 (section 3.5). Tailscale.com seems to adhere to this, +// and do not make use of RFC2317 ("Classless IN-ADDR.ARPA delegation") - hence generating the entries for the next +// class block only. + +// From the netmask we can find out the wildcard bits (the bits that are not set in the netmask). +// This allows us to then calculate the subnets included in the subsequent class block and generate the entries. +func GenerateIPv6DNSRootDomain(ipPrefix netip.Prefix) []dnsname.FQDN { const nibbleLen = 4 maskBits, _ := netipx.PrefixIPNet(ipPrefix).Mask.Size() diff --git a/hscontrol/util/dns_test.go b/hscontrol/util/dns_test.go index 9d9b08b3..2559cae6 100644 --- a/hscontrol/util/dns_test.go +++ b/hscontrol/util/dns_test.go @@ -148,10 +148,7 @@ func TestCheckForFQDNRules(t *testing.T) { } func TestMagicDNSRootDomains100(t *testing.T) { - prefixes := []netip.Prefix{ - netip.MustParsePrefix("100.64.0.0/10"), - } - domains := GenerateMagicDNSRootDomains(prefixes) + domains := GenerateIPv4DNSRootDomain(netip.MustParsePrefix("100.64.0.0/10")) found := false for _, domain := range domains { @@ -185,10 +182,7 @@ func TestMagicDNSRootDomains100(t *testing.T) { } func TestMagicDNSRootDomains172(t *testing.T) { - prefixes := []netip.Prefix{ - netip.MustParsePrefix("172.16.0.0/16"), - } - domains := GenerateMagicDNSRootDomains(prefixes) + domains := GenerateIPv4DNSRootDomain(netip.MustParsePrefix("172.16.0.0/16")) found := false for _, domain := range domains { @@ -213,20 +207,14 @@ func TestMagicDNSRootDomains172(t *testing.T) { // Happens when netmask is a multiple of 4 bits (sounds likely). func TestMagicDNSRootDomainsIPv6Single(t *testing.T) { - prefixes := []netip.Prefix{ - netip.MustParsePrefix("fd7a:115c:a1e0::/48"), - } - domains := GenerateMagicDNSRootDomains(prefixes) + domains := GenerateIPv6DNSRootDomain(netip.MustParsePrefix("fd7a:115c:a1e0::/48")) assert.Len(t, domains, 1) assert.Equal(t, "0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa.", domains[0].WithTrailingDot()) } func TestMagicDNSRootDomainsIPv6SingleMultiple(t *testing.T) { - prefixes := []netip.Prefix{ - netip.MustParsePrefix("fd7a:115c:a1e0::/50"), - } - domains := GenerateMagicDNSRootDomains(prefixes) + domains := GenerateIPv6DNSRootDomain(netip.MustParsePrefix("fd7a:115c:a1e0::/50")) yieldsRoot := func(dom string) bool { for _, candidate := range domains { diff --git a/integration/general_test.go b/integration/general_test.go index e9f9abea..ffd209d8 100644 --- a/integration/general_test.go +++ b/integration/general_test.go @@ -9,6 +9,7 @@ import ( "time" v1 "github.com/juanfont/headscale/gen/go/headscale/v1" + "github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/integration/hsic" "github.com/juanfont/headscale/integration/tsic" "github.com/rs/zerolog/log" @@ -39,6 +40,7 @@ func TestPingAllByIP(t *testing.T) { hsic.WithEmbeddedDERPServerOnly(), hsic.WithTLS(), hsic.WithHostnameAsServerURL(), + hsic.WithIPAllocationStrategy(types.IPAllocationStrategyRandom), ) assertNoErrHeadscaleEnv(t, err) diff --git a/integration/hsic/config.go b/integration/hsic/config.go index 64e6e6eb..d66fd34c 100644 --- a/integration/hsic/config.go +++ b/integration/hsic/config.go @@ -1,5 +1,7 @@ package hsic +import "github.com/juanfont/headscale/hscontrol/types" + // const ( // defaultEphemeralNodeInactivityTimeout = time.Second * 30 // defaultNodeUpdateCheckInterval = time.Second * 10 @@ -129,5 +131,9 @@ func DefaultConfigEnv() map[string]string { "HEADSCALE_DERP_URLS": "https://controlplane.tailscale.com/derpmap/default", "HEADSCALE_DERP_AUTO_UPDATE_ENABLED": "false", "HEADSCALE_DERP_UPDATE_FREQUENCY": "1m", + + // a bunch of tests (ACL/Policy) rely on predicable IP alloc, + // so ensure the sequential alloc is used by default. + "HEADSCALE_PREFIXES_ALLOCATION": string(types.IPAllocationStrategySequential), } } diff --git a/integration/hsic/hsic.go b/integration/hsic/hsic.go index 2bd60954..de4ec41f 100644 --- a/integration/hsic/hsic.go +++ b/integration/hsic/hsic.go @@ -24,6 +24,7 @@ import ( "github.com/davecgh/go-spew/spew" v1 "github.com/juanfont/headscale/gen/go/headscale/v1" "github.com/juanfont/headscale/hscontrol/policy" + "github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/util" "github.com/juanfont/headscale/integration/dockertestutil" "github.com/juanfont/headscale/integration/integrationutil" @@ -173,6 +174,13 @@ func WithPostgres() Option { } } +// WithIPAllocationStrategy sets the tests IP Allocation strategy. +func WithIPAllocationStrategy(strat types.IPAllocationStrategy) Option { + return func(hsic *HeadscaleInContainer) { + hsic.env["HEADSCALE_PREFIXES_ALLOCATION"] = string(strat) + } +} + // WithEmbeddedDERPServerOnly configures Headscale to start // and only use the embedded DERP server. // It requires WithTLS and WithHostnameAsServerURL to be diff --git a/proto/headscale/v1/headscale.proto b/proto/headscale/v1/headscale.proto index f8cc596f..1ccc7029 100644 --- a/proto/headscale/v1/headscale.proto +++ b/proto/headscale/v1/headscale.proto @@ -123,6 +123,13 @@ service HeadscaleService { post: "/api/v1/node/{node_id}/user" }; } + + rpc BackfillNodeIPs(BackfillNodeIPsRequest) returns (BackfillNodeIPsResponse) { + option (google.api.http) = { + post: "/api/v1/node/backfillips" + }; + } + // --- Node end --- // --- Route start --- diff --git a/proto/headscale/v1/node.proto b/proto/headscale/v1/node.proto index a9551530..26fe73c7 100644 --- a/proto/headscale/v1/node.proto +++ b/proto/headscale/v1/node.proto @@ -126,3 +126,11 @@ message DebugCreateNodeRequest { message DebugCreateNodeResponse { Node node = 1; } + +message BackfillNodeIPsRequest { + bool confirmed = 1; +} + +message BackfillNodeIPsResponse { + repeated string changes = 1; +} From d4af0c386c39116cd85c0397f2ebb3bfe8ccc33f Mon Sep 17 00:00:00 2001 From: Cas de Reuver Date: Wed, 17 Apr 2024 11:22:53 +0200 Subject: [PATCH 028/145] Log available update as warning (#1877) --- CHANGELOG.md | 1 + cmd/headscale/cli/root.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54ad9a55..e8c7590f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,7 @@ after improving the test harness as part of adopting [#1460](https://github.com/ - Added the possibility to manually create a DERP-map entry which can be customized, instead of automatically creating it. [#1565](https://github.com/juanfont/headscale/pull/1565) - Add support for deleting api keys [#1702](https://github.com/juanfont/headscale/pull/1702) - Add command to backfill IP addresses for nodes missing IPs from configured prefixes. [#1869](https://github.com/juanfont/headscale/pull/1869) +- Log available update as warning [#1877](https://github.com/juanfont/headscale/pull/1877) ## 0.22.3 (2023-05-12) diff --git a/cmd/headscale/cli/root.go b/cmd/headscale/cli/root.go index 40a9b18a..68298c55 100644 --- a/cmd/headscale/cli/root.go +++ b/cmd/headscale/cli/root.go @@ -78,7 +78,7 @@ func initConfig() { res, err := latest.Check(githubTag, Version) if err == nil && res.Outdated { //nolint - fmt.Printf( + log.Warn().Msgf( "An updated version of Headscale has been found (%s vs. your current %s). Check it out https://github.com/juanfont/headscale/releases\n", res.Current, Version, From 40953727cfb9fa9861becd7d34d44591d18528d5 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 17 Apr 2024 11:09:22 +0200 Subject: [PATCH 029/145] fix ip migration Signed-off-by: Kristoffer Dalby --- hscontrol/db/db.go | 9 +++++++-- hscontrol/types/config.go | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/hscontrol/db/db.go b/hscontrol/db/db.go index b219ffe1..e54ea19f 100644 --- a/hscontrol/db/db.go +++ b/hscontrol/db/db.go @@ -356,7 +356,7 @@ func NewHeadscaleDatabase( addrs := strings.Split(node.Addresses, ",") if len(addrs) == 0 { - fmt.Errorf("no addresses found for node(%d)", node.ID) + return fmt.Errorf("no addresses found for node(%d)", node.ID) } var v4 *netip.Addr @@ -377,7 +377,12 @@ func NewHeadscaleDatabase( } } - err = tx.Save(&types.Node{ID: types.NodeID(node.ID), IPv4: v4, IPv6: v6}).Error + err = tx.Model(&types.Node{}).Where("id = ?", node.ID).Update("ipv4", v4.String()).Error + if err != nil { + return fmt.Errorf("saving ip addresses to new columns: %w", err) + } + + err = tx.Model(&types.Node{}).Where("id = ?", node.ID).Update("ipv6", v6.String()).Error if err != nil { return fmt.Errorf("saving ip addresses to new columns: %w", err) } diff --git a/hscontrol/types/config.go b/hscontrol/types/config.go index 5cc11668..f8073608 100644 --- a/hscontrol/types/config.go +++ b/hscontrol/types/config.go @@ -238,7 +238,7 @@ func LoadConfig(path string, isFile bool) error { viper.SetDefault("tuning.batch_change_delay", "800ms") viper.SetDefault("tuning.node_mapsession_buffered_chan_size", 30) - viper.SetDefault("prefixes.allocation", IPAllocationStrategySequential) + viper.SetDefault("prefixes.allocation", string(IPAllocationStrategySequential)) if IsCLIConfigured() { return nil From c4c8cfe5ea7dd39cb07410f1142b8f6496589f70 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Wed, 17 Apr 2024 13:23:29 +0000 Subject: [PATCH 030/145] Fix crash when a prefix family was empty --- hscontrol/db/db.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/hscontrol/db/db.go b/hscontrol/db/db.go index e54ea19f..c8ec3378 100644 --- a/hscontrol/db/db.go +++ b/hscontrol/db/db.go @@ -377,14 +377,18 @@ func NewHeadscaleDatabase( } } - err = tx.Model(&types.Node{}).Where("id = ?", node.ID).Update("ipv4", v4.String()).Error - if err != nil { - return fmt.Errorf("saving ip addresses to new columns: %w", err) + if v4 != nil { + err = tx.Model(&types.Node{}).Where("id = ?", node.ID).Update("ipv4", v4.String()).Error + if err != nil { + return fmt.Errorf("saving ip addresses to new columns: %w", err) + } } - err = tx.Model(&types.Node{}).Where("id = ?", node.ID).Update("ipv6", v6.String()).Error - if err != nil { - return fmt.Errorf("saving ip addresses to new columns: %w", err) + if v6 != nil { + err = tx.Model(&types.Node{}).Where("id = ?", node.ID).Update("ipv6", v6.String()).Error + if err != nil { + return fmt.Errorf("saving ip addresses to new columns: %w", err) + } } } From 580f96ce833ed2ddebfcf1f5090849ba043387c6 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Wed, 17 Apr 2024 16:42:50 +0000 Subject: [PATCH 031/145] Remove unused node check interval --- config-example.yaml | 6 ------ hscontrol/types/config.go | 16 ---------------- integration/hsic/config.go | 2 -- 3 files changed, 24 deletions(-) diff --git a/config-example.yaml b/config-example.yaml index ac0a5eb8..0f1c2412 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -137,12 +137,6 @@ disable_check_updates: false # Time before an inactive ephemeral node is deleted? ephemeral_node_inactivity_timeout: 30m -# Period to check for node updates within the tailnet. A value too low will severely affect -# CPU consumption of Headscale. A value too high (over 60s) will cause problems -# for the nodes, as they won't get updates or keep alive messages frequently enough. -# In case of doubts, do not touch the default 10s. -node_update_check_interval: 10s - database: type: sqlite diff --git a/hscontrol/types/config.go b/hscontrol/types/config.go index f8073608..20591a6c 100644 --- a/hscontrol/types/config.go +++ b/hscontrol/types/config.go @@ -46,7 +46,6 @@ type Config struct { GRPCAddr string GRPCAllowInsecure bool EphemeralNodeInactivityTimeout time.Duration - NodeUpdateCheckInterval time.Duration PrefixV4 *netip.Prefix PrefixV6 *netip.Prefix IPAllocation IPAllocationStrategy @@ -233,8 +232,6 @@ func LoadConfig(path string, isFile bool) error { viper.SetDefault("ephemeral_node_inactivity_timeout", "120s") - viper.SetDefault("node_update_check_interval", "10s") - viper.SetDefault("tuning.batch_change_delay", "800ms") viper.SetDefault("tuning.node_mapsession_buffered_chan_size", 30) @@ -290,15 +287,6 @@ func LoadConfig(path string, isFile bool) error { ) } - maxNodeUpdateCheckInterval, _ := time.ParseDuration("60s") - if viper.GetDuration("node_update_check_interval") > maxNodeUpdateCheckInterval { - errorText += fmt.Sprintf( - "Fatal config error: node_update_check_interval (%s) is set too high, must be less than %s", - viper.GetString("node_update_check_interval"), - maxNodeUpdateCheckInterval, - ) - } - if errorText != "" { // nolint return errors.New(strings.TrimSuffix(errorText, "\n")) @@ -714,10 +702,6 @@ func GetHeadscaleConfig() (*Config, error) { "ephemeral_node_inactivity_timeout", ), - NodeUpdateCheckInterval: viper.GetDuration( - "node_update_check_interval", - ), - Database: GetDatabaseConfig(), TLS: GetTLSConfig(), diff --git a/integration/hsic/config.go b/integration/hsic/config.go index d66fd34c..7953799e 100644 --- a/integration/hsic/config.go +++ b/integration/hsic/config.go @@ -73,7 +73,6 @@ database: type: sqlite3 sqlite.path: /tmp/integration_test_db.sqlite3 ephemeral_node_inactivity_timeout: 30m -node_update_check_interval: 10s prefixes: v6: fd7a:115c:a1e0::/48 v4: 100.64.0.0/10 @@ -116,7 +115,6 @@ func DefaultConfigEnv() map[string]string { "HEADSCALE_DATABASE_TYPE": "sqlite", "HEADSCALE_DATABASE_SQLITE_PATH": "/tmp/integration_test_db.sqlite3", "HEADSCALE_EPHEMERAL_NODE_INACTIVITY_TIMEOUT": "30m", - "HEADSCALE_NODE_UPDATE_CHECK_INTERVAL": "10s", "HEADSCALE_PREFIXES_V4": "100.64.0.0/10", "HEADSCALE_PREFIXES_V6": "fd7a:115c:a1e0::/48", "HEADSCALE_DNS_CONFIG_BASE_DOMAIN": "headscale.net", From c906aaf9278fd5f9df8551600dfaa57ff557733d Mon Sep 17 00:00:00 2001 From: Juan Font Date: Wed, 17 Apr 2024 22:08:32 +0000 Subject: [PATCH 032/145] Allow to remove forced tags of a node Set as empty StringList --- hscontrol/db/node.go | 5 +++++ hscontrol/db/node_test.go | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/hscontrol/db/node.go b/hscontrol/db/node.go index b3977214..109fd610 100644 --- a/hscontrol/db/node.go +++ b/hscontrol/db/node.go @@ -206,6 +206,11 @@ func SetTags( tags []string, ) error { if len(tags) == 0 { + // if no tags are provided, we remove all forced tags + if err := tx.Model(&types.Node{}).Where("id = ?", nodeID).Update("forced_tags", types.StringList{}).Error; err != nil { + return fmt.Errorf("failed to remove tags for node in the database: %w", err) + } + return nil } diff --git a/hscontrol/db/node_test.go b/hscontrol/db/node_test.go index 84116f25..9ff02287 100644 --- a/hscontrol/db/node_test.go +++ b/hscontrol/db/node_test.go @@ -386,6 +386,13 @@ func (s *Suite) TestSetTags(c *check.C) { check.DeepEquals, types.StringList([]string{"tag:bar", "tag:test", "tag:unknown"}), ) + + // test removing tags + err = db.SetTags(node.ID, []string{}) + c.Assert(err, check.IsNil) + node, err = db.getNode("test", "testnode") + c.Assert(err, check.IsNil) + c.Assert(node.ForcedTags, check.DeepEquals, types.StringList([]string{})) } func TestHeadscale_generateGivenName(t *testing.T) { From e2afd30b1c73e930b8cd52855a65e40da4ba07f9 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Thu, 18 Apr 2024 10:47:12 +0000 Subject: [PATCH 033/145] Add the latest UI to the website --- docs/web-ui.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/web-ui.md b/docs/web-ui.md index d018666e..fae71be1 100644 --- a/docs/web-ui.md +++ b/docs/web-ui.md @@ -5,10 +5,11 @@ This page contains community contributions. The projects listed here are not maintained by the Headscale authors and are written by community members. -| Name | Repository Link | Description | Status | -| --------------- | ------------------------------------------------------- | ------------------------------------------------------------------------- | ------ | -| headscale-webui | [Github](https://github.com/ifargle/headscale-webui) | A simple Headscale web UI for small-scale deployments. | Alpha | -| headscale-ui | [Github](https://github.com/gurucomputing/headscale-ui) | A web frontend for the headscale Tailscale-compatible coordination server | Alpha | -| HeadscaleUi | [GitHub](https://github.com/simcu/headscale-ui) | A static headscale admin ui, no backend enviroment required | Alpha | +| Name | Repository Link | Description | Status | +| --------------- | ------------------------------------------------------- | --------------------------------------------------------------------------- | ------ | +| headscale-webui | [Github](https://github.com/ifargle/headscale-webui) | A simple Headscale web UI for small-scale deployments. | Alpha | +| headscale-ui | [Github](https://github.com/gurucomputing/headscale-ui) | A web frontend for the headscale Tailscale-compatible coordination server | Alpha | +| HeadscaleUi | [GitHub](https://github.com/simcu/headscale-ui) | A static headscale admin ui, no backend enviroment required | Alpha | +| headscale-admin | [Github](https://github.com/GoodiesHQ/headscale-admin) | Headscale-Admin is meant to be a simple, modern web interface for Headscale | Beta | You can ask for support on our dedicated [Discord channel](https://discord.com/channels/896711691637780480/1105842846386356294). From 68503581a07a6addf77372d47f1589881a4b41c0 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Sun, 21 Apr 2024 16:33:22 +0200 Subject: [PATCH 034/145] Add test stage to docs (#1893) * Add test stage to docs Add new file with docs tets Run only in pulls * set explicit python version * Revert "set explicit python version" This reverts commit 4dd7b81f26421a3fc0b742547aae503f457954de. * docs/requirements: update mkdocs-material --------- Co-authored-by: ohdearaugustin --- .github/workflows/docs-test.yml | 27 +++++++++++++++++++++++++++ docs/requirements.txt | 3 +-- 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/docs-test.yml diff --git a/.github/workflows/docs-test.yml b/.github/workflows/docs-test.yml new file mode 100644 index 00000000..575f7c98 --- /dev/null +++ b/.github/workflows/docs-test.yml @@ -0,0 +1,27 @@ +name: Test documentation build + +on: [pull_request] + +concurrency: + group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Install python + uses: actions/setup-python@v4 + with: + python-version: 3.x + - name: Setup cache + uses: actions/cache@v2 + with: + key: ${{ github.ref }} + path: .cache + - name: Setup dependencies + run: pip install -r docs/requirements.txt + - name: Build docs + run: mkdocs build --strict \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt index 32bd08c1..bcbf7c0e 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,4 @@ cairosvg~=2.7.1 -mkdocs-material~=9.4.14 +mkdocs-material~=9.5.18 mkdocs-minify-plugin~=0.7.1 pillow~=10.1.0 - From d6ec31c4e05f68e22611677f1b7395cea2ed5625 Mon Sep 17 00:00:00 2001 From: Carson Yang Date: Sun, 21 Apr 2024 22:43:31 +0800 Subject: [PATCH 035/145] docs: Add docs for running headscale on sealos (#1666) * docs: Add docs for running headscale on sealos Signed-off-by: Carson Yang * run prettier --------- Signed-off-by: Carson Yang Co-authored-by: ohdearaugustin --- docs/images/headscale-sealos-grpc-url.png | Bin 0 -> 35911 bytes docs/images/headscale-sealos-url.png | Bin 0 -> 36024 bytes docs/running-headscale-sealos.md | 136 ++++++++++++++++++++++ 3 files changed, 136 insertions(+) create mode 100644 docs/images/headscale-sealos-grpc-url.png create mode 100644 docs/images/headscale-sealos-url.png create mode 100644 docs/running-headscale-sealos.md diff --git a/docs/images/headscale-sealos-grpc-url.png b/docs/images/headscale-sealos-grpc-url.png new file mode 100644 index 0000000000000000000000000000000000000000..1b0df4f3fd2cfd085830c56c90bb70de02fdc648 GIT binary patch literal 35911 zcmb@ucU)87vNs$=MMXgoL8K{F=^dr2C`gwcS|FiG?=6s^(z{5Lt`rdn5PCE8(2KOt zn{+~lP(piy=XdTY_q@;NzVCDQAMCv{vu1tQtl2ZG%qIA)irlRm4{iVefLro!Ua12B zq(A_GSm+ut;TO)*&u4_&Ws4WeF93j|NYWGV6+)TFSzYcWprD6th4B3Tt&-;J!$Z8D zsom1T@sZHW-96mQ;`Zj&_VMw_-u~hF`T5G?@h~2nIE{$pZyp@rH@EQ1OUJ8=CtF*119OL!we`*I-Sy3_<<&Ko zCtOy}-uUAaFW=Cq#cltv=)%&ft)0D+il3P|1#=5aMwU)>O>JMZ@+yAT%M@*5MkhXh zNdsHEE-tNZb@Vi~c76jzKf2yLkKhFW9s=ZFz0mX+TbnRceK3CMJy`ptOZ3@$SKa&~KdN5| zrNSXXmK+Mfj9y8PANk!!?ZVH`>5Q*lAq~Nl^h$N_P7Kmeb&-7jiRb}7z61c&A!aB6 zfcMC&008(F0MPgV0Jx1IG=d-kJO>j0jo>E?09>>F2jRas|4#TX&i^bTwWyZRk_7Us z3mG>A0BlW^@^oQWLx)~m2LQeZ08t(Y1gW$;u2su|fB;^OcXZS}r^Ik_RujN1#^X9V ziovK_r&(_zK)p9LX*PnCjeyQrH#8gJLuP%zeF;#1$=AZKtb|~-q5q8Y0@j}%{T=5; z&;F&Wer5mM@xQVCy$}DWbfJ`glrEIx|9`0ws()88nERg<{s#gc*N9Jl){4fiAGF>V zrII?_*gb3Y*`Oq|irv7H!+xNSKiZ3=Om5`Sfu#+-h6Kx(Km0y6latD=s+o(v1vudW zpV}2lv`*Fdt5zaR_i0HK(Ss`2vPl`&{I_P0veQ91gXY>Su^j>*FxS5o+br`)?|mpc z1bcw4T8<@)Sy(OwTWHN~ke0{x=?=~by3piA6tkz4-tKPcK*qBmcW}dXeW znQJ5Jx4V=UGB8`7ZP9>jS}sm}>zW z4QycgPRI6GckpSs*yk9TL&u$BB*VUoRL!Vu?TltvW3_0RfxZAj(K@AJZ841wZ!_s( z2rWax@6Khk4q1kK2bFEtO<`qac=$@xXdbAw zu1^9ovX|t}6~XiyGA`mLUX4)Jmfz==z(VxM;}|xuVJEX^>F!+Tj3`9u#vHE^e;rqK zJpGWR(hl#l(=Ru_%SO0D)*&K@0U-SZqusHq8v|g*hY>>^eysS8cEPFY$YFsQJV`!l zd?Mq1K8{KADPxO8Y3I?|!t0VlGRtF!?U6N_#B(qxJwQwvZ@zLyTO=@bbj#Y5%8C{uAdCH3*v8>h@ zllN+O#TJN*K4?P2XL%4h`r)AbF-BD?(E+Ql;UgH_cX2D&$39Ks4#rXpDu?MWeJgSP zHZP*bTXKEqfxAYV^>~FmujChMO#8cEEQsi*RfjNf&_dKSEvjKjC;GRYDf!@dcg-6W z?QGcy1p9WW&|S>r7laZl@;>~o?!oL;06Y|#YPZ(Mcr92pJn;tB?b;lc!EHB*X4iY{ z>@)Y7ZW<$IdUy8Jktvc#>e)?xukbm1%i>{R_gZ31Xo=18V!JPLLnRRow3^G}La=-7 zn9!m8Z}UXkpwA9xE57lU9Muey>vHWkMs8@|uiNujeuwT+E4LWEdSk)HyZ<_P%Clh6 zd0@8@D%@fRpPP1-TJieCjhR}=s&W)}r8O3>W8V_UTT(jRMn*Vlw&LzfgCYYrif=^1Qu+0#+@-cL&=#|b zbyYj%`eum|rn8wo0ycbXCI?!xlNnZ8RMTJhFrc26TZ=k7!LBB|nlwS4V_f$&u`h2q+{7D8u>OQaXrhn~&f!LUXqCiL&)cGDSgVng?7ZY1_Vr9#qSH~ww z_1q_g+)pIwRW!y~vVf6qRBQHiLzTjS2VR*4{V;a1SB89!mQXR)Is}lNr}vuL@kTOj z|ASm?6uzofmIb5Nw<23d8vjYz#XJs3o$+ahp~##*mU%n9uCt;`gB5FDTOeJrCmDNL zowTfEj1K#)Z(@wCr}!xsM&HxjN*|1II!V}HNVlMJK=sDu0|(9$g|+8&wp~?M;>k#D zZ>>gKm5rO9ZID;btcbbEQwW`?Qh+ zMSHU4%*>S(FL*;vzmoN4h%py6DuX=ivF*teCnm zp3h4+i$JC6$OE)yY(CkX@+WNsj`Z~@h_^ip6sXZli4`!3m4VQ&&RZ2Ul?v*o6?FC| zQICs-S);4(vYIFV0J@PCVdRGj`=EPQ7 zD6vuPcXV1#en^bry;v};8ru*K`ZU$xZiyaB>?4!xz-BR|$P-DpC8+Sf- z-8YJ(JIs@Ju;R|%?DVJyNr(F1fztdf zQkaEKO6=>GGechjs&AAGAjm^;K#41wPZtD5OV$+!EE-f)Iz~HG$Hk_blo@M-= zTg{`gR1cfRBC}$?~8Di^ua$zk1 zpDLgn$`~kMoRa+_%$mdU8cjjnyD1Q;vde<)+-^K?`)fQU)@k5v7cYkXrMBDZVgb-y z9-EvRlc}B*RHhOWH>i;5Xn;VUL=B*#DWrKYDxo9gPZA*XfkvmM-~A%w#EWbWv^Gl0 z4PU<#;VoXzt+9y{w=AMcJQf+e8F1PgTGso(E%A{g3nu#A)mYJjRFdRct(A~g1NPnd zVGq*=3)@p>`x4}6a*mnu9;Y>?ukL#AL+K0T^+r4xwS%2nWsR>#dWplS?J1x0T3t+b zUq9(6g)4=)>Rs6l##tL6#QdqO#n=mg$|-t9@Y2yKnU`a&XuL3J3^g|Cp@guUG>@`~ zAHUn^opoLoo~7H8EvJVl?B+Ba%6RJ&OSxBJP`=ROs{w&^?5_)-NEU%gW=1xB?_eLM zMq@25x0V#`T(HB(+ChUaZ~}~8AtLK4nK8xDmBq%g zsnL&G?>rK#GD% zeH_URhql0K`NnaX2`xEIFCPc$A%+)K%3_zaX$C4@ByN@UTbG>L+oJWRYYt>2`*uTd zxa}KsvWUXD%i+avuG`Lu^3=b-J&K|_?V0MTqf0Thl?8d;kuphTpPKHkZTzz7iO)wN zg{3Ag03T97)-i&Pk)gtA(IhJshA&GCwtW^;#RkEP4t2mtra4J2%wE9Buf=Dny?Kny z&^~7A_R`y-WtDrX!e7O-a4+_Q%TOn*D=UEjBcsh9z_jTo(A)3I6?+6%Kw7>VvepM# zE7`>U9*9`?c>0T`OE@iG>zB_Rjv^|EScHl;#4r>x!JNMc$*j?ZgAm7EZ}{x(wMo!o zJopCUQ)E4@>;@@qxxAey9lt%8@nPy`X6C8S+}$`{wPX`|K59n0i#~W?FDjTY^~5PJ zR00*HwPaCLKIG3B&(w#u=^m9C25Rf(l<6RnUWLt``~dc*w$CX>@qElt-gy&LHV979 zqIU(^1kDjidnH*vnr6rB@5EX-RdDQp-%Yiv6@5R=v9VWq1B;pL_wEGc^A)$ugzb!u zM1GE;Ast=zOO?9*t$?OsL#Rs71(xDJ37+~YOKmtUYN#PWMMhWjb_xuyc;o3zZ+q<| z2B`SLCrJ&o!iSN5jG4HnnB=&sX{gWgR&^zaI>5}=oBFj8LXRu=kZFovHLQyr^GJWT ziLjytyyty{ap7;9c|MDJrMOGmuTHd(rY@rYJ5&JqoI&z-f9mEvYq*SyMl&;nTc3x5 zX_0An_hu2*M}$fRB>w@EilX0MO#8-NYcJ2Su*JHCqvk}i<~d?ZPm4L5BGBlewX(4C zb=Z;C@bX;OV`}1!)I%QrxpAS3T>_`XMR89U zl)jrIc_bfW1}!_YY!`6T>Pe(>?LwBg>1%?|IATIci<8>vD6nV{n&e`L4#%7E?(#vn zejx>y2hx7Faqt8d|Gtc;)%ty>5M|Da0RxC~{6YT*pUNCQTNX^?8n2}SqUxBdH7R!O z0{U$G0qe7KSz0hz5f#T%gp;mxl7Z<&iqtN1)%;w3d2d8JBno2a6{U^U6Hk4^I4P8Y zccv~lESH{*0*Yt64HY&t=)8M6%yi0Jw8*UWt5D;+T(jPUSryfy>(!b_vKojo>F}nL z?S^S@a$qyGUV`$8=>#Yx@S(WgO$_SWD{?A)E!l{Xvh8dz=t!TY=u zVK3J&?J)1|FeuJH@4Y&g{Vl(L?Z*Sglk{Y7g&Ccp*LW3Bmd+ekSCO+8_Zcr-6TSL+ z>cDHL%L}bnck)cyj&UrGu$4gw8K=$onqTl_>|{h)^(cE)r-F81lwUNw{wK14-5-2>YlV;5w=ZERs%nayy)+0QjCv~`dxDESxf zZ=OFS$SL=eb7#3XmT>V2(f2h|sf_REJc%djDNXD@CjH?E!c}DbDx78eQ+Q6;QnqWPktaH?yBAl3Ln z{Ov!D|LyNb!oCUM`Nf_K;2P#KVN2;>c>bcY{Q>ibI-^R4k(Op5@YOSJH1$r?BYbug zFG?bZM@hcIr&fe#8-Mb;XTV+Bi=W*d{-f@DW92^$H6;EMjDK$8?+n)nn=t@D17VW~ z@DOdxiZ-^eQGiE zT@-BTfH%+|pQw3Ihws0oZ*H{Ytqz!}d(v|L;ple(p4zRme~!iSEJYOX{=CF^^5zC1@9sB0LK8w}&~<_#83dC;CS8Ew)S&>2(O{!>7(ly!o zp4xg2nVbT{4si{Wbj;olZXI~9S9{2A4=dw6z<48Nz1e+3znue~9mt8JRjy5*N3}&r zO~^rLxzQ6f=IHz(zOmHc(s=88Q6{^;`pab_r+3e<0FbvDs_mR`j*}AQul8X`DIp)7 zUiRALGY(+n1gM>zJJ$TOM2Jh=INNXhuVE&;pf?=seWZ0A4|DwSFxJ=lKbc9qH32qi z$TK~J(zGBj5c2a^x+^6@Z?I=dwFdQwyi>-4^^k6YNdR1;SOfE6sTU|-jtDBgQIc1d~>9SzX9Lb^C#+-ULjOMuJR zaIJF@Bb%BxhL^FaKaO~PT|wsLroS0l6&QAHiSu`5L-P7__gpiN6&zXI94V+bgc#Yb z0rYPp1|W-V;ksm6MaN$cY7cvwAm?oD`?x{Gt?cT4a^h54xoDTM2lF*u;>@V~s@d># zMOQrrY7|$C^k5NvT@(rSF6&a3F}*$S&HUV!*e^6nYVQQdnKg;}=U9j6Oh$#I|6BvQn>|?f{!}Louz7*p zSOixWEO2r4rU!E+lpfpJMjgpNF6h~hnO@KzSs3GTm*FpaqF1LLM&ipth9U1)RcH{K zDqsoA-)QzS6`)_ZA{h;n#?RH$v7a~}rd)amtb2fne4V&p5n?Rqpd4y+8T;0(`}*87 zA5S0GG&$)O(}(NXQJ-Bfq(pfBs?w(F)`ie;)5f*?fbqKsqkcZcW6geN#-O^;4j!+X z-y){zniWTycUTWl<~mgm#(oJ@TS-laLicLg(CMNoZcRU9awdMNhsg*sU$(xxnzumE zkm^E>Q62eR@QyJoHhxyKHr5xn?i6#4pr2tE44&aNQtE`$1QDAyuYT`(MkcSqx+tUUa0#@die~F?=qu z%@?*4+SzkgzR4_6y{BPG zqRs|9TNs`g@Daxb%dsKDum}FGz3D9u-1G}L2z{d#t@EH~7uIK&CKzSOET5|0S0IDB zpW`;X-=&$$ap>mX@^4T$(1H%kuP9R+@} zK&ORcgNdg#I^vl29F!=~Dg+h5Q*P^v13rnal;+UVzmBm88zO#=gfjyfgEfagmet&u zD`UKO=!S&fxKaa(S!nOLk0n)8<>XC7$LBJPz6hxK?&NdRonhpO%Hs<+1bqli?ONZu z#fXqY+g7e!k$GvA+S&jGnu0|Q9cd7G{7bE0DXM+jTA*~L_P$! z|ADdl;6+y^Ay8zyra!)^0$Z;OJLUmwwUS`xOOg{BMHOBmQ{U9k?uob~XwjnXVs=(F zen|-~UN!qtule@_t*UM#^f3NERF+t$o>lj_p5`p-jqf@k8O8+GW%c5HaeerQl!VyU zs&y`Aw5G|EZcNXW`GsX*j1T{e@siU+c(R;EJ2K|mP&%;on!a6}C==~t(X%nLRbuJ$ z?PPuopqvxvuFO|3V!z~#w2NKzuwMlZyH+wkH$356V**YmpvN5&qQcQnpNREy2RGA4 zs;VK$URR~W_?ER#xl0?6XVM|W^L$SUvjO^JrnB-O`Tn;Bh{DU*+>XM6L8W1yL-5j+ zM8M^Bi%g>!J4OeNRsEv=c6O9zToWNK{2)QG!0y8JHX!r$`q-5_^jwEu7%=O{> zr+v0I8^EOWWzuc6N{2V%V#YEnw+^zy?RxHs^}C-Y96LYk-;{1S4lsu0dmAmhirxJA z)YWHNWwKxia8^lJ1g-6+@9A6JQtS=m`-{|t318;0t8W$Fb z*=4)cPrZCC*uCzav~gxKoURgrSeNmdUbN1V!TQNB0m?amN!8yCPInuips3PCj<58b zs2r0NgCp_a=_f0Q%UIGr+CtV89&sv8`W5d(*YEZn<7p>JLeh%KgD_+P$M%5{7YWwQ zkyic!lfbVX`~8iBtZ;F%nM0j7p^=%`jL$E1TVvJN$Cy6!V{V!@kJ{-~vuG|xzvQAO zBZXy7`qUj;3ROLAfmn56hcwYA&o!lLHMpm zt2NVXxMy0e<-G4R+D0$O?Muu#nfHhLwzkKG=JTB6e8K|5cYNoEuqT^x*CJa|GcE)7 zkZ1fHR5_h@o7c>U43)5Q>n7k+u8{Oohju

iut0)R?9c`ieZP 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 RNGVZJ-peSzt&jo*{VyECz<2-v literal 0 HcmV?d00001 diff --git a/docs/images/headscale-sealos-url.png b/docs/images/headscale-sealos-url.png new file mode 100644 index 0000000000000000000000000000000000000000..66233698b73cffd0458a40c1c66a98656fb97e34 GIT binary patch literal 36024 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 U3^H2&avIA^t4LM9Hx2oJ0A&Ins{jB1 literal 0 HcmV?d00001 diff --git a/docs/running-headscale-sealos.md b/docs/running-headscale-sealos.md new file mode 100644 index 00000000..01aecb0e --- /dev/null +++ b/docs/running-headscale-sealos.md @@ -0,0 +1,136 @@ +# Running headscale on Sealos + +!!! warning "Community documentation" + + This page is not actively maintained by the headscale authors and is + written by community members. It is _not_ verified by `headscale` developers. + + **It might be outdated and it might miss necessary steps**. + +## Goal + +This documentation has the goal of showing a user how-to run `headscale` on Sealos. + +## Running headscale server + +1. Click the following prebuilt template(version [0.23.0-alpha2](https://github.com/juanfont/headscale/releases/tag/v0.23.0-alpha2)): + + [![](https://cdn.jsdelivr.net/gh/labring-actions/templates@main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-template%3FtemplateName%3Dheadscale) + +2. Click "Deploy Application" on the template page to start deployment. Upon completion, two applications appear: Headscale, and its [visual interface](https://github.com/GoodiesHQ/headscale-admin). +3. Once deployment concludes, click 'Details' on the Headscale application page to navigate to the application's details. +4. Wait for the application's status to switch to running. For accessing the headscale server, the Public Address associated with port 8080 is the address of the headscale server. To access the Headscale console, simply append `/admin/` to the Headscale public URL. + + ![](./images/headscale-sealos-url.png) + +5. Click on 'Terminal' button on the right side of the details to access the Terminal of the headscale application. then create a user ([tailnet](https://tailscale.com/kb/1136/tailnet/)): + + ```bash + headscale users create myfirstuser + ``` + +### Register a machine (normal login) + +On a client machine, execute the `tailscale` login command: + +```bash +# replace with the public domain provided by Sealos +tailscale up --login-server YOUR_HEADSCALE_URL +``` + +To register a machine when running headscale in [Sealos](https://sealos.io), click on 'Terminal' button on the right side of the headscale application's detail page to access the Terminal of the headscale application, then take the headscale command: + +```bash +headscale --user myfirstuser nodes register --key +``` + +### Register machine using a pre authenticated key + +click on 'Terminal' button on the right side of the headscale application's detail page to access the Terminal of the headscale application, then generate a key using the command line: + +```bash +headscale --user myfirstuser preauthkeys create --reusable --expiration 24h +``` + +This will return a pre-authenticated key that can be used to connect a node to `headscale` during the `tailscale` command: + +```bash +tailscale up --login-server --authkey +``` + +## Controlling headscale with remote CLI + +This documentation has the goal of showing a user how-to set control a headscale instance from a remote machine with the headscale command line binary. + +### Create an API key + +We need to create an API key to authenticate our remote headscale when using it from our workstation. + +To create a API key, click on 'Terminal' button on the right side of the headscale application's detail page to access the Terminal of the headscale application, then generate a key: + +```bash +headscale apikeys create --expiration 90d +``` + +Copy the output of the command and save it for later. Please note that you can not retrieve a key again, if the key is lost, expire the old one, and create a new key. + +To list the keys currently assosicated with the server: + +```bash +headscale apikeys list +``` + +and to expire a key: + +```bash +headscale apikeys expire --prefix "" +``` + +### Download and configure `headscale` client + +1. Download the latest [`headscale` binary from GitHub's release page](https://github.com/juanfont/headscale/releases): + +2. Put the binary somewhere in your `PATH`, e.g. `/usr/local/bin/headscale` + +3. Make `headscale` executable: + +```shell +chmod +x /usr/local/bin/headscale +``` + +4. Configure the CLI through Environment Variables + +```shell +export HEADSCALE_CLI_ADDRESS=":443" +export HEADSCALE_CLI_API_KEY="" +``` + +In the headscale application's detail page, The Public Address corresponding to port 50443 corresponds to the value of . + +![](./images/headscale-sealos-grpc-url.png) + +for example: + +```shell +export HEADSCALE_CLI_ADDRESS="pwnjnnly.cloud.sealos.io:443" +export HEADSCALE_CLI_API_KEY="abcde12345" +``` + +This will tell the `headscale` binary to connect to a remote instance, instead of looking +for a local instance. + +The API key is needed to make sure that your are allowed to access the server. The key is _not_ +needed when running directly on the server, as the connection is local. + +1. Test the connection + +Let us run the headscale command to verify that we can connect by listing our nodes: + +```shell +headscale nodes list +``` + +You should now be able to see a list of your nodes from your workstation, and you can +now control the `headscale` server from your workstation. + +> Reference: [Headscale Deployment and Usage Guide: Mastering Tailscale's Self-Hosting Basics](https://icloudnative.io/en/posts/how-to-set-up-or-migrate-headscale/) From 803269a64cfadcd174bf3b8b8fa6806dde71fc04 Mon Sep 17 00:00:00 2001 From: Arnaud Dezandee <4415204+arnaud-dezandee@users.noreply.github.com> Date: Sun, 21 Apr 2024 16:48:33 +0200 Subject: [PATCH 036/145] docs(readme): change contributors section (#1889) --- .github/workflows/contributors.yml | 36 - README.md | 1035 +--------------------------- 2 files changed, 5 insertions(+), 1066 deletions(-) delete mode 100644 .github/workflows/contributors.yml diff --git a/.github/workflows/contributors.yml b/.github/workflows/contributors.yml deleted file mode 100644 index 2c55c002..00000000 --- a/.github/workflows/contributors.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Contributors - -on: - push: - branches: - - main - workflow_dispatch: - -jobs: - add-contributors: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Delete upstream contributor branch - # Allow continue on failure to account for when the - # upstream branch is deleted or does not exist. - continue-on-error: true - run: git push origin --delete update-contributors - - name: Create up-to-date contributors branch - run: git checkout -B update-contributors - - name: Push empty contributors branch - run: git push origin update-contributors - - name: Switch back to main - run: git checkout main - - uses: BobAnkh/add-contributors@v0.2.2 - with: - CONTRIBUTOR: "## Contributors" - COLUMN_PER_ROW: "6" - ACCESS_TOKEN: ${{secrets.GITHUB_TOKEN}} - IMG_WIDTH: "100" - FONT_SIZE: "14" - PATH: "/README.md" - COMMIT_MESSAGE: "docs(README): update contributors" - AVATAR_SHAPE: "round" - BRANCH: "update-contributors" - PULL_REQUEST: "main" diff --git a/README.md b/README.md index 4c7ccacb..3103e44c 100644 --- a/README.md +++ b/README.md @@ -172,1033 +172,8 @@ make build ## Contributors - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - Kristoffer -
- Kristoffer Dalby -
-
- - Juan -
- Juan Font -
-
- - Adrien -
- Adrien Raffin-Caboisse -
-
- - Ward -
- Ward Vandewege -
-
- - Jiang -
- Jiang Zhu -
-
- - Benjamin -
- Benjamin Roberts -
-
- - Nico/ -
- Nico -
-
- - e-zk/ -
- e-zk -
-
- - Even -
- Even Holthe -
-
- - Justin -
- Justin Angel -
-
- - Alessandro -
- Alessandro (Ale) Segala -
-
- - ohdearaugustin/ -
- ohdearaugustin -
-
- - Moritz -
- Moritz Poldrack -
-
- - Andriy -
- Andriy Kushnir -
-
- - GrigoriyMikhalkin/ -
- GrigoriyMikhalkin -
-
- - Mike -
- Mike Lloyd -
-
- - Christian -
- Christian Heusel -
-
- - Anton -
- Anton Schubert -
-
- - Niek -
- Niek van der Maas -
-
- - Eugen -
- Eugen Biegler -
-
- - Azz/ -
- Azz -
-
- - Aaron -
- Aaron Bieber -
-
- - Igor -
- Igor Perepilitsyn -
-
- - Laurent -
- Laurent Marchaud -
-
- - Stefan -
- Stefan Majer -
-
- - bravechamp/ -
- bravechamp -
-
- - hdhoang/ -
- hdhoang -
-
- - Orville -
- Orville Q. Song -
-
- - Fernando -
- Fernando De Lucchi -
-
- - MichaelKo/ -
- MichaelKo -
-
- - kevinlin/ -
- kevinlin -
-
- - unreality/ -
- unreality -
-
- - loprima-l/ -
- loprima-l -
-
- - Samuel -
- Samuel Lock -
-
- - Paul -
- Paul Tötterman -
-
- - Michael -
- Michael G. -
-
- - Mevan -
- Mevan Samaratunga -
-
- - Maja -
- Maja Bojarska -
-
- - ChibangLW/ -
- ChibangLW -
-
- - Jonathan -
- Jonathan Wright -
-
- - Jamie -
- Jamie Greeff -
-
- - Deon -
- Deon Thomas -
-
- - Sean -
- Sean Reifschneider -
-
- - derelm/ -
- derelm -
-
- - Andrei -
- Andrei Pechkurov -
-
- - thomas/ -
- thomas -
-
- - Zhenyu -
- Zhenyu Qi -
-
- - Vitalij -
- Vitalij Dovhanyc -
-
- - Victor -
- Victor Freire -
-
- - Steven -
- Steven Honson -
-
- - Silver -
- Silver Bullet -
-
- - Snack/ -
- Snack -
-
- - Artem -
- Artem Klevtsov -
-
- - Casey -
- Casey Marshall -
-
- - DeveloperDragon/ -
- DeveloperDragon -
-
- - dbevacqua/ -
- dbevacqua -
-
- - Sandro/ -
- Sandro -
-
- - Pavlos -
- Pavlos Vinieratos -
-
- - Pallab -
- Pallab Pain -
-
- - Josh -
- Josh Taylor -
-
- - Motiejus -
- Motiejus Jakštys -
-
- - LIU -
- LIU HANCHENG -
-
- - caelansar/ -
- caelansar -
-
- - Ziyuan -
- Ziyuan Han -
-
- - Zhiyuan -
- Zhiyuan Zheng -
-
- - Zakhar -
- Zakhar Bessarab -
-
- - Zachary -
- Zachary Newell -
-
- - Yujie -
- Yujie Xia -
-
- - 杨斌 -
- 杨斌 Aben -
-
- - Tjerk -
- Tjerk Woudsma -
-
- - Till -
- Till Hoffmann -
-
- - Tianon -
- Tianon Gravi -
-
- - The -
- The Gitter Badger -
-
- - Teteros/ -
- Teteros -
-
- - Tanner/ -
- Tanner -
-
- - sophware/ -
- sophware -
-
- - Stepan/ -
- Stepan -
-
- - Stefan -
- Stefan VanBuren -
-
- - Six/ -
- Six -
-
- - Shaanan -
- Shaanan Cohney -
-
- - Sebastian/ -
- Sebastian -
-
- - Àlex -
- Àlex Torregrosa -
-
- - zy/ -
- zy -
-
- - zx/ -
- zx -
-
- - Wakeful -
- Wakeful Cloud -
-
- - phpmalik/ -
- phpmalik -
-
- - Tommi -
- Tommi Pernila -
-
- - nicholas-yap/ -
- nicholas-yap -
-
- - manju-rn/ -
- manju-rn -
-
- - ma6174/ -
- ma6174 -
-
- - lööps/ -
- lööps -
-
- - lionel.codes/ -
- lionel.codes -
-
- - suhelen/ -
- suhelen -
-
- - jimyag/ -
- jimyag -
-
- - ignoramous/ -
- ignoramous -
-
- - henning -
- henning mueller -
-
- - foosinn/ -
- foosinn -
-
- - dyz/ -
- dyz -
-
- - dnaq/ -
- dnaq -
-
- - danielalvsaaker/ -
- danielalvsaaker -
-
- - JJGadgets/ -
- JJGadgets -
-
- - Gabe -
- Gabe Cook -
-
- - Felix -
- Felix Yan -
-
- - Felix -
- Felix Kronlage-Dammers -
-
- - fatih-acar/ -
- fatih-acar -
-
- - Darrell -
- Darrell Kundel -
-
- - Carson -
- Carson Yang -
-
- - Calvin -
- Calvin Figuereo-Supraner -
-
- - Bryan -
- Bryan Stenson -
-
- - Azamat -
- Azamat H. Hackimov -
-
- - Avirut -
- Avirut Mehta -
-
- - Arthur -
- Arthur Woimbée -
-
- - Arnar/ -
- Arnar -
-
- - Aofei -
- Aofei Sheng -
-
- - Antonio -
- Antonio Fernandez -
-
- - Antoine -
- Antoine POPINEAU -
-
- - Anoop -
- Anoop Sundaresh -
-
- - Alexander -
- Alexander Halbarth -
-
- - Albert -
- Albert Copeland -
-
- - Abraham -
- Abraham Ingersoll -
-
- - Ryan -
- Ryan Fowler -
-
- - Mend -
- Mend Renovate -
-
- - rcursaru/ -
- rcursaru -
-
- - Rasmus -
- Rasmus Moorats -
-
- - Pontus -
- Pontus N -
-
- - Pierre -
- Pierre Carru -
-
- - Philipp -
- Philipp Krivanec -
-
- - Michael -
- Michael Savage -
-
- - Mesar -
- Mesar Hameed -
-
- - Marc/ -
- Marc -
-
- - Lucalux/ -
- Lucalux -
-
- - Linus/ -
- Linus -
-
- - Kurnia -
- Kurnia D Win -
-
- - Julien -
- Julien Zweverink -
-
- - Jonathan -
- Jonathan de Jong -
-
- - John -
- John Axel Eriksson -
-
- - Johan -
- Johan Siebens -
-
- - Jim -
- Jim Tittsler -
-
- - JesseBot/ -
- JesseBot -
-
- - hrtkpf/ -
- hrtkpf -
-
+
+ + + +Made with [contrib.rocks](https://contrib.rocks). From 839420885657a2af99f24581660d1f60fa04273a Mon Sep 17 00:00:00 2001 From: ohdearaugustin Date: Sun, 21 Apr 2024 16:36:49 +0200 Subject: [PATCH 037/145] fix prettier --- .github/workflows/docs-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs-test.yml b/.github/workflows/docs-test.yml index 575f7c98..b0e60131 100644 --- a/.github/workflows/docs-test.yml +++ b/.github/workflows/docs-test.yml @@ -24,4 +24,4 @@ jobs: - name: Setup dependencies run: pip install -r docs/requirements.txt - name: Build docs - run: mkdocs build --strict \ No newline at end of file + run: mkdocs build --strict From 7d8178406dc79d57c8a39bfe083b9fdea5ba0f8e Mon Sep 17 00:00:00 2001 From: oftenoccur <166520808+oftenoccur@users.noreply.github.com> Date: Mon, 22 Apr 2024 00:19:38 +0800 Subject: [PATCH 038/145] chore: fix function names in comment (#1866) * chore: fix function names in comment Signed-off-by: oftenoccur --------- Signed-off-by: oftenoccur Co-authored-by: ohdearaugustin --- integration/scenario.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration/scenario.go b/integration/scenario.go index ebd12bca..0ba44e7d 100644 --- a/integration/scenario.go +++ b/integration/scenario.go @@ -510,7 +510,7 @@ func (s *Scenario) GetIPs(user string) ([]netip.Addr, error) { return ips, fmt.Errorf("failed to get ips: %w", errNoUserAvailable) } -// GetIPs returns all TailscaleClients associated with a User in a Scenario. +// GetClients returns all TailscaleClients associated with a User in a Scenario. func (s *Scenario) GetClients(user string) ([]TailscaleClient, error) { var clients []TailscaleClient if ns, ok := s.users[user]; ok { @@ -586,7 +586,7 @@ func (s *Scenario) ListTailscaleClientsIPs(users ...string) ([]netip.Addr, error return allIps, nil } -// ListTailscaleClientsIPs returns a list of FQDN based on Users +// ListTailscaleClientsFQDNs returns a list of FQDN based on Users // passed as parameters. func (s *Scenario) ListTailscaleClientsFQDNs(users ...string) ([]string, error) { allFQDNs := make([]string, 0) From ba614a5e6cb6470b2ea967ad5dd68719125d5079 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sun, 21 Apr 2024 18:28:17 +0200 Subject: [PATCH 039/145] metrics, tuning in tests, db cleanups, fix concurrency issue (#1895) --- hscontrol/app.go | 7 +- hscontrol/auth.go | 31 +-------- hscontrol/auth_noise.go | 1 - hscontrol/db/node.go | 9 +-- hscontrol/db/node_test.go | 3 +- hscontrol/db/preauth_keys_test.go | 4 +- hscontrol/db/routes.go | 37 ++++++----- hscontrol/db/routes_test.go | 55 +++++++++------- hscontrol/grpcv1.go | 20 +++--- hscontrol/mapper/mapper.go | 21 +++--- hscontrol/metrics.go | 104 ++++++++++++++++++++++++++---- hscontrol/noise.go | 4 ++ hscontrol/notifier/metrics.go | 27 ++++++++ hscontrol/notifier/notifier.go | 73 +++++++++------------ hscontrol/oidc.go | 2 +- hscontrol/poll.go | 40 +++++++++--- hscontrol/types/node.go | 3 +- integration/acl_test.go | 4 +- integration/auth_oidc_test.go | 4 +- integration/auth_web_flow_test.go | 4 +- integration/cli_test.go | 24 +++---- integration/embedded_derp_test.go | 2 +- integration/general_test.go | 20 +++--- integration/hsic/hsic.go | 9 +++ integration/route_test.go | 8 +-- integration/scenario.go | 5 +- integration/scenario_test.go | 6 +- integration/ssh_test.go | 2 +- 28 files changed, 328 insertions(+), 201 deletions(-) create mode 100644 hscontrol/notifier/metrics.go diff --git a/hscontrol/app.go b/hscontrol/app.go index 64d40ed1..acc94229 100644 --- a/hscontrol/app.go +++ b/hscontrol/app.go @@ -225,7 +225,7 @@ func (h *Headscale) deleteExpireEphemeralNodes(milliSeconds int64) { for range ticker.C { var removed []types.NodeID var changed []types.NodeID - if err := h.db.DB.Transaction(func(tx *gorm.DB) error { + if err := h.db.Write(func(tx *gorm.DB) error { removed, changed = db.DeleteExpiredEphemeralNodes(tx, h.cfg.EphemeralNodeInactivityTimeout) return nil @@ -263,7 +263,7 @@ func (h *Headscale) expireExpiredMachines(intervalMs int64) { var changed bool for range ticker.C { - if err := h.db.DB.Transaction(func(tx *gorm.DB) error { + if err := h.db.Write(func(tx *gorm.DB) error { lastCheck, update, changed = db.ExpireExpiredNodes(tx, lastCheck) return nil @@ -452,6 +452,7 @@ func (h *Headscale) ensureUnixSocketIsAbsent() error { func (h *Headscale) createRouter(grpcMux *grpcRuntime.ServeMux) *mux.Router { router := mux.NewRouter() + router.Use(prometheusMiddleware) router.PathPrefix("/debug/pprof/").Handler(http.DefaultServeMux) router.HandleFunc(ts2021UpgradePath, h.NoiseUpgradeHandler).Methods(http.MethodPost) @@ -508,7 +509,7 @@ func (h *Headscale) Serve() error { // Fetch an initial DERP Map before we start serving h.DERPMap = derp.GetDERPMap(h.cfg.DERP) - h.mapper = mapper.NewMapper(h.db, h.cfg, h.DERPMap, h.nodeNotifier.ConnectedMap()) + h.mapper = mapper.NewMapper(h.db, h.cfg, h.DERPMap, h.nodeNotifier) if h.cfg.DERP.ServerEnabled { // When embedded DERP is enabled we always need a STUN server diff --git a/hscontrol/auth.go b/hscontrol/auth.go index 8307d314..0679d72e 100644 --- a/hscontrol/auth.go +++ b/hscontrol/auth.go @@ -273,8 +273,6 @@ func (h *Headscale) handleAuthKey( Err(err). Msg("Cannot encode message") http.Error(writer, "Internal server error", http.StatusInternalServerError) - nodeRegistrations.WithLabelValues("new", util.RegisterMethodAuthKey, "error", pak.User.Name). - Inc() return } @@ -294,13 +292,6 @@ func (h *Headscale) handleAuthKey( Str("node", registerRequest.Hostinfo.Hostname). Msg("Failed authentication via AuthKey") - if pak != nil { - nodeRegistrations.WithLabelValues("new", util.RegisterMethodAuthKey, "error", pak.User.Name). - Inc() - } else { - nodeRegistrations.WithLabelValues("new", util.RegisterMethodAuthKey, "error", "unknown").Inc() - } - return } @@ -404,15 +395,13 @@ func (h *Headscale) handleAuthKey( Caller(). Err(err). Msg("could not register node") - nodeRegistrations.WithLabelValues("new", util.RegisterMethodAuthKey, "error", pak.User.Name). - Inc() http.Error(writer, "Internal server error", http.StatusInternalServerError) return } } - err = h.db.DB.Transaction(func(tx *gorm.DB) error { + h.db.Write(func(tx *gorm.DB) error { return db.UsePreAuthKey(tx, pak) }) if err != nil { @@ -420,8 +409,6 @@ func (h *Headscale) handleAuthKey( Caller(). Err(err). Msg("Failed to use pre-auth key") - nodeRegistrations.WithLabelValues("new", util.RegisterMethodAuthKey, "error", pak.User.Name). - Inc() http.Error(writer, "Internal server error", http.StatusInternalServerError) return @@ -440,14 +427,10 @@ func (h *Headscale) handleAuthKey( Str("node", registerRequest.Hostinfo.Hostname). Err(err). Msg("Cannot encode message") - nodeRegistrations.WithLabelValues("new", util.RegisterMethodAuthKey, "error", pak.User.Name). - Inc() http.Error(writer, "Internal server error", http.StatusInternalServerError) return } - nodeRegistrations.WithLabelValues("new", util.RegisterMethodAuthKey, "success", pak.User.Name). - Inc() writer.Header().Set("Content-Type", "application/json; charset=utf-8") writer.WriteHeader(http.StatusOK) _, err = writer.Write(respBody) @@ -563,7 +546,7 @@ func (h *Headscale) handleNodeLogOut( } if node.IsEphemeral() { - changedNodes, err := h.db.DeleteNode(&node, h.nodeNotifier.ConnectedMap()) + changedNodes, err := h.db.DeleteNode(&node, h.nodeNotifier.LikelyConnectedMap()) if err != nil { log.Error(). Err(err). @@ -616,14 +599,10 @@ func (h *Headscale) handleNodeWithValidRegistration( Caller(). Err(err). Msg("Cannot encode message") - nodeRegistrations.WithLabelValues("update", "web", "error", node.User.Name). - Inc() http.Error(writer, "Internal server error", http.StatusInternalServerError) return } - nodeRegistrations.WithLabelValues("update", "web", "success", node.User.Name). - Inc() writer.Header().Set("Content-Type", "application/json; charset=utf-8") writer.WriteHeader(http.StatusOK) @@ -654,7 +633,7 @@ func (h *Headscale) handleNodeKeyRefresh( Str("node", node.Hostname). Msg("We have the OldNodeKey in the database. This is a key refresh") - err := h.db.DB.Transaction(func(tx *gorm.DB) error { + err := h.db.Write(func(tx *gorm.DB) error { return db.NodeSetNodeKey(tx, &node, registerRequest.NodeKey) }) if err != nil { @@ -737,14 +716,10 @@ func (h *Headscale) handleNodeExpiredOrLoggedOut( Caller(). Err(err). Msg("Cannot encode message") - nodeRegistrations.WithLabelValues("reauth", "web", "error", node.User.Name). - Inc() http.Error(writer, "Internal server error", http.StatusInternalServerError) return } - nodeRegistrations.WithLabelValues("reauth", "web", "success", node.User.Name). - Inc() writer.Header().Set("Content-Type", "application/json; charset=utf-8") writer.WriteHeader(http.StatusOK) diff --git a/hscontrol/auth_noise.go b/hscontrol/auth_noise.go index 323a49b0..6659dfa5 100644 --- a/hscontrol/auth_noise.go +++ b/hscontrol/auth_noise.go @@ -33,7 +33,6 @@ func (ns *noiseServer) NoiseRegistrationHandler( Caller(). Err(err). Msg("Cannot parse RegisterRequest") - nodeRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc() http.Error(writer, "Internal error", http.StatusInternalServerError) return diff --git a/hscontrol/db/node.go b/hscontrol/db/node.go index 109fd610..91bf0cb3 100644 --- a/hscontrol/db/node.go +++ b/hscontrol/db/node.go @@ -10,6 +10,7 @@ import ( "github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/util" "github.com/patrickmn/go-cache" + "github.com/puzpuzpuz/xsync/v3" "github.com/rs/zerolog/log" "gorm.io/gorm" "tailscale.com/tailcfg" @@ -260,9 +261,9 @@ func NodeSetExpiry(tx *gorm.DB, return tx.Model(&types.Node{}).Where("id = ?", nodeID).Update("expiry", expiry).Error } -func (hsdb *HSDatabase) DeleteNode(node *types.Node, isConnected types.NodeConnectedMap) ([]types.NodeID, error) { +func (hsdb *HSDatabase) DeleteNode(node *types.Node, isLikelyConnected *xsync.MapOf[types.NodeID, bool]) ([]types.NodeID, error) { return Write(hsdb.DB, func(tx *gorm.DB) ([]types.NodeID, error) { - return DeleteNode(tx, node, isConnected) + return DeleteNode(tx, node, isLikelyConnected) }) } @@ -270,9 +271,9 @@ func (hsdb *HSDatabase) DeleteNode(node *types.Node, isConnected types.NodeConne // Caller is responsible for notifying all of change. func DeleteNode(tx *gorm.DB, node *types.Node, - isConnected types.NodeConnectedMap, + isLikelyConnected *xsync.MapOf[types.NodeID, bool], ) ([]types.NodeID, error) { - changed, err := deleteNodeRoutes(tx, node, isConnected) + changed, err := deleteNodeRoutes(tx, node, isLikelyConnected) if err != nil { return changed, err } diff --git a/hscontrol/db/node_test.go b/hscontrol/db/node_test.go index 9ff02287..ce2ada33 100644 --- a/hscontrol/db/node_test.go +++ b/hscontrol/db/node_test.go @@ -11,6 +11,7 @@ import ( "github.com/juanfont/headscale/hscontrol/policy" "github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/util" + "github.com/puzpuzpuz/xsync/v3" "gopkg.in/check.v1" "tailscale.com/tailcfg" "tailscale.com/types/key" @@ -120,7 +121,7 @@ func (s *Suite) TestHardDeleteNode(c *check.C) { } db.DB.Save(&node) - _, err = db.DeleteNode(&node, types.NodeConnectedMap{}) + _, err = db.DeleteNode(&node, xsync.NewMapOf[types.NodeID, bool]()) c.Assert(err, check.IsNil) _, err = db.getNode(user.Name, "testnode3") diff --git a/hscontrol/db/preauth_keys_test.go b/hscontrol/db/preauth_keys_test.go index 2cd59c40..fa9681ac 100644 --- a/hscontrol/db/preauth_keys_test.go +++ b/hscontrol/db/preauth_keys_test.go @@ -147,7 +147,7 @@ func (*Suite) TestEphemeralKeyReusable(c *check.C) { _, err = db.getNode("test7", "testest") c.Assert(err, check.IsNil) - db.DB.Transaction(func(tx *gorm.DB) error { + db.Write(func(tx *gorm.DB) error { DeleteExpiredEphemeralNodes(tx, time.Second*20) return nil }) @@ -181,7 +181,7 @@ func (*Suite) TestEphemeralKeyNotReusable(c *check.C) { _, err = db.getNode("test7", "testest") c.Assert(err, check.IsNil) - db.DB.Transaction(func(tx *gorm.DB) error { + db.Write(func(tx *gorm.DB) error { DeleteExpiredEphemeralNodes(tx, time.Second*20) return nil }) diff --git a/hscontrol/db/routes.go b/hscontrol/db/routes.go index bc3f88a5..74b2b4b7 100644 --- a/hscontrol/db/routes.go +++ b/hscontrol/db/routes.go @@ -8,6 +8,7 @@ import ( "github.com/juanfont/headscale/hscontrol/policy" "github.com/juanfont/headscale/hscontrol/types" + "github.com/puzpuzpuz/xsync/v3" "github.com/rs/zerolog/log" "gorm.io/gorm" "tailscale.com/util/set" @@ -126,7 +127,7 @@ func EnableRoute(tx *gorm.DB, id uint64) (*types.StateUpdate, error) { func DisableRoute(tx *gorm.DB, id uint64, - isConnected types.NodeConnectedMap, + isLikelyConnected *xsync.MapOf[types.NodeID, bool], ) ([]types.NodeID, error) { route, err := GetRoute(tx, id) if err != nil { @@ -147,7 +148,7 @@ func DisableRoute(tx *gorm.DB, return nil, err } - update, err = failoverRouteTx(tx, isConnected, route) + update, err = failoverRouteTx(tx, isLikelyConnected, route) if err != nil { return nil, err } @@ -182,17 +183,17 @@ func DisableRoute(tx *gorm.DB, func (hsdb *HSDatabase) DeleteRoute( id uint64, - isConnected types.NodeConnectedMap, + isLikelyConnected *xsync.MapOf[types.NodeID, bool], ) ([]types.NodeID, error) { return Write(hsdb.DB, func(tx *gorm.DB) ([]types.NodeID, error) { - return DeleteRoute(tx, id, isConnected) + return DeleteRoute(tx, id, isLikelyConnected) }) } func DeleteRoute( tx *gorm.DB, id uint64, - isConnected types.NodeConnectedMap, + isLikelyConnected *xsync.MapOf[types.NodeID, bool], ) ([]types.NodeID, error) { route, err := GetRoute(tx, id) if err != nil { @@ -207,7 +208,7 @@ func DeleteRoute( // https://github.com/juanfont/headscale/issues/804#issuecomment-1399314002 var update []types.NodeID if !route.IsExitRoute() { - update, err = failoverRouteTx(tx, isConnected, route) + update, err = failoverRouteTx(tx, isLikelyConnected, route) if err != nil { return nil, nil } @@ -252,7 +253,7 @@ func DeleteRoute( return update, nil } -func deleteNodeRoutes(tx *gorm.DB, node *types.Node, isConnected types.NodeConnectedMap) ([]types.NodeID, error) { +func deleteNodeRoutes(tx *gorm.DB, node *types.Node, isLikelyConnected *xsync.MapOf[types.NodeID, bool]) ([]types.NodeID, error) { routes, err := GetNodeRoutes(tx, node) if err != nil { return nil, fmt.Errorf("getting node routes: %w", err) @@ -266,7 +267,7 @@ func deleteNodeRoutes(tx *gorm.DB, node *types.Node, isConnected types.NodeConne // TODO(kradalby): This is a bit too aggressive, we could probably // figure out which routes needs to be failed over rather than all. - chn, err := failoverRouteTx(tx, isConnected, &routes[i]) + chn, err := failoverRouteTx(tx, isLikelyConnected, &routes[i]) if err != nil { return changed, fmt.Errorf("failing over route after delete: %w", err) } @@ -409,7 +410,7 @@ func SaveNodeRoutes(tx *gorm.DB, node *types.Node) (bool, error) { // If needed, the failover will be attempted. func FailoverNodeRoutesIfNeccessary( tx *gorm.DB, - isConnected types.NodeConnectedMap, + isLikelyConnected *xsync.MapOf[types.NodeID, bool], node *types.Node, ) (*types.StateUpdate, error) { nodeRoutes, err := GetNodeRoutes(tx, node) @@ -430,12 +431,12 @@ nodeRouteLoop: if route.IsPrimary { // if we have a primary route, and the node is connected // nothing needs to be done. - if conn, ok := isConnected[route.Node.ID]; conn && ok { + if val, ok := isLikelyConnected.Load(route.Node.ID); ok && val { continue nodeRouteLoop } // if not, we need to failover the route - failover := failoverRoute(isConnected, &route, routes) + failover := failoverRoute(isLikelyConnected, &route, routes) if failover != nil { err := failover.save(tx) if err != nil { @@ -477,7 +478,7 @@ nodeRouteLoop: // If the given route was not primary, it returns early. func failoverRouteTx( tx *gorm.DB, - isConnected types.NodeConnectedMap, + isLikelyConnected *xsync.MapOf[types.NodeID, bool], r *types.Route, ) ([]types.NodeID, error) { if r == nil { @@ -500,7 +501,7 @@ func failoverRouteTx( return nil, fmt.Errorf("getting routes by prefix: %w", err) } - fo := failoverRoute(isConnected, r, routes) + fo := failoverRoute(isLikelyConnected, r, routes) if fo == nil { return nil, nil } @@ -538,7 +539,7 @@ func (f *failover) save(tx *gorm.DB) error { } func failoverRoute( - isConnected types.NodeConnectedMap, + isLikelyConnected *xsync.MapOf[types.NodeID, bool], routeToReplace *types.Route, altRoutes types.Routes, @@ -570,9 +571,11 @@ func failoverRoute( continue } - if isConnected != nil && isConnected[route.Node.ID] { - newPrimary = &altRoutes[idx] - break + if isLikelyConnected != nil { + if val, ok := isLikelyConnected.Load(route.Node.ID); ok && val { + newPrimary = &altRoutes[idx] + break + } } } diff --git a/hscontrol/db/routes_test.go b/hscontrol/db/routes_test.go index 453a7503..02342ca2 100644 --- a/hscontrol/db/routes_test.go +++ b/hscontrol/db/routes_test.go @@ -10,11 +10,22 @@ import ( "github.com/google/go-cmp/cmp/cmpopts" "github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/util" + "github.com/puzpuzpuz/xsync/v3" "gopkg.in/check.v1" "gorm.io/gorm" "tailscale.com/tailcfg" ) +var smap = func(m map[types.NodeID]bool) *xsync.MapOf[types.NodeID, bool] { + s := xsync.NewMapOf[types.NodeID, bool]() + + for k, v := range m { + s.Store(k, v) + } + + return s +} + func (s *Suite) TestGetRoutes(c *check.C) { user, err := db.CreateUser("test") c.Assert(err, check.IsNil) @@ -331,7 +342,7 @@ func TestFailoverNodeRoutesIfNeccessary(t *testing.T) { name string nodes types.Nodes routes types.Routes - isConnected []types.NodeConnectedMap + isConnected []map[types.NodeID]bool want []*types.StateUpdate wantErr bool }{ @@ -346,7 +357,7 @@ func TestFailoverNodeRoutesIfNeccessary(t *testing.T) { r(1, 1, ipp("10.0.0.0/24"), true, true), r(2, 2, ipp("10.0.0.0/24"), true, false), }, - isConnected: []types.NodeConnectedMap{ + isConnected: []map[types.NodeID]bool{ // n1 goes down { 1: false, @@ -384,7 +395,7 @@ func TestFailoverNodeRoutesIfNeccessary(t *testing.T) { r(1, 1, ipp("10.0.0.0/24"), true, true), r(2, 2, ipp("10.0.0.0/24"), true, false), }, - isConnected: []types.NodeConnectedMap{ + isConnected: []map[types.NodeID]bool{ // n1 up recon = noop { 1: true, @@ -428,7 +439,7 @@ func TestFailoverNodeRoutesIfNeccessary(t *testing.T) { r(2, 2, ipp("10.0.0.0/24"), true, false), r(3, 3, ipp("10.0.0.0/24"), true, false), }, - isConnected: []types.NodeConnectedMap{ + isConnected: []map[types.NodeID]bool{ // n1 goes down { 1: false, @@ -486,7 +497,7 @@ func TestFailoverNodeRoutesIfNeccessary(t *testing.T) { r(2, 2, ipp("10.0.0.0/24"), false, false), r(3, 3, ipp("10.0.0.0/24"), true, false), }, - isConnected: []types.NodeConnectedMap{ + isConnected: []map[types.NodeID]bool{ // n1 goes down { 1: false, @@ -516,7 +527,7 @@ func TestFailoverNodeRoutesIfNeccessary(t *testing.T) { r(2, 2, ipp("10.0.0.0/24"), true, false), r(3, 3, ipp("10.1.0.0/24"), true, false), }, - isConnected: []types.NodeConnectedMap{ + isConnected: []map[types.NodeID]bool{ // n1 goes down { 1: false, @@ -539,7 +550,7 @@ func TestFailoverNodeRoutesIfNeccessary(t *testing.T) { r(2, 2, ipp("10.0.0.0/24"), true, false), r(3, 3, ipp("10.1.0.0/24"), false, false), }, - isConnected: []types.NodeConnectedMap{ + isConnected: []map[types.NodeID]bool{ // n1 goes down { 1: false, @@ -562,7 +573,7 @@ func TestFailoverNodeRoutesIfNeccessary(t *testing.T) { r(2, 2, ipp("10.0.0.0/24"), true, false), r(3, 3, ipp("10.1.0.0/24"), true, false), }, - isConnected: []types.NodeConnectedMap{ + isConnected: []map[types.NodeID]bool{ // n1 goes down { 1: false, @@ -585,7 +596,7 @@ func TestFailoverNodeRoutesIfNeccessary(t *testing.T) { r(2, 2, ipp("10.0.0.0/24"), true, true), r(3, 3, ipp("10.1.0.0/24"), true, false), }, - isConnected: []types.NodeConnectedMap{ + isConnected: []map[types.NodeID]bool{ // n1 goes down { 1: true, @@ -618,7 +629,7 @@ func TestFailoverNodeRoutesIfNeccessary(t *testing.T) { want := tt.want[step] got, err := Write(db.DB, func(tx *gorm.DB) (*types.StateUpdate, error) { - return FailoverNodeRoutesIfNeccessary(tx, isConnected, node) + return FailoverNodeRoutesIfNeccessary(tx, smap(isConnected), node) }) if (err != nil) != tt.wantErr { @@ -640,7 +651,7 @@ func TestFailoverRouteTx(t *testing.T) { name string failingRoute types.Route routes types.Routes - isConnected types.NodeConnectedMap + isConnected map[types.NodeID]bool want []types.NodeID wantErr bool }{ @@ -743,7 +754,7 @@ func TestFailoverRouteTx(t *testing.T) { Enabled: true, }, }, - isConnected: types.NodeConnectedMap{ + isConnected: map[types.NodeID]bool{ 1: false, 2: true, }, @@ -841,7 +852,7 @@ func TestFailoverRouteTx(t *testing.T) { Enabled: true, }, }, - isConnected: types.NodeConnectedMap{ + isConnected: map[types.NodeID]bool{ 1: true, 2: true, 3: true, @@ -889,7 +900,7 @@ func TestFailoverRouteTx(t *testing.T) { Enabled: true, }, }, - isConnected: types.NodeConnectedMap{ + isConnected: map[types.NodeID]bool{ 1: true, 4: false, }, @@ -945,7 +956,7 @@ func TestFailoverRouteTx(t *testing.T) { Enabled: true, }, }, - isConnected: types.NodeConnectedMap{ + isConnected: map[types.NodeID]bool{ 1: false, 2: true, 4: false, @@ -1010,7 +1021,7 @@ func TestFailoverRouteTx(t *testing.T) { } got, err := Write(db.DB, func(tx *gorm.DB) ([]types.NodeID, error) { - return failoverRouteTx(tx, tt.isConnected, &tt.failingRoute) + return failoverRouteTx(tx, smap(tt.isConnected), &tt.failingRoute) }) if (err != nil) != tt.wantErr { @@ -1048,7 +1059,7 @@ func TestFailoverRoute(t *testing.T) { name string failingRoute types.Route routes types.Routes - isConnected types.NodeConnectedMap + isConnected map[types.NodeID]bool want *failover }{ { @@ -1085,7 +1096,7 @@ func TestFailoverRoute(t *testing.T) { r(1, 1, ipp("10.0.0.0/24"), true, true), r(2, 2, ipp("10.0.0.0/24"), true, false), }, - isConnected: types.NodeConnectedMap{ + isConnected: map[types.NodeID]bool{ 1: false, 2: true, }, @@ -1111,7 +1122,7 @@ func TestFailoverRoute(t *testing.T) { r(2, 2, ipp("10.0.0.0/24"), true, true), r(3, 3, ipp("10.0.0.0/24"), true, false), }, - isConnected: types.NodeConnectedMap{ + isConnected: map[types.NodeID]bool{ 1: true, 2: true, 3: true, @@ -1128,7 +1139,7 @@ func TestFailoverRoute(t *testing.T) { r(1, 1, ipp("10.0.0.0/24"), true, true), r(2, 4, ipp("10.0.0.0/24"), true, false), }, - isConnected: types.NodeConnectedMap{ + isConnected: map[types.NodeID]bool{ 1: true, 4: false, }, @@ -1142,7 +1153,7 @@ func TestFailoverRoute(t *testing.T) { r(2, 4, ipp("10.0.0.0/24"), true, false), r(3, 2, ipp("10.0.0.0/24"), true, false), }, - isConnected: types.NodeConnectedMap{ + isConnected: map[types.NodeID]bool{ 1: false, 2: true, 4: false, @@ -1172,7 +1183,7 @@ func TestFailoverRoute(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotf := failoverRoute(tt.isConnected, &tt.failingRoute, tt.routes) + gotf := failoverRoute(smap(tt.isConnected), &tt.failingRoute, tt.routes) if tt.want == nil && gotf != nil { t.Fatalf("expected nil, got %+v", gotf) diff --git a/hscontrol/grpcv1.go b/hscontrol/grpcv1.go index a24dcead..41be5e9d 100644 --- a/hscontrol/grpcv1.go +++ b/hscontrol/grpcv1.go @@ -145,7 +145,7 @@ func (api headscaleV1APIServer) ExpirePreAuthKey( ctx context.Context, request *v1.ExpirePreAuthKeyRequest, ) (*v1.ExpirePreAuthKeyResponse, error) { - err := api.h.db.DB.Transaction(func(tx *gorm.DB) error { + err := api.h.db.Write(func(tx *gorm.DB) error { preAuthKey, err := db.GetPreAuthKey(tx, request.GetUser(), request.Key) if err != nil { return err @@ -301,7 +301,7 @@ func (api headscaleV1APIServer) DeleteNode( changedNodes, err := api.h.db.DeleteNode( node, - api.h.nodeNotifier.ConnectedMap(), + api.h.nodeNotifier.LikelyConnectedMap(), ) if err != nil { return nil, err @@ -343,7 +343,7 @@ func (api headscaleV1APIServer) ExpireNode( } ctx = types.NotifyCtx(ctx, "cli-expirenode-self", node.Hostname) - api.h.nodeNotifier.NotifyByMachineKey( + api.h.nodeNotifier.NotifyByNodeID( ctx, types.StateUpdate{ Type: types.StateSelfUpdate, @@ -401,7 +401,7 @@ func (api headscaleV1APIServer) ListNodes( ctx context.Context, request *v1.ListNodesRequest, ) (*v1.ListNodesResponse, error) { - isConnected := api.h.nodeNotifier.ConnectedMap() + isLikelyConnected := api.h.nodeNotifier.LikelyConnectedMap() if request.GetUser() != "" { nodes, err := db.Read(api.h.db.DB, func(rx *gorm.DB) (types.Nodes, error) { return db.ListNodesByUser(rx, request.GetUser()) @@ -416,7 +416,9 @@ func (api headscaleV1APIServer) ListNodes( // Populate the online field based on // currently connected nodes. - resp.Online = isConnected[node.ID] + if val, ok := isLikelyConnected.Load(node.ID); ok && val { + resp.Online = true + } response[index] = resp } @@ -439,7 +441,9 @@ func (api headscaleV1APIServer) ListNodes( // Populate the online field based on // currently connected nodes. - resp.Online = isConnected[node.ID] + if val, ok := isLikelyConnected.Load(node.ID); ok && val { + resp.Online = true + } validTags, invalidTags := api.h.ACLPolicy.TagsOfNode( node, @@ -528,7 +532,7 @@ func (api headscaleV1APIServer) DisableRoute( request *v1.DisableRouteRequest, ) (*v1.DisableRouteResponse, error) { update, err := db.Write(api.h.db.DB, func(tx *gorm.DB) ([]types.NodeID, error) { - return db.DisableRoute(tx, request.GetRouteId(), api.h.nodeNotifier.ConnectedMap()) + return db.DisableRoute(tx, request.GetRouteId(), api.h.nodeNotifier.LikelyConnectedMap()) }) if err != nil { return nil, err @@ -568,7 +572,7 @@ func (api headscaleV1APIServer) DeleteRoute( ctx context.Context, request *v1.DeleteRouteRequest, ) (*v1.DeleteRouteResponse, error) { - isConnected := api.h.nodeNotifier.ConnectedMap() + isConnected := api.h.nodeNotifier.LikelyConnectedMap() update, err := db.Write(api.h.db.DB, func(tx *gorm.DB) ([]types.NodeID, error) { return db.DeleteRoute(tx, request.GetRouteId(), isConnected) }) diff --git a/hscontrol/mapper/mapper.go b/hscontrol/mapper/mapper.go index fe8af4d3..d4f4392a 100644 --- a/hscontrol/mapper/mapper.go +++ b/hscontrol/mapper/mapper.go @@ -17,6 +17,7 @@ import ( mapset "github.com/deckarep/golang-set/v2" "github.com/juanfont/headscale/hscontrol/db" + "github.com/juanfont/headscale/hscontrol/notifier" "github.com/juanfont/headscale/hscontrol/policy" "github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/util" @@ -51,10 +52,10 @@ var debugDumpMapResponsePath = envknob.String("HEADSCALE_DEBUG_DUMP_MAPRESPONSE_ type Mapper struct { // Configuration // TODO(kradalby): figure out if this is the format we want this in - db *db.HSDatabase - cfg *types.Config - derpMap *tailcfg.DERPMap - isLikelyConnected types.NodeConnectedMap + db *db.HSDatabase + cfg *types.Config + derpMap *tailcfg.DERPMap + notif *notifier.Notifier uid string created time.Time @@ -70,15 +71,15 @@ func NewMapper( db *db.HSDatabase, cfg *types.Config, derpMap *tailcfg.DERPMap, - isLikelyConnected types.NodeConnectedMap, + notif *notifier.Notifier, ) *Mapper { uid, _ := util.GenerateRandomStringDNSSafe(mapperIDLength) return &Mapper{ - db: db, - cfg: cfg, - derpMap: derpMap, - isLikelyConnected: isLikelyConnected, + db: db, + cfg: cfg, + derpMap: derpMap, + notif: notif, uid: uid, created: time.Now(), @@ -517,7 +518,7 @@ func (m *Mapper) ListPeers(nodeID types.NodeID) (types.Nodes, error) { } for _, peer := range peers { - online := m.isLikelyConnected[peer.ID] + online := m.notif.IsLikelyConnected(peer.ID) peer.IsOnline = &online } diff --git a/hscontrol/metrics.go b/hscontrol/metrics.go index fc56f584..9d802caf 100644 --- a/hscontrol/metrics.go +++ b/hscontrol/metrics.go @@ -1,6 +1,10 @@ package hscontrol import ( + "net/http" + "strconv" + + "github.com/gorilla/mux" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" ) @@ -8,18 +12,94 @@ import ( const prometheusNamespace = "headscale" var ( - // This is a high cardinality metric (user x node), we might want to make this - // configurable/opt-in in the future. - nodeRegistrations = promauto.NewCounterVec(prometheus.CounterOpts{ + mapResponseSent = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: prometheusNamespace, - Name: "node_registrations_total", - Help: "The total amount of registered node attempts", - }, []string{"action", "auth", "status", "user"}) - - updateRequestsSentToNode = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "mapresponse_sent_total", + Help: "total count of mapresponses sent to clients", + }, []string{"status", "type"}) + mapResponseUpdateReceived = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: prometheusNamespace, - Name: "update_request_sent_to_node_total", - Help: "The number of calls/messages issued on a specific nodes update channel", - }, []string{"user", "node", "status"}) - // TODO(kradalby): This is very debugging, we might want to remove it. + Name: "mapresponse_updates_received_total", + Help: "total count of mapresponse updates received on update channel", + }, []string{"type"}) + mapResponseWriteUpdatesInStream = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: prometheusNamespace, + Name: "mapresponse_write_updates_in_stream_total", + Help: "total count of writes that occured in a stream session, pre-68 nodes", + }, []string{"status"}) + mapResponseEndpointUpdates = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: prometheusNamespace, + Name: "mapresponse_endpoint_updates_total", + Help: "total count of endpoint updates received", + }, []string{"status"}) + mapResponseReadOnly = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: prometheusNamespace, + Name: "mapresponse_readonly_requests_total", + Help: "total count of readonly requests received", + }, []string{"status"}) + mapResponseSessions = promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: prometheusNamespace, + Name: "mapresponse_current_sessions_total", + Help: "total count open map response sessions", + }) + mapResponseRejected = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: prometheusNamespace, + Name: "mapresponse_rejected_new_sessions_total", + Help: "total count of new mapsessions rejected", + }, []string{"reason"}) + httpDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: prometheusNamespace, + Name: "http_duration_seconds", + Help: "Duration of HTTP requests.", + }, []string{"path"}) + httpCounter = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: prometheusNamespace, + Name: "http_requests_total", + Help: "Total number of http requests processed", + }, []string{"code", "method", "path"}, + ) ) + +// prometheusMiddleware implements mux.MiddlewareFunc. +func prometheusMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + route := mux.CurrentRoute(r) + path, _ := route.GetPathTemplate() + + // Ignore streaming and noise sessions + // it has its own router further down. + if path == "/ts2021" || path == "/machine/map" || path == "/derp" || path == "/derp/probe" || path == "/bootstrap-dns" { + next.ServeHTTP(w, r) + return + } + + rw := &respWriterProm{ResponseWriter: w} + + timer := prometheus.NewTimer(httpDuration.WithLabelValues(path)) + next.ServeHTTP(rw, r) + timer.ObserveDuration() + httpCounter.WithLabelValues(strconv.Itoa(rw.status), r.Method, path).Inc() + }) +} + +type respWriterProm struct { + http.ResponseWriter + status int + written int64 + wroteHeader bool +} + +func (r *respWriterProm) WriteHeader(code int) { + r.status = code + r.wroteHeader = true + r.ResponseWriter.WriteHeader(code) +} + +func (r *respWriterProm) Write(b []byte) (int, error) { + if !r.wroteHeader { + r.WriteHeader(http.StatusOK) + } + n, err := r.ResponseWriter.Write(b) + r.written += int64(n) + return n, err +} diff --git a/hscontrol/noise.go b/hscontrol/noise.go index 92a89d0f..9ddf2c85 100644 --- a/hscontrol/noise.go +++ b/hscontrol/noise.go @@ -95,6 +95,7 @@ func (h *Headscale) NoiseUpgradeHandler( // The HTTP2 server that exposes this router is created for // a single hijacked connection from /ts2021, using netutil.NewOneConnListener router := mux.NewRouter() + router.Use(prometheusMiddleware) router.HandleFunc("/machine/register", noiseServer.NoiseRegistrationHandler). Methods(http.MethodPost) @@ -267,10 +268,12 @@ func (ns *noiseServer) NoisePollNetMapHandler( defer ns.headscale.mapSessionMu.Unlock() sess.infof("node has an open stream(%p), rejecting new stream", sess) + mapResponseRejected.WithLabelValues("exists").Inc() return } ns.headscale.mapSessions[node.ID] = sess + mapResponseSessions.Inc() ns.headscale.mapSessionMu.Unlock() sess.tracef("releasing lock to check stream") } @@ -283,6 +286,7 @@ func (ns *noiseServer) NoisePollNetMapHandler( defer ns.headscale.mapSessionMu.Unlock() delete(ns.headscale.mapSessions, node.ID) + mapResponseSessions.Dec() sess.tracef("releasing lock to remove stream") } diff --git a/hscontrol/notifier/metrics.go b/hscontrol/notifier/metrics.go new file mode 100644 index 00000000..c461d379 --- /dev/null +++ b/hscontrol/notifier/metrics.go @@ -0,0 +1,27 @@ +package notifier + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +const prometheusNamespace = "headscale" + +var ( + notifierWaitForLock = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: prometheusNamespace, + Name: "notifier_wait_for_lock_seconds", + Help: "histogram of time spent waiting for the notifier lock", + Buckets: []float64{0.001, 0.01, 0.1, 0.3, 0.5, 1, 3, 5, 10}, + }, []string{"action"}) + notifierUpdateSent = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: prometheusNamespace, + Name: "notifier_update_sent_total", + Help: "total count of update sent on nodes channel", + }, []string{"status", "type"}) + notifierNodeUpdateChans = promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: prometheusNamespace, + Name: "notifier_open_channels_total", + Help: "total count open channels in notifier", + }) +) diff --git a/hscontrol/notifier/notifier.go b/hscontrol/notifier/notifier.go index 6c34af57..4ad58723 100644 --- a/hscontrol/notifier/notifier.go +++ b/hscontrol/notifier/notifier.go @@ -6,21 +6,23 @@ import ( "slices" "strings" "sync" + "time" "github.com/juanfont/headscale/hscontrol/types" + "github.com/puzpuzpuz/xsync/v3" "github.com/rs/zerolog/log" ) type Notifier struct { l sync.RWMutex nodes map[types.NodeID]chan<- types.StateUpdate - connected types.NodeConnectedMap + connected *xsync.MapOf[types.NodeID, bool] } func NewNotifier() *Notifier { return &Notifier{ nodes: make(map[types.NodeID]chan<- types.StateUpdate), - connected: make(types.NodeConnectedMap), + connected: xsync.NewMapOf[types.NodeID, bool](), } } @@ -31,16 +33,19 @@ func (n *Notifier) AddNode(nodeID types.NodeID, c chan<- types.StateUpdate) { Uint64("node.id", nodeID.Uint64()). Msg("releasing lock to add node") + start := time.Now() n.l.Lock() defer n.l.Unlock() + notifierWaitForLock.WithLabelValues("add").Observe(time.Since(start).Seconds()) n.nodes[nodeID] = c - n.connected[nodeID] = true + n.connected.Store(nodeID, true) log.Trace(). Uint64("node.id", nodeID.Uint64()). Int("open_chans", len(n.nodes)). Msg("Added new channel") + notifierNodeUpdateChans.Inc() } func (n *Notifier) RemoveNode(nodeID types.NodeID) { @@ -50,20 +55,23 @@ func (n *Notifier) RemoveNode(nodeID types.NodeID) { Uint64("node.id", nodeID.Uint64()). Msg("releasing lock to remove node") + start := time.Now() n.l.Lock() defer n.l.Unlock() + notifierWaitForLock.WithLabelValues("remove").Observe(time.Since(start).Seconds()) if len(n.nodes) == 0 { return } delete(n.nodes, nodeID) - n.connected[nodeID] = false + n.connected.Store(nodeID, false) log.Trace(). Uint64("node.id", nodeID.Uint64()). Int("open_chans", len(n.nodes)). Msg("Removed channel") + notifierNodeUpdateChans.Dec() } // IsConnected reports if a node is connected to headscale and has a @@ -72,17 +80,22 @@ func (n *Notifier) IsConnected(nodeID types.NodeID) bool { n.l.RLock() defer n.l.RUnlock() - return n.connected[nodeID] + if val, ok := n.connected.Load(nodeID); ok { + return val + } + return false } // IsLikelyConnected reports if a node is connected to headscale and has a // poll session open, but doesnt lock, so might be wrong. func (n *Notifier) IsLikelyConnected(nodeID types.NodeID) bool { - return n.connected[nodeID] + if val, ok := n.connected.Load(nodeID); ok { + return val + } + return false } -// TODO(kradalby): This returns a pointer and can be dangerous. -func (n *Notifier) ConnectedMap() types.NodeConnectedMap { +func (n *Notifier) LikelyConnectedMap() *xsync.MapOf[types.NodeID, bool] { return n.connected } @@ -95,45 +108,16 @@ func (n *Notifier) NotifyWithIgnore( update types.StateUpdate, ignoreNodeIDs ...types.NodeID, ) { - log.Trace().Caller().Str("type", update.Type.String()).Msg("acquiring lock to notify") - defer log.Trace(). - Caller(). - Str("type", update.Type.String()). - Msg("releasing lock, finished notifying") - - n.l.RLock() - defer n.l.RUnlock() - - if update.Type == types.StatePeerChangedPatch { - log.Trace().Interface("update", update).Interface("online", n.connected).Msg("PATCH UPDATE SENT") - } - - for nodeID, c := range n.nodes { + for nodeID := range n.nodes { if slices.Contains(ignoreNodeIDs, nodeID) { continue } - select { - case <-ctx.Done(): - log.Error(). - Err(ctx.Err()). - Uint64("node.id", nodeID.Uint64()). - Any("origin", ctx.Value("origin")). - Any("origin-hostname", ctx.Value("hostname")). - Msgf("update not sent, context cancelled") - - return - case c <- update: - log.Trace(). - Uint64("node.id", nodeID.Uint64()). - Any("origin", ctx.Value("origin")). - Any("origin-hostname", ctx.Value("hostname")). - Msgf("update successfully sent on chan") - } + n.NotifyByNodeID(ctx, update, nodeID) } } -func (n *Notifier) NotifyByMachineKey( +func (n *Notifier) NotifyByNodeID( ctx context.Context, update types.StateUpdate, nodeID types.NodeID, @@ -144,8 +128,10 @@ func (n *Notifier) NotifyByMachineKey( Str("type", update.Type.String()). Msg("releasing lock, finished notifying") + start := time.Now() n.l.RLock() defer n.l.RUnlock() + notifierWaitForLock.WithLabelValues("notify").Observe(time.Since(start).Seconds()) if c, ok := n.nodes[nodeID]; ok { select { @@ -156,6 +142,7 @@ func (n *Notifier) NotifyByMachineKey( Any("origin", ctx.Value("origin")). Any("origin-hostname", ctx.Value("hostname")). Msgf("update not sent, context cancelled") + notifierUpdateSent.WithLabelValues("cancelled", update.Type.String()).Inc() return case c <- update: @@ -164,6 +151,7 @@ func (n *Notifier) NotifyByMachineKey( Any("origin", ctx.Value("origin")). Any("origin-hostname", ctx.Value("hostname")). Msgf("update successfully sent on chan") + notifierUpdateSent.WithLabelValues("ok", update.Type.String()).Inc() } } } @@ -182,9 +170,10 @@ func (n *Notifier) String() string { b.WriteString("\n") b.WriteString("connected:\n") - for k, v := range n.connected { + n.connected.Range(func(k types.NodeID, v bool) bool { fmt.Fprintf(&b, "\t%d: %t\n", k, v) - } + return true + }) return b.String() } diff --git a/hscontrol/oidc.go b/hscontrol/oidc.go index 0680ce2f..b728a6d0 100644 --- a/hscontrol/oidc.go +++ b/hscontrol/oidc.go @@ -602,7 +602,7 @@ func (h *Headscale) registerNodeForOIDCCallback( return err } - if err := h.db.DB.Transaction(func(tx *gorm.DB) error { + if err := h.db.Write(func(tx *gorm.DB) error { if _, err := db.RegisterNodeFromAuthCallback( // TODO(kradalby): find a better way to use the cache across modules tx, diff --git a/hscontrol/poll.go b/hscontrol/poll.go index c38c65e2..b903f122 100644 --- a/hscontrol/poll.go +++ b/hscontrol/poll.go @@ -64,7 +64,7 @@ func (h *Headscale) newMapSession( w http.ResponseWriter, node *types.Node, ) *mapSession { - warnf, tracef, infof, errf := logPollFunc(req, node) + warnf, infof, tracef, errf := logPollFunc(req, node) // Use a buffered channel in case a node is not fully ready // to receive a message to make sure we dont block the entire @@ -196,8 +196,10 @@ func (m *mapSession) serve() { // return err := m.handleSaveNode() if err != nil { + mapResponseWriteUpdatesInStream.WithLabelValues("error").Inc() return } + mapResponseWriteUpdatesInStream.WithLabelValues("ok").Inc() } // Set up the client stream @@ -284,6 +286,7 @@ func (m *mapSession) serve() { patches = filteredPatches } + updateType := "full" // When deciding what update to send, the following is considered, // Full is a superset of all updates, when a full update is requested, // send only that and move on, all other updates will be present in @@ -303,12 +306,15 @@ func (m *mapSession) serve() { } else if changed != nil { m.tracef(fmt.Sprintf("Sending Changed MapResponse: %v", lastMessage)) data, err = m.mapper.PeerChangedResponse(m.req, m.node, changed, patches, m.h.ACLPolicy, lastMessage) + updateType = "change" } else if patches != nil { m.tracef(fmt.Sprintf("Sending Changed Patch MapResponse: %v", lastMessage)) data, err = m.mapper.PeerChangedPatchResponse(m.req, m.node, patches, m.h.ACLPolicy) + updateType = "patch" } else if derp { m.tracef("Sending DERPUpdate MapResponse") data, err = m.mapper.DERPMapResponse(m.req, m.node, m.h.DERPMap) + updateType = "derp" } if err != nil { @@ -324,19 +330,22 @@ func (m *mapSession) serve() { startWrite := time.Now() _, err = m.w.Write(data) if err != nil { + mapResponseSent.WithLabelValues("error", updateType).Inc() m.errf(err, "Could not write the map response, for mapSession: %p", m) return } err = rc.Flush() if err != nil { + mapResponseSent.WithLabelValues("error", updateType).Inc() m.errf(err, "flushing the map response to client, for mapSession: %p", m) return } log.Trace().Str("node", m.node.Hostname).TimeDiff("timeSpent", time.Now(), startWrite).Str("mkey", m.node.MachineKey.String()).Msg("finished writing mapresp to node") - m.infof("update sent") + mapResponseSent.WithLabelValues("ok", updateType).Inc() + m.tracef("update sent") } // reset @@ -364,7 +373,8 @@ func (m *mapSession) serve() { // Consume all updates sent to node case update := <-m.ch: - m.tracef("received stream update: %d %s", update.Type, update.Message) + m.tracef("received stream update: %s %s", update.Type.String(), update.Message) + mapResponseUpdateReceived.WithLabelValues(update.Type.String()).Inc() switch update.Type { case types.StateFullUpdate: @@ -404,27 +414,30 @@ func (m *mapSession) serve() { data, err := m.mapper.KeepAliveResponse(m.req, m.node) if err != nil { m.errf(err, "Error generating the keep alive msg") - + mapResponseSent.WithLabelValues("error", "keepalive").Inc() return } _, err = m.w.Write(data) if err != nil { m.errf(err, "Cannot write keep alive message") - + mapResponseSent.WithLabelValues("error", "keepalive").Inc() return } err = rc.Flush() if err != nil { m.errf(err, "flushing keep alive to client, for mapSession: %p", m) + mapResponseSent.WithLabelValues("error", "keepalive").Inc() return } + + mapResponseSent.WithLabelValues("ok", "keepalive").Inc() } } } func (m *mapSession) pollFailoverRoutes(where string, node *types.Node) { update, err := db.Write(m.h.db.DB, func(tx *gorm.DB) (*types.StateUpdate, error) { - return db.FailoverNodeRoutesIfNeccessary(tx, m.h.nodeNotifier.ConnectedMap(), node) + return db.FailoverNodeRoutesIfNeccessary(tx, m.h.nodeNotifier.LikelyConnectedMap(), node) }) if err != nil { m.errf(err, fmt.Sprintf("failed to ensure failover routes, %s", where)) @@ -454,7 +467,7 @@ func (h *Headscale) updateNodeOnlineStatus(online bool, node *types.Node) { node.LastSeen = &now change.LastSeen = &now - err := h.db.DB.Transaction(func(tx *gorm.DB) error { + err := h.db.Write(func(tx *gorm.DB) error { return db.SetLastSeen(tx, node.ID, *node.LastSeen) }) if err != nil { @@ -501,6 +514,7 @@ func (m *mapSession) handleEndpointUpdate() { // If there is no changes and nothing to save, // return early. if peerChangeEmpty(change) && !sendUpdate { + mapResponseEndpointUpdates.WithLabelValues("noop").Inc() return } @@ -518,6 +532,7 @@ func (m *mapSession) handleEndpointUpdate() { if err != nil { m.errf(err, "Error processing node routes") http.Error(m.w, "", http.StatusInternalServerError) + mapResponseEndpointUpdates.WithLabelValues("error").Inc() return } @@ -527,6 +542,7 @@ func (m *mapSession) handleEndpointUpdate() { err := m.h.db.EnableAutoApprovedRoutes(m.h.ACLPolicy, m.node) if err != nil { m.errf(err, "Error running auto approved routes") + mapResponseEndpointUpdates.WithLabelValues("error").Inc() } } @@ -534,19 +550,19 @@ func (m *mapSession) handleEndpointUpdate() { // has an updated packetfilter allowing the new route // if it is defined in the ACL. ctx := types.NotifyCtx(context.Background(), "poll-nodeupdate-self-hostinfochange", m.node.Hostname) - m.h.nodeNotifier.NotifyByMachineKey( + m.h.nodeNotifier.NotifyByNodeID( ctx, types.StateUpdate{ Type: types.StateSelfUpdate, ChangeNodes: []types.NodeID{m.node.ID}, }, m.node.ID) - } if err := m.h.db.DB.Save(m.node).Error; err != nil { m.errf(err, "Failed to persist/update node in the database") http.Error(m.w, "", http.StatusInternalServerError) + mapResponseEndpointUpdates.WithLabelValues("error").Inc() return } @@ -562,6 +578,7 @@ func (m *mapSession) handleEndpointUpdate() { m.node.ID) m.w.WriteHeader(http.StatusOK) + mapResponseEndpointUpdates.WithLabelValues("ok").Inc() return } @@ -639,7 +656,7 @@ func (m *mapSession) handleReadOnlyRequest() { if err != nil { m.errf(err, "Failed to create MapResponse") http.Error(m.w, "", http.StatusInternalServerError) - + mapResponseReadOnly.WithLabelValues("error").Inc() return } @@ -648,9 +665,12 @@ func (m *mapSession) handleReadOnlyRequest() { _, err = m.w.Write(mapResp) if err != nil { m.errf(err, "Failed to write response") + mapResponseReadOnly.WithLabelValues("error").Inc() + return } m.w.WriteHeader(http.StatusOK) + mapResponseReadOnly.WithLabelValues("ok").Inc() return } diff --git a/hscontrol/types/node.go b/hscontrol/types/node.go index 0e30bd9e..7f285924 100644 --- a/hscontrol/types/node.go +++ b/hscontrol/types/node.go @@ -28,7 +28,8 @@ var ( ) type NodeID uint64 -type NodeConnectedMap map[NodeID]bool + +// type NodeConnectedMap *xsync.MapOf[NodeID, bool] func (id NodeID) StableID() tailcfg.StableNodeID { return tailcfg.StableNodeID(strconv.FormatUint(uint64(id), util.Base10)) diff --git a/integration/acl_test.go b/integration/acl_test.go index 517e2dfb..9d763965 100644 --- a/integration/acl_test.go +++ b/integration/acl_test.go @@ -51,7 +51,7 @@ func aclScenario( clientsPerUser int, ) *Scenario { t.Helper() - scenario, err := NewScenario() + scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) spec := map[string]int{ @@ -264,7 +264,7 @@ func TestACLHostsInNetMapTable(t *testing.T) { for name, testCase := range tests { t.Run(name, func(t *testing.T) { - scenario, err := NewScenario() + scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) spec := testCase.users diff --git a/integration/auth_oidc_test.go b/integration/auth_oidc_test.go index 347dbcc1..d24bf452 100644 --- a/integration/auth_oidc_test.go +++ b/integration/auth_oidc_test.go @@ -42,7 +42,7 @@ func TestOIDCAuthenticationPingAll(t *testing.T) { IntegrationSkip(t) t.Parallel() - baseScenario, err := NewScenario() + baseScenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) scenario := AuthOIDCScenario{ @@ -100,7 +100,7 @@ func TestOIDCExpireNodesBasedOnTokenExpiry(t *testing.T) { shortAccessTTL := 5 * time.Minute - baseScenario, err := NewScenario() + baseScenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) baseScenario.pool.MaxWait = 5 * time.Minute diff --git a/integration/auth_web_flow_test.go b/integration/auth_web_flow_test.go index 6d981bc1..8e121ca0 100644 --- a/integration/auth_web_flow_test.go +++ b/integration/auth_web_flow_test.go @@ -26,7 +26,7 @@ func TestAuthWebFlowAuthenticationPingAll(t *testing.T) { IntegrationSkip(t) t.Parallel() - baseScenario, err := NewScenario() + baseScenario, err := NewScenario(dockertestMaxWait()) if err != nil { t.Fatalf("failed to create scenario: %s", err) } @@ -67,7 +67,7 @@ func TestAuthWebFlowLogoutAndRelogin(t *testing.T) { IntegrationSkip(t) t.Parallel() - baseScenario, err := NewScenario() + baseScenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) scenario := AuthWebFlowScenario{ diff --git a/integration/cli_test.go b/integration/cli_test.go index af7b073b..24e3b19b 100644 --- a/integration/cli_test.go +++ b/integration/cli_test.go @@ -32,7 +32,7 @@ func TestUserCommand(t *testing.T) { IntegrationSkip(t) t.Parallel() - scenario, err := NewScenario() + scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) defer scenario.Shutdown() @@ -112,7 +112,7 @@ func TestPreAuthKeyCommand(t *testing.T) { user := "preauthkeyspace" count := 3 - scenario, err := NewScenario() + scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) defer scenario.Shutdown() @@ -254,7 +254,7 @@ func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) { user := "pre-auth-key-without-exp-user" - scenario, err := NewScenario() + scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) defer scenario.Shutdown() @@ -317,7 +317,7 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) { user := "pre-auth-key-reus-ephm-user" - scenario, err := NewScenario() + scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) defer scenario.Shutdown() @@ -394,7 +394,7 @@ func TestApiKeyCommand(t *testing.T) { count := 5 - scenario, err := NewScenario() + scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) defer scenario.Shutdown() @@ -562,7 +562,7 @@ func TestNodeTagCommand(t *testing.T) { IntegrationSkip(t) t.Parallel() - scenario, err := NewScenario() + scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) defer scenario.Shutdown() @@ -695,7 +695,7 @@ func TestNodeAdvertiseTagNoACLCommand(t *testing.T) { IntegrationSkip(t) t.Parallel() - scenario, err := NewScenario() + scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) defer scenario.Shutdown() @@ -745,7 +745,7 @@ func TestNodeAdvertiseTagWithACLCommand(t *testing.T) { IntegrationSkip(t) t.Parallel() - scenario, err := NewScenario() + scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) defer scenario.Shutdown() @@ -808,7 +808,7 @@ func TestNodeCommand(t *testing.T) { IntegrationSkip(t) t.Parallel() - scenario, err := NewScenario() + scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) defer scenario.Shutdown() @@ -1049,7 +1049,7 @@ func TestNodeExpireCommand(t *testing.T) { IntegrationSkip(t) t.Parallel() - scenario, err := NewScenario() + scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) defer scenario.Shutdown() @@ -1176,7 +1176,7 @@ func TestNodeRenameCommand(t *testing.T) { IntegrationSkip(t) t.Parallel() - scenario, err := NewScenario() + scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) defer scenario.Shutdown() @@ -1343,7 +1343,7 @@ func TestNodeMoveCommand(t *testing.T) { IntegrationSkip(t) t.Parallel() - scenario, err := NewScenario() + scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) defer scenario.Shutdown() diff --git a/integration/embedded_derp_test.go b/integration/embedded_derp_test.go index b6a62e5f..39a9acca 100644 --- a/integration/embedded_derp_test.go +++ b/integration/embedded_derp_test.go @@ -23,7 +23,7 @@ func TestDERPServerScenario(t *testing.T) { IntegrationSkip(t) // t.Parallel() - baseScenario, err := NewScenario() + baseScenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) scenario := EmbeddedDERPServerScenario{ diff --git a/integration/general_test.go b/integration/general_test.go index ffd209d8..89e0d342 100644 --- a/integration/general_test.go +++ b/integration/general_test.go @@ -23,7 +23,7 @@ func TestPingAllByIP(t *testing.T) { IntegrationSkip(t) t.Parallel() - scenario, err := NewScenario() + scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) defer scenario.Shutdown() @@ -67,7 +67,7 @@ func TestPingAllByIPPublicDERP(t *testing.T) { IntegrationSkip(t) t.Parallel() - scenario, err := NewScenario() + scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) defer scenario.Shutdown() @@ -105,7 +105,7 @@ func TestAuthKeyLogoutAndRelogin(t *testing.T) { IntegrationSkip(t) t.Parallel() - scenario, err := NewScenario() + scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) defer scenario.Shutdown() @@ -216,7 +216,7 @@ func TestEphemeral(t *testing.T) { IntegrationSkip(t) t.Parallel() - scenario, err := NewScenario() + scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) defer scenario.Shutdown() @@ -299,7 +299,7 @@ func TestPingAllByHostname(t *testing.T) { IntegrationSkip(t) t.Parallel() - scenario, err := NewScenario() + scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) defer scenario.Shutdown() @@ -348,7 +348,7 @@ func TestTaildrop(t *testing.T) { return err } - scenario, err := NewScenario() + scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) defer scenario.Shutdown() @@ -509,7 +509,7 @@ func TestResolveMagicDNS(t *testing.T) { IntegrationSkip(t) t.Parallel() - scenario, err := NewScenario() + scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) defer scenario.Shutdown() @@ -577,7 +577,7 @@ func TestExpireNode(t *testing.T) { IntegrationSkip(t) t.Parallel() - scenario, err := NewScenario() + scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) defer scenario.Shutdown() @@ -703,7 +703,7 @@ func TestNodeOnlineStatus(t *testing.T) { IntegrationSkip(t) t.Parallel() - scenario, err := NewScenario() + scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) defer scenario.Shutdown() @@ -818,7 +818,7 @@ func TestPingAllByIPManyUpDown(t *testing.T) { IntegrationSkip(t) t.Parallel() - scenario, err := NewScenario() + scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) defer scenario.Shutdown() diff --git a/integration/hsic/hsic.go b/integration/hsic/hsic.go index de4ec41f..0483213b 100644 --- a/integration/hsic/hsic.go +++ b/integration/hsic/hsic.go @@ -18,6 +18,7 @@ import ( "net/url" "os" "path" + "strconv" "strings" "time" @@ -201,6 +202,14 @@ func WithEmbeddedDERPServerOnly() Option { } } +// WithTuning allows changing the tuning settings easily. +func WithTuning(batchTimeout time.Duration, mapSessionChanSize int) Option { + return func(hsic *HeadscaleInContainer) { + hsic.env["HEADSCALE_TUNING_BATCH_CHANGE_DELAY"] = batchTimeout.String() + hsic.env["HEADSCALE_TUNING_NODE_MAPSESSION_BUFFERED_CHAN_SIZE"] = strconv.Itoa(mapSessionChanSize) + } +} + // New returns a new HeadscaleInContainer instance. func New( pool *dockertest.Pool, diff --git a/integration/route_test.go b/integration/route_test.go index 150dbd27..15ea22b1 100644 --- a/integration/route_test.go +++ b/integration/route_test.go @@ -28,7 +28,7 @@ func TestEnablingRoutes(t *testing.T) { user := "enable-routing" - scenario, err := NewScenario() + scenario, err := NewScenario(dockertestMaxWait()) assertNoErrf(t, "failed to create scenario: %s", err) defer scenario.Shutdown() @@ -250,7 +250,7 @@ func TestHASubnetRouterFailover(t *testing.T) { user := "enable-routing" - scenario, err := NewScenario() + scenario, err := NewScenario(dockertestMaxWait()) assertNoErrf(t, "failed to create scenario: %s", err) // defer scenario.Shutdown() @@ -822,7 +822,7 @@ func TestEnableDisableAutoApprovedRoute(t *testing.T) { user := "enable-disable-routing" - scenario, err := NewScenario() + scenario, err := NewScenario(dockertestMaxWait()) assertNoErrf(t, "failed to create scenario: %s", err) defer scenario.Shutdown() @@ -966,7 +966,7 @@ func TestSubnetRouteACL(t *testing.T) { user := "subnet-route-acl" - scenario, err := NewScenario() + scenario, err := NewScenario(dockertestMaxWait()) assertNoErrf(t, "failed to create scenario: %s", err) defer scenario.Shutdown() diff --git a/integration/scenario.go b/integration/scenario.go index 0ba44e7d..9444d882 100644 --- a/integration/scenario.go +++ b/integration/scenario.go @@ -8,6 +8,7 @@ import ( "os" "sort" "sync" + "time" v1 "github.com/juanfont/headscale/gen/go/headscale/v1" "github.com/juanfont/headscale/hscontrol/util" @@ -141,7 +142,7 @@ type Scenario struct { // NewScenario creates a test Scenario which can be used to bootstraps a ControlServer with // a set of Users and TailscaleClients. -func NewScenario() (*Scenario, error) { +func NewScenario(maxWait time.Duration) (*Scenario, error) { hash, err := util.GenerateRandomStringDNSSafe(scenarioHashLength) if err != nil { return nil, err @@ -152,7 +153,7 @@ func NewScenario() (*Scenario, error) { return nil, fmt.Errorf("could not connect to docker: %w", err) } - pool.MaxWait = dockertestMaxWait() + pool.MaxWait = maxWait networkName := fmt.Sprintf("hs-%s", hash) if overrideNetworkName := os.Getenv("HEADSCALE_TEST_NETWORK_NAME"); overrideNetworkName != "" { diff --git a/integration/scenario_test.go b/integration/scenario_test.go index cc9810a4..ea941ed7 100644 --- a/integration/scenario_test.go +++ b/integration/scenario_test.go @@ -33,7 +33,7 @@ func TestHeadscale(t *testing.T) { user := "test-space" - scenario, err := NewScenario() + scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) defer scenario.Shutdown() @@ -78,7 +78,7 @@ func TestCreateTailscale(t *testing.T) { user := "only-create-containers" - scenario, err := NewScenario() + scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) defer scenario.Shutdown() @@ -114,7 +114,7 @@ func TestTailscaleNodesJoiningHeadcale(t *testing.T) { count := 1 - scenario, err := NewScenario() + scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) defer scenario.Shutdown() diff --git a/integration/ssh_test.go b/integration/ssh_test.go index 587190e4..6d053b0d 100644 --- a/integration/ssh_test.go +++ b/integration/ssh_test.go @@ -44,7 +44,7 @@ var retry = func(times int, sleepInterval time.Duration, func sshScenario(t *testing.T, policy *policy.ACLPolicy, clientsPerUser int) *Scenario { t.Helper() - scenario, err := NewScenario() + scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) spec := map[string]int{ From 9375b0920696f3aae23f6cf96643304b96531e71 Mon Sep 17 00:00:00 2001 From: ChengenH Date: Sun, 21 Apr 2024 22:53:50 +0800 Subject: [PATCH 040/145] chore: use errors.New to replace fmt.Errorf with no parameters will much better Signed-off-by: ChengenH --- hscontrol/grpcv1.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/hscontrol/grpcv1.go b/hscontrol/grpcv1.go index 41be5e9d..d9cd653d 100644 --- a/hscontrol/grpcv1.go +++ b/hscontrol/grpcv1.go @@ -4,7 +4,6 @@ package hscontrol import ( "context" "errors" - "fmt" "sort" "strings" "time" @@ -279,13 +278,13 @@ func (api headscaleV1APIServer) SetTags( func validateTag(tag string) error { if strings.Index(tag, "tag:") != 0 { - return fmt.Errorf("tag must start with the string 'tag:'") + return errors.New("tag must start with the string 'tag:'") } if strings.ToLower(tag) != tag { - return fmt.Errorf("tag should be lowercase") + return errors.New("tag should be lowercase") } if len(strings.Fields(tag)) > 1 { - return fmt.Errorf("tag should not contains space") + return errors.New("tag should not contains space") } return nil } From bd047928f73687c5d75b804b996951c6fd37b2e6 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Sun, 21 Apr 2024 22:08:59 +0200 Subject: [PATCH 041/145] Move pprof to metrics router (#1902) --- hscontrol/app.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/hscontrol/app.go b/hscontrol/app.go index acc94229..9eb6b4cc 100644 --- a/hscontrol/app.go +++ b/hscontrol/app.go @@ -453,7 +453,6 @@ func (h *Headscale) ensureUnixSocketIsAbsent() error { func (h *Headscale) createRouter(grpcMux *grpcRuntime.ServeMux) *mux.Router { router := mux.NewRouter() router.Use(prometheusMiddleware) - router.PathPrefix("/debug/pprof/").Handler(http.DefaultServeMux) router.HandleFunc(ts2021UpgradePath, h.NoiseUpgradeHandler).Methods(http.MethodPost) @@ -681,7 +680,7 @@ func (h *Headscale) Serve() error { // HTTP setup // // This is the regular router that we expose - // over our main Addr. It also serves the legacy Tailcale API + // over our main Addr router := h.createRouter(grpcGatewayMux) httpServer := &http.Server{ @@ -711,11 +710,10 @@ func (h *Headscale) Serve() error { Msgf("listening and serving HTTP on: %s", h.cfg.Addr) debugMux := http.NewServeMux() + debugMux.Handle("/debug/pprof/", http.DefaultServeMux) debugMux.HandleFunc("/debug/notifier", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte(h.nodeNotifier.String())) - - return }) debugMux.HandleFunc("/debug/mapresp", func(w http.ResponseWriter, r *http.Request) { h.mapSessionMu.Lock() @@ -729,8 +727,6 @@ func (h *Headscale) Serve() error { w.WriteHeader(http.StatusOK) w.Write([]byte(b.String())) - - return }) debugMux.Handle("/metrics", promhttp.Handler()) From aba4b36030e5fe6c8fd41721a4f6ed5ae4079554 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Mon, 22 Apr 2024 20:37:59 +0200 Subject: [PATCH 042/145] Clarify relation with Tailscale (#1908) --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3103e44c..61619385 100644 --- a/README.md +++ b/README.md @@ -87,8 +87,11 @@ Please have a look at the [`documentation`](https://headscale.net/). ## Disclaimer -1. This project is not associated with Tailscale Inc. -2. The purpose of Headscale is maintaining a working, self-hosted Tailscale control panel. +This project is not associated with Tailscale Inc. + +However, one of the active maintainers for Headscale [is employed by Tailscale](https://tailscale.com/blog/opensource) and he is allowed to spend work hours contributing to the project. Contributions from this maintainer are reviewed by other maintainers. + +The maintainers work together on setting the direction for the project. The underlying principle is to serve the community of self-hosters, enthusiasts and hobbyists - while having a sustainable project. ## Contributing From 9229d17bbe28800fe54d3d56a274069dad3b21b7 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Mon, 22 Apr 2024 21:21:38 +0200 Subject: [PATCH 043/145] remove examples/, and kustomize (#1906) this directory is unmaintained and not verified, if it should be restored, it should end up under the community docs effort. Signed-off-by: Kristoffer Dalby --- examples/README.md | 5 - examples/kustomize/.gitignore | 2 - examples/kustomize/README.md | 100 ------------------ examples/kustomize/base/configmap.yaml | 9 -- examples/kustomize/base/ingress.yaml | 18 ---- examples/kustomize/base/kustomization.yaml | 42 -------- examples/kustomize/base/service.yaml | 13 --- examples/kustomize/headscale.bash | 3 - examples/kustomize/init.bash | 22 ---- examples/kustomize/install-cert-manager.bash | 3 - examples/kustomize/postgres/deployment.yaml | 81 -------------- .../kustomize/postgres/kustomization.yaml | 13 --- .../kustomize/postgres/postgres-service.yaml | 13 --- .../postgres/postgres-statefulset.yaml | 49 --------- .../production-tls/ingress-patch.yaml | 11 -- .../production-tls/kustomization.yaml | 9 -- .../production-tls/production-issuer.yaml | 16 --- examples/kustomize/sqlite/kustomization.yaml | 5 - examples/kustomize/sqlite/statefulset.yaml | 82 -------------- .../kustomize/staging-tls/ingress-patch.yaml | 11 -- .../kustomize/staging-tls/kustomization.yaml | 9 -- .../kustomize/staging-tls/staging-issuer.yaml | 16 --- 22 files changed, 532 deletions(-) delete mode 100644 examples/README.md delete mode 100644 examples/kustomize/.gitignore delete mode 100644 examples/kustomize/README.md delete mode 100644 examples/kustomize/base/configmap.yaml delete mode 100644 examples/kustomize/base/ingress.yaml delete mode 100644 examples/kustomize/base/kustomization.yaml delete mode 100644 examples/kustomize/base/service.yaml delete mode 100755 examples/kustomize/headscale.bash delete mode 100755 examples/kustomize/init.bash delete mode 100755 examples/kustomize/install-cert-manager.bash delete mode 100644 examples/kustomize/postgres/deployment.yaml delete mode 100644 examples/kustomize/postgres/kustomization.yaml delete mode 100644 examples/kustomize/postgres/postgres-service.yaml delete mode 100644 examples/kustomize/postgres/postgres-statefulset.yaml delete mode 100644 examples/kustomize/production-tls/ingress-patch.yaml delete mode 100644 examples/kustomize/production-tls/kustomization.yaml delete mode 100644 examples/kustomize/production-tls/production-issuer.yaml delete mode 100644 examples/kustomize/sqlite/kustomization.yaml delete mode 100644 examples/kustomize/sqlite/statefulset.yaml delete mode 100644 examples/kustomize/staging-tls/ingress-patch.yaml delete mode 100644 examples/kustomize/staging-tls/kustomization.yaml delete mode 100644 examples/kustomize/staging-tls/staging-issuer.yaml diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index f9e85ff3..00000000 --- a/examples/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Examples - -This directory contains examples on how to run `headscale` on different platforms. - -All examples are provided by the community and they are not verified by the `headscale` authors. diff --git a/examples/kustomize/.gitignore b/examples/kustomize/.gitignore deleted file mode 100644 index 229058d2..00000000 --- a/examples/kustomize/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/**/site -/**/secrets diff --git a/examples/kustomize/README.md b/examples/kustomize/README.md deleted file mode 100644 index cc57f147..00000000 --- a/examples/kustomize/README.md +++ /dev/null @@ -1,100 +0,0 @@ -# Deploying headscale on Kubernetes - -**Note:** This is contributed by the community and not verified by the headscale authors. - -This directory contains [Kustomize](https://kustomize.io) templates that deploy -headscale in various configurations. - -These templates currently support Rancher k3s. Other clusters may require -adaptation, especially around volume claims and ingress. - -Commands below assume this directory is your current working directory. - -# Generate secrets and site configuration - -Run `./init.bash` to generate keys, passwords, and site configuration files. - -Edit `base/site/public.env`, changing `public-hostname` to the public DNS name -that will be used for your headscale deployment. - -Set `public-proto` to "https" if you're planning to use TLS & Let's Encrypt. - -Configure DERP servers by editing `base/site/derp.yaml` if needed. - -# Add the image to the registry - -You'll somehow need to get `headscale:latest` into your cluster image registry. - -An easy way to do this with k3s: - -- Reconfigure k3s to use docker instead of containerd (`k3s server --docker`) -- `docker build -t headscale:latest ..` from here - -# Create the namespace - -If it doesn't already exist, `kubectl create ns headscale`. - -# Deploy headscale - -## sqlite - -`kubectl -n headscale apply -k ./sqlite` - -## postgres - -`kubectl -n headscale apply -k ./postgres` - -# TLS & Let's Encrypt - -Test a staging certificate with your configured DNS name and Let's Encrypt. - -`kubectl -n headscale apply -k ./staging-tls` - -Replace with a production certificate. - -`kubectl -n headscale apply -k ./production-tls` - -## Static / custom TLS certificates - -Only Let's Encrypt is supported. If you need other TLS settings, modify or patch the ingress. - -# Administration - -Use the wrapper script to remotely operate headscale to perform administrative -tasks like creating namespaces, authkeys, etc. - -``` -[c@nix-slate:~/Projects/headscale/k8s]$ ./headscale.bash - -headscale is an open source implementation of the Tailscale control server - -https://github.com/juanfont/headscale - -Usage: - headscale [command] - -Available Commands: - help Help about any command - namespace Manage the namespaces of headscale - node Manage the nodes of headscale - preauthkey Handle the preauthkeys in headscale - routes Manage the routes of headscale - serve Launches the headscale server - version Print the version. - -Flags: - -h, --help help for headscale - -o, --output string Output format. Empty for human-readable, 'json' or 'json-line' - -Use "headscale [command] --help" for more information about a command. - -``` - -# TODO / Ideas - -- Interpolate `email:` option to the ClusterIssuer from site configuration. - This probably needs to be done with a transformer, kustomize vars don't seem to work. -- Add kustomize examples for cloud-native ingress, load balancer -- CockroachDB for the backend -- DERP server deployment -- Tor hidden service diff --git a/examples/kustomize/base/configmap.yaml b/examples/kustomize/base/configmap.yaml deleted file mode 100644 index 0ac2d563..00000000 --- a/examples/kustomize/base/configmap.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: headscale-config -data: - server_url: $(PUBLIC_PROTO)://$(PUBLIC_HOSTNAME) - listen_addr: "0.0.0.0:8080" - metrics_listen_addr: "127.0.0.1:9090" - ephemeral_node_inactivity_timeout: "30m" diff --git a/examples/kustomize/base/ingress.yaml b/examples/kustomize/base/ingress.yaml deleted file mode 100644 index 51da3427..00000000 --- a/examples/kustomize/base/ingress.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: headscale - annotations: - kubernetes.io/ingress.class: traefik -spec: - rules: - - host: $(PUBLIC_HOSTNAME) - http: - paths: - - backend: - service: - name: headscale - port: - number: 8080 - path: / - pathType: Prefix diff --git a/examples/kustomize/base/kustomization.yaml b/examples/kustomize/base/kustomization.yaml deleted file mode 100644 index 93278f7d..00000000 --- a/examples/kustomize/base/kustomization.yaml +++ /dev/null @@ -1,42 +0,0 @@ -namespace: headscale -resources: - - configmap.yaml - - ingress.yaml - - service.yaml -generatorOptions: - disableNameSuffixHash: true -configMapGenerator: - - name: headscale-site - files: - - derp.yaml=site/derp.yaml - envs: - - site/public.env - - name: headscale-etc - literals: - - config.json={} -secretGenerator: - - name: headscale - files: - - secrets/private-key -vars: - - name: PUBLIC_PROTO - objRef: - kind: ConfigMap - name: headscale-site - apiVersion: v1 - fieldRef: - fieldPath: data.public-proto - - name: PUBLIC_HOSTNAME - objRef: - kind: ConfigMap - name: headscale-site - apiVersion: v1 - fieldRef: - fieldPath: data.public-hostname - - name: CONTACT_EMAIL - objRef: - kind: ConfigMap - name: headscale-site - apiVersion: v1 - fieldRef: - fieldPath: data.contact-email diff --git a/examples/kustomize/base/service.yaml b/examples/kustomize/base/service.yaml deleted file mode 100644 index 39e67253..00000000 --- a/examples/kustomize/base/service.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: headscale - labels: - app: headscale -spec: - selector: - app: headscale - ports: - - name: http - targetPort: http - port: 8080 diff --git a/examples/kustomize/headscale.bash b/examples/kustomize/headscale.bash deleted file mode 100755 index 66bfe92c..00000000 --- a/examples/kustomize/headscale.bash +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash -set -eu -exec kubectl -n headscale exec -ti pod/headscale-0 -- /go/bin/headscale "$@" diff --git a/examples/kustomize/init.bash b/examples/kustomize/init.bash deleted file mode 100755 index e5b7965c..00000000 --- a/examples/kustomize/init.bash +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash -set -eux -cd $(dirname $0) - -umask 022 -mkdir -p base/site/ -[ ! -e base/site/public.env ] && ( - cat >base/site/public.env < base/secrets/private-key -) -mkdir -p postgres/secrets/ -[ ! -e postgres/secrets/password ] && (head -c 32 /dev/urandom | base64 -w0 > postgres/secrets/password) diff --git a/examples/kustomize/install-cert-manager.bash b/examples/kustomize/install-cert-manager.bash deleted file mode 100755 index 1a5ecacb..00000000 --- a/examples/kustomize/install-cert-manager.bash +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash -set -eux -kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.4.0/cert-manager.yaml diff --git a/examples/kustomize/postgres/deployment.yaml b/examples/kustomize/postgres/deployment.yaml deleted file mode 100644 index 1dd88b41..00000000 --- a/examples/kustomize/postgres/deployment.yaml +++ /dev/null @@ -1,81 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: headscale -spec: - replicas: 2 - selector: - matchLabels: - app: headscale - template: - metadata: - labels: - app: headscale - spec: - containers: - - name: headscale - image: "headscale:latest" - imagePullPolicy: IfNotPresent - command: ["/go/bin/headscale", "serve"] - env: - - name: SERVER_URL - value: $(PUBLIC_PROTO)://$(PUBLIC_HOSTNAME) - - name: LISTEN_ADDR - valueFrom: - configMapKeyRef: - name: headscale-config - key: listen_addr - - name: METRICS_LISTEN_ADDR - valueFrom: - configMapKeyRef: - name: headscale-config - key: metrics_listen_addr - - name: DERP_MAP_PATH - value: /vol/config/derp.yaml - - name: EPHEMERAL_NODE_INACTIVITY_TIMEOUT - valueFrom: - configMapKeyRef: - name: headscale-config - key: ephemeral_node_inactivity_timeout - - name: DB_TYPE - value: postgres - - name: DB_HOST - value: postgres.headscale.svc.cluster.local - - name: DB_PORT - value: "5432" - - name: DB_USER - value: headscale - - name: DB_PASS - valueFrom: - secretKeyRef: - name: postgresql - key: password - - name: DB_NAME - value: headscale - ports: - - name: http - protocol: TCP - containerPort: 8080 - livenessProbe: - tcpSocket: - port: http - initialDelaySeconds: 30 - timeoutSeconds: 5 - periodSeconds: 15 - volumeMounts: - - name: config - mountPath: /vol/config - - name: secret - mountPath: /vol/secret - - name: etc - mountPath: /etc/headscale - volumes: - - name: config - configMap: - name: headscale-site - - name: etc - configMap: - name: headscale-etc - - name: secret - secret: - secretName: headscale diff --git a/examples/kustomize/postgres/kustomization.yaml b/examples/kustomize/postgres/kustomization.yaml deleted file mode 100644 index e732e3b9..00000000 --- a/examples/kustomize/postgres/kustomization.yaml +++ /dev/null @@ -1,13 +0,0 @@ -namespace: headscale -bases: - - ../base -resources: - - deployment.yaml - - postgres-service.yaml - - postgres-statefulset.yaml -generatorOptions: - disableNameSuffixHash: true -secretGenerator: - - name: postgresql - files: - - secrets/password diff --git a/examples/kustomize/postgres/postgres-service.yaml b/examples/kustomize/postgres/postgres-service.yaml deleted file mode 100644 index 6252e7f9..00000000 --- a/examples/kustomize/postgres/postgres-service.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: postgres - labels: - app: postgres -spec: - selector: - app: postgres - ports: - - name: postgres - targetPort: postgres - port: 5432 diff --git a/examples/kustomize/postgres/postgres-statefulset.yaml b/examples/kustomize/postgres/postgres-statefulset.yaml deleted file mode 100644 index b81c9bf0..00000000 --- a/examples/kustomize/postgres/postgres-statefulset.yaml +++ /dev/null @@ -1,49 +0,0 @@ -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: postgres -spec: - serviceName: postgres - replicas: 1 - selector: - matchLabels: - app: postgres - template: - metadata: - labels: - app: postgres - spec: - containers: - - name: postgres - image: "postgres:13" - imagePullPolicy: IfNotPresent - env: - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: postgresql - key: password - - name: POSTGRES_USER - value: headscale - ports: - - name: postgres - protocol: TCP - containerPort: 5432 - livenessProbe: - tcpSocket: - port: 5432 - initialDelaySeconds: 30 - timeoutSeconds: 5 - periodSeconds: 15 - volumeMounts: - - name: pgdata - mountPath: /var/lib/postgresql/data - volumeClaimTemplates: - - metadata: - name: pgdata - spec: - storageClassName: local-path - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 1Gi diff --git a/examples/kustomize/production-tls/ingress-patch.yaml b/examples/kustomize/production-tls/ingress-patch.yaml deleted file mode 100644 index 9e6177fb..00000000 --- a/examples/kustomize/production-tls/ingress-patch.yaml +++ /dev/null @@ -1,11 +0,0 @@ -kind: Ingress -metadata: - name: headscale - annotations: - cert-manager.io/cluster-issuer: letsencrypt-production - traefik.ingress.kubernetes.io/router.tls: "true" -spec: - tls: - - hosts: - - $(PUBLIC_HOSTNAME) - secretName: production-cert diff --git a/examples/kustomize/production-tls/kustomization.yaml b/examples/kustomize/production-tls/kustomization.yaml deleted file mode 100644 index d3147f5f..00000000 --- a/examples/kustomize/production-tls/kustomization.yaml +++ /dev/null @@ -1,9 +0,0 @@ -namespace: headscale -bases: - - ../base -resources: - - production-issuer.yaml -patches: - - path: ingress-patch.yaml - target: - kind: Ingress diff --git a/examples/kustomize/production-tls/production-issuer.yaml b/examples/kustomize/production-tls/production-issuer.yaml deleted file mode 100644 index f436090b..00000000 --- a/examples/kustomize/production-tls/production-issuer.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: cert-manager.io/v1 -kind: ClusterIssuer -metadata: - name: letsencrypt-production -spec: - acme: - # TODO: figure out how to get kustomize to interpolate this, or use a transformer - #email: $(CONTACT_EMAIL) - server: https://acme-v02.api.letsencrypt.org/directory - privateKeySecretRef: - # Secret resource used to store the account's private key. - name: letsencrypt-production-acc-key - solvers: - - http01: - ingress: - class: traefik diff --git a/examples/kustomize/sqlite/kustomization.yaml b/examples/kustomize/sqlite/kustomization.yaml deleted file mode 100644 index ca799419..00000000 --- a/examples/kustomize/sqlite/kustomization.yaml +++ /dev/null @@ -1,5 +0,0 @@ -namespace: headscale -bases: - - ../base -resources: - - statefulset.yaml diff --git a/examples/kustomize/sqlite/statefulset.yaml b/examples/kustomize/sqlite/statefulset.yaml deleted file mode 100644 index 2321d39d..00000000 --- a/examples/kustomize/sqlite/statefulset.yaml +++ /dev/null @@ -1,82 +0,0 @@ -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: headscale -spec: - serviceName: headscale - replicas: 1 - selector: - matchLabels: - app: headscale - template: - metadata: - labels: - app: headscale - spec: - containers: - - name: headscale - image: "headscale:latest" - imagePullPolicy: IfNotPresent - command: ["/go/bin/headscale", "serve"] - env: - - name: SERVER_URL - value: $(PUBLIC_PROTO)://$(PUBLIC_HOSTNAME) - - name: LISTEN_ADDR - valueFrom: - configMapKeyRef: - name: headscale-config - key: listen_addr - - name: METRICS_LISTEN_ADDR - valueFrom: - configMapKeyRef: - name: headscale-config - key: metrics_listen_addr - - name: DERP_MAP_PATH - value: /vol/config/derp.yaml - - name: EPHEMERAL_NODE_INACTIVITY_TIMEOUT - valueFrom: - configMapKeyRef: - name: headscale-config - key: ephemeral_node_inactivity_timeout - - name: DB_TYPE - value: sqlite3 - - name: DB_PATH - value: /vol/data/db.sqlite - ports: - - name: http - protocol: TCP - containerPort: 8080 - livenessProbe: - tcpSocket: - port: http - initialDelaySeconds: 30 - timeoutSeconds: 5 - periodSeconds: 15 - volumeMounts: - - name: config - mountPath: /vol/config - - name: data - mountPath: /vol/data - - name: secret - mountPath: /vol/secret - - name: etc - mountPath: /etc/headscale - volumes: - - name: config - configMap: - name: headscale-site - - name: etc - configMap: - name: headscale-etc - - name: secret - secret: - secretName: headscale - volumeClaimTemplates: - - metadata: - name: data - spec: - storageClassName: local-path - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 1Gi diff --git a/examples/kustomize/staging-tls/ingress-patch.yaml b/examples/kustomize/staging-tls/ingress-patch.yaml deleted file mode 100644 index 5a1daf0c..00000000 --- a/examples/kustomize/staging-tls/ingress-patch.yaml +++ /dev/null @@ -1,11 +0,0 @@ -kind: Ingress -metadata: - name: headscale - annotations: - cert-manager.io/cluster-issuer: letsencrypt-staging - traefik.ingress.kubernetes.io/router.tls: "true" -spec: - tls: - - hosts: - - $(PUBLIC_HOSTNAME) - secretName: staging-cert diff --git a/examples/kustomize/staging-tls/kustomization.yaml b/examples/kustomize/staging-tls/kustomization.yaml deleted file mode 100644 index 0900c583..00000000 --- a/examples/kustomize/staging-tls/kustomization.yaml +++ /dev/null @@ -1,9 +0,0 @@ -namespace: headscale -bases: - - ../base -resources: - - staging-issuer.yaml -patches: - - path: ingress-patch.yaml - target: - kind: Ingress diff --git a/examples/kustomize/staging-tls/staging-issuer.yaml b/examples/kustomize/staging-tls/staging-issuer.yaml deleted file mode 100644 index cf290415..00000000 --- a/examples/kustomize/staging-tls/staging-issuer.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: cert-manager.io/v1 -kind: ClusterIssuer -metadata: - name: letsencrypt-staging -spec: - acme: - # TODO: figure out how to get kustomize to interpolate this, or use a transformer - #email: $(CONTACT_EMAIL) - server: https://acme-staging-v02.api.letsencrypt.org/directory - privateKeySecretRef: - # Secret resource used to store the account's private key. - name: letsencrypt-staging-acc-key - solvers: - - http01: - ingress: - class: traefik From 318d5d2b215810e714e7ad6e1b16852410cd5422 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Mon, 22 Apr 2024 12:31:16 +0200 Subject: [PATCH 044/145] replace issue templates with github issue forms Signed-off-by: Kristoffer Dalby --- .github/ISSUE_TEMPLATE/bug_report.md | 65 ---------------- .github/ISSUE_TEMPLATE/bug_report.yaml | 83 +++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 26 ------- .github/ISSUE_TEMPLATE/feature_request.yaml | 36 +++++++++ 4 files changed, 119 insertions(+), 91 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yaml delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yaml diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 8563e7af..00000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -name: "Bug report" -about: "Create a bug report to help us improve" -title: "" -labels: ["bug"] -assignees: "" ---- - - - -## Bug description - - - -## Environment - - - -- OS: -- Headscale version: -- Tailscale version: - - - -- [ ] Headscale is behind a (reverse) proxy -- [ ] Headscale runs in a container - -## To Reproduce - - - -## Logs and attachments - - diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 00000000..a7afb6d3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,83 @@ +name: 🐞 Bug +description: File a bug/issue +title: "[Bug] " +labels: ["bug", "needs triage"] +body: + - type: checkboxes + attributes: + label: Is this a support request? + description: This issue tracker is for bugs and feature requests only. If you need help, please use ask in our Discord community + options: + - label: This is not a support request + required: true + - type: checkboxes + attributes: + label: Is there an existing issue for this? + description: Please search to see if an issue already exists for the bug you encountered. + options: + - label: I have searched the existing issues + required: true + - type: textarea + attributes: + label: Current Behavior + description: A concise description of what you're experiencing. + validations: + required: true + - type: textarea + attributes: + label: Expected Behavior + description: A concise description of what you expected to happen. + validations: + required: true + - type: textarea + attributes: + label: Steps To Reproduce + description: Steps to reproduce the behavior. + placeholder: | + 1. In this environment... + 1. With this config... + 1. Run '...' + 1. See error... + validations: + required: true + - type: textarea + attributes: + label: Environment + description: | + examples: + - **OS**: Ubuntu 20.04 + - **Headscale version**: 0.22.3 + - **Tailscale version**: 1.64.0 + value: | + - OS: + - Headscale version: + - Tailscale version: + render: markdown + validations: + required: true + - type: checkboxes + attributes: + label: Runtime environment + options: + - label: Headscale is behind a (reverse) proxy + required: false + - label: Headscale runs in a container + required: false + - type: textarea + attributes: + label: Anything else? + description: | + Links? References? Anything that will give us more context about the issue you are encountering! + + - Client netmap dump (see below) + - ACL configuration + - Headscale configuration + + Dump the netmap of tailscale clients: + `tailscale debug netmap > DESCRIPTIVE_NAME.json` + + Please provide information describing the netmap, which client, which headscale version etc. + + Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 92c51b8f..00000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -name: "Feature request" -about: "Suggest an idea for headscale" -title: "" -labels: ["enhancement"] -assignees: "" ---- - -<!-- -We typically have a clear roadmap for what we want to improve and reserve the right -to close feature requests that does not fit in the roadmap, or fit with the scope -of the project, or we actually want to implement ourselves. - -Headscale is a multinational community across the globe. Our language is English. -All bug reports needs to be in English. ---> - -## Why - -<!-- Include the reason, why you would need the feature. E.g. what problem - does it solve? Or which workflow is currently frustrating and will be improved by - this? --> - -## Description - -<!-- A clear and precise description of what new or changed feature you want. --> diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml new file mode 100644 index 00000000..b95cd5e6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -0,0 +1,36 @@ +name: 🚀 Feature Request +description: Suggest an idea for Headscale +title: "[Feature] <title>" +labels: [enhancement] +body: + - type: textarea + attributes: + label: Use case + description: Please describe the use case for this feature. + placeholder: | + <!-- Include the reason, why you would need the feature. E.g. what problem + does it solve? Or which workflow is currently frustrating and will be improved by + this? --> + validations: + required: true + - type: textarea + attributes: + label: Description + description: A clear and precise description of what new or changed feature you want. + validations: + required: true + - type: checkboxes + attributes: + label: Contribution + description: Are you willing to contribute to the implementation of this feature? + options: + - label: I can write the design doc for this feature + required: true + - label: I can contribute this feature + required: true + - type: textarea + attributes: + label: How can it be implemented? + description: Free text for your ideas on how this feature could be implemented. + validations: + required: false From c62d5570f20bb7413992e89b829baf7528bac6a5 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby <kristoffer@dalby.cc> Date: Wed, 24 Apr 2024 07:44:07 +0200 Subject: [PATCH 045/145] remove multistep build, build go last, allowing cached build layers (#1903) --- Dockerfile.debug | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/Dockerfile.debug b/Dockerfile.debug index 659ae4cc..4e63dca8 100644 --- a/Dockerfile.debug +++ b/Dockerfile.debug @@ -2,31 +2,24 @@ # and are in no way endorsed by Headscale's maintainers as an # official nor supported release or distribution. -FROM docker.io/golang:1.22-bookworm AS build +FROM docker.io/golang:1.22-bookworm ARG VERSION=dev ENV GOPATH /go WORKDIR /go/src/headscale -COPY go.mod go.sum /go/src/headscale/ -RUN go mod download - -COPY . . - -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 -FROM docker.io/golang:1.22-bookworm - -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 RUN mkdir -p /var/run/headscale +COPY go.mod go.sum /go/src/headscale/ +RUN go mod download + +COPY . . + +RUN CGO_ENABLED=0 GOOS=linux go install -ldflags="-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$VERSION" -a ./cmd/headscale && test -e /go/bin/headscale + # Need to reset the entrypoint or everything will run as a busybox script ENTRYPOINT [] EXPOSE 8080/tcp From fef8261339899fe526777a7aa42df57ca02021c5 Mon Sep 17 00:00:00 2001 From: Juan Font <juanfontalonso@gmail.com> Date: Wed, 24 Apr 2024 07:44:35 +0200 Subject: [PATCH 046/145] Do not access node ID when node is not found (#1912) --- hscontrol/noise.go | 1 - 1 file changed, 1 deletion(-) diff --git a/hscontrol/noise.go b/hscontrol/noise.go index 9ddf2c85..7fcbc252 100644 --- a/hscontrol/noise.go +++ b/hscontrol/noise.go @@ -226,7 +226,6 @@ func (ns *noiseServer) NoisePollNetMapHandler( if err != nil { log.Error(). Str("handler", "NoisePollNetMap"). - Uint64("node.id", node.ID.Uint64()). Msgf("Failed to fetch node from the database with node key: %s", mapRequest.NodeKey.String()) http.Error(writer, "Internal error", http.StatusInternalServerError) From cb0b495ea9b4a6728db84b6da7df4401d31190ae Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby <kristoffer@dalby.cc> Date: Sat, 27 Apr 2024 10:47:39 +0200 Subject: [PATCH 047/145] batch updates in notifier (#1905) --- hscontrol/app.go | 2 +- hscontrol/notifier/metrics.go | 7 +- hscontrol/notifier/notifier.go | 206 +++++++++++++++++++++-- hscontrol/notifier/notifier_test.go | 249 ++++++++++++++++++++++++++++ hscontrol/poll.go | 192 ++++++--------------- hscontrol/types/common.go | 13 +- integration/hsic/hsic.go | 28 ++++ integration/route_test.go | 2 +- 8 files changed, 541 insertions(+), 158 deletions(-) create mode 100644 hscontrol/notifier/notifier_test.go diff --git a/hscontrol/app.go b/hscontrol/app.go index 9eb6b4cc..ce2fd1d8 100644 --- a/hscontrol/app.go +++ b/hscontrol/app.go @@ -137,7 +137,7 @@ func NewHeadscale(cfg *types.Config) (*Headscale, error) { noisePrivateKey: noisePrivateKey, registrationCache: registrationCache, pollNetMapStreamWG: sync.WaitGroup{}, - nodeNotifier: notifier.NewNotifier(), + nodeNotifier: notifier.NewNotifier(cfg), mapSessions: make(map[types.NodeID]*mapSession), } diff --git a/hscontrol/notifier/metrics.go b/hscontrol/notifier/metrics.go index c461d379..1cc4df2b 100644 --- a/hscontrol/notifier/metrics.go +++ b/hscontrol/notifier/metrics.go @@ -18,7 +18,12 @@ var ( Namespace: prometheusNamespace, Name: "notifier_update_sent_total", Help: "total count of update sent on nodes channel", - }, []string{"status", "type"}) + }, []string{"status", "type", "trigger"}) + notifierUpdateReceived = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: prometheusNamespace, + Name: "notifier_update_received_total", + Help: "total count of updates received by notifier", + }, []string{"type", "trigger"}) notifierNodeUpdateChans = promauto.NewGauge(prometheus.GaugeOpts{ Namespace: prometheusNamespace, Name: "notifier_open_channels_total", diff --git a/hscontrol/notifier/notifier.go b/hscontrol/notifier/notifier.go index 4ad58723..74b6645e 100644 --- a/hscontrol/notifier/notifier.go +++ b/hscontrol/notifier/notifier.go @@ -3,7 +3,7 @@ package notifier import ( "context" "fmt" - "slices" + "sort" "strings" "sync" "time" @@ -11,19 +11,27 @@ import ( "github.com/juanfont/headscale/hscontrol/types" "github.com/puzpuzpuz/xsync/v3" "github.com/rs/zerolog/log" + "tailscale.com/tailcfg" + "tailscale.com/util/set" ) type Notifier struct { l sync.RWMutex nodes map[types.NodeID]chan<- types.StateUpdate connected *xsync.MapOf[types.NodeID, bool] + b *batcher } -func NewNotifier() *Notifier { - return &Notifier{ +func NewNotifier(cfg *types.Config) *Notifier { + n := &Notifier{ nodes: make(map[types.NodeID]chan<- types.StateUpdate), connected: xsync.NewMapOf[types.NodeID, bool](), } + b := newBatcher(cfg.Tuning.BatchChangeDelay, n) + n.b = b + // TODO(kradalby): clean this up + go b.doWork() + return n } func (n *Notifier) AddNode(nodeID types.NodeID, c chan<- types.StateUpdate) { @@ -108,13 +116,8 @@ func (n *Notifier) NotifyWithIgnore( update types.StateUpdate, ignoreNodeIDs ...types.NodeID, ) { - for nodeID := range n.nodes { - if slices.Contains(ignoreNodeIDs, nodeID) { - continue - } - - n.NotifyByNodeID(ctx, update, nodeID) - } + notifierUpdateReceived.WithLabelValues(update.Type.String(), types.NotifyOriginKey.Value(ctx)).Inc() + n.b.addOrPassthrough(update) } func (n *Notifier) NotifyByNodeID( @@ -139,10 +142,10 @@ func (n *Notifier) NotifyByNodeID( log.Error(). Err(ctx.Err()). Uint64("node.id", nodeID.Uint64()). - Any("origin", ctx.Value("origin")). - Any("origin-hostname", ctx.Value("hostname")). + Any("origin", types.NotifyOriginKey.Value(ctx)). + Any("origin-hostname", types.NotifyHostnameKey.Value(ctx)). Msgf("update not sent, context cancelled") - notifierUpdateSent.WithLabelValues("cancelled", update.Type.String()).Inc() + notifierUpdateSent.WithLabelValues("cancelled", update.Type.String(), types.NotifyOriginKey.Value(ctx)).Inc() return case c <- update: @@ -151,11 +154,23 @@ func (n *Notifier) NotifyByNodeID( Any("origin", ctx.Value("origin")). Any("origin-hostname", ctx.Value("hostname")). Msgf("update successfully sent on chan") - notifierUpdateSent.WithLabelValues("ok", update.Type.String()).Inc() + notifierUpdateSent.WithLabelValues("ok", update.Type.String(), types.NotifyOriginKey.Value(ctx)).Inc() } } } +func (n *Notifier) sendAll(update types.StateUpdate) { + start := time.Now() + n.l.RLock() + defer n.l.RUnlock() + notifierWaitForLock.WithLabelValues("send-all").Observe(time.Since(start).Seconds()) + + for _, c := range n.nodes { + c <- update + notifierUpdateSent.WithLabelValues("ok", update.Type.String(), "send-all").Inc() + } +} + func (n *Notifier) String() string { n.l.RLock() defer n.l.RUnlock() @@ -177,3 +192,166 @@ func (n *Notifier) String() string { return b.String() } + +type batcher struct { + tick *time.Ticker + + mu sync.Mutex + + cancelCh chan struct{} + + changedNodeIDs set.Slice[types.NodeID] + nodesChanged bool + patches map[types.NodeID]tailcfg.PeerChange + patchesChanged bool + + n *Notifier +} + +func newBatcher(batchTime time.Duration, n *Notifier) *batcher { + return &batcher{ + tick: time.NewTicker(batchTime), + cancelCh: make(chan struct{}), + patches: make(map[types.NodeID]tailcfg.PeerChange), + n: n, + } + +} + +func (b *batcher) close() { + b.cancelCh <- struct{}{} +} + +// addOrPassthrough adds the update to the batcher, if it is not a +// type that is currently batched, it will be sent immediately. +func (b *batcher) addOrPassthrough(update types.StateUpdate) { + b.mu.Lock() + defer b.mu.Unlock() + + switch update.Type { + case types.StatePeerChanged: + b.changedNodeIDs.Add(update.ChangeNodes...) + b.nodesChanged = true + + case types.StatePeerChangedPatch: + for _, newPatch := range update.ChangePatches { + if curr, ok := b.patches[types.NodeID(newPatch.NodeID)]; ok { + overwritePatch(&curr, newPatch) + b.patches[types.NodeID(newPatch.NodeID)] = curr + } else { + b.patches[types.NodeID(newPatch.NodeID)] = *newPatch + } + } + b.patchesChanged = true + + default: + b.n.sendAll(update) + } +} + +// flush sends all the accumulated patches to all +// nodes in the notifier. +func (b *batcher) flush() { + b.mu.Lock() + defer b.mu.Unlock() + + if b.nodesChanged || b.patchesChanged { + var patches []*tailcfg.PeerChange + // If a node is getting a full update from a change + // node update, then the patch can be dropped. + for nodeID, patch := range b.patches { + if b.changedNodeIDs.Contains(nodeID) { + delete(b.patches, nodeID) + } else { + patches = append(patches, &patch) + } + } + + changedNodes := b.changedNodeIDs.Slice().AsSlice() + sort.Slice(changedNodes, func(i, j int) bool { + return changedNodes[i] < changedNodes[j] + }) + + if b.changedNodeIDs.Slice().Len() > 0 { + update := types.StateUpdate{ + Type: types.StatePeerChanged, + ChangeNodes: changedNodes, + } + + b.n.sendAll(update) + } + + if len(patches) > 0 { + patchUpdate := types.StateUpdate{ + Type: types.StatePeerChangedPatch, + ChangePatches: patches, + } + + b.n.sendAll(patchUpdate) + } + + b.changedNodeIDs = set.Slice[types.NodeID]{} + b.nodesChanged = false + b.patches = make(map[types.NodeID]tailcfg.PeerChange, len(b.patches)) + b.patchesChanged = false + } +} + +func (b *batcher) doWork() { + for { + select { + case <-b.cancelCh: + return + case <-b.tick.C: + b.flush() + } + } +} + +// overwritePatch takes the current patch and a newer patch +// and override any field that has changed +func overwritePatch(currPatch, newPatch *tailcfg.PeerChange) { + if newPatch.DERPRegion != 0 { + currPatch.DERPRegion = newPatch.DERPRegion + } + + if newPatch.Cap != 0 { + currPatch.Cap = newPatch.Cap + } + + if newPatch.CapMap != nil { + currPatch.CapMap = newPatch.CapMap + } + + if newPatch.Endpoints != nil { + currPatch.Endpoints = newPatch.Endpoints + } + + if newPatch.Key != nil { + currPatch.Key = newPatch.Key + } + + if newPatch.KeySignature != nil { + currPatch.KeySignature = newPatch.KeySignature + } + + if newPatch.DiscoKey != nil { + currPatch.DiscoKey = newPatch.DiscoKey + } + + if newPatch.Online != nil { + currPatch.Online = newPatch.Online + } + + if newPatch.LastSeen != nil { + currPatch.LastSeen = newPatch.LastSeen + } + + if newPatch.KeyExpiry != nil { + currPatch.KeyExpiry = newPatch.KeyExpiry + } + + if newPatch.Capabilities != nil { + currPatch.Capabilities = newPatch.Capabilities + } +} diff --git a/hscontrol/notifier/notifier_test.go b/hscontrol/notifier/notifier_test.go new file mode 100644 index 00000000..4d61f134 --- /dev/null +++ b/hscontrol/notifier/notifier_test.go @@ -0,0 +1,249 @@ +package notifier + +import ( + "context" + "net/netip" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/juanfont/headscale/hscontrol/types" + "github.com/juanfont/headscale/hscontrol/util" + "tailscale.com/tailcfg" +) + +func TestBatcher(t *testing.T) { + tests := []struct { + name string + updates []types.StateUpdate + want []types.StateUpdate + }{ + { + name: "full-passthrough", + updates: []types.StateUpdate{ + { + Type: types.StateFullUpdate, + }, + }, + want: []types.StateUpdate{ + { + Type: types.StateFullUpdate, + }, + }, + }, + { + name: "derp-passthrough", + updates: []types.StateUpdate{ + { + Type: types.StateDERPUpdated, + }, + }, + want: []types.StateUpdate{ + { + Type: types.StateDERPUpdated, + }, + }, + }, + { + name: "single-node-update", + updates: []types.StateUpdate{ + { + Type: types.StatePeerChanged, + ChangeNodes: []types.NodeID{ + 2, + }, + }, + }, + want: []types.StateUpdate{ + { + Type: types.StatePeerChanged, + ChangeNodes: []types.NodeID{ + 2, + }, + }, + }, + }, + { + name: "merge-node-update", + updates: []types.StateUpdate{ + { + Type: types.StatePeerChanged, + ChangeNodes: []types.NodeID{ + 2, 4, + }, + }, + { + Type: types.StatePeerChanged, + ChangeNodes: []types.NodeID{ + 2, 3, + }, + }, + }, + want: []types.StateUpdate{ + { + Type: types.StatePeerChanged, + ChangeNodes: []types.NodeID{ + 2, 3, 4, + }, + }, + }, + }, + { + name: "single-patch-update", + updates: []types.StateUpdate{ + { + Type: types.StatePeerChangedPatch, + ChangePatches: []*tailcfg.PeerChange{ + { + NodeID: 2, + DERPRegion: 5, + }, + }, + }, + }, + want: []types.StateUpdate{ + { + Type: types.StatePeerChangedPatch, + ChangePatches: []*tailcfg.PeerChange{ + { + NodeID: 2, + DERPRegion: 5, + }, + }, + }, + }, + }, + { + name: "merge-patch-to-same-node-update", + updates: []types.StateUpdate{ + { + Type: types.StatePeerChangedPatch, + ChangePatches: []*tailcfg.PeerChange{ + { + NodeID: 2, + DERPRegion: 5, + }, + }, + }, + { + Type: types.StatePeerChangedPatch, + ChangePatches: []*tailcfg.PeerChange{ + { + NodeID: 2, + DERPRegion: 6, + }, + }, + }, + }, + want: []types.StateUpdate{ + { + Type: types.StatePeerChangedPatch, + ChangePatches: []*tailcfg.PeerChange{ + { + NodeID: 2, + DERPRegion: 6, + }, + }, + }, + }, + }, + { + name: "merge-patch-to-multiple-node-update", + updates: []types.StateUpdate{ + { + Type: types.StatePeerChangedPatch, + ChangePatches: []*tailcfg.PeerChange{ + { + NodeID: 3, + Endpoints: []netip.AddrPort{ + netip.MustParseAddrPort("1.1.1.1:9090"), + }, + }, + }, + }, + { + Type: types.StatePeerChangedPatch, + ChangePatches: []*tailcfg.PeerChange{ + { + NodeID: 3, + Endpoints: []netip.AddrPort{ + netip.MustParseAddrPort("1.1.1.1:9090"), + netip.MustParseAddrPort("2.2.2.2:8080"), + }, + }, + }, + }, + { + Type: types.StatePeerChangedPatch, + ChangePatches: []*tailcfg.PeerChange{ + { + NodeID: 4, + DERPRegion: 6, + }, + }, + }, + { + Type: types.StatePeerChangedPatch, + ChangePatches: []*tailcfg.PeerChange{ + { + NodeID: 4, + Cap: tailcfg.CapabilityVersion(54), + }, + }, + }, + }, + want: []types.StateUpdate{ + { + Type: types.StatePeerChangedPatch, + ChangePatches: []*tailcfg.PeerChange{ + { + NodeID: 3, + Endpoints: []netip.AddrPort{ + netip.MustParseAddrPort("1.1.1.1:9090"), + netip.MustParseAddrPort("2.2.2.2:8080"), + }, + }, + { + NodeID: 4, + DERPRegion: 6, + Cap: tailcfg.CapabilityVersion(54), + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := NewNotifier(&types.Config{ + Tuning: types.Tuning{ + // We will call flush manually for the tests, + // so do not run the worker. + BatchChangeDelay: time.Hour, + }, + }) + + ch := make(chan types.StateUpdate, 30) + defer close(ch) + n.AddNode(1, ch) + defer n.RemoveNode(1) + + for _, u := range tt.updates { + n.NotifyAll(context.Background(), u) + } + + n.b.flush() + + var got []types.StateUpdate + for len(ch) > 0 { + out := <-ch + got = append(got, out) + } + + if diff := cmp.Diff(tt.want, got, util.Comparers...); diff != "" { + t.Errorf("batcher() unexpected result (-want +got):\n%s", diff) + } + }) + } +} diff --git a/hscontrol/poll.go b/hscontrol/poll.go index b903f122..e3137cc6 100644 --- a/hscontrol/poll.go +++ b/hscontrol/poll.go @@ -66,10 +66,16 @@ func (h *Headscale) newMapSession( ) *mapSession { warnf, infof, tracef, errf := logPollFunc(req, node) - // 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. - updateChan := make(chan types.StateUpdate, h.cfg.Tuning.NodeMapSessionBufferedChanSize) + var updateChan chan types.StateUpdate + if req.Stream { + // 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. + updateChan = make(chan types.StateUpdate, h.cfg.Tuning.NodeMapSessionBufferedChanSize) + updateChan <- types.StateUpdate{ + Type: types.StateFullUpdate, + } + } return &mapSession{ h: h, @@ -218,33 +224,26 @@ func (m *mapSession) serve() { ctx, cancel := context.WithCancel(context.WithValue(m.ctx, nodeNameContextKey, m.node.Hostname)) defer cancel() - // TODO(kradalby): Make this available through a tuning envvar - wait := time.Second - - // Add a circuit breaker, if the loop is not interrupted - // inbetween listening for the channels, some updates - // might get stale and stucked in the "changed" map - // defined below. - blockBreaker := time.NewTicker(wait) - - // true means changed, false means removed - var changed map[types.NodeID]bool - var patches []*tailcfg.PeerChange - var derp bool - - // Set full to true to immediatly send a full mapresponse - full := true - prev := time.Now() - lastMessage := "" - // Loop through updates and continuously send them to the // client. for { - // If a full update has been requested or there are patches, then send it immediately - // otherwise wait for the "batching" of changes or patches - if full || patches != nil || (changed != nil && time.Since(prev) > wait) { + // consume channels with update, keep alives or "batch" blocking signals + select { + case <-m.cancelCh: + m.tracef("poll cancelled received") + return + case <-ctx.Done(): + m.tracef("poll context done") + return + + // Consume all updates sent to node + case update := <-m.ch: + m.tracef("received stream update: %s %s", update.Type.String(), update.Message) + mapResponseUpdateReceived.WithLabelValues(update.Type.String()).Inc() + var data []byte var err error + var lastMessage string // Ensure the node object is updated, for example, there // might have been a hostinfo update in a sidechannel @@ -256,62 +255,43 @@ func (m *mapSession) serve() { return } - // If there are patches _and_ fully changed nodes, filter the - // patches and remove all patches that are present for the full - // changes updates. This allows us to send them as part of the - // PeerChange update, but only for nodes that are not fully changed. - // The fully changed nodes will be updated from the database and - // have all the updates needed. - // This means that the patches left are for nodes that has no - // updates that requires a full update. - // Patches are not suppose to be mixed in, but can be. - // - // From tailcfg docs: - // These are applied after Peers* above, but in practice the - // control server should only send these on their own, without - // - // Currently, there is no effort to merge patch updates, they - // are all sent, and the client will apply them in order. - // TODO(kradalby): Merge Patches for the same IDs to send less - // data and give the client less work. - if patches != nil && changed != nil { - var filteredPatches []*tailcfg.PeerChange - - for _, patch := range patches { - if _, ok := changed[types.NodeID(patch.NodeID)]; !ok { - filteredPatches = append(filteredPatches, patch) - } - } - - patches = filteredPatches - } - updateType := "full" - // When deciding what update to send, the following is considered, - // Full is a superset of all updates, when a full update is requested, - // send only that and move on, all other updates will be present in - // a full map response. - // - // If a map of changed nodes exists, prefer sending that as it will - // contain all the updates for the node, including patches, as it - // is fetched freshly from the database when building the response. - // - // If there is full changes registered, but we have patches for individual - // nodes, send them. - // - // Finally, if a DERP map is the only request, send that alone. - if full { + switch update.Type { + case types.StateFullUpdate: m.tracef("Sending Full MapResponse") data, err = m.mapper.FullMapResponse(m.req, m.node, m.h.ACLPolicy, fmt.Sprintf("from mapSession: %p, stream: %t", m, m.isStreaming())) - } else if changed != nil { + case types.StatePeerChanged: + changed := make(map[types.NodeID]bool, len(update.ChangeNodes)) + + for _, nodeID := range update.ChangeNodes { + changed[nodeID] = true + } + + lastMessage = update.Message m.tracef(fmt.Sprintf("Sending Changed MapResponse: %v", lastMessage)) - data, err = m.mapper.PeerChangedResponse(m.req, m.node, changed, patches, m.h.ACLPolicy, lastMessage) + data, err = m.mapper.PeerChangedResponse(m.req, m.node, changed, update.ChangePatches, m.h.ACLPolicy, lastMessage) updateType = "change" - } else if patches != nil { + + case types.StatePeerChangedPatch: m.tracef(fmt.Sprintf("Sending Changed Patch MapResponse: %v", lastMessage)) - data, err = m.mapper.PeerChangedPatchResponse(m.req, m.node, patches, m.h.ACLPolicy) + data, err = m.mapper.PeerChangedPatchResponse(m.req, m.node, update.ChangePatches, m.h.ACLPolicy) updateType = "patch" - } else if derp { + case types.StatePeerRemoved: + changed := make(map[types.NodeID]bool, len(update.Removed)) + + for _, nodeID := range update.Removed { + changed[nodeID] = false + } + m.tracef(fmt.Sprintf("Sending Changed MapResponse: %v", lastMessage)) + data, err = m.mapper.PeerChangedResponse(m.req, m.node, changed, update.ChangePatches, m.h.ACLPolicy, lastMessage) + updateType = "remove" + case types.StateSelfUpdate: + lastMessage = update.Message + m.tracef(fmt.Sprintf("Sending Changed MapResponse: %v", lastMessage)) + // create the map so an empty (self) update is sent + data, err = m.mapper.PeerChangedResponse(m.req, m.node, make(map[types.NodeID]bool), update.ChangePatches, m.h.ACLPolicy, lastMessage) + updateType = "remove" + case types.StateDERPUpdated: m.tracef("Sending DERPUpdate MapResponse") data, err = m.mapper.DERPMapResponse(m.req, m.node, m.h.DERPMap) updateType = "derp" @@ -348,68 +328,6 @@ func (m *mapSession) serve() { m.tracef("update sent") } - // reset - changed = nil - patches = nil - lastMessage = "" - full = false - derp = false - prev = time.Now() - } - - // consume channels with update, keep alives or "batch" blocking signals - select { - case <-m.cancelCh: - m.tracef("poll cancelled received") - return - case <-ctx.Done(): - m.tracef("poll context done") - return - - // Avoid infinite block that would potentially leave - // some updates in the changed map. - case <-blockBreaker.C: - continue - - // Consume all updates sent to node - case update := <-m.ch: - m.tracef("received stream update: %s %s", update.Type.String(), update.Message) - mapResponseUpdateReceived.WithLabelValues(update.Type.String()).Inc() - - switch update.Type { - case types.StateFullUpdate: - full = true - case types.StatePeerChanged: - if changed == nil { - changed = make(map[types.NodeID]bool) - } - - for _, nodeID := range update.ChangeNodes { - changed[nodeID] = true - } - - lastMessage = update.Message - case types.StatePeerChangedPatch: - patches = append(patches, update.ChangePatches...) - case types.StatePeerRemoved: - if changed == nil { - changed = make(map[types.NodeID]bool) - } - - for _, nodeID := range update.Removed { - changed[nodeID] = false - } - case types.StateSelfUpdate: - // create the map so an empty (self) update is sent - if changed == nil { - changed = make(map[types.NodeID]bool) - } - - lastMessage = update.Message - case types.StateDERPUpdated: - derp = true - } - case <-m.keepAliveTicker.C: data, err := m.mapper.KeepAliveResponse(m.req, m.node) if err != nil { diff --git a/hscontrol/types/common.go b/hscontrol/types/common.go index 6d63f301..35f5e5e4 100644 --- a/hscontrol/types/common.go +++ b/hscontrol/types/common.go @@ -10,6 +10,7 @@ import ( "time" "tailscale.com/tailcfg" + "tailscale.com/util/ctxkey" ) const ( @@ -183,10 +184,14 @@ func StateUpdateExpire(nodeID NodeID, expiry time.Time) StateUpdate { } } +var ( + NotifyOriginKey = ctxkey.New("notify.origin", "") + NotifyHostnameKey = ctxkey.New("notify.hostname", "") +) + func NotifyCtx(ctx context.Context, origin, hostname string) context.Context { - ctx2, _ := context.WithTimeout( - context.WithValue(context.WithValue(ctx, "hostname", hostname), "origin", origin), - 3*time.Second, - ) + ctx2, _ := context.WithTimeout(ctx, 3*time.Second) + ctx2 = NotifyOriginKey.WithValue(ctx2, origin) + ctx2 = NotifyHostnameKey.WithValue(ctx2, hostname) return ctx2 } diff --git a/integration/hsic/hsic.go b/integration/hsic/hsic.go index 0483213b..a118b6fc 100644 --- a/integration/hsic/hsic.go +++ b/integration/hsic/hsic.go @@ -11,6 +11,7 @@ import ( "encoding/pem" "errors" "fmt" + "io" "log" "math/big" "net" @@ -396,6 +397,14 @@ func (t *HeadscaleInContainer) Shutdown() error { ) } + err = t.SaveMetrics("/tmp/control/metrics.txt") + if err != nil { + log.Printf( + "Failed to metrics from control: %s", + err, + ) + } + // Send a interrupt signal to the "headscale" process inside the container // allowing it to shut down gracefully and flush the profile to disk. // The container will live for a bit longer due to the sleep at the end. @@ -448,6 +457,25 @@ func (t *HeadscaleInContainer) SaveLog(path string) error { return dockertestutil.SaveLog(t.pool, t.container, path) } +func (t *HeadscaleInContainer) SaveMetrics(savePath string) error { + resp, err := http.Get(fmt.Sprintf("http://%s:9090/metrics", t.hostname)) + if err != nil { + return fmt.Errorf("getting metrics: %w", err) + } + defer resp.Body.Close() + out, err := os.Create(savePath) + if err != nil { + return fmt.Errorf("creating file for metrics: %w", err) + } + defer out.Close() + _, err = io.Copy(out, resp.Body) + if err != nil { + return fmt.Errorf("copy response to file: %w", err) + } + + return nil +} + func (t *HeadscaleInContainer) SaveProfile(savePath string) error { tarFile, err := t.FetchPath("/tmp/profile") if err != nil { diff --git a/integration/route_test.go b/integration/route_test.go index 15ea22b1..48b6c07f 100644 --- a/integration/route_test.go +++ b/integration/route_test.go @@ -252,7 +252,7 @@ func TestHASubnetRouterFailover(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErrf(t, "failed to create scenario: %s", err) - // defer scenario.Shutdown() + defer scenario.Shutdown() spec := map[string]int{ user: 3, From 10e37ec28decefe62e74da75567ba18fdc5c6c7b Mon Sep 17 00:00:00 2001 From: Juan Font <juanfontalonso@gmail.com> Date: Mon, 22 Apr 2024 10:00:20 +0000 Subject: [PATCH 048/145] Add contributing document --- .github/pull_request_template.md | 2 +- CONTRIBUTING.md | 34 ++++++++++++++++++++++++++++++++ README.md | 10 +--------- 3 files changed, 36 insertions(+), 10 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d4e4f4f9..9d8e731d 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -12,7 +12,7 @@ If you find mistakes in the documentation, please submit a fix to the documentat <!-- Please tick if the following things apply. You… --> -- [ ] read the [CONTRIBUTING guidelines](README.md#contributing) +- [ ] have read the [CONTRIBUTING.md](./CONTRIBUTING.md) file - [ ] raised a GitHub issue or discussed it on the projects chat beforehand - [ ] added unit tests - [ ] added integration tests diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..c37e8fca --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,34 @@ +# Contributing + +Headscale is "Open Source, acknowledged contribution", this means that any contribution will have to be discussed with the Maintainers before being submitted. +This model has been chosen to reduce the risk of burnout by limiting the maintenance overhead of reviewing and validating third-party code. + +## Why do we have this model? + +Headscale has a small maintainer team that tries to balance working on the project, fixing bugs and reviewing contributions. + +When we work on issues ourselves, we develop first hand knowledge of the code and it makes it possible for us to maintain and own the code as the project develops. + +When code is contributed to the project, it is typically a positive thing. People enjoy and engage with our project, but it also comes with some challenges; we have to understand the code, we have to understand the feature, we might have to become familiar with external libraries or services that this new feature integrates with and it needs to be reviewed from a security perspective. And that is only when it comes to reviewing it. After the code has been merged, the feature has to be maintained, meaning that changes to external parts need to be updated, and kept working. + +The review and the day-1 maintenance adds a significant burden on them maintainers. Often we hope that the contributor of the feature will help out, but we found that most of the time, they disappear when they have their new feature added. + +This means that when someone contributes, we are mostly happy about it, but we do have to run it through a series of checks to establish if we actually can maintain this feature. + +## What do we require? + +A general description is provided here and an explicit list is provided in our pull request template. + +All new features have to start with a design document that has to be discussed in the issue tracker (not discord) with a use case for the feature, how it can be implemented, who will implement it and what the plan for maintaining it is. + +All features have to be end to end tested (integration tests) and have good unit test coverage to ensure that they work as expected, and work as expected over time. If the change cannot be tested, a strong case for why this is not possible needs to be presented. + +The contributor must help maintain the feature over time, if a feature is found to be left unmaintained, we will have to remove it. + +## Bug fixes + +Headscale is open to code contributions for bug fixes without discussion. + +## Documentation + +If you find mistakes in the documentation, please submit a fix to the documentation. diff --git a/README.md b/README.md index 61619385..30874296 100644 --- a/README.md +++ b/README.md @@ -95,15 +95,7 @@ The maintainers work together on setting the direction for the project. The unde ## Contributing -Headscale is "Open Source, acknowledged contribution", this means that any -contribution will have to be discussed with the Maintainers before being submitted. - -This model has been chosen to reduce the risk of burnout by limiting the -maintenance overhead of reviewing and validating third-party code. - -Headscale is open to code contributions for bug fixes without discussion. - -If you find mistakes in the documentation, please submit a fix to the documentation. +Please read the [CONTRIBUTING.md](./CONTRIBUTING.md) file. ### Requirements From d740ee489ec78aa86aa5415422376adc6d23aad0 Mon Sep 17 00:00:00 2001 From: Juan Font <juanfontalonso@gmail.com> Date: Tue, 23 Apr 2024 10:50:05 +0200 Subject: [PATCH 049/145] Update CONTRIBUTING.md Co-authored-by: ohdearaugustin <ohdearaugustin@users.noreply.github.com> --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c37e8fca..6799e333 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,7 @@ When we work on issues ourselves, we develop first hand knowledge of the code an When code is contributed to the project, it is typically a positive thing. People enjoy and engage with our project, but it also comes with some challenges; we have to understand the code, we have to understand the feature, we might have to become familiar with external libraries or services that this new feature integrates with and it needs to be reviewed from a security perspective. And that is only when it comes to reviewing it. After the code has been merged, the feature has to be maintained, meaning that changes to external parts need to be updated, and kept working. -The review and the day-1 maintenance adds a significant burden on them maintainers. Often we hope that the contributor of the feature will help out, but we found that most of the time, they disappear when they have their new feature added. +The review and the day-1 maintenance adds a significant burden on the maintainers. Often we hope that the contributor will help out, but we found that most of the time, they disappear after their new feature was added. This means that when someone contributes, we are mostly happy about it, but we do have to run it through a series of checks to establish if we actually can maintain this feature. From 50a7d15769d5dea1e9513f64cd7ea48594f47dae Mon Sep 17 00:00:00 2001 From: Juan Font <juanfontalonso@gmail.com> Date: Tue, 23 Apr 2024 10:50:10 +0200 Subject: [PATCH 050/145] Update CONTRIBUTING.md Co-authored-by: ohdearaugustin <ohdearaugustin@users.noreply.github.com> --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6799e333..8296dd9f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,7 +19,7 @@ This means that when someone contributes, we are mostly happy about it, but we d A general description is provided here and an explicit list is provided in our pull request template. -All new features have to start with a design document that has to be discussed in the issue tracker (not discord) with a use case for the feature, how it can be implemented, who will implement it and what the plan for maintaining it is. +All new features have to start out with a design document, which should be discussed on the issue tracker (not discord). It should include a use case for the feature, how it can be implemented, who will implement it and a plan for maintaining it. All features have to be end to end tested (integration tests) and have good unit test coverage to ensure that they work as expected, and work as expected over time. If the change cannot be tested, a strong case for why this is not possible needs to be presented. From 39277844dd4c2b8a741734d6d453e34281e114a6 Mon Sep 17 00:00:00 2001 From: Juan Font <juanfontalonso@gmail.com> Date: Tue, 23 Apr 2024 22:04:56 +0200 Subject: [PATCH 051/145] Apply suggestions from code review Co-authored-by: ohdearaugustin <ohdearaugustin@users.noreply.github.com> --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8296dd9f..c1f8d9ca 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing -Headscale is "Open Source, acknowledged contribution", this means that any contribution will have to be discussed with the Maintainers before being submitted. +Headscale is "Open Source, acknowledged contribution", this means that any contribution will have to be discussed with the maintainers before being added to the project. This model has been chosen to reduce the risk of burnout by limiting the maintenance overhead of reviewing and validating third-party code. ## Why do we have this model? @@ -9,7 +9,7 @@ Headscale has a small maintainer team that tries to balance working on the proje When we work on issues ourselves, we develop first hand knowledge of the code and it makes it possible for us to maintain and own the code as the project develops. -When code is contributed to the project, it is typically a positive thing. People enjoy and engage with our project, but it also comes with some challenges; we have to understand the code, we have to understand the feature, we might have to become familiar with external libraries or services that this new feature integrates with and it needs to be reviewed from a security perspective. And that is only when it comes to reviewing it. After the code has been merged, the feature has to be maintained, meaning that changes to external parts need to be updated, and kept working. +Code contributions are seen as a positive thing. People enjoy and engage with our project, but it also comes with some challenges; we have to understand the code, we have to understand the feature, we might have to become familiar with external libraries or services and we think about security implications. All those steps are required during reviewing process. After the code has been merged, the feature has to be maintained. Any changes reliant on external services must be updated and expanded accordingly. . The review and the day-1 maintenance adds a significant burden on the maintainers. Often we hope that the contributor will help out, but we found that most of the time, they disappear after their new feature was added. @@ -21,7 +21,7 @@ A general description is provided here and an explicit list is provided in our p All new features have to start out with a design document, which should be discussed on the issue tracker (not discord). It should include a use case for the feature, how it can be implemented, who will implement it and a plan for maintaining it. -All features have to be end to end tested (integration tests) and have good unit test coverage to ensure that they work as expected, and work as expected over time. If the change cannot be tested, a strong case for why this is not possible needs to be presented. +All features have to be end-to-end tested (integration tests) and have good unit test coverage to ensure that they work as expected. This will also ensure that the feature continues to work as expected over time. If a change cannot be tested, a strong case for why this is not possible needs to be presented. The contributor must help maintain the feature over time, if a feature is found to be left unmaintained, we will have to remove it. From ff427ccb7868bebc34f8f5ee24280e79ad599ce5 Mon Sep 17 00:00:00 2001 From: Juan Font <juanfontalonso@gmail.com> Date: Fri, 26 Apr 2024 21:06:16 +0200 Subject: [PATCH 052/145] Apply suggestions from code review Co-authored-by: ohdearaugustin <ohdearaugustin@users.noreply.github.com> --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c1f8d9ca..18d7dfb8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,9 +9,9 @@ Headscale has a small maintainer team that tries to balance working on the proje When we work on issues ourselves, we develop first hand knowledge of the code and it makes it possible for us to maintain and own the code as the project develops. -Code contributions are seen as a positive thing. People enjoy and engage with our project, but it also comes with some challenges; we have to understand the code, we have to understand the feature, we might have to become familiar with external libraries or services and we think about security implications. All those steps are required during reviewing process. After the code has been merged, the feature has to be maintained. Any changes reliant on external services must be updated and expanded accordingly. . +Code contributions are seen as a positive thing. People enjoy and engage with our project, but it also comes with some challenges; we have to understand the code, we have to understand the feature, we might have to become familiar with external libraries or services and we think about security implications. All those steps are required during the reviewing process. After the code has been merged, the feature has to be maintained. Any changes reliant on external services must be updated and expanded accordingly. -The review and the day-1 maintenance adds a significant burden on the maintainers. Often we hope that the contributor will help out, but we found that most of the time, they disappear after their new feature was added. +The review and day-1 maintenance adds a significant burden on the maintainers. Often we hope that the contributor will help out, but we found that most of the time, they disappear after their new feature was added. This means that when someone contributes, we are mostly happy about it, but we do have to run it through a series of checks to establish if we actually can maintain this feature. @@ -23,7 +23,7 @@ All new features have to start out with a design document, which should be discu All features have to be end-to-end tested (integration tests) and have good unit test coverage to ensure that they work as expected. This will also ensure that the feature continues to work as expected over time. If a change cannot be tested, a strong case for why this is not possible needs to be presented. -The contributor must help maintain the feature over time, if a feature is found to be left unmaintained, we will have to remove it. +The contributor should help to maintain the feature over time. In case the feature is not maintained probably, the maintainers reserve themselves the right to remove features they redeem as unmaintainable. This should help to improve the quality of the software and keep it in a maintainable state. ## Bug fixes From 87e2ae4d52632c5276e903ebdc1db92de1b82dd6 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby <kristoffer@dalby.cc> Date: Tue, 30 Apr 2024 07:23:16 +0200 Subject: [PATCH 053/145] add autogroup:internet, fix reduce filter rules (#1917) --- CHANGELOG.md | 1 + hscontrol/policy/acls.go | 61 +++- hscontrol/policy/acls_test.go | 564 +++++++++++++++++++++++++++++++++- 3 files changed, 619 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8c7590f..7cd82830 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ after improving the test harness as part of adopting [#1460](https://github.com/ - Add support for deleting api keys [#1702](https://github.com/juanfont/headscale/pull/1702) - Add command to backfill IP addresses for nodes missing IPs from configured prefixes. [#1869](https://github.com/juanfont/headscale/pull/1869) - Log available update as warning [#1877](https://github.com/juanfont/headscale/pull/1877) +- Add `autogroup:internet` to Policy [#1917](https://github.com/juanfont/headscale/pull/1917) ## 0.22.3 (2023-05-12) diff --git a/hscontrol/policy/acls.go b/hscontrol/policy/acls.go index 0f6158c6..1196995d 100644 --- a/hscontrol/policy/acls.go +++ b/hscontrol/policy/acls.go @@ -36,6 +36,38 @@ const ( expectedTokenItems = 2 ) +var theInternetSet *netipx.IPSet + +// theInternet returns the IPSet for the Internet. +// https://www.youtube.com/watch?v=iDbyYGrswtg +func theInternet() *netipx.IPSet { + if theInternetSet != nil { + return theInternetSet + } + + var internetBuilder netipx.IPSetBuilder + internetBuilder.AddPrefix(netip.MustParsePrefix("2000::/3")) + internetBuilder.AddPrefix(netip.MustParsePrefix("0.0.0.0/0")) + + // Delete Private network addresses + // https://datatracker.ietf.org/doc/html/rfc1918 + internetBuilder.RemovePrefix(netip.MustParsePrefix("fc00::/7")) + internetBuilder.RemovePrefix(netip.MustParsePrefix("10.0.0.0/8")) + internetBuilder.RemovePrefix(netip.MustParsePrefix("172.16.0.0/12")) + internetBuilder.RemovePrefix(netip.MustParsePrefix("192.168.0.0/16")) + + // Delete Tailscale networks + internetBuilder.RemovePrefix(netip.MustParsePrefix("fd7a:115c:a1e0::/48")) + internetBuilder.RemovePrefix(netip.MustParsePrefix("100.64.0.0/10")) + + // Delete "cant find DHCP networks" + internetBuilder.RemovePrefix(netip.MustParsePrefix("fe80::/10")) // link-loca + internetBuilder.RemovePrefix(netip.MustParsePrefix("169.254.0.0/16")) + + theInternetSet, _ := internetBuilder.IPSet() + return theInternetSet +} + // For some reason golang.org/x/net/internal/iana is an internal package. const ( protocolICMP = 1 // Internet Control Message @@ -221,28 +253,28 @@ func ReduceFilterRules(node *types.Node, rules []tailcfg.FilterRule) []tailcfg.F // record if the rule is actually relevant for the given node. dests := []tailcfg.NetPortRange{} + DEST_LOOP: for _, dest := range rule.DstPorts { expanded, err := util.ParseIPSet(dest.IP, nil) // Fail closed, if we cant parse it, then we should not allow // access. if err != nil { - continue + continue DEST_LOOP } if node.InIPSet(expanded) { dests = append(dests, dest) + continue DEST_LOOP } // If the node exposes routes, ensure they are note removed // when the filters are reduced. if node.Hostinfo != nil { - // TODO(kradalby): Evaluate if we should only keep - // the routes if the route is enabled. This will - // require database access in this part of the code. if len(node.Hostinfo.RoutableIPs) > 0 { for _, routableIP := range node.Hostinfo.RoutableIPs { - if expanded.ContainsPrefix(routableIP) { + if expanded.OverlapsPrefix(routableIP) { dests = append(dests, dest) + continue DEST_LOOP } } } @@ -517,6 +549,7 @@ func (pol *ACLPolicy) expandSource( // - a host // - an ip // - a cidr +// - an autogroup // and transform these in IPAddresses. func (pol *ACLPolicy) ExpandAlias( nodes types.Nodes, @@ -542,6 +575,10 @@ func (pol *ACLPolicy) ExpandAlias( return pol.expandIPsFromTag(alias, nodes) } + if isAutoGroup(alias) { + return expandAutoGroup(alias) + } + // if alias is a user if ips, err := pol.expandIPsFromUser(alias, nodes); ips != nil { return ips, err @@ -862,6 +899,16 @@ func (pol *ACLPolicy) expandIPsFromIPPrefix( return build.IPSet() } +func expandAutoGroup(alias string) (*netipx.IPSet, error) { + switch { + case strings.HasPrefix(alias, "autogroup:internet"): + return theInternet(), nil + + default: + return nil, fmt.Errorf("unknown autogroup %q", alias) + } +} + func isWildcard(str string) bool { return str == "*" } @@ -874,6 +921,10 @@ func isTag(str string) bool { return strings.HasPrefix(str, "tag:") } +func isAutoGroup(str string) bool { + return strings.HasPrefix(str, "autogroup:") +} + // TagsOfNode will return the tags of the current node. // Invalid tags are tags added by a user on a node, and that user doesn't have authority to add this tag. // Valid tags are tags added by a user that is allowed in the ACL policy to add this tag. diff --git a/hscontrol/policy/acls_test.go b/hscontrol/policy/acls_test.go index 417ed1d1..dd4d95bb 100644 --- a/hscontrol/policy/acls_test.go +++ b/hscontrol/policy/acls_test.go @@ -1765,6 +1765,108 @@ func TestACLPolicy_generateFilterRules(t *testing.T) { } } +// tsExitNodeDest is the list of destination IP ranges that are allowed when +// you dump the filter list from a Tailscale node connected to Tailscale SaaS. +var tsExitNodeDest = []tailcfg.NetPortRange{ + { + IP: "0.0.0.0-9.255.255.255", + Ports: tailcfg.PortRangeAny, + }, + { + IP: "11.0.0.0-100.63.255.255", + Ports: tailcfg.PortRangeAny, + }, + { + IP: "100.128.0.0-169.253.255.255", + Ports: tailcfg.PortRangeAny, + }, + { + IP: "169.255.0.0-172.15.255.255", + Ports: tailcfg.PortRangeAny, + }, + { + IP: "172.32.0.0-192.167.255.255", + Ports: tailcfg.PortRangeAny, + }, + { + IP: "192.169.0.0-255.255.255.255", + Ports: tailcfg.PortRangeAny, + }, + { + IP: "2000::-3fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", + Ports: tailcfg.PortRangeAny, + }, +} + +// hsExitNodeDest is the list of destination IP ranges that are allowed when +// we use headscale "autogroup:internet" +var hsExitNodeDest = []tailcfg.NetPortRange{ + {IP: "0.0.0.0/5", Ports: tailcfg.PortRangeAny}, + {IP: "8.0.0.0/7", Ports: tailcfg.PortRangeAny}, + {IP: "11.0.0.0/8", Ports: tailcfg.PortRangeAny}, + {IP: "12.0.0.0/6", Ports: tailcfg.PortRangeAny}, + {IP: "16.0.0.0/4", Ports: tailcfg.PortRangeAny}, + {IP: "32.0.0.0/3", Ports: tailcfg.PortRangeAny}, + {IP: "64.0.0.0/3", Ports: tailcfg.PortRangeAny}, + {IP: "96.0.0.0/6", Ports: tailcfg.PortRangeAny}, + {IP: "100.0.0.0/10", Ports: tailcfg.PortRangeAny}, + {IP: "100.128.0.0/9", Ports: tailcfg.PortRangeAny}, + {IP: "101.0.0.0/8", Ports: tailcfg.PortRangeAny}, + {IP: "102.0.0.0/7", Ports: tailcfg.PortRangeAny}, + {IP: "104.0.0.0/5", Ports: tailcfg.PortRangeAny}, + {IP: "112.0.0.0/4", Ports: tailcfg.PortRangeAny}, + {IP: "128.0.0.0/3", Ports: tailcfg.PortRangeAny}, + {IP: "160.0.0.0/5", Ports: tailcfg.PortRangeAny}, + {IP: "168.0.0.0/8", Ports: tailcfg.PortRangeAny}, + {IP: "169.0.0.0/9", Ports: tailcfg.PortRangeAny}, + {IP: "169.128.0.0/10", Ports: tailcfg.PortRangeAny}, + {IP: "169.192.0.0/11", Ports: tailcfg.PortRangeAny}, + {IP: "169.224.0.0/12", Ports: tailcfg.PortRangeAny}, + {IP: "169.240.0.0/13", Ports: tailcfg.PortRangeAny}, + {IP: "169.248.0.0/14", Ports: tailcfg.PortRangeAny}, + {IP: "169.252.0.0/15", Ports: tailcfg.PortRangeAny}, + {IP: "169.255.0.0/16", Ports: tailcfg.PortRangeAny}, + {IP: "170.0.0.0/7", Ports: tailcfg.PortRangeAny}, + {IP: "172.0.0.0/12", Ports: tailcfg.PortRangeAny}, + {IP: "172.32.0.0/11", Ports: tailcfg.PortRangeAny}, + {IP: "172.64.0.0/10", Ports: tailcfg.PortRangeAny}, + {IP: "172.128.0.0/9", Ports: tailcfg.PortRangeAny}, + {IP: "173.0.0.0/8", Ports: tailcfg.PortRangeAny}, + {IP: "174.0.0.0/7", Ports: tailcfg.PortRangeAny}, + {IP: "176.0.0.0/4", Ports: tailcfg.PortRangeAny}, + {IP: "192.0.0.0/9", Ports: tailcfg.PortRangeAny}, + {IP: "192.128.0.0/11", Ports: tailcfg.PortRangeAny}, + {IP: "192.160.0.0/13", Ports: tailcfg.PortRangeAny}, + {IP: "192.169.0.0/16", Ports: tailcfg.PortRangeAny}, + {IP: "192.170.0.0/15", Ports: tailcfg.PortRangeAny}, + {IP: "192.172.0.0/14", Ports: tailcfg.PortRangeAny}, + {IP: "192.176.0.0/12", Ports: tailcfg.PortRangeAny}, + {IP: "192.192.0.0/10", Ports: tailcfg.PortRangeAny}, + {IP: "193.0.0.0/8", Ports: tailcfg.PortRangeAny}, + {IP: "194.0.0.0/7", Ports: tailcfg.PortRangeAny}, + {IP: "196.0.0.0/6", Ports: tailcfg.PortRangeAny}, + {IP: "200.0.0.0/5", Ports: tailcfg.PortRangeAny}, + {IP: "208.0.0.0/4", Ports: tailcfg.PortRangeAny}, + {IP: "224.0.0.0/3", Ports: tailcfg.PortRangeAny}, + {IP: "2000::/3", Ports: tailcfg.PortRangeAny}, +} + +func TestTheInternet(t *testing.T) { + internetSet := theInternet() + + internetPrefs := internetSet.Prefixes() + + for i, _ := range internetPrefs { + if internetPrefs[i].String() != hsExitNodeDest[i].IP { + t.Errorf("prefix from internet set %q != hsExit list %q", internetPrefs[i].String(), hsExitNodeDest[i].IP) + } + } + + if len(internetPrefs) != len(hsExitNodeDest) { + t.Fatalf("expected same length of prefixes, internet: %d, hsExit: %d", len(internetPrefs), len(hsExitNodeDest)) + } +} + func TestReduceFilterRules(t *testing.T) { tests := []struct { name string @@ -1869,15 +1971,473 @@ func TestReduceFilterRules(t *testing.T) { }, }, }, + { + name: "1786-reducing-breaks-exit-nodes-the-client", + pol: ACLPolicy{ + Hosts: Hosts{ + // Exit node + "internal": netip.MustParsePrefix("100.64.0.100/32"), + }, + Groups: Groups{ + "group:team": {"user3", "user2", "user1"}, + }, + ACLs: []ACL{ + { + Action: "accept", + Sources: []string{"group:team"}, + Destinations: []string{ + "internal:*", + }, + }, + { + Action: "accept", + Sources: []string{"group:team"}, + Destinations: []string{ + "autogroup:internet:*", + }, + }, + }, + }, + node: &types.Node{ + IPv4: iap("100.64.0.1"), + IPv6: iap("fd7a:115c:a1e0::1"), + User: types.User{Name: "user1"}, + }, + peers: types.Nodes{ + &types.Node{ + IPv4: iap("100.64.0.2"), + IPv6: iap("fd7a:115c:a1e0::2"), + User: types.User{Name: "user2"}, + }, + // "internal" exit node + &types.Node{ + IPv4: iap("100.64.0.100"), + IPv6: iap("fd7a:115c:a1e0::100"), + User: types.User{Name: "user100"}, + Hostinfo: &tailcfg.Hostinfo{ + RoutableIPs: []netip.Prefix{types.ExitRouteV4, types.ExitRouteV6}, + }, + }, + }, + want: []tailcfg.FilterRule{}, + }, + { + name: "1786-reducing-breaks-exit-nodes-the-exit", + pol: ACLPolicy{ + Hosts: Hosts{ + // Exit node + "internal": netip.MustParsePrefix("100.64.0.100/32"), + }, + Groups: Groups{ + "group:team": {"user3", "user2", "user1"}, + }, + ACLs: []ACL{ + { + Action: "accept", + Sources: []string{"group:team"}, + Destinations: []string{ + "internal:*", + }, + }, + { + Action: "accept", + Sources: []string{"group:team"}, + Destinations: []string{ + "autogroup:internet:*", + }, + }, + }, + }, + node: &types.Node{ + IPv4: iap("100.64.0.100"), + IPv6: iap("fd7a:115c:a1e0::100"), + User: types.User{Name: "user100"}, + Hostinfo: &tailcfg.Hostinfo{ + RoutableIPs: []netip.Prefix{types.ExitRouteV4, types.ExitRouteV6}, + }, + }, + peers: types.Nodes{ + &types.Node{ + IPv4: iap("100.64.0.2"), + IPv6: iap("fd7a:115c:a1e0::2"), + User: types.User{Name: "user2"}, + }, + &types.Node{ + IPv4: iap("100.64.0.1"), + IPv6: iap("fd7a:115c:a1e0::1"), + User: types.User{Name: "user1"}, + }, + }, + want: []tailcfg.FilterRule{ + { + SrcIPs: []string{"100.64.0.1/32", "100.64.0.2/32", "fd7a:115c:a1e0::1/128", "fd7a:115c:a1e0::2/128"}, + DstPorts: []tailcfg.NetPortRange{ + { + IP: "100.64.0.100/32", + Ports: tailcfg.PortRangeAny, + }, + { + IP: "fd7a:115c:a1e0::100/128", + Ports: tailcfg.PortRangeAny, + }, + }, + }, + { + SrcIPs: []string{"100.64.0.1/32", "100.64.0.2/32", "fd7a:115c:a1e0::1/128", "fd7a:115c:a1e0::2/128"}, + DstPorts: hsExitNodeDest, + }, + }, + }, + { + name: "1786-reducing-breaks-exit-nodes-the-example-from-issue", + pol: ACLPolicy{ + Hosts: Hosts{ + // Exit node + "internal": netip.MustParsePrefix("100.64.0.100/32"), + }, + Groups: Groups{ + "group:team": {"user3", "user2", "user1"}, + }, + ACLs: []ACL{ + { + Action: "accept", + Sources: []string{"group:team"}, + Destinations: []string{ + "internal:*", + }, + }, + { + Action: "accept", + Sources: []string{"group:team"}, + Destinations: []string{ + "0.0.0.0/5:*", + "8.0.0.0/7:*", + "11.0.0.0/8:*", + "12.0.0.0/6:*", + "16.0.0.0/4:*", + "32.0.0.0/3:*", + "64.0.0.0/2:*", + "128.0.0.0/3:*", + "160.0.0.0/5:*", + "168.0.0.0/6:*", + "172.0.0.0/12:*", + "172.32.0.0/11:*", + "172.64.0.0/10:*", + "172.128.0.0/9:*", + "173.0.0.0/8:*", + "174.0.0.0/7:*", + "176.0.0.0/4:*", + "192.0.0.0/9:*", + "192.128.0.0/11:*", + "192.160.0.0/13:*", + "192.169.0.0/16:*", + "192.170.0.0/15:*", + "192.172.0.0/14:*", + "192.176.0.0/12:*", + "192.192.0.0/10:*", + "193.0.0.0/8:*", + "194.0.0.0/7:*", + "196.0.0.0/6:*", + "200.0.0.0/5:*", + "208.0.0.0/4:*", + }, + }, + }, + }, + node: &types.Node{ + IPv4: iap("100.64.0.100"), + IPv6: iap("fd7a:115c:a1e0::100"), + User: types.User{Name: "user100"}, + Hostinfo: &tailcfg.Hostinfo{ + RoutableIPs: []netip.Prefix{types.ExitRouteV4, types.ExitRouteV6}, + }, + }, + peers: types.Nodes{ + &types.Node{ + IPv4: iap("100.64.0.2"), + IPv6: iap("fd7a:115c:a1e0::2"), + User: types.User{Name: "user2"}, + }, + &types.Node{ + IPv4: iap("100.64.0.1"), + IPv6: iap("fd7a:115c:a1e0::1"), + User: types.User{Name: "user1"}, + }, + }, + want: []tailcfg.FilterRule{ + { + SrcIPs: []string{"100.64.0.1/32", "100.64.0.2/32", "fd7a:115c:a1e0::1/128", "fd7a:115c:a1e0::2/128"}, + DstPorts: []tailcfg.NetPortRange{ + { + IP: "100.64.0.100/32", + Ports: tailcfg.PortRangeAny, + }, + { + IP: "fd7a:115c:a1e0::100/128", + Ports: tailcfg.PortRangeAny, + }, + }, + }, + { + SrcIPs: []string{"100.64.0.1/32", "100.64.0.2/32", "fd7a:115c:a1e0::1/128", "fd7a:115c:a1e0::2/128"}, + DstPorts: []tailcfg.NetPortRange{ + {IP: "0.0.0.0/5", Ports: tailcfg.PortRangeAny}, + {IP: "8.0.0.0/7", Ports: tailcfg.PortRangeAny}, + {IP: "11.0.0.0/8", Ports: tailcfg.PortRangeAny}, + {IP: "12.0.0.0/6", Ports: tailcfg.PortRangeAny}, + {IP: "16.0.0.0/4", Ports: tailcfg.PortRangeAny}, + {IP: "32.0.0.0/3", Ports: tailcfg.PortRangeAny}, + {IP: "64.0.0.0/2", Ports: tailcfg.PortRangeAny}, + {IP: "fd7a:115c:a1e0::1/128", Ports: tailcfg.PortRangeAny}, + {IP: "fd7a:115c:a1e0::2/128", Ports: tailcfg.PortRangeAny}, + {IP: "fd7a:115c:a1e0::100/128", Ports: tailcfg.PortRangeAny}, + {IP: "128.0.0.0/3", Ports: tailcfg.PortRangeAny}, + {IP: "160.0.0.0/5", Ports: tailcfg.PortRangeAny}, + {IP: "168.0.0.0/6", Ports: tailcfg.PortRangeAny}, + {IP: "172.0.0.0/12", Ports: tailcfg.PortRangeAny}, + {IP: "172.32.0.0/11", Ports: tailcfg.PortRangeAny}, + {IP: "172.64.0.0/10", Ports: tailcfg.PortRangeAny}, + {IP: "172.128.0.0/9", Ports: tailcfg.PortRangeAny}, + {IP: "173.0.0.0/8", Ports: tailcfg.PortRangeAny}, + {IP: "174.0.0.0/7", Ports: tailcfg.PortRangeAny}, + {IP: "176.0.0.0/4", Ports: tailcfg.PortRangeAny}, + {IP: "192.0.0.0/9", Ports: tailcfg.PortRangeAny}, + {IP: "192.128.0.0/11", Ports: tailcfg.PortRangeAny}, + {IP: "192.160.0.0/13", Ports: tailcfg.PortRangeAny}, + {IP: "192.169.0.0/16", Ports: tailcfg.PortRangeAny}, + {IP: "192.170.0.0/15", Ports: tailcfg.PortRangeAny}, + {IP: "192.172.0.0/14", Ports: tailcfg.PortRangeAny}, + {IP: "192.176.0.0/12", Ports: tailcfg.PortRangeAny}, + {IP: "192.192.0.0/10", Ports: tailcfg.PortRangeAny}, + {IP: "193.0.0.0/8", Ports: tailcfg.PortRangeAny}, + {IP: "194.0.0.0/7", Ports: tailcfg.PortRangeAny}, + {IP: "196.0.0.0/6", Ports: tailcfg.PortRangeAny}, + {IP: "200.0.0.0/5", Ports: tailcfg.PortRangeAny}, + {IP: "208.0.0.0/4", Ports: tailcfg.PortRangeAny}, + }, + }, + }, + }, + { + name: "1786-reducing-breaks-exit-nodes-app-connector-like", + pol: ACLPolicy{ + Hosts: Hosts{ + // Exit node + "internal": netip.MustParsePrefix("100.64.0.100/32"), + }, + Groups: Groups{ + "group:team": {"user3", "user2", "user1"}, + }, + ACLs: []ACL{ + { + Action: "accept", + Sources: []string{"group:team"}, + Destinations: []string{ + "internal:*", + }, + }, + { + Action: "accept", + Sources: []string{"group:team"}, + Destinations: []string{ + "8.0.0.0/8:*", + "16.0.0.0/8:*", + }, + }, + }, + }, + node: &types.Node{ + IPv4: iap("100.64.0.100"), + IPv6: iap("fd7a:115c:a1e0::100"), + User: types.User{Name: "user100"}, + Hostinfo: &tailcfg.Hostinfo{ + RoutableIPs: []netip.Prefix{netip.MustParsePrefix("8.0.0.0/16"), netip.MustParsePrefix("16.0.0.0/16")}, + }, + }, + peers: types.Nodes{ + &types.Node{ + IPv4: iap("100.64.0.2"), + IPv6: iap("fd7a:115c:a1e0::2"), + User: types.User{Name: "user2"}, + }, + &types.Node{ + IPv4: iap("100.64.0.1"), + IPv6: iap("fd7a:115c:a1e0::1"), + User: types.User{Name: "user1"}, + }, + }, + want: []tailcfg.FilterRule{ + { + SrcIPs: []string{"100.64.0.1/32", "100.64.0.2/32", "fd7a:115c:a1e0::1/128", "fd7a:115c:a1e0::2/128"}, + DstPorts: []tailcfg.NetPortRange{ + { + IP: "100.64.0.100/32", + Ports: tailcfg.PortRangeAny, + }, + { + IP: "fd7a:115c:a1e0::100/128", + Ports: tailcfg.PortRangeAny, + }, + }, + }, + { + SrcIPs: []string{"100.64.0.1/32", "100.64.0.2/32", "fd7a:115c:a1e0::1/128", "fd7a:115c:a1e0::2/128"}, + DstPorts: []tailcfg.NetPortRange{ + { + IP: "8.0.0.0/8", + Ports: tailcfg.PortRangeAny, + }, + { + IP: "16.0.0.0/8", + Ports: tailcfg.PortRangeAny, + }, + }, + }, + }, + }, + { + name: "1786-reducing-breaks-exit-nodes-app-connector-like2", + pol: ACLPolicy{ + Hosts: Hosts{ + // Exit node + "internal": netip.MustParsePrefix("100.64.0.100/32"), + }, + Groups: Groups{ + "group:team": {"user3", "user2", "user1"}, + }, + ACLs: []ACL{ + { + Action: "accept", + Sources: []string{"group:team"}, + Destinations: []string{ + "internal:*", + }, + }, + { + Action: "accept", + Sources: []string{"group:team"}, + Destinations: []string{ + "8.0.0.0/16:*", + "16.0.0.0/16:*", + }, + }, + }, + }, + node: &types.Node{ + IPv4: iap("100.64.0.100"), + IPv6: iap("fd7a:115c:a1e0::100"), + User: types.User{Name: "user100"}, + Hostinfo: &tailcfg.Hostinfo{ + RoutableIPs: []netip.Prefix{netip.MustParsePrefix("8.0.0.0/8"), netip.MustParsePrefix("16.0.0.0/8")}, + }, + }, + peers: types.Nodes{ + &types.Node{ + IPv4: iap("100.64.0.2"), + IPv6: iap("fd7a:115c:a1e0::2"), + User: types.User{Name: "user2"}, + }, + &types.Node{ + IPv4: iap("100.64.0.1"), + IPv6: iap("fd7a:115c:a1e0::1"), + User: types.User{Name: "user1"}, + }, + }, + want: []tailcfg.FilterRule{ + { + SrcIPs: []string{"100.64.0.1/32", "100.64.0.2/32", "fd7a:115c:a1e0::1/128", "fd7a:115c:a1e0::2/128"}, + DstPorts: []tailcfg.NetPortRange{ + { + IP: "100.64.0.100/32", + Ports: tailcfg.PortRangeAny, + }, + { + IP: "fd7a:115c:a1e0::100/128", + Ports: tailcfg.PortRangeAny, + }, + }, + }, + { + SrcIPs: []string{"100.64.0.1/32", "100.64.0.2/32", "fd7a:115c:a1e0::1/128", "fd7a:115c:a1e0::2/128"}, + DstPorts: []tailcfg.NetPortRange{ + { + IP: "8.0.0.0/16", + Ports: tailcfg.PortRangeAny, + }, + { + IP: "16.0.0.0/16", + Ports: tailcfg.PortRangeAny, + }, + }, + }, + }, + }, + { + name: "1817-reduce-breaks-32-mask", + pol: ACLPolicy{ + Hosts: Hosts{ + "vlan1": netip.MustParsePrefix("172.16.0.0/24"), + "dns1": netip.MustParsePrefix("172.16.0.21/32"), + }, + Groups: Groups{ + "group:access": {"user1"}, + }, + ACLs: []ACL{ + { + Action: "accept", + Sources: []string{"group:access"}, + Destinations: []string{ + "tag:access-servers:*", + "dns1:*", + }, + }, + }, + }, + node: &types.Node{ + IPv4: iap("100.64.0.100"), + IPv6: iap("fd7a:115c:a1e0::100"), + User: types.User{Name: "user100"}, + Hostinfo: &tailcfg.Hostinfo{ + RoutableIPs: []netip.Prefix{netip.MustParsePrefix("172.16.0.0/24")}, + }, + ForcedTags: types.StringList{"tag:access-servers"}, + }, + peers: types.Nodes{ + &types.Node{ + IPv4: iap("100.64.0.1"), + IPv6: iap("fd7a:115c:a1e0::1"), + User: types.User{Name: "user1"}, + }, + }, + want: []tailcfg.FilterRule{ + { + SrcIPs: []string{"100.64.0.1/32", "fd7a:115c:a1e0::1/128"}, + DstPorts: []tailcfg.NetPortRange{ + { + IP: "100.64.0.100/32", + Ports: tailcfg.PortRangeAny, + }, + { + IP: "fd7a:115c:a1e0::100/128", + Ports: tailcfg.PortRangeAny, + }, + { + IP: "172.16.0.21/32", + Ports: tailcfg.PortRangeAny, + }, + }, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - rules, _ := tt.pol.CompileFilterRules( + got, _ := tt.pol.CompileFilterRules( append(tt.peers, tt.node), ) - got := ReduceFilterRules(tt.node, rules) + got = ReduceFilterRules(tt.node, got) if diff := cmp.Diff(tt.want, got); diff != "" { log.Trace().Interface("got", got).Msg("result") From d5ed8bc0747351a5e2d8645f3883ff544f7667ac Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby <kristoffer@dalby.cc> Date: Tue, 30 Apr 2024 11:11:29 +0200 Subject: [PATCH 054/145] error if no prefix is configured (#1918) Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> --- cmd/headscale/cli/root.go | 2 +- hscontrol/types/config.go | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cmd/headscale/cli/root.go b/cmd/headscale/cli/root.go index 68298c55..72c72a20 100644 --- a/cmd/headscale/cli/root.go +++ b/cmd/headscale/cli/root.go @@ -51,7 +51,7 @@ func initConfig() { cfg, err := types.GetHeadscaleConfig() if err != nil { - log.Fatal().Caller().Err(err).Msg("Failed to get headscale configuration") + log.Fatal().Err(err).Msg("Failed to read headscale configuration") } machineOutput := HasMachineOutputFlag() diff --git a/hscontrol/types/config.go b/hscontrol/types/config.go index 20591a6c..fa3a64c6 100644 --- a/hscontrol/types/config.go +++ b/hscontrol/types/config.go @@ -650,6 +650,10 @@ func GetHeadscaleConfig() (*Config, error) { return nil, err } + if prefix4 == nil && prefix6 == nil { + return nil, fmt.Errorf("no IPv4 or IPv6 prefix configured, minimum one prefix is required") + } + allocStr := viper.GetString("prefixes.allocation") var alloc IPAllocationStrategy switch allocStr { @@ -658,7 +662,7 @@ func GetHeadscaleConfig() (*Config, error) { case string(IPAllocationStrategyRandom): alloc = IPAllocationStrategyRandom default: - log.Fatal().Msgf("config error, prefixes.allocation is set to %s, which is not a valid strategy, allowed options: %s, %s", allocStr, IPAllocationStrategySequential, IPAllocationStrategyRandom) + return nil, fmt.Errorf("config error, prefixes.allocation is set to %s, which is not a valid strategy, allowed options: %s, %s", allocStr, IPAllocationStrategySequential, IPAllocationStrategyRandom) } dnsConfig, baseDomain := GetDNSConfig() From 55b35f4160c08323039842ec608e7a1b89c17c51 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby <kristoffer@dalby.cc> Date: Wed, 1 May 2024 17:06:42 +0200 Subject: [PATCH 055/145] fix issue preveting get node when disco is missing (#1919) Fixed #1816 Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> --- hscontrol/types/node.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/hscontrol/types/node.go b/hscontrol/types/node.go index 7f285924..b0afe99d 100644 --- a/hscontrol/types/node.go +++ b/hscontrol/types/node.go @@ -306,11 +306,15 @@ func (node *Node) AfterFind(tx *gorm.DB) error { } node.NodeKey = nodeKey - var discoKey key.DiscoPublic - if err := discoKey.UnmarshalText([]byte(node.DiscoKeyDatabaseField)); err != nil { - return fmt.Errorf("unmarshalling disco key from db: %w", err) + // DiscoKey might be empty if a node has not sent it to headscale. + // This means that this might fail if the disco key is empty. + if node.DiscoKeyDatabaseField != "" { + var discoKey key.DiscoPublic + if err := discoKey.UnmarshalText([]byte(node.DiscoKeyDatabaseField)); err != nil { + return fmt.Errorf("unmarshalling disco key from db: %w", err) + } + node.DiscoKey = discoKey } - node.DiscoKey = discoKey endpoints := make([]netip.AddrPort, len(node.EndpointsDatabaseField)) for idx, ep := range node.EndpointsDatabaseField { From 1c6bfc503c9b8f7adbaaae662138c9a0fb89b213 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby <kristoffer@dalby.cc> Date: Thu, 2 May 2024 11:53:16 +0200 Subject: [PATCH 056/145] fix preauth key logging in as previous user (#1920) * add test case to reproduce #1885 Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * fix preauth key issue logging in as wrong user Fixes #1885 Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * add test to gh Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> --------- Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> --- .github/workflows/test-integration.yaml | 1 + hscontrol/auth.go | 9 ++- integration/cli_test.go | 95 +++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-integration.yaml b/.github/workflows/test-integration.yaml index d93aaca2..9581bada 100644 --- a/.github/workflows/test-integration.yaml +++ b/.github/workflows/test-integration.yaml @@ -26,6 +26,7 @@ jobs: - TestPreAuthKeyCommand - TestPreAuthKeyCommandWithoutExpiry - TestPreAuthKeyCommandReusableEphemeral + - TestPreAuthKeyCorrectUserLoggedInCommand - TestApiKeyCommand - TestNodeTagCommand - TestNodeAdvertiseTagNoACLCommand diff --git a/hscontrol/auth.go b/hscontrol/auth.go index 0679d72e..dab9ff42 100644 --- a/hscontrol/auth.go +++ b/hscontrol/auth.go @@ -315,13 +315,16 @@ func (h *Headscale) handleAuthKey( node.NodeKey = nodeKey node.AuthKeyID = uint(pak.ID) - err := h.db.NodeSetExpiry(node.ID, registerRequest.Expiry) + node.Expiry = ®isterRequest.Expiry + node.User = pak.User + node.UserID = pak.UserID + err := h.db.DB.Save(node).Error if err != nil { log.Error(). Caller(). Str("node", node.Hostname). Err(err). - Msg("Failed to refresh node") + Msg("failed to save node after logging in with auth key") return } @@ -344,7 +347,7 @@ func (h *Headscale) handleAuthKey( } ctx := types.NotifyCtx(context.Background(), "handle-authkey", "na") - h.nodeNotifier.NotifyWithIgnore(ctx, types.StateUpdateExpire(node.ID, registerRequest.Expiry), node.ID) + h.nodeNotifier.NotifyAll(ctx, types.StateUpdate{Type: types.StatePeerChanged, ChangeNodes: []types.NodeID{node.ID}}) } else { now := time.Now().UTC() diff --git a/integration/cli_test.go b/integration/cli_test.go index 24e3b19b..57edf58e 100644 --- a/integration/cli_test.go +++ b/integration/cli_test.go @@ -388,6 +388,101 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) { assert.Len(t, listedPreAuthKeys, 3) } +func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) { + IntegrationSkip(t) + t.Parallel() + + user1 := "user1" + user2 := "user2" + + scenario, err := NewScenario(dockertestMaxWait()) + assertNoErr(t, err) + defer scenario.Shutdown() + + spec := map[string]int{ + user1: 1, + user2: 0, + } + + err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clipak")) + assertNoErr(t, err) + + headscale, err := scenario.Headscale() + assertNoErr(t, err) + + var user2Key v1.PreAuthKey + + err = executeAndUnmarshal( + headscale, + []string{ + "headscale", + "preauthkeys", + "--user", + user2, + "create", + "--reusable", + "--expiration", + "24h", + "--output", + "json", + "--tags", + "tag:test1,tag:test2", + }, + &user2Key, + ) + assertNoErr(t, err) + + allClients, err := scenario.ListTailscaleClients() + assertNoErrListClients(t, err) + + assert.Len(t, allClients, 1) + + client := allClients[0] + + // Log out from user1 + err = client.Logout() + assertNoErr(t, err) + + err = scenario.WaitForTailscaleLogout() + assertNoErr(t, err) + + status, err := client.Status() + assertNoErr(t, err) + if status.BackendState == "Starting" || status.BackendState == "Running" { + t.Fatalf("expected node to be logged out, backend state: %s", status.BackendState) + } + + err = client.Login(headscale.GetEndpoint(), user2Key.GetKey()) + assertNoErr(t, err) + + status, err = client.Status() + assertNoErr(t, err) + if status.BackendState != "Running" { + t.Fatalf("expected node to be logged in, backend state: %s", status.BackendState) + } + + if status.Self.UserID.String() != "userid:2" { + t.Fatalf("expected node to be logged in as userid:2, got: %s", status.Self.UserID.String()) + } + + var listNodes []v1.Node + err = executeAndUnmarshal( + headscale, + []string{ + "headscale", + "nodes", + "list", + "--output", + "json", + }, + &listNodes, + ) + assert.Nil(t, err) + assert.Len(t, listNodes, 1) + + assert.Equal(t, "user2", listNodes[0].User.Name) +} + func TestApiKeyCommand(t *testing.T) { IntegrationSkip(t) t.Parallel() From a9c568c801a514855396c7dcec031b3598457f20 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby <kristoffer@dalby.cc> Date: Thu, 2 May 2024 13:39:19 +0200 Subject: [PATCH 057/145] trace log and notifier shutdown (#1922) Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> --- hscontrol/app.go | 25 +++++++++++++++++++++++++ hscontrol/notifier/notifier.go | 5 +++++ 2 files changed, 30 insertions(+) diff --git a/hscontrol/app.go b/hscontrol/app.go index ce2fd1d8..e72aca2b 100644 --- a/hscontrol/app.go +++ b/hscontrol/app.go @@ -800,10 +800,23 @@ func (h *Headscale) Serve() error { } default: + trace := log.Trace().Msgf log.Info(). Str("signal", sig.String()). Msg("Received signal to stop, shutting down gracefully") + trace("closing map sessions") + wg := sync.WaitGroup{} + for _, mapSess := range h.mapSessions { + wg.Add(1) + go func() { + mapSess.close() + wg.Done() + }() + } + wg.Wait() + + trace("waiting for netmap stream to close") h.pollNetMapStreamWG.Wait() // Gracefully shut down servers @@ -811,32 +824,44 @@ func (h *Headscale) Serve() error { context.Background(), types.HTTPShutdownTimeout, ) + trace("shutting down debug http server") if err := debugHTTPServer.Shutdown(ctx); err != nil { log.Error().Err(err).Msg("Failed to shutdown prometheus http") } + trace("shutting down main http server") if err := httpServer.Shutdown(ctx); err != nil { log.Error().Err(err).Msg("Failed to shutdown http") } + + trace("shutting down grpc server (socket)") grpcSocket.GracefulStop() if grpcServer != nil { + trace("shutting down grpc server (external)") grpcServer.GracefulStop() grpcListener.Close() } if tailsqlContext != nil { + trace("shutting down tailsql") tailsqlContext.Done() } + trace("closing node notifier") + h.nodeNotifier.Close() + // Close network listeners + trace("closing network listeners") debugHTTPListener.Close() httpListener.Close() grpcGatewayConn.Close() // Stop listening (and unlink the socket if unix type): + trace("closing socket listener") socketListener.Close() // Close db connections + trace("closing database connection") err = h.db.Close() if err != nil { log.Error().Err(err).Msg("Failed to close db") diff --git a/hscontrol/notifier/notifier.go b/hscontrol/notifier/notifier.go index 74b6645e..d5ef89f5 100644 --- a/hscontrol/notifier/notifier.go +++ b/hscontrol/notifier/notifier.go @@ -34,6 +34,11 @@ func NewNotifier(cfg *types.Config) *Notifier { return n } +// Close stops the batcher inside the notifier. +func (n *Notifier) Close() { + n.b.close() +} + func (n *Notifier) AddNode(nodeID types.NodeID, c chan<- types.StateUpdate) { log.Trace().Caller().Uint64("node.id", nodeID.Uint64()).Msg("acquiring lock to add node") defer log.Trace(). From 622aa82da2091a0a646dfd2376e20d0facaef582 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby <kristoffer@dalby.cc> Date: Thu, 2 May 2024 17:57:53 +0200 Subject: [PATCH 058/145] ensure expire routines are cleaned up (#1924) Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> --- hscontrol/app.go | 109 +++++++++++++++++++++++++++-------------------- 1 file changed, 63 insertions(+), 46 deletions(-) diff --git a/hscontrol/app.go b/hscontrol/app.go index e72aca2b..b8eb6f69 100644 --- a/hscontrol/app.go +++ b/hscontrol/app.go @@ -70,7 +70,7 @@ var ( const ( AuthPrefix = "Bearer " - updateInterval = 5000 + updateInterval = 5 * time.Second privateKeyFileMode = 0o600 headscaleDirPerm = 0o700 @@ -219,64 +219,75 @@ func (h *Headscale) redirect(w http.ResponseWriter, req *http.Request) { // deleteExpireEphemeralNodes deletes ephemeral node records that have not been // seen for longer than h.cfg.EphemeralNodeInactivityTimeout. -func (h *Headscale) deleteExpireEphemeralNodes(milliSeconds int64) { - ticker := time.NewTicker(time.Duration(milliSeconds) * time.Millisecond) +func (h *Headscale) deleteExpireEphemeralNodes(ctx context.Context, every time.Duration) { + ticker := time.NewTicker(every) - for range ticker.C { - var removed []types.NodeID - var changed []types.NodeID - if err := h.db.Write(func(tx *gorm.DB) error { - removed, changed = db.DeleteExpiredEphemeralNodes(tx, h.cfg.EphemeralNodeInactivityTimeout) + for { + select { + case <-ctx.Done(): + ticker.Stop() + return + case <-ticker.C: + var removed []types.NodeID + var changed []types.NodeID + if err := h.db.Write(func(tx *gorm.DB) error { + removed, changed = db.DeleteExpiredEphemeralNodes(tx, h.cfg.EphemeralNodeInactivityTimeout) - return nil - }); err != nil { - log.Error().Err(err).Msg("database error while expiring ephemeral nodes") - continue - } + return nil + }); err != nil { + log.Error().Err(err).Msg("database error while expiring ephemeral nodes") + continue + } - if removed != nil { - ctx := types.NotifyCtx(context.Background(), "expire-ephemeral", "na") - h.nodeNotifier.NotifyAll(ctx, types.StateUpdate{ - Type: types.StatePeerRemoved, - Removed: removed, - }) - } + if removed != nil { + ctx := types.NotifyCtx(context.Background(), "expire-ephemeral", "na") + h.nodeNotifier.NotifyAll(ctx, types.StateUpdate{ + Type: types.StatePeerRemoved, + Removed: removed, + }) + } - if changed != nil { - ctx := types.NotifyCtx(context.Background(), "expire-ephemeral", "na") - h.nodeNotifier.NotifyAll(ctx, types.StateUpdate{ - Type: types.StatePeerChanged, - ChangeNodes: changed, - }) + if changed != nil { + ctx := types.NotifyCtx(context.Background(), "expire-ephemeral", "na") + h.nodeNotifier.NotifyAll(ctx, types.StateUpdate{ + Type: types.StatePeerChanged, + ChangeNodes: changed, + }) + } } } } -// expireExpiredMachines expires nodes that have an explicit expiry set +// expireExpiredNodes expires nodes that have an explicit expiry set // after that expiry time has passed. -func (h *Headscale) expireExpiredMachines(intervalMs int64) { - interval := time.Duration(intervalMs) * time.Millisecond - ticker := time.NewTicker(interval) +func (h *Headscale) expireExpiredNodes(ctx context.Context, every time.Duration) { + ticker := time.NewTicker(every) lastCheck := time.Unix(0, 0) var update types.StateUpdate var changed bool - for range ticker.C { - if err := h.db.Write(func(tx *gorm.DB) error { - lastCheck, update, changed = db.ExpireExpiredNodes(tx, lastCheck) + for { + select { + case <-ctx.Done(): + ticker.Stop() + return + case <-ticker.C: + if err := h.db.Write(func(tx *gorm.DB) error { + lastCheck, update, changed = db.ExpireExpiredNodes(tx, lastCheck) - return nil - }); err != nil { - log.Error().Err(err).Msg("database error while expiring nodes") - continue - } + return nil + }); err != nil { + log.Error().Err(err).Msg("database error while expiring nodes") + continue + } - if changed { - log.Trace().Interface("nodes", update.ChangePatches).Msgf("expiring nodes") + if changed { + log.Trace().Interface("nodes", update.ChangePatches).Msgf("expiring nodes") - ctx := types.NotifyCtx(context.Background(), "expire-expired", "na") - h.nodeNotifier.NotifyAll(ctx, update) + ctx := types.NotifyCtx(context.Background(), "expire-expired", "na") + h.nodeNotifier.NotifyAll(ctx, update) + } } } } @@ -538,10 +549,13 @@ func (h *Headscale) Serve() error { return errEmptyInitialDERPMap } - // TODO(kradalby): These should have cancel channels and be cleaned - // up on shutdown. - go h.deleteExpireEphemeralNodes(updateInterval) - go h.expireExpiredMachines(updateInterval) + expireEphemeralCtx, expireEphemeralCancel := context.WithCancel(context.Background()) + defer expireEphemeralCancel() + go h.deleteExpireEphemeralNodes(expireEphemeralCtx, updateInterval) + + expireNodeCtx, expireNodeCancel := context.WithCancel(context.Background()) + defer expireNodeCancel() + go h.expireExpiredNodes(expireNodeCtx, updateInterval) if zl.GlobalLevel() == zl.TraceLevel { zerolog.RespLog = true @@ -805,6 +819,9 @@ func (h *Headscale) Serve() error { Str("signal", sig.String()). Msg("Received signal to stop, shutting down gracefully") + expireNodeCancel() + expireEphemeralCancel() + trace("closing map sessions") wg := sync.WaitGroup{} for _, mapSess := range h.mapSessions { From 93a915c096f155be097aac5f3a0c7fc2bc0fc570 Mon Sep 17 00:00:00 2001 From: Michael Savage <mike@mikejsavage.co.uk> Date: Mon, 6 May 2024 21:03:21 +0300 Subject: [PATCH 059/145] Update OpenBSD installation docs for 2024 (#1915) --- docs/running-headscale-openbsd.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/running-headscale-openbsd.md b/docs/running-headscale-openbsd.md index 29e340fc..a490439a 100644 --- a/docs/running-headscale-openbsd.md +++ b/docs/running-headscale-openbsd.md @@ -9,19 +9,17 @@ ## Goal -This documentation has the goal of showing a user how-to install and run `headscale` on OpenBSD 7.1. +This documentation has the goal of showing a user how-to install and run `headscale` on OpenBSD. In additional to the "get up and running section", there is an optional [rc.d section](#running-headscale-in-the-background-with-rcd) describing how to make `headscale` run properly in a server environment. ## Install `headscale` -1. Install from ports (not recommended) +1. Install from ports - !!! info + You can install headscale from ports by running `pkg_add headscale`. - As of OpenBSD 7.2, there's a headscale in ports collection, however, it's severely outdated(v0.12.4). You can install it via `pkg_add headscale`. - -1. Install from source on OpenBSD 7.2 +1. Install from source ```shell # Install prerequistes From 2bac80cfbfb1bcd67af59d43ceae607fb8338279 Mon Sep 17 00:00:00 2001 From: Dan Pastusek <dan@hexial.com> Date: Mon, 6 May 2024 11:06:30 -0700 Subject: [PATCH 060/145] [DOCS] Make linux installation instructions more clear (#1927) * Make linux installation instructions more clear * Update running-headscale-linux.md --- docs/running-headscale-linux.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/running-headscale-linux.md b/docs/running-headscale-linux.md index 5f906009..f08789c4 100644 --- a/docs/running-headscale-linux.md +++ b/docs/running-headscale-linux.md @@ -20,17 +20,19 @@ configuration (`/etc/headscale/config.yaml`). ## Installation -1. Download the latest Headscale package for your platform (`.deb` for Ubuntu and Debian) from [Headscale's releases page](https://github.com/juanfont/headscale/releases): +1. Download the [latest Headscale package](https://github.com/juanfont/headscale/releases/latest) for your platform (`.deb` for Ubuntu and Debian). ```shell + HEADSCALE_VERSION="" # See above URL for latest version, e.g. "X.Y.Z" (NOTE: do not add the "v" prefix!) + HEADSCALE_ARCH="" # Your system architecture, e.g. "amd64" wget --output-document=headscale.deb \ - https://github.com/juanfont/headscale/releases/download/v<HEADSCALE VERSION>/headscale_<HEADSCALE VERSION>_linux_<ARCH>.deb + "https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_${HEADSCALE_ARCH}.deb" ``` 1. Install Headscale: ```shell - sudo apt install headscale.deb + sudo apt install ./headscale.deb ``` 1. Enable Headscale service, this will start Headscale at boot: From 7fd2485000c743666316f4eaf691967de7030361 Mon Sep 17 00:00:00 2001 From: MichaelKo <viacheslav.sychov@gmail.com> Date: Thu, 16 May 2024 02:40:14 +0200 Subject: [PATCH 061/145] Restore foreign keys and add constraints (#1562) * fix #1482, restore foregin keys, add constraints * #1562, fix tests, fix formatting * #1562: fix tests * #1562: fix local run of test_integration --- CHANGELOG.md | 1 + Makefile | 1 + hscontrol/auth.go | 11 ++++-- hscontrol/db/db.go | 9 +++-- hscontrol/db/ip_test.go | 26 ++++++++++++++ hscontrol/db/node.go | 2 +- hscontrol/db/node_test.go | 57 ++++++++++++++++++++----------- hscontrol/db/preauth_keys.go | 3 +- hscontrol/db/preauth_keys_test.go | 21 ++++++++---- hscontrol/db/routes_test.go | 43 ++++++++++++++++++----- hscontrol/db/users_test.go | 12 ++++--- hscontrol/mapper/mapper_test.go | 7 ++-- hscontrol/mapper/tail_test.go | 1 - hscontrol/types/node.go | 8 ++--- hscontrol/types/preauth_key.go | 8 ++--- 15 files changed, 149 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cd82830..a8e15c0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,7 @@ after improving the test harness as part of adopting [#1460](https://github.com/ - Add command to backfill IP addresses for nodes missing IPs from configured prefixes. [#1869](https://github.com/juanfont/headscale/pull/1869) - Log available update as warning [#1877](https://github.com/juanfont/headscale/pull/1877) - Add `autogroup:internet` to Policy [#1917](https://github.com/juanfont/headscale/pull/1917) +- Restore foreign keys and add constraints [#1562](https://github.com/juanfont/headscale/pull/1562) ## 0.22.3 (2023-05-12) diff --git a/Makefile b/Makefile index 442690ed..719393f5 100644 --- a/Makefile +++ b/Makefile @@ -31,6 +31,7 @@ test_integration: --name headscale-test-suite \ -v $$PWD:$$PWD -w $$PWD/integration \ -v /var/run/docker.sock:/var/run/docker.sock \ + -v $$PWD/control_logs:/tmp/control \ golang:1 \ go run gotest.tools/gotestsum@latest -- -failfast ./... -timeout 120m -parallel 8 diff --git a/hscontrol/auth.go b/hscontrol/auth.go index dab9ff42..c4511db3 100644 --- a/hscontrol/auth.go +++ b/hscontrol/auth.go @@ -314,7 +314,11 @@ func (h *Headscale) handleAuthKey( Msg("node was already registered before, refreshing with new auth key") node.NodeKey = nodeKey - node.AuthKeyID = uint(pak.ID) + pakID := uint(pak.ID) + if pakID != 0 { + node.AuthKeyID = &pakID + } + node.Expiry = ®isterRequest.Expiry node.User = pak.User node.UserID = pak.UserID @@ -373,7 +377,6 @@ func (h *Headscale) handleAuthKey( Expiry: ®isterRequest.Expiry, NodeKey: nodeKey, LastSeen: &now, - AuthKeyID: uint(pak.ID), ForcedTags: pak.Proto().GetAclTags(), } @@ -389,6 +392,10 @@ func (h *Headscale) handleAuthKey( return } + pakID := uint(pak.ID) + if pakID != 0 { + nodeToRegister.AuthKeyID = &pakID + } node, err = h.db.RegisterNode( nodeToRegister, ipv4, ipv6, diff --git a/hscontrol/db/db.go b/hscontrol/db/db.go index c8ec3378..a30939c1 100644 --- a/hscontrol/db/db.go +++ b/hscontrol/db/db.go @@ -91,7 +91,8 @@ func NewHeadscaleDatabase( _ = tx.Migrator(). RenameColumn(&types.Node{}, "nickname", "given_name") - // If the Node table has a column for registered, + dbConn.Model(&types.Node{}).Where("auth_key_id = ?", 0).Update("auth_key_id", nil) + // If the Node table has a column for registered, // find all occourences of "false" and drop them. Then // remove the column. if tx.Migrator().HasColumn(&types.Node{}, "registered") { @@ -441,8 +442,7 @@ func openDB(cfg types.DatabaseConfig) (*gorm.DB, error) { db, err := gorm.Open( sqlite.Open(cfg.Sqlite.Path+"?_synchronous=1&_journal_mode=WAL"), &gorm.Config{ - DisableForeignKeyConstraintWhenMigrating: true, - Logger: dbLogger, + Logger: dbLogger, }, ) @@ -488,8 +488,7 @@ func openDB(cfg types.DatabaseConfig) (*gorm.DB, error) { } db, err := gorm.Open(postgres.Open(dbString), &gorm.Config{ - DisableForeignKeyConstraintWhenMigrating: true, - Logger: dbLogger, + Logger: dbLogger, }) if err != nil { return nil, err diff --git a/hscontrol/db/ip_test.go b/hscontrol/db/ip_test.go index a651476c..c922fcdf 100644 --- a/hscontrol/db/ip_test.go +++ b/hscontrol/db/ip_test.go @@ -87,8 +87,11 @@ func TestIPAllocatorSequential(t *testing.T) { name: "simple-with-db", dbFunc: func() *HSDatabase { db := dbForTest(t, "simple-with-db") + user := types.User{Name: ""} + db.DB.Save(&user) db.DB.Save(&types.Node{ + User: user, IPv4: nap("100.64.0.1"), IPv6: nap("fd7a:115c:a1e0::1"), }) @@ -112,8 +115,11 @@ func TestIPAllocatorSequential(t *testing.T) { name: "before-after-free-middle-in-db", dbFunc: func() *HSDatabase { db := dbForTest(t, "before-after-free-middle-in-db") + user := types.User{Name: ""} + db.DB.Save(&user) db.DB.Save(&types.Node{ + User: user, IPv4: nap("100.64.0.2"), IPv6: nap("fd7a:115c:a1e0::2"), }) @@ -307,8 +313,11 @@ func TestBackfillIPAddresses(t *testing.T) { name: "simple-backfill-ipv6", dbFunc: func() *HSDatabase { db := dbForTest(t, "simple-backfill-ipv6") + user := types.User{Name: ""} + db.DB.Save(&user) db.DB.Save(&types.Node{ + User: user, IPv4: nap("100.64.0.1"), }) @@ -337,8 +346,11 @@ func TestBackfillIPAddresses(t *testing.T) { name: "simple-backfill-ipv4", dbFunc: func() *HSDatabase { db := dbForTest(t, "simple-backfill-ipv4") + user := types.User{Name: ""} + db.DB.Save(&user) db.DB.Save(&types.Node{ + User: user, IPv6: nap("fd7a:115c:a1e0::1"), }) @@ -367,8 +379,11 @@ func TestBackfillIPAddresses(t *testing.T) { name: "simple-backfill-remove-ipv6", dbFunc: func() *HSDatabase { db := dbForTest(t, "simple-backfill-remove-ipv6") + user := types.User{Name: ""} + db.DB.Save(&user) db.DB.Save(&types.Node{ + User: user, IPv4: nap("100.64.0.1"), IPv6: nap("fd7a:115c:a1e0::1"), }) @@ -392,8 +407,11 @@ func TestBackfillIPAddresses(t *testing.T) { name: "simple-backfill-remove-ipv4", dbFunc: func() *HSDatabase { db := dbForTest(t, "simple-backfill-remove-ipv4") + user := types.User{Name: ""} + db.DB.Save(&user) db.DB.Save(&types.Node{ + User: user, IPv4: nap("100.64.0.1"), IPv6: nap("fd7a:115c:a1e0::1"), }) @@ -417,17 +435,23 @@ func TestBackfillIPAddresses(t *testing.T) { name: "multi-backfill-ipv6", dbFunc: func() *HSDatabase { db := dbForTest(t, "simple-backfill-ipv6") + user := types.User{Name: ""} + db.DB.Save(&user) db.DB.Save(&types.Node{ + User: user, IPv4: nap("100.64.0.1"), }) db.DB.Save(&types.Node{ + User: user, IPv4: nap("100.64.0.2"), }) db.DB.Save(&types.Node{ + User: user, IPv4: nap("100.64.0.3"), }) db.DB.Save(&types.Node{ + User: user, IPv4: nap("100.64.0.4"), }) @@ -451,6 +475,8 @@ func TestBackfillIPAddresses(t *testing.T) { "MachineKeyDatabaseField", "NodeKeyDatabaseField", "DiscoKeyDatabaseField", + "User", + "UserID", "Endpoints", "HostinfoDatabaseField", "Hostinfo", diff --git a/hscontrol/db/node.go b/hscontrol/db/node.go index 91bf0cb3..e9a4ea04 100644 --- a/hscontrol/db/node.go +++ b/hscontrol/db/node.go @@ -279,7 +279,7 @@ func DeleteNode(tx *gorm.DB, } // Unscoped causes the node to be fully removed from the database. - if err := tx.Unscoped().Delete(&node).Error; err != nil { + if err := tx.Unscoped().Delete(&types.Node{}, node.ID).Error; err != nil { return changed, err } diff --git a/hscontrol/db/node_test.go b/hscontrol/db/node_test.go index ce2ada33..fa187653 100644 --- a/hscontrol/db/node_test.go +++ b/hscontrol/db/node_test.go @@ -29,6 +29,7 @@ func (s *Suite) TestGetNode(c *check.C) { nodeKey := key.NewNode() machineKey := key.NewMachine() + pakID := uint(pak.ID) node := &types.Node{ ID: 0, @@ -37,9 +38,10 @@ func (s *Suite) TestGetNode(c *check.C) { Hostname: "testnode", UserID: user.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: uint(pak.ID), + AuthKeyID: &pakID, } - db.DB.Save(node) + trx := db.DB.Save(node) + c.Assert(trx.Error, check.IsNil) _, err = db.getNode("test", "testnode") c.Assert(err, check.IsNil) @@ -58,6 +60,7 @@ func (s *Suite) TestGetNodeByID(c *check.C) { nodeKey := key.NewNode() machineKey := key.NewMachine() + pakID := uint(pak.ID) node := types.Node{ ID: 0, MachineKey: machineKey.Public(), @@ -65,9 +68,10 @@ func (s *Suite) TestGetNodeByID(c *check.C) { Hostname: "testnode", UserID: user.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: uint(pak.ID), + AuthKeyID: &pakID, } - db.DB.Save(&node) + trx := db.DB.Save(&node) + c.Assert(trx.Error, check.IsNil) _, err = db.GetNodeByID(0) c.Assert(err, check.IsNil) @@ -88,6 +92,7 @@ func (s *Suite) TestGetNodeByAnyNodeKey(c *check.C) { machineKey := key.NewMachine() + pakID := uint(pak.ID) node := types.Node{ ID: 0, MachineKey: machineKey.Public(), @@ -95,9 +100,10 @@ func (s *Suite) TestGetNodeByAnyNodeKey(c *check.C) { Hostname: "testnode", UserID: user.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: uint(pak.ID), + AuthKeyID: &pakID, } - db.DB.Save(&node) + trx := db.DB.Save(&node) + c.Assert(trx.Error, check.IsNil) _, err = db.GetNodeByAnyKey(machineKey.Public(), nodeKey.Public(), oldNodeKey.Public()) c.Assert(err, check.IsNil) @@ -117,9 +123,9 @@ func (s *Suite) TestHardDeleteNode(c *check.C) { Hostname: "testnode3", UserID: user.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: uint(1), } - db.DB.Save(&node) + trx := db.DB.Save(&node) + c.Assert(trx.Error, check.IsNil) _, err = db.DeleteNode(&node, xsync.NewMapOf[types.NodeID, bool]()) c.Assert(err, check.IsNil) @@ -138,6 +144,7 @@ func (s *Suite) TestListPeers(c *check.C) { _, err = db.GetNodeByID(0) c.Assert(err, check.NotNil) + pakID := uint(pak.ID) for index := 0; index <= 10; index++ { nodeKey := key.NewNode() machineKey := key.NewMachine() @@ -149,9 +156,10 @@ func (s *Suite) TestListPeers(c *check.C) { Hostname: "testnode" + strconv.Itoa(index), UserID: user.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: uint(pak.ID), + AuthKeyID: &pakID, } - db.DB.Save(&node) + trx := db.DB.Save(&node) + c.Assert(trx.Error, check.IsNil) } node0ByID, err := db.GetNodeByID(0) @@ -188,6 +196,7 @@ func (s *Suite) TestGetACLFilteredPeers(c *check.C) { for index := 0; index <= 10; index++ { nodeKey := key.NewNode() machineKey := key.NewMachine() + pakID := uint(stor[index%2].key.ID) v4 := netip.MustParseAddr(fmt.Sprintf("100.64.0.%v", strconv.Itoa(index+1))) node := types.Node{ @@ -198,9 +207,10 @@ func (s *Suite) TestGetACLFilteredPeers(c *check.C) { Hostname: "testnode" + strconv.Itoa(index), UserID: stor[index%2].user.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: uint(stor[index%2].key.ID), + AuthKeyID: &pakID, } - db.DB.Save(&node) + trx := db.DB.Save(&node) + c.Assert(trx.Error, check.IsNil) } aclPolicy := &policy.ACLPolicy{ @@ -272,6 +282,7 @@ func (s *Suite) TestExpireNode(c *check.C) { nodeKey := key.NewNode() machineKey := key.NewMachine() + pakID := uint(pak.ID) node := &types.Node{ ID: 0, @@ -280,7 +291,7 @@ func (s *Suite) TestExpireNode(c *check.C) { Hostname: "testnode", UserID: user.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: uint(pak.ID), + AuthKeyID: &pakID, Expiry: &time.Time{}, } db.DB.Save(node) @@ -316,6 +327,7 @@ func (s *Suite) TestGenerateGivenName(c *check.C) { machineKey2 := key.NewMachine() + pakID := uint(pak.ID) node := &types.Node{ ID: 0, MachineKey: machineKey.Public(), @@ -324,9 +336,11 @@ func (s *Suite) TestGenerateGivenName(c *check.C) { GivenName: "hostname-1", UserID: user1.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: uint(pak.ID), + AuthKeyID: &pakID, } - db.DB.Save(node) + + trx := db.DB.Save(node) + c.Assert(trx.Error, check.IsNil) givenName, err := db.GenerateGivenName(machineKey2.Public(), "hostname-2") comment := check.Commentf("Same user, unique nodes, unique hostnames, no conflict") @@ -357,6 +371,7 @@ func (s *Suite) TestSetTags(c *check.C) { nodeKey := key.NewNode() machineKey := key.NewMachine() + pakID := uint(pak.ID) node := &types.Node{ ID: 0, MachineKey: machineKey.Public(), @@ -364,9 +379,11 @@ func (s *Suite) TestSetTags(c *check.C) { Hostname: "testnode", UserID: user.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: uint(pak.ID), + AuthKeyID: &pakID, } - db.DB.Save(node) + + trx := db.DB.Save(node) + c.Assert(trx.Error, check.IsNil) // assign simple tags sTags := []string{"tag:test", "tag:foo"} @@ -548,6 +565,7 @@ func (s *Suite) TestAutoApproveRoutes(c *check.C) { route2 := netip.MustParsePrefix("10.11.0.0/24") v4 := netip.MustParseAddr("100.64.0.1") + pakID := uint(pak.ID) node := types.Node{ ID: 0, MachineKey: machineKey.Public(), @@ -555,7 +573,7 @@ func (s *Suite) TestAutoApproveRoutes(c *check.C) { Hostname: "test", UserID: user.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: uint(pak.ID), + AuthKeyID: &pakID, Hostinfo: &tailcfg.Hostinfo{ RequestTags: []string{"tag:exit"}, RoutableIPs: []netip.Prefix{defaultRouteV4, defaultRouteV6, route1, route2}, @@ -563,7 +581,8 @@ func (s *Suite) TestAutoApproveRoutes(c *check.C) { IPv4: &v4, } - db.DB.Save(&node) + trx := db.DB.Save(&node) + c.Assert(trx.Error, check.IsNil) sendUpdate, err := db.SaveNodeRoutes(&node) c.Assert(err, check.IsNil) diff --git a/hscontrol/db/preauth_keys.go b/hscontrol/db/preauth_keys.go index 5d38de29..16a8689f 100644 --- a/hscontrol/db/preauth_keys.go +++ b/hscontrol/db/preauth_keys.go @@ -197,9 +197,10 @@ func ValidatePreAuthKey(tx *gorm.DB, k string) (*types.PreAuthKey, error) { } nodes := types.Nodes{} + pakID := uint(pak.ID) if err := tx. Preload("AuthKey"). - Where(&types.Node{AuthKeyID: uint(pak.ID)}). + Where(&types.Node{AuthKeyID: &pakID}). Find(&nodes).Error; err != nil { return nil, err } diff --git a/hscontrol/db/preauth_keys_test.go b/hscontrol/db/preauth_keys_test.go index fa9681ac..9cdcba80 100644 --- a/hscontrol/db/preauth_keys_test.go +++ b/hscontrol/db/preauth_keys_test.go @@ -76,14 +76,16 @@ func (*Suite) TestAlreadyUsedKey(c *check.C) { pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil) c.Assert(err, check.IsNil) + pakID := uint(pak.ID) node := types.Node{ ID: 0, Hostname: "testest", UserID: user.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: uint(pak.ID), + AuthKeyID: &pakID, } - db.DB.Save(&node) + trx := db.DB.Save(&node) + c.Assert(trx.Error, check.IsNil) key, err := db.ValidatePreAuthKey(pak.Key) c.Assert(err, check.Equals, ErrSingleUseAuthKeyHasBeenUsed) @@ -97,14 +99,16 @@ func (*Suite) TestReusableBeingUsedKey(c *check.C) { pak, err := db.CreatePreAuthKey(user.Name, true, false, nil, nil) c.Assert(err, check.IsNil) + pakID := uint(pak.ID) node := types.Node{ ID: 1, Hostname: "testest", UserID: user.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: uint(pak.ID), + AuthKeyID: &pakID, } - db.DB.Save(&node) + trx := db.DB.Save(&node) + c.Assert(trx.Error, check.IsNil) key, err := db.ValidatePreAuthKey(pak.Key) c.Assert(err, check.IsNil) @@ -131,15 +135,17 @@ func (*Suite) TestEphemeralKeyReusable(c *check.C) { c.Assert(err, check.IsNil) now := time.Now().Add(-time.Second * 30) + pakID := uint(pak.ID) node := types.Node{ ID: 0, Hostname: "testest", UserID: user.ID, RegisterMethod: util.RegisterMethodAuthKey, LastSeen: &now, - AuthKeyID: uint(pak.ID), + AuthKeyID: &pakID, } - db.DB.Save(&node) + trx := db.DB.Save(&node) + c.Assert(trx.Error, check.IsNil) _, err = db.ValidatePreAuthKey(pak.Key) c.Assert(err, check.IsNil) @@ -165,13 +171,14 @@ func (*Suite) TestEphemeralKeyNotReusable(c *check.C) { c.Assert(err, check.IsNil) now := time.Now().Add(-time.Second * 30) + pakId := uint(pak.ID) node := types.Node{ ID: 0, Hostname: "testest", UserID: user.ID, RegisterMethod: util.RegisterMethodAuthKey, LastSeen: &now, - AuthKeyID: uint(pak.ID), + AuthKeyID: &pakId, } db.DB.Save(&node) diff --git a/hscontrol/db/routes_test.go b/hscontrol/db/routes_test.go index 02342ca2..8bbc5948 100644 --- a/hscontrol/db/routes_test.go +++ b/hscontrol/db/routes_test.go @@ -43,15 +43,17 @@ func (s *Suite) TestGetRoutes(c *check.C) { RoutableIPs: []netip.Prefix{route}, } + pakID := uint(pak.ID) node := types.Node{ ID: 0, Hostname: "test_get_route_node", UserID: user.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: uint(pak.ID), + AuthKeyID: &pakID, Hostinfo: &hostInfo, } - db.DB.Save(&node) + trx := db.DB.Save(&node) + c.Assert(trx.Error, check.IsNil) su, err := db.SaveNodeRoutes(&node) c.Assert(err, check.IsNil) @@ -93,15 +95,17 @@ func (s *Suite) TestGetEnableRoutes(c *check.C) { RoutableIPs: []netip.Prefix{route, route2}, } + pakID := uint(pak.ID) node := types.Node{ ID: 0, Hostname: "test_enable_route_node", UserID: user.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: uint(pak.ID), + AuthKeyID: &pakID, Hostinfo: &hostInfo, } - db.DB.Save(&node) + trx := db.DB.Save(&node) + c.Assert(trx.Error, check.IsNil) sendUpdate, err := db.SaveNodeRoutes(&node) c.Assert(err, check.IsNil) @@ -165,15 +169,17 @@ func (s *Suite) TestIsUniquePrefix(c *check.C) { hostInfo1 := tailcfg.Hostinfo{ RoutableIPs: []netip.Prefix{route, route2}, } + pakID := uint(pak.ID) node1 := types.Node{ ID: 1, Hostname: "test_enable_route_node", UserID: user.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: uint(pak.ID), + AuthKeyID: &pakID, Hostinfo: &hostInfo1, } - db.DB.Save(&node1) + trx := db.DB.Save(&node1) + c.Assert(trx.Error, check.IsNil) sendUpdate, err := db.SaveNodeRoutes(&node1) c.Assert(err, check.IsNil) @@ -193,7 +199,7 @@ func (s *Suite) TestIsUniquePrefix(c *check.C) { Hostname: "test_enable_route_node", UserID: user.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: uint(pak.ID), + AuthKeyID: &pakID, Hostinfo: &hostInfo2, } db.DB.Save(&node2) @@ -247,16 +253,18 @@ func (s *Suite) TestDeleteRoutes(c *check.C) { } now := time.Now() + pakID := uint(pak.ID) node1 := types.Node{ ID: 1, Hostname: "test_enable_route_node", UserID: user.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: uint(pak.ID), + AuthKeyID: &pakID, Hostinfo: &hostInfo1, LastSeen: &now, } - db.DB.Save(&node1) + trx := db.DB.Save(&node1) + c.Assert(trx.Error, check.IsNil) sendUpdate, err := db.SaveNodeRoutes(&node1) c.Assert(err, check.IsNil) @@ -617,7 +625,16 @@ func TestFailoverNodeRoutesIfNeccessary(t *testing.T) { db := dbForTest(t, tt.name) + user := types.User{Name: tt.name} + if err := db.DB.Save(&user).Error; err != nil { + t.Fatalf("failed to create user: %s", err) + } + for _, route := range tt.routes { + route.Node.User = user + if err := db.DB.Save(&route.Node).Error; err != nil { + t.Fatalf("failed to create node: %s", err) + } if err := db.DB.Save(&route).Error; err != nil { t.Fatalf("failed to create route: %s", err) } @@ -1013,8 +1030,16 @@ func TestFailoverRouteTx(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { db := dbForTest(t, tt.name) + user := types.User{Name: "test"} + if err := db.DB.Save(&user).Error; err != nil { + t.Fatalf("failed to create user: %s", err) + } for _, route := range tt.routes { + route.Node.User = user + if err := db.DB.Save(&route.Node).Error; err != nil { + t.Fatalf("failed to create node: %s", err) + } if err := db.DB.Save(&route).Error; err != nil { t.Fatalf("failed to create route: %s", err) } diff --git a/hscontrol/db/users_test.go b/hscontrol/db/users_test.go index b36e8613..98dea6c0 100644 --- a/hscontrol/db/users_test.go +++ b/hscontrol/db/users_test.go @@ -46,14 +46,16 @@ func (s *Suite) TestDestroyUserErrors(c *check.C) { pak, err = db.CreatePreAuthKey(user.Name, false, false, nil, nil) c.Assert(err, check.IsNil) + pakID := uint(pak.ID) node := types.Node{ ID: 0, Hostname: "testnode", UserID: user.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: uint(pak.ID), + AuthKeyID: &pakID, } - db.DB.Save(&node) + trx := db.DB.Save(&node) + c.Assert(trx.Error, check.IsNil) err = db.DestroyUser("test") c.Assert(err, check.Equals, ErrUserStillHasNodes) @@ -98,14 +100,16 @@ func (s *Suite) TestSetMachineUser(c *check.C) { pak, err := db.CreatePreAuthKey(oldUser.Name, false, false, nil, nil) c.Assert(err, check.IsNil) + pakID := uint(pak.ID) node := types.Node{ ID: 0, Hostname: "testnode", UserID: oldUser.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: uint(pak.ID), + AuthKeyID: &pakID, } - db.DB.Save(&node) + trx := db.DB.Save(&node) + c.Assert(trx.Error, check.IsNil) c.Assert(node.UserID, check.Equals, oldUser.ID) err = db.AssignNodeToUser(&node, newUser.Name) diff --git a/hscontrol/mapper/mapper_test.go b/hscontrol/mapper/mapper_test.go index f6248470..2ba3d031 100644 --- a/hscontrol/mapper/mapper_test.go +++ b/hscontrol/mapper/mapper_test.go @@ -187,10 +187,9 @@ func Test_fullMapResponse(t *testing.T) { UserID: 0, User: types.User{Name: "mini"}, ForcedTags: []string{}, - AuthKeyID: 0, - AuthKey: &types.PreAuthKey{}, - LastSeen: &lastSeen, - Expiry: &expire, + AuthKey: &types.PreAuthKey{}, + LastSeen: &lastSeen, + Expiry: &expire, Hostinfo: &tailcfg.Hostinfo{}, Routes: []types.Route{ { diff --git a/hscontrol/mapper/tail_test.go b/hscontrol/mapper/tail_test.go index 229f0f88..47af68fe 100644 --- a/hscontrol/mapper/tail_test.go +++ b/hscontrol/mapper/tail_test.go @@ -97,7 +97,6 @@ func TestTailNode(t *testing.T) { Name: "mini", }, ForcedTags: []string{}, - AuthKeyID: 0, AuthKey: &types.PreAuthKey{}, LastSeen: &lastSeen, Expiry: &expire, diff --git a/hscontrol/types/node.go b/hscontrol/types/node.go index b0afe99d..7a5756ae 100644 --- a/hscontrol/types/node.go +++ b/hscontrol/types/node.go @@ -108,20 +108,20 @@ type Node struct { // parts of headscale. GivenName string `gorm:"type:varchar(63);unique_index"` UserID uint - User User `gorm:"foreignKey:UserID"` + User User `gorm:"constraint:OnDelete:CASCADE;"` RegisterMethod string ForcedTags StringList // TODO(kradalby): This seems like irrelevant information? - AuthKeyID uint - AuthKey *PreAuthKey + AuthKeyID *uint `sql:"DEFAULT:NULL"` + AuthKey *PreAuthKey `gorm:"constraint:OnDelete:SET NULL;"` LastSeen *time.Time Expiry *time.Time - Routes []Route + Routes []Route `gorm:"constraint:OnDelete:CASCADE;"` CreatedAt time.Time UpdatedAt time.Time diff --git a/hscontrol/types/preauth_key.go b/hscontrol/types/preauth_key.go index 0d8c9cff..8b02569a 100644 --- a/hscontrol/types/preauth_key.go +++ b/hscontrol/types/preauth_key.go @@ -14,11 +14,11 @@ type PreAuthKey struct { ID uint64 `gorm:"primary_key"` Key string UserID uint - User User + User User `gorm:"constraint:OnDelete:CASCADE;"` Reusable bool - Ephemeral bool `gorm:"default:false"` - Used bool `gorm:"default:false"` - ACLTags []PreAuthKeyACLTag + Ephemeral bool `gorm:"default:false"` + Used bool `gorm:"default:false"` + ACLTags []PreAuthKeyACLTag `gorm:"constraint:OnDelete:CASCADE;"` CreatedAt *time.Time Expiration *time.Time From a9763c96929b331fb2bbe79b614d78a496098dea Mon Sep 17 00:00:00 2001 From: Sandro <sandro.jaeckel@gmail.com> Date: Thu, 16 May 2024 02:40:30 +0200 Subject: [PATCH 062/145] Initialize log config earlier to prevent trace messages being printed early on (#1939) like TRC DNS configuration loaded dns_config={....} --- cmd/headscale/cli/root.go | 2 -- hscontrol/types/config.go | 9 ++++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/cmd/headscale/cli/root.go b/cmd/headscale/cli/root.go index 72c72a20..b0d9500e 100644 --- a/cmd/headscale/cli/root.go +++ b/cmd/headscale/cli/root.go @@ -56,8 +56,6 @@ func initConfig() { machineOutput := HasMachineOutputFlag() - zerolog.SetGlobalLevel(cfg.Log.Level) - // If the user has requested a "node" readable format, // then disable login so the output remains valid. if machineOutput { diff --git a/hscontrol/types/config.go b/hscontrol/types/config.go index fa3a64c6..bd0bfeac 100644 --- a/hscontrol/types/config.go +++ b/hscontrol/types/config.go @@ -640,6 +640,9 @@ func GetHeadscaleConfig() (*Config, error) { }, nil } + logConfig := GetLogConfig() + zerolog.SetGlobalLevel(logConfig.Level) + prefix4, err := PrefixV4() if err != nil { return nil, err @@ -667,7 +670,7 @@ func GetHeadscaleConfig() (*Config, error) { dnsConfig, baseDomain := GetDNSConfig() derpConfig := GetDERPConfig() - logConfig := GetLogTailConfig() + logTailConfig := GetLogTailConfig() randomizeClientPort := viper.GetBool("randomize_client_port") oidcClientSecret := viper.GetString("oidc.client_secret") @@ -749,7 +752,7 @@ func GetHeadscaleConfig() (*Config, error) { UseExpiryFromToken: viper.GetBool("oidc.use_expiry_from_token"), }, - LogTail: logConfig, + LogTail: logTailConfig, RandomizeClientPort: randomizeClientPort, ACL: GetACLConfig(), @@ -761,7 +764,7 @@ func GetHeadscaleConfig() (*Config, error) { Insecure: viper.GetBool("cli.insecure"), }, - Log: GetLogConfig(), + Log: logConfig, // TODO(kradalby): Document these settings when more stable Tuning: Tuning{ From 151f224a98892ce947472f3b070020ad6ba1a378 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 May 2024 01:22:02 +0000 Subject: [PATCH 063/145] Bump golang.org/x/net from 0.22.0 to 0.23.0 (#1943) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.22.0 to 0.23.0. - [Commits](https://github.com/golang/net/compare/v0.22.0...v0.23.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 488b60f8..594128ea 100644 --- a/go.mod +++ b/go.mod @@ -38,7 +38,7 @@ require ( go4.org/netipx v0.0.0-20231129151722-fdeea329fbba golang.org/x/crypto v0.21.0 golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 - golang.org/x/net v0.22.0 + golang.org/x/net v0.23.0 golang.org/x/oauth2 v0.17.0 golang.org/x/sync v0.6.0 google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014 diff --git a/go.sum b/go.sum index 7f6cad4e..985a8ab2 100644 --- a/go.sum +++ b/go.sum @@ -526,8 +526,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v 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.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ= golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= From fd4f921281705bb0b1ae3575b06fba94d339fc79 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 May 2024 02:15:41 +0000 Subject: [PATCH 064/145] Bump google.golang.org/protobuf from 1.32.0 to 1.33.0 (#1944) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 594128ea..1cf3d32b 100644 --- a/go.mod +++ b/go.mod @@ -43,7 +43,7 @@ require ( golang.org/x/sync v0.6.0 google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014 google.golang.org/grpc v1.61.0 - google.golang.org/protobuf v1.32.0 + google.golang.org/protobuf v1.33.0 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/postgres v1.5.4 diff --git a/go.sum b/go.sum index 985a8ab2..8d2bfe4f 100644 --- a/go.sum +++ b/go.sum @@ -635,8 +635,8 @@ google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0= google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= 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.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= -google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 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= From fff229f4f6b23ec2b6aa014a20446a72545ce98d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 May 2024 03:01:18 +0000 Subject: [PATCH 065/145] Bump github.com/jackc/pgx/v5 from 5.5.3 to 5.5.4 (#1946) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1cf3d32b..ee6db81c 100644 --- a/go.mod +++ b/go.mod @@ -119,7 +119,7 @@ require ( github.com/insomniacslk/dhcp v0.0.0-20240129002554-15c9b8791914 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect - github.com/jackc/pgx/v5 v5.5.3 // indirect + github.com/jackc/pgx/v5 v5.5.4 // 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 diff --git a/go.sum b/go.sum index 8d2bfe4f..0f5d029a 100644 --- a/go.sum +++ b/go.sum @@ -237,8 +237,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.5.3 h1:Ces6/M3wbDXYpM8JyyPD57ivTtJACFZJd885pdIaV2s= -github.com/jackc/pgx/v5 v5.5.3/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8= +github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= 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/jagottsicher/termcolor v1.0.2 h1:fo0c51pQSuLBN1+yVX2ZE+hE+P7ULb/TY8eRowJnrsM= From faa57ddc289131b777651736a26ed43d44924eb5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 May 2024 03:02:12 +0000 Subject: [PATCH 066/145] Bump github.com/go-jose/go-jose/v3 from 3.0.1 to 3.0.3 (#1945) --- go.mod | 2 +- go.sum | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index ee6db81c..b1bb9489 100644 --- a/go.mod +++ b/go.mod @@ -94,7 +94,7 @@ require ( github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.5.0 // indirect github.com/glebarez/go-sqlite v1.22.0 // indirect - github.com/go-jose/go-jose/v3 v3.0.1 // indirect + github.com/go-jose/go-jose/v3 v3.0.3 // 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 diff --git a/go.sum b/go.sum index 0f5d029a..a0de14f8 100644 --- a/go.sum +++ b/go.sum @@ -151,8 +151,8 @@ github.com/glebarez/sqlite v1.10.0 h1:u4gt8y7OND/cCei/NMHmfbLxF6xP2wgKcT/BJf2pYk 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-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= +github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-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-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= @@ -183,10 +183,10 @@ github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu 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.5.0/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.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= @@ -490,11 +490,11 @@ go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wus go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -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-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.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -526,6 +526,7 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v 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.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -570,7 +571,9 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -578,6 +581,8 @@ golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9sn 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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 30986c29cd211a066744b5fcbca8535057489683 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 May 2024 04:42:53 +0000 Subject: [PATCH 067/145] Bump github.com/docker/docker (#1947) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b1bb9489..3640ed40 100644 --- a/go.mod +++ b/go.mod @@ -86,7 +86,7 @@ require ( github.com/dblohm7/wingoes v0.0.0-20240123200102-b75a8a7d7eb0 // indirect github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e // indirect github.com/docker/cli v25.0.3+incompatible // indirect - github.com/docker/docker v25.0.3+incompatible // indirect + github.com/docker/docker v25.0.5+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect diff --git a/go.sum b/go.sum index a0de14f8..b4069a6d 100644 --- a/go.sum +++ b/go.sum @@ -123,8 +123,8 @@ github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yez github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A= github.com/docker/cli v25.0.3+incompatible h1:KLeNs7zws74oFuVhgZQ5ONGZiXUUdgsdy6/EsX/6284= github.com/docker/cli v25.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v25.0.3+incompatible h1:D5fy/lYmY7bvZa0XTZ5/UJPljor41F+vdyJG5luQLfQ= -github.com/docker/docker v25.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v25.0.5+incompatible h1:UmQydMduGkrD5nQde1mecF/YnSbTOaPeFIeP5C4W+DE= +github.com/docker/docker v25.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= From 723a0408a3f4411f4320d87039887f93f66b7769 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 16 May 2024 14:01:34 +0000 Subject: [PATCH 068/145] flake.lock: Update (#1897) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 13f9133e..ffa1f931 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1712883908, - "narHash": "sha256-icE1IJE9fHcbDfJ0+qWoDdcBXUoZCcIJxME4lMHwvSM=", + "lastModified": 1715774670, + "narHash": "sha256-iJYnKMtLi5u6hZhJm94cRNSDG5Rz6ZzIkGbhPFtDRm0=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "a0c9e3aee1000ac2bfb0e5b98c94c946a5d180a9", + "rev": "b3fcfcfabd01b947a1e4f36622bbffa3985bdac6", "type": "github" }, "original": { From 5ad0aa44cb3caa3d76ac3bfa4469c7f84a813f55 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby <kristoffer@dalby.cc> Date: Fri, 17 May 2024 08:58:33 -0400 Subject: [PATCH 069/145] update tailscale go dep (#1948) * update tailscale go dep Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * update gorm go dep Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * update grpc go dep Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * update golang.org go dep Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * update rest of go dep Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> --------- Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> --- flake.nix | 2 +- go.mod | 112 ++++++------ go.sum | 302 ++++++++++++++++++--------------- hscontrol/auth.go | 62 +++---- hscontrol/notifier/notifier.go | 4 - 5 files changed, 250 insertions(+), 232 deletions(-) diff --git a/flake.nix b/flake.nix index bf11c898..f2046dae 100644 --- a/flake.nix +++ b/flake.nix @@ -31,7 +31,7 @@ # When updating go.mod or go.sum, a new sha will need to be calculated, # update this if you have a mismatch after doing a change to thos files. - vendorHash = "sha256-HGu/OCtjzPeBki5FSL6v1XivCJ30eqj9rL0x7ZVv1TM="; + vendorHash = "sha256-wXfKeiJaGe6ahOsONrQhvbuMN8flQ13b0ZjxdbFs1e8="; subPackages = ["cmd/headscale"]; diff --git a/go.mod b/go.mod index 3640ed40..0e0e12af 100644 --- a/go.mod +++ b/go.mod @@ -1,54 +1,54 @@ module github.com/juanfont/headscale -go 1.22 +go 1.22.0 -toolchain go1.22.0 +toolchain go1.22.2 require ( github.com/AlecAivazis/survey/v2 v2.3.7 - github.com/coreos/go-oidc/v3 v3.9.0 + github.com/coreos/go-oidc/v3 v3.10.0 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/deckarep/golang-set/v2 v2.6.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/glebarez/sqlite v1.11.0 + github.com/go-gormigrate/gormigrate/v2 v2.1.2 + github.com/gofrs/uuid/v5 v5.2.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.19.1 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 github.com/jagottsicher/termcolor v1.0.2 - github.com/klauspost/compress v1.17.6 - github.com/oauth2-proxy/mockoidc v0.0.0-20220308204021-b9169deeb282 + github.com/klauspost/compress v1.17.8 + github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 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.18.0 github.com/prometheus/common v0.46.0 - github.com/pterm/pterm v0.12.78 - github.com/puzpuzpuz/xsync/v3 v3.0.2 + github.com/pterm/pterm v0.12.79 + github.com/puzpuzpuz/xsync/v3 v3.1.0 github.com/rs/zerolog v1.32.0 github.com/samber/lo v1.39.0 github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.18.2 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a - github.com/tailscale/tailsql v0.0.0-20231216172832-51483e0c711b + github.com/tailscale/tailsql v0.0.0-20240418235827-820559f382c1 github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e go4.org/netipx v0.0.0-20231129151722-fdeea329fbba - golang.org/x/crypto v0.21.0 - golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 - golang.org/x/net v0.23.0 - golang.org/x/oauth2 v0.17.0 - golang.org/x/sync v0.6.0 - google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014 - google.golang.org/grpc v1.61.0 - google.golang.org/protobuf v1.33.0 + golang.org/x/crypto v0.23.0 + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 + golang.org/x/net v0.25.0 + golang.org/x/oauth2 v0.20.0 + golang.org/x/sync v0.7.0 + google.golang.org/genproto/googleapis/api v0.0.0-20240515191416-fc5f0ca64291 + google.golang.org/grpc v1.64.0 + google.golang.org/protobuf v1.34.1 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c gopkg.in/yaml.v3 v3.0.1 - gorm.io/driver/postgres v1.5.4 - gorm.io/gorm v1.25.5 - tailscale.com v1.58.2 + gorm.io/driver/postgres v1.5.7 + gorm.io/gorm v1.25.10 + tailscale.com v1.66.3 ) require ( @@ -58,7 +58,7 @@ require ( dario.cat/mergo v1.0.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect - github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/Microsoft/go-winio v0.6.2 // 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-20231016080023-1a75b4708caa // indirect @@ -77,35 +77,39 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect github.com/aws/smithy-go v1.19.0 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/bits-and-blooms/bitset v1.13.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/containerd/console v1.0.4 // 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/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect + github.com/creachadair/mds v0.14.5 // indirect github.com/dblohm7/wingoes v0.0.0-20240123200102-b75a8a7d7eb0 // indirect github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e // indirect - github.com/docker/cli v25.0.3+incompatible // indirect - github.com/docker/docker v25.0.5+incompatible // indirect + github.com/docker/cli v26.1.3+incompatible // indirect + github.com/docker/docker v26.1.3+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/felixge/fgprof v0.9.3 // indirect + github.com/felixge/fgprof v0.9.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.5.0 // indirect + github.com/gaissmai/bart v0.4.1 // indirect github.com/glebarez/go-sqlite v1.22.0 // indirect github.com/go-jose/go-jose/v3 v3.0.3 // indirect + github.com/go-jose/go-jose/v4 v4.0.1 // indirect + github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 // 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-jwt/jwt/v5 v5.2.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/protobuf v1.5.4 // 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-20240207164012-fb44976bdcd5 // indirect + github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect + github.com/google/pprof v0.0.0-20240509144519-723abb6459b7 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gookit/color v1.5.4 // indirect @@ -119,7 +123,7 @@ require ( github.com/insomniacslk/dhcp v0.0.0-20240129002554-15c9b8791914 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect - github.com/jackc/pgx/v5 v5.5.4 // indirect + github.com/jackc/pgx/v5 v5.5.5 // 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 @@ -144,12 +148,13 @@ require ( github.com/miekg/dns v1.1.58 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/term v0.5.0 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0-rc6 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect github.com/opencontainers/runc v1.1.12 // indirect - github.com/pelletier/go-toml/v2 v2.1.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect @@ -166,16 +171,17 @@ require ( 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/stretchr/objx v0.5.1 // 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-20240108194725-7ce1f622c780 // 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-20240102233422-ba738f8ab5a0 // indirect - github.com/tailscale/web-client-prebuilt v0.0.0-20240111230031-5ca22df9e6e7 // indirect - github.com/tailscale/wireguard-go v0.0.0-20231121184858-cc193a0b3272 // indirect + github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 // indirect + github.com/tailscale/setec v0.0.0-20240314234648-9da8e7407257 // indirect + github.com/tailscale/squibble v0.0.0-20240418235321-9ee0eeb78185 // indirect + github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 // indirect + github.com/tailscale/wireguard-go v0.0.0-20240429185444-03c5a0ccf754 // indirect github.com/tcnksm/go-httpstat v0.2.0 // indirect github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e // indirect github.com/vishvananda/netlink v1.2.1-beta.2 // indirect @@ -187,25 +193,21 @@ require ( github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.uber.org/multierr v1.11.0 // indirect go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect - golang.org/x/mod v0.16.0 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/term v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/term v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.19.0 // indirect + golang.org/x/tools v0.21.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.8 // indirect - google.golang.org/genproto v0.0.0-20240205150955-31a09d347014 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240205150955-31a09d347014 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 // 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 - gvisor.dev/gvisor v0.0.0-20230928000133-4fe30062272c // indirect - inet.af/peercred v0.0.0-20210906144145-0893ea02156a // indirect - modernc.org/libc v1.49.3 // indirect + gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3 // indirect + modernc.org/libc v1.50.6 // indirect modernc.org/mathutil v1.6.0 // indirect modernc.org/memory v1.8.0 // indirect - modernc.org/sqlite v1.28.0 // indirect + modernc.org/sqlite v1.29.9 // indirect nhooyr.io/websocket v1.8.10 // indirect ) diff --git a/go.sum b/go.sum index b4069a6d..309d14e7 100644 --- a/go.sum +++ b/go.sum @@ -29,8 +29,8 @@ github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/ github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4= github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= @@ -83,14 +83,22 @@ github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6 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.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= +github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/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/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= +github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= +github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= 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= @@ -100,17 +108,18 @@ github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= 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.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo= -github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0= +github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= +github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU= +github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac= 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/creachadair/mds v0.14.5 h1:2amuO4yCbQkaAyDoLO5iCbwbTRQZz4EpRhOejQbf4+8= +github.com/creachadair/mds v0.14.5/go.mod h1:4vrFYUzTXMJpMBU+OA292I6IUxKWCCfZkgXg+/kBZMo= 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/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= +github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -121,38 +130,49 @@ github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80N github.com/deckarep/golang-set/v2 v2.6.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 v25.0.3+incompatible h1:KLeNs7zws74oFuVhgZQ5ONGZiXUUdgsdy6/EsX/6284= -github.com/docker/cli v25.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v25.0.5+incompatible h1:UmQydMduGkrD5nQde1mecF/YnSbTOaPeFIeP5C4W+DE= -github.com/docker/docker v25.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= +github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= +github.com/docker/cli v26.1.3+incompatible h1:bUpXT/N0kDE3VUHI2r5VMsYQgi38kYuoC0oL9yt3lqc= +github.com/docker/cli v26.1.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v26.1.3+incompatible h1:lLCzRbrVZrljpVNobJu1J2FHk8V0s4BawoZippkc+xo= +github.com/docker/docker v26.1.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dsnet/try v0.0.3 h1:ptR59SsrcFUYbT/FhAbKTV6iLkeD6O18qfIWRml2fqI= +github.com/dsnet/try v0.0.3/go.mod h1:WBM8tRpUmnXXhY1U6/S8dt6UWdHTQ7y8A5YSkRCkq40= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 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/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/felixge/fgprof v0.9.4 h1:ocDNwMFlnA0NU0zSB3I52xkO4sFXk80VK9lXjLClu88= +github.com/felixge/fgprof v0.9.4/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE= github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= +github.com/gaissmai/bart v0.4.1 h1:G1t58voWkNmT47lBDawH5QhtTDsdqRIO+ftq5x4P9Ls= +github.com/gaissmai/bart v0.4.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg= 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.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= -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/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= +github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +github.com/go-gormigrate/gormigrate/v2 v2.1.2 h1:F/d1hpHbRAvKezziV2CC5KUE82cVe9zTgHSBoOOZ4CY= +github.com/go-gormigrate/gormigrate/v2 v2.1.2/go.mod h1:9nHVX6z3FCMCQPA7PThGcA55t22yKQfK/Dnsf5i7hUo= github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= +github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U= +github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= +github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 h1:ymLjT4f35nQbASLnvxEde4XOBL+Sn7rFuV+FOJqkljg= +github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0/go.mod h1:6daplAwHHGbUGib4990V3Il26O0OC4aRyvewaaAihaA= github.com/go-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-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= @@ -160,15 +180,18 @@ github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW 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.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= github.com/godbus/dbus/v5 v5.0.4/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/gofrs/uuid/v5 v5.2.0 h1:qw1GMx6/y8vhVsx626ImfKMuS5CvJmhIKKtuyvfajMM= +github.com/gofrs/uuid/v5 v5.2.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -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-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 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= @@ -176,16 +199,12 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb github.com/golang/protobuf v1.2.0/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.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/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 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.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.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= @@ -195,11 +214,12 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -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/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI= +github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= -github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5 h1:E/LAvt58di64hlYjx7AsNS6C/ysHWYo+2qPCZKTQhRo= -github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/pprof v0.0.0-20240509144519-723abb6459b7 h1:velgFPYr1X9TDwLIfkV7fWqsFlf7TeP11M/7kPd/dVI= +github.com/google/pprof v0.0.0-20240509144519-723abb6459b7/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -216,10 +236,13 @@ github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kX github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= 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.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/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.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= @@ -227,6 +250,7 @@ github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3s 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-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= 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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -237,12 +261,14 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8= -github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= 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/jagottsicher/termcolor v1.0.2 h1:fo0c51pQSuLBN1+yVX2ZE+hE+P7ULb/TY8eRowJnrsM= github.com/jagottsicher/termcolor v1.0.2/go.mod h1:RcH8uFwF/0wbEdQmi83rjmlJ+QOKdMSE9Rc1BEB7zFo= +github.com/jellydator/ttlcache/v3 v3.1.0 h1:0gPFG0IHHP6xyUyXq+JaD8fwkDCqgqwohXNJBcYE71g= +github.com/jellydator/ttlcache/v3 v3.1.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4= 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.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= @@ -251,6 +277,7 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y 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/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0WwEDBeqxKwb7WB62QX8bvZ/FJnVXIfk= github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8= @@ -260,13 +287,13 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= -github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= +github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 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/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 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= @@ -280,12 +307,14 @@ 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/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= 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.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/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= @@ -314,27 +343,30 @@ 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/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 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/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= -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/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 h1:9bCMuD3TcnjeqjPT2gSlha4asp8NvgcFRYExCaikCxk= +github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25/go.mod h1:eDjgYHYDJbPLBLsyZ6qRaugP0mX8vePOhZ5id1fdzJw= 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-rc6 h1:XDqvyKsJEbRtATzkgItUqBA7QHk58yxX1Ov9HERHNqU= -github.com/opencontainers/image-spec v1.1.0-rc6/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opencontainers/runc v1.1.12 h1:BOIssBaW1La0/qbNZHXOOa71dZfZEQOzW7dqQf3phss= github.com/opencontainers/runc v1.1.12/go.mod h1:S+lQwSfncpBha7XTy/5lBwWgm5+y5Ma/O44Ekby9FK8= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/ory/dockertest/v3 v3.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.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= -github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 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= @@ -367,10 +399,10 @@ 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.78 h1:QTWKaIAa4B32GKwqVXtu9m1DUMgWw3VRljMkMevX+b8= -github.com/pterm/pterm v0.12.78/go.mod h1:1v/gzOF1N0FsjbgTHZ1wVycRkKiatFvJSJC4IGaQAAo= -github.com/puzpuzpuz/xsync/v3 v3.0.2 h1:3yESHrRFYr6xzkz61LLkvNiPFXxJEAABanTQpKbAaew= -github.com/puzpuzpuz/xsync/v3 v3.0.2/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/pterm/pterm v0.12.79 h1:lH3yrYMhdpeqX9y5Ep1u7DejyHy7NSQg9qrBjF9dFT4= +github.com/pterm/pterm v0.12.79/go.mod h1:1v/gzOF1N0FsjbgTHZ1wVycRkKiatFvJSJC4IGaQAAo= +github.com/puzpuzpuz/xsync/v3 v3.1.0 h1:EewKT7/LNac5SLiEblJeUu8z5eERHrmRLnMQL2d7qX4= +github.com/puzpuzpuz/xsync/v3 v3.1.0/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= @@ -413,8 +445,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ 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= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0= -github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 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= @@ -422,9 +454,9 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -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/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/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= @@ -439,14 +471,22 @@ github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29X 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-20240102233422-ba738f8ab5a0 h1:0bcWsoeSBbY3XWRS1F8yp/g343E5TQMakwy5cxJS+ZU= -github.com/tailscale/setec v0.0.0-20240102233422-ba738f8ab5a0/go.mod h1:/8aqnX9aU8yubwQ2InR5mHi1OlfWQ8ei8Ea2eyLScOY= -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-20240111230031-5ca22df9e6e7 h1:xAgOVncJuuxkFZ2oXXDKFTH4HDdFYSZRYdA6oMrCewg= -github.com/tailscale/web-client-prebuilt v0.0.0-20240111230031-5ca22df9e6e7/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/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 h1:Gz0rz40FvFVLTBk/K8UNAenb36EbDSnh+q7Z9ldcC8w= +github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4/go.mod h1:phI29ccmHQBc+wvroosENp1IF9195449VDnFDhJ4rJU= +github.com/tailscale/setec v0.0.0-20240314234648-9da8e7407257 h1:6WsbDYsikRNmmbfZoRoyIEA9tfl0aspPAE0t7nBj2B4= +github.com/tailscale/setec v0.0.0-20240314234648-9da8e7407257/go.mod h1:hrq01/0LUDZf4mMkcZ7Ovmy33jvCi4RpESpb9kPxV6E= +github.com/tailscale/squibble v0.0.0-20240418235321-9ee0eeb78185 h1:zT+qB+2Ghulj50d5Wq6h6vQYqD2sPdhy4FF6+FHedVE= +github.com/tailscale/squibble v0.0.0-20240418235321-9ee0eeb78185/go.mod h1:LoIjI6z/6efr9ebISQ5l2vjQmjc8QJrAYZdy3Ec3sVs= +github.com/tailscale/tailsql v0.0.0-20240418235827-820559f382c1 h1:wmsnxEEuRlgK7Bhdkmm0JGrjjc0JoHZThLLo0WXXbLs= +github.com/tailscale/tailsql v0.0.0-20240418235827-820559f382c1/go.mod h1:XN193fbz9RR/5stlWPMMIZR+TTa1BUkDJm5Azwzxwgw= +github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 h1:tdUdyPqJ0C97SJfjB9tW6EylTtreyee9C44de+UBG0g= +github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ= +github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M= +github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y= +github.com/tailscale/wireguard-go v0.0.0-20240429185444-03c5a0ccf754 h1:iazWjqVHE6CbNam7WXRhi33Qad5o7a8LVYgVoILpZdI= +github.com/tailscale/wireguard-go v0.0.0-20240429185444-03c5a0ccf754/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4= +github.com/tailscale/xnet v0.0.0-20240117122442-62b9a7c569f9 h1:81P7rjnikHKTJ75EkjppvbwUfKHDHYk6LJpO5PZy8pA= +github.com/tailscale/xnet v0.0.0-20240117122442-62b9a7c569f9/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg= github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA= github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk= github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0= @@ -455,8 +495,8 @@ github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e h1:IWllFTiDjjLIf2 github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e/go.mod h1:d7u6HkTYKSv5m6MCKkOQlHwaShTMl3HjqSGW3XtVhXM= github.com/tink-crypto/tink-go/v2 v2.1.0 h1:QXFBguwMwTIaU17EgZpEJWsUSc60b1BAGTzBIoMdmok= github.com/tink-crypto/tink-go/v2 v2.1.0/go.mod h1:y1TnYFt1i2eZVfx4OGc+C+EMp4CoKWAw2VSEuoicHHI= -github.com/u-root/u-root v0.11.0 h1:6gCZLOeRyevw7gbTwMj3fKxnr9+yHFlgF3N7udUVNO8= -github.com/u-root/u-root v0.11.0/go.mod h1:DBkDtiZyONk9hzVEdB/PWI9B4TxDkElWlVTHseglrZY= +github.com/u-root/u-root v0.12.0 h1:K0AuBFriwr0w/PGS3HawiAw89e3+MU7ks80GpghAsNs= +github.com/u-root/u-root v0.12.0/go.mod h1:FYjTOh4IkIZHhjsd17lb8nYW6udgXdJhG1c0r6u0arI= github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e h1:BA9O3BmlTmpjbvajAwzWx4Wo2TRVdpPXZEeemGQcajw= github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs= @@ -493,17 +533,16 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk 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-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.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 h1:/RIbNt/Zr7rVhIkQhooTxCxFcdWLGIKnZA4IXNFSrvo= -golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= -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/image v0.12.0 h1:w13vZbU4o5rKOFFR8y7M+c4A5jXDC0uXTdHYRP8X2DQ= -golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a h1:8qmSSA8Gz/1kTrCe0nqR0R3Gb/NDhykzWw2q2mWZydM= +golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= +golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -512,8 +551,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= -golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.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-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -523,15 +562,14 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 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.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ= -golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= +golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= +golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -540,8 +578,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.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= @@ -552,14 +590,13 @@ golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7w 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-20210124154548-22da62e12c0c/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-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-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-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -574,8 +611,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.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-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -583,18 +620,17 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.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.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.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/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.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= @@ -608,8 +644,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= -golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= +golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= +golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 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= @@ -620,28 +656,22 @@ golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI= 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.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-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20240205150955-31a09d347014 h1:g/4bk7P6TPMkAUbUhquq98xey1slwvuVJPosdBqYJlU= -google.golang.org/genproto v0.0.0-20240205150955-31a09d347014/go.mod h1:xEgQu1e4stdSSsxPDK8Azkrk/ECl5HvdPf6nbZrTS5M= -google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014 h1:x9PwdEgd11LgK+orcck69WVRo7DezSO4VUMPI4xpc8A= -google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014/go.mod h1:rbHMSEDyoYX62nRVLOCc4Qt1HbsdytAYoVwgjiOhF3I= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240205150955-31a09d347014 h1:FSL3lRCkhaPFxqi0s9o+V4UI2WTzAVOvkgbd4kVV4Wg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240205150955-31a09d347014/go.mod h1:SaPjaZGWb0lPqs6Ittu0spdfrOArqji4ZdeP5IC/9N4= +google.golang.org/genproto/googleapis/api v0.0.0-20240515191416-fc5f0ca64291 h1:4HZJ3Xv1cmrJ+0aFo304Zn79ur1HMxptAE7aCPNLSqc= +google.golang.org/genproto/googleapis/api v0.0.0-20240515191416-fc5f0ca64291/go.mod h1:RGnPtTG7r4i8sPlNyDeikXF99hMM+hN6QMm4ooG9g2g= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 h1:AgADTJarZTBqgjiUzRgfaBchgYB3/WFTC80GPwsMcRI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0= -google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= -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.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= +google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 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= @@ -649,8 +679,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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= -gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -660,40 +688,32 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/driver/postgres v1.5.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= +gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM= +gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA= +gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= +gorm.io/gorm v1.25.10/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= +gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3 h1:/8/t5pz/mgdRXhYOIeqqYhFAQLE4DDGegc0Y4ZjyFJM= +gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3/go.mod h1:NQHVAzMwvZ+Qe3ElSiHmq9RUm1MdNHpUZ52fiEqvn+0= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/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.4.6 h1:oFEHCKeID7to/3autwsWfnuv69j3NsfcXbvJKuIcep8= -honnef.co/go/tools v0.4.6/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0= +honnef.co/go/tools v0.4.7 h1:9MDAWxMoSnB6QoSqiVr7P5mtkT9pOc1kSxchzPCnqJs= +honnef.co/go/tools v0.4.7/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= -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.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo= -lukechampine.com/uint128 v1.3.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/cc/v4 v4.20.0 h1:45Or8mQfbUqJOG9WaxvlFYOAQO0lQ5RvqBcFCXngjxk= -modernc.org/cc/v4 v4.20.0/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= -modernc.org/ccgo/v3 v3.17.0 h1:o3OmOqx4/OFnl4Vm3G8Bgmqxnvxnh0nbxeT5p/dWChA= -modernc.org/ccgo/v3 v3.17.0/go.mod h1:Sg3fwVpmLvCUTaqEUjiBDAvshIaKDB0RXaf+zgqFu8I= -modernc.org/ccgo/v4 v4.16.0 h1:ofwORa6vx2FMm0916/CkZjpFPSR70VwTjUCe2Eg5BnA= -modernc.org/ccgo/v4 v4.16.0/go.mod h1:dkNyWIjFrVIZ68DTo36vHK+6/ShBn4ysU61So6PIqCI= +modernc.org/cc/v4 v4.21.2 h1:dycHFB/jDc3IyacKipCNSDrjIC0Lm1hyoWOZTRR20Lk= +modernc.org/cc/v4 v4.21.2/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v4 v4.17.7 h1:+MG+Np7uYtsuPvtoH3KtZ1+pqNiJAOqqqVIxggE1iIo= +modernc.org/ccgo/v4 v4.17.7/go.mod h1:x87xuLLXuJv3Nn5ULTUqJn/HsTMMMiT1Eavo6rz1NiY= modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= -modernc.org/libc v1.49.3 h1:j2MRCRdwJI2ls/sGbeSk0t2bypOG/uvPZUsGQFDulqg= -modernc.org/libc v1.49.3/go.mod h1:yMZuGkn7pXbKfoT/M35gFJOAEdSKdxL0q64sF7KqCDo= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= +modernc.org/libc v1.50.6 h1:72NPEFMyKP01RJrKXS2eLXv35UklKqlJZ1b9P7gSo6I= +modernc.org/libc v1.50.6/go.mod h1:8lr2m1THY5Z3ikGyUc3JhLEQg1oaIBz/AQixw8/eksQ= 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.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= @@ -702,15 +722,15 @@ modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= -modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ= -modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0= +modernc.org/sqlite v1.29.9 h1:9RhNMklxJs+1596GNuAX+O/6040bvOwacTxuFcRuQow= +modernc.org/sqlite v1.29.9/go.mod h1:ItX2a1OVGgNsFh6Dv60JQvGfJfTPHPVpV6DF59akYOA= modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= 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.58.2 h1:5trkhh/fpUn7f6TUcGUQYJ0GokdNNfNrjh9ONJhoc5A= -tailscale.com v1.58.2/go.mod h1:faWR8XaXemnSKCDjHC7SAQzaagkUjA5x4jlLWiwxtuk= +software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= +software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= +tailscale.com v1.66.3 h1:jpWat+hiobTtCosSV/c8D6S/ubgROf/S59MaIBdM9pY= +tailscale.com v1.66.3/go.mod h1:99BIV4U3UPw36Sva04xK2ZsEpVRUkY9jCdEDSAhaNGM= diff --git a/hscontrol/auth.go b/hscontrol/auth.go index c4511db3..5ee925a6 100644 --- a/hscontrol/auth.go +++ b/hscontrol/auth.go @@ -62,18 +62,18 @@ func logAuthFunc( func (h *Headscale) handleRegister( writer http.ResponseWriter, req *http.Request, - registerRequest tailcfg.RegisterRequest, + regReq tailcfg.RegisterRequest, machineKey key.MachinePublic, ) { - logInfo, logTrace, logErr := logAuthFunc(registerRequest, machineKey) + logInfo, logTrace, logErr := logAuthFunc(regReq, machineKey) now := time.Now().UTC() logTrace("handleRegister called, looking up machine in DB") - node, err := h.db.GetNodeByAnyKey(machineKey, registerRequest.NodeKey, registerRequest.OldNodeKey) + node, err := h.db.GetNodeByAnyKey(machineKey, regReq.NodeKey, regReq.OldNodeKey) logTrace("handleRegister database lookup has returned") if errors.Is(err, gorm.ErrRecordNotFound) { // If the node has AuthKey set, handle registration via PreAuthKeys - if registerRequest.Auth.AuthKey != "" { - h.handleAuthKey(writer, registerRequest, machineKey) + if regReq.Auth != nil && regReq.Auth.AuthKey != "" { + h.handleAuthKey(writer, regReq, machineKey) return } @@ -86,7 +86,7 @@ func (h *Headscale) handleRegister( // This is not implemented yet, as it is no strictly required. The only side-effect // is that the client will hammer headscale with requests until it gets a // successful RegisterResponse. - if registerRequest.Followup != "" { + if regReq.Followup != "" { logTrace("register request is a followup") if _, ok := h.registrationCache.Get(machineKey.String()); ok { logTrace("Node is waiting for interactive login") @@ -95,7 +95,7 @@ func (h *Headscale) handleRegister( case <-req.Context().Done(): return case <-time.After(registrationHoldoff): - h.handleNewNode(writer, registerRequest, machineKey) + h.handleNewNode(writer, regReq, machineKey) return } @@ -106,7 +106,7 @@ func (h *Headscale) handleRegister( givenName, err := h.db.GenerateGivenName( machineKey, - registerRequest.Hostinfo.Hostname, + regReq.Hostinfo.Hostname, ) if err != nil { logErr(err, "Failed to generate given name for node") @@ -120,16 +120,16 @@ func (h *Headscale) handleRegister( // happens newNode := types.Node{ MachineKey: machineKey, - Hostname: registerRequest.Hostinfo.Hostname, + Hostname: regReq.Hostinfo.Hostname, GivenName: givenName, - NodeKey: registerRequest.NodeKey, + NodeKey: regReq.NodeKey, LastSeen: &now, Expiry: &time.Time{}, } - if !registerRequest.Expiry.IsZero() { + if !regReq.Expiry.IsZero() { logTrace("Non-zero expiry time requested") - newNode.Expiry = ®isterRequest.Expiry + newNode.Expiry = ®Req.Expiry } h.registrationCache.Set( @@ -138,7 +138,7 @@ func (h *Headscale) handleRegister( registerCacheExpiration, ) - h.handleNewNode(writer, registerRequest, machineKey) + h.handleNewNode(writer, regReq, machineKey) return } @@ -169,11 +169,11 @@ 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.String() == registerRequest.NodeKey.String() { + if node.NodeKey.String() == regReq.NodeKey.String() { // The client sends an Expiry in the past if the client is requesting to expire the key (aka logout) // https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L648 - if !registerRequest.Expiry.IsZero() && - registerRequest.Expiry.UTC().Before(now) { + if !regReq.Expiry.IsZero() && + regReq.Expiry.UTC().Before(now) { h.handleNodeLogOut(writer, *node, machineKey) return @@ -189,11 +189,11 @@ func (h *Headscale) handleRegister( } // The NodeKey we have matches OldNodeKey, which means this is a refresh after a key expiration - if node.NodeKey.String() == registerRequest.OldNodeKey.String() && + if node.NodeKey.String() == regReq.OldNodeKey.String() && !node.IsExpired() { h.handleNodeKeyRefresh( writer, - registerRequest, + regReq, *node, machineKey, ) @@ -202,11 +202,11 @@ func (h *Headscale) handleRegister( } // When logged out and reauthenticating with OIDC, the OldNodeKey is not passed, but the NodeKey has changed - if node.NodeKey.String() != registerRequest.NodeKey.String() && - registerRequest.OldNodeKey.IsZero() && !node.IsExpired() { + if node.NodeKey.String() != regReq.NodeKey.String() && + regReq.OldNodeKey.IsZero() && !node.IsExpired() { h.handleNodeKeyRefresh( writer, - registerRequest, + regReq, *node, machineKey, ) @@ -214,7 +214,7 @@ func (h *Headscale) handleRegister( return } - if registerRequest.Followup != "" { + if regReq.Followup != "" { select { case <-req.Context().Done(): return @@ -223,7 +223,7 @@ func (h *Headscale) handleRegister( } // The node has expired or it is logged out - h.handleNodeExpiredOrLoggedOut(writer, registerRequest, *node, machineKey) + h.handleNodeExpiredOrLoggedOut(writer, regReq, *node, machineKey) // TODO(juan): RegisterRequest includes an Expiry time, that we could optionally use node.Expiry = &time.Time{} @@ -232,7 +232,7 @@ 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 = registerRequest.NodeKey + node.NodeKey = regReq.NodeKey h.registrationCache.Set( machineKey.String(), *node, @@ -689,14 +689,14 @@ func (h *Headscale) handleNodeKeyRefresh( func (h *Headscale) handleNodeExpiredOrLoggedOut( writer http.ResponseWriter, - registerRequest tailcfg.RegisterRequest, + regReq tailcfg.RegisterRequest, node types.Node, machineKey key.MachinePublic, ) { resp := tailcfg.RegisterResponse{} - if registerRequest.Auth.AuthKey != "" { - h.handleAuthKey(writer, registerRequest, machineKey) + if regReq.Auth != nil && regReq.Auth.AuthKey != "" { + h.handleAuthKey(writer, regReq, machineKey) return } @@ -706,8 +706,8 @@ func (h *Headscale) handleNodeExpiredOrLoggedOut( Caller(). Str("node", node.Hostname). Str("machine_key", machineKey.ShortString()). - Str("node_key", registerRequest.NodeKey.ShortString()). - Str("node_key_old", registerRequest.OldNodeKey.ShortString()). + Str("node_key", regReq.NodeKey.ShortString()). + Str("node_key_old", regReq.OldNodeKey.ShortString()). Msg("Node registration has expired or logged out. Sending a auth url to register") if h.oauth2Config != nil { @@ -744,8 +744,8 @@ func (h *Headscale) handleNodeExpiredOrLoggedOut( log.Trace(). Caller(). Str("machine_key", machineKey.ShortString()). - Str("node_key", registerRequest.NodeKey.ShortString()). - Str("node_key_old", registerRequest.OldNodeKey.ShortString()). + Str("node_key", regReq.NodeKey.ShortString()). + Str("node_key_old", regReq.OldNodeKey.ShortString()). Str("node", node.Hostname). Msg("Node logged out. Sent AuthURL for reauthentication") } diff --git a/hscontrol/notifier/notifier.go b/hscontrol/notifier/notifier.go index d5ef89f5..339a56f1 100644 --- a/hscontrol/notifier/notifier.go +++ b/hscontrol/notifier/notifier.go @@ -355,8 +355,4 @@ func overwritePatch(currPatch, newPatch *tailcfg.PeerChange) { if newPatch.KeyExpiry != nil { currPatch.KeyExpiry = newPatch.KeyExpiry } - - if newPatch.Capabilities != nil { - currPatch.Capabilities = newPatch.Capabilities - } } From 2dc62e981e1ace5c247f9a84e79b003d1deeea5d Mon Sep 17 00:00:00 2001 From: ohdearaugustin <ohdearaugustin@users.noreply.github.com> Date: Sun, 19 May 2024 11:17:37 +0200 Subject: [PATCH 070/145] move debug image to distroless (#1950) --- .goreleaser.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index b1df31c7..4e91c74d 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -135,7 +135,7 @@ kos: - id: ghcr-debug repository: ghcr.io/juanfont/headscale bare: true - base_image: "debian:12" + base_image: gcr.io/distroless/base-debian12:debug build: headscale main: ./cmd/headscale env: @@ -160,7 +160,7 @@ kos: - id: dockerhub-debug build: headscale - base_image: "debian:12" + base_image: gcr.io/distroless/base-debian12:debug repository: headscale/headscale bare: true platforms: From 8185a70dc785883c258f6bcca5f635182905d4b2 Mon Sep 17 00:00:00 2001 From: Stefan Majer <stefan.majer@f-i-ts.de> Date: Sun, 19 May 2024 23:49:27 +0200 Subject: [PATCH 071/145] Fix typos (#1860) * Fix typos * trigger GitHub actions * remove kdiff3 orig files * fix unicode * remove unnecessary function call * remove unnecessary comment * remove unnecessary comment --------- Co-authored-by: ohdearaugustin <ohdearaugustin@users.noreply.github.com> --- CHANGELOG.md | 14 +++++++------- README.md | 2 +- config-example.yaml | 4 ++-- docs/exit-node.md | 2 +- docs/faq.md | 2 +- docs/proposals/001-acls.md | 8 ++++---- docs/remote-cli.md | 6 +++--- docs/reverse-proxy.md | 2 +- docs/running-headscale-openbsd.md | 4 ++-- flake.nix | 2 +- hscontrol/app.go | 2 +- hscontrol/db/node.go | 4 ++-- hscontrol/db/node_test.go | 2 +- hscontrol/db/preauth_keys.go | 2 +- hscontrol/derp/server/derp_server.go | 2 +- hscontrol/policy/acls_test.go | 18 +++++++++--------- integration/general_test.go | 6 +++--- integration/scenario.go | 2 +- integration/utils.go | 2 +- 19 files changed, 43 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8e15c0c..03516fd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,7 @@ after improving the test harness as part of adopting [#1460](https://github.com/ - Code reorganisation, a lot of code has moved, please review the following PRs accordingly [#1473](https://github.com/juanfont/headscale/pull/1473) - Change the structure of database configuration, see [config-example.yaml](./config-example.yaml) for the new structure. [#1700](https://github.com/juanfont/headscale/pull/1700) - Old structure has been remove and the configuration _must_ be converted. - - Adds additional configuration for PostgreSQL for setting max open, idle conection and idle connection lifetime. + - Adds additional configuration for PostgreSQL for setting max open, idle connection and idle connection lifetime. - 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.38 @@ -70,7 +70,7 @@ after improving the test harness as part of adopting [#1460](https://github.com/ ### Changes - Add environment flags to enable pprof (profiling) [#1382](https://github.com/juanfont/headscale/pull/1382) - - Profiles are continously generated in our integration tests. + - Profiles are continuously generated in our integration tests. - Fix systemd service file location in `.deb` packages [#1391](https://github.com/juanfont/headscale/pull/1391) - Improvements on Noise implementation [#1379](https://github.com/juanfont/headscale/pull/1379) - Replace node filter logic, ensuring nodes with access can see eachother [#1381](https://github.com/juanfont/headscale/pull/1381) @@ -161,7 +161,7 @@ after improving the test harness as part of adopting [#1460](https://github.com/ - SSH ACLs status: - Support `accept` and `check` (SSH can be enabled and used for connecting and authentication) - Rejecting connections **are not supported**, meaning that if you enable SSH, then assume that _all_ `ssh` connections **will be allowed**. - - If you decied to try this feature, please carefully managed permissions by blocking port `22` with regular ACLs or do _not_ set `--ssh` on your clients. + - If you decided to try this feature, please carefully managed permissions by blocking port `22` with regular ACLs or do _not_ set `--ssh` on your clients. - We are currently improving our testing of the SSH ACLs, help us get an overview by testing and giving feedback. - This feature should be considered dangerous and it is disabled by default. Enable by setting `HEADSCALE_EXPERIMENTAL_FEATURE_SSH=1`. @@ -211,7 +211,7 @@ after improving the test harness as part of adopting [#1460](https://github.com/ ### Changes - Updated dependencies (including the library that lacked armhf support) [#722](https://github.com/juanfont/headscale/pull/722) -- Fix missing group expansion in function `excludeCorretlyTaggedNodes` [#563](https://github.com/juanfont/headscale/issues/563) +- Fix missing group expansion in function `excludeCorrectlyTaggedNodes` [#563](https://github.com/juanfont/headscale/issues/563) - Improve registration protocol implementation and switch to NodeKey as main identifier [#725](https://github.com/juanfont/headscale/pull/725) - Add ability to connect to PostgreSQL via unix socket [#734](https://github.com/juanfont/headscale/pull/734) @@ -231,7 +231,7 @@ after improving the test harness as part of adopting [#1460](https://github.com/ - Fix send on closed channel crash in polling [#542](https://github.com/juanfont/headscale/pull/542) - Fixed spurious calls to setLastStateChangeToNow from ephemeral nodes [#566](https://github.com/juanfont/headscale/pull/566) - Add command for moving nodes between namespaces [#362](https://github.com/juanfont/headscale/issues/362) -- Added more configuration parameters for OpenID Connect (scopes, free-form paramters, domain and user allowlist) +- Added more configuration parameters for OpenID Connect (scopes, free-form parameters, domain and user allowlist) - Add command to set tags on a node [#525](https://github.com/juanfont/headscale/issues/525) - Add command to view tags of nodes [#356](https://github.com/juanfont/headscale/issues/356) - Add --all (-a) flag to enable routes command [#360](https://github.com/juanfont/headscale/issues/360) @@ -279,10 +279,10 @@ after improving the test harness as part of adopting [#1460](https://github.com/ - Fix a bug were the same IP could be assigned to multiple hosts if joined in quick succession [#346](https://github.com/juanfont/headscale/pull/346) - Simplify the code behind registration of machines [#366](https://github.com/juanfont/headscale/pull/366) - - Nodes are now only written to database if they are registrated successfully + - Nodes are now only written to database if they are registered successfully - Fix a limitation in the ACLs that prevented users to write rules with `*` as source [#374](https://github.com/juanfont/headscale/issues/374) - Reduce the overhead of marshal/unmarshal for Hostinfo, routes and endpoints by using specific types in Machine [#371](https://github.com/juanfont/headscale/pull/371) -- Apply normalization function to FQDN on hostnames when hosts registers and retrieve informations [#363](https://github.com/juanfont/headscale/issues/363) +- Apply normalization function to FQDN on hostnames when hosts registers and retrieve information [#363](https://github.com/juanfont/headscale/issues/363) - Fix a bug that prevented the use of `tailscale logout` with OIDC [#508](https://github.com/juanfont/headscale/issues/508) - Added Tailscale repo HEAD and unstable releases channel to the integration tests targets [#513](https://github.com/juanfont/headscale/pull/513) diff --git a/README.md b/README.md index 30874296..2ee8f4eb 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ Please read the [CONTRIBUTING.md](./CONTRIBUTING.md) file. ### Requirements -To contribute to headscale you would need the lastest version of [Go](https://golang.org) +To contribute to headscale you would need the latest version of [Go](https://golang.org) and [Buf](https://buf.build)(Protobuf generator). We recommend using [Nix](https://nixos.org/) to setup a development environment. This can diff --git a/config-example.yaml b/config-example.yaml index 0f1c2412..867f8903 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -105,7 +105,7 @@ derp: automatically_add_embedded_derp_region: true # For better connection stability (especially when using an Exit-Node and DNS is not working), - # it is possible to optionall add the public IPv4 and IPv6 address to the Derp-Map using: + # it is possible to optionally add the public IPv4 and IPv6 address to the Derp-Map using: ipv4: 1.2.3.4 ipv6: 2001:db8::1 @@ -199,7 +199,7 @@ log: format: text level: info -# Path to a file containg ACL policies. +# Path to a file containing ACL policies. # ACLs can be defined as YAML or HUJSON. # https://tailscale.com/kb/1018/acls/ acl_policy_path: "" diff --git a/docs/exit-node.md b/docs/exit-node.md index 898b7811..831652b3 100644 --- a/docs/exit-node.md +++ b/docs/exit-node.md @@ -14,7 +14,7 @@ If the node is already registered, it can advertise exit capabilities like this: $ sudo tailscale set --advertise-exit-node ``` -To use a node as an exit node, IP forwarding must be enabled on the node. Check the official [Tailscale documentation](https://tailscale.com/kb/1019/subnets/?tab=linux#enable-ip-forwarding) for how to enable IP fowarding. +To use a node as an exit node, IP forwarding must be enabled on the node. Check the official [Tailscale documentation](https://tailscale.com/kb/1019/subnets/?tab=linux#enable-ip-forwarding) for how to enable IP forwarding. ## On the control server diff --git a/docs/faq.md b/docs/faq.md index fff96132..ba30911b 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -36,7 +36,7 @@ We don't know. We might be working on it. If you want to help, please send us a Please be aware that there are a number of reasons why we might not accept specific contributions: - It is not possible to implement the feature in a way that makes sense in a self-hosted environment. -- Given that we are reverse-engineering Tailscale to satify our own curiosity, we might be interested in implementing the feature ourselves. +- Given that we are reverse-engineering Tailscale to satisfy our own curiosity, we might be interested in implementing the feature ourselves. - You are not sending unit and integration tests with it. ## Do you support Y method of deploying Headscale? diff --git a/docs/proposals/001-acls.md b/docs/proposals/001-acls.md index 8a02e836..74bcd13e 100644 --- a/docs/proposals/001-acls.md +++ b/docs/proposals/001-acls.md @@ -58,12 +58,12 @@ A solution could be to consider a headscale server (in it's entirety) as a tailnet. For personal users the default behavior could either allow all communications -between all namespaces (like tailscale) or dissallow all communications between +between all namespaces (like tailscale) or disallow all communications between namespaces (current behavior). For businesses and organisations, viewing a headscale instance a single tailnet would allow users (namespace) to talk to each other with the ACLs. As described -in tailscale's documentation [[1]], a server should be tagged and personnal +in tailscale's documentation [[1]], a server should be tagged and personal devices should be tied to a user. Translated in headscale's terms each user can have multiple devices and all those devices should be in the same namespace. The servers should be tagged and used as such. @@ -88,7 +88,7 @@ the ability to rules in either format (HuJSON or YAML). Let's build an example use case for a small business (It may be the place where ACL's are the most useful). -We have a small company with a boss, an admin, two developper and an intern. +We have a small company with a boss, an admin, two developer and an intern. The boss should have access to all servers but not to the users hosts. Admin should also have access to all hosts except that their permissions should be @@ -173,7 +173,7 @@ need to add the following ACLs "ports": ["prod:*", "dev:*", "internal:*"] }, - // admin have access to adminstration port (lets only consider port 22 here) + // admin have access to administration port (lets only consider port 22 here) { "action": "accept", "users": ["group:admin"], diff --git a/docs/remote-cli.md b/docs/remote-cli.md index 96a6333a..3d44eabc 100644 --- a/docs/remote-cli.md +++ b/docs/remote-cli.md @@ -1,13 +1,13 @@ # Controlling `headscale` with remote CLI -## Prerequisit +## Prerequisite - A workstation to run `headscale` (could be Linux, macOS, other supported platforms) - A `headscale` server (version `0.13.0` or newer) - Access to create API keys (local access to the `headscale` server) - `headscale` _must_ be served over TLS/HTTPS - Remote access does _not_ support unencrypted traffic. -- Port `50443` must be open in the firewall (or port overriden by `grpc_listen_addr` option) +- Port `50443` must be open in the firewall (or port overridden by `grpc_listen_addr` option) ## Goal @@ -97,4 +97,4 @@ Checklist: - Make sure you use version `0.13.0` or newer. - Verify that your TLS certificate is valid and trusted - If you do not have access to a trusted certificate (e.g. from Let's Encrypt), add your self signed certificate to the trust store of your OS or - - Set `HEADSCALE_CLI_INSECURE` to 0 in your environement + - Set `HEADSCALE_CLI_INSECURE` to 0 in your environment diff --git a/docs/reverse-proxy.md b/docs/reverse-proxy.md index 1f417c9b..c6fd4b16 100644 --- a/docs/reverse-proxy.md +++ b/docs/reverse-proxy.md @@ -115,7 +115,7 @@ The following Caddyfile is all that is necessary to use Caddy as a reverse proxy } ``` -Caddy v2 will [automatically](https://caddyserver.com/docs/automatic-https) provision a certficate for your domain/subdomain, force HTTPS, and proxy websockets - no further configuration is necessary. +Caddy v2 will [automatically](https://caddyserver.com/docs/automatic-https) provision a certificate for your domain/subdomain, force HTTPS, and proxy websockets - no further configuration is necessary. For a slightly more complex configuration which utilizes Docker containers to manage Caddy, Headscale, and Headscale-UI, [Guru Computing's guide](https://blog.gurucomputing.com.au/smart-vpns-with-headscale/) is an excellent reference. diff --git a/docs/running-headscale-openbsd.md b/docs/running-headscale-openbsd.md index a490439a..e1d8d83f 100644 --- a/docs/running-headscale-openbsd.md +++ b/docs/running-headscale-openbsd.md @@ -30,7 +30,7 @@ describing how to make `headscale` run properly in a server environment. cd headscale # optionally checkout a release - # option a. you can find offical relase at https://github.com/juanfont/headscale/releases/latest + # option a. you can find official release at https://github.com/juanfont/headscale/releases/latest # option b. get latest tag, this may be a beta release latestTag=$(git describe --tags `git rev-list --tags --max-count=1`) @@ -57,7 +57,7 @@ describing how to make `headscale` run properly in a server environment. cd headscale # optionally checkout a release - # option a. you can find offical relase at https://github.com/juanfont/headscale/releases/latest + # option a. you can find official release at https://github.com/juanfont/headscale/releases/latest # option b. get latest tag, this may be a beta release latestTag=$(git describe --tags `git rev-list --tags --max-count=1`) diff --git a/flake.nix b/flake.nix index f2046dae..94ec6150 100644 --- a/flake.nix +++ b/flake.nix @@ -30,7 +30,7 @@ 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. + # update this if you have a mismatch after doing a change to those files. vendorHash = "sha256-wXfKeiJaGe6ahOsONrQhvbuMN8flQ13b0ZjxdbFs1e8="; subPackages = ["cmd/headscale"]; diff --git a/hscontrol/app.go b/hscontrol/app.go index b8eb6f69..28211db3 100644 --- a/hscontrol/app.go +++ b/hscontrol/app.go @@ -330,7 +330,7 @@ func (h *Headscale) grpcAuthenticationInterceptor(ctx context.Context, // Check if the request is coming from the on-server client. // This is not secure, but it is to maintain maintainability // with the "legacy" database-based client - // It is also neede for grpc-gateway to be able to connect to + // It is also needed for grpc-gateway to be able to connect to // the server client, _ := peer.FromContext(ctx) diff --git a/hscontrol/db/node.go b/hscontrol/db/node.go index e9a4ea04..c675dc7c 100644 --- a/hscontrol/db/node.go +++ b/hscontrol/db/node.go @@ -661,7 +661,7 @@ func GenerateGivenName( } func DeleteExpiredEphemeralNodes(tx *gorm.DB, - inactivityThreshhold time.Duration, + inactivityThreshold time.Duration, ) ([]types.NodeID, []types.NodeID) { users, err := ListUsers(tx) if err != nil { @@ -679,7 +679,7 @@ func DeleteExpiredEphemeralNodes(tx *gorm.DB, for idx, node := range nodes { if node.IsEphemeral() && node.LastSeen != nil && time.Now(). - After(node.LastSeen.Add(inactivityThreshhold)) { + After(node.LastSeen.Add(inactivityThreshold)) { expired = append(expired, node.ID) log.Info(). diff --git a/hscontrol/db/node_test.go b/hscontrol/db/node_test.go index fa187653..e95ee4ae 100644 --- a/hscontrol/db/node_test.go +++ b/hscontrol/db/node_test.go @@ -393,7 +393,7 @@ func (s *Suite) TestSetTags(c *check.C) { c.Assert(err, check.IsNil) c.Assert(node.ForcedTags, check.DeepEquals, types.StringList(sTags)) - // assign duplicat tags, expect no errors but no doubles in DB + // assign duplicate tags, expect no errors but no doubles in DB eTags := []string{"tag:bar", "tag:test", "tag:unknown", "tag:test"} err = db.SetTags(node.ID, eTags) c.Assert(err, check.IsNil) diff --git a/hscontrol/db/preauth_keys.go b/hscontrol/db/preauth_keys.go index 16a8689f..adfd289a 100644 --- a/hscontrol/db/preauth_keys.go +++ b/hscontrol/db/preauth_keys.go @@ -83,7 +83,7 @@ func CreatePreAuthKey( if !seenTags[tag] { if err := tx.Save(&types.PreAuthKeyACLTag{PreAuthKeyID: key.ID, Tag: tag}).Error; err != nil { return nil, fmt.Errorf( - "failed to ceate key tag in the database: %w", + "failed to create key tag in the database: %w", err, ) } diff --git a/hscontrol/derp/server/derp_server.go b/hscontrol/derp/server/derp_server.go index 52a63e9f..0b0c9b16 100644 --- a/hscontrol/derp/server/derp_server.go +++ b/hscontrol/derp/server/derp_server.go @@ -204,7 +204,7 @@ func DERPProbeHandler( } } -// DERPBootstrapDNSHandler implements the /bootsrap-dns endpoint +// DERPBootstrapDNSHandler implements the /bootstrap-dns endpoint // Described in https://github.com/tailscale/tailscale/issues/1405, // this endpoint provides a way to help a client when it fails to start up // because its DNS are broken. diff --git a/hscontrol/policy/acls_test.go b/hscontrol/policy/acls_test.go index dd4d95bb..b0cafe10 100644 --- a/hscontrol/policy/acls_test.go +++ b/hscontrol/policy/acls_test.go @@ -532,7 +532,7 @@ func (s *Suite) TestRuleInvalidGeneration(c *check.C) { "example-host-2:80" ], "deny": [ - "exapmle-host-2:100" + "example-host-2:100" ], }, { @@ -635,7 +635,7 @@ func Test_expandGroup(t *testing.T) { wantErr: false, }, { - name: "InexistantGroup", + name: "InexistentGroup", field: field{ pol: ACLPolicy{ Groups: Groups{ @@ -2604,7 +2604,7 @@ func Test_getFilteredByACLPeers(t *testing.T) { { name: "all hosts can talk to each other", args: args{ - nodes: types.Nodes{ // list of all nodess in the database + nodes: types.Nodes{ // list of all nodes in the database &types.Node{ ID: 1, IPv4: iap("100.64.0.1"), @@ -2651,7 +2651,7 @@ func Test_getFilteredByACLPeers(t *testing.T) { { name: "One host can talk to another, but not all hosts", args: args{ - nodes: types.Nodes{ // list of all nodess in the database + nodes: types.Nodes{ // list of all nodes in the database &types.Node{ ID: 1, IPv4: iap("100.64.0.1"), @@ -2693,7 +2693,7 @@ func Test_getFilteredByACLPeers(t *testing.T) { { name: "host cannot directly talk to destination, but return path is authorized", args: args{ - nodes: types.Nodes{ // list of all nodess in the database + nodes: types.Nodes{ // list of all nodes in the database &types.Node{ ID: 1, IPv4: iap("100.64.0.1"), @@ -2735,7 +2735,7 @@ func Test_getFilteredByACLPeers(t *testing.T) { { name: "rules allows all hosts to reach one destination", args: args{ - nodes: types.Nodes{ // list of all nodess in the database + nodes: types.Nodes{ // list of all nodes in the database &types.Node{ ID: 1, IPv4: iap("100.64.0.1"), @@ -2777,7 +2777,7 @@ func Test_getFilteredByACLPeers(t *testing.T) { { name: "rules allows all hosts to reach one destination, destination can reach all hosts", args: args{ - nodes: types.Nodes{ // list of all nodess in the database + nodes: types.Nodes{ // list of all nodes in the database &types.Node{ ID: 1, IPv4: iap("100.64.0.1"), @@ -2824,7 +2824,7 @@ func Test_getFilteredByACLPeers(t *testing.T) { { name: "rule allows all hosts to reach all destinations", args: args{ - nodes: types.Nodes{ // list of all nodess in the database + nodes: types.Nodes{ // list of all nodes in the database &types.Node{ ID: 1, IPv4: iap("100.64.0.1"), @@ -2871,7 +2871,7 @@ func Test_getFilteredByACLPeers(t *testing.T) { { name: "without rule all communications are forbidden", args: args{ - nodes: types.Nodes{ // list of all nodess in the database + nodes: types.Nodes{ // list of all nodes in the database &types.Node{ ID: 1, IPv4: iap("100.64.0.1"), diff --git a/integration/general_test.go b/integration/general_test.go index 89e0d342..db9bf83b 100644 --- a/integration/general_test.go +++ b/integration/general_test.go @@ -335,14 +335,14 @@ func TestTaildrop(t *testing.T) { IntegrationSkip(t) t.Parallel() - retry := func(times int, sleepInverval time.Duration, doWork func() error) error { + retry := func(times int, sleepInterval time.Duration, doWork func() error) error { var err error for attempts := 0; attempts < times; attempts++ { err = doWork() if err == nil { return nil } - time.Sleep(sleepInverval) + time.Sleep(sleepInterval) } return err @@ -793,7 +793,7 @@ func TestNodeOnlineStatus(t *testing.T) { continue } - // All peers of this nodess are reporting to be + // All peers of this nodes are reporting to be // connected to the control server assert.Truef( t, diff --git a/integration/scenario.go b/integration/scenario.go index 9444d882..3f0eb7d2 100644 --- a/integration/scenario.go +++ b/integration/scenario.go @@ -450,7 +450,7 @@ func (s *Scenario) WaitForTailscaleSyncWithPeerCount(peerCount int) error { return nil } -// CreateHeadscaleEnv is a conventient method returning a complete Headcale +// CreateHeadscaleEnv is a convenient method returning a complete Headcale // test environment with nodes of all versions, joined to the server with X // users. func (s *Scenario) CreateHeadscaleEnv( diff --git a/integration/utils.go b/integration/utils.go index 1e2cfd2c..840dbc4c 100644 --- a/integration/utils.go +++ b/integration/utils.go @@ -331,7 +331,7 @@ func dockertestMaxWait() time.Duration { // return timeout // } -// pingAllNegativeHelper is intended to have 1 or more nodes timeing out from the ping, +// pingAllNegativeHelper is intended to have 1 or more nodes timing out from the ping, // it counts failures instead of successes. // func pingAllNegativeHelper(t *testing.T, clients []TailscaleClient, addrs []string) int { // t.Helper() From c8ebbede54f2f9aeb32939fd705077966de74579 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby <kristoffer@dalby.cc> Date: Fri, 24 May 2024 09:15:34 +0100 Subject: [PATCH 072/145] Simplify map session management (#1931) This PR removes the complicated session management introduced in https://github.com/juanfont/headscale/pull/1791 which kept track of the sessions in a map, in addition to the channel already kept track of in the notifier. Instead of trying to close the mapsession, it will now be replaced by the new one and closed after so all new updates goes to the right place. The map session serve function is also split into a streaming and a non-streaming version for better readability. RemoveNode in the notifier will not remove a node if the channel is not matching the one that has been passed (e.g. it has been replaced with a new one). A new tuning parameter has been added to added to set timeout before the notifier gives up to send an update to a node. Add a keep alive resetter so we wait with sending keep alives if a node has just received an update. In addition it adds a bunch of env debug flags that can be set: - `HEADSCALE_DEBUG_HIGH_CARDINALITY_METRICS`: make certain metrics include per node.id, not recommended to use in prod. - `HEADSCALE_DEBUG_PROFILING_ENABLED`: activate tracing - `HEADSCALE_DEBUG_PROFILING_PATH`: where to store traces - `HEADSCALE_DEBUG_DUMP_CONFIG`: calls `spew.Dump` on the config object startup - `HEADSCALE_DEBUG_DEADLOCK`: enable go-deadlock to dump goroutines if it looks like a deadlock has occured, enabled in integration tests. Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> --- Dockerfile.tailscale-HEAD | 50 ++++++-- flake.nix | 4 +- go.mod | 2 + go.sum | 4 + hscontrol/app.go | 45 ++----- hscontrol/metrics.go | 31 +++-- hscontrol/noise.go | 60 +-------- hscontrol/notifier/metrics.go | 46 ++++++- hscontrol/notifier/notifier.go | 191 ++++++++++++++++++++-------- hscontrol/notifier/notifier_test.go | 2 +- hscontrol/poll.go | 174 ++++++++++++++----------- hscontrol/types/config.go | 5 +- hscontrol/types/node.go | 4 + integration/general_test.go | 44 ++++--- integration/hsic/hsic.go | 12 +- integration/scenario.go | 13 +- integration/tailscale.go | 5 +- integration/tsic/tsic.go | 19 +-- 18 files changed, 426 insertions(+), 285 deletions(-) diff --git a/Dockerfile.tailscale-HEAD b/Dockerfile.tailscale-HEAD index 83ff9fe5..f78d687a 100644 --- a/Dockerfile.tailscale-HEAD +++ b/Dockerfile.tailscale-HEAD @@ -1,21 +1,43 @@ -# This Dockerfile and the images produced are for testing headscale, -# and are in no way endorsed by Headscale's maintainers as an -# official nor supported release or distribution. +# Copyright (c) Tailscale Inc & AUTHORS +# SPDX-License-Identifier: BSD-3-Clause -FROM golang:latest +# This Dockerfile is more or less lifted from tailscale/tailscale +# to ensure a similar build process when testing the HEAD of tailscale. -RUN apt-get update \ - && apt-get install -y dnsutils git iptables ssh ca-certificates \ - && rm -rf /var/lib/apt/lists/* +FROM golang:1.22-alpine AS build-env -RUN useradd --shell=/bin/bash --create-home ssh-it-user +WORKDIR /go/src +RUN apk add --no-cache git + +# Replace `RUN git...` with `COPY` and a local checked out version of Tailscale in `./tailscale` +# to test specific commits of the Tailscale client. This is useful when trying to find out why +# something specific broke between two versions of Tailscale with for example `git bisect`. +# COPY ./tailscale . RUN git clone https://github.com/tailscale/tailscale.git -WORKDIR /go/tailscale +WORKDIR /go/src/tailscale -RUN git checkout main \ - && sh build_dist.sh tailscale.com/cmd/tailscale \ - && sh build_dist.sh tailscale.com/cmd/tailscaled \ - && cp tailscale /usr/local/bin/ \ - && cp tailscaled /usr/local/bin/ + +# see build_docker.sh +ARG VERSION_LONG="" +ENV VERSION_LONG=$VERSION_LONG +ARG VERSION_SHORT="" +ENV VERSION_SHORT=$VERSION_SHORT +ARG VERSION_GIT_HASH="" +ENV VERSION_GIT_HASH=$VERSION_GIT_HASH +ARG TARGETARCH + +RUN GOARCH=$TARGETARCH go install -ldflags="\ + -X tailscale.com/version.longStamp=$VERSION_LONG \ + -X tailscale.com/version.shortStamp=$VERSION_SHORT \ + -X tailscale.com/version.gitCommitStamp=$VERSION_GIT_HASH" \ + -v ./cmd/tailscale ./cmd/tailscaled ./cmd/containerboot + +FROM alpine:3.18 +RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables curl + +COPY --from=build-env /go/bin/* /usr/local/bin/ +# For compat with the previous run.sh, although ideally you should be +# using build_docker.sh which sets an entrypoint for the image. +RUN mkdir /tailscale && ln -s /usr/local/bin/containerboot /tailscale/run.sh diff --git a/flake.nix b/flake.nix index 94ec6150..5d4978ca 100644 --- a/flake.nix +++ b/flake.nix @@ -30,8 +30,8 @@ checkFlags = ["-short"]; # When updating go.mod or go.sum, a new sha will need to be calculated, - # update this if you have a mismatch after doing a change to those files. - vendorHash = "sha256-wXfKeiJaGe6ahOsONrQhvbuMN8flQ13b0ZjxdbFs1e8="; + # update this if you have a mismatch after doing a change to thos files. + vendorHash = "sha256-EorT2AVwA3usly/LcNor6r5UIhLCdj3L4O4ilgTIC2o="; subPackages = ["cmd/headscale"]; diff --git a/go.mod b/go.mod index 0e0e12af..e96bcc8a 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,7 @@ require ( github.com/puzpuzpuz/xsync/v3 v3.1.0 github.com/rs/zerolog v1.32.0 github.com/samber/lo v1.39.0 + github.com/sasha-s/go-deadlock v0.3.1 github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.18.2 github.com/stretchr/testify v1.9.0 @@ -155,6 +156,7 @@ require ( github.com/opencontainers/image-spec v1.1.0 // indirect github.com/opencontainers/runc v1.1.12 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect diff --git a/go.sum b/go.sum index 309d14e7..a534a8e4 100644 --- a/go.sum +++ b/go.sum @@ -367,6 +367,8 @@ github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaR github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ= +github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= 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= @@ -423,6 +425,8 @@ github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6g github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= +github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0= +github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM= 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= diff --git a/hscontrol/app.go b/hscontrol/app.go index 28211db3..253c2671 100644 --- a/hscontrol/app.go +++ b/hscontrol/app.go @@ -19,6 +19,7 @@ import ( "time" "github.com/coreos/go-oidc/v3/oidc" + "github.com/davecgh/go-spew/spew" "github.com/gorilla/mux" grpcMiddleware "github.com/grpc-ecosystem/go-grpc-middleware" grpcRuntime "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" @@ -104,16 +105,15 @@ type Headscale struct { registrationCache *cache.Cache pollNetMapStreamWG sync.WaitGroup - - mapSessions map[types.NodeID]*mapSession - mapSessionMu sync.Mutex } var ( - profilingEnabled = envknob.Bool("HEADSCALE_PROFILING_ENABLED") + profilingEnabled = envknob.Bool("HEADSCALE_DEBUG_PROFILING_ENABLED") + profilingPath = envknob.String("HEADSCALE_DEBUG_PROFILING_PATH") tailsqlEnabled = envknob.Bool("HEADSCALE_DEBUG_TAILSQL_ENABLED") tailsqlStateDir = envknob.String("HEADSCALE_DEBUG_TAILSQL_STATE_DIR") tailsqlTSKey = envknob.String("TS_AUTHKEY") + dumpConfig = envknob.Bool("HEADSCALE_DEBUG_DUMP_CONFIG") ) func NewHeadscale(cfg *types.Config) (*Headscale, error) { @@ -138,7 +138,6 @@ func NewHeadscale(cfg *types.Config) (*Headscale, error) { registrationCache: registrationCache, pollNetMapStreamWG: sync.WaitGroup{}, nodeNotifier: notifier.NewNotifier(cfg), - mapSessions: make(map[types.NodeID]*mapSession), } app.db, err = db.NewHeadscaleDatabase( @@ -502,14 +501,14 @@ func (h *Headscale) createRouter(grpcMux *grpcRuntime.ServeMux) *mux.Router { // Serve launches the HTTP and gRPC server service Headscale and the API. func (h *Headscale) Serve() error { - if _, enableProfile := os.LookupEnv("HEADSCALE_PROFILING_ENABLED"); enableProfile { - if profilePath, ok := os.LookupEnv("HEADSCALE_PROFILING_PATH"); ok { - err := os.MkdirAll(profilePath, os.ModePerm) + if profilingEnabled { + if profilingPath != "" { + err := os.MkdirAll(profilingPath, os.ModePerm) if err != nil { log.Fatal().Err(err).Msg("failed to create profiling directory") } - defer profile.Start(profile.ProfilePath(profilePath)).Stop() + defer profile.Start(profile.ProfilePath(profilingPath)).Stop() } else { defer profile.Start().Stop() } @@ -517,6 +516,10 @@ func (h *Headscale) Serve() error { var err error + if dumpConfig { + spew.Dump(h.cfg) + } + // Fetch an initial DERP Map before we start serving h.DERPMap = derp.GetDERPMap(h.cfg.DERP) h.mapper = mapper.NewMapper(h.db, h.cfg, h.DERPMap, h.nodeNotifier) @@ -729,19 +732,6 @@ func (h *Headscale) Serve() error { w.WriteHeader(http.StatusOK) w.Write([]byte(h.nodeNotifier.String())) }) - debugMux.HandleFunc("/debug/mapresp", func(w http.ResponseWriter, r *http.Request) { - h.mapSessionMu.Lock() - defer h.mapSessionMu.Unlock() - - var b strings.Builder - b.WriteString("mapresponders:\n") - for k, v := range h.mapSessions { - fmt.Fprintf(&b, "\t%d: %p\n", k, v) - } - - w.WriteHeader(http.StatusOK) - w.Write([]byte(b.String())) - }) debugMux.Handle("/metrics", promhttp.Handler()) debugHTTPServer := &http.Server{ @@ -822,17 +812,6 @@ func (h *Headscale) Serve() error { expireNodeCancel() expireEphemeralCancel() - trace("closing map sessions") - wg := sync.WaitGroup{} - for _, mapSess := range h.mapSessions { - wg.Add(1) - go func() { - mapSess.close() - wg.Done() - }() - } - wg.Wait() - trace("waiting for netmap stream to close") h.pollNetMapStreamWG.Wait() diff --git a/hscontrol/metrics.go b/hscontrol/metrics.go index 9d802caf..835a6aac 100644 --- a/hscontrol/metrics.go +++ b/hscontrol/metrics.go @@ -7,8 +7,23 @@ import ( "github.com/gorilla/mux" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" + "tailscale.com/envknob" ) +var debugHighCardinalityMetrics = envknob.Bool("HEADSCALE_DEBUG_HIGH_CARDINALITY_METRICS") + +var mapResponseLastSentSeconds *prometheus.GaugeVec + +func init() { + if debugHighCardinalityMetrics { + mapResponseLastSentSeconds = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: prometheusNamespace, + Name: "mapresponse_last_sent_seconds", + Help: "last sent metric to node.id", + }, []string{"type", "id"}) + } +} + const prometheusNamespace = "headscale" var ( @@ -37,16 +52,16 @@ var ( Name: "mapresponse_readonly_requests_total", Help: "total count of readonly requests received", }, []string{"status"}) - mapResponseSessions = promauto.NewGauge(prometheus.GaugeOpts{ + mapResponseEnded = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: prometheusNamespace, - Name: "mapresponse_current_sessions_total", - Help: "total count open map response sessions", - }) - mapResponseRejected = promauto.NewCounterVec(prometheus.CounterOpts{ - Namespace: prometheusNamespace, - Name: "mapresponse_rejected_new_sessions_total", - Help: "total count of new mapsessions rejected", + Name: "mapresponse_ended_total", + Help: "total count of new mapsessions ended", }, []string{"reason"}) + mapResponseClosed = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: prometheusNamespace, + Name: "mapresponse_closed_total", + Help: "total count of calls to mapresponse close", + }, []string{"return"}) httpDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ Namespace: prometheusNamespace, Name: "http_duration_seconds", diff --git a/hscontrol/noise.go b/hscontrol/noise.go index 7fcbc252..360c7045 100644 --- a/hscontrol/noise.go +++ b/hscontrol/noise.go @@ -231,62 +231,12 @@ func (ns *noiseServer) NoisePollNetMapHandler( return } + sess := ns.headscale.newMapSession(req.Context(), mapRequest, writer, node) - sess.tracef("a node sending a MapRequest with Noise protocol") - - // If a streaming mapSession exists for this node, close it - // and start a new one. - if sess.isStreaming() { - sess.tracef("aquiring lock to check stream") - - ns.headscale.mapSessionMu.Lock() - if _, ok := ns.headscale.mapSessions[node.ID]; ok { - // NOTE/TODO(kradalby): From how I understand the protocol, when - // a client connects with stream=true, and already has a streaming - // connection open, the correct way is to close the current channel - // and replace it. However, I cannot manage to get that working with - // some sort of lock/block happening on the cancelCh in the streaming - // session. - // Not closing the channel and replacing it puts us in a weird state - // which keeps a ghost stream open, receiving keep alives, but no updates. - // - // Typically a new connection is opened when one exists as a client which - // is already authenticated reconnects (e.g. down, then up). The client will - // start auth and streaming at the same time, and then cancel the streaming - // when the auth has finished successfully, opening a new connection. - // - // As a work-around to not replacing, abusing the clients "resilience" - // by reject the new connection which will cause the client to immediately - // reconnect and "fix" the issue, as the other connection typically has been - // closed, meaning there is nothing to replace. - // - // sess.infof("node has an open stream(%p), replacing with %p", oldSession, sess) - // oldSession.close() - - defer ns.headscale.mapSessionMu.Unlock() - - sess.infof("node has an open stream(%p), rejecting new stream", sess) - mapResponseRejected.WithLabelValues("exists").Inc() - return - } - - ns.headscale.mapSessions[node.ID] = sess - mapResponseSessions.Inc() - ns.headscale.mapSessionMu.Unlock() - sess.tracef("releasing lock to check stream") - } - - sess.serve() - - if sess.isStreaming() { - sess.tracef("aquiring lock to remove stream") - ns.headscale.mapSessionMu.Lock() - defer ns.headscale.mapSessionMu.Unlock() - - delete(ns.headscale.mapSessions, node.ID) - mapResponseSessions.Dec() - - sess.tracef("releasing lock to remove stream") + if !sess.isStreaming() { + sess.serve() + } else { + sess.serveLongPoll() } } diff --git a/hscontrol/notifier/metrics.go b/hscontrol/notifier/metrics.go index 1cc4df2b..8a7a8839 100644 --- a/hscontrol/notifier/metrics.go +++ b/hscontrol/notifier/metrics.go @@ -3,22 +3,43 @@ package notifier import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" + "tailscale.com/envknob" ) const prometheusNamespace = "headscale" +var debugHighCardinalityMetrics = envknob.Bool("HEADSCALE_DEBUG_HIGH_CARDINALITY_METRICS") + +var notifierUpdateSent *prometheus.CounterVec + +func init() { + if debugHighCardinalityMetrics { + notifierUpdateSent = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: prometheusNamespace, + Name: "notifier_update_sent_total", + Help: "total count of update sent on nodes channel", + }, []string{"status", "type", "trigger", "id"}) + } else { + notifierUpdateSent = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: prometheusNamespace, + Name: "notifier_update_sent_total", + Help: "total count of update sent on nodes channel", + }, []string{"status", "type", "trigger"}) + } +} + var ( + notifierWaitersForLock = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: prometheusNamespace, + Name: "notifier_waiters_for_lock", + Help: "gauge of waiters for the notifier lock", + }, []string{"type", "action"}) notifierWaitForLock = promauto.NewHistogramVec(prometheus.HistogramOpts{ Namespace: prometheusNamespace, Name: "notifier_wait_for_lock_seconds", Help: "histogram of time spent waiting for the notifier lock", Buckets: []float64{0.001, 0.01, 0.1, 0.3, 0.5, 1, 3, 5, 10}, }, []string{"action"}) - notifierUpdateSent = promauto.NewCounterVec(prometheus.CounterOpts{ - Namespace: prometheusNamespace, - Name: "notifier_update_sent_total", - Help: "total count of update sent on nodes channel", - }, []string{"status", "type", "trigger"}) notifierUpdateReceived = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: prometheusNamespace, Name: "notifier_update_received_total", @@ -29,4 +50,19 @@ var ( Name: "notifier_open_channels_total", Help: "total count open channels in notifier", }) + notifierBatcherWaitersForLock = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: prometheusNamespace, + Name: "notifier_batcher_waiters_for_lock", + Help: "gauge of waiters for the notifier batcher lock", + }, []string{"type", "action"}) + notifierBatcherChanges = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: prometheusNamespace, + Name: "notifier_batcher_changes_pending", + Help: "gauge of full changes pending in the notifier batcher", + }, []string{}) + notifierBatcherPatches = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: prometheusNamespace, + Name: "notifier_batcher_patches_pending", + Help: "gauge of patches pending in the notifier batcher", + }, []string{}) ) diff --git a/hscontrol/notifier/notifier.go b/hscontrol/notifier/notifier.go index 339a56f1..483c3f37 100644 --- a/hscontrol/notifier/notifier.go +++ b/hscontrol/notifier/notifier.go @@ -11,25 +11,40 @@ import ( "github.com/juanfont/headscale/hscontrol/types" "github.com/puzpuzpuz/xsync/v3" "github.com/rs/zerolog/log" + "github.com/sasha-s/go-deadlock" + "tailscale.com/envknob" "tailscale.com/tailcfg" "tailscale.com/util/set" ) +var debugDeadlock = envknob.Bool("HEADSCALE_DEBUG_DEADLOCK") +var debugDeadlockTimeout = envknob.RegisterDuration("HEADSCALE_DEBUG_DEADLOCK_TIMEOUT") + +func init() { + deadlock.Opts.Disable = !debugDeadlock + if debugDeadlock { + deadlock.Opts.DeadlockTimeout = debugDeadlockTimeout() + deadlock.Opts.PrintAllCurrentGoroutines = true + } +} + type Notifier struct { - l sync.RWMutex + l deadlock.Mutex nodes map[types.NodeID]chan<- types.StateUpdate connected *xsync.MapOf[types.NodeID, bool] b *batcher + cfg *types.Config } func NewNotifier(cfg *types.Config) *Notifier { n := &Notifier{ nodes: make(map[types.NodeID]chan<- types.StateUpdate), connected: xsync.NewMapOf[types.NodeID, bool](), + cfg: cfg, } b := newBatcher(cfg.Tuning.BatchChangeDelay, n) n.b = b - // TODO(kradalby): clean this up + go b.doWork() return n } @@ -39,59 +54,75 @@ func (n *Notifier) Close() { n.b.close() } -func (n *Notifier) AddNode(nodeID types.NodeID, c chan<- types.StateUpdate) { - log.Trace().Caller().Uint64("node.id", nodeID.Uint64()).Msg("acquiring lock to add node") - defer log.Trace(). - Caller(). - Uint64("node.id", nodeID.Uint64()). - Msg("releasing lock to add node") +func (n *Notifier) tracef(nID types.NodeID, msg string, args ...any) { + log.Trace(). + Uint64("node.id", nID.Uint64()). + Int("open_chans", len(n.nodes)).Msgf(msg, args...) +} +func (n *Notifier) AddNode(nodeID types.NodeID, c chan<- types.StateUpdate) { start := time.Now() + notifierWaitersForLock.WithLabelValues("lock", "add").Inc() n.l.Lock() defer n.l.Unlock() + notifierWaitersForLock.WithLabelValues("lock", "add").Dec() notifierWaitForLock.WithLabelValues("add").Observe(time.Since(start).Seconds()) + // If a channel exists, it means the node has opened a new + // connection. Close the old channel and replace it. + if curr, ok := n.nodes[nodeID]; ok { + n.tracef(nodeID, "channel present, closing and replacing") + close(curr) + } + n.nodes[nodeID] = c n.connected.Store(nodeID, true) - log.Trace(). - Uint64("node.id", nodeID.Uint64()). - Int("open_chans", len(n.nodes)). - Msg("Added new channel") + n.tracef(nodeID, "added new channel") notifierNodeUpdateChans.Inc() } -func (n *Notifier) RemoveNode(nodeID types.NodeID) { - log.Trace().Caller().Uint64("node.id", nodeID.Uint64()).Msg("acquiring lock to remove node") - defer log.Trace(). - Caller(). - Uint64("node.id", nodeID.Uint64()). - Msg("releasing lock to remove node") - +// RemoveNode removes a node and a given channel from the notifier. +// It checks that the channel is the same as currently being updated +// and ignores the removal if it is not. +// RemoveNode reports if the node/chan was removed. +func (n *Notifier) RemoveNode(nodeID types.NodeID, c chan<- types.StateUpdate) bool { start := time.Now() + notifierWaitersForLock.WithLabelValues("lock", "remove").Inc() n.l.Lock() defer n.l.Unlock() + notifierWaitersForLock.WithLabelValues("lock", "remove").Dec() notifierWaitForLock.WithLabelValues("remove").Observe(time.Since(start).Seconds()) if len(n.nodes) == 0 { - return + return true + } + + // If the channel exist, but it does not belong + // to the caller, ignore. + if curr, ok := n.nodes[nodeID]; ok { + if curr != c { + n.tracef(nodeID, "channel has been replaced, not removing") + return false + } } delete(n.nodes, nodeID) n.connected.Store(nodeID, false) - log.Trace(). - Uint64("node.id", nodeID.Uint64()). - Int("open_chans", len(n.nodes)). - Msg("Removed channel") + n.tracef(nodeID, "removed channel") notifierNodeUpdateChans.Dec() + + return true } // IsConnected reports if a node is connected to headscale and has a // poll session open. func (n *Notifier) IsConnected(nodeID types.NodeID) bool { - n.l.RLock() - defer n.l.RUnlock() + notifierWaitersForLock.WithLabelValues("lock", "conncheck").Inc() + n.l.Lock() + defer n.l.Unlock() + notifierWaitersForLock.WithLabelValues("lock", "conncheck").Dec() if val, ok := n.connected.Load(nodeID); ok { return val @@ -130,15 +161,11 @@ func (n *Notifier) NotifyByNodeID( update types.StateUpdate, nodeID types.NodeID, ) { - log.Trace().Caller().Str("type", update.Type.String()).Msg("acquiring lock to notify") - defer log.Trace(). - Caller(). - Str("type", update.Type.String()). - Msg("releasing lock, finished notifying") - start := time.Now() - n.l.RLock() - defer n.l.RUnlock() + notifierWaitersForLock.WithLabelValues("lock", "notify").Inc() + n.l.Lock() + defer n.l.Unlock() + notifierWaitersForLock.WithLabelValues("lock", "notify").Dec() notifierWaitForLock.WithLabelValues("notify").Observe(time.Since(start).Seconds()) if c, ok := n.nodes[nodeID]; ok { @@ -150,50 +177,94 @@ func (n *Notifier) NotifyByNodeID( Any("origin", types.NotifyOriginKey.Value(ctx)). Any("origin-hostname", types.NotifyHostnameKey.Value(ctx)). Msgf("update not sent, context cancelled") - notifierUpdateSent.WithLabelValues("cancelled", update.Type.String(), types.NotifyOriginKey.Value(ctx)).Inc() + if debugHighCardinalityMetrics { + notifierUpdateSent.WithLabelValues("cancelled", update.Type.String(), types.NotifyOriginKey.Value(ctx), nodeID.String()).Inc() + } else { + notifierUpdateSent.WithLabelValues("cancelled", update.Type.String(), types.NotifyOriginKey.Value(ctx)).Inc() + } return case c <- update: - log.Trace(). - Uint64("node.id", nodeID.Uint64()). - Any("origin", ctx.Value("origin")). - Any("origin-hostname", ctx.Value("hostname")). - Msgf("update successfully sent on chan") - notifierUpdateSent.WithLabelValues("ok", update.Type.String(), types.NotifyOriginKey.Value(ctx)).Inc() + n.tracef(nodeID, "update successfully sent on chan, origin: %s, origin-hostname: %s", ctx.Value("origin"), ctx.Value("hostname")) + if debugHighCardinalityMetrics { + notifierUpdateSent.WithLabelValues("ok", update.Type.String(), types.NotifyOriginKey.Value(ctx), nodeID.String()).Inc() + } else { + notifierUpdateSent.WithLabelValues("ok", update.Type.String(), types.NotifyOriginKey.Value(ctx)).Inc() + } } } } func (n *Notifier) sendAll(update types.StateUpdate) { start := time.Now() - n.l.RLock() - defer n.l.RUnlock() + notifierWaitersForLock.WithLabelValues("lock", "send-all").Inc() + n.l.Lock() + defer n.l.Unlock() + notifierWaitersForLock.WithLabelValues("lock", "send-all").Dec() notifierWaitForLock.WithLabelValues("send-all").Observe(time.Since(start).Seconds()) - for _, c := range n.nodes { - c <- update - notifierUpdateSent.WithLabelValues("ok", update.Type.String(), "send-all").Inc() + for id, c := range n.nodes { + // Whenever an update is sent to all nodes, there is a chance that the node + // has disconnected and the goroutine that was supposed to consume the update + // has shut down the channel and is waiting for the lock held here in RemoveNode. + // This means that there is potential for a deadlock which would stop all updates + // going out to clients. This timeout prevents that from happening by moving on to the + // next node if the context is cancelled. Afther sendAll releases the lock, the add/remove + // call will succeed and the update will go to the correct nodes on the next call. + ctx, cancel := context.WithTimeout(context.Background(), n.cfg.Tuning.NotifierSendTimeout) + defer cancel() + select { + case <-ctx.Done(): + log.Error(). + Err(ctx.Err()). + Uint64("node.id", id.Uint64()). + Msgf("update not sent, context cancelled") + if debugHighCardinalityMetrics { + notifierUpdateSent.WithLabelValues("cancelled", update.Type.String(), "send-all", id.String()).Inc() + } else { + notifierUpdateSent.WithLabelValues("cancelled", update.Type.String(), "send-all").Inc() + } + + return + case c <- update: + if debugHighCardinalityMetrics { + notifierUpdateSent.WithLabelValues("ok", update.Type.String(), "send-all", id.String()).Inc() + } else { + notifierUpdateSent.WithLabelValues("ok", update.Type.String(), "send-all").Inc() + } + } } } func (n *Notifier) String() string { - n.l.RLock() - defer n.l.RUnlock() + notifierWaitersForLock.WithLabelValues("lock", "string").Inc() + n.l.Lock() + defer n.l.Unlock() + notifierWaitersForLock.WithLabelValues("lock", "string").Dec() var b strings.Builder - b.WriteString("chans:\n") + fmt.Fprintf(&b, "chans (%d):\n", len(n.nodes)) - for k, v := range n.nodes { - fmt.Fprintf(&b, "\t%d: %p\n", k, v) + var keys []types.NodeID + n.connected.Range(func(key types.NodeID, value bool) bool { + keys = append(keys, key) + return true + }) + sort.Slice(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + for _, key := range keys { + fmt.Fprintf(&b, "\t%d: %p\n", key, n.nodes[key]) } b.WriteString("\n") - b.WriteString("connected:\n") + fmt.Fprintf(&b, "connected (%d):\n", len(n.nodes)) - n.connected.Range(func(k types.NodeID, v bool) bool { - fmt.Fprintf(&b, "\t%d: %t\n", k, v) - return true - }) + for _, key := range keys { + val, _ := n.connected.Load(key) + fmt.Fprintf(&b, "\t%d: %t\n", key, val) + } return b.String() } @@ -230,13 +301,16 @@ func (b *batcher) close() { // addOrPassthrough adds the update to the batcher, if it is not a // type that is currently batched, it will be sent immediately. func (b *batcher) addOrPassthrough(update types.StateUpdate) { + notifierBatcherWaitersForLock.WithLabelValues("lock", "add").Inc() b.mu.Lock() defer b.mu.Unlock() + notifierBatcherWaitersForLock.WithLabelValues("lock", "add").Dec() switch update.Type { case types.StatePeerChanged: b.changedNodeIDs.Add(update.ChangeNodes...) b.nodesChanged = true + notifierBatcherChanges.WithLabelValues().Set(float64(b.changedNodeIDs.Len())) case types.StatePeerChangedPatch: for _, newPatch := range update.ChangePatches { @@ -248,6 +322,7 @@ func (b *batcher) addOrPassthrough(update types.StateUpdate) { } } b.patchesChanged = true + notifierBatcherPatches.WithLabelValues().Set(float64(len(b.patches))) default: b.n.sendAll(update) @@ -257,8 +332,10 @@ func (b *batcher) addOrPassthrough(update types.StateUpdate) { // flush sends all the accumulated patches to all // nodes in the notifier. func (b *batcher) flush() { + notifierBatcherWaitersForLock.WithLabelValues("lock", "flush").Inc() b.mu.Lock() defer b.mu.Unlock() + notifierBatcherWaitersForLock.WithLabelValues("lock", "flush").Dec() if b.nodesChanged || b.patchesChanged { var patches []*tailcfg.PeerChange @@ -296,8 +373,10 @@ func (b *batcher) flush() { } b.changedNodeIDs = set.Slice[types.NodeID]{} + notifierBatcherChanges.WithLabelValues().Set(0) b.nodesChanged = false b.patches = make(map[types.NodeID]tailcfg.PeerChange, len(b.patches)) + notifierBatcherPatches.WithLabelValues().Set(0) b.patchesChanged = false } } diff --git a/hscontrol/notifier/notifier_test.go b/hscontrol/notifier/notifier_test.go index 4d61f134..8841a46d 100644 --- a/hscontrol/notifier/notifier_test.go +++ b/hscontrol/notifier/notifier_test.go @@ -227,7 +227,7 @@ func TestBatcher(t *testing.T) { ch := make(chan types.StateUpdate, 30) defer close(ch) n.AddNode(1, ch) - defer n.RemoveNode(1) + defer n.RemoveNode(1, ch) for _, u := range tt.updates { n.NotifyAll(context.Background(), u) diff --git a/hscontrol/poll.go b/hscontrol/poll.go index e3137cc6..d3c82117 100644 --- a/hscontrol/poll.go +++ b/hscontrol/poll.go @@ -9,13 +9,13 @@ import ( "net/netip" "sort" "strings" - "sync" "time" "github.com/juanfont/headscale/hscontrol/db" "github.com/juanfont/headscale/hscontrol/mapper" "github.com/juanfont/headscale/hscontrol/types" "github.com/rs/zerolog/log" + "github.com/sasha-s/go-deadlock" xslices "golang.org/x/exp/slices" "gorm.io/gorm" "tailscale.com/tailcfg" @@ -29,11 +29,6 @@ type contextKey string const nodeNameContextKey = contextKey("nodeName") -type sessionManager struct { - mu sync.RWMutex - sess map[types.NodeID]*mapSession -} - type mapSession struct { h *Headscale req tailcfg.MapRequest @@ -41,12 +36,13 @@ type mapSession struct { capVer tailcfg.CapabilityVersion mapper *mapper.Mapper - serving bool - servingMu sync.Mutex + cancelChMu deadlock.Mutex - ch chan types.StateUpdate - cancelCh chan struct{} + ch chan types.StateUpdate + cancelCh chan struct{} + cancelChOpen bool + keepAlive time.Duration keepAliveTicker *time.Ticker node *types.Node @@ -77,6 +73,8 @@ func (h *Headscale) newMapSession( } } + ka := keepAliveInterval + (time.Duration(rand.IntN(9000)) * time.Millisecond) + return &mapSession{ h: h, ctx: ctx, @@ -86,13 +84,12 @@ func (h *Headscale) newMapSession( capVer: req.Version, mapper: h.mapper, - // serving indicates if a client is being served. - serving: false, + ch: updateChan, + cancelCh: make(chan struct{}), + cancelChOpen: true, - ch: updateChan, - cancelCh: make(chan struct{}), - - keepAliveTicker: time.NewTicker(keepAliveInterval + (time.Duration(rand.IntN(9000)) * time.Millisecond)), + keepAlive: ka, + keepAliveTicker: nil, // Loggers warnf: warnf, @@ -103,15 +100,23 @@ func (h *Headscale) newMapSession( } func (m *mapSession) close() { - m.servingMu.Lock() - defer m.servingMu.Unlock() - if !m.serving { + m.cancelChMu.Lock() + defer m.cancelChMu.Unlock() + + if !m.cancelChOpen { + mapResponseClosed.WithLabelValues("chanclosed").Inc() return } - m.tracef("mapSession (%p) sending message on cancel chan") - m.cancelCh <- struct{}{} - m.tracef("mapSession (%p) sent message on cancel chan") + m.tracef("mapSession (%p) sending message on cancel chan", m) + select { + case m.cancelCh <- struct{}{}: + mapResponseClosed.WithLabelValues("sent").Inc() + m.tracef("mapSession (%p) sent message on cancel chan", m) + case <-time.After(30 * time.Second): + mapResponseClosed.WithLabelValues("timeout").Inc() + m.tracef("mapSession (%p) timed out sending close message", m) + } } func (m *mapSession) isStreaming() bool { @@ -126,40 +131,12 @@ func (m *mapSession) isReadOnlyUpdate() bool { return !m.req.Stream && m.req.OmitPeers && m.req.ReadOnly } -// handlePoll ensures the node gets the appropriate updates from either -// polling or immediate responses. -// -//nolint:gocyclo +func (m *mapSession) resetKeepAlive() { + m.keepAliveTicker.Reset(m.keepAlive) +} + +// serve handles non-streaming requests. func (m *mapSession) serve() { - // Register with the notifier if this is a streaming - // session - if m.isStreaming() { - // defers are called in reverse order, - // so top one is executed last. - - // Failover the node's routes if any. - defer m.infof("node has disconnected, mapSession: %p", m) - defer m.pollFailoverRoutes("node closing connection", m.node) - - defer m.h.updateNodeOnlineStatus(false, m.node) - defer m.h.nodeNotifier.RemoveNode(m.node.ID) - - defer func() { - m.servingMu.Lock() - defer m.servingMu.Unlock() - - m.serving = false - close(m.cancelCh) - }() - - m.serving = true - - m.h.nodeNotifier.AddNode(m.node.ID, m.ch) - m.h.updateNodeOnlineStatus(true, m.node) - - m.infof("node has connected, mapSession: %p", m) - } - // TODO(kradalby): A set todos to harden: // - func to tell the stream to die, readonly -> false, !stream && omitpeers -> false, true @@ -196,13 +173,43 @@ func (m *mapSession) serve() { return } +} + +// serveLongPoll ensures the node gets the appropriate updates from either +// polling or immediate responses. +// +//nolint:gocyclo +func (m *mapSession) serveLongPoll() { + // Clean up the session when the client disconnects + defer func() { + m.cancelChMu.Lock() + m.cancelChOpen = false + close(m.cancelCh) + m.cancelChMu.Unlock() + + // only update node status if the node channel was removed. + // in principal, it will be removed, but the client rapidly + // reconnects, the channel might be of another connection. + // In that case, it is not closed and the node is still online. + if m.h.nodeNotifier.RemoveNode(m.node.ID, m.ch) { + // Failover the node's routes if any. + m.h.updateNodeOnlineStatus(false, m.node) + m.pollFailoverRoutes("node closing connection", m.node) + } + + m.infof("node has disconnected, mapSession: %p, chan: %p", m, m.ch) + }() + // From version 68, all streaming requests can be treated as read only. + // TODO: Remove when we drop support for 1.48 if m.capVer < 68 { // Error has been handled/written to client in the func // return err := m.handleSaveNode() if err != nil { mapResponseWriteUpdatesInStream.WithLabelValues("error").Inc() + + m.close() return } mapResponseWriteUpdatesInStream.WithLabelValues("ok").Inc() @@ -224,6 +231,13 @@ func (m *mapSession) serve() { ctx, cancel := context.WithCancel(context.WithValue(m.ctx, nodeNameContextKey, m.node.Hostname)) defer cancel() + m.keepAliveTicker = time.NewTicker(m.keepAlive) + + m.h.nodeNotifier.AddNode(m.node.ID, m.ch) + go m.h.updateNodeOnlineStatus(true, m.node) + + m.infof("node has connected, mapSession: %p, chan: %p", m, m.ch) + // Loop through updates and continuously send them to the // client. for { @@ -231,13 +245,21 @@ func (m *mapSession) serve() { select { case <-m.cancelCh: m.tracef("poll cancelled received") - return - case <-ctx.Done(): - m.tracef("poll context done") + mapResponseEnded.WithLabelValues("cancelled").Inc() return - // Consume all updates sent to node - case update := <-m.ch: + case <-ctx.Done(): + m.tracef("poll context done") + mapResponseEnded.WithLabelValues("done").Inc() + return + + // Consume updates sent to node + case update, ok := <-m.ch: + if !ok { + m.tracef("update channel closed, streaming session is likely being replaced") + return + } + m.tracef("received stream update: %s %s", update.Type.String(), update.Message) mapResponseUpdateReceived.WithLabelValues(update.Type.String()).Inc() @@ -303,15 +325,13 @@ func (m *mapSession) serve() { return } - // log.Trace().Str("node", m.node.Hostname).TimeDiff("timeSpent", time.Now(), startMapResp).Str("mkey", m.node.MachineKey.String()).Int("type", int(update.Type)).Msg("finished making map response") - // Only send update if there is change if data != nil { startWrite := time.Now() _, err = m.w.Write(data) if err != nil { mapResponseSent.WithLabelValues("error", updateType).Inc() - m.errf(err, "Could not write the map response, for mapSession: %p", m) + m.errf(err, "could not write the map response(%s), for mapSession: %p", update.Type.String(), m) return } @@ -324,8 +344,12 @@ func (m *mapSession) serve() { log.Trace().Str("node", m.node.Hostname).TimeDiff("timeSpent", time.Now(), startWrite).Str("mkey", m.node.MachineKey.String()).Msg("finished writing mapresp to node") + if debugHighCardinalityMetrics { + mapResponseLastSentSeconds.WithLabelValues(updateType, m.node.ID.String()).Set(float64(time.Now().Unix())) + } mapResponseSent.WithLabelValues("ok", updateType).Inc() m.tracef("update sent") + m.resetKeepAlive() } case <-m.keepAliveTicker.C: @@ -348,6 +372,9 @@ func (m *mapSession) serve() { return } + if debugHighCardinalityMetrics { + mapResponseLastSentSeconds.WithLabelValues("keepalive", m.node.ID.String()).Set(float64(time.Now().Unix())) + } mapResponseSent.WithLabelValues("ok", "keepalive").Inc() } } @@ -404,16 +431,6 @@ func (h *Headscale) updateNodeOnlineStatus(online bool, node *types.Node) { }, node.ID) } -func closeChanWithLog[C chan []byte | chan struct{} | chan types.StateUpdate](channel C, node, name string) { - log.Trace(). - Str("handler", "PollNetMap"). - Str("node", node). - Str("channel", "Done"). - Msg(fmt.Sprintf("Closing %s channel", name)) - - close(channel) -} - func (m *mapSession) handleEndpointUpdate() { m.tracef("received endpoint update") @@ -425,6 +442,17 @@ func (m *mapSession) handleEndpointUpdate() { m.node.ApplyPeerChange(&change) sendUpdate, routesChanged := hostInfoChanged(m.node.Hostinfo, m.req.Hostinfo) + + // The node might not set NetInfo if it has not changed and if + // the full HostInfo object is overrwritten, the information is lost. + // If there is no NetInfo, keep the previous one. + // From 1.66 the client only sends it if changed: + // https://github.com/tailscale/tailscale/commit/e1011f138737286ecf5123ff887a7a5800d129a2 + // TODO(kradalby): evaulate if we need better comparing of hostinfo + // before we take the changes. + if m.req.Hostinfo.NetInfo == nil { + m.req.Hostinfo.NetInfo = m.node.Hostinfo.NetInfo + } m.node.Hostinfo = m.req.Hostinfo logTracePeerChange(m.node.Hostname, sendUpdate, &change) diff --git a/hscontrol/types/config.go b/hscontrol/types/config.go index bd0bfeac..ab17cfb0 100644 --- a/hscontrol/types/config.go +++ b/hscontrol/types/config.go @@ -171,6 +171,7 @@ type LogConfig struct { } type Tuning struct { + NotifierSendTimeout time.Duration BatchChangeDelay time.Duration NodeMapSessionBufferedChanSize int } @@ -232,6 +233,7 @@ func LoadConfig(path string, isFile bool) error { viper.SetDefault("ephemeral_node_inactivity_timeout", "120s") + viper.SetDefault("tuning.notifier_send_timeout", "800ms") viper.SetDefault("tuning.batch_change_delay", "800ms") viper.SetDefault("tuning.node_mapsession_buffered_chan_size", 30) @@ -640,7 +642,7 @@ func GetHeadscaleConfig() (*Config, error) { }, nil } - logConfig := GetLogConfig() + logConfig := GetLogConfig() zerolog.SetGlobalLevel(logConfig.Level) prefix4, err := PrefixV4() @@ -768,6 +770,7 @@ func GetHeadscaleConfig() (*Config, error) { // TODO(kradalby): Document these settings when more stable Tuning: Tuning{ + NotifierSendTimeout: viper.GetDuration("tuning.notifier_send_timeout"), BatchChangeDelay: viper.GetDuration("tuning.batch_change_delay"), NodeMapSessionBufferedChanSize: viper.GetInt("tuning.node_mapsession_buffered_chan_size"), }, diff --git a/hscontrol/types/node.go b/hscontrol/types/node.go index 7a5756ae..3ccadc38 100644 --- a/hscontrol/types/node.go +++ b/hscontrol/types/node.go @@ -43,6 +43,10 @@ func (id NodeID) Uint64() uint64 { return uint64(id) } +func (id NodeID) String() string { + return strconv.FormatUint(id.Uint64(), util.Base10) +} + // Node is a Headscale client. type Node struct { ID NodeID `gorm:"primary_key"` diff --git a/integration/general_test.go b/integration/general_test.go index db9bf83b..245e8f09 100644 --- a/integration/general_test.go +++ b/integration/general_test.go @@ -1,6 +1,7 @@ package integration import ( + "context" "encoding/json" "fmt" "net/netip" @@ -15,6 +16,7 @@ import ( "github.com/rs/zerolog/log" "github.com/samber/lo" "github.com/stretchr/testify/assert" + "golang.org/x/sync/errgroup" "tailscale.com/client/tailscale/apitype" "tailscale.com/types/key" ) @@ -829,24 +831,10 @@ func TestPingAllByIPManyUpDown(t *testing.T) { "user2": len(MustTestVersions), } - headscaleConfig := map[string]string{ - "HEADSCALE_DERP_URLS": "", - "HEADSCALE_DERP_SERVER_ENABLED": "true", - "HEADSCALE_DERP_SERVER_REGION_ID": "999", - "HEADSCALE_DERP_SERVER_REGION_CODE": "headscale", - "HEADSCALE_DERP_SERVER_REGION_NAME": "Headscale Embedded DERP", - "HEADSCALE_DERP_SERVER_STUN_LISTEN_ADDR": "0.0.0.0:3478", - "HEADSCALE_DERP_SERVER_PRIVATE_KEY_PATH": "/tmp/derp.key", - - // Envknob for enabling DERP debug logs - "DERP_DEBUG_LOGS": "true", - "DERP_PROBER_DEBUG_LOGS": "true", - } - err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, - hsic.WithTestName("pingallbyip"), - hsic.WithConfigEnv(headscaleConfig), + hsic.WithTestName("pingallbyipmany"), + hsic.WithEmbeddedDERPServerOnly(), hsic.WithTLS(), hsic.WithHostnameAsServerURL(), ) @@ -870,19 +858,35 @@ func TestPingAllByIPManyUpDown(t *testing.T) { success := pingAllHelper(t, allClients, allAddrs) t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps)) + wg, _ := errgroup.WithContext(context.Background()) + for run := range 3 { t.Logf("Starting DownUpPing run %d", run+1) for _, client := range allClients { - t.Logf("taking down %q", client.Hostname()) - client.Down() + c := client + wg.Go(func() error { + t.Logf("taking down %q", c.Hostname()) + return c.Down() + }) + } + + if err := wg.Wait(); err != nil { + t.Fatalf("failed to take down all nodes: %s", err) } time.Sleep(5 * time.Second) for _, client := range allClients { - t.Logf("bringing up %q", client.Hostname()) - client.Up() + c := client + wg.Go(func() error { + t.Logf("bringing up %q", c.Hostname()) + return c.Up() + }) + } + + if err := wg.Wait(); err != nil { + t.Fatalf("failed to take down all nodes: %s", err) } time.Sleep(5 * time.Second) diff --git a/integration/hsic/hsic.go b/integration/hsic/hsic.go index a118b6fc..5b55a0a8 100644 --- a/integration/hsic/hsic.go +++ b/integration/hsic/hsic.go @@ -286,9 +286,13 @@ func New( } env := []string{ - "HEADSCALE_PROFILING_ENABLED=1", - "HEADSCALE_PROFILING_PATH=/tmp/profile", + "HEADSCALE_DEBUG_PROFILING_ENABLED=1", + "HEADSCALE_DEBUG_PROFILING_PATH=/tmp/profile", "HEADSCALE_DEBUG_DUMP_MAPRESPONSE_PATH=/tmp/mapresponses", + "HEADSCALE_DEBUG_DEADLOCK=1", + "HEADSCALE_DEBUG_DEADLOCK_TIMEOUT=5s", + "HEADSCALE_DEBUG_HIGH_CARDINALITY_METRICS=1", + "HEADSCALE_DEBUG_DUMP_CONFIG=1", } for key, value := range hsic.env { env = append(env, fmt.Sprintf("%s=%s", key, value)) @@ -397,7 +401,7 @@ func (t *HeadscaleInContainer) Shutdown() error { ) } - err = t.SaveMetrics("/tmp/control/metrics.txt") + err = t.SaveMetrics(fmt.Sprintf("/tmp/control/%s_metrics.txt", t.hostname)) if err != nil { log.Printf( "Failed to metrics from control: %s", @@ -747,7 +751,7 @@ func createCertificate(hostname string) ([]byte, []byte, error) { Locality: []string{"Leiden"}, }, NotBefore: time.Now(), - NotAfter: time.Now().Add(60 * time.Minute), + NotAfter: time.Now().Add(60 * time.Hour), IsCA: true, ExtKeyUsage: []x509.ExtKeyUsage{ x509.ExtKeyUsageClientAuth, diff --git a/integration/scenario.go b/integration/scenario.go index 3f0eb7d2..bd004247 100644 --- a/integration/scenario.go +++ b/integration/scenario.go @@ -51,8 +51,11 @@ var ( tailscaleVersions2021 = map[string]bool{ "head": true, "unstable": true, - "1.60": true, // CapVer: 82 - "1.58": true, // CapVer: 82 + "1.66": true, // CapVer: not checked + "1.64": true, // CapVer: not checked + "1.62": true, // CapVer: not checked + "1.60": true, // CapVer: not checked + "1.58": true, // CapVer: not checked "1.56": true, // CapVer: 82 "1.54": true, // CapVer: 79 "1.52": true, // CapVer: 79 @@ -423,8 +426,10 @@ func (s *Scenario) WaitForTailscaleSync() error { if err != nil { for _, user := range s.users { for _, client := range user.Clients { - peers, _ := client.PrettyPeers() - log.Println(peers) + peers, allOnline, _ := client.FailingPeersAsString() + if !allOnline { + log.Println(peers) + } } } } diff --git a/integration/tailscale.go b/integration/tailscale.go index 6bcf6073..2ea3faa9 100644 --- a/integration/tailscale.go +++ b/integration/tailscale.go @@ -36,5 +36,8 @@ type TailscaleClient interface { Ping(hostnameOrIP string, opts ...tsic.PingOption) error Curl(url string, opts ...tsic.CurlOption) (string, error) ID() string - PrettyPeers() (string, error) + + // FailingPeersAsString returns a formatted-ish multi-line-string of peers in the client + // and a bool indicating if the clients online count and peer count is equal. + FailingPeersAsString() (string, bool, error) } diff --git a/integration/tsic/tsic.go b/integration/tsic/tsic.go index 6ae0226a..0e3c91f8 100644 --- a/integration/tsic/tsic.go +++ b/integration/tsic/tsic.go @@ -691,15 +691,18 @@ 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) { +// FailingPeersAsString returns a formatted-ish multi-line-string of peers in the client +// and a bool indicating if the clients online count and peer count is equal. +func (t *TailscaleInContainer) FailingPeersAsString() (string, bool, error) { status, err := t.Status() if err != nil { - return "", fmt.Errorf("failed to get FQDN: %w", err) + return "", false, fmt.Errorf("failed to get FQDN: %w", err) } - str := fmt.Sprintf("Peers of %s\n", t.hostname) - str += "Hostname\tOnline\tLastSeen\n" + var b strings.Builder + + fmt.Fprintf(&b, "Peers of %s\n", t.hostname) + fmt.Fprint(&b, "Hostname\tOnline\tLastSeen\n") peerCount := len(status.Peers()) onlineCount := 0 @@ -711,12 +714,12 @@ func (t *TailscaleInContainer) PrettyPeers() (string, error) { onlineCount++ } - str += fmt.Sprintf("%s\t%t\t%s\n", peer.HostName, peer.Online, peer.LastSeen) + fmt.Fprintf(&b, "%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) + fmt.Fprintf(&b, "Peer Count: %d, Online Count: %d\n\n", peerCount, onlineCount) - return str, nil + return b.String(), peerCount == onlineCount, nil } // WaitForNeedsLogin blocks until the Tailscale (tailscaled) instance has From 51b56ba447b7b2b3a38ee4e80ce9bc6f179f4144 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 24 May 2024 10:30:11 +0100 Subject: [PATCH 073/145] Update flake.lock (#1952) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/b3fcfcfabd01b947a1e4f36622bbffa3985bdac6?narHash=sha256-iJYnKMtLi5u6hZhJm94cRNSDG5Rz6ZzIkGbhPFtDRm0%3D' (2024-05-15) → 'github:NixOS/nixpkgs/02923630b89aa1ab36ef8e422501a6f4fd4b2016?narHash=sha256-OhysviwHQz4p2HZL4g7XGMLoUbWMjkMr/ogaR3VUYNA%3D' (2024-05-18) Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index ffa1f931..351c657c 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1715774670, - "narHash": "sha256-iJYnKMtLi5u6hZhJm94cRNSDG5Rz6ZzIkGbhPFtDRm0=", + "lastModified": 1716062047, + "narHash": "sha256-OhysviwHQz4p2HZL4g7XGMLoUbWMjkMr/ogaR3VUYNA=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "b3fcfcfabd01b947a1e4f36622bbffa3985bdac6", + "rev": "02923630b89aa1ab36ef8e422501a6f4fd4b2016", "type": "github" }, "original": { From 5a4e52b727d402d78a5f3c5ef6b74dcb3b448fe2 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby <kristoffer@dalby.cc> Date: Mon, 27 May 2024 11:53:37 +0100 Subject: [PATCH 074/145] remove last_successful_update error check (#1959) most of the time we dont even check this error and checking the string for particular errors is very flake as different databases (sqlite and psql) use different error messages, and some users might have it in other languages. Fixes #1956 Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> --- hscontrol/db/db.go | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/hscontrol/db/db.go b/hscontrol/db/db.go index a30939c1..b87d6da6 100644 --- a/hscontrol/db/db.go +++ b/hscontrol/db/db.go @@ -92,7 +92,7 @@ func NewHeadscaleDatabase( RenameColumn(&types.Node{}, "nickname", "given_name") dbConn.Model(&types.Node{}).Where("auth_key_id = ?", 0).Update("auth_key_id", nil) - // If the Node table has a column for registered, + // 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") { @@ -319,14 +319,8 @@ func NewHeadscaleDatabase( // no longer used. ID: "202402151347", Migrate: func(tx *gorm.DB) error { - err := tx.Migrator().DropColumn(&types.Node{}, "last_successful_update") - if err != nil && strings.Contains(err.Error(), `of relation "nodes" does not exist`) { - return nil - } else { - return err - } - - return err + _ = tx.Migrator().DropColumn(&types.Node{}, "last_successful_update") + return nil }, Rollback: func(tx *gorm.DB) error { return nil From 5f9c26930ce796284a8fc7ad167f76e187813d64 Mon Sep 17 00:00:00 2001 From: Dongjun Na <kmu5544616@gmail.com> Date: Wed, 29 May 2024 01:11:39 +0900 Subject: [PATCH 075/145] fixed typo and path (#1960) --- docs/index.md | 2 +- docs/running-headscale-linux-manual.md | 2 +- docs/running-headscale-openbsd.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/index.md b/docs/index.md index d13339d8..f0b8bb00 100644 --- a/docs/index.md +++ b/docs/index.md @@ -8,7 +8,7 @@ hide: `headscale` is an open source, self-hosted implementation of the Tailscale control server. -This page contains the documentation for the latest version of headscale. Please also check our [FAQ](/faq/). +This page contains the documentation for the latest version of headscale. Please also check our [FAQ](faq.md). Join our [Discord](https://discord.gg/c84AZQhmpx) server for a chat and community support. diff --git a/docs/running-headscale-linux-manual.md b/docs/running-headscale-linux-manual.md index 4108208f..3651c892 100644 --- a/docs/running-headscale-linux-manual.md +++ b/docs/running-headscale-linux-manual.md @@ -57,7 +57,7 @@ describing how to make `headscale` run properly in a server environment. touch /etc/headscale/config.yaml ``` - **(Strongly Recommended)** Download a copy of the [example configuration][config-example.yaml](https://github.com/juanfont/headscale/blob/main/config-example.yaml) from the headscale repository. + **(Strongly Recommended)** Download a copy of the [example configuration](https://github.com/juanfont/headscale/blob/main/config-example.yaml) from the headscale repository. 1. Start the headscale server: diff --git a/docs/running-headscale-openbsd.md b/docs/running-headscale-openbsd.md index e1d8d83f..72c7bf79 100644 --- a/docs/running-headscale-openbsd.md +++ b/docs/running-headscale-openbsd.md @@ -93,7 +93,7 @@ describing how to make `headscale` run properly in a server environment. touch /etc/headscale/config.yaml ``` -**(Strongly Recommended)** Download a copy of the [example configuration][config-example.yaml](https://github.com/juanfont/headscale/blob/main/config-example.yaml) from the headscale repository. +**(Strongly Recommended)** Download a copy of the [example configuration](https://github.com/juanfont/headscale/blob/main/config-example.yaml) from the headscale repository. 1. Start the headscale server: From 1f4b59566a27d5933f32ae58d0fcc71ba8f1ea9a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 9 Jun 2024 07:23:16 +0000 Subject: [PATCH 076/145] flake.lock: Update (#1958) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 351c657c..060a290a 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1716062047, - "narHash": "sha256-OhysviwHQz4p2HZL4g7XGMLoUbWMjkMr/ogaR3VUYNA=", + "lastModified": 1717774105, + "narHash": "sha256-HV97wqUQv9wvptiHCb3Y0/YH0lJ60uZ8FYfEOIzYEqI=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "02923630b89aa1ab36ef8e422501a6f4fd4b2016", + "rev": "d226935fd75012939397c83f6c385e4d6d832288", "type": "github" }, "original": { From 51676c668bbe1bd36747b4e8f3b54ca58ef8c2b8 Mon Sep 17 00:00:00 2001 From: Lars Kiesow <lkiesow@uos.de> Date: Sat, 15 Jun 2024 09:40:49 +0200 Subject: [PATCH 077/145] Make registration screen easier to use (#1975) --- CHANGELOG.md | 1 + hscontrol/handlers.go | 14 +++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03516fd6..dce08f68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,7 @@ after improving the test harness as part of adopting [#1460](https://github.com/ - Log available update as warning [#1877](https://github.com/juanfont/headscale/pull/1877) - Add `autogroup:internet` to Policy [#1917](https://github.com/juanfont/headscale/pull/1917) - Restore foreign keys and add constraints [#1562](https://github.com/juanfont/headscale/pull/1562) +- Make registration page easier to use on mobile devices ## 0.22.3 (2023-05-12) diff --git a/hscontrol/handlers.go b/hscontrol/handlers.go index a6bbd1b8..6efe1984 100644 --- a/hscontrol/handlers.go +++ b/hscontrol/handlers.go @@ -143,6 +143,18 @@ var registerWebAPITemplate = template.Must( <html> <head> <title>Registration - Headscale + +

headscale

@@ -150,7 +162,7 @@ var registerWebAPITemplate = template.Must(

Run the command below in the headscale server to add this machine to your network:

-
headscale nodes register --user USERNAME --key {{.Key}}
+ headscale nodes register --user USERNAME --key {{.Key}} `)) From dfc089ed6a5c116f3c8dff17f97a68822468fd2c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 16 Jun 2024 20:24:08 +0000 Subject: [PATCH 078/145] flake.lock: Update (#1979) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 060a290a..51019abd 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1717774105, - "narHash": "sha256-HV97wqUQv9wvptiHCb3Y0/YH0lJ60uZ8FYfEOIzYEqI=", + "lastModified": 1718276985, + "narHash": "sha256-u1fA0DYQYdeG+5kDm1bOoGcHtX0rtC7qs2YA2N1X++I=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "d226935fd75012939397c83f6c385e4d6d832288", + "rev": "3f84a279f1a6290ce154c5531378acc827836fbb", "type": "github" }, "original": { From 99e91a9d8a740df793b54f4070de1739d894bf53 Mon Sep 17 00:00:00 2001 From: Kyhwana Pardus Date: Sun, 23 Jun 2024 10:47:26 +1200 Subject: [PATCH 079/145] Update reverse-proxy.md (#1986) Add blurb about how cloudflare proxy/tunnels is not supported/will not work --- docs/reverse-proxy.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/reverse-proxy.md b/docs/reverse-proxy.md index c6fd4b16..23c61c26 100644 --- a/docs/reverse-proxy.md +++ b/docs/reverse-proxy.md @@ -15,6 +15,10 @@ The reverse proxy MUST be configured to support WebSockets, as it is needed for WebSockets support is required when using the headscale embedded DERP server. In this case, you will also need to expose the UDP port used for STUN (by default, udp/3478). Please check our [config-example.yaml](https://github.com/juanfont/headscale/blob/main/config-example.yaml). +### Cloudflare + +Running headscale behind a cloudflare proxy or cloudflare tunnel is not supported and will not work as Cloudflare does not support WebSocket POSTs as required by the Tailscale protocol. See [this issue](https://github.com/juanfont/headscale/issues/1468) + ### TLS Headscale can be configured not to use TLS, leaving it to the reverse proxy to handle. Add the following configuration values to your headscale config file. From 69c33658f63fd5af1c19ba176afaa31993aa83e4 Mon Sep 17 00:00:00 2001 From: Lars Kiesow Date: Sun, 23 Jun 2024 00:52:23 +0200 Subject: [PATCH 080/145] Fix android docs (#1976) The current Tailscale app for Android looks and behaves differently. This patch updates the documentation for that. --- docs/android-client.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/android-client.md b/docs/android-client.md index d4f8129c..21dd8d21 100644 --- a/docs/android-client.md +++ b/docs/android-client.md @@ -12,8 +12,8 @@ Ensure that the installed version is at least 1.30.0, as that is the first relea ## Configuring the headscale URL -After opening the app, the kebab menu icon (three dots) on the top bar on the right must be repeatedly opened and closed until the _Change server_ option appears in the menu. This is where you can enter your headscale URL. +After opening the app: -A screen recording of this process can be seen in the `tailscale-android` PR which implemented this functionality: - -After saving and restarting the app, selecting the regular _Sign in_ option (non-SSO) should open up the headscale authentication page. +- Open setting and go into account settings +- In the kebab menu icon (three dots) on the top bar on the right select “Use an alternate server” +- Enter your server URL and follow the instructions From 8f8f469c0ac2ecbdfce79f14f055cbf1e1ff444d Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sun, 23 Jun 2024 22:06:50 +0200 Subject: [PATCH 081/145] Remove allocations of lists before use (#1989) * policy: remove allocs before appends in acls Signed-off-by: Kristoffer Dalby * notifier: make batcher tests stable/non-flaky Signed-off-by: Kristoffer Dalby * {db,derp,mapper}: dont allocate until append Signed-off-by: Kristoffer Dalby --------- Signed-off-by: Kristoffer Dalby --- hscontrol/db/node.go | 6 ++--- hscontrol/db/routes.go | 4 +-- hscontrol/derp/derp.go | 2 +- hscontrol/mapper/mapper.go | 2 +- hscontrol/notifier/notifier_test.go | 16 ++++++++++++ hscontrol/policy/acls.go | 40 ++++++++++++++--------------- hscontrol/policy/acls_test.go | 8 +++--- 7 files changed, 46 insertions(+), 32 deletions(-) diff --git a/hscontrol/db/node.go b/hscontrol/db/node.go index c675dc7c..e36d6ed1 100644 --- a/hscontrol/db/node.go +++ b/hscontrol/db/node.go @@ -215,7 +215,7 @@ func SetTags( return nil } - newTags := types.StringList{} + var newTags types.StringList for _, tag := range tags { if !util.StringOrPrefixListContains(newTags, tag) { newTags = append(newTags, tag) @@ -452,7 +452,7 @@ func GetAdvertisedRoutes(tx *gorm.DB, node *types.Node) ([]netip.Prefix, error) return nil, fmt.Errorf("getting advertised routes for node(%d): %w", node.ID, err) } - prefixes := []netip.Prefix{} + var prefixes []netip.Prefix for _, route := range routes { prefixes = append(prefixes, netip.Prefix(route.Prefix)) } @@ -478,7 +478,7 @@ func GetEnabledRoutes(tx *gorm.DB, node *types.Node) ([]netip.Prefix, error) { return nil, fmt.Errorf("getting enabled routes for node(%d): %w", node.ID, err) } - prefixes := []netip.Prefix{} + var prefixes []netip.Prefix for _, route := range routes { prefixes = append(prefixes, netip.Prefix(route.Prefix)) } diff --git a/hscontrol/db/routes.go b/hscontrol/db/routes.go index 74b2b4b7..3b897190 100644 --- a/hscontrol/db/routes.go +++ b/hscontrol/db/routes.go @@ -222,7 +222,7 @@ func DeleteRoute( return nil, err } - routesToDelete := types.Routes{} + var routesToDelete types.Routes for _, r := range routes { if r.IsExitRoute() { routesToDelete = append(routesToDelete, r) @@ -623,7 +623,7 @@ func EnableAutoApprovedRoutes( log.Trace().Interface("routes", routes).Msg("routes for autoapproving") - approvedRoutes := types.Routes{} + var approvedRoutes types.Routes for _, advertisedRoute := range routes { if advertisedRoute.Enabled { diff --git a/hscontrol/derp/derp.go b/hscontrol/derp/derp.go index 80ec520d..3afcb4ea 100644 --- a/hscontrol/derp/derp.go +++ b/hscontrol/derp/derp.go @@ -81,7 +81,7 @@ func mergeDERPMaps(derpMaps []*tailcfg.DERPMap) *tailcfg.DERPMap { } func GetDERPMap(cfg types.DERPConfig) *tailcfg.DERPMap { - derpMaps := make([]*tailcfg.DERPMap, 0) + var derpMaps []*tailcfg.DERPMap for _, path := range cfg.Paths { log.Debug(). diff --git a/hscontrol/mapper/mapper.go b/hscontrol/mapper/mapper.go index d4f4392a..a6fa9ad6 100644 --- a/hscontrol/mapper/mapper.go +++ b/hscontrol/mapper/mapper.go @@ -102,7 +102,7 @@ func generateUserProfiles( userMap[peer.User.Name] = peer.User // not worth checking if already is there } - profiles := []tailcfg.UserProfile{} + var profiles []tailcfg.UserProfile for _, user := range userMap { displayName := user.Name diff --git a/hscontrol/notifier/notifier_test.go b/hscontrol/notifier/notifier_test.go index 8841a46d..c41e0039 100644 --- a/hscontrol/notifier/notifier_test.go +++ b/hscontrol/notifier/notifier_test.go @@ -3,6 +3,7 @@ package notifier import ( "context" "net/netip" + "sort" "testing" "time" @@ -221,6 +222,11 @@ func TestBatcher(t *testing.T) { // We will call flush manually for the tests, // so do not run the worker. BatchChangeDelay: time.Hour, + + // Since we do not load the config, we wont get the + // default, so set it manually so we dont time out + // and have flakes. + NotifierSendTimeout: time.Second, }, }) @@ -241,6 +247,16 @@ func TestBatcher(t *testing.T) { got = append(got, out) } + // Make the inner order stable for comparison. + for _, u := range got { + sort.Slice(u.ChangeNodes, func(i, j int) bool { + return u.ChangeNodes[i] < u.ChangeNodes[j] + }) + sort.Slice(u.ChangePatches, func(i, j int) bool { + return u.ChangePatches[i].NodeID < u.ChangePatches[j].NodeID + }) + } + if diff := cmp.Diff(tt.want, got, util.Comparers...); diff != "" { t.Errorf("batcher() unexpected result (-want +got):\n%s", diff) } diff --git a/hscontrol/policy/acls.go b/hscontrol/policy/acls.go index 1196995d..9dde401b 100644 --- a/hscontrol/policy/acls.go +++ b/hscontrol/policy/acls.go @@ -180,14 +180,14 @@ func (pol *ACLPolicy) CompileFilterRules( return tailcfg.FilterAllowAll, nil } - rules := []tailcfg.FilterRule{} + var rules []tailcfg.FilterRule for index, acl := range pol.ACLs { if acl.Action != "accept" { return nil, ErrInvalidAction } - srcIPs := []string{} + var srcIPs []string for srcIndex, src := range acl.Sources { srcs, err := pol.expandSource(src, nodes) if err != nil { @@ -221,7 +221,7 @@ func (pol *ACLPolicy) CompileFilterRules( return nil, err } - dests := []tailcfg.NetPortRange{} + var dests []tailcfg.NetPortRange for _, dest := range expanded.Prefixes() { for _, port := range *ports { pr := tailcfg.NetPortRange{ @@ -251,8 +251,7 @@ func ReduceFilterRules(node *types.Node, rules []tailcfg.FilterRule) []tailcfg.F for _, rule := range rules { // record if the rule is actually relevant for the given node. - dests := []tailcfg.NetPortRange{} - + var dests []tailcfg.NetPortRange DEST_LOOP: for _, dest := range rule.DstPorts { expanded, err := util.ParseIPSet(dest.IP, nil) @@ -301,7 +300,7 @@ func (pol *ACLPolicy) CompileSSHPolicy( return nil, nil } - rules := []*tailcfg.SSHRule{} + var rules []*tailcfg.SSHRule acceptAction := tailcfg.SSHAction{ Message: "", @@ -533,8 +532,7 @@ func (pol *ACLPolicy) expandSource( return []string{}, err } - prefixes := []string{} - + var prefixes []string for _, prefix := range ipSet.Prefixes() { prefixes = append(prefixes, prefix.String()) } @@ -615,8 +613,8 @@ func excludeCorrectlyTaggedNodes( nodes types.Nodes, user string, ) types.Nodes { - out := types.Nodes{} - tags := []string{} + var out types.Nodes + var tags []string for tag := range aclPolicy.TagOwners { owners, _ := expandOwnersFromTag(aclPolicy, user) ns := append(owners, user) @@ -661,7 +659,7 @@ func expandPorts(portsStr string, isWild bool) (*[]tailcfg.PortRange, error) { return nil, ErrWildcardIsNeeded } - ports := []tailcfg.PortRange{} + var ports []tailcfg.PortRange for _, portStr := range strings.Split(portsStr, ",") { log.Trace().Msgf("parsing portstring: %s", portStr) rang := strings.Split(portStr, "-") @@ -737,7 +735,7 @@ func expandOwnersFromTag( func (pol *ACLPolicy) expandUsersFromGroup( group string, ) ([]string, error) { - users := []string{} + var users []string log.Trace().Caller().Interface("pol", pol).Msg("test") aclGroups, ok := pol.Groups[group] if !ok { @@ -772,7 +770,7 @@ func (pol *ACLPolicy) expandIPsFromGroup( group string, nodes types.Nodes, ) (*netipx.IPSet, error) { - build := netipx.IPSetBuilder{} + var build netipx.IPSetBuilder users, err := pol.expandUsersFromGroup(group) if err != nil { @@ -792,7 +790,7 @@ func (pol *ACLPolicy) expandIPsFromTag( alias string, nodes types.Nodes, ) (*netipx.IPSet, error) { - build := netipx.IPSetBuilder{} + var build netipx.IPSetBuilder // check for forced tags for _, node := range nodes { @@ -841,7 +839,7 @@ func (pol *ACLPolicy) expandIPsFromUser( user string, nodes types.Nodes, ) (*netipx.IPSet, error) { - build := netipx.IPSetBuilder{} + var build netipx.IPSetBuilder filteredNodes := filterNodesByUser(nodes, user) filteredNodes = excludeCorrectlyTaggedNodes(pol, filteredNodes, user) @@ -866,7 +864,7 @@ func (pol *ACLPolicy) expandIPsFromSingleIP( matches := nodes.FilterByIP(ip) - build := netipx.IPSetBuilder{} + var build netipx.IPSetBuilder build.Add(ip) for _, node := range matches { @@ -881,7 +879,7 @@ func (pol *ACLPolicy) expandIPsFromIPPrefix( nodes types.Nodes, ) (*netipx.IPSet, error) { log.Trace().Str("prefix", prefix.String()).Msg("expandAlias got prefix") - build := netipx.IPSetBuilder{} + var build netipx.IPSetBuilder build.AddPrefix(prefix) // This is suboptimal and quite expensive, but if we only add the prefix, we will miss all the relevant IPv6 @@ -931,8 +929,8 @@ func isAutoGroup(str string) bool { func (pol *ACLPolicy) TagsOfNode( node *types.Node, ) ([]string, []string) { - validTags := make([]string, 0) - invalidTags := make([]string, 0) + var validTags []string + var invalidTags []string // TODO(kradalby): Why is this sometimes nil? coming from tailNode? if node == nil { @@ -973,7 +971,7 @@ func (pol *ACLPolicy) TagsOfNode( } func filterNodesByUser(nodes types.Nodes, user string) types.Nodes { - out := types.Nodes{} + var out types.Nodes for _, node := range nodes { if node.User.Name == user { out = append(out, node) @@ -989,7 +987,7 @@ func FilterNodesByACL( nodes types.Nodes, filter []tailcfg.FilterRule, ) types.Nodes { - result := types.Nodes{} + var result types.Nodes for index, peer := range nodes { if peer.ID == node.ID { diff --git a/hscontrol/policy/acls_test.go b/hscontrol/policy/acls_test.go index b0cafe10..c1e7ae08 100644 --- a/hscontrol/policy/acls_test.go +++ b/hscontrol/policy/acls_test.go @@ -943,7 +943,7 @@ func Test_listNodesInUser(t *testing.T) { }, user: "mickael", }, - want: types.Nodes{}, + want: nil, }, } for _, test := range tests { @@ -1645,7 +1645,7 @@ func TestACLPolicy_generateFilterRules(t *testing.T) { name: "no-policy", field: field{}, args: args{}, - want: []tailcfg.FilterRule{}, + want: nil, wantErr: false, }, { @@ -2896,7 +2896,7 @@ func Test_getFilteredByACLPeers(t *testing.T) { User: types.User{Name: "marc"}, }, }, - want: types.Nodes{}, + want: nil, }, { // Investigating 699 @@ -3426,7 +3426,7 @@ func TestSSHRules(t *testing.T) { }, }, }, - want: &tailcfg.SSHPolicy{Rules: []*tailcfg.SSHRule{}}, + want: &tailcfg.SSHPolicy{Rules: nil}, }, } From 4a34cfc4a6fbdbde818647b818dc171512fa1253 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sun, 23 Jun 2024 22:06:59 +0200 Subject: [PATCH 082/145] Make write-ahead-log default and configurable for sqlite (#1985) * correctly enable WAL log for sqlite this commit makes headscale correctly enable write-ahead-log for sqlite and adds an option to turn it on and off. WAL is enabled by default and should make sqlite perform a lot better, even further eliminating the need to use postgres. It also adds a couple of other useful defaults. Signed-off-by: Kristoffer Dalby * update changelog Signed-off-by: Kristoffer Dalby --------- Signed-off-by: Kristoffer Dalby --- CHANGELOG.md | 1 + config-example.yaml | 4 ++++ hscontrol/db/db.go | 20 ++++++++++++++++++-- hscontrol/types/config.go | 6 +++++- 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dce08f68..666e1670 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,7 @@ after improving the test harness as part of adopting [#1460](https://github.com/ - Add `autogroup:internet` to Policy [#1917](https://github.com/juanfont/headscale/pull/1917) - Restore foreign keys and add constraints [#1562](https://github.com/juanfont/headscale/pull/1562) - Make registration page easier to use on mobile devices +- Make write-ahead-log default on and configurable for SQLite [#1985](https://github.com/juanfont/headscale/pull/1985) ## 0.22.3 (2023-05-12) diff --git a/config-example.yaml b/config-example.yaml index 867f8903..f1bc1631 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -144,6 +144,10 @@ database: sqlite: path: /var/lib/headscale/db.sqlite + # Enable WAL mode for SQLite. This is recommended for production environments. + # https://www.sqlite.org/wal.html + write_ahead_log: true + # # Postgres config # postgres: # # If using a Unix socket to connect to Postgres, set the socket path in the 'host' field and leave 'port' blank. diff --git a/hscontrol/db/db.go b/hscontrol/db/db.go index b87d6da6..69994d02 100644 --- a/hscontrol/db/db.go +++ b/hscontrol/db/db.go @@ -434,13 +434,29 @@ func openDB(cfg types.DatabaseConfig) (*gorm.DB, error) { Msg("Opening database") db, err := gorm.Open( - sqlite.Open(cfg.Sqlite.Path+"?_synchronous=1&_journal_mode=WAL"), + sqlite.Open(cfg.Sqlite.Path), &gorm.Config{ Logger: dbLogger, }, ) - db.Exec("PRAGMA foreign_keys=ON") + if err := db.Exec(` + PRAGMA foreign_keys=ON; + PRAGMA busy_timeout=10000; + PRAGMA auto_vacuum=INCREMENTAL; + PRAGMA synchronous=NORMAL; + `).Error; err != nil { + return nil, fmt.Errorf("enabling foreign keys: %w", err) + } + + if cfg.Sqlite.WriteAheadLog { + if err := db.Exec(` + PRAGMA journal_mode=WAL; + PRAGMA wal_autocheckpoint=0; + `).Error; err != nil { + return nil, fmt.Errorf("setting WAL mode: %w", err) + } + } // The pure Go SQLite library does not handle locking in // the same way as the C based one and we cant use the gorm diff --git a/hscontrol/types/config.go b/hscontrol/types/config.go index ab17cfb0..00934af6 100644 --- a/hscontrol/types/config.go +++ b/hscontrol/types/config.go @@ -81,7 +81,8 @@ type Config struct { } type SqliteConfig struct { - Path string + Path string + WriteAheadLog bool } type PostgresConfig struct { @@ -222,6 +223,8 @@ func LoadConfig(path string, isFile bool) error { viper.SetDefault("database.postgres.max_idle_conns", 10) viper.SetDefault("database.postgres.conn_max_idle_time_secs", 3600) + viper.SetDefault("database.sqlite.write_ahead_log", true) + viper.SetDefault("oidc.scope", []string{oidc.ScopeOpenID, "profile", "email"}) viper.SetDefault("oidc.strip_email_domain", true) viper.SetDefault("oidc.only_start_if_oidc_is_available", true) @@ -443,6 +446,7 @@ func GetDatabaseConfig() DatabaseConfig { Path: util.AbsolutePathFromConfigPath( viper.GetString("database.sqlite.path"), ), + WriteAheadLog: viper.GetBool("database.sqlite.write_ahead_log"), }, Postgres: PostgresConfig{ Host: viper.GetString("database.postgres.host"), From 14a3f94f0cab3f88322350bc060f28c406754821 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 26 Jun 2024 13:44:40 +0200 Subject: [PATCH 083/145] fix search domains and remove username from magicdns (#1987) --- CHANGELOG.md | 4 ++ config-example.yaml | 9 +++ hscontrol/mapper/mapper.go | 52 ++++++++------- hscontrol/mapper/mapper_test.go | 11 +-- hscontrol/mapper/tail.go | 2 +- hscontrol/types/config.go | 29 ++++---- hscontrol/types/node.go | 25 ++++--- hscontrol/types/node_test.go | 114 ++++++++++++++++++++++++++++---- 8 files changed, 183 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 666e1670..fced0b6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,10 @@ after improving the test harness as part of adopting [#1460](https://github.com/ - Prefixes are now defined per v4 and v6 range. [#1756](https://github.com/juanfont/headscale/pull/1756) - `ip_prefixes` option is now `prefixes.v4` and `prefixes.v6` - `prefixes.allocation` can be set to assign IPs at `sequential` or `random`. [#1869](https://github.com/juanfont/headscale/pull/1869) +- MagicDNS domains no longer contain usernames []() + - This is in preperation to fix Headscales implementation of tags which currently does not correctly remove the link between a tagged device and a user. As tagged devices will not have a user, this will require a change to the DNS generation, removing the username, see [#1369](https://github.com/juanfont/headscale/issues/1369) for more information. + - `use_username_in_magic_dns` can be used to turn this behaviour on again, but note that this option _will be removed_ when tags are fixed. + - This option brings Headscales behaviour in line with Tailscale. ### Changes diff --git a/config-example.yaml b/config-example.yaml index f1bc1631..4608317a 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -265,6 +265,15 @@ dns_config: # Only works if there is at least a nameserver defined. magic_dns: true + # DEPRECATED + # Use the username as part of the DNS name for nodes, with this option enabled: + # node1.username.example.com + # while when this is disabled: + # node1.example.com + # This is a legacy option as Headscale has have this wrongly implemented + # while in upstream Tailscale, the username is not included. + use_username_in_magic_dns: false + # Defines the base domain to create the hostnames for MagicDNS. # `base_domain` must be a FQDNs, without the trailing dot. # The FQDN of the hosts will be diff --git a/hscontrol/mapper/mapper.go b/hscontrol/mapper/mapper.go index a6fa9ad6..adc49669 100644 --- a/hscontrol/mapper/mapper.go +++ b/hscontrol/mapper/mapper.go @@ -122,37 +122,41 @@ func generateUserProfiles( } func generateDNSConfig( - base *tailcfg.DNSConfig, + cfg *types.Config, baseDomain string, node *types.Node, peers types.Nodes, ) *tailcfg.DNSConfig { - dnsConfig := base.Clone() + if cfg.DNSConfig == nil { + return nil + } + + dnsConfig := cfg.DNSConfig.Clone() // if MagicDNS is enabled - if base != nil && base.Proxied { - // Only inject the Search Domain of the current user - // shared nodes should use their full FQDN - dnsConfig.Domains = append( - dnsConfig.Domains, - fmt.Sprintf( - "%s.%s", - node.User.Name, - baseDomain, - ), - ) + if dnsConfig.Proxied { + if cfg.DNSUserNameInMagicDNS { + // Only inject the Search Domain of the current user + // shared nodes should use their full FQDN + dnsConfig.Domains = append( + dnsConfig.Domains, + fmt.Sprintf( + "%s.%s", + node.User.Name, + baseDomain, + ), + ) - userSet := mapset.NewSet[types.User]() - userSet.Add(node.User) - for _, p := range peers { - userSet.Add(p.User) + userSet := mapset.NewSet[types.User]() + userSet.Add(node.User) + for _, p := range peers { + userSet.Add(p.User) + } + for _, user := range userSet.ToSlice() { + dnsRoute := fmt.Sprintf("%v.%v", user.Name, baseDomain) + dnsConfig.Routes[dnsRoute] = nil + } } - for _, user := range userSet.ToSlice() { - dnsRoute := fmt.Sprintf("%v.%v", user.Name, baseDomain) - dnsConfig.Routes[dnsRoute] = nil - } - } else { - dnsConfig = base } addNextDNSMetadata(dnsConfig.Resolvers, node) @@ -568,7 +572,7 @@ func appendPeerChanges( profiles := generateUserProfiles(node, changed, cfg.BaseDomain) dnsConfig := generateDNSConfig( - cfg.DNSConfig, + cfg, cfg.BaseDomain, node, peers, diff --git a/hscontrol/mapper/mapper_test.go b/hscontrol/mapper/mapper_test.go index 2ba3d031..be48c6fa 100644 --- a/hscontrol/mapper/mapper_test.go +++ b/hscontrol/mapper/mapper_test.go @@ -127,7 +127,10 @@ func TestDNSConfigMapResponse(t *testing.T) { } got := generateDNSConfig( - &dnsConfigOrig, + &types.Config{ + DNSConfig: &dnsConfigOrig, + DNSUserNameInMagicDNS: true, + }, baseDomain, nodeInShared1, peersOfNodeInShared1, @@ -187,9 +190,9 @@ func Test_fullMapResponse(t *testing.T) { UserID: 0, User: types.User{Name: "mini"}, ForcedTags: []string{}, - AuthKey: &types.PreAuthKey{}, - LastSeen: &lastSeen, - Expiry: &expire, + AuthKey: &types.PreAuthKey{}, + LastSeen: &lastSeen, + Expiry: &expire, Hostinfo: &tailcfg.Hostinfo{}, Routes: []types.Route{ { diff --git a/hscontrol/mapper/tail.go b/hscontrol/mapper/tail.go index ac39d35e..92fbed81 100644 --- a/hscontrol/mapper/tail.go +++ b/hscontrol/mapper/tail.go @@ -77,7 +77,7 @@ func tailNode( keyExpiry = time.Time{} } - hostname, err := node.GetFQDN(cfg.DNSConfig, cfg.BaseDomain) + hostname, err := node.GetFQDN(cfg, cfg.BaseDomain) if err != nil { return nil, fmt.Errorf("tailNode, failed to create FQDN: %s", err) } diff --git a/hscontrol/types/config.go b/hscontrol/types/config.go index 00934af6..8ac8dcc4 100644 --- a/hscontrol/types/config.go +++ b/hscontrol/types/config.go @@ -63,7 +63,8 @@ type Config struct { ACMEURL string ACMEEmail string - DNSConfig *tailcfg.DNSConfig + DNSConfig *tailcfg.DNSConfig + DNSUserNameInMagicDNS bool UnixSocket string UnixSocketPermission fs.FileMode @@ -204,6 +205,7 @@ func LoadConfig(path string, isFile bool) error { viper.SetDefault("dns_config", nil) viper.SetDefault("dns_config.override_local_dns", true) + viper.SetDefault("dns_config.use_username_in_magic_dns", false) viper.SetDefault("derp.server.enabled", false) viper.SetDefault("derp.server.stun.enabled", true) @@ -540,16 +542,6 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) { dnsConfig.Domains = domains } - if viper.IsSet("dns_config.domains") { - domains := viper.GetStringSlice("dns_config.domains") - if len(dnsConfig.Resolvers) > 0 { - dnsConfig.Domains = domains - } else if domains != nil { - log.Warn(). - Msg("Warning: dns_config.domains is set, but no nameservers are configured. Ignoring domains.") - } - } - if viper.IsSet("dns_config.extra_records") { var extraRecords []tailcfg.DNSRecord @@ -575,8 +567,18 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) { baseDomain = "headscale.net" // does not really matter when MagicDNS is not enabled } - log.Trace().Interface("dns_config", dnsConfig).Msg("DNS configuration loaded") + if !viper.GetBool("dns_config.use_username_in_magic_dns") { + dnsConfig.Domains = []string{baseDomain} + } else { + log.Warn().Msg("DNS: Usernames in DNS has been deprecated, this option will be remove in future versions") + log.Warn().Msg("DNS: see 0.23.0 changelog for more information.") + } + if domains := viper.GetStringSlice("dns_config.domains"); len(domains) > 0 { + dnsConfig.Domains = append(dnsConfig.Domains, domains...) + } + + log.Trace().Interface("dns_config", dnsConfig).Msg("DNS configuration loaded") return dnsConfig, baseDomain } @@ -719,7 +721,8 @@ func GetHeadscaleConfig() (*Config, error) { TLS: GetTLSConfig(), - DNSConfig: dnsConfig, + DNSConfig: dnsConfig, + DNSUserNameInMagicDNS: viper.GetBool("dns_config.use_username_in_magic_dns"), ACMEEmail: viper.GetString("acme_email"), ACMEURL: viper.GetString("acme_url"), diff --git a/hscontrol/types/node.go b/hscontrol/types/node.go index 3ccadc38..6bee5c42 100644 --- a/hscontrol/types/node.go +++ b/hscontrol/types/node.go @@ -394,23 +394,32 @@ func (node *Node) Proto() *v1.Node { return nodeProto } -func (node *Node) GetFQDN(dnsConfig *tailcfg.DNSConfig, baseDomain string) (string, error) { +func (node *Node) GetFQDN(cfg *Config, baseDomain string) (string, error) { var hostname string - if dnsConfig != nil && dnsConfig.Proxied { // MagicDNS + if cfg.DNSConfig != nil && cfg.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", + "%s.%s", node.GivenName, - node.User.Name, baseDomain, ) + + if cfg.DNSUserNameInMagicDNS { + if node.User.Name == "" { + return "", fmt.Errorf("failed to create valid FQDN: %w", ErrNodeUserHasNoName) + } + + hostname = fmt.Sprintf( + "%s.%s.%s", + node.GivenName, + node.User.Name, + baseDomain, + ) + } + if len(hostname) > MaxHostnameLength { return "", fmt.Errorf( "failed to create valid FQDN (%s): %w", diff --git a/hscontrol/types/node_test.go b/hscontrol/types/node_test.go index 157be89e..85857c3a 100644 --- a/hscontrol/types/node_test.go +++ b/hscontrol/types/node_test.go @@ -126,11 +126,87 @@ func TestNodeFQDN(t *testing.T) { tests := []struct { name string node Node - dns tailcfg.DNSConfig + cfg Config domain string want string wantErr string }{ + { + name: "all-set-with-username", + node: Node{ + GivenName: "test", + User: User{ + Name: "user", + }, + }, + cfg: Config{ + DNSConfig: &tailcfg.DNSConfig{ + Proxied: true, + }, + DNSUserNameInMagicDNS: true, + }, + domain: "example.com", + want: "test.user.example.com", + }, + { + name: "no-given-name-with-username", + node: Node{ + User: User{ + Name: "user", + }, + }, + cfg: Config{ + DNSConfig: &tailcfg.DNSConfig{ + Proxied: true, + }, + DNSUserNameInMagicDNS: true, + }, + domain: "example.com", + wantErr: "failed to create valid FQDN: node has no given name", + }, + { + name: "no-user-name-with-username", + node: Node{ + GivenName: "test", + User: User{}, + }, + cfg: Config{ + DNSConfig: &tailcfg.DNSConfig{ + Proxied: true, + }, + DNSUserNameInMagicDNS: true, + }, + domain: "example.com", + wantErr: "failed to create valid FQDN: node user has no name", + }, + { + name: "no-magic-dns-with-username", + node: Node{ + GivenName: "test", + User: User{ + Name: "user", + }, + }, + cfg: Config{ + DNSConfig: &tailcfg.DNSConfig{ + Proxied: false, + }, + DNSUserNameInMagicDNS: true, + }, + domain: "example.com", + want: "test", + }, + { + name: "no-dnsconfig-with-username", + node: Node{ + GivenName: "test", + User: User{ + Name: "user", + }, + }, + domain: "example.com", + want: "test", + }, { name: "all-set", node: Node{ @@ -139,11 +215,14 @@ func TestNodeFQDN(t *testing.T) { Name: "user", }, }, - dns: tailcfg.DNSConfig{ - Proxied: true, + cfg: Config{ + DNSConfig: &tailcfg.DNSConfig{ + Proxied: true, + }, + DNSUserNameInMagicDNS: false, }, domain: "example.com", - want: "test.user.example.com", + want: "test.example.com", }, { name: "no-given-name", @@ -152,8 +231,11 @@ func TestNodeFQDN(t *testing.T) { Name: "user", }, }, - dns: tailcfg.DNSConfig{ - Proxied: true, + cfg: Config{ + DNSConfig: &tailcfg.DNSConfig{ + Proxied: true, + }, + DNSUserNameInMagicDNS: false, }, domain: "example.com", wantErr: "failed to create valid FQDN: node has no given name", @@ -164,11 +246,14 @@ func TestNodeFQDN(t *testing.T) { GivenName: "test", User: User{}, }, - dns: tailcfg.DNSConfig{ - Proxied: true, + cfg: Config{ + DNSConfig: &tailcfg.DNSConfig{ + Proxied: true, + }, + DNSUserNameInMagicDNS: false, }, - domain: "example.com", - wantErr: "failed to create valid FQDN: node user has no name", + domain: "example.com", + want: "test.example.com", }, { name: "no-magic-dns", @@ -178,8 +263,11 @@ func TestNodeFQDN(t *testing.T) { Name: "user", }, }, - dns: tailcfg.DNSConfig{ - Proxied: false, + cfg: Config{ + DNSConfig: &tailcfg.DNSConfig{ + Proxied: false, + }, + DNSUserNameInMagicDNS: false, }, domain: "example.com", want: "test", @@ -199,7 +287,7 @@ func TestNodeFQDN(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - got, err := tc.node.GetFQDN(&tc.dns, tc.domain) + got, err := tc.node.GetFQDN(&tc.cfg, tc.domain) if (err != nil) && (err.Error() != tc.wantErr) { t.Errorf("GetFQDN() error = %s, wantErr %s", err, tc.wantErr) From 89ada557bc2e2772323e5380e89723815292f106 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 1 Jul 2024 05:46:33 +0000 Subject: [PATCH 084/145] flake.lock: Update (#1991) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 51019abd..0ca54945 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1718276985, - "narHash": "sha256-u1fA0DYQYdeG+5kDm1bOoGcHtX0rtC7qs2YA2N1X++I=", + "lastModified": 1719468428, + "narHash": "sha256-vN5xJAZ4UGREEglh3lfbbkIj+MPEYMuqewMn4atZFaQ=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "3f84a279f1a6290ce154c5531378acc827836fbb", + "rev": "1e3deb3d8a86a870d925760db1a5adecc64d329d", "type": "github" }, "original": { From eb1591df35624b6cbe85e5c671869a0806dedfba Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 7 Jul 2024 06:16:36 +0000 Subject: [PATCH 085/145] flake.lock: Update (#2000) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 0ca54945..6de98223 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1719468428, - "narHash": "sha256-vN5xJAZ4UGREEglh3lfbbkIj+MPEYMuqewMn4atZFaQ=", + "lastModified": 1720181791, + "narHash": "sha256-i4vJL12/AdyuQuviMMd1Hk2tsGt02hDNhA0Zj1m16N8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "1e3deb3d8a86a870d925760db1a5adecc64d329d", + "rev": "4284c2b73c8bce4b46a6adf23e16d9e2ec8da4bb", "type": "github" }, "original": { From 3f60ab23a68eff50d70054f7cbb622e53b52c625 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 14 Jul 2024 06:20:22 +0000 Subject: [PATCH 086/145] Update flake.lock (#2011) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/4284c2b73c8bce4b46a6adf23e16d9e2ec8da4bb?narHash=sha256-i4vJL12/AdyuQuviMMd1Hk2tsGt02hDNhA0Zj1m16N8%3D' (2024-07-05) → 'github:NixOS/nixpkgs/8b5a3d5a1d951344d683b442c0739010b80039db?narHash=sha256-po3TZO9kcZwzvkyMJKb0WCzzDtiHWD34XeRaX1lWXp0%3D' (2024-07-12) Co-authored-by: github-actions[bot] --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 6de98223..48ef53b6 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1720181791, - "narHash": "sha256-i4vJL12/AdyuQuviMMd1Hk2tsGt02hDNhA0Zj1m16N8=", + "lastModified": 1720781449, + "narHash": "sha256-po3TZO9kcZwzvkyMJKb0WCzzDtiHWD34XeRaX1lWXp0=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "4284c2b73c8bce4b46a6adf23e16d9e2ec8da4bb", + "rev": "8b5a3d5a1d951344d683b442c0739010b80039db", "type": "github" }, "original": { From 74d27ee5fa461c113107749ab698c7c8075f9922 Mon Sep 17 00:00:00 2001 From: greizgh Date: Wed, 17 Jul 2024 10:08:41 +0200 Subject: [PATCH 087/145] Remove deprecated linters from golangci-lint (#2009) When running lints, golangci-lint complained about removed linters (which were already disabled). This removes the relevant warnings. --- .golangci.yaml | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index 65a88511..cd41a4df 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -12,19 +12,13 @@ linters: disable: - depguard - - exhaustivestruct - revive - lll - - interfacer - - scopelint - - maligned - - golint - gofmt - gochecknoglobals - gochecknoinits - gocognit - funlen - - exhaustivestruct - tagliatelle - godox - ireturn @@ -34,13 +28,6 @@ linters: - musttag # causes issues with imported libs - depguard - # deprecated - - structcheck # replaced by unused - - ifshort # deprecated by the owner - - varcheck # replaced by unused - - nosnakecase # replaced by revive - - deadcode # replaced by unused - # We should strive to enable these: - wrapcheck - dupl From 8823778d0582e01e19a90613f6c4afe7b01c740b Mon Sep 17 00:00:00 2001 From: greizgh Date: Wed, 17 Jul 2024 13:12:02 +0200 Subject: [PATCH 088/145] Add gofumpt to dev dependencies (#2010) --- flake.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/flake.nix b/flake.nix index 5d4978ca..ed4f24de 100644 --- a/flake.nix +++ b/flake.nix @@ -74,6 +74,7 @@ nfpm gotestsum gotests + gofumpt ksh ko yq-go From 00ff288f0cae53d25f4c40355d3bd77cb3b06f4b Mon Sep 17 00:00:00 2001 From: Rubens Peculis Date: Wed, 17 Jul 2024 21:12:16 +1000 Subject: [PATCH 089/145] fix(1996): Implement register method enum converter (#2013) Added a new function `RegisterMethodToV1Enum()` to Node, converting the internal register method string to the corresponding V1 Enum value. Included corresponding unit test in `node_test.go` to ensure correct conversion for various register methods. --- hscontrol/types/node.go | 16 +++++++++-- hscontrol/types/node_test.go | 51 ++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/hscontrol/types/node.go b/hscontrol/types/node.go index 6bee5c42..19b287a1 100644 --- a/hscontrol/types/node.go +++ b/hscontrol/types/node.go @@ -373,8 +373,7 @@ func (node *Node) Proto() *v1.Node { User: node.User.Proto(), ForcedTags: node.ForcedTags, - // TODO(kradalby): Implement register method enum converter - // RegisterMethod: , + RegisterMethod: node.RegisterMethodToV1Enum(), CreatedAt: timestamppb.New(node.CreatedAt), } @@ -489,6 +488,19 @@ func (node *Node) PeerChangeFromMapRequest(req tailcfg.MapRequest) tailcfg.PeerC return ret } +func (node *Node) RegisterMethodToV1Enum() v1.RegisterMethod { + switch node.RegisterMethod { + case "authkey": + return v1.RegisterMethod_REGISTER_METHOD_AUTH_KEY + case "oidc": + return v1.RegisterMethod_REGISTER_METHOD_OIDC + case "cli": + return v1.RegisterMethod_REGISTER_METHOD_CLI + default: + return v1.RegisterMethod_REGISTER_METHOD_UNSPECIFIED + } +} + // ApplyPeerChange takes a PeerChange struct and updates the node. func (node *Node) ApplyPeerChange(change *tailcfg.PeerChange) { if change.Key != nil { diff --git a/hscontrol/types/node_test.go b/hscontrol/types/node_test.go index 85857c3a..798a54d3 100644 --- a/hscontrol/types/node_test.go +++ b/hscontrol/types/node_test.go @@ -6,6 +6,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + v1 "github.com/juanfont/headscale/gen/go/headscale/v1" "github.com/juanfont/headscale/hscontrol/util" "tailscale.com/tailcfg" "tailscale.com/types/key" @@ -540,3 +541,53 @@ func TestApplyPeerChange(t *testing.T) { }) } } + +func TestNodeRegisterMethodToV1Enum(t *testing.T) { + tests := []struct { + name string + node Node + want v1.RegisterMethod + }{ + { + name: "authkey", + node: Node{ + ID: 1, + RegisterMethod: util.RegisterMethodAuthKey, + }, + want: v1.RegisterMethod_REGISTER_METHOD_AUTH_KEY, + }, + { + name: "oidc", + node: Node{ + ID: 1, + RegisterMethod: util.RegisterMethodOIDC, + }, + want: v1.RegisterMethod_REGISTER_METHOD_OIDC, + }, + { + name: "cli", + node: Node{ + ID: 1, + RegisterMethod: util.RegisterMethodCLI, + }, + want: v1.RegisterMethod_REGISTER_METHOD_CLI, + }, + { + name: "unknown", + node: Node{ + ID: 0, + }, + want: v1.RegisterMethod_REGISTER_METHOD_UNSPECIFIED, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.node.RegisterMethodToV1Enum() + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("RegisterMethodToV1Enum() unexpected result (-want +got):\n%s", diff) + } + }) + } +} From 58bd38a609a7e1e142035c282ab0163eadab7876 Mon Sep 17 00:00:00 2001 From: Pallab Pain Date: Thu, 18 Jul 2024 11:08:25 +0530 Subject: [PATCH 090/145] feat: implements apis for managing headscale policy (#1792) --- .github/workflows/test-integration.yaml | 2 + CHANGELOG.md | 3 + cmd/headscale/cli/policy.go | 91 +++ cmd/headscale/cli/utils.go | 31 +- config-example.yaml | 16 +- gen/go/headscale/v1/apikey.pb.go | 2 +- gen/go/headscale/v1/device.pb.go | 2 +- gen/go/headscale/v1/headscale.pb.go | 538 +++++++++--------- gen/go/headscale/v1/headscale.pb.gw.go | 240 +++++++- gen/go/headscale/v1/headscale_grpc.pb.go | 214 ++++--- gen/go/headscale/v1/node.pb.go | 2 +- gen/go/headscale/v1/policy.pb.go | 352 ++++++++++++ gen/go/headscale/v1/preauthkey.pb.go | 2 +- gen/go/headscale/v1/routes.pb.go | 2 +- gen/go/headscale/v1/user.pb.go | 2 +- .../headscale/v1/apikey.swagger.json | 1 - .../headscale/v1/device.swagger.json | 1 - .../headscale/v1/headscale.swagger.json | 113 +++- gen/openapiv2/headscale/v1/node.swagger.json | 1 - .../headscale/v1/policy.swagger.json | 43 ++ .../headscale/v1/preauthkey.swagger.json | 1 - .../headscale/v1/routes.swagger.json | 1 - gen/openapiv2/headscale/v1/user.swagger.json | 1 - hscontrol/app.go | 86 ++- hscontrol/db/db.go | 12 + hscontrol/db/node_test.go | 9 +- hscontrol/db/policy.go | 44 ++ hscontrol/grpcv1.go | 74 +++ hscontrol/mapper/mapper.go | 23 +- hscontrol/policy/acls.go | 44 +- hscontrol/policy/acls_test.go | 64 +-- hscontrol/policy/acls_types.go | 70 +-- hscontrol/types/config.go | 55 +- hscontrol/types/policy.go | 20 + integration/acl_test.go | 155 +++++ integration/cli_test.go | 83 ++- integration/control.go | 4 +- proto/headscale/v1/headscale.proto | 17 + proto/headscale/v1/policy.proto | 21 + 39 files changed, 1875 insertions(+), 567 deletions(-) create mode 100644 cmd/headscale/cli/policy.go create mode 100644 gen/go/headscale/v1/policy.pb.go create mode 100644 gen/openapiv2/headscale/v1/policy.swagger.json create mode 100644 hscontrol/db/policy.go create mode 100644 hscontrol/types/policy.go create mode 100644 proto/headscale/v1/policy.proto diff --git a/.github/workflows/test-integration.yaml b/.github/workflows/test-integration.yaml index 9581bada..ed1d1221 100644 --- a/.github/workflows/test-integration.yaml +++ b/.github/workflows/test-integration.yaml @@ -18,6 +18,7 @@ jobs: - TestACLNamedHostsCanReachBySubnet - TestACLNamedHostsCanReach - TestACLDevice1CanAccessDevice2 + - TestPolicyUpdateWhileRunningWithCLIInDatabase - TestOIDCAuthenticationPingAll - TestOIDCExpireNodesBasedOnTokenExpiry - TestAuthWebFlowAuthenticationPingAll @@ -35,6 +36,7 @@ jobs: - TestNodeExpireCommand - TestNodeRenameCommand - TestNodeMoveCommand + - TestPolicyCommand - TestDERPServerScenario - TestPingAllByIP - TestPingAllByIPPublicDERP diff --git a/CHANGELOG.md b/CHANGELOG.md index fced0b6d..fd8787ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,8 @@ after improving the test harness as part of adopting [#1460](https://github.com/ - This is in preperation to fix Headscales implementation of tags which currently does not correctly remove the link between a tagged device and a user. As tagged devices will not have a user, this will require a change to the DNS generation, removing the username, see [#1369](https://github.com/juanfont/headscale/issues/1369) for more information. - `use_username_in_magic_dns` can be used to turn this behaviour on again, but note that this option _will be removed_ when tags are fixed. - This option brings Headscales behaviour in line with Tailscale. +- YAML files are no longer supported for headscale policy. [#1792](https://github.com/juanfont/headscale/pull/1792) + - HuJSON is now the only supported format for policy. ### Changes @@ -64,6 +66,7 @@ after improving the test harness as part of adopting [#1460](https://github.com/ - Restore foreign keys and add constraints [#1562](https://github.com/juanfont/headscale/pull/1562) - Make registration page easier to use on mobile devices - Make write-ahead-log default on and configurable for SQLite [#1985](https://github.com/juanfont/headscale/pull/1985) +- Add APIs for managing headscale policy. [#1792](https://github.com/juanfont/headscale/pull/1792) ## 0.22.3 (2023-05-12) diff --git a/cmd/headscale/cli/policy.go b/cmd/headscale/cli/policy.go new file mode 100644 index 00000000..5b34a1e1 --- /dev/null +++ b/cmd/headscale/cli/policy.go @@ -0,0 +1,91 @@ +package cli + +import ( + "io" + "os" + + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + + v1 "github.com/juanfont/headscale/gen/go/headscale/v1" +) + +func init() { + rootCmd.AddCommand(policyCmd) + policyCmd.AddCommand(getPolicy) + + setPolicy.Flags().StringP("file", "f", "", "Path to a policy file in HuJSON format") + if err := setPolicy.MarkFlagRequired("file"); err != nil { + log.Fatal().Err(err).Msg("") + } + policyCmd.AddCommand(setPolicy) +} + +var policyCmd = &cobra.Command{ + Use: "policy", + Short: "Manage the Headscale ACL Policy", +} + +var getPolicy = &cobra.Command{ + Use: "get", + Short: "Print the current ACL Policy", + Aliases: []string{"show", "view", "fetch"}, + Run: func(cmd *cobra.Command, args []string) { + ctx, client, conn, cancel := getHeadscaleCLIClient() + defer cancel() + defer conn.Close() + + request := &v1.GetPolicyRequest{} + + response, err := client.GetPolicy(ctx, request) + if err != nil { + log.Fatal().Err(err).Msg("Failed to get the policy") + + return + } + + // TODO(pallabpain): Maybe print this better? + SuccessOutput("", response.GetPolicy(), "hujson") + }, +} + +var setPolicy = &cobra.Command{ + Use: "set", + Short: "Updates the ACL Policy", + Long: ` + Updates the existing ACL Policy with the provided policy. The policy must be a valid HuJSON object. + This command only works when the acl.policy_mode is set to "db", and the policy will be stored in the database.`, + Aliases: []string{"put", "update"}, + Run: func(cmd *cobra.Command, args []string) { + policyPath, _ := cmd.Flags().GetString("file") + + f, err := os.Open(policyPath) + if err != nil { + log.Fatal().Err(err).Msg("Error opening the policy file") + + return + } + defer f.Close() + + policyBytes, err := io.ReadAll(f) + if err != nil { + log.Fatal().Err(err).Msg("Error reading the policy file") + + return + } + + request := &v1.SetPolicyRequest{Policy: string(policyBytes)} + + ctx, client, conn, cancel := getHeadscaleCLIClient() + defer cancel() + defer conn.Close() + + if _, err := client.SetPolicy(ctx, request); err != nil { + log.Fatal().Err(err).Msg("Failed to set ACL Policy") + + return + } + + SuccessOutput(nil, "Policy updated.", "") + }, +} diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index a193d17d..8a91c5c6 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -8,16 +8,16 @@ import ( "os" "reflect" - v1 "github.com/juanfont/headscale/gen/go/headscale/v1" - "github.com/juanfont/headscale/hscontrol" - "github.com/juanfont/headscale/hscontrol/policy" - "github.com/juanfont/headscale/hscontrol/types" - "github.com/juanfont/headscale/hscontrol/util" "github.com/rs/zerolog/log" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" "gopkg.in/yaml.v3" + + v1 "github.com/juanfont/headscale/gen/go/headscale/v1" + "github.com/juanfont/headscale/hscontrol" + "github.com/juanfont/headscale/hscontrol/types" + "github.com/juanfont/headscale/hscontrol/util" ) const ( @@ -39,21 +39,6 @@ func getHeadscaleApp() (*hscontrol.Headscale, error) { return nil, err } - // We are doing this here, as in the future could be cool to have it also hot-reload - - if cfg.ACL.PolicyPath != "" { - aclPath := util.AbsolutePathFromConfigPath(cfg.ACL.PolicyPath) - pol, err := policy.LoadACLPolicyFromPath(aclPath) - if err != nil { - log.Fatal(). - Str("path", aclPath). - Err(err). - Msg("Could not load the ACL policy") - } - - app.ACLPolicy = pol - } - return app, nil } @@ -89,7 +74,7 @@ func getHeadscaleCLIClient() (context.Context, v1.HeadscaleServiceClient, *grpc. // Try to give the user better feedback if we cannot write to the headscale // socket. - socket, err := os.OpenFile(cfg.UnixSocket, os.O_WRONLY, SocketWritePermissions) //nolint + socket, err := os.OpenFile(cfg.UnixSocket, os.O_WRONLY, SocketWritePermissions) // nolint if err != nil { if os.IsPermission(err) { log.Fatal(). @@ -167,13 +152,13 @@ func SuccessOutput(result interface{}, override string, outputFormat string) { log.Fatal().Err(err).Msg("failed to unmarshal output") } default: - //nolint + // nolint fmt.Println(override) return } - //nolint + // nolint fmt.Println(string(jsonBytes)) } diff --git a/config-example.yaml b/config-example.yaml index 4608317a..f408ff50 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -203,10 +203,18 @@ log: format: text level: info -# Path to a file containing ACL policies. -# ACLs can be defined as YAML or HUJSON. -# https://tailscale.com/kb/1018/acls/ -acl_policy_path: "" +## Policy +# headscale supports Tailscale's ACL policies. +# Please have a look to their KB to better +# understand the concepts: https://tailscale.com/kb/1018/acls/ +policy: + # The mode can be "file" or "database" that defines + # where the ACL policies are stored and read from. + mode: file + # If the mode is set to "file", the + # path to a file containing ACL policies. + # The file can be in YAML or HuJSON format. + path: "" ## DNS # diff --git a/gen/go/headscale/v1/apikey.pb.go b/gen/go/headscale/v1/apikey.pb.go index c4377e48..d1a5f555 100644 --- a/gen/go/headscale/v1/apikey.pb.go +++ b/gen/go/headscale/v1/apikey.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.33.0 +// protoc-gen-go v1.32.0 // protoc (unknown) // source: headscale/v1/apikey.proto diff --git a/gen/go/headscale/v1/device.pb.go b/gen/go/headscale/v1/device.pb.go index 7a382dd6..40e2e24f 100644 --- a/gen/go/headscale/v1/device.pb.go +++ b/gen/go/headscale/v1/device.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.33.0 +// protoc-gen-go v1.32.0 // protoc (unknown) // source: headscale/v1/device.proto diff --git a/gen/go/headscale/v1/headscale.pb.go b/gen/go/headscale/v1/headscale.pb.go index 9de6b060..63e7d536 100644 --- a/gen/go/headscale/v1/headscale.pb.go +++ b/gen/go/headscale/v1/headscale.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.33.0 +// protoc-gen-go v1.32.0 // protoc (unknown) // source: headscale/v1/headscale.proto @@ -36,210 +36,225 @@ var file_headscale_v1_headscale_proto_rawDesc = []byte{ 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x19, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x69, 0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x32, 0x80, 0x19, 0x0a, 0x10, 0x48, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x53, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x63, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, - 0x72, 0x12, 0x1c, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, - 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x1d, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, - 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1b, - 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x15, 0x12, 0x13, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, - 0x75, 0x73, 0x65, 0x72, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x12, 0x68, 0x0a, 0x0a, 0x43, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x12, 0x1f, 0x2e, 0x68, 0x65, 0x61, 0x64, - 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, - 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x68, 0x65, 0x61, - 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x17, 0x82, 0xd3, - 0xe4, 0x93, 0x02, 0x11, 0x3a, 0x01, 0x2a, 0x22, 0x0c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, - 0x2f, 0x75, 0x73, 0x65, 0x72, 0x12, 0x82, 0x01, 0x0a, 0x0a, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, + 0x6f, 0x1a, 0x19, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x32, 0xcf, 0x1a, 0x0a, + 0x10, 0x48, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x12, 0x63, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x68, + 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, + 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x68, 0x65, 0x61, + 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, + 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1b, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x15, 0x12, 0x13, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, + 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x12, 0x68, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x12, 0x1f, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, - 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, + 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, - 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x31, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x2b, 0x22, - 0x29, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x7b, 0x6f, - 0x6c, 0x64, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x72, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x2f, - 0x7b, 0x6e, 0x65, 0x77, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x12, 0x6c, 0x0a, 0x0a, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x12, 0x1f, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, - 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, - 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x68, 0x65, 0x61, 0x64, - 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, - 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1b, 0x82, 0xd3, 0xe4, - 0x93, 0x02, 0x15, 0x2a, 0x13, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, - 0x72, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x12, 0x62, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, - 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x1e, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, - 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, - 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x14, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0e, 0x12, 0x0c, - 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x12, 0x80, 0x01, 0x0a, - 0x10, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, - 0x79, 0x12, 0x25, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, - 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, - 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, - 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, - 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x17, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x11, 0x3a, + 0x01, 0x2a, 0x22, 0x0c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, + 0x12, 0x82, 0x01, 0x0a, 0x0a, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x55, 0x73, 0x65, 0x72, 0x12, + 0x1f, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, + 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x20, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, + 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x31, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x2b, 0x22, 0x29, 0x2f, 0x61, 0x70, 0x69, + 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x7b, 0x6f, 0x6c, 0x64, 0x5f, 0x6e, 0x61, + 0x6d, 0x65, 0x7d, 0x2f, 0x72, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x2f, 0x7b, 0x6e, 0x65, 0x77, 0x5f, + 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x12, 0x6c, 0x0a, 0x0a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, + 0x73, 0x65, 0x72, 0x12, 0x1f, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, + 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, + 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x15, 0x2a, 0x13, + 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x7b, 0x6e, 0x61, + 0x6d, 0x65, 0x7d, 0x12, 0x62, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, + 0x12, 0x1e, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, + 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1f, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, + 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x14, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0e, 0x12, 0x0c, 0x2f, 0x61, 0x70, 0x69, 0x2f, + 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x12, 0x80, 0x01, 0x0a, 0x10, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x12, 0x25, 0x2e, 0x68, + 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, + 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, + 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1d, 0x82, 0xd3, 0xe4, + 0x93, 0x02, 0x17, 0x3a, 0x01, 0x2a, 0x22, 0x12, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, + 0x70, 0x72, 0x65, 0x61, 0x75, 0x74, 0x68, 0x6b, 0x65, 0x79, 0x12, 0x87, 0x01, 0x0a, 0x10, 0x45, + 0x78, 0x70, 0x69, 0x72, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x12, + 0x25, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, + 0x78, 0x70, 0x69, 0x72, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, + 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x50, 0x72, 0x65, 0x41, + 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x24, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1e, 0x3a, 0x01, 0x2a, 0x22, 0x19, 0x2f, 0x61, 0x70, 0x69, 0x2f, + 0x76, 0x31, 0x2f, 0x70, 0x72, 0x65, 0x61, 0x75, 0x74, 0x68, 0x6b, 0x65, 0x79, 0x2f, 0x65, 0x78, + 0x70, 0x69, 0x72, 0x65, 0x12, 0x7a, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, 0x65, 0x41, + 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x24, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, + 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, 0x65, 0x41, 0x75, + 0x74, 0x68, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, + 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, + 0x74, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x12, 0x12, 0x2f, 0x61, + 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x65, 0x61, 0x75, 0x74, 0x68, 0x6b, 0x65, 0x79, + 0x12, 0x7d, 0x0a, 0x0f, 0x44, 0x65, 0x62, 0x75, 0x67, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4e, + 0x6f, 0x64, 0x65, 0x12, 0x24, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, + 0x76, 0x31, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4e, 0x6f, + 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x68, 0x65, 0x61, 0x64, + 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1d, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x3a, 0x01, 0x2a, 0x22, 0x12, 0x2f, 0x61, 0x70, - 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x65, 0x61, 0x75, 0x74, 0x68, 0x6b, 0x65, 0x79, 0x12, - 0x87, 0x01, 0x0a, 0x10, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, - 0x68, 0x4b, 0x65, 0x79, 0x12, 0x25, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, - 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, - 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x68, 0x65, - 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, - 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x24, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1e, 0x3a, 0x01, 0x2a, 0x22, 0x19, - 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x65, 0x61, 0x75, 0x74, 0x68, 0x6b, - 0x65, 0x79, 0x2f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x12, 0x7a, 0x0a, 0x0f, 0x4c, 0x69, 0x73, - 0x74, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x24, 0x2e, 0x68, - 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, - 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1a, 0x82, 0xd3, 0xe4, 0x93, 0x02, - 0x14, 0x12, 0x12, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x65, 0x61, 0x75, - 0x74, 0x68, 0x6b, 0x65, 0x79, 0x12, 0x7d, 0x0a, 0x0f, 0x44, 0x65, 0x62, 0x75, 0x67, 0x43, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x24, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, - 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, - 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, - 0x62, 0x75, 0x67, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1d, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x3a, 0x01, 0x2a, - 0x22, 0x12, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2f, - 0x6e, 0x6f, 0x64, 0x65, 0x12, 0x66, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x12, - 0x1c, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, - 0x65, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, - 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, - 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1e, 0x82, 0xd3, - 0xe4, 0x93, 0x02, 0x18, 0x12, 0x16, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x6f, - 0x64, 0x65, 0x2f, 0x7b, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x12, 0x6e, 0x0a, 0x07, - 0x53, 0x65, 0x74, 0x54, 0x61, 0x67, 0x73, 0x12, 0x1c, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, - 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x54, 0x61, 0x67, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, - 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x54, 0x61, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x26, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x20, 0x3a, 0x01, 0x2a, 0x22, - 0x1b, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x7b, 0x6e, - 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x74, 0x61, 0x67, 0x73, 0x12, 0x74, 0x0a, 0x0c, - 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x21, 0x2e, 0x68, - 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, - 0x73, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x22, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, - 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x1d, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x22, 0x15, 0x2f, 0x61, 0x70, - 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, - 0x65, 0x72, 0x12, 0x6f, 0x0a, 0x0a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4e, 0x6f, 0x64, 0x65, - 0x12, 0x1f, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x20, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, - 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x1e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18, 0x2a, 0x16, 0x2f, 0x61, 0x70, - 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x7b, 0x6e, 0x6f, 0x64, 0x65, 0x5f, - 0x69, 0x64, 0x7d, 0x12, 0x76, 0x0a, 0x0a, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x4e, 0x6f, 0x64, - 0x65, 0x12, 0x1f, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, - 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, - 0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x25, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1f, 0x22, 0x1d, 0x2f, 0x61, - 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x7b, 0x6e, 0x6f, 0x64, 0x65, - 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x12, 0x81, 0x01, 0x0a, 0x0a, - 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x1f, 0x2e, 0x68, 0x65, 0x61, - 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, - 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x68, 0x65, - 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6e, 0x61, 0x6d, - 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x30, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x2a, 0x22, 0x28, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6e, - 0x6f, 0x64, 0x65, 0x2f, 0x7b, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x72, 0x65, - 0x6e, 0x61, 0x6d, 0x65, 0x2f, 0x7b, 0x6e, 0x65, 0x77, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x12, - 0x62, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x73, 0x12, 0x1e, 0x2e, 0x68, - 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x4e, 0x6f, 0x64, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x68, - 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x4e, 0x6f, 0x64, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x14, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x0e, 0x12, 0x0c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6e, - 0x6f, 0x64, 0x65, 0x12, 0x6e, 0x0a, 0x08, 0x4d, 0x6f, 0x76, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x12, - 0x1d, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, - 0x6f, 0x76, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, - 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x6f, - 0x76, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x23, - 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1d, 0x22, 0x1b, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, - 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x7b, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x75, - 0x73, 0x65, 0x72, 0x12, 0x80, 0x01, 0x0a, 0x0f, 0x42, 0x61, 0x63, 0x6b, 0x66, 0x69, 0x6c, 0x6c, - 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x50, 0x73, 0x12, 0x24, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, - 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x63, 0x6b, 0x66, 0x69, 0x6c, 0x6c, 0x4e, - 0x6f, 0x64, 0x65, 0x49, 0x50, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, - 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x63, - 0x6b, 0x66, 0x69, 0x6c, 0x6c, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x50, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x22, 0x18, 0x2f, 0x61, - 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x66, - 0x69, 0x6c, 0x6c, 0x69, 0x70, 0x73, 0x12, 0x64, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x75, - 0x74, 0x65, 0x73, 0x12, 0x1e, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, - 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, - 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x16, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x12, 0x0e, 0x2f, 0x61, - 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x7c, 0x0a, 0x0b, - 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x20, 0x2e, 0x68, 0x65, - 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x61, 0x62, 0x6c, - 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, - 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x61, - 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x28, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x22, 0x22, 0x20, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, - 0x31, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x2f, 0x7b, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x5f, - 0x69, 0x64, 0x7d, 0x2f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x80, 0x01, 0x0a, 0x0c, 0x44, - 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x21, 0x2e, 0x68, 0x65, - 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x69, 0x73, 0x61, 0x62, - 0x6c, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, - 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x69, - 0x73, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x29, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x23, 0x22, 0x21, 0x2f, 0x61, 0x70, 0x69, - 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x2f, 0x7b, 0x72, 0x6f, 0x75, 0x74, - 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x7f, 0x0a, - 0x0d, 0x47, 0x65, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x22, - 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, - 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, - 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x25, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1f, 0x12, - 0x1d, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x7b, 0x6e, - 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x75, - 0x0a, 0x0b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x20, 0x2e, - 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x21, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1b, 0x2a, 0x19, 0x2f, 0x61, 0x70, 0x69, - 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x2f, 0x7b, 0x72, 0x6f, 0x75, 0x74, - 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x12, 0x70, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, - 0x70, 0x69, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, - 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, - 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, - 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x70, - 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x19, 0x82, 0xd3, - 0xe4, 0x93, 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x22, 0x0e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, - 0x2f, 0x61, 0x70, 0x69, 0x6b, 0x65, 0x79, 0x12, 0x77, 0x0a, 0x0c, 0x45, 0x78, 0x70, 0x69, 0x72, - 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, - 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x41, 0x70, 0x69, - 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x65, 0x61, - 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, - 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x20, - 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x3a, 0x01, 0x2a, 0x22, 0x15, 0x2f, 0x61, 0x70, 0x69, 0x2f, - 0x76, 0x31, 0x2f, 0x61, 0x70, 0x69, 0x6b, 0x65, 0x79, 0x2f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, - 0x12, 0x6a, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x73, 0x12, - 0x20, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, - 0x69, 0x73, 0x74, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, - 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x16, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x12, 0x0e, 0x2f, 0x61, - 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x69, 0x6b, 0x65, 0x79, 0x12, 0x76, 0x0a, 0x0c, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x2e, 0x68, + 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x12, + 0x66, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x1c, 0x2e, 0x68, 0x65, 0x61, + 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4e, 0x6f, 0x64, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, + 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18, 0x12, + 0x16, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x7b, 0x6e, + 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x12, 0x6e, 0x0a, 0x07, 0x53, 0x65, 0x74, 0x54, 0x61, + 0x67, 0x73, 0x12, 0x1c, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, + 0x31, 0x2e, 0x53, 0x65, 0x74, 0x54, 0x61, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1d, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, + 0x53, 0x65, 0x74, 0x54, 0x61, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x26, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x20, 0x3a, 0x01, 0x2a, 0x22, 0x1b, 0x2f, 0x61, 0x70, 0x69, + 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x7b, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, + 0x64, 0x7d, 0x2f, 0x74, 0x61, 0x67, 0x73, 0x12, 0x74, 0x0a, 0x0c, 0x52, 0x65, 0x67, 0x69, 0x73, + 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, + 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4e, + 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x65, 0x61, + 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1d, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x22, 0x15, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, + 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x12, 0x6f, 0x0a, + 0x0a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x1f, 0x2e, 0x68, 0x65, + 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x22, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x2a, 0x17, 0x2f, 0x61, 0x70, - 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x69, 0x6b, 0x65, 0x79, 0x2f, 0x7b, 0x70, 0x72, 0x65, - 0x66, 0x69, 0x78, 0x7d, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x6a, 0x75, 0x61, 0x6e, 0x66, 0x6f, 0x6e, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, - 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1e, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18, 0x2a, 0x16, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, + 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x7b, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x12, 0x76, + 0x0a, 0x0a, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x1f, 0x2e, 0x68, + 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, + 0x72, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, + 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, + 0x69, 0x72, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x25, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1f, 0x22, 0x1d, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, + 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x7b, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x2f, + 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x12, 0x81, 0x01, 0x0a, 0x0a, 0x52, 0x65, 0x6e, 0x61, 0x6d, + 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x1f, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, + 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, + 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x4e, 0x6f, 0x64, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x30, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x2a, + 0x22, 0x28, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x7b, + 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x72, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x2f, + 0x7b, 0x6e, 0x65, 0x77, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x12, 0x62, 0x0a, 0x09, 0x4c, 0x69, + 0x73, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x73, 0x12, 0x1e, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, + 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, + 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x14, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0e, + 0x12, 0x0c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x12, 0x6e, + 0x0a, 0x08, 0x4d, 0x6f, 0x76, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x1d, 0x2e, 0x68, 0x65, 0x61, + 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x6f, 0x76, 0x65, 0x4e, 0x6f, + 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x68, 0x65, 0x61, 0x64, + 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x6f, 0x76, 0x65, 0x4e, 0x6f, 0x64, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x23, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x1d, 0x22, 0x1b, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, + 0x7b, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x12, 0x80, + 0x01, 0x0a, 0x0f, 0x42, 0x61, 0x63, 0x6b, 0x66, 0x69, 0x6c, 0x6c, 0x4e, 0x6f, 0x64, 0x65, 0x49, + 0x50, 0x73, 0x12, 0x24, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, + 0x31, 0x2e, 0x42, 0x61, 0x63, 0x6b, 0x66, 0x69, 0x6c, 0x6c, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x50, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, + 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x63, 0x6b, 0x66, 0x69, 0x6c, 0x6c, + 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x50, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x22, 0x18, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, + 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x66, 0x69, 0x6c, 0x6c, 0x69, 0x70, + 0x73, 0x12, 0x64, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x1e, + 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, + 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, + 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, + 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x16, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x12, 0x0e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, + 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x7c, 0x0a, 0x0b, 0x45, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x20, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, + 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x75, 0x74, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, + 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, + 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x28, 0x82, 0xd3, 0xe4, + 0x93, 0x02, 0x22, 0x22, 0x20, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x75, + 0x74, 0x65, 0x73, 0x2f, 0x7b, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x65, + 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x80, 0x01, 0x0a, 0x0c, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, + 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, + 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x75, + 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x65, 0x61, 0x64, + 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, + 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x29, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x23, 0x22, 0x21, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x72, + 0x6f, 0x75, 0x74, 0x65, 0x73, 0x2f, 0x7b, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x7d, + 0x2f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x7f, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x4e, + 0x6f, 0x64, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x22, 0x2e, 0x68, 0x65, 0x61, 0x64, + 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4e, 0x6f, 0x64, 0x65, + 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, + 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, + 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x25, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1f, 0x12, 0x1d, 0x2f, 0x61, 0x70, 0x69, + 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x7b, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, + 0x64, 0x7d, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x75, 0x0a, 0x0b, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x20, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, + 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x6f, + 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x68, 0x65, 0x61, + 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x21, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x1b, 0x2a, 0x19, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x72, + 0x6f, 0x75, 0x74, 0x65, 0x73, 0x2f, 0x7b, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x7d, + 0x12, 0x70, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, + 0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, + 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x3a, + 0x01, 0x2a, 0x22, 0x0e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x69, 0x6b, + 0x65, 0x79, 0x12, 0x77, 0x0a, 0x0c, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x41, 0x70, 0x69, 0x4b, + 0x65, 0x79, 0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, + 0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, + 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, + 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x1a, 0x3a, 0x01, 0x2a, 0x22, 0x15, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, + 0x69, 0x6b, 0x65, 0x79, 0x2f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x12, 0x6a, 0x0a, 0x0b, 0x4c, + 0x69, 0x73, 0x74, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x20, 0x2e, 0x68, 0x65, 0x61, + 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x70, + 0x69, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x68, + 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x16, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x12, 0x0e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, + 0x2f, 0x61, 0x70, 0x69, 0x6b, 0x65, 0x79, 0x12, 0x76, 0x0a, 0x0c, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, + 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x70, 0x69, + 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x65, 0x61, + 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1f, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x2a, 0x17, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, + 0x61, 0x70, 0x69, 0x6b, 0x65, 0x79, 0x2f, 0x7b, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x7d, 0x12, + 0x64, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x1e, 0x2e, 0x68, + 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x50, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x68, + 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x50, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x16, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x12, 0x0e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x67, 0x0a, 0x09, 0x53, 0x65, 0x74, 0x50, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x12, 0x1e, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, + 0x31, 0x2e, 0x53, 0x65, 0x74, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, + 0x31, 0x2e, 0x53, 0x65, 0x74, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x1a, 0x0e, + 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x42, 0x29, + 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x75, 0x61, + 0x6e, 0x66, 0x6f, 0x6e, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, + 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, } var file_headscale_v1_headscale_proto_goTypes = []interface{}{ @@ -270,33 +285,37 @@ var file_headscale_v1_headscale_proto_goTypes = []interface{}{ (*ExpireApiKeyRequest)(nil), // 24: headscale.v1.ExpireApiKeyRequest (*ListApiKeysRequest)(nil), // 25: headscale.v1.ListApiKeysRequest (*DeleteApiKeyRequest)(nil), // 26: headscale.v1.DeleteApiKeyRequest - (*GetUserResponse)(nil), // 27: headscale.v1.GetUserResponse - (*CreateUserResponse)(nil), // 28: headscale.v1.CreateUserResponse - (*RenameUserResponse)(nil), // 29: headscale.v1.RenameUserResponse - (*DeleteUserResponse)(nil), // 30: headscale.v1.DeleteUserResponse - (*ListUsersResponse)(nil), // 31: headscale.v1.ListUsersResponse - (*CreatePreAuthKeyResponse)(nil), // 32: headscale.v1.CreatePreAuthKeyResponse - (*ExpirePreAuthKeyResponse)(nil), // 33: headscale.v1.ExpirePreAuthKeyResponse - (*ListPreAuthKeysResponse)(nil), // 34: headscale.v1.ListPreAuthKeysResponse - (*DebugCreateNodeResponse)(nil), // 35: headscale.v1.DebugCreateNodeResponse - (*GetNodeResponse)(nil), // 36: headscale.v1.GetNodeResponse - (*SetTagsResponse)(nil), // 37: headscale.v1.SetTagsResponse - (*RegisterNodeResponse)(nil), // 38: headscale.v1.RegisterNodeResponse - (*DeleteNodeResponse)(nil), // 39: headscale.v1.DeleteNodeResponse - (*ExpireNodeResponse)(nil), // 40: headscale.v1.ExpireNodeResponse - (*RenameNodeResponse)(nil), // 41: headscale.v1.RenameNodeResponse - (*ListNodesResponse)(nil), // 42: headscale.v1.ListNodesResponse - (*MoveNodeResponse)(nil), // 43: headscale.v1.MoveNodeResponse - (*BackfillNodeIPsResponse)(nil), // 44: headscale.v1.BackfillNodeIPsResponse - (*GetRoutesResponse)(nil), // 45: headscale.v1.GetRoutesResponse - (*EnableRouteResponse)(nil), // 46: headscale.v1.EnableRouteResponse - (*DisableRouteResponse)(nil), // 47: headscale.v1.DisableRouteResponse - (*GetNodeRoutesResponse)(nil), // 48: headscale.v1.GetNodeRoutesResponse - (*DeleteRouteResponse)(nil), // 49: headscale.v1.DeleteRouteResponse - (*CreateApiKeyResponse)(nil), // 50: headscale.v1.CreateApiKeyResponse - (*ExpireApiKeyResponse)(nil), // 51: headscale.v1.ExpireApiKeyResponse - (*ListApiKeysResponse)(nil), // 52: headscale.v1.ListApiKeysResponse - (*DeleteApiKeyResponse)(nil), // 53: headscale.v1.DeleteApiKeyResponse + (*GetPolicyRequest)(nil), // 27: headscale.v1.GetPolicyRequest + (*SetPolicyRequest)(nil), // 28: headscale.v1.SetPolicyRequest + (*GetUserResponse)(nil), // 29: headscale.v1.GetUserResponse + (*CreateUserResponse)(nil), // 30: headscale.v1.CreateUserResponse + (*RenameUserResponse)(nil), // 31: headscale.v1.RenameUserResponse + (*DeleteUserResponse)(nil), // 32: headscale.v1.DeleteUserResponse + (*ListUsersResponse)(nil), // 33: headscale.v1.ListUsersResponse + (*CreatePreAuthKeyResponse)(nil), // 34: headscale.v1.CreatePreAuthKeyResponse + (*ExpirePreAuthKeyResponse)(nil), // 35: headscale.v1.ExpirePreAuthKeyResponse + (*ListPreAuthKeysResponse)(nil), // 36: headscale.v1.ListPreAuthKeysResponse + (*DebugCreateNodeResponse)(nil), // 37: headscale.v1.DebugCreateNodeResponse + (*GetNodeResponse)(nil), // 38: headscale.v1.GetNodeResponse + (*SetTagsResponse)(nil), // 39: headscale.v1.SetTagsResponse + (*RegisterNodeResponse)(nil), // 40: headscale.v1.RegisterNodeResponse + (*DeleteNodeResponse)(nil), // 41: headscale.v1.DeleteNodeResponse + (*ExpireNodeResponse)(nil), // 42: headscale.v1.ExpireNodeResponse + (*RenameNodeResponse)(nil), // 43: headscale.v1.RenameNodeResponse + (*ListNodesResponse)(nil), // 44: headscale.v1.ListNodesResponse + (*MoveNodeResponse)(nil), // 45: headscale.v1.MoveNodeResponse + (*BackfillNodeIPsResponse)(nil), // 46: headscale.v1.BackfillNodeIPsResponse + (*GetRoutesResponse)(nil), // 47: headscale.v1.GetRoutesResponse + (*EnableRouteResponse)(nil), // 48: headscale.v1.EnableRouteResponse + (*DisableRouteResponse)(nil), // 49: headscale.v1.DisableRouteResponse + (*GetNodeRoutesResponse)(nil), // 50: headscale.v1.GetNodeRoutesResponse + (*DeleteRouteResponse)(nil), // 51: headscale.v1.DeleteRouteResponse + (*CreateApiKeyResponse)(nil), // 52: headscale.v1.CreateApiKeyResponse + (*ExpireApiKeyResponse)(nil), // 53: headscale.v1.ExpireApiKeyResponse + (*ListApiKeysResponse)(nil), // 54: headscale.v1.ListApiKeysResponse + (*DeleteApiKeyResponse)(nil), // 55: headscale.v1.DeleteApiKeyResponse + (*GetPolicyResponse)(nil), // 56: headscale.v1.GetPolicyResponse + (*SetPolicyResponse)(nil), // 57: headscale.v1.SetPolicyResponse } var file_headscale_v1_headscale_proto_depIdxs = []int32{ 0, // 0: headscale.v1.HeadscaleService.GetUser:input_type -> headscale.v1.GetUserRequest @@ -326,35 +345,39 @@ var file_headscale_v1_headscale_proto_depIdxs = []int32{ 24, // 24: headscale.v1.HeadscaleService.ExpireApiKey:input_type -> headscale.v1.ExpireApiKeyRequest 25, // 25: headscale.v1.HeadscaleService.ListApiKeys:input_type -> headscale.v1.ListApiKeysRequest 26, // 26: headscale.v1.HeadscaleService.DeleteApiKey:input_type -> headscale.v1.DeleteApiKeyRequest - 27, // 27: headscale.v1.HeadscaleService.GetUser:output_type -> headscale.v1.GetUserResponse - 28, // 28: headscale.v1.HeadscaleService.CreateUser:output_type -> headscale.v1.CreateUserResponse - 29, // 29: headscale.v1.HeadscaleService.RenameUser:output_type -> headscale.v1.RenameUserResponse - 30, // 30: headscale.v1.HeadscaleService.DeleteUser:output_type -> headscale.v1.DeleteUserResponse - 31, // 31: headscale.v1.HeadscaleService.ListUsers:output_type -> headscale.v1.ListUsersResponse - 32, // 32: headscale.v1.HeadscaleService.CreatePreAuthKey:output_type -> headscale.v1.CreatePreAuthKeyResponse - 33, // 33: headscale.v1.HeadscaleService.ExpirePreAuthKey:output_type -> headscale.v1.ExpirePreAuthKeyResponse - 34, // 34: headscale.v1.HeadscaleService.ListPreAuthKeys:output_type -> headscale.v1.ListPreAuthKeysResponse - 35, // 35: headscale.v1.HeadscaleService.DebugCreateNode:output_type -> headscale.v1.DebugCreateNodeResponse - 36, // 36: headscale.v1.HeadscaleService.GetNode:output_type -> headscale.v1.GetNodeResponse - 37, // 37: headscale.v1.HeadscaleService.SetTags:output_type -> headscale.v1.SetTagsResponse - 38, // 38: headscale.v1.HeadscaleService.RegisterNode:output_type -> headscale.v1.RegisterNodeResponse - 39, // 39: headscale.v1.HeadscaleService.DeleteNode:output_type -> headscale.v1.DeleteNodeResponse - 40, // 40: headscale.v1.HeadscaleService.ExpireNode:output_type -> headscale.v1.ExpireNodeResponse - 41, // 41: headscale.v1.HeadscaleService.RenameNode:output_type -> headscale.v1.RenameNodeResponse - 42, // 42: headscale.v1.HeadscaleService.ListNodes:output_type -> headscale.v1.ListNodesResponse - 43, // 43: headscale.v1.HeadscaleService.MoveNode:output_type -> headscale.v1.MoveNodeResponse - 44, // 44: headscale.v1.HeadscaleService.BackfillNodeIPs:output_type -> headscale.v1.BackfillNodeIPsResponse - 45, // 45: headscale.v1.HeadscaleService.GetRoutes:output_type -> headscale.v1.GetRoutesResponse - 46, // 46: headscale.v1.HeadscaleService.EnableRoute:output_type -> headscale.v1.EnableRouteResponse - 47, // 47: headscale.v1.HeadscaleService.DisableRoute:output_type -> headscale.v1.DisableRouteResponse - 48, // 48: headscale.v1.HeadscaleService.GetNodeRoutes:output_type -> headscale.v1.GetNodeRoutesResponse - 49, // 49: headscale.v1.HeadscaleService.DeleteRoute:output_type -> headscale.v1.DeleteRouteResponse - 50, // 50: headscale.v1.HeadscaleService.CreateApiKey:output_type -> headscale.v1.CreateApiKeyResponse - 51, // 51: headscale.v1.HeadscaleService.ExpireApiKey:output_type -> headscale.v1.ExpireApiKeyResponse - 52, // 52: headscale.v1.HeadscaleService.ListApiKeys:output_type -> headscale.v1.ListApiKeysResponse - 53, // 53: headscale.v1.HeadscaleService.DeleteApiKey:output_type -> headscale.v1.DeleteApiKeyResponse - 27, // [27:54] is the sub-list for method output_type - 0, // [0:27] is the sub-list for method input_type + 27, // 27: headscale.v1.HeadscaleService.GetPolicy:input_type -> headscale.v1.GetPolicyRequest + 28, // 28: headscale.v1.HeadscaleService.SetPolicy:input_type -> headscale.v1.SetPolicyRequest + 29, // 29: headscale.v1.HeadscaleService.GetUser:output_type -> headscale.v1.GetUserResponse + 30, // 30: headscale.v1.HeadscaleService.CreateUser:output_type -> headscale.v1.CreateUserResponse + 31, // 31: headscale.v1.HeadscaleService.RenameUser:output_type -> headscale.v1.RenameUserResponse + 32, // 32: headscale.v1.HeadscaleService.DeleteUser:output_type -> headscale.v1.DeleteUserResponse + 33, // 33: headscale.v1.HeadscaleService.ListUsers:output_type -> headscale.v1.ListUsersResponse + 34, // 34: headscale.v1.HeadscaleService.CreatePreAuthKey:output_type -> headscale.v1.CreatePreAuthKeyResponse + 35, // 35: headscale.v1.HeadscaleService.ExpirePreAuthKey:output_type -> headscale.v1.ExpirePreAuthKeyResponse + 36, // 36: headscale.v1.HeadscaleService.ListPreAuthKeys:output_type -> headscale.v1.ListPreAuthKeysResponse + 37, // 37: headscale.v1.HeadscaleService.DebugCreateNode:output_type -> headscale.v1.DebugCreateNodeResponse + 38, // 38: headscale.v1.HeadscaleService.GetNode:output_type -> headscale.v1.GetNodeResponse + 39, // 39: headscale.v1.HeadscaleService.SetTags:output_type -> headscale.v1.SetTagsResponse + 40, // 40: headscale.v1.HeadscaleService.RegisterNode:output_type -> headscale.v1.RegisterNodeResponse + 41, // 41: headscale.v1.HeadscaleService.DeleteNode:output_type -> headscale.v1.DeleteNodeResponse + 42, // 42: headscale.v1.HeadscaleService.ExpireNode:output_type -> headscale.v1.ExpireNodeResponse + 43, // 43: headscale.v1.HeadscaleService.RenameNode:output_type -> headscale.v1.RenameNodeResponse + 44, // 44: headscale.v1.HeadscaleService.ListNodes:output_type -> headscale.v1.ListNodesResponse + 45, // 45: headscale.v1.HeadscaleService.MoveNode:output_type -> headscale.v1.MoveNodeResponse + 46, // 46: headscale.v1.HeadscaleService.BackfillNodeIPs:output_type -> headscale.v1.BackfillNodeIPsResponse + 47, // 47: headscale.v1.HeadscaleService.GetRoutes:output_type -> headscale.v1.GetRoutesResponse + 48, // 48: headscale.v1.HeadscaleService.EnableRoute:output_type -> headscale.v1.EnableRouteResponse + 49, // 49: headscale.v1.HeadscaleService.DisableRoute:output_type -> headscale.v1.DisableRouteResponse + 50, // 50: headscale.v1.HeadscaleService.GetNodeRoutes:output_type -> headscale.v1.GetNodeRoutesResponse + 51, // 51: headscale.v1.HeadscaleService.DeleteRoute:output_type -> headscale.v1.DeleteRouteResponse + 52, // 52: headscale.v1.HeadscaleService.CreateApiKey:output_type -> headscale.v1.CreateApiKeyResponse + 53, // 53: headscale.v1.HeadscaleService.ExpireApiKey:output_type -> headscale.v1.ExpireApiKeyResponse + 54, // 54: headscale.v1.HeadscaleService.ListApiKeys:output_type -> headscale.v1.ListApiKeysResponse + 55, // 55: headscale.v1.HeadscaleService.DeleteApiKey:output_type -> headscale.v1.DeleteApiKeyResponse + 56, // 56: headscale.v1.HeadscaleService.GetPolicy:output_type -> headscale.v1.GetPolicyResponse + 57, // 57: headscale.v1.HeadscaleService.SetPolicy:output_type -> headscale.v1.SetPolicyResponse + 29, // [29:58] is the sub-list for method output_type + 0, // [0:29] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name @@ -370,6 +393,7 @@ func file_headscale_v1_headscale_proto_init() { file_headscale_v1_node_proto_init() file_headscale_v1_routes_proto_init() file_headscale_v1_apikey_proto_init() + file_headscale_v1_policy_proto_init() type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/gen/go/headscale/v1/headscale.pb.gw.go b/gen/go/headscale/v1/headscale.pb.gw.go index adc7beeb..98c6039b 100644 --- a/gen/go/headscale/v1/headscale.pb.gw.go +++ b/gen/go/headscale/v1/headscale.pb.gw.go @@ -87,7 +87,11 @@ func request_HeadscaleService_CreateUser_0(ctx context.Context, marshaler runtim var protoReq CreateUserRequest var metadata runtime.ServerMetadata - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -100,7 +104,11 @@ func local_request_HeadscaleService_CreateUser_0(ctx context.Context, marshaler var protoReq CreateUserRequest var metadata runtime.ServerMetadata - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -255,7 +263,11 @@ func request_HeadscaleService_CreatePreAuthKey_0(ctx context.Context, marshaler var protoReq CreatePreAuthKeyRequest var metadata runtime.ServerMetadata - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -268,7 +280,11 @@ func local_request_HeadscaleService_CreatePreAuthKey_0(ctx context.Context, mars var protoReq CreatePreAuthKeyRequest var metadata runtime.ServerMetadata - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -281,7 +297,11 @@ func request_HeadscaleService_ExpirePreAuthKey_0(ctx context.Context, marshaler var protoReq ExpirePreAuthKeyRequest var metadata runtime.ServerMetadata - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -294,7 +314,11 @@ func local_request_HeadscaleService_ExpirePreAuthKey_0(ctx context.Context, mars var protoReq ExpirePreAuthKeyRequest var metadata runtime.ServerMetadata - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -343,7 +367,11 @@ func request_HeadscaleService_DebugCreateNode_0(ctx context.Context, marshaler r var protoReq DebugCreateNodeRequest var metadata runtime.ServerMetadata - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -356,7 +384,11 @@ func local_request_HeadscaleService_DebugCreateNode_0(ctx context.Context, marsh var protoReq DebugCreateNodeRequest var metadata runtime.ServerMetadata - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -421,7 +453,11 @@ func request_HeadscaleService_SetTags_0(ctx context.Context, marshaler runtime.M var protoReq SetTagsRequest var metadata runtime.ServerMetadata - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -451,7 +487,11 @@ func local_request_HeadscaleService_SetTags_0(ctx context.Context, marshaler run var protoReq SetTagsRequest var metadata runtime.ServerMetadata - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -1061,7 +1101,11 @@ func request_HeadscaleService_CreateApiKey_0(ctx context.Context, marshaler runt var protoReq CreateApiKeyRequest var metadata runtime.ServerMetadata - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -1074,7 +1118,11 @@ func local_request_HeadscaleService_CreateApiKey_0(ctx context.Context, marshale var protoReq CreateApiKeyRequest var metadata runtime.ServerMetadata - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -1087,7 +1135,11 @@ func request_HeadscaleService_ExpireApiKey_0(ctx context.Context, marshaler runt var protoReq ExpireApiKeyRequest var metadata runtime.ServerMetadata - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -1100,7 +1152,11 @@ func local_request_HeadscaleService_ExpireApiKey_0(ctx context.Context, marshale var protoReq ExpireApiKeyRequest var metadata runtime.ServerMetadata - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -1179,6 +1235,58 @@ func local_request_HeadscaleService_DeleteApiKey_0(ctx context.Context, marshale } +func request_HeadscaleService_GetPolicy_0(ctx context.Context, marshaler runtime.Marshaler, client HeadscaleServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetPolicyRequest + var metadata runtime.ServerMetadata + + msg, err := client.GetPolicy(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_HeadscaleService_GetPolicy_0(ctx context.Context, marshaler runtime.Marshaler, server HeadscaleServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetPolicyRequest + var metadata runtime.ServerMetadata + + msg, err := server.GetPolicy(ctx, &protoReq) + return msg, metadata, err + +} + +func request_HeadscaleService_SetPolicy_0(ctx context.Context, marshaler runtime.Marshaler, client HeadscaleServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq SetPolicyRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.SetPolicy(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_HeadscaleService_SetPolicy_0(ctx context.Context, marshaler runtime.Marshaler, server HeadscaleServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq SetPolicyRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.SetPolicy(ctx, &protoReq) + return msg, metadata, err + +} + // RegisterHeadscaleServiceHandlerServer registers the http handlers for service HeadscaleService to "mux". // UnaryRPC :call HeadscaleServiceServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. @@ -1860,13 +1968,63 @@ func RegisterHeadscaleServiceHandlerServer(ctx context.Context, mux *runtime.Ser }) + mux.Handle("GET", pattern_HeadscaleService_GetPolicy_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/headscale.v1.HeadscaleService/GetPolicy", runtime.WithHTTPPathPattern("/api/v1/policy")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_HeadscaleService_GetPolicy_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_HeadscaleService_GetPolicy_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("PUT", pattern_HeadscaleService_SetPolicy_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/headscale.v1.HeadscaleService/SetPolicy", runtime.WithHTTPPathPattern("/api/v1/policy")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_HeadscaleService_SetPolicy_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_HeadscaleService_SetPolicy_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + return nil } // RegisterHeadscaleServiceHandlerFromEndpoint is same as RegisterHeadscaleServiceHandler but // automatically dials to "endpoint" and closes the connection when "ctx" gets done. func RegisterHeadscaleServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { - conn, err := grpc.DialContext(ctx, endpoint, opts...) + conn, err := grpc.Dial(endpoint, opts...) if err != nil { return err } @@ -2495,6 +2653,50 @@ func RegisterHeadscaleServiceHandlerClient(ctx context.Context, mux *runtime.Ser }) + mux.Handle("GET", pattern_HeadscaleService_GetPolicy_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/headscale.v1.HeadscaleService/GetPolicy", runtime.WithHTTPPathPattern("/api/v1/policy")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_HeadscaleService_GetPolicy_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_HeadscaleService_GetPolicy_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("PUT", pattern_HeadscaleService_SetPolicy_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/headscale.v1.HeadscaleService/SetPolicy", runtime.WithHTTPPathPattern("/api/v1/policy")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_HeadscaleService_SetPolicy_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_HeadscaleService_SetPolicy_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + return nil } @@ -2552,6 +2754,10 @@ var ( pattern_HeadscaleService_ListApiKeys_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "apikey"}, "")) pattern_HeadscaleService_DeleteApiKey_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "v1", "apikey", "prefix"}, "")) + + pattern_HeadscaleService_GetPolicy_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "policy"}, "")) + + pattern_HeadscaleService_SetPolicy_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "policy"}, "")) ) var ( @@ -2608,4 +2814,8 @@ var ( forward_HeadscaleService_ListApiKeys_0 = runtime.ForwardResponseMessage forward_HeadscaleService_DeleteApiKey_0 = runtime.ForwardResponseMessage + + forward_HeadscaleService_GetPolicy_0 = runtime.ForwardResponseMessage + + forward_HeadscaleService_SetPolicy_0 = runtime.ForwardResponseMessage ) diff --git a/gen/go/headscale/v1/headscale_grpc.pb.go b/gen/go/headscale/v1/headscale_grpc.pb.go index 6557f880..df9cf197 100644 --- a/gen/go/headscale/v1/headscale_grpc.pb.go +++ b/gen/go/headscale/v1/headscale_grpc.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.3.0 +// - protoc-gen-go-grpc v1.2.0 // - protoc (unknown) // source: headscale/v1/headscale.proto @@ -18,36 +18,6 @@ import ( // Requires gRPC-Go v1.32.0 or later. const _ = grpc.SupportPackageIsVersion7 -const ( - HeadscaleService_GetUser_FullMethodName = "/headscale.v1.HeadscaleService/GetUser" - HeadscaleService_CreateUser_FullMethodName = "/headscale.v1.HeadscaleService/CreateUser" - HeadscaleService_RenameUser_FullMethodName = "/headscale.v1.HeadscaleService/RenameUser" - HeadscaleService_DeleteUser_FullMethodName = "/headscale.v1.HeadscaleService/DeleteUser" - HeadscaleService_ListUsers_FullMethodName = "/headscale.v1.HeadscaleService/ListUsers" - HeadscaleService_CreatePreAuthKey_FullMethodName = "/headscale.v1.HeadscaleService/CreatePreAuthKey" - HeadscaleService_ExpirePreAuthKey_FullMethodName = "/headscale.v1.HeadscaleService/ExpirePreAuthKey" - HeadscaleService_ListPreAuthKeys_FullMethodName = "/headscale.v1.HeadscaleService/ListPreAuthKeys" - HeadscaleService_DebugCreateNode_FullMethodName = "/headscale.v1.HeadscaleService/DebugCreateNode" - HeadscaleService_GetNode_FullMethodName = "/headscale.v1.HeadscaleService/GetNode" - HeadscaleService_SetTags_FullMethodName = "/headscale.v1.HeadscaleService/SetTags" - HeadscaleService_RegisterNode_FullMethodName = "/headscale.v1.HeadscaleService/RegisterNode" - HeadscaleService_DeleteNode_FullMethodName = "/headscale.v1.HeadscaleService/DeleteNode" - HeadscaleService_ExpireNode_FullMethodName = "/headscale.v1.HeadscaleService/ExpireNode" - HeadscaleService_RenameNode_FullMethodName = "/headscale.v1.HeadscaleService/RenameNode" - HeadscaleService_ListNodes_FullMethodName = "/headscale.v1.HeadscaleService/ListNodes" - HeadscaleService_MoveNode_FullMethodName = "/headscale.v1.HeadscaleService/MoveNode" - HeadscaleService_BackfillNodeIPs_FullMethodName = "/headscale.v1.HeadscaleService/BackfillNodeIPs" - HeadscaleService_GetRoutes_FullMethodName = "/headscale.v1.HeadscaleService/GetRoutes" - HeadscaleService_EnableRoute_FullMethodName = "/headscale.v1.HeadscaleService/EnableRoute" - HeadscaleService_DisableRoute_FullMethodName = "/headscale.v1.HeadscaleService/DisableRoute" - HeadscaleService_GetNodeRoutes_FullMethodName = "/headscale.v1.HeadscaleService/GetNodeRoutes" - HeadscaleService_DeleteRoute_FullMethodName = "/headscale.v1.HeadscaleService/DeleteRoute" - HeadscaleService_CreateApiKey_FullMethodName = "/headscale.v1.HeadscaleService/CreateApiKey" - HeadscaleService_ExpireApiKey_FullMethodName = "/headscale.v1.HeadscaleService/ExpireApiKey" - HeadscaleService_ListApiKeys_FullMethodName = "/headscale.v1.HeadscaleService/ListApiKeys" - HeadscaleService_DeleteApiKey_FullMethodName = "/headscale.v1.HeadscaleService/DeleteApiKey" -) - // HeadscaleServiceClient is the client API for HeadscaleService service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. @@ -84,6 +54,9 @@ type HeadscaleServiceClient interface { ExpireApiKey(ctx context.Context, in *ExpireApiKeyRequest, opts ...grpc.CallOption) (*ExpireApiKeyResponse, error) ListApiKeys(ctx context.Context, in *ListApiKeysRequest, opts ...grpc.CallOption) (*ListApiKeysResponse, error) DeleteApiKey(ctx context.Context, in *DeleteApiKeyRequest, opts ...grpc.CallOption) (*DeleteApiKeyResponse, error) + // --- Policy start --- + GetPolicy(ctx context.Context, in *GetPolicyRequest, opts ...grpc.CallOption) (*GetPolicyResponse, error) + SetPolicy(ctx context.Context, in *SetPolicyRequest, opts ...grpc.CallOption) (*SetPolicyResponse, error) } type headscaleServiceClient struct { @@ -96,7 +69,7 @@ func NewHeadscaleServiceClient(cc grpc.ClientConnInterface) HeadscaleServiceClie func (c *headscaleServiceClient) GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*GetUserResponse, error) { out := new(GetUserResponse) - err := c.cc.Invoke(ctx, HeadscaleService_GetUser_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/GetUser", in, out, opts...) if err != nil { return nil, err } @@ -105,7 +78,7 @@ func (c *headscaleServiceClient) GetUser(ctx context.Context, in *GetUserRequest func (c *headscaleServiceClient) CreateUser(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*CreateUserResponse, error) { out := new(CreateUserResponse) - err := c.cc.Invoke(ctx, HeadscaleService_CreateUser_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/CreateUser", in, out, opts...) if err != nil { return nil, err } @@ -114,7 +87,7 @@ func (c *headscaleServiceClient) CreateUser(ctx context.Context, in *CreateUserR func (c *headscaleServiceClient) RenameUser(ctx context.Context, in *RenameUserRequest, opts ...grpc.CallOption) (*RenameUserResponse, error) { out := new(RenameUserResponse) - err := c.cc.Invoke(ctx, HeadscaleService_RenameUser_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/RenameUser", in, out, opts...) if err != nil { return nil, err } @@ -123,7 +96,7 @@ func (c *headscaleServiceClient) RenameUser(ctx context.Context, in *RenameUserR func (c *headscaleServiceClient) DeleteUser(ctx context.Context, in *DeleteUserRequest, opts ...grpc.CallOption) (*DeleteUserResponse, error) { out := new(DeleteUserResponse) - err := c.cc.Invoke(ctx, HeadscaleService_DeleteUser_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/DeleteUser", in, out, opts...) if err != nil { return nil, err } @@ -132,7 +105,7 @@ func (c *headscaleServiceClient) DeleteUser(ctx context.Context, in *DeleteUserR func (c *headscaleServiceClient) ListUsers(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (*ListUsersResponse, error) { out := new(ListUsersResponse) - err := c.cc.Invoke(ctx, HeadscaleService_ListUsers_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ListUsers", in, out, opts...) if err != nil { return nil, err } @@ -141,7 +114,7 @@ func (c *headscaleServiceClient) ListUsers(ctx context.Context, in *ListUsersReq func (c *headscaleServiceClient) CreatePreAuthKey(ctx context.Context, in *CreatePreAuthKeyRequest, opts ...grpc.CallOption) (*CreatePreAuthKeyResponse, error) { out := new(CreatePreAuthKeyResponse) - err := c.cc.Invoke(ctx, HeadscaleService_CreatePreAuthKey_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/CreatePreAuthKey", in, out, opts...) if err != nil { return nil, err } @@ -150,7 +123,7 @@ func (c *headscaleServiceClient) CreatePreAuthKey(ctx context.Context, in *Creat func (c *headscaleServiceClient) ExpirePreAuthKey(ctx context.Context, in *ExpirePreAuthKeyRequest, opts ...grpc.CallOption) (*ExpirePreAuthKeyResponse, error) { out := new(ExpirePreAuthKeyResponse) - err := c.cc.Invoke(ctx, HeadscaleService_ExpirePreAuthKey_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ExpirePreAuthKey", in, out, opts...) if err != nil { return nil, err } @@ -159,7 +132,7 @@ func (c *headscaleServiceClient) ExpirePreAuthKey(ctx context.Context, in *Expir func (c *headscaleServiceClient) ListPreAuthKeys(ctx context.Context, in *ListPreAuthKeysRequest, opts ...grpc.CallOption) (*ListPreAuthKeysResponse, error) { out := new(ListPreAuthKeysResponse) - err := c.cc.Invoke(ctx, HeadscaleService_ListPreAuthKeys_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ListPreAuthKeys", in, out, opts...) if err != nil { return nil, err } @@ -168,7 +141,7 @@ func (c *headscaleServiceClient) ListPreAuthKeys(ctx context.Context, in *ListPr func (c *headscaleServiceClient) DebugCreateNode(ctx context.Context, in *DebugCreateNodeRequest, opts ...grpc.CallOption) (*DebugCreateNodeResponse, error) { out := new(DebugCreateNodeResponse) - err := c.cc.Invoke(ctx, HeadscaleService_DebugCreateNode_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/DebugCreateNode", in, out, opts...) if err != nil { return nil, err } @@ -177,7 +150,7 @@ func (c *headscaleServiceClient) DebugCreateNode(ctx context.Context, in *DebugC func (c *headscaleServiceClient) GetNode(ctx context.Context, in *GetNodeRequest, opts ...grpc.CallOption) (*GetNodeResponse, error) { out := new(GetNodeResponse) - err := c.cc.Invoke(ctx, HeadscaleService_GetNode_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/GetNode", in, out, opts...) if err != nil { return nil, err } @@ -186,7 +159,7 @@ func (c *headscaleServiceClient) GetNode(ctx context.Context, in *GetNodeRequest func (c *headscaleServiceClient) SetTags(ctx context.Context, in *SetTagsRequest, opts ...grpc.CallOption) (*SetTagsResponse, error) { out := new(SetTagsResponse) - err := c.cc.Invoke(ctx, HeadscaleService_SetTags_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/SetTags", in, out, opts...) if err != nil { return nil, err } @@ -195,7 +168,7 @@ func (c *headscaleServiceClient) SetTags(ctx context.Context, in *SetTagsRequest func (c *headscaleServiceClient) RegisterNode(ctx context.Context, in *RegisterNodeRequest, opts ...grpc.CallOption) (*RegisterNodeResponse, error) { out := new(RegisterNodeResponse) - err := c.cc.Invoke(ctx, HeadscaleService_RegisterNode_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/RegisterNode", in, out, opts...) if err != nil { return nil, err } @@ -204,7 +177,7 @@ func (c *headscaleServiceClient) RegisterNode(ctx context.Context, in *RegisterN func (c *headscaleServiceClient) DeleteNode(ctx context.Context, in *DeleteNodeRequest, opts ...grpc.CallOption) (*DeleteNodeResponse, error) { out := new(DeleteNodeResponse) - err := c.cc.Invoke(ctx, HeadscaleService_DeleteNode_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/DeleteNode", in, out, opts...) if err != nil { return nil, err } @@ -213,7 +186,7 @@ func (c *headscaleServiceClient) DeleteNode(ctx context.Context, in *DeleteNodeR func (c *headscaleServiceClient) ExpireNode(ctx context.Context, in *ExpireNodeRequest, opts ...grpc.CallOption) (*ExpireNodeResponse, error) { out := new(ExpireNodeResponse) - err := c.cc.Invoke(ctx, HeadscaleService_ExpireNode_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ExpireNode", in, out, opts...) if err != nil { return nil, err } @@ -222,7 +195,7 @@ func (c *headscaleServiceClient) ExpireNode(ctx context.Context, in *ExpireNodeR func (c *headscaleServiceClient) RenameNode(ctx context.Context, in *RenameNodeRequest, opts ...grpc.CallOption) (*RenameNodeResponse, error) { out := new(RenameNodeResponse) - err := c.cc.Invoke(ctx, HeadscaleService_RenameNode_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/RenameNode", in, out, opts...) if err != nil { return nil, err } @@ -231,7 +204,7 @@ func (c *headscaleServiceClient) RenameNode(ctx context.Context, in *RenameNodeR func (c *headscaleServiceClient) ListNodes(ctx context.Context, in *ListNodesRequest, opts ...grpc.CallOption) (*ListNodesResponse, error) { out := new(ListNodesResponse) - err := c.cc.Invoke(ctx, HeadscaleService_ListNodes_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ListNodes", in, out, opts...) if err != nil { return nil, err } @@ -240,7 +213,7 @@ func (c *headscaleServiceClient) ListNodes(ctx context.Context, in *ListNodesReq func (c *headscaleServiceClient) MoveNode(ctx context.Context, in *MoveNodeRequest, opts ...grpc.CallOption) (*MoveNodeResponse, error) { out := new(MoveNodeResponse) - err := c.cc.Invoke(ctx, HeadscaleService_MoveNode_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/MoveNode", in, out, opts...) if err != nil { return nil, err } @@ -249,7 +222,7 @@ func (c *headscaleServiceClient) MoveNode(ctx context.Context, in *MoveNodeReque func (c *headscaleServiceClient) BackfillNodeIPs(ctx context.Context, in *BackfillNodeIPsRequest, opts ...grpc.CallOption) (*BackfillNodeIPsResponse, error) { out := new(BackfillNodeIPsResponse) - err := c.cc.Invoke(ctx, HeadscaleService_BackfillNodeIPs_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/BackfillNodeIPs", in, out, opts...) if err != nil { return nil, err } @@ -258,7 +231,7 @@ func (c *headscaleServiceClient) BackfillNodeIPs(ctx context.Context, in *Backfi func (c *headscaleServiceClient) GetRoutes(ctx context.Context, in *GetRoutesRequest, opts ...grpc.CallOption) (*GetRoutesResponse, error) { out := new(GetRoutesResponse) - err := c.cc.Invoke(ctx, HeadscaleService_GetRoutes_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/GetRoutes", in, out, opts...) if err != nil { return nil, err } @@ -267,7 +240,7 @@ func (c *headscaleServiceClient) GetRoutes(ctx context.Context, in *GetRoutesReq func (c *headscaleServiceClient) EnableRoute(ctx context.Context, in *EnableRouteRequest, opts ...grpc.CallOption) (*EnableRouteResponse, error) { out := new(EnableRouteResponse) - err := c.cc.Invoke(ctx, HeadscaleService_EnableRoute_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/EnableRoute", in, out, opts...) if err != nil { return nil, err } @@ -276,7 +249,7 @@ func (c *headscaleServiceClient) EnableRoute(ctx context.Context, in *EnableRout func (c *headscaleServiceClient) DisableRoute(ctx context.Context, in *DisableRouteRequest, opts ...grpc.CallOption) (*DisableRouteResponse, error) { out := new(DisableRouteResponse) - err := c.cc.Invoke(ctx, HeadscaleService_DisableRoute_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/DisableRoute", in, out, opts...) if err != nil { return nil, err } @@ -285,7 +258,7 @@ func (c *headscaleServiceClient) DisableRoute(ctx context.Context, in *DisableRo func (c *headscaleServiceClient) GetNodeRoutes(ctx context.Context, in *GetNodeRoutesRequest, opts ...grpc.CallOption) (*GetNodeRoutesResponse, error) { out := new(GetNodeRoutesResponse) - err := c.cc.Invoke(ctx, HeadscaleService_GetNodeRoutes_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/GetNodeRoutes", in, out, opts...) if err != nil { return nil, err } @@ -294,7 +267,7 @@ func (c *headscaleServiceClient) GetNodeRoutes(ctx context.Context, in *GetNodeR func (c *headscaleServiceClient) DeleteRoute(ctx context.Context, in *DeleteRouteRequest, opts ...grpc.CallOption) (*DeleteRouteResponse, error) { out := new(DeleteRouteResponse) - err := c.cc.Invoke(ctx, HeadscaleService_DeleteRoute_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/DeleteRoute", in, out, opts...) if err != nil { return nil, err } @@ -303,7 +276,7 @@ func (c *headscaleServiceClient) DeleteRoute(ctx context.Context, in *DeleteRout func (c *headscaleServiceClient) CreateApiKey(ctx context.Context, in *CreateApiKeyRequest, opts ...grpc.CallOption) (*CreateApiKeyResponse, error) { out := new(CreateApiKeyResponse) - err := c.cc.Invoke(ctx, HeadscaleService_CreateApiKey_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/CreateApiKey", in, out, opts...) if err != nil { return nil, err } @@ -312,7 +285,7 @@ func (c *headscaleServiceClient) CreateApiKey(ctx context.Context, in *CreateApi func (c *headscaleServiceClient) ExpireApiKey(ctx context.Context, in *ExpireApiKeyRequest, opts ...grpc.CallOption) (*ExpireApiKeyResponse, error) { out := new(ExpireApiKeyResponse) - err := c.cc.Invoke(ctx, HeadscaleService_ExpireApiKey_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ExpireApiKey", in, out, opts...) if err != nil { return nil, err } @@ -321,7 +294,7 @@ func (c *headscaleServiceClient) ExpireApiKey(ctx context.Context, in *ExpireApi func (c *headscaleServiceClient) ListApiKeys(ctx context.Context, in *ListApiKeysRequest, opts ...grpc.CallOption) (*ListApiKeysResponse, error) { out := new(ListApiKeysResponse) - err := c.cc.Invoke(ctx, HeadscaleService_ListApiKeys_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ListApiKeys", in, out, opts...) if err != nil { return nil, err } @@ -330,7 +303,25 @@ func (c *headscaleServiceClient) ListApiKeys(ctx context.Context, in *ListApiKey func (c *headscaleServiceClient) DeleteApiKey(ctx context.Context, in *DeleteApiKeyRequest, opts ...grpc.CallOption) (*DeleteApiKeyResponse, error) { out := new(DeleteApiKeyResponse) - err := c.cc.Invoke(ctx, HeadscaleService_DeleteApiKey_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/DeleteApiKey", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *headscaleServiceClient) GetPolicy(ctx context.Context, in *GetPolicyRequest, opts ...grpc.CallOption) (*GetPolicyResponse, error) { + out := new(GetPolicyResponse) + err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/GetPolicy", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *headscaleServiceClient) SetPolicy(ctx context.Context, in *SetPolicyRequest, opts ...grpc.CallOption) (*SetPolicyResponse, error) { + out := new(SetPolicyResponse) + err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/SetPolicy", in, out, opts...) if err != nil { return nil, err } @@ -373,6 +364,9 @@ type HeadscaleServiceServer interface { ExpireApiKey(context.Context, *ExpireApiKeyRequest) (*ExpireApiKeyResponse, error) ListApiKeys(context.Context, *ListApiKeysRequest) (*ListApiKeysResponse, error) DeleteApiKey(context.Context, *DeleteApiKeyRequest) (*DeleteApiKeyResponse, error) + // --- Policy start --- + GetPolicy(context.Context, *GetPolicyRequest) (*GetPolicyResponse, error) + SetPolicy(context.Context, *SetPolicyRequest) (*SetPolicyResponse, error) mustEmbedUnimplementedHeadscaleServiceServer() } @@ -461,6 +455,12 @@ func (UnimplementedHeadscaleServiceServer) ListApiKeys(context.Context, *ListApi func (UnimplementedHeadscaleServiceServer) DeleteApiKey(context.Context, *DeleteApiKeyRequest) (*DeleteApiKeyResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method DeleteApiKey not implemented") } +func (UnimplementedHeadscaleServiceServer) GetPolicy(context.Context, *GetPolicyRequest) (*GetPolicyResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetPolicy not implemented") +} +func (UnimplementedHeadscaleServiceServer) SetPolicy(context.Context, *SetPolicyRequest) (*SetPolicyResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SetPolicy not implemented") +} func (UnimplementedHeadscaleServiceServer) mustEmbedUnimplementedHeadscaleServiceServer() {} // UnsafeHeadscaleServiceServer may be embedded to opt out of forward compatibility for this service. @@ -484,7 +484,7 @@ func _HeadscaleService_GetUser_Handler(srv interface{}, ctx context.Context, dec } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: HeadscaleService_GetUser_FullMethodName, + FullMethod: "/headscale.v1.HeadscaleService/GetUser", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).GetUser(ctx, req.(*GetUserRequest)) @@ -502,7 +502,7 @@ func _HeadscaleService_CreateUser_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: HeadscaleService_CreateUser_FullMethodName, + FullMethod: "/headscale.v1.HeadscaleService/CreateUser", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).CreateUser(ctx, req.(*CreateUserRequest)) @@ -520,7 +520,7 @@ func _HeadscaleService_RenameUser_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: HeadscaleService_RenameUser_FullMethodName, + FullMethod: "/headscale.v1.HeadscaleService/RenameUser", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).RenameUser(ctx, req.(*RenameUserRequest)) @@ -538,7 +538,7 @@ func _HeadscaleService_DeleteUser_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: HeadscaleService_DeleteUser_FullMethodName, + FullMethod: "/headscale.v1.HeadscaleService/DeleteUser", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).DeleteUser(ctx, req.(*DeleteUserRequest)) @@ -556,7 +556,7 @@ func _HeadscaleService_ListUsers_Handler(srv interface{}, ctx context.Context, d } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: HeadscaleService_ListUsers_FullMethodName, + FullMethod: "/headscale.v1.HeadscaleService/ListUsers", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).ListUsers(ctx, req.(*ListUsersRequest)) @@ -574,7 +574,7 @@ func _HeadscaleService_CreatePreAuthKey_Handler(srv interface{}, ctx context.Con } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: HeadscaleService_CreatePreAuthKey_FullMethodName, + FullMethod: "/headscale.v1.HeadscaleService/CreatePreAuthKey", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).CreatePreAuthKey(ctx, req.(*CreatePreAuthKeyRequest)) @@ -592,7 +592,7 @@ func _HeadscaleService_ExpirePreAuthKey_Handler(srv interface{}, ctx context.Con } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: HeadscaleService_ExpirePreAuthKey_FullMethodName, + FullMethod: "/headscale.v1.HeadscaleService/ExpirePreAuthKey", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).ExpirePreAuthKey(ctx, req.(*ExpirePreAuthKeyRequest)) @@ -610,7 +610,7 @@ func _HeadscaleService_ListPreAuthKeys_Handler(srv interface{}, ctx context.Cont } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: HeadscaleService_ListPreAuthKeys_FullMethodName, + FullMethod: "/headscale.v1.HeadscaleService/ListPreAuthKeys", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).ListPreAuthKeys(ctx, req.(*ListPreAuthKeysRequest)) @@ -628,7 +628,7 @@ func _HeadscaleService_DebugCreateNode_Handler(srv interface{}, ctx context.Cont } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: HeadscaleService_DebugCreateNode_FullMethodName, + FullMethod: "/headscale.v1.HeadscaleService/DebugCreateNode", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).DebugCreateNode(ctx, req.(*DebugCreateNodeRequest)) @@ -646,7 +646,7 @@ func _HeadscaleService_GetNode_Handler(srv interface{}, ctx context.Context, dec } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: HeadscaleService_GetNode_FullMethodName, + FullMethod: "/headscale.v1.HeadscaleService/GetNode", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).GetNode(ctx, req.(*GetNodeRequest)) @@ -664,7 +664,7 @@ func _HeadscaleService_SetTags_Handler(srv interface{}, ctx context.Context, dec } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: HeadscaleService_SetTags_FullMethodName, + FullMethod: "/headscale.v1.HeadscaleService/SetTags", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).SetTags(ctx, req.(*SetTagsRequest)) @@ -682,7 +682,7 @@ func _HeadscaleService_RegisterNode_Handler(srv interface{}, ctx context.Context } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: HeadscaleService_RegisterNode_FullMethodName, + FullMethod: "/headscale.v1.HeadscaleService/RegisterNode", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).RegisterNode(ctx, req.(*RegisterNodeRequest)) @@ -700,7 +700,7 @@ func _HeadscaleService_DeleteNode_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: HeadscaleService_DeleteNode_FullMethodName, + FullMethod: "/headscale.v1.HeadscaleService/DeleteNode", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).DeleteNode(ctx, req.(*DeleteNodeRequest)) @@ -718,7 +718,7 @@ func _HeadscaleService_ExpireNode_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: HeadscaleService_ExpireNode_FullMethodName, + FullMethod: "/headscale.v1.HeadscaleService/ExpireNode", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).ExpireNode(ctx, req.(*ExpireNodeRequest)) @@ -736,7 +736,7 @@ func _HeadscaleService_RenameNode_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: HeadscaleService_RenameNode_FullMethodName, + FullMethod: "/headscale.v1.HeadscaleService/RenameNode", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).RenameNode(ctx, req.(*RenameNodeRequest)) @@ -754,7 +754,7 @@ func _HeadscaleService_ListNodes_Handler(srv interface{}, ctx context.Context, d } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: HeadscaleService_ListNodes_FullMethodName, + FullMethod: "/headscale.v1.HeadscaleService/ListNodes", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).ListNodes(ctx, req.(*ListNodesRequest)) @@ -772,7 +772,7 @@ func _HeadscaleService_MoveNode_Handler(srv interface{}, ctx context.Context, de } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: HeadscaleService_MoveNode_FullMethodName, + FullMethod: "/headscale.v1.HeadscaleService/MoveNode", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).MoveNode(ctx, req.(*MoveNodeRequest)) @@ -790,7 +790,7 @@ func _HeadscaleService_BackfillNodeIPs_Handler(srv interface{}, ctx context.Cont } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: HeadscaleService_BackfillNodeIPs_FullMethodName, + FullMethod: "/headscale.v1.HeadscaleService/BackfillNodeIPs", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).BackfillNodeIPs(ctx, req.(*BackfillNodeIPsRequest)) @@ -808,7 +808,7 @@ func _HeadscaleService_GetRoutes_Handler(srv interface{}, ctx context.Context, d } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: HeadscaleService_GetRoutes_FullMethodName, + FullMethod: "/headscale.v1.HeadscaleService/GetRoutes", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).GetRoutes(ctx, req.(*GetRoutesRequest)) @@ -826,7 +826,7 @@ func _HeadscaleService_EnableRoute_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: HeadscaleService_EnableRoute_FullMethodName, + FullMethod: "/headscale.v1.HeadscaleService/EnableRoute", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).EnableRoute(ctx, req.(*EnableRouteRequest)) @@ -844,7 +844,7 @@ func _HeadscaleService_DisableRoute_Handler(srv interface{}, ctx context.Context } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: HeadscaleService_DisableRoute_FullMethodName, + FullMethod: "/headscale.v1.HeadscaleService/DisableRoute", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).DisableRoute(ctx, req.(*DisableRouteRequest)) @@ -862,7 +862,7 @@ func _HeadscaleService_GetNodeRoutes_Handler(srv interface{}, ctx context.Contex } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: HeadscaleService_GetNodeRoutes_FullMethodName, + FullMethod: "/headscale.v1.HeadscaleService/GetNodeRoutes", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).GetNodeRoutes(ctx, req.(*GetNodeRoutesRequest)) @@ -880,7 +880,7 @@ func _HeadscaleService_DeleteRoute_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: HeadscaleService_DeleteRoute_FullMethodName, + FullMethod: "/headscale.v1.HeadscaleService/DeleteRoute", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).DeleteRoute(ctx, req.(*DeleteRouteRequest)) @@ -898,7 +898,7 @@ func _HeadscaleService_CreateApiKey_Handler(srv interface{}, ctx context.Context } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: HeadscaleService_CreateApiKey_FullMethodName, + FullMethod: "/headscale.v1.HeadscaleService/CreateApiKey", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).CreateApiKey(ctx, req.(*CreateApiKeyRequest)) @@ -916,7 +916,7 @@ func _HeadscaleService_ExpireApiKey_Handler(srv interface{}, ctx context.Context } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: HeadscaleService_ExpireApiKey_FullMethodName, + FullMethod: "/headscale.v1.HeadscaleService/ExpireApiKey", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).ExpireApiKey(ctx, req.(*ExpireApiKeyRequest)) @@ -934,7 +934,7 @@ func _HeadscaleService_ListApiKeys_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: HeadscaleService_ListApiKeys_FullMethodName, + FullMethod: "/headscale.v1.HeadscaleService/ListApiKeys", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).ListApiKeys(ctx, req.(*ListApiKeysRequest)) @@ -952,7 +952,7 @@ func _HeadscaleService_DeleteApiKey_Handler(srv interface{}, ctx context.Context } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: HeadscaleService_DeleteApiKey_FullMethodName, + FullMethod: "/headscale.v1.HeadscaleService/DeleteApiKey", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).DeleteApiKey(ctx, req.(*DeleteApiKeyRequest)) @@ -960,6 +960,42 @@ func _HeadscaleService_DeleteApiKey_Handler(srv interface{}, ctx context.Context return interceptor(ctx, in, info, handler) } +func _HeadscaleService_GetPolicy_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetPolicyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(HeadscaleServiceServer).GetPolicy(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/headscale.v1.HeadscaleService/GetPolicy", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(HeadscaleServiceServer).GetPolicy(ctx, req.(*GetPolicyRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _HeadscaleService_SetPolicy_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SetPolicyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(HeadscaleServiceServer).SetPolicy(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/headscale.v1.HeadscaleService/SetPolicy", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(HeadscaleServiceServer).SetPolicy(ctx, req.(*SetPolicyRequest)) + } + return interceptor(ctx, in, info, handler) +} + // HeadscaleService_ServiceDesc is the grpc.ServiceDesc for HeadscaleService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -1075,6 +1111,14 @@ var HeadscaleService_ServiceDesc = grpc.ServiceDesc{ MethodName: "DeleteApiKey", Handler: _HeadscaleService_DeleteApiKey_Handler, }, + { + MethodName: "GetPolicy", + Handler: _HeadscaleService_GetPolicy_Handler, + }, + { + MethodName: "SetPolicy", + Handler: _HeadscaleService_SetPolicy_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "headscale/v1/headscale.proto", diff --git a/gen/go/headscale/v1/node.pb.go b/gen/go/headscale/v1/node.pb.go index 93d2c6b0..b961ca73 100644 --- a/gen/go/headscale/v1/node.pb.go +++ b/gen/go/headscale/v1/node.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.33.0 +// protoc-gen-go v1.32.0 // protoc (unknown) // source: headscale/v1/node.proto diff --git a/gen/go/headscale/v1/policy.pb.go b/gen/go/headscale/v1/policy.pb.go new file mode 100644 index 00000000..31ecffdf --- /dev/null +++ b/gen/go/headscale/v1/policy.pb.go @@ -0,0 +1,352 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.32.0 +// protoc (unknown) +// source: headscale/v1/policy.proto + +package v1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SetPolicyRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Policy string `protobuf:"bytes,1,opt,name=policy,proto3" json:"policy,omitempty"` +} + +func (x *SetPolicyRequest) Reset() { + *x = SetPolicyRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_headscale_v1_policy_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SetPolicyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetPolicyRequest) ProtoMessage() {} + +func (x *SetPolicyRequest) ProtoReflect() protoreflect.Message { + mi := &file_headscale_v1_policy_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SetPolicyRequest.ProtoReflect.Descriptor instead. +func (*SetPolicyRequest) Descriptor() ([]byte, []int) { + return file_headscale_v1_policy_proto_rawDescGZIP(), []int{0} +} + +func (x *SetPolicyRequest) GetPolicy() string { + if x != nil { + return x.Policy + } + return "" +} + +type SetPolicyResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Policy string `protobuf:"bytes,1,opt,name=policy,proto3" json:"policy,omitempty"` + UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` +} + +func (x *SetPolicyResponse) Reset() { + *x = SetPolicyResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_headscale_v1_policy_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SetPolicyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetPolicyResponse) ProtoMessage() {} + +func (x *SetPolicyResponse) ProtoReflect() protoreflect.Message { + mi := &file_headscale_v1_policy_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SetPolicyResponse.ProtoReflect.Descriptor instead. +func (*SetPolicyResponse) Descriptor() ([]byte, []int) { + return file_headscale_v1_policy_proto_rawDescGZIP(), []int{1} +} + +func (x *SetPolicyResponse) GetPolicy() string { + if x != nil { + return x.Policy + } + return "" +} + +func (x *SetPolicyResponse) GetUpdatedAt() *timestamppb.Timestamp { + if x != nil { + return x.UpdatedAt + } + return nil +} + +type GetPolicyRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *GetPolicyRequest) Reset() { + *x = GetPolicyRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_headscale_v1_policy_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetPolicyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetPolicyRequest) ProtoMessage() {} + +func (x *GetPolicyRequest) ProtoReflect() protoreflect.Message { + mi := &file_headscale_v1_policy_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetPolicyRequest.ProtoReflect.Descriptor instead. +func (*GetPolicyRequest) Descriptor() ([]byte, []int) { + return file_headscale_v1_policy_proto_rawDescGZIP(), []int{2} +} + +type GetPolicyResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Policy string `protobuf:"bytes,1,opt,name=policy,proto3" json:"policy,omitempty"` + UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` +} + +func (x *GetPolicyResponse) Reset() { + *x = GetPolicyResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_headscale_v1_policy_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetPolicyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetPolicyResponse) ProtoMessage() {} + +func (x *GetPolicyResponse) ProtoReflect() protoreflect.Message { + mi := &file_headscale_v1_policy_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetPolicyResponse.ProtoReflect.Descriptor instead. +func (*GetPolicyResponse) Descriptor() ([]byte, []int) { + return file_headscale_v1_policy_proto_rawDescGZIP(), []int{3} +} + +func (x *GetPolicyResponse) GetPolicy() string { + if x != nil { + return x.Policy + } + return "" +} + +func (x *GetPolicyResponse) GetUpdatedAt() *timestamppb.Timestamp { + if x != nil { + return x.UpdatedAt + } + return nil +} + +var File_headscale_v1_policy_proto protoreflect.FileDescriptor + +var file_headscale_v1_policy_proto_rawDesc = []byte{ + 0x0a, 0x19, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0c, 0x68, 0x65, 0x61, + 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x2a, 0x0a, 0x10, 0x53, 0x65, + 0x74, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, + 0x0a, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x22, 0x66, 0x0a, 0x11, 0x53, 0x65, 0x74, 0x50, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x12, 0x39, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, + 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x09, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0x12, + 0x0a, 0x10, 0x47, 0x65, 0x74, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x22, 0x66, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, + 0x39, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x09, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x75, 0x61, 0x6e, 0x66, 0x6f, 0x6e, + 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x67, 0x65, 0x6e, 0x2f, + 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_headscale_v1_policy_proto_rawDescOnce sync.Once + file_headscale_v1_policy_proto_rawDescData = file_headscale_v1_policy_proto_rawDesc +) + +func file_headscale_v1_policy_proto_rawDescGZIP() []byte { + file_headscale_v1_policy_proto_rawDescOnce.Do(func() { + file_headscale_v1_policy_proto_rawDescData = protoimpl.X.CompressGZIP(file_headscale_v1_policy_proto_rawDescData) + }) + return file_headscale_v1_policy_proto_rawDescData +} + +var file_headscale_v1_policy_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_headscale_v1_policy_proto_goTypes = []interface{}{ + (*SetPolicyRequest)(nil), // 0: headscale.v1.SetPolicyRequest + (*SetPolicyResponse)(nil), // 1: headscale.v1.SetPolicyResponse + (*GetPolicyRequest)(nil), // 2: headscale.v1.GetPolicyRequest + (*GetPolicyResponse)(nil), // 3: headscale.v1.GetPolicyResponse + (*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp +} +var file_headscale_v1_policy_proto_depIdxs = []int32{ + 4, // 0: headscale.v1.SetPolicyResponse.updated_at:type_name -> google.protobuf.Timestamp + 4, // 1: headscale.v1.GetPolicyResponse.updated_at:type_name -> google.protobuf.Timestamp + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_headscale_v1_policy_proto_init() } +func file_headscale_v1_policy_proto_init() { + if File_headscale_v1_policy_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_headscale_v1_policy_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SetPolicyRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_headscale_v1_policy_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SetPolicyResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_headscale_v1_policy_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetPolicyRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_headscale_v1_policy_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetPolicyResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_headscale_v1_policy_proto_rawDesc, + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_headscale_v1_policy_proto_goTypes, + DependencyIndexes: file_headscale_v1_policy_proto_depIdxs, + MessageInfos: file_headscale_v1_policy_proto_msgTypes, + }.Build() + File_headscale_v1_policy_proto = out.File + file_headscale_v1_policy_proto_rawDesc = nil + file_headscale_v1_policy_proto_goTypes = nil + file_headscale_v1_policy_proto_depIdxs = nil +} diff --git a/gen/go/headscale/v1/preauthkey.pb.go b/gen/go/headscale/v1/preauthkey.pb.go index c3ae2818..35a0dfe0 100644 --- a/gen/go/headscale/v1/preauthkey.pb.go +++ b/gen/go/headscale/v1/preauthkey.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.33.0 +// protoc-gen-go v1.32.0 // protoc (unknown) // source: headscale/v1/preauthkey.proto diff --git a/gen/go/headscale/v1/routes.pb.go b/gen/go/headscale/v1/routes.pb.go index 9c7475b4..d2273047 100644 --- a/gen/go/headscale/v1/routes.pb.go +++ b/gen/go/headscale/v1/routes.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.33.0 +// protoc-gen-go v1.32.0 // protoc (unknown) // source: headscale/v1/routes.proto diff --git a/gen/go/headscale/v1/user.pb.go b/gen/go/headscale/v1/user.pb.go index 3fcd12bf..17cb4b54 100644 --- a/gen/go/headscale/v1/user.pb.go +++ b/gen/go/headscale/v1/user.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.33.0 +// protoc-gen-go v1.32.0 // protoc (unknown) // source: headscale/v1/user.proto diff --git a/gen/openapiv2/headscale/v1/apikey.swagger.json b/gen/openapiv2/headscale/v1/apikey.swagger.json index 8c8596a9..0d4ebbe9 100644 --- a/gen/openapiv2/headscale/v1/apikey.swagger.json +++ b/gen/openapiv2/headscale/v1/apikey.swagger.json @@ -34,7 +34,6 @@ "details": { "type": "array", "items": { - "type": "object", "$ref": "#/definitions/protobufAny" } } diff --git a/gen/openapiv2/headscale/v1/device.swagger.json b/gen/openapiv2/headscale/v1/device.swagger.json index 99d20deb..5360527a 100644 --- a/gen/openapiv2/headscale/v1/device.swagger.json +++ b/gen/openapiv2/headscale/v1/device.swagger.json @@ -34,7 +34,6 @@ "details": { "type": "array", "items": { - "type": "object", "$ref": "#/definitions/protobufAny" } } diff --git a/gen/openapiv2/headscale/v1/headscale.swagger.json b/gen/openapiv2/headscale/v1/headscale.swagger.json index 51b4ad22..9c1cf0e9 100644 --- a/gen/openapiv2/headscale/v1/headscale.swagger.json +++ b/gen/openapiv2/headscale/v1/headscale.swagger.json @@ -449,7 +449,15 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/HeadscaleServiceSetTagsBody" + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + } } } ], @@ -495,6 +503,59 @@ ] } }, + "/api/v1/policy": { + "get": { + "summary": "--- Policy start ---", + "operationId": "HeadscaleService_GetPolicy", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1GetPolicyResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "tags": [ + "HeadscaleService" + ] + }, + "put": { + "operationId": "HeadscaleService_SetPolicy", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1SetPolicyResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1SetPolicyRequest" + } + } + ], + "tags": [ + "HeadscaleService" + ] + } + }, "/api/v1/preauthkey": { "get": { "operationId": "HeadscaleService_ListPreAuthKeys", @@ -853,17 +914,6 @@ } }, "definitions": { - "HeadscaleServiceSetTagsBody": { - "type": "object", - "properties": { - "tags": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, "protobufAny": { "type": "object", "properties": { @@ -886,7 +936,6 @@ "details": { "type": "array", "items": { - "type": "object", "$ref": "#/definitions/protobufAny" } } @@ -1085,19 +1134,29 @@ "routes": { "type": "array", "items": { - "type": "object", "$ref": "#/definitions/v1Route" } } } }, + "v1GetPolicyResponse": { + "type": "object", + "properties": { + "policy": { + "type": "string" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + } + }, "v1GetRoutesResponse": { "type": "object", "properties": { "routes": { "type": "array", "items": { - "type": "object", "$ref": "#/definitions/v1Route" } } @@ -1117,7 +1176,6 @@ "apiKeys": { "type": "array", "items": { - "type": "object", "$ref": "#/definitions/v1ApiKey" } } @@ -1129,7 +1187,6 @@ "nodes": { "type": "array", "items": { - "type": "object", "$ref": "#/definitions/v1Node" } } @@ -1141,7 +1198,6 @@ "preAuthKeys": { "type": "array", "items": { - "type": "object", "$ref": "#/definitions/v1PreAuthKey" } } @@ -1153,7 +1209,6 @@ "users": { "type": "array", "items": { - "type": "object", "$ref": "#/definitions/v1User" } } @@ -1346,6 +1401,26 @@ } } }, + "v1SetPolicyRequest": { + "type": "object", + "properties": { + "policy": { + "type": "string" + } + } + }, + "v1SetPolicyResponse": { + "type": "object", + "properties": { + "policy": { + "type": "string" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + } + }, "v1SetTagsResponse": { "type": "object", "properties": { diff --git a/gen/openapiv2/headscale/v1/node.swagger.json b/gen/openapiv2/headscale/v1/node.swagger.json index 16321347..8271250e 100644 --- a/gen/openapiv2/headscale/v1/node.swagger.json +++ b/gen/openapiv2/headscale/v1/node.swagger.json @@ -34,7 +34,6 @@ "details": { "type": "array", "items": { - "type": "object", "$ref": "#/definitions/protobufAny" } } diff --git a/gen/openapiv2/headscale/v1/policy.swagger.json b/gen/openapiv2/headscale/v1/policy.swagger.json new file mode 100644 index 00000000..63afc575 --- /dev/null +++ b/gen/openapiv2/headscale/v1/policy.swagger.json @@ -0,0 +1,43 @@ +{ + "swagger": "2.0", + "info": { + "title": "headscale/v1/policy.proto", + "version": "version not set" + }, + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": {}, + "definitions": { + "protobufAny": { + "type": "object", + "properties": { + "@type": { + "type": "string" + } + }, + "additionalProperties": {} + }, + "rpcStatus": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "$ref": "#/definitions/protobufAny" + } + } + } + } + } +} diff --git a/gen/openapiv2/headscale/v1/preauthkey.swagger.json b/gen/openapiv2/headscale/v1/preauthkey.swagger.json index 17a2be1a..ef16319c 100644 --- a/gen/openapiv2/headscale/v1/preauthkey.swagger.json +++ b/gen/openapiv2/headscale/v1/preauthkey.swagger.json @@ -34,7 +34,6 @@ "details": { "type": "array", "items": { - "type": "object", "$ref": "#/definitions/protobufAny" } } diff --git a/gen/openapiv2/headscale/v1/routes.swagger.json b/gen/openapiv2/headscale/v1/routes.swagger.json index 11087f2a..34eda676 100644 --- a/gen/openapiv2/headscale/v1/routes.swagger.json +++ b/gen/openapiv2/headscale/v1/routes.swagger.json @@ -34,7 +34,6 @@ "details": { "type": "array", "items": { - "type": "object", "$ref": "#/definitions/protobufAny" } } diff --git a/gen/openapiv2/headscale/v1/user.swagger.json b/gen/openapiv2/headscale/v1/user.swagger.json index 008ca3e8..1355a9cc 100644 --- a/gen/openapiv2/headscale/v1/user.swagger.json +++ b/gen/openapiv2/headscale/v1/user.swagger.json @@ -34,7 +34,6 @@ "details": { "type": "array", "items": { - "type": "object", "$ref": "#/definitions/protobufAny" } } diff --git a/hscontrol/app.go b/hscontrol/app.go index 253c2671..726b9d0b 100644 --- a/hscontrol/app.go +++ b/hscontrol/app.go @@ -8,7 +8,7 @@ import ( "io" "net" "net/http" - _ "net/http/pprof" //nolint + _ "net/http/pprof" // nolint "os" "os/signal" "path/filepath" @@ -23,16 +23,6 @@ import ( "github.com/gorilla/mux" grpcMiddleware "github.com/grpc-ecosystem/go-grpc-middleware" grpcRuntime "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" - "github.com/juanfont/headscale" - v1 "github.com/juanfont/headscale/gen/go/headscale/v1" - "github.com/juanfont/headscale/hscontrol/db" - "github.com/juanfont/headscale/hscontrol/derp" - derpServer "github.com/juanfont/headscale/hscontrol/derp/server" - "github.com/juanfont/headscale/hscontrol/mapper" - "github.com/juanfont/headscale/hscontrol/notifier" - "github.com/juanfont/headscale/hscontrol/policy" - "github.com/juanfont/headscale/hscontrol/types" - "github.com/juanfont/headscale/hscontrol/util" "github.com/patrickmn/go-cache" zerolog "github.com/philip-bui/grpc-zerolog" "github.com/pkg/profile" @@ -57,6 +47,17 @@ import ( "tailscale.com/types/dnstype" "tailscale.com/types/key" "tailscale.com/util/dnsname" + + "github.com/juanfont/headscale" + v1 "github.com/juanfont/headscale/gen/go/headscale/v1" + "github.com/juanfont/headscale/hscontrol/db" + "github.com/juanfont/headscale/hscontrol/derp" + derpServer "github.com/juanfont/headscale/hscontrol/derp/server" + "github.com/juanfont/headscale/hscontrol/mapper" + "github.com/juanfont/headscale/hscontrol/notifier" + "github.com/juanfont/headscale/hscontrol/policy" + "github.com/juanfont/headscale/hscontrol/types" + "github.com/juanfont/headscale/hscontrol/util" ) var ( @@ -516,6 +517,10 @@ func (h *Headscale) Serve() error { var err error + if err = h.loadACLPolicy(); err != nil { + return fmt.Errorf("failed to load ACL policy: %w", err) + } + if dumpConfig { spew.Dump(h.cfg) } @@ -784,17 +789,12 @@ func (h *Headscale) Serve() error { Msg("Received SIGHUP, reloading ACL and Config") // TODO(kradalby): Reload config on SIGHUP + if err := h.loadACLPolicy(); err != nil { + log.Error().Err(err).Msg("failed to reload ACL policy") + } - if h.cfg.ACL.PolicyPath != "" { - aclPath := util.AbsolutePathFromConfigPath(h.cfg.ACL.PolicyPath) - pol, err := policy.LoadACLPolicyFromPath(aclPath) - if err != nil { - log.Error().Err(err).Msg("Failed to reload ACL policy") - } - - h.ACLPolicy = pol + if h.ACLPolicy != nil { log.Info(). - Str("path", aclPath). Msg("ACL policy successfully reloaded, notifying nodes of change") ctx := types.NotifyCtx(context.Background(), "acl-sighup", "na") @@ -802,7 +802,6 @@ func (h *Headscale) Serve() error { Type: types.StateFullUpdate, }) } - default: trace := log.Trace().Msgf log.Info(). @@ -1012,3 +1011,48 @@ func readOrCreatePrivateKey(path string) (*key.MachinePrivate, error) { return &machineKey, nil } + +func (h *Headscale) loadACLPolicy() error { + var ( + pol *policy.ACLPolicy + err error + ) + + switch h.cfg.Policy.Mode { + case types.PolicyModeFile: + path := h.cfg.Policy.Path + + // It is fine to start headscale without a policy file. + if len(path) == 0 { + return nil + } + + absPath := util.AbsolutePathFromConfigPath(path) + pol, err = policy.LoadACLPolicyFromPath(absPath) + if err != nil { + return fmt.Errorf("failed to load ACL policy from file: %w", err) + } + case types.PolicyModeDB: + p, err := h.db.GetPolicy() + if err != nil { + if errors.Is(err, types.ErrPolicyNotFound) { + return nil + } + + return fmt.Errorf("failed to get policy from database: %w", err) + } + + pol, err = policy.LoadACLPolicyFromBytes([]byte(p.Data)) + if err != nil { + return fmt.Errorf("failed to parse policy: %w", err) + } + default: + log.Fatal(). + Str("mode", string(h.cfg.Policy.Mode)). + Msg("Unknown ACL policy mode") + } + + h.ACLPolicy = pol + + return nil +} diff --git a/hscontrol/db/db.go b/hscontrol/db/db.go index 69994d02..b44d76ab 100644 --- a/hscontrol/db/db.go +++ b/hscontrol/db/db.go @@ -395,6 +395,18 @@ func NewHeadscaleDatabase( return nil }, }, + { + ID: "202406021630", + Migrate: func(tx *gorm.DB) error { + err := tx.AutoMigrate(&types.Policy{}) + if err != nil { + return err + } + + return nil + }, + Rollback: func(db *gorm.DB) error { return nil }, + }, }, ) diff --git a/hscontrol/db/node_test.go b/hscontrol/db/node_test.go index e95ee4ae..f1762a44 100644 --- a/hscontrol/db/node_test.go +++ b/hscontrol/db/node_test.go @@ -8,13 +8,14 @@ import ( "testing" "time" - "github.com/juanfont/headscale/hscontrol/policy" - "github.com/juanfont/headscale/hscontrol/types" - "github.com/juanfont/headscale/hscontrol/util" "github.com/puzpuzpuz/xsync/v3" "gopkg.in/check.v1" "tailscale.com/tailcfg" "tailscale.com/types/key" + + "github.com/juanfont/headscale/hscontrol/policy" + "github.com/juanfont/headscale/hscontrol/types" + "github.com/juanfont/headscale/hscontrol/util" ) func (s *Suite) TestGetNode(c *check.C) { @@ -545,7 +546,7 @@ func (s *Suite) TestAutoApproveRoutes(c *check.C) { } `) - pol, err := policy.LoadACLPolicyFromBytes(acl, "hujson") + pol, err := policy.LoadACLPolicyFromBytes(acl) c.Assert(err, check.IsNil) c.Assert(pol, check.NotNil) diff --git a/hscontrol/db/policy.go b/hscontrol/db/policy.go new file mode 100644 index 00000000..dcbdc812 --- /dev/null +++ b/hscontrol/db/policy.go @@ -0,0 +1,44 @@ +package db + +import ( + "gorm.io/gorm" + "gorm.io/gorm/clause" + + "errors" + + "github.com/juanfont/headscale/hscontrol/types" +) + +// SetPolicy sets the policy in the database. +func (hsdb *HSDatabase) SetPolicy(policy string) (*types.Policy, error) { + // Create a new policy. + p := types.Policy{ + Data: policy, + } + + if err := hsdb.DB.Clauses(clause.Returning{}).Create(&p).Error; err != nil { + return nil, err + } + + return &p, nil +} + +// GetPolicy returns the latest policy in the database. +func (hsdb *HSDatabase) GetPolicy() (*types.Policy, error) { + var p types.Policy + + // Query: + // SELECT * FROM policies ORDER BY id DESC LIMIT 1; + if err := hsdb.DB. + Order("id DESC"). + Limit(1). + First(&p).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, types.ErrPolicyNotFound + } + + return nil, err + } + + return &p, nil +} diff --git a/hscontrol/grpcv1.go b/hscontrol/grpcv1.go index d9cd653d..a351048f 100644 --- a/hscontrol/grpcv1.go +++ b/hscontrol/grpcv1.go @@ -4,6 +4,8 @@ package hscontrol import ( "context" "errors" + "io" + "os" "sort" "strings" "time" @@ -11,12 +13,14 @@ import ( "github.com/rs/zerolog/log" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" "gorm.io/gorm" "tailscale.com/tailcfg" "tailscale.com/types/key" v1 "github.com/juanfont/headscale/gen/go/headscale/v1" "github.com/juanfont/headscale/hscontrol/db" + "github.com/juanfont/headscale/hscontrol/policy" "github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/util" ) @@ -671,6 +675,76 @@ func (api headscaleV1APIServer) DeleteApiKey( return &v1.DeleteApiKeyResponse{}, nil } +func (api headscaleV1APIServer) GetPolicy( + _ context.Context, + _ *v1.GetPolicyRequest, +) (*v1.GetPolicyResponse, error) { + switch api.h.cfg.Policy.Mode { + case types.PolicyModeDB: + p, err := api.h.db.GetPolicy() + if err != nil { + return nil, err + } + + return &v1.GetPolicyResponse{ + Policy: p.Data, + UpdatedAt: timestamppb.New(p.UpdatedAt), + }, nil + case types.PolicyModeFile: + // Read the file and return the contents as-is. + f, err := os.Open(api.h.cfg.Policy.Path) + if err != nil { + return nil, err + } + + defer f.Close() + + b, err := io.ReadAll(f) + if err != nil { + return nil, err + } + + return &v1.GetPolicyResponse{Policy: string(b)}, nil + } + + return nil, nil +} + +func (api headscaleV1APIServer) SetPolicy( + _ context.Context, + request *v1.SetPolicyRequest, +) (*v1.SetPolicyResponse, error) { + if api.h.cfg.Policy.Mode != types.PolicyModeDB { + return nil, types.ErrPolicyUpdateIsDisabled + } + + p := request.GetPolicy() + + valid, err := policy.LoadACLPolicyFromBytes([]byte(p)) + if err != nil { + return nil, err + } + + updated, err := api.h.db.SetPolicy(p) + if err != nil { + return nil, err + } + + api.h.ACLPolicy = valid + + ctx := types.NotifyCtx(context.Background(), "acl-update", "na") + api.h.nodeNotifier.NotifyAll(ctx, types.StateUpdate{ + Type: types.StateFullUpdate, + }) + + response := &v1.SetPolicyResponse{ + Policy: updated.Data, + UpdatedAt: timestamppb.New(updated.UpdatedAt), + } + + return response, nil +} + // The following service calls are for testing and debugging func (api headscaleV1APIServer) DebugCreateNode( ctx context.Context, diff --git a/hscontrol/mapper/mapper.go b/hscontrol/mapper/mapper.go index adc49669..d7a6cfce 100644 --- a/hscontrol/mapper/mapper.go +++ b/hscontrol/mapper/mapper.go @@ -594,9 +594,30 @@ func appendPeerChanges( resp.PeersChanged = tailPeers } resp.DNSConfig = dnsConfig - resp.PacketFilter = policy.ReduceFilterRules(node, packetFilter) resp.UserProfiles = profiles resp.SSHPolicy = sshPolicy + // 81: 2023-11-17: MapResponse.PacketFilters (incremental packet filter updates) + if capVer >= 81 { + // Currently, we do not send incremental package filters, however using the + // new PacketFilters field and "base" allows us to send a full update when we + // have to send an empty list, avoiding the hack in the else block. + resp.PacketFilters = map[string][]tailcfg.FilterRule{ + "base": policy.ReduceFilterRules(node, packetFilter), + } + } else { + // This is a hack to avoid sending an empty list of packet filters. + // Since tailcfg.PacketFilter has omitempty, any empty PacketFilter will + // be omitted, causing the client to consider it unchange, keeping the + // previous packet filter. Worst case, this can cause a node that previously + // has access to a node to _not_ loose access if an empty (allow none) is sent. + reduced := policy.ReduceFilterRules(node, packetFilter) + if len(reduced) > 0 { + resp.PacketFilter = reduced + } else { + resp.PacketFilter = packetFilter + } + } + return nil } diff --git a/hscontrol/policy/acls.go b/hscontrol/policy/acls.go index 9dde401b..64697e33 100644 --- a/hscontrol/policy/acls.go +++ b/hscontrol/policy/acls.go @@ -7,18 +7,17 @@ import ( "io" "net/netip" "os" - "path/filepath" "strconv" "strings" "time" - "github.com/juanfont/headscale/hscontrol/types" - "github.com/juanfont/headscale/hscontrol/util" "github.com/rs/zerolog/log" "github.com/tailscale/hujson" "go4.org/netipx" - "gopkg.in/yaml.v3" "tailscale.com/tailcfg" + + "github.com/juanfont/headscale/hscontrol/types" + "github.com/juanfont/headscale/hscontrol/util" ) var ( @@ -108,35 +107,22 @@ func LoadACLPolicyFromPath(path string) (*ACLPolicy, error) { Bytes("file", policyBytes). Msg("Loading ACLs") - switch filepath.Ext(path) { - case ".yml", ".yaml": - return LoadACLPolicyFromBytes(policyBytes, "yaml") - } - - return LoadACLPolicyFromBytes(policyBytes, "hujson") + return LoadACLPolicyFromBytes(policyBytes) } -func LoadACLPolicyFromBytes(acl []byte, format string) (*ACLPolicy, error) { +func LoadACLPolicyFromBytes(acl []byte) (*ACLPolicy, error) { var policy ACLPolicy - switch format { - case "yaml": - err := yaml.Unmarshal(acl, &policy) - if err != nil { - return nil, err - } - default: - ast, err := hujson.Parse(acl) - if err != nil { - return nil, err - } + ast, err := hujson.Parse(acl) + if err != nil { + return nil, fmt.Errorf("parsing hujson, err: %w", err) + } - ast.Standardize() - acl = ast.Pack() - err = json.Unmarshal(acl, &policy) - if err != nil { - return nil, err - } + ast.Standardize() + acl = ast.Pack() + + if err := json.Unmarshal(acl, &policy); err != nil { + return nil, fmt.Errorf("unmarshalling policy, err: %w", err) } if policy.IsZero() { @@ -846,7 +832,7 @@ func (pol *ACLPolicy) expandIPsFromUser( // shortcurcuit if we have no nodes to get ips from. if len(filteredNodes) == 0 { - return nil, nil //nolint + return nil, nil // nolint } for _, node := range filteredNodes { diff --git a/hscontrol/policy/acls_test.go b/hscontrol/policy/acls_test.go index c1e7ae08..b3cc10f0 100644 --- a/hscontrol/policy/acls_test.go +++ b/hscontrol/policy/acls_test.go @@ -6,14 +6,15 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/juanfont/headscale/hscontrol/types" - "github.com/juanfont/headscale/hscontrol/util" "github.com/rs/zerolog/log" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "go4.org/netipx" "gopkg.in/check.v1" "tailscale.com/tailcfg" + + "github.com/juanfont/headscale/hscontrol/types" + "github.com/juanfont/headscale/hscontrol/util" ) var iap = func(ipStr string) *netip.Addr { @@ -321,44 +322,27 @@ func TestParsing(t *testing.T) { wantErr: false, }, { - name: "port-wildcard-yaml", - format: "yaml", + name: "ipv6", + format: "hujson", acl: ` ---- -hosts: - host-1: 100.100.100.100/32 - subnet-1: 100.100.101.100/24 -acls: - - action: accept - src: - - "*" - dst: - - host-1:* -`, - want: []tailcfg.FilterRule{ - { - SrcIPs: []string{"0.0.0.0/0", "::/0"}, - DstPorts: []tailcfg.NetPortRange{ - {IP: "100.100.100.100/32", Ports: tailcfg.PortRangeAny}, - }, - }, - }, - wantErr: false, - }, +{ + "hosts": { + "host-1": "100.100.100.100/32", + "subnet-1": "100.100.101.100/24", + }, + + "acls": [ { - name: "ipv6-yaml", - format: "yaml", - acl: ` ---- -hosts: - host-1: 100.100.100.100/32 - subnet-1: 100.100.101.100/24 -acls: - - action: accept - src: - - "*" - dst: - - host-1:* + "action": "accept", + "src": [ + "*", + ], + "dst": [ + "host-1:*", + ], + }, + ], +} `, want: []tailcfg.FilterRule{ { @@ -374,7 +358,7 @@ acls: for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - pol, err := LoadACLPolicyFromBytes([]byte(tt.acl), tt.format) + pol, err := LoadACLPolicyFromBytes([]byte(tt.acl)) if tt.wantErr && err == nil { t.Errorf("parsing() error = %v, wantErr %v", err, tt.wantErr) @@ -544,7 +528,7 @@ func (s *Suite) TestRuleInvalidGeneration(c *check.C) { ], } `) - pol, err := LoadACLPolicyFromBytes(acl, "hujson") + pol, err := LoadACLPolicyFromBytes(acl) c.Assert(pol.ACLs, check.HasLen, 6) c.Assert(err, check.IsNil) diff --git a/hscontrol/policy/acls_types.go b/hscontrol/policy/acls_types.go index e9c44909..25f02f16 100644 --- a/hscontrol/policy/acls_types.go +++ b/hscontrol/policy/acls_types.go @@ -6,26 +6,25 @@ import ( "strings" "github.com/tailscale/hujson" - "gopkg.in/yaml.v3" ) // ACLPolicy represents a Tailscale ACL Policy. type ACLPolicy struct { - Groups Groups `json:"groups" yaml:"groups"` - Hosts Hosts `json:"hosts" yaml:"hosts"` - TagOwners TagOwners `json:"tagOwners" yaml:"tagOwners"` - ACLs []ACL `json:"acls" yaml:"acls"` - Tests []ACLTest `json:"tests" yaml:"tests"` - AutoApprovers AutoApprovers `json:"autoApprovers" yaml:"autoApprovers"` - SSHs []SSH `json:"ssh" yaml:"ssh"` + Groups Groups `json:"groups" ` + Hosts Hosts `json:"hosts"` + TagOwners TagOwners `json:"tagOwners"` + ACLs []ACL `json:"acls"` + Tests []ACLTest `json:"tests"` + AutoApprovers AutoApprovers `json:"autoApprovers"` + SSHs []SSH `json:"ssh"` } // ACL is a basic rule for the ACL Policy. type ACL struct { - Action string `json:"action" yaml:"action"` - Protocol string `json:"proto" yaml:"proto"` - Sources []string `json:"src" yaml:"src"` - Destinations []string `json:"dst" yaml:"dst"` + Action string `json:"action"` + Protocol string `json:"proto"` + Sources []string `json:"src"` + Destinations []string `json:"dst"` } // Groups references a series of alias in the ACL rules. @@ -37,27 +36,27 @@ type Hosts map[string]netip.Prefix // TagOwners specify what users (users?) are allow to use certain tags. type TagOwners map[string][]string -// ACLTest is not implemented, but should be use to check if a certain rule is allowed. +// ACLTest is not implemented, but should be used to check if a certain rule is allowed. type ACLTest struct { - Source string `json:"src" yaml:"src"` - Accept []string `json:"accept" yaml:"accept"` - Deny []string `json:"deny,omitempty" yaml:"deny,omitempty"` + Source string `json:"src"` + Accept []string `json:"accept"` + Deny []string `json:"deny,omitempty"` } // AutoApprovers specify which users (users?), groups or tags have their advertised routes // or exit node status automatically enabled. type AutoApprovers struct { - Routes map[string][]string `json:"routes" yaml:"routes"` - ExitNode []string `json:"exitNode" yaml:"exitNode"` + Routes map[string][]string `json:"routes"` + ExitNode []string `json:"exitNode"` } // SSH controls who can ssh into which machines. type SSH struct { - Action string `json:"action" yaml:"action"` - Sources []string `json:"src" yaml:"src"` - Destinations []string `json:"dst" yaml:"dst"` - Users []string `json:"users" yaml:"users"` - CheckPeriod string `json:"checkPeriod,omitempty" yaml:"checkPeriod,omitempty"` + Action string `json:"action"` + Sources []string `json:"src"` + Destinations []string `json:"dst"` + Users []string `json:"users"` + CheckPeriod string `json:"checkPeriod,omitempty"` } // UnmarshalJSON allows to parse the Hosts directly into netip objects. @@ -89,27 +88,6 @@ func (hosts *Hosts) UnmarshalJSON(data []byte) error { return nil } -// UnmarshalYAML allows to parse the Hosts directly into netip objects. -func (hosts *Hosts) UnmarshalYAML(data []byte) error { - newHosts := Hosts{} - hostIPPrefixMap := make(map[string]string) - - err := yaml.Unmarshal(data, &hostIPPrefixMap) - if err != nil { - return err - } - for host, prefixStr := range hostIPPrefixMap { - prefix, err := netip.ParsePrefix(prefixStr) - if err != nil { - return err - } - newHosts[host] = prefix - } - *hosts = newHosts - - return nil -} - // IsZero is perhaps a bit naive here. func (pol ACLPolicy) IsZero() bool { if len(pol.Groups) == 0 && len(pol.Hosts) == 0 && len(pol.ACLs) == 0 { @@ -119,7 +97,7 @@ func (pol ACLPolicy) IsZero() bool { return false } -// Returns the list of autoApproving users, groups or tags for a given IPPrefix. +// GetRouteApprovers returns the list of autoApproving users, groups or tags for a given IPPrefix. func (autoApprovers *AutoApprovers) GetRouteApprovers( prefix netip.Prefix, ) ([]string, error) { @@ -127,7 +105,7 @@ func (autoApprovers *AutoApprovers) GetRouteApprovers( return autoApprovers.ExitNode, nil // 0.0.0.0/0, ::/0 or equivalent } - approverAliases := []string{} + approverAliases := make([]string, 0) for autoApprovedPrefix, autoApproverAliases := range autoApprovers.Routes { autoApprovedPrefix, err := netip.ParsePrefix(autoApprovedPrefix) diff --git a/hscontrol/types/config.go b/hscontrol/types/config.go index 8ac8dcc4..6eae9a32 100644 --- a/hscontrol/types/config.go +++ b/hscontrol/types/config.go @@ -11,7 +11,6 @@ import ( "time" "github.com/coreos/go-oidc/v3/oidc" - "github.com/juanfont/headscale/hscontrol/util" "github.com/prometheus/common/model" "github.com/rs/zerolog" "github.com/rs/zerolog/log" @@ -20,6 +19,8 @@ import ( "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" "tailscale.com/types/dnstype" + + "github.com/juanfont/headscale/hscontrol/util" ) const ( @@ -38,6 +39,13 @@ const ( IPAllocationStrategyRandom IPAllocationStrategy = "random" ) +type PolicyMode string + +const ( + PolicyModeDB = "database" + PolicyModeFile = "file" +) + // Config contains the initial Headscale configuration. type Config struct { ServerURL string @@ -76,7 +84,7 @@ type Config struct { CLI CLIConfig - ACL ACLConfig + Policy PolicyConfig Tuning Tuning } @@ -163,8 +171,9 @@ type CLIConfig struct { Insecure bool } -type ACLConfig struct { - PolicyPath string +type PolicyConfig struct { + Path string + Mode PolicyMode } type LogConfig struct { @@ -197,6 +206,8 @@ func LoadConfig(path string, isFile bool) error { viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) viper.AutomaticEnv() + viper.SetDefault("policy.mode", "file") + viper.SetDefault("tls_letsencrypt_cache_dir", "/var/www/.cache") viper.SetDefault("tls_letsencrypt_challenge_type", HTTP01ChallengeType) @@ -254,6 +265,13 @@ func LoadConfig(path string, isFile bool) error { return fmt.Errorf("fatal error reading config file: %w", err) } + // Register aliases for backward compatibility + // Has to be called _after_ viper.ReadInConfig() + // https://github.com/spf13/viper/issues/560 + + // Alias the old ACL Policy path with the new configuration option. + registerAliasAndDeprecate("policy.path", "acl_policy_path") + // Collect any validation errors and return them all at once var errorText string if (viper.GetString("tls_letsencrypt_hostname") != "") && @@ -390,11 +408,13 @@ func GetLogTailConfig() LogTailConfig { } } -func GetACLConfig() ACLConfig { - policyPath := viper.GetString("acl_policy_path") +func GetPolicyConfig() PolicyConfig { + policyPath := viper.GetString("policy.path") + policyMode := viper.GetString("policy.mode") - return ACLConfig{ - PolicyPath: policyPath, + return PolicyConfig{ + Path: policyPath, + Mode: PolicyMode(policyMode), } } @@ -764,7 +784,7 @@ func GetHeadscaleConfig() (*Config, error) { LogTail: logTailConfig, RandomizeClientPort: randomizeClientPort, - ACL: GetACLConfig(), + Policy: GetPolicyConfig(), CLI: CLIConfig{ Address: viper.GetString("cli.address"), @@ -787,3 +807,20 @@ func GetHeadscaleConfig() (*Config, error) { func IsCLIConfigured() bool { return viper.GetString("cli.address") != "" && viper.GetString("cli.api_key") != "" } + +// registerAliasAndDeprecate will register an alias between the newKey and the oldKey, +// and log a deprecation warning if the oldKey is set. +func registerAliasAndDeprecate(newKey, oldKey string) { + // NOTE: RegisterAlias is called with NEW KEY -> OLD KEY + viper.RegisterAlias(newKey, oldKey) + if viper.IsSet(oldKey) { + log.Warn().Msgf("The %q configuration key is deprecated. Please use %q instead. %q will be removed in the future.", oldKey, newKey, oldKey) + } +} + +// deprecateAndFatal will log a fatal deprecation warning if the oldKey is set. +func deprecateAndFatal(newKey, oldKey string) { + if viper.IsSet(oldKey) { + log.Fatal().Msgf("The %q configuration key is deprecated. Please use %q instead. %q has been removed.", oldKey, newKey, oldKey) + } +} diff --git a/hscontrol/types/policy.go b/hscontrol/types/policy.go new file mode 100644 index 00000000..a30bf640 --- /dev/null +++ b/hscontrol/types/policy.go @@ -0,0 +1,20 @@ +package types + +import ( + "errors" + + "gorm.io/gorm" +) + +var ( + ErrPolicyNotFound = errors.New("acl policy not found") + ErrPolicyUpdateIsDisabled = errors.New("update is disabled for modes other than 'database'") +) + +// Policy represents a policy in the database. +type Policy struct { + gorm.Model + + // Data contains the policy in HuJSON format. + Data string +} diff --git a/integration/acl_test.go b/integration/acl_test.go index 9d763965..f7b59eb7 100644 --- a/integration/acl_test.go +++ b/integration/acl_test.go @@ -1,11 +1,13 @@ package integration import ( + "encoding/json" "fmt" "net/netip" "strings" "testing" + "github.com/google/go-cmp/cmp" "github.com/juanfont/headscale/hscontrol/policy" "github.com/juanfont/headscale/integration/hsic" "github.com/juanfont/headscale/integration/tsic" @@ -1012,3 +1014,156 @@ func TestACLDevice1CanAccessDevice2(t *testing.T) { }) } } + +func TestPolicyUpdateWhileRunningWithCLIInDatabase(t *testing.T) { + IntegrationSkip(t) + t.Parallel() + + scenario, err := NewScenario(dockertestMaxWait()) + assertNoErr(t, err) + defer scenario.Shutdown() + + spec := map[string]int{ + "user1": 1, + "user2": 1, + } + + err = scenario.CreateHeadscaleEnv(spec, + []tsic.Option{ + // Alpine containers dont have ip6tables set up, which causes + // tailscaled to stop configuring the wgengine, causing it + // to not configure DNS. + tsic.WithNetfilter("off"), + tsic.WithDockerEntrypoint([]string{ + "/bin/sh", + "-c", + "/bin/sleep 3 ; apk add python3 curl ; update-ca-certificates ; python3 -m http.server --bind :: 80 & tailscaled --tun=tsdev", + }), + tsic.WithDockerWorkdir("/"), + }, + hsic.WithTestName("policyreload"), + hsic.WithConfigEnv(map[string]string{ + "HEADSCALE_POLICY_MODE": "database", + }), + ) + assertNoErr(t, err) + + _, err = scenario.ListTailscaleClientsFQDNs() + assertNoErrListFQDN(t, err) + + err = scenario.WaitForTailscaleSync() + assertNoErrSync(t, err) + + user1Clients, err := scenario.ListTailscaleClients("user1") + assertNoErr(t, err) + + user2Clients, err := scenario.ListTailscaleClients("user2") + assertNoErr(t, err) + + all := append(user1Clients, user2Clients...) + + // Initially all nodes can reach each other + for _, client := range all { + for _, peer := range all { + if client.ID() == peer.ID() { + continue + } + + fqdn, err := peer.FQDN() + assertNoErr(t, err) + + url := fmt.Sprintf("http://%s/etc/hostname", fqdn) + t.Logf("url from %s to %s", client.Hostname(), url) + + result, err := client.Curl(url) + assert.Len(t, result, 13) + assertNoErr(t, err) + } + } + + headscale, err := scenario.Headscale() + assertNoErr(t, err) + + p := policy.ACLPolicy{ + ACLs: []policy.ACL{ + { + Action: "accept", + Sources: []string{"user1"}, + Destinations: []string{"user2:*"}, + }, + }, + Hosts: policy.Hosts{}, + } + + pBytes, _ := json.Marshal(p) + + policyFilePath := "/etc/headscale/policy.json" + + err = headscale.WriteFile(policyFilePath, pBytes) + assertNoErr(t, err) + + // No policy is present at this time. + // Add a new policy from a file. + _, err = headscale.Execute( + []string{ + "headscale", + "policy", + "set", + "-f", + policyFilePath, + }, + ) + assertNoErr(t, err) + + // Get the current policy and check + // if it is the same as the one we set. + var output *policy.ACLPolicy + err = executeAndUnmarshal( + headscale, + []string{ + "headscale", + "policy", + "get", + "--output", + "json", + }, + &output, + ) + assertNoErr(t, err) + + assert.Len(t, output.ACLs, 1) + + if diff := cmp.Diff(p, *output); diff != "" { + t.Errorf("unexpected policy(-want +got):\n%s", diff) + } + + // Test that user1 can visit all user2 + for _, client := range user1Clients { + for _, peer := range user2Clients { + fqdn, err := peer.FQDN() + assertNoErr(t, err) + + url := fmt.Sprintf("http://%s/etc/hostname", fqdn) + t.Logf("url from %s to %s", client.Hostname(), url) + + result, err := client.Curl(url) + assert.Len(t, result, 13) + assertNoErr(t, err) + } + } + + // Test that user2 _cannot_ visit user1 + for _, client := range user2Clients { + for _, peer := range user1Clients { + fqdn, err := peer.FQDN() + assertNoErr(t, err) + + url := fmt.Sprintf("http://%s/etc/hostname", fqdn) + t.Logf("url from %s to %s", client.Hostname(), url) + + result, err := client.Curl(url) + assert.Empty(t, result) + assert.Error(t, err) + } + } +} diff --git a/integration/cli_test.go b/integration/cli_test.go index 57edf58e..9bc67a89 100644 --- a/integration/cli_test.go +++ b/integration/cli_test.go @@ -7,11 +7,12 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + 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" ) func executeAndUnmarshal[T any](headscale ControlServer, command []string, result T) error { @@ -1596,3 +1597,83 @@ func TestNodeMoveCommand(t *testing.T) { assert.Equal(t, node.GetUser().GetName(), "old-user") } + +func TestPolicyCommand(t *testing.T) { + IntegrationSkip(t) + t.Parallel() + + scenario, err := NewScenario(dockertestMaxWait()) + assertNoErr(t, err) + defer scenario.Shutdown() + + spec := map[string]int{ + "policy-user": 0, + } + + err = scenario.CreateHeadscaleEnv( + spec, + []tsic.Option{}, + hsic.WithTestName("clins"), + hsic.WithConfigEnv(map[string]string{ + "HEADSCALE_POLICY_MODE": "database", + }), + ) + assertNoErr(t, err) + + headscale, err := scenario.Headscale() + assertNoErr(t, err) + + p := policy.ACLPolicy{ + ACLs: []policy.ACL{ + { + Action: "accept", + Sources: []string{"*"}, + Destinations: []string{"*:*"}, + }, + }, + TagOwners: map[string][]string{ + "tag:exists": {"policy-user"}, + }, + } + + pBytes, _ := json.Marshal(p) + + policyFilePath := "/etc/headscale/policy.json" + + err = headscale.WriteFile(policyFilePath, pBytes) + assertNoErr(t, err) + + // No policy is present at this time. + // Add a new policy from a file. + _, err = headscale.Execute( + []string{ + "headscale", + "policy", + "set", + "-f", + policyFilePath, + }, + ) + + assertNoErr(t, err) + + // Get the current policy and check + // if it is the same as the one we set. + var output *policy.ACLPolicy + err = executeAndUnmarshal( + headscale, + []string{ + "headscale", + "policy", + "get", + "--output", + "json", + }, + &output, + ) + assertNoErr(t, err) + + assert.Len(t, output.TagOwners, 1) + assert.Len(t, output.ACLs, 1) + assert.Equal(t, output.TagOwners["tag:exists"], []string{"policy-user"}) +} diff --git a/integration/control.go b/integration/control.go index f5557495..4260ac4b 100644 --- a/integration/control.go +++ b/integration/control.go @@ -1,8 +1,9 @@ package integration import ( - v1 "github.com/juanfont/headscale/gen/go/headscale/v1" "github.com/ory/dockertest/v3" + + v1 "github.com/juanfont/headscale/gen/go/headscale/v1" ) type ControlServer interface { @@ -10,6 +11,7 @@ type ControlServer interface { SaveLog(string) error SaveProfile(string) error Execute(command []string) (string, error) + WriteFile(path string, content []byte) error ConnectToNetwork(network *dockertest.Network) error GetHealthEndpoint() string GetEndpoint() string diff --git a/proto/headscale/v1/headscale.proto b/proto/headscale/v1/headscale.proto index 1ccc7029..183927ed 100644 --- a/proto/headscale/v1/headscale.proto +++ b/proto/headscale/v1/headscale.proto @@ -9,6 +9,7 @@ import "headscale/v1/preauthkey.proto"; import "headscale/v1/node.proto"; import "headscale/v1/routes.proto"; import "headscale/v1/apikey.proto"; +import "headscale/v1/policy.proto"; // import "headscale/v1/device.proto"; service HeadscaleService { @@ -193,6 +194,22 @@ service HeadscaleService { } // --- ApiKeys end --- + // --- Policy start --- + rpc GetPolicy(GetPolicyRequest) returns (GetPolicyResponse) { + option (google.api.http) = { + get: "/api/v1/policy" + }; + } + + rpc SetPolicy(SetPolicyRequest) returns (SetPolicyResponse) { + option (google.api.http) = { + put: "/api/v1/policy" + body: "*" + }; + } + // --- Policy end --- + + // Implement Tailscale API // rpc GetDevice(GetDeviceRequest) returns(GetDeviceResponse) { // option(google.api.http) = { diff --git a/proto/headscale/v1/policy.proto b/proto/headscale/v1/policy.proto new file mode 100644 index 00000000..3c929385 --- /dev/null +++ b/proto/headscale/v1/policy.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; +package headscale.v1; +option go_package = "github.com/juanfont/headscale/gen/go/v1"; + +import "google/protobuf/timestamp.proto"; + +message SetPolicyRequest { + string policy = 1; +} + +message SetPolicyResponse { + string policy = 1; + google.protobuf.Timestamp updated_at = 2; +} + +message GetPolicyRequest {} + +message GetPolicyResponse { + string policy = 1; + google.protobuf.Timestamp updated_at = 2; +} \ No newline at end of file From 7e62031444228718ecbcd8a7af46e38f35a1ea25 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 18 Jul 2024 10:01:59 +0200 Subject: [PATCH 091/145] replace ephemeral deletion logic (#2008) * replace ephemeral deletion logic this commit replaces the way we remove ephemeral nodes, currently they are deleted in a loop and we look at last seen time. This time is now only set when a node disconnects and there was a bug (#2006) where nodes that had never disconnected was deleted since they did not have a last seen. The new logic will start an expiry timer when the node disconnects and delete the node from the database when the timer is up. If the node reconnects within the expiry, the timer is cancelled. Fixes #2006 Signed-off-by: Kristoffer Dalby * use uint64 as authekyid and ptr helper in tests Signed-off-by: Kristoffer Dalby * add test db helper Signed-off-by: Kristoffer Dalby * add list ephemeral node func Signed-off-by: Kristoffer Dalby * schedule ephemeral nodes for removal on startup Signed-off-by: Kristoffer Dalby * fix gorm query for postgres Signed-off-by: Kristoffer Dalby * add godoc Signed-off-by: Kristoffer Dalby --------- Signed-off-by: Kristoffer Dalby --- .github/workflows/test-integration.yaml | 1 + hscontrol/app.go | 65 ++++------ hscontrol/auth.go | 8 +- hscontrol/db/node.go | 146 ++++++++++++++++------- hscontrol/db/node_test.go | 151 +++++++++++++++++++++--- hscontrol/db/preauth_keys.go | 4 +- hscontrol/db/preauth_keys_test.go | 79 +------------ hscontrol/db/routes_test.go | 15 +-- hscontrol/db/suite_test.go | 14 ++- hscontrol/db/users_test.go | 7 +- hscontrol/poll.go | 15 +++ hscontrol/types/node.go | 2 +- integration/general_test.go | 116 ++++++++++++++++++ 13 files changed, 417 insertions(+), 206 deletions(-) diff --git a/.github/workflows/test-integration.yaml b/.github/workflows/test-integration.yaml index ed1d1221..bf55e2de 100644 --- a/.github/workflows/test-integration.yaml +++ b/.github/workflows/test-integration.yaml @@ -42,6 +42,7 @@ jobs: - TestPingAllByIPPublicDERP - TestAuthKeyLogoutAndRelogin - TestEphemeral + - TestEphemeral2006DeletedTooQuickly - TestPingAllByHostname - TestTaildrop - TestResolveMagicDNS diff --git a/hscontrol/app.go b/hscontrol/app.go index 726b9d0b..0a23f07d 100644 --- a/hscontrol/app.go +++ b/hscontrol/app.go @@ -91,6 +91,7 @@ type Headscale struct { db *db.HSDatabase ipAlloc *db.IPAllocator noisePrivateKey *key.MachinePrivate + ephemeralGC *db.EphemeralGarbageCollector DERPMap *tailcfg.DERPMap DERPServer *derpServer.DERPServer @@ -153,6 +154,12 @@ func NewHeadscale(cfg *types.Config) (*Headscale, error) { return nil, err } + app.ephemeralGC = db.NewEphemeralGarbageCollector(func(ni types.NodeID) { + if err := app.db.DeleteEphemeralNode(ni); err != nil { + log.Err(err).Uint64("node.id", ni.Uint64()).Msgf("failed to delete ephemeral node") + } + }) + if cfg.OIDC.Issuer != "" { err = app.initOIDC() if err != nil { @@ -217,47 +224,6 @@ func (h *Headscale) redirect(w http.ResponseWriter, req *http.Request) { http.Redirect(w, req, target, http.StatusFound) } -// deleteExpireEphemeralNodes deletes ephemeral node records that have not been -// seen for longer than h.cfg.EphemeralNodeInactivityTimeout. -func (h *Headscale) deleteExpireEphemeralNodes(ctx context.Context, every time.Duration) { - ticker := time.NewTicker(every) - - for { - select { - case <-ctx.Done(): - ticker.Stop() - return - case <-ticker.C: - var removed []types.NodeID - var changed []types.NodeID - if err := h.db.Write(func(tx *gorm.DB) error { - removed, changed = db.DeleteExpiredEphemeralNodes(tx, h.cfg.EphemeralNodeInactivityTimeout) - - return nil - }); err != nil { - log.Error().Err(err).Msg("database error while expiring ephemeral nodes") - continue - } - - if removed != nil { - ctx := types.NotifyCtx(context.Background(), "expire-ephemeral", "na") - h.nodeNotifier.NotifyAll(ctx, types.StateUpdate{ - Type: types.StatePeerRemoved, - Removed: removed, - }) - } - - if changed != nil { - ctx := types.NotifyCtx(context.Background(), "expire-ephemeral", "na") - h.nodeNotifier.NotifyAll(ctx, types.StateUpdate{ - Type: types.StatePeerChanged, - ChangeNodes: changed, - }) - } - } - } -} - // expireExpiredNodes expires nodes that have an explicit expiry set // after that expiry time has passed. func (h *Headscale) expireExpiredNodes(ctx context.Context, every time.Duration) { @@ -557,9 +523,18 @@ func (h *Headscale) Serve() error { return errEmptyInitialDERPMap } - expireEphemeralCtx, expireEphemeralCancel := context.WithCancel(context.Background()) - defer expireEphemeralCancel() - go h.deleteExpireEphemeralNodes(expireEphemeralCtx, updateInterval) + // Start ephemeral node garbage collector and schedule all nodes + // that are already in the database and ephemeral. If they are still + // around between restarts, they will reconnect and the GC will + // be cancelled. + go h.ephemeralGC.Start() + ephmNodes, err := h.db.ListEphemeralNodes() + if err != nil { + return fmt.Errorf("failed to list ephemeral nodes: %w", err) + } + for _, node := range ephmNodes { + h.ephemeralGC.Schedule(node.ID, h.cfg.EphemeralNodeInactivityTimeout) + } expireNodeCtx, expireNodeCancel := context.WithCancel(context.Background()) defer expireNodeCancel() @@ -809,7 +784,7 @@ func (h *Headscale) Serve() error { Msg("Received signal to stop, shutting down gracefully") expireNodeCancel() - expireEphemeralCancel() + h.ephemeralGC.Close() trace("waiting for netmap stream to close") h.pollNetMapStreamWG.Wait() diff --git a/hscontrol/auth.go b/hscontrol/auth.go index 5ee925a6..010d15a2 100644 --- a/hscontrol/auth.go +++ b/hscontrol/auth.go @@ -16,6 +16,7 @@ import ( "gorm.io/gorm" "tailscale.com/tailcfg" "tailscale.com/types/key" + "tailscale.com/types/ptr" ) func logAuthFunc( @@ -314,9 +315,8 @@ func (h *Headscale) handleAuthKey( Msg("node was already registered before, refreshing with new auth key") node.NodeKey = nodeKey - pakID := uint(pak.ID) - if pakID != 0 { - node.AuthKeyID = &pakID + if pak.ID != 0 { + node.AuthKeyID = ptr.To(pak.ID) } node.Expiry = ®isterRequest.Expiry @@ -394,7 +394,7 @@ func (h *Headscale) handleAuthKey( pakID := uint(pak.ID) if pakID != 0 { - nodeToRegister.AuthKeyID = &pakID + nodeToRegister.AuthKeyID = ptr.To(pak.ID) } node, err = h.db.RegisterNode( nodeToRegister, diff --git a/hscontrol/db/node.go b/hscontrol/db/node.go index e36d6ed1..a2515ebf 100644 --- a/hscontrol/db/node.go +++ b/hscontrol/db/node.go @@ -12,6 +12,7 @@ import ( "github.com/patrickmn/go-cache" "github.com/puzpuzpuz/xsync/v3" "github.com/rs/zerolog/log" + "github.com/sasha-s/go-deadlock" "gorm.io/gorm" "tailscale.com/tailcfg" "tailscale.com/types/key" @@ -78,6 +79,17 @@ func ListNodes(tx *gorm.DB) (types.Nodes, error) { return nodes, nil } +func (hsdb *HSDatabase) ListEphemeralNodes() (types.Nodes, error) { + return Read(hsdb.DB, func(rx *gorm.DB) (types.Nodes, error) { + nodes := types.Nodes{} + if err := rx.Joins("AuthKey").Where(`"AuthKey"."ephemeral" = true`).Find(&nodes).Error; err != nil { + return nil, err + } + + return nodes, nil + }) +} + func listNodesByGivenName(tx *gorm.DB, givenName string) (types.Nodes, error) { nodes := types.Nodes{} if err := tx. @@ -286,6 +298,20 @@ func DeleteNode(tx *gorm.DB, return changed, nil } +// DeleteEphemeralNode deletes a Node from the database, note that this method +// will remove it straight, and not notify any changes or consider any routes. +// It is intended for Ephemeral nodes. +func (hsdb *HSDatabase) DeleteEphemeralNode( + nodeID types.NodeID, +) error { + return hsdb.Write(func(tx *gorm.DB) error { + if err := tx.Unscoped().Delete(&types.Node{}, nodeID).Error; err != nil { + return err + } + return nil + }) +} + // SetLastSeen sets a node's last seen field indicating that we // have recently communicating with this node. func SetLastSeen(tx *gorm.DB, nodeID types.NodeID, lastSeen time.Time) error { @@ -660,51 +686,6 @@ func GenerateGivenName( return givenName, nil } -func DeleteExpiredEphemeralNodes(tx *gorm.DB, - inactivityThreshold time.Duration, -) ([]types.NodeID, []types.NodeID) { - users, err := ListUsers(tx) - if err != nil { - return nil, nil - } - - var expired []types.NodeID - var changedNodes []types.NodeID - for _, user := range users { - nodes, err := ListNodesByUser(tx, user.Name) - if err != nil { - return nil, nil - } - - for idx, node := range nodes { - if node.IsEphemeral() && node.LastSeen != nil && - time.Now(). - After(node.LastSeen.Add(inactivityThreshold)) { - expired = append(expired, node.ID) - - log.Info(). - Str("node", node.Hostname). - Msg("Ephemeral client removed from database") - - // empty isConnected map as ephemeral nodes are not routes - changed, err := DeleteNode(tx, nodes[idx], nil) - if err != nil { - log.Error(). - Err(err). - Str("node", node.Hostname). - Msg("🤮 Cannot delete ephemeral node from the database") - } - - changedNodes = append(changedNodes, changed...) - } - } - - // TODO(kradalby): needs to be moved out of transaction - } - - return expired, changedNodes -} - func ExpireExpiredNodes(tx *gorm.DB, lastCheck time.Time, ) (time.Time, types.StateUpdate, bool) { @@ -737,3 +718,78 @@ func ExpireExpiredNodes(tx *gorm.DB, return started, types.StateUpdate{}, false } + +// EphemeralGarbageCollector is a garbage collector that will delete nodes after +// a certain amount of time. +// It is used to delete ephemeral nodes that have disconnected and should be +// cleaned up. +type EphemeralGarbageCollector struct { + mu deadlock.Mutex + + deleteFunc func(types.NodeID) + toBeDeleted map[types.NodeID]*time.Timer + + deleteCh chan types.NodeID + cancelCh chan struct{} +} + +// NewEphemeralGarbageCollector creates a new EphemeralGarbageCollector, it takes +// a deleteFunc that will be called when a node is scheduled for deletion. +func NewEphemeralGarbageCollector(deleteFunc func(types.NodeID)) *EphemeralGarbageCollector { + return &EphemeralGarbageCollector{ + toBeDeleted: make(map[types.NodeID]*time.Timer), + deleteCh: make(chan types.NodeID, 10), + cancelCh: make(chan struct{}), + deleteFunc: deleteFunc, + } +} + +// Close stops the garbage collector. +func (e *EphemeralGarbageCollector) Close() { + e.cancelCh <- struct{}{} +} + +// Schedule schedules a node for deletion after the expiry duration. +func (e *EphemeralGarbageCollector) Schedule(nodeID types.NodeID, expiry time.Duration) { + e.mu.Lock() + defer e.mu.Unlock() + + timer := time.NewTimer(expiry) + e.toBeDeleted[nodeID] = timer + + go func() { + select { + case _, ok := <-timer.C: + if ok { + e.deleteCh <- nodeID + } + } + }() +} + +// Cancel cancels the deletion of a node. +func (e *EphemeralGarbageCollector) Cancel(nodeID types.NodeID) { + e.mu.Lock() + defer e.mu.Unlock() + + if timer, ok := e.toBeDeleted[nodeID]; ok { + timer.Stop() + delete(e.toBeDeleted, nodeID) + } +} + +// Start starts the garbage collector. +func (e *EphemeralGarbageCollector) Start() { + for { + select { + case <-e.cancelCh: + return + case nodeID := <-e.deleteCh: + e.mu.Lock() + delete(e.toBeDeleted, nodeID) + e.mu.Unlock() + + go e.deleteFunc(nodeID) + } + } +} diff --git a/hscontrol/db/node_test.go b/hscontrol/db/node_test.go index f1762a44..d88d0458 100644 --- a/hscontrol/db/node_test.go +++ b/hscontrol/db/node_test.go @@ -1,17 +1,23 @@ package db import ( + "crypto/rand" "fmt" + "math/big" "net/netip" "regexp" "strconv" + "sync" "testing" "time" + "github.com/google/go-cmp/cmp" "github.com/puzpuzpuz/xsync/v3" + "github.com/stretchr/testify/assert" "gopkg.in/check.v1" "tailscale.com/tailcfg" "tailscale.com/types/key" + "tailscale.com/types/ptr" "github.com/juanfont/headscale/hscontrol/policy" "github.com/juanfont/headscale/hscontrol/types" @@ -30,7 +36,6 @@ func (s *Suite) TestGetNode(c *check.C) { nodeKey := key.NewNode() machineKey := key.NewMachine() - pakID := uint(pak.ID) node := &types.Node{ ID: 0, @@ -39,7 +44,7 @@ func (s *Suite) TestGetNode(c *check.C) { Hostname: "testnode", UserID: user.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: &pakID, + AuthKeyID: ptr.To(pak.ID), } trx := db.DB.Save(node) c.Assert(trx.Error, check.IsNil) @@ -61,7 +66,6 @@ func (s *Suite) TestGetNodeByID(c *check.C) { nodeKey := key.NewNode() machineKey := key.NewMachine() - pakID := uint(pak.ID) node := types.Node{ ID: 0, MachineKey: machineKey.Public(), @@ -69,7 +73,7 @@ func (s *Suite) TestGetNodeByID(c *check.C) { Hostname: "testnode", UserID: user.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: &pakID, + AuthKeyID: ptr.To(pak.ID), } trx := db.DB.Save(&node) c.Assert(trx.Error, check.IsNil) @@ -93,7 +97,6 @@ func (s *Suite) TestGetNodeByAnyNodeKey(c *check.C) { machineKey := key.NewMachine() - pakID := uint(pak.ID) node := types.Node{ ID: 0, MachineKey: machineKey.Public(), @@ -101,7 +104,7 @@ func (s *Suite) TestGetNodeByAnyNodeKey(c *check.C) { Hostname: "testnode", UserID: user.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: &pakID, + AuthKeyID: ptr.To(pak.ID), } trx := db.DB.Save(&node) c.Assert(trx.Error, check.IsNil) @@ -145,7 +148,6 @@ func (s *Suite) TestListPeers(c *check.C) { _, err = db.GetNodeByID(0) c.Assert(err, check.NotNil) - pakID := uint(pak.ID) for index := 0; index <= 10; index++ { nodeKey := key.NewNode() machineKey := key.NewMachine() @@ -157,7 +159,7 @@ func (s *Suite) TestListPeers(c *check.C) { Hostname: "testnode" + strconv.Itoa(index), UserID: user.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: &pakID, + AuthKeyID: ptr.To(pak.ID), } trx := db.DB.Save(&node) c.Assert(trx.Error, check.IsNil) @@ -197,7 +199,6 @@ func (s *Suite) TestGetACLFilteredPeers(c *check.C) { for index := 0; index <= 10; index++ { nodeKey := key.NewNode() machineKey := key.NewMachine() - pakID := uint(stor[index%2].key.ID) v4 := netip.MustParseAddr(fmt.Sprintf("100.64.0.%v", strconv.Itoa(index+1))) node := types.Node{ @@ -208,7 +209,7 @@ func (s *Suite) TestGetACLFilteredPeers(c *check.C) { Hostname: "testnode" + strconv.Itoa(index), UserID: stor[index%2].user.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: &pakID, + AuthKeyID: ptr.To(stor[index%2].key.ID), } trx := db.DB.Save(&node) c.Assert(trx.Error, check.IsNil) @@ -283,7 +284,6 @@ func (s *Suite) TestExpireNode(c *check.C) { nodeKey := key.NewNode() machineKey := key.NewMachine() - pakID := uint(pak.ID) node := &types.Node{ ID: 0, @@ -292,7 +292,7 @@ func (s *Suite) TestExpireNode(c *check.C) { Hostname: "testnode", UserID: user.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: &pakID, + AuthKeyID: ptr.To(pak.ID), Expiry: &time.Time{}, } db.DB.Save(node) @@ -328,7 +328,6 @@ func (s *Suite) TestGenerateGivenName(c *check.C) { machineKey2 := key.NewMachine() - pakID := uint(pak.ID) node := &types.Node{ ID: 0, MachineKey: machineKey.Public(), @@ -337,7 +336,7 @@ func (s *Suite) TestGenerateGivenName(c *check.C) { GivenName: "hostname-1", UserID: user1.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: &pakID, + AuthKeyID: ptr.To(pak.ID), } trx := db.DB.Save(node) @@ -372,7 +371,6 @@ func (s *Suite) TestSetTags(c *check.C) { nodeKey := key.NewNode() machineKey := key.NewMachine() - pakID := uint(pak.ID) node := &types.Node{ ID: 0, MachineKey: machineKey.Public(), @@ -380,7 +378,7 @@ func (s *Suite) TestSetTags(c *check.C) { Hostname: "testnode", UserID: user.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: &pakID, + AuthKeyID: ptr.To(pak.ID), } trx := db.DB.Save(node) @@ -566,7 +564,6 @@ func (s *Suite) TestAutoApproveRoutes(c *check.C) { route2 := netip.MustParsePrefix("10.11.0.0/24") v4 := netip.MustParseAddr("100.64.0.1") - pakID := uint(pak.ID) node := types.Node{ ID: 0, MachineKey: machineKey.Public(), @@ -574,7 +571,7 @@ func (s *Suite) TestAutoApproveRoutes(c *check.C) { Hostname: "test", UserID: user.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: &pakID, + AuthKeyID: ptr.To(pak.ID), Hostinfo: &tailcfg.Hostinfo{ RequestTags: []string{"tag:exit"}, RoutableIPs: []netip.Prefix{defaultRouteV4, defaultRouteV6, route1, route2}, @@ -600,3 +597,121 @@ func (s *Suite) TestAutoApproveRoutes(c *check.C) { c.Assert(err, check.IsNil) c.Assert(enabledRoutes, check.HasLen, 4) } + +func TestEphemeralGarbageCollectorOrder(t *testing.T) { + want := []types.NodeID{1, 3} + got := []types.NodeID{} + + e := NewEphemeralGarbageCollector(func(ni types.NodeID) { + got = append(got, ni) + }) + go e.Start() + + e.Schedule(1, 1*time.Second) + e.Schedule(2, 2*time.Second) + e.Schedule(3, 3*time.Second) + e.Schedule(4, 4*time.Second) + e.Cancel(2) + e.Cancel(4) + + time.Sleep(6 * time.Second) + + e.Close() + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("wrong nodes deleted, unexpected result (-want +got):\n%s", diff) + } +} + +func TestEphemeralGarbageCollectorLoads(t *testing.T) { + var got []types.NodeID + var mu sync.Mutex + + want := 1000 + + e := NewEphemeralGarbageCollector(func(ni types.NodeID) { + defer mu.Unlock() + mu.Lock() + + time.Sleep(time.Duration(generateRandomNumber(t, 3)) * time.Millisecond) + got = append(got, ni) + }) + go e.Start() + + for i := 0; i < want; i++ { + go e.Schedule(types.NodeID(i), 1*time.Second) + } + + time.Sleep(10 * time.Second) + + e.Close() + if len(got) != want { + t.Errorf("expected %d, got %d", want, len(got)) + } +} + +func generateRandomNumber(t *testing.T, max int64) int64 { + t.Helper() + maxB := big.NewInt(max) + n, err := rand.Int(rand.Reader, maxB) + if err != nil { + t.Fatalf("getting random number: %s", err) + } + return n.Int64() + 1 +} + +func TestListEphemeralNodes(t *testing.T) { + db, err := newTestDB() + if err != nil { + t.Fatalf("creating db: %s", err) + } + + user, err := db.CreateUser("test") + assert.NoError(t, err) + + pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil) + assert.NoError(t, err) + + pakEph, err := db.CreatePreAuthKey(user.Name, false, true, nil, nil) + assert.NoError(t, err) + + node := types.Node{ + ID: 0, + MachineKey: key.NewMachine().Public(), + NodeKey: key.NewNode().Public(), + Hostname: "test", + UserID: user.ID, + RegisterMethod: util.RegisterMethodAuthKey, + AuthKeyID: ptr.To(pak.ID), + } + + nodeEph := types.Node{ + ID: 0, + MachineKey: key.NewMachine().Public(), + NodeKey: key.NewNode().Public(), + Hostname: "ephemeral", + UserID: user.ID, + RegisterMethod: util.RegisterMethodAuthKey, + AuthKeyID: ptr.To(pakEph.ID), + } + + err = db.DB.Save(&node).Error + assert.NoError(t, err) + + err = db.DB.Save(&nodeEph).Error + assert.NoError(t, err) + + nodes, err := db.ListNodes() + assert.NoError(t, err) + + ephemeralNodes, err := db.ListEphemeralNodes() + assert.NoError(t, err) + + assert.Len(t, nodes, 2) + assert.Len(t, ephemeralNodes, 1) + + assert.Equal(t, nodeEph.ID, ephemeralNodes[0].ID) + assert.Equal(t, nodeEph.AuthKeyID, ephemeralNodes[0].AuthKeyID) + assert.Equal(t, nodeEph.UserID, ephemeralNodes[0].UserID) + assert.Equal(t, nodeEph.Hostname, ephemeralNodes[0].Hostname) +} diff --git a/hscontrol/db/preauth_keys.go b/hscontrol/db/preauth_keys.go index adfd289a..5ea59a9c 100644 --- a/hscontrol/db/preauth_keys.go +++ b/hscontrol/db/preauth_keys.go @@ -10,6 +10,7 @@ import ( "github.com/juanfont/headscale/hscontrol/types" "gorm.io/gorm" + "tailscale.com/types/ptr" ) var ( @@ -197,10 +198,9 @@ func ValidatePreAuthKey(tx *gorm.DB, k string) (*types.PreAuthKey, error) { } nodes := types.Nodes{} - pakID := uint(pak.ID) if err := tx. Preload("AuthKey"). - Where(&types.Node{AuthKeyID: &pakID}). + Where(&types.Node{AuthKeyID: ptr.To(pak.ID)}). Find(&nodes).Error; err != nil { return nil, err } diff --git a/hscontrol/db/preauth_keys_test.go b/hscontrol/db/preauth_keys_test.go index 9cdcba80..9dd5b199 100644 --- a/hscontrol/db/preauth_keys_test.go +++ b/hscontrol/db/preauth_keys_test.go @@ -6,7 +6,7 @@ import ( "github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/util" "gopkg.in/check.v1" - "gorm.io/gorm" + "tailscale.com/types/ptr" ) func (*Suite) TestCreatePreAuthKey(c *check.C) { @@ -76,13 +76,12 @@ func (*Suite) TestAlreadyUsedKey(c *check.C) { pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil) c.Assert(err, check.IsNil) - pakID := uint(pak.ID) node := types.Node{ ID: 0, Hostname: "testest", UserID: user.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: &pakID, + AuthKeyID: ptr.To(pak.ID), } trx := db.DB.Save(&node) c.Assert(trx.Error, check.IsNil) @@ -99,13 +98,12 @@ func (*Suite) TestReusableBeingUsedKey(c *check.C) { pak, err := db.CreatePreAuthKey(user.Name, true, false, nil, nil) c.Assert(err, check.IsNil) - pakID := uint(pak.ID) node := types.Node{ ID: 1, Hostname: "testest", UserID: user.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: &pakID, + AuthKeyID: ptr.To(pak.ID), } trx := db.DB.Save(&node) c.Assert(trx.Error, check.IsNil) @@ -127,77 +125,6 @@ func (*Suite) TestNotReusableNotBeingUsedKey(c *check.C) { c.Assert(key.ID, check.Equals, pak.ID) } -func (*Suite) TestEphemeralKeyReusable(c *check.C) { - user, err := db.CreateUser("test7") - c.Assert(err, check.IsNil) - - pak, err := db.CreatePreAuthKey(user.Name, true, true, nil, nil) - c.Assert(err, check.IsNil) - - now := time.Now().Add(-time.Second * 30) - pakID := uint(pak.ID) - node := types.Node{ - ID: 0, - Hostname: "testest", - UserID: user.ID, - RegisterMethod: util.RegisterMethodAuthKey, - LastSeen: &now, - AuthKeyID: &pakID, - } - trx := db.DB.Save(&node) - c.Assert(trx.Error, check.IsNil) - - _, err = db.ValidatePreAuthKey(pak.Key) - c.Assert(err, check.IsNil) - - _, err = db.getNode("test7", "testest") - c.Assert(err, check.IsNil) - - db.Write(func(tx *gorm.DB) error { - DeleteExpiredEphemeralNodes(tx, time.Second*20) - return nil - }) - - // The machine record should have been deleted - _, err = db.getNode("test7", "testest") - c.Assert(err, check.NotNil) -} - -func (*Suite) TestEphemeralKeyNotReusable(c *check.C) { - user, err := db.CreateUser("test7") - c.Assert(err, check.IsNil) - - pak, err := db.CreatePreAuthKey(user.Name, false, true, nil, nil) - c.Assert(err, check.IsNil) - - now := time.Now().Add(-time.Second * 30) - pakId := uint(pak.ID) - node := types.Node{ - ID: 0, - Hostname: "testest", - UserID: user.ID, - RegisterMethod: util.RegisterMethodAuthKey, - LastSeen: &now, - AuthKeyID: &pakId, - } - db.DB.Save(&node) - - _, err = db.ValidatePreAuthKey(pak.Key) - c.Assert(err, check.NotNil) - - _, err = db.getNode("test7", "testest") - c.Assert(err, check.IsNil) - - db.Write(func(tx *gorm.DB) error { - DeleteExpiredEphemeralNodes(tx, time.Second*20) - return nil - }) - - // The machine record should have been deleted - _, err = db.getNode("test7", "testest") - c.Assert(err, check.NotNil) -} - func (*Suite) TestExpirePreauthKey(c *check.C) { user, err := db.CreateUser("test3") c.Assert(err, check.IsNil) diff --git a/hscontrol/db/routes_test.go b/hscontrol/db/routes_test.go index 8bbc5948..122a7ff3 100644 --- a/hscontrol/db/routes_test.go +++ b/hscontrol/db/routes_test.go @@ -14,6 +14,7 @@ import ( "gopkg.in/check.v1" "gorm.io/gorm" "tailscale.com/tailcfg" + "tailscale.com/types/ptr" ) var smap = func(m map[types.NodeID]bool) *xsync.MapOf[types.NodeID, bool] { @@ -43,13 +44,12 @@ func (s *Suite) TestGetRoutes(c *check.C) { RoutableIPs: []netip.Prefix{route}, } - pakID := uint(pak.ID) node := types.Node{ ID: 0, Hostname: "test_get_route_node", UserID: user.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: &pakID, + AuthKeyID: ptr.To(pak.ID), Hostinfo: &hostInfo, } trx := db.DB.Save(&node) @@ -95,13 +95,12 @@ func (s *Suite) TestGetEnableRoutes(c *check.C) { RoutableIPs: []netip.Prefix{route, route2}, } - pakID := uint(pak.ID) node := types.Node{ ID: 0, Hostname: "test_enable_route_node", UserID: user.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: &pakID, + AuthKeyID: ptr.To(pak.ID), Hostinfo: &hostInfo, } trx := db.DB.Save(&node) @@ -169,13 +168,12 @@ func (s *Suite) TestIsUniquePrefix(c *check.C) { hostInfo1 := tailcfg.Hostinfo{ RoutableIPs: []netip.Prefix{route, route2}, } - pakID := uint(pak.ID) node1 := types.Node{ ID: 1, Hostname: "test_enable_route_node", UserID: user.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: &pakID, + AuthKeyID: ptr.To(pak.ID), Hostinfo: &hostInfo1, } trx := db.DB.Save(&node1) @@ -199,7 +197,7 @@ func (s *Suite) TestIsUniquePrefix(c *check.C) { Hostname: "test_enable_route_node", UserID: user.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: &pakID, + AuthKeyID: ptr.To(pak.ID), Hostinfo: &hostInfo2, } db.DB.Save(&node2) @@ -253,13 +251,12 @@ func (s *Suite) TestDeleteRoutes(c *check.C) { } now := time.Now() - pakID := uint(pak.ID) node1 := types.Node{ ID: 1, Hostname: "test_enable_route_node", UserID: user.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: &pakID, + AuthKeyID: ptr.To(pak.ID), Hostinfo: &hostInfo1, LastSeen: &now, } diff --git a/hscontrol/db/suite_test.go b/hscontrol/db/suite_test.go index 1b97ce06..d546b33d 100644 --- a/hscontrol/db/suite_test.go +++ b/hscontrol/db/suite_test.go @@ -36,10 +36,18 @@ func (s *Suite) ResetDB(c *check.C) { // } var err error - tmpDir, err = os.MkdirTemp("", "headscale-db-test-*") + db, err = newTestDB() if err != nil { c.Fatal(err) } +} + +func newTestDB() (*HSDatabase, error) { + var err error + tmpDir, err = os.MkdirTemp("", "headscale-db-test-*") + if err != nil { + return nil, err + } log.Printf("database path: %s", tmpDir+"/headscale_test.db") @@ -53,6 +61,8 @@ func (s *Suite) ResetDB(c *check.C) { "", ) if err != nil { - c.Fatal(err) + return nil, err } + + return db, nil } diff --git a/hscontrol/db/users_test.go b/hscontrol/db/users_test.go index 98dea6c0..0629480c 100644 --- a/hscontrol/db/users_test.go +++ b/hscontrol/db/users_test.go @@ -5,6 +5,7 @@ import ( "github.com/juanfont/headscale/hscontrol/util" "gopkg.in/check.v1" "gorm.io/gorm" + "tailscale.com/types/ptr" ) func (s *Suite) TestCreateAndDestroyUser(c *check.C) { @@ -46,13 +47,12 @@ func (s *Suite) TestDestroyUserErrors(c *check.C) { pak, err = db.CreatePreAuthKey(user.Name, false, false, nil, nil) c.Assert(err, check.IsNil) - pakID := uint(pak.ID) node := types.Node{ ID: 0, Hostname: "testnode", UserID: user.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: &pakID, + AuthKeyID: ptr.To(pak.ID), } trx := db.DB.Save(&node) c.Assert(trx.Error, check.IsNil) @@ -100,13 +100,12 @@ func (s *Suite) TestSetMachineUser(c *check.C) { pak, err := db.CreatePreAuthKey(oldUser.Name, false, false, nil, nil) c.Assert(err, check.IsNil) - pakID := uint(pak.ID) node := types.Node{ ID: 0, Hostname: "testnode", UserID: oldUser.ID, RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: &pakID, + AuthKeyID: ptr.To(pak.ID), } trx := db.DB.Save(&node) c.Assert(trx.Error, check.IsNil) diff --git a/hscontrol/poll.go b/hscontrol/poll.go index d3c82117..8122064b 100644 --- a/hscontrol/poll.go +++ b/hscontrol/poll.go @@ -135,6 +135,18 @@ func (m *mapSession) resetKeepAlive() { m.keepAliveTicker.Reset(m.keepAlive) } +func (m *mapSession) beforeServeLongPoll() { + if m.node.IsEphemeral() { + m.h.ephemeralGC.Cancel(m.node.ID) + } +} + +func (m *mapSession) afterServeLongPoll() { + if m.node.IsEphemeral() { + m.h.ephemeralGC.Schedule(m.node.ID, m.h.cfg.EphemeralNodeInactivityTimeout) + } +} + // serve handles non-streaming requests. func (m *mapSession) serve() { // TODO(kradalby): A set todos to harden: @@ -180,6 +192,8 @@ func (m *mapSession) serve() { // //nolint:gocyclo func (m *mapSession) serveLongPoll() { + m.beforeServeLongPoll() + // Clean up the session when the client disconnects defer func() { m.cancelChMu.Lock() @@ -197,6 +211,7 @@ func (m *mapSession) serveLongPoll() { m.pollFailoverRoutes("node closing connection", m.node) } + m.afterServeLongPoll() m.infof("node has disconnected, mapSession: %p, chan: %p", m, m.ch) }() diff --git a/hscontrol/types/node.go b/hscontrol/types/node.go index 19b287a1..24e36535 100644 --- a/hscontrol/types/node.go +++ b/hscontrol/types/node.go @@ -119,7 +119,7 @@ type Node struct { ForcedTags StringList // TODO(kradalby): This seems like irrelevant information? - AuthKeyID *uint `sql:"DEFAULT:NULL"` + AuthKeyID *uint64 `sql:"DEFAULT:NULL"` AuthKey *PreAuthKey `gorm:"constraint:OnDelete:SET NULL;"` LastSeen *time.Time diff --git a/integration/general_test.go b/integration/general_test.go index 245e8f09..c17b977e 100644 --- a/integration/general_test.go +++ b/integration/general_test.go @@ -297,6 +297,122 @@ func TestEphemeral(t *testing.T) { } } +// TestEphemeral2006DeletedTooQuickly verifies that ephemeral nodes are not +// deleted by accident if they are still online and active. +func TestEphemeral2006DeletedTooQuickly(t *testing.T) { + IntegrationSkip(t) + t.Parallel() + + scenario, err := NewScenario(dockertestMaxWait()) + assertNoErr(t, err) + defer scenario.Shutdown() + + spec := map[string]int{ + "user1": len(MustTestVersions), + "user2": len(MustTestVersions), + } + + headscale, err := scenario.Headscale( + hsic.WithTestName("ephemeral2006"), + hsic.WithConfigEnv(map[string]string{ + "HEADSCALE_EPHEMERAL_NODE_INACTIVITY_TIMEOUT": "1m6s", + }), + ) + assertNoErrHeadscaleEnv(t, err) + + for userName, clientCount := range spec { + err = scenario.CreateUser(userName) + if err != nil { + t.Fatalf("failed to create user %s: %s", userName, err) + } + + err = scenario.CreateTailscaleNodesInUser(userName, "all", clientCount, []tsic.Option{}...) + if err != nil { + t.Fatalf("failed to create tailscale nodes in user %s: %s", userName, err) + } + + key, err := scenario.CreatePreAuthKey(userName, true, true) + if err != nil { + t.Fatalf("failed to create pre-auth key for user %s: %s", userName, err) + } + + err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.GetKey()) + if err != nil { + t.Fatalf("failed to run tailscale up for user %s: %s", userName, err) + } + } + + err = scenario.WaitForTailscaleSync() + assertNoErrSync(t, err) + + allClients, err := scenario.ListTailscaleClients() + assertNoErrListClients(t, err) + + allIps, err := scenario.ListTailscaleClientsIPs() + assertNoErrListClientIPs(t, err) + + allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string { + return x.String() + }) + + // All ephemeral nodes should be online and reachable. + success := pingAllHelper(t, allClients, allAddrs) + t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps)) + + // Take down all clients, this should start an expiry timer for each. + for _, client := range allClients { + err := client.Down() + if err != nil { + t.Fatalf("failed to take down client %s: %s", client.Hostname(), err) + } + } + + // Wait a bit and bring up the clients again before the expiry + // time of the ephemeral nodes. + // Nodes should be able to reconnect and work fine. + time.Sleep(30 * time.Second) + + for _, client := range allClients { + err := client.Up() + if err != nil { + t.Fatalf("failed to take down client %s: %s", client.Hostname(), err) + } + } + err = scenario.WaitForTailscaleSync() + assertNoErrSync(t, err) + + success = pingAllHelper(t, allClients, allAddrs) + t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps)) + + // Take down all clients, this should start an expiry timer for each. + for _, client := range allClients { + err := client.Down() + if err != nil { + t.Fatalf("failed to take down client %s: %s", client.Hostname(), err) + } + } + + // This time wait for all of the nodes to expire and check that they are no longer + // registered. + time.Sleep(3 * time.Minute) + + for userName := range spec { + nodes, err := headscale.ListNodesInUser(userName) + if err != nil { + log.Error(). + Err(err). + Str("user", userName). + Msg("Error listing nodes in user") + + return + } + + if len(nodes) != 0 { + t.Fatalf("expected no nodes, got %d in user %s", len(nodes), userName) + } + } +} + func TestPingAllByHostname(t *testing.T) { IntegrationSkip(t) t.Parallel() From 9e523d4687f9504146b60f246e01256893ba47a6 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 19 Jul 2024 09:03:18 +0200 Subject: [PATCH 092/145] move userprofiles into method on user struct (#2014) Signed-off-by: Kristoffer Dalby --- hscontrol/mapper/mapper.go | 15 ++----------- hscontrol/mapper/mapper_test.go | 1 - hscontrol/types/users.go | 40 ++++++++++++++++++++++----------- 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/hscontrol/mapper/mapper.go b/hscontrol/mapper/mapper.go index d7a6cfce..73420419 100644 --- a/hscontrol/mapper/mapper.go +++ b/hscontrol/mapper/mapper.go @@ -94,7 +94,6 @@ func (m *Mapper) String() string { func generateUserProfiles( node *types.Node, peers types.Nodes, - baseDomain string, ) []tailcfg.UserProfile { userMap := make(map[string]types.User) userMap[node.User.Name] = node.User @@ -104,18 +103,8 @@ func generateUserProfiles( var profiles []tailcfg.UserProfile for _, user := range userMap { - displayName := user.Name - - if baseDomain != "" { - displayName = fmt.Sprintf("%s@%s", user.Name, baseDomain) - } - profiles = append(profiles, - tailcfg.UserProfile{ - ID: tailcfg.UserID(user.ID), - LoginName: user.Name, - DisplayName: displayName, - }) + user.TailscaleUserProfile()) } return profiles @@ -569,7 +558,7 @@ func appendPeerChanges( changed = policy.FilterNodesByACL(node, changed, packetFilter) } - profiles := generateUserProfiles(node, changed, cfg.BaseDomain) + profiles := generateUserProfiles(node, changed) dnsConfig := generateDNSConfig( cfg, diff --git a/hscontrol/mapper/mapper_test.go b/hscontrol/mapper/mapper_test.go index be48c6fa..0484fc02 100644 --- a/hscontrol/mapper/mapper_test.go +++ b/hscontrol/mapper/mapper_test.go @@ -43,7 +43,6 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) { types.Nodes{ nodeInShared2, nodeInShared3, node2InShared1, }, - "", ) c.Assert(len(userProfiles), check.Equals, 3) diff --git a/hscontrol/types/users.go b/hscontrol/types/users.go index 0b8324f2..63e73a56 100644 --- a/hscontrol/types/users.go +++ b/hscontrol/types/users.go @@ -19,32 +19,46 @@ type User struct { Name string `gorm:"unique"` } -func (n *User) TailscaleUser() *tailcfg.User { +// TODO(kradalby): See if we can fill in Gravatar here +func (u *User) profilePicURL() string { + return "" +} + +func (u *User) TailscaleUser() *tailcfg.User { user := tailcfg.User{ - ID: tailcfg.UserID(n.ID), - LoginName: n.Name, - DisplayName: n.Name, - // TODO(kradalby): See if we can fill in Gravatar here - ProfilePicURL: "", + ID: tailcfg.UserID(u.ID), + LoginName: u.Name, + DisplayName: u.Name, + ProfilePicURL: u.profilePicURL(), Logins: []tailcfg.LoginID{}, - Created: n.CreatedAt, + Created: u.CreatedAt, } return &user } -func (n *User) TailscaleLogin() *tailcfg.Login { +func (u *User) TailscaleLogin() *tailcfg.Login { login := tailcfg.Login{ - ID: tailcfg.LoginID(n.ID), - LoginName: n.Name, - DisplayName: n.Name, - // TODO(kradalby): See if we can fill in Gravatar here - ProfilePicURL: "", + ID: tailcfg.LoginID(u.ID), + // TODO(kradalby): this should reflect registration method. + Provider: "", + LoginName: u.Name, + DisplayName: u.Name, + ProfilePicURL: u.profilePicURL(), } return &login } +func (u *User) TailscaleUserProfile() tailcfg.UserProfile { + return tailcfg.UserProfile{ + ID: tailcfg.UserID(u.ID), + LoginName: u.Name, + DisplayName: u.Name, + ProfilePicURL: u.profilePicURL(), + } +} + func (n *User) Proto() *v1.User { return &v1.User{ Id: strconv.FormatUint(uint64(n.ID), util.Base10), From 11fde62b8c4e61c3d037df985486d70ec12d9bc6 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 19 Jul 2024 09:04:04 +0200 Subject: [PATCH 093/145] remove custom contains funcs for slices.Contains (#2015) Signed-off-by: Kristoffer Dalby --- cmd/headscale/cli/nodes.go | 5 +++-- cmd/headscale/cli/utils.go | 11 ----------- hscontrol/oidc.go | 7 ++++--- hscontrol/util/string.go | 10 ---------- 4 files changed, 7 insertions(+), 26 deletions(-) diff --git a/cmd/headscale/cli/nodes.go b/cmd/headscale/cli/nodes.go index 58890cb0..4de7b969 100644 --- a/cmd/headscale/cli/nodes.go +++ b/cmd/headscale/cli/nodes.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "net/netip" + "slices" "strconv" "strings" "time" @@ -617,14 +618,14 @@ func nodesToPtables( forcedTags = strings.TrimLeft(forcedTags, ",") var invalidTags string for _, tag := range node.GetInvalidTags() { - if !contains(node.GetForcedTags(), tag) { + if !slices.Contains(node.GetForcedTags(), tag) { invalidTags += "," + pterm.LightRed(tag) } } invalidTags = strings.TrimLeft(invalidTags, ",") var validTags string for _, tag := range node.GetValidTags() { - if !contains(node.GetForcedTags(), tag) { + if !slices.Contains(node.GetForcedTags(), tag) { validTags += "," + pterm.LightGreen(tag) } } diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index 8a91c5c6..e4fef807 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -6,7 +6,6 @@ import ( "encoding/json" "fmt" "os" - "reflect" "github.com/rs/zerolog/log" "google.golang.org/grpc" @@ -197,13 +196,3 @@ func (t tokenAuth) GetRequestMetadata( func (tokenAuth) RequireTransportSecurity() bool { return true } - -func contains[T string](ts []T, t T) bool { - for _, v := range ts { - if reflect.DeepEqual(v, t) { - return true - } - } - - return false -} diff --git a/hscontrol/oidc.go b/hscontrol/oidc.go index b728a6d0..bb836a06 100644 --- a/hscontrol/oidc.go +++ b/hscontrol/oidc.go @@ -10,6 +10,7 @@ import ( "fmt" "html/template" "net/http" + "slices" "strings" "time" @@ -365,7 +366,7 @@ func validateOIDCAllowedDomains( ) error { if len(allowedDomains) > 0 { if at := strings.LastIndex(claims.Email, "@"); at < 0 || - !util.IsStringInSlice(allowedDomains, claims.Email[at+1:]) { + !slices.Contains(allowedDomains, claims.Email[at+1:]) { log.Trace().Msg("authenticated principal does not match any allowed domain") writer.Header().Set("Content-Type", "text/plain; charset=utf-8") @@ -393,7 +394,7 @@ func validateOIDCAllowedGroups( ) error { if len(allowedGroups) > 0 { for _, group := range allowedGroups { - if util.IsStringInSlice(claims.Groups, group) { + if slices.Contains(claims.Groups, group) { return nil } } @@ -420,7 +421,7 @@ func validateOIDCAllowedUsers( claims *IDTokenClaims, ) error { if len(allowedUsers) > 0 && - !util.IsStringInSlice(allowedUsers, claims.Email) { + !slices.Contains(allowedUsers, claims.Email) { log.Trace().Msg("authenticated principal does not match any allowed user") writer.Header().Set("Content-Type", "text/plain; charset=utf-8") writer.WriteHeader(http.StatusBadRequest) diff --git a/hscontrol/util/string.go b/hscontrol/util/string.go index 6f018aff..ce38b82e 100644 --- a/hscontrol/util/string.go +++ b/hscontrol/util/string.go @@ -56,16 +56,6 @@ func GenerateRandomStringDNSSafe(size int) (string, error) { return str[:size], nil } -func IsStringInSlice(slice []string, str string) bool { - for _, s := range slice { - if s == str { - return true - } - } - - return false -} - func TailNodesToString(nodes []*tailcfg.Node) string { temp := make([]string, len(nodes)) From ca47d6f353701b9a475abac497f3ed0bdb8ffb49 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 19 Jul 2024 09:21:14 +0200 Subject: [PATCH 094/145] small cleanups (#2017) --- hscontrol/app.go | 5 ----- hscontrol/types/config.go | 2 -- 2 files changed, 7 deletions(-) diff --git a/hscontrol/app.go b/hscontrol/app.go index 0a23f07d..fe025adc 100644 --- a/hscontrol/app.go +++ b/hscontrol/app.go @@ -80,11 +80,6 @@ const ( registerCacheCleanup = time.Minute * 20 ) -// func init() { -// deadlock.Opts.DeadlockTimeout = 15 * time.Second -// deadlock.Opts.PrintAllCurrentGoroutines = true -// } - // Headscale represents the base app of the service. type Headscale struct { cfg *types.Config diff --git a/hscontrol/types/config.go b/hscontrol/types/config.go index 6eae9a32..2143d182 100644 --- a/hscontrol/types/config.go +++ b/hscontrol/types/config.go @@ -619,7 +619,6 @@ func PrefixV4() (*netip.Prefix, error) { builder := netipx.IPSetBuilder{} builder.AddPrefix(tsaddr.CGNATRange()) - builder.AddPrefix(tsaddr.TailscaleULARange()) ipSet, _ := builder.IPSet() if !ipSet.ContainsPrefix(prefixV4) { log.Warn(). @@ -643,7 +642,6 @@ func PrefixV6() (*netip.Prefix, error) { } builder := netipx.IPSetBuilder{} - builder.AddPrefix(tsaddr.CGNATRange()) builder.AddPrefix(tsaddr.TailscaleULARange()) ipSet, _ := builder.IPSet() From 8571513e3c6d601deb10d2cca0a7f837dc466770 Mon Sep 17 00:00:00 2001 From: greizgh Date: Mon, 22 Jul 2024 08:56:00 +0200 Subject: [PATCH 095/145] reformat code (#2019) * reformat code This is mostly an automated change with `make lint`. I had to manually please golangci-lint in routes_test because of a short variable name. * fix start -> strategy which was wrongly corrected by linter --- cmd/headscale/cli/policy.go | 3 +-- cmd/headscale/cli/utils.go | 9 ++++----- hscontrol/app.go | 23 +++++++++++------------ hscontrol/auth.go | 1 - hscontrol/db/db.go | 7 +++---- hscontrol/db/ip.go | 1 - hscontrol/db/ip_test.go | 2 ++ hscontrol/db/node_test.go | 7 +++---- hscontrol/db/policy.go | 5 ++--- hscontrol/db/routes.go | 1 - hscontrol/db/routes_test.go | 17 +++++++++++------ hscontrol/mapper/mapper.go | 1 - hscontrol/metrics.go | 2 +- hscontrol/notifier/notifier.go | 9 +++++---- hscontrol/oidc.go | 1 - hscontrol/policy/acls.go | 5 ++--- hscontrol/policy/acls_test.go | 9 ++++----- hscontrol/policy/acls_types.go | 2 +- hscontrol/poll.go | 1 - hscontrol/types/config.go | 3 +-- integration/cli_test.go | 5 ++--- integration/control.go | 3 +-- integration/hsic/hsic.go | 4 ++-- 23 files changed, 56 insertions(+), 65 deletions(-) diff --git a/cmd/headscale/cli/policy.go b/cmd/headscale/cli/policy.go index 5b34a1e1..00c4566d 100644 --- a/cmd/headscale/cli/policy.go +++ b/cmd/headscale/cli/policy.go @@ -4,10 +4,9 @@ import ( "io" "os" + v1 "github.com/juanfont/headscale/gen/go/headscale/v1" "github.com/rs/zerolog/log" "github.com/spf13/cobra" - - v1 "github.com/juanfont/headscale/gen/go/headscale/v1" ) func init() { diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index e4fef807..409e3dc4 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -7,16 +7,15 @@ import ( "fmt" "os" + v1 "github.com/juanfont/headscale/gen/go/headscale/v1" + "github.com/juanfont/headscale/hscontrol" + "github.com/juanfont/headscale/hscontrol/types" + "github.com/juanfont/headscale/hscontrol/util" "github.com/rs/zerolog/log" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" "gopkg.in/yaml.v3" - - v1 "github.com/juanfont/headscale/gen/go/headscale/v1" - "github.com/juanfont/headscale/hscontrol" - "github.com/juanfont/headscale/hscontrol/types" - "github.com/juanfont/headscale/hscontrol/util" ) const ( diff --git a/hscontrol/app.go b/hscontrol/app.go index fe025adc..b66e939b 100644 --- a/hscontrol/app.go +++ b/hscontrol/app.go @@ -23,6 +23,16 @@ import ( "github.com/gorilla/mux" grpcMiddleware "github.com/grpc-ecosystem/go-grpc-middleware" grpcRuntime "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/juanfont/headscale" + v1 "github.com/juanfont/headscale/gen/go/headscale/v1" + "github.com/juanfont/headscale/hscontrol/db" + "github.com/juanfont/headscale/hscontrol/derp" + derpServer "github.com/juanfont/headscale/hscontrol/derp/server" + "github.com/juanfont/headscale/hscontrol/mapper" + "github.com/juanfont/headscale/hscontrol/notifier" + "github.com/juanfont/headscale/hscontrol/policy" + "github.com/juanfont/headscale/hscontrol/types" + "github.com/juanfont/headscale/hscontrol/util" "github.com/patrickmn/go-cache" zerolog "github.com/philip-bui/grpc-zerolog" "github.com/pkg/profile" @@ -47,17 +57,6 @@ import ( "tailscale.com/types/dnstype" "tailscale.com/types/key" "tailscale.com/util/dnsname" - - "github.com/juanfont/headscale" - v1 "github.com/juanfont/headscale/gen/go/headscale/v1" - "github.com/juanfont/headscale/hscontrol/db" - "github.com/juanfont/headscale/hscontrol/derp" - derpServer "github.com/juanfont/headscale/hscontrol/derp/server" - "github.com/juanfont/headscale/hscontrol/mapper" - "github.com/juanfont/headscale/hscontrol/notifier" - "github.com/juanfont/headscale/hscontrol/policy" - "github.com/juanfont/headscale/hscontrol/types" - "github.com/juanfont/headscale/hscontrol/util" ) var ( @@ -680,7 +679,7 @@ func (h *Headscale) Serve() error { Handler: router, ReadTimeout: types.HTTPTimeout, - // Long polling should not have any timeout, this is overriden + // Long polling should not have any timeout, this is overridden // further down the chain WriteTimeout: types.HTTPTimeout, } diff --git a/hscontrol/auth.go b/hscontrol/auth.go index 010d15a2..aaab03ce 100644 --- a/hscontrol/auth.go +++ b/hscontrol/auth.go @@ -337,7 +337,6 @@ func (h *Headscale) handleAuthKey( if len(aclTags) > 0 { // This conditional preserves the existing behaviour, although SaaS would reset the tags on auth-key login err = h.db.SetTags(node.ID, aclTags) - if err != nil { log.Error(). Caller(). diff --git a/hscontrol/db/db.go b/hscontrol/db/db.go index b44d76ab..c1908134 100644 --- a/hscontrol/db/db.go +++ b/hscontrol/db/db.go @@ -13,13 +13,12 @@ import ( "github.com/glebarez/sqlite" "github.com/go-gormigrate/gormigrate/v2" + "github.com/juanfont/headscale/hscontrol/types" + "github.com/juanfont/headscale/hscontrol/util" "github.com/rs/zerolog/log" "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/gorm/logger" - - "github.com/juanfont/headscale/hscontrol/types" - "github.com/juanfont/headscale/hscontrol/util" ) var errDatabaseNotSupported = errors.New("database type not supported") @@ -331,7 +330,7 @@ func NewHeadscaleDatabase( // IP v4 and v6 column. // Note that previously, the list _could_ contain more // than two addresses, which should not really happen. - // In that case, the first occurence of each type will + // In that case, the first occurrence of each type will // be kept. ID: "2024041121742", Migrate: func(tx *gorm.DB) error { diff --git a/hscontrol/db/ip.go b/hscontrol/db/ip.go index 7d06e2e8..d0e030d6 100644 --- a/hscontrol/db/ip.go +++ b/hscontrol/db/ip.go @@ -76,7 +76,6 @@ func NewIPAllocator( if err != nil { return nil, fmt.Errorf("reading IPv6 addresses from database: %w", err) } - } var ips netipx.IPSetBuilder diff --git a/hscontrol/db/ip_test.go b/hscontrol/db/ip_test.go index c922fcdf..ce9c134c 100644 --- a/hscontrol/db/ip_test.go +++ b/hscontrol/db/ip_test.go @@ -18,9 +18,11 @@ var mpp = func(pref string) *netip.Prefix { p := netip.MustParsePrefix(pref) return &p } + var na = func(pref string) netip.Addr { return netip.MustParseAddr(pref) } + var nap = func(pref string) *netip.Addr { n := na(pref) return &n diff --git a/hscontrol/db/node_test.go b/hscontrol/db/node_test.go index d88d0458..065e70b7 100644 --- a/hscontrol/db/node_test.go +++ b/hscontrol/db/node_test.go @@ -12,16 +12,15 @@ import ( "time" "github.com/google/go-cmp/cmp" + "github.com/juanfont/headscale/hscontrol/policy" + "github.com/juanfont/headscale/hscontrol/types" + "github.com/juanfont/headscale/hscontrol/util" "github.com/puzpuzpuz/xsync/v3" "github.com/stretchr/testify/assert" "gopkg.in/check.v1" "tailscale.com/tailcfg" "tailscale.com/types/key" "tailscale.com/types/ptr" - - "github.com/juanfont/headscale/hscontrol/policy" - "github.com/juanfont/headscale/hscontrol/types" - "github.com/juanfont/headscale/hscontrol/util" ) func (s *Suite) TestGetNode(c *check.C) { diff --git a/hscontrol/db/policy.go b/hscontrol/db/policy.go index dcbdc812..49b419b5 100644 --- a/hscontrol/db/policy.go +++ b/hscontrol/db/policy.go @@ -1,12 +1,11 @@ package db import ( - "gorm.io/gorm" - "gorm.io/gorm/clause" - "errors" "github.com/juanfont/headscale/hscontrol/types" + "gorm.io/gorm" + "gorm.io/gorm/clause" ) // SetPolicy sets the policy in the database. diff --git a/hscontrol/db/routes.go b/hscontrol/db/routes.go index 3b897190..fd837c29 100644 --- a/hscontrol/db/routes.go +++ b/hscontrol/db/routes.go @@ -542,7 +542,6 @@ func failoverRoute( isLikelyConnected *xsync.MapOf[types.NodeID, bool], routeToReplace *types.Route, altRoutes types.Routes, - ) *failover { if routeToReplace == nil { return nil diff --git a/hscontrol/db/routes_test.go b/hscontrol/db/routes_test.go index 122a7ff3..2324a21b 100644 --- a/hscontrol/db/routes_test.go +++ b/hscontrol/db/routes_test.go @@ -285,25 +285,30 @@ func (s *Suite) TestDeleteRoutes(c *check.C) { c.Assert(len(enabledRoutes1), check.Equals, 1) } -var ipp = func(s string) types.IPPrefix { return types.IPPrefix(netip.MustParsePrefix(s)) } -var n = func(nid types.NodeID) types.Node { - return types.Node{ID: nid} -} +var ( + ipp = func(s string) types.IPPrefix { return types.IPPrefix(netip.MustParsePrefix(s)) } + mkNode = func(nid types.NodeID) types.Node { + return types.Node{ID: nid} + } +) + var np = func(nid types.NodeID) *types.Node { - no := n(nid) + no := mkNode(nid) return &no } + var r = func(id uint, nid types.NodeID, prefix types.IPPrefix, enabled, primary bool) types.Route { return types.Route{ Model: gorm.Model{ ID: id, }, - Node: n(nid), + Node: mkNode(nid), Prefix: prefix, Enabled: enabled, IsPrimary: primary, } } + var rp = func(id uint, nid types.NodeID, prefix types.IPPrefix, enabled, primary bool) *types.Route { ro := r(id, nid, prefix, enabled, primary) return &ro diff --git a/hscontrol/mapper/mapper.go b/hscontrol/mapper/mapper.go index 73420419..702b7845 100644 --- a/hscontrol/mapper/mapper.go +++ b/hscontrol/mapper/mapper.go @@ -541,7 +541,6 @@ func appendPeerChanges( changed types.Nodes, cfg *types.Config, ) error { - packetFilter, err := pol.CompileFilterRules(append(peers, node)) if err != nil { return err diff --git a/hscontrol/metrics.go b/hscontrol/metrics.go index 835a6aac..4870e74e 100644 --- a/hscontrol/metrics.go +++ b/hscontrol/metrics.go @@ -40,7 +40,7 @@ var ( mapResponseWriteUpdatesInStream = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: prometheusNamespace, Name: "mapresponse_write_updates_in_stream_total", - Help: "total count of writes that occured in a stream session, pre-68 nodes", + Help: "total count of writes that occurred in a stream session, pre-68 nodes", }, []string{"status"}) mapResponseEndpointUpdates = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: prometheusNamespace, diff --git a/hscontrol/notifier/notifier.go b/hscontrol/notifier/notifier.go index 483c3f37..0b663776 100644 --- a/hscontrol/notifier/notifier.go +++ b/hscontrol/notifier/notifier.go @@ -17,8 +17,10 @@ import ( "tailscale.com/util/set" ) -var debugDeadlock = envknob.Bool("HEADSCALE_DEBUG_DEADLOCK") -var debugDeadlockTimeout = envknob.RegisterDuration("HEADSCALE_DEBUG_DEADLOCK_TIMEOUT") +var ( + debugDeadlock = envknob.Bool("HEADSCALE_DEBUG_DEADLOCK") + debugDeadlockTimeout = envknob.RegisterDuration("HEADSCALE_DEBUG_DEADLOCK_TIMEOUT") +) func init() { deadlock.Opts.Disable = !debugDeadlock @@ -291,7 +293,6 @@ func newBatcher(batchTime time.Duration, n *Notifier) *batcher { patches: make(map[types.NodeID]tailcfg.PeerChange), n: n, } - } func (b *batcher) close() { @@ -393,7 +394,7 @@ func (b *batcher) doWork() { } // overwritePatch takes the current patch and a newer patch -// and override any field that has changed +// and override any field that has changed. func overwritePatch(currPatch, newPatch *tailcfg.PeerChange) { if newPatch.DERPRegion != 0 { currPatch.DERPRegion = newPatch.DERPRegion diff --git a/hscontrol/oidc.go b/hscontrol/oidc.go index bb836a06..fe4d357c 100644 --- a/hscontrol/oidc.go +++ b/hscontrol/oidc.go @@ -57,7 +57,6 @@ func (h *Headscale) initOIDC() error { // grab oidc config if it hasn't been already if h.oauth2Config == nil { h.oidcProvider, err = oidc.NewProvider(context.Background(), h.cfg.OIDC.Issuer) - if err != nil { return fmt.Errorf("creating OIDC provider from issuer config: %w", err) } diff --git a/hscontrol/policy/acls.go b/hscontrol/policy/acls.go index 64697e33..2b3a50f7 100644 --- a/hscontrol/policy/acls.go +++ b/hscontrol/policy/acls.go @@ -11,13 +11,12 @@ import ( "strings" "time" + "github.com/juanfont/headscale/hscontrol/types" + "github.com/juanfont/headscale/hscontrol/util" "github.com/rs/zerolog/log" "github.com/tailscale/hujson" "go4.org/netipx" "tailscale.com/tailcfg" - - "github.com/juanfont/headscale/hscontrol/types" - "github.com/juanfont/headscale/hscontrol/util" ) var ( diff --git a/hscontrol/policy/acls_test.go b/hscontrol/policy/acls_test.go index b3cc10f0..6b2e0f97 100644 --- a/hscontrol/policy/acls_test.go +++ b/hscontrol/policy/acls_test.go @@ -6,15 +6,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/juanfont/headscale/hscontrol/types" + "github.com/juanfont/headscale/hscontrol/util" "github.com/rs/zerolog/log" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "go4.org/netipx" "gopkg.in/check.v1" "tailscale.com/tailcfg" - - "github.com/juanfont/headscale/hscontrol/types" - "github.com/juanfont/headscale/hscontrol/util" ) var iap = func(ipStr string) *netip.Addr { @@ -1783,7 +1782,7 @@ var tsExitNodeDest = []tailcfg.NetPortRange{ } // hsExitNodeDest is the list of destination IP ranges that are allowed when -// we use headscale "autogroup:internet" +// we use headscale "autogroup:internet". var hsExitNodeDest = []tailcfg.NetPortRange{ {IP: "0.0.0.0/5", Ports: tailcfg.PortRangeAny}, {IP: "8.0.0.0/7", Ports: tailcfg.PortRangeAny}, @@ -1840,7 +1839,7 @@ func TestTheInternet(t *testing.T) { internetPrefs := internetSet.Prefixes() - for i, _ := range internetPrefs { + for i := range internetPrefs { if internetPrefs[i].String() != hsExitNodeDest[i].IP { t.Errorf("prefix from internet set %q != hsExit list %q", internetPrefs[i].String(), hsExitNodeDest[i].IP) } diff --git a/hscontrol/policy/acls_types.go b/hscontrol/policy/acls_types.go index 25f02f16..5b5d1838 100644 --- a/hscontrol/policy/acls_types.go +++ b/hscontrol/policy/acls_types.go @@ -10,7 +10,7 @@ import ( // ACLPolicy represents a Tailscale ACL Policy. type ACLPolicy struct { - Groups Groups `json:"groups" ` + Groups Groups `json:"groups"` Hosts Hosts `json:"hosts"` TagOwners TagOwners `json:"tagOwners"` ACLs []ACL `json:"acls"` diff --git a/hscontrol/poll.go b/hscontrol/poll.go index 8122064b..b9bf65a2 100644 --- a/hscontrol/poll.go +++ b/hscontrol/poll.go @@ -184,7 +184,6 @@ func (m *mapSession) serve() { return } - } // serveLongPoll ensures the node gets the appropriate updates from either diff --git a/hscontrol/types/config.go b/hscontrol/types/config.go index 2143d182..0c077870 100644 --- a/hscontrol/types/config.go +++ b/hscontrol/types/config.go @@ -11,6 +11,7 @@ import ( "time" "github.com/coreos/go-oidc/v3/oidc" + "github.com/juanfont/headscale/hscontrol/util" "github.com/prometheus/common/model" "github.com/rs/zerolog" "github.com/rs/zerolog/log" @@ -19,8 +20,6 @@ import ( "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" "tailscale.com/types/dnstype" - - "github.com/juanfont/headscale/hscontrol/util" ) const ( diff --git a/integration/cli_test.go b/integration/cli_test.go index 9bc67a89..088db786 100644 --- a/integration/cli_test.go +++ b/integration/cli_test.go @@ -7,12 +7,11 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" - 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" ) func executeAndUnmarshal[T any](headscale ControlServer, command []string, result T) error { @@ -481,7 +480,7 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) { assert.Nil(t, err) assert.Len(t, listNodes, 1) - assert.Equal(t, "user2", listNodes[0].User.Name) + assert.Equal(t, "user2", listNodes[0].GetUser().GetName()) } func TestApiKeyCommand(t *testing.T) { diff --git a/integration/control.go b/integration/control.go index 4260ac4b..8a34bde8 100644 --- a/integration/control.go +++ b/integration/control.go @@ -1,9 +1,8 @@ package integration import ( - "github.com/ory/dockertest/v3" - v1 "github.com/juanfont/headscale/gen/go/headscale/v1" + "github.com/ory/dockertest/v3" ) type ControlServer interface { diff --git a/integration/hsic/hsic.go b/integration/hsic/hsic.go index 5b55a0a8..3794e085 100644 --- a/integration/hsic/hsic.go +++ b/integration/hsic/hsic.go @@ -177,9 +177,9 @@ func WithPostgres() Option { } // WithIPAllocationStrategy sets the tests IP Allocation strategy. -func WithIPAllocationStrategy(strat types.IPAllocationStrategy) Option { +func WithIPAllocationStrategy(strategy types.IPAllocationStrategy) Option { return func(hsic *HeadscaleInContainer) { - hsic.env["HEADSCALE_PREFIXES_ALLOCATION"] = string(strat) + hsic.env["HEADSCALE_PREFIXES_ALLOCATION"] = string(strategy) } } From b799245f1e0a09217dd7e5f6730df9530c67458f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 22 Jul 2024 08:56:21 +0200 Subject: [PATCH 096/145] flake.lock: Update (#2021) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 48ef53b6..f41aeaf6 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1720781449, - "narHash": "sha256-po3TZO9kcZwzvkyMJKb0WCzzDtiHWD34XeRaX1lWXp0=", + "lastModified": 1721466660, + "narHash": "sha256-pFSxgSZqZ3h+5Du0KvEL1ccDZBwu4zvOil1zzrPNb3c=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "8b5a3d5a1d951344d683b442c0739010b80039db", + "rev": "6e14bbce7bea6c4efd7adfa88a40dac750d80100", "type": "github" }, "original": { From db7a4358e91d77303ca3c71efbd2070c703ae9dc Mon Sep 17 00:00:00 2001 From: Steven Honson Date: Mon, 22 Jul 2024 23:38:42 +1000 Subject: [PATCH 097/145] config-example.yaml: Remove reference to yaml for policy files (#2022) --- config-example.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/config-example.yaml b/config-example.yaml index f408ff50..8f6f01c3 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -211,9 +211,8 @@ policy: # The mode can be "file" or "database" that defines # where the ACL policies are stored and read from. mode: file - # If the mode is set to "file", the - # path to a file containing ACL policies. - # The file can be in YAML or HuJSON format. + # If the mode is set to "file", the path to a + # HuJSON file containing ACL policies. path: "" ## DNS From 4ad3f3c484c060b4bfb5d8762fa12a4e4529a672 Mon Sep 17 00:00:00 2001 From: nadongjun Date: Tue, 23 Jul 2024 00:11:57 +0900 Subject: [PATCH 098/145] Fix data race issues in EphemeralGarbageCollector tests (#2023) * Fix data race issues in EphemeralGarbageCollector tests * Add defer for mutex unlock in TestEphemeralGarbageCollectorOrder * Fix mutex unlock order in closure by updating defer placement --- hscontrol/db/node_test.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/hscontrol/db/node_test.go b/hscontrol/db/node_test.go index 065e70b7..ad94f064 100644 --- a/hscontrol/db/node_test.go +++ b/hscontrol/db/node_test.go @@ -600,8 +600,11 @@ func (s *Suite) TestAutoApproveRoutes(c *check.C) { func TestEphemeralGarbageCollectorOrder(t *testing.T) { want := []types.NodeID{1, 3} got := []types.NodeID{} + var mu sync.Mutex e := NewEphemeralGarbageCollector(func(ni types.NodeID) { + mu.Lock() + defer mu.Unlock() got = append(got, ni) }) go e.Start() @@ -617,6 +620,9 @@ func TestEphemeralGarbageCollectorOrder(t *testing.T) { e.Close() + mu.Lock() + defer mu.Unlock() + if diff := cmp.Diff(want, got); diff != "" { t.Errorf("wrong nodes deleted, unexpected result (-want +got):\n%s", diff) } @@ -629,8 +635,8 @@ func TestEphemeralGarbageCollectorLoads(t *testing.T) { want := 1000 e := NewEphemeralGarbageCollector(func(ni types.NodeID) { - defer mu.Unlock() mu.Lock() + defer mu.Unlock() time.Sleep(time.Duration(generateRandomNumber(t, 3)) * time.Millisecond) got = append(got, ni) @@ -644,6 +650,10 @@ func TestEphemeralGarbageCollectorLoads(t *testing.T) { time.Sleep(10 * time.Second) e.Close() + + mu.Lock() + defer mu.Unlock() + if len(got) != want { t.Errorf("expected %d, got %d", want, len(got)) } From 06f07053eb3ef08c3236483891a94c0b81eb8393 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 28 Jul 2024 08:42:38 +0000 Subject: [PATCH 099/145] flake.lock: Update (#2035) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index f41aeaf6..ec02aa37 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1721466660, - "narHash": "sha256-pFSxgSZqZ3h+5Du0KvEL1ccDZBwu4zvOil1zzrPNb3c=", + "lastModified": 1722073938, + "narHash": "sha256-OpX0StkL8vpXyWOGUD6G+MA26wAXK6SpT94kLJXo6B4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "6e14bbce7bea6c4efd7adfa88a40dac750d80100", + "rev": "e36e9f57337d0ff0cf77aceb58af4c805472bfae", "type": "github" }, "original": { From 948d53f934b83f0ca6d4d5007973b334a4ed306a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 4 Aug 2024 06:35:46 +0000 Subject: [PATCH 100/145] flake.lock: Update (#2042) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index ec02aa37..7e855a25 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1722073938, - "narHash": "sha256-OpX0StkL8vpXyWOGUD6G+MA26wAXK6SpT94kLJXo6B4=", + "lastModified": 1722640603, + "narHash": "sha256-TcXjLVNd3VeH1qKPH335Tc4RbFDbZQX+d7rqnDUoRaY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e36e9f57337d0ff0cf77aceb58af4c805472bfae", + "rev": "81610abc161d4021b29199aa464d6a1a521e0cc9", "type": "github" }, "original": { From ece907d878fc6fd08085fbed796eba35dfe103fa Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sun, 11 Aug 2024 07:44:59 +0200 Subject: [PATCH 101/145] test embedded derp with derp updater, check client health (#2030) --- hscontrol/derp/derp.go | 5 --- integration/embedded_derp_test.go | 64 ++++++++++++++++++++++++++++--- 2 files changed, 58 insertions(+), 11 deletions(-) diff --git a/hscontrol/derp/derp.go b/hscontrol/derp/derp.go index 3afcb4ea..5d4b24f2 100644 --- a/hscontrol/derp/derp.go +++ b/hscontrol/derp/derp.go @@ -125,10 +125,5 @@ func GetDERPMap(cfg types.DERPConfig) *tailcfg.DERPMap { log.Trace().Interface("derpMap", derpMap).Msg("DERPMap loaded") - if len(derpMap.Regions) == 0 { - log.Warn(). - Msg("DERP map is empty, not a single DERP map datasource was loaded correctly or contained a region") - } - return derpMap } diff --git a/integration/embedded_derp_test.go b/integration/embedded_derp_test.go index 39a9acca..745f2c89 100644 --- a/integration/embedded_derp_test.go +++ b/integration/embedded_derp_test.go @@ -4,7 +4,9 @@ import ( "fmt" "log" "net/url" + "strings" "testing" + "time" "github.com/juanfont/headscale/hscontrol/util" "github.com/juanfont/headscale/integration/dockertestutil" @@ -33,8 +35,7 @@ func TestDERPServerScenario(t *testing.T) { defer scenario.Shutdown() spec := map[string]int{ - "user1": 10, - // "user1": len(MustTestVersions), + "user1": len(MustTestVersions), } err = scenario.CreateHeadscaleEnv( @@ -44,24 +45,75 @@ func TestDERPServerScenario(t *testing.T) { hsic.WithEmbeddedDERPServerOnly(), hsic.WithTLS(), hsic.WithHostnameAsServerURL(), + hsic.WithConfigEnv(map[string]string{ + "HEADSCALE_DERP_AUTO_UPDATE_ENABLED": "true", + "HEADSCALE_DERP_UPDATE_FREQUENCY": "10s", + }), ) assertNoErrHeadscaleEnv(t, err) allClients, err := scenario.ListTailscaleClients() assertNoErrListClients(t, err) - allIps, err := scenario.ListTailscaleClientsIPs() - assertNoErrListClientIPs(t, err) - err = scenario.WaitForTailscaleSync() assertNoErrSync(t, err) allHostnames, err := scenario.ListTailscaleClientsFQDNs() assertNoErrListFQDN(t, err) + for _, client := range allClients { + status, err := client.Status() + assertNoErr(t, err) + + for _, health := range status.Health { + if strings.Contains(health, "could not connect to any relay server") { + t.Errorf("expected to be connected to derp, found: %s", health) + } + if strings.Contains(health, "could not connect to the 'Headscale Embedded DERP' relay server.") { + t.Errorf("expected to be connected to derp, found: %s", health) + } + } + } + success := pingDerpAllHelper(t, allClients, allHostnames) - t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps)) + for _, client := range allClients { + status, err := client.Status() + assertNoErr(t, err) + + for _, health := range status.Health { + if strings.Contains(health, "could not connect to any relay server") { + t.Errorf("expected to be connected to derp, found: %s", health) + } + if strings.Contains(health, "could not connect to the 'Headscale Embedded DERP' relay server.") { + t.Errorf("expected to be connected to derp, found: %s", health) + } + } + } + + t.Logf("Run 1: %d successful pings out of %d", success, len(allClients)*len(allHostnames)) + + // Let the DERP updater run a couple of times to ensure it does not + // break the DERPMap. + time.Sleep(30 * time.Second) + + success = pingDerpAllHelper(t, allClients, allHostnames) + + for _, client := range allClients { + status, err := client.Status() + assertNoErr(t, err) + + for _, health := range status.Health { + if strings.Contains(health, "could not connect to any relay server") { + t.Errorf("expected to be connected to derp, found: %s", health) + } + if strings.Contains(health, "could not connect to the 'Headscale Embedded DERP' relay server.") { + t.Errorf("expected to be connected to derp, found: %s", health) + } + } + } + + t.Logf("Run2: %d successful pings out of %d", success, len(allClients)*len(allHostnames)) } func (s *EmbeddedDERPServerScenario) CreateHeadscaleEnv( From fcd1183805df3da6f8fe5bec79edf970015c63b9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 11 Aug 2024 05:48:15 +0000 Subject: [PATCH 102/145] flake.lock: Update (#2052) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 7e855a25..627b7598 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1722640603, - "narHash": "sha256-TcXjLVNd3VeH1qKPH335Tc4RbFDbZQX+d7rqnDUoRaY=", + "lastModified": 1723221148, + "narHash": "sha256-7pjpeQlZUNQ4eeVntytU3jkw9dFK3k1Htgk2iuXjaD8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "81610abc161d4021b29199aa464d6a1a521e0cc9", + "rev": "154bcb95ad51bc257c2ce4043a725de6ca700ef6", "type": "github" }, "original": { From 022fb24cd92035470496d50d86bf8c9ee74b1e7e Mon Sep 17 00:00:00 2001 From: Chuangbo Li Date: Mon, 12 Aug 2024 18:11:59 +0800 Subject: [PATCH 103/145] Fix command get policy works with relative policy path (#2051) --- hscontrol/grpcv1.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hscontrol/grpcv1.go b/hscontrol/grpcv1.go index a351048f..d4e10849 100644 --- a/hscontrol/grpcv1.go +++ b/hscontrol/grpcv1.go @@ -692,7 +692,8 @@ func (api headscaleV1APIServer) GetPolicy( }, nil case types.PolicyModeFile: // Read the file and return the contents as-is. - f, err := os.Open(api.h.cfg.Policy.Path) + absPath := util.AbsolutePathFromConfigPath(api.h.cfg.Policy.Path) + f, err := os.Open(absPath) if err != nil { return nil, err } From ac8491efec4b5ed088ce90e48d14136a1fe228da Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Mon, 19 Aug 2024 11:41:05 +0200 Subject: [PATCH 104/145] Redo DNS configuration (#2034) this commit changes and streamlines the dns_config into a new key, dns. It removes a combination of outdates and incompatible configuration options that made it easy to confuse what headscale could and could not do, or what to expect from ones configuration. Signed-off-by: Kristoffer Dalby --- .github/workflows/test-integration.yaml | 3 +- CHANGELOG.md | 7 +- cmd/headscale/headscale_test.go | 35 -- config-example.yaml | 81 ++-- flake.nix | 2 +- go.mod | 25 +- go.sum | 51 ++- hscontrol/mapper/tail.go | 3 +- hscontrol/mapper/tail_test.go | 4 +- hscontrol/noise.go | 2 +- hscontrol/types/config.go | 368 ++++++++++++------ hscontrol/types/config_test.go | 272 +++++++++++++ hscontrol/types/node.go | 49 ++- hscontrol/types/node_test.go | 8 +- .../testdata/base-domain-in-server-url.yaml | 16 + .../base-domain-not-in-server-url.yaml | 16 + hscontrol/types/testdata/dns_full.yaml | 37 ++ .../types/testdata/dns_full_no_magic.yaml | 37 ++ hscontrol/types/testdata/minimal.yaml | 3 + integration/dns_test.go | 246 ++++++++++++ integration/general_test.go | 68 ---- integration/hsic/config.go | 105 +---- integration/scenario.go | 10 +- integration/tailscale.go | 1 + integration/tsic/tsic.go | 40 ++ 25 files changed, 1036 insertions(+), 453 deletions(-) create mode 100644 hscontrol/types/config_test.go create mode 100644 hscontrol/types/testdata/base-domain-in-server-url.yaml create mode 100644 hscontrol/types/testdata/base-domain-not-in-server-url.yaml create mode 100644 hscontrol/types/testdata/dns_full.yaml create mode 100644 hscontrol/types/testdata/dns_full_no_magic.yaml create mode 100644 hscontrol/types/testdata/minimal.yaml create mode 100644 integration/dns_test.go diff --git a/.github/workflows/test-integration.yaml b/.github/workflows/test-integration.yaml index bf55e2de..6203e51b 100644 --- a/.github/workflows/test-integration.yaml +++ b/.github/workflows/test-integration.yaml @@ -37,6 +37,8 @@ jobs: - TestNodeRenameCommand - TestNodeMoveCommand - TestPolicyCommand + - TestResolveMagicDNS + - TestValidateResolvConf - TestDERPServerScenario - TestPingAllByIP - TestPingAllByIPPublicDERP @@ -45,7 +47,6 @@ jobs: - TestEphemeral2006DeletedTooQuickly - TestPingAllByHostname - TestTaildrop - - TestResolveMagicDNS - TestExpireNode - TestNodeOnlineStatus - TestPingAllByIPManyUpDown diff --git a/CHANGELOG.md b/CHANGELOG.md index fd8787ad..93898f38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,7 @@ after improving the test harness as part of adopting [#1460](https://github.com/ - Adds additional configuration for PostgreSQL for setting max open, idle connection and idle connection lifetime. - 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.38 + - The latest supported client is 1.42 - 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) @@ -43,9 +43,12 @@ after improving the test harness as part of adopting [#1460](https://github.com/ - MagicDNS domains no longer contain usernames []() - This is in preperation to fix Headscales implementation of tags which currently does not correctly remove the link between a tagged device and a user. As tagged devices will not have a user, this will require a change to the DNS generation, removing the username, see [#1369](https://github.com/juanfont/headscale/issues/1369) for more information. - `use_username_in_magic_dns` can be used to turn this behaviour on again, but note that this option _will be removed_ when tags are fixed. - - This option brings Headscales behaviour in line with Tailscale. + - dns.base_domain can no longer be the same as (or part of) server_url. + - This option brings Headscales behaviour in line with Tailscale. - YAML files are no longer supported for headscale policy. [#1792](https://github.com/juanfont/headscale/pull/1792) - HuJSON is now the only supported format for policy. +- DNS configuration has been restructured [#2034](https://github.com/juanfont/headscale/pull/2034) + - Please review the new [config-example.yaml](./config-example.yaml) for the new structure. ### Changes diff --git a/cmd/headscale/headscale_test.go b/cmd/headscale/headscale_test.go index c27fa20a..580caf17 100644 --- a/cmd/headscale/headscale_test.go +++ b/cmd/headscale/headscale_test.go @@ -63,7 +63,6 @@ func (*Suite) TestConfigFileLoading(c *check.C) { c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "") c.Assert(viper.GetString("tls_letsencrypt_listen"), check.Equals, ":http") c.Assert(viper.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01") - c.Assert(viper.GetStringSlice("dns_config.nameservers")[0], check.Equals, "1.1.1.1") c.Assert( util.GetFileMode("unix_socket_permission"), check.Equals, @@ -106,7 +105,6 @@ func (*Suite) TestConfigLoading(c *check.C) { c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "") c.Assert(viper.GetString("tls_letsencrypt_listen"), check.Equals, ":http") c.Assert(viper.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01") - c.Assert(viper.GetStringSlice("dns_config.nameservers")[0], check.Equals, "1.1.1.1") c.Assert( util.GetFileMode("unix_socket_permission"), check.Equals, @@ -116,39 +114,6 @@ func (*Suite) TestConfigLoading(c *check.C) { c.Assert(viper.GetBool("randomize_client_port"), check.Equals, false) } -func (*Suite) TestDNSConfigLoading(c *check.C) { - tmpDir, err := os.MkdirTemp("", "headscale") - if err != nil { - c.Fatal(err) - } - defer os.RemoveAll(tmpDir) - - path, err := os.Getwd() - if err != nil { - c.Fatal(err) - } - - // Symlink the example config file - err = os.Symlink( - filepath.Clean(path+"/../../config-example.yaml"), - filepath.Join(tmpDir, "config.yaml"), - ) - if err != nil { - c.Fatal(err) - } - - // Load example config, it should load without validation errors - err = types.LoadConfig(tmpDir, false) - c.Assert(err, check.IsNil) - - dnsConfig, baseDomain := types.GetDNSConfig() - - c.Assert(dnsConfig.Nameservers[0].String(), check.Equals, "1.1.1.1") - c.Assert(dnsConfig.Resolvers[0].Addr, check.Equals, "1.1.1.1") - c.Assert(dnsConfig.Proxied, check.Equals, true) - c.Assert(baseDomain, check.Equals, "example.com") -} - func writeConfig(c *check.C, tmpDir string, configYaml []byte) { // Populate a custom config file configFile := filepath.Join(tmpDir, "config.yaml") diff --git a/config-example.yaml b/config-example.yaml index 8f6f01c3..40e5c8e4 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -224,43 +224,60 @@ policy: # - https://tailscale.com/kb/1081/magicdns/ # - https://tailscale.com/blog/2021-09-private-dns-with-magicdns/ # -dns_config: - # Whether to prefer using Headscale provided DNS or use local. - override_local_dns: true +# Please not that for the DNS configuration to have any effect, +# clients must have the `--accept-ds=true` option enabled. This is the +# default for the Tailscale client. This option is enabled by default +# in the Tailscale client. +# +# Setting _any_ of the configuration and `--accept-dns=true` on the +# clients will integrate with the DNS manager on the client or +# overwrite /etc/resolv.conf. +# https://tailscale.com/kb/1235/resolv-conf +# +# If you want stop Headscale from managing the DNS configuration +# all the fields under `dns` should be set to empty values. +dns: + # Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/). + # Only works if there is at least a nameserver defined. + magic_dns: true + + # Defines the base domain to create the hostnames for MagicDNS. + # This domain _must_ be different from the server_url domain. + # `base_domain` must be a FQDN, without the trailing dot. + # The FQDN of the hosts will be + # `hostname.base_domain` (e.g., _myhost.example.com_). + base_domain: example.com # List of DNS servers to expose to clients. nameservers: - - 1.1.1.1 + global: + - 1.1.1.1 + - 1.0.0.1 + - 2606:4700:4700::1111 + - 2606:4700:4700::1001 - # NextDNS (see https://tailscale.com/kb/1218/nextdns/). - # "abc123" is example NextDNS ID, replace with yours. - # - # With metadata sharing: - # nameservers: - # - https://dns.nextdns.io/abc123 - # - # Without metadata sharing: - # nameservers: - # - 2a07:a8c0::ab:c123 - # - 2a07:a8c1::ab:c123 + # NextDNS (see https://tailscale.com/kb/1218/nextdns/). + # "abc123" is example NextDNS ID, replace with yours. + # - https://dns.nextdns.io/abc123 - # Split DNS (see https://tailscale.com/kb/1054/dns/), - # list of search domains and the DNS to query for each one. - # - # restricted_nameservers: - # foo.bar.com: - # - 1.1.1.1 - # darp.headscale.net: - # - 1.1.1.1 - # - 8.8.8.8 + # Split DNS (see https://tailscale.com/kb/1054/dns/), + # a map of domains and which DNS server to use for each. + split: + {} + # foo.bar.com: + # - 1.1.1.1 + # darp.headscale.net: + # - 1.1.1.1 + # - 8.8.8.8 - # Search domains to inject. - domains: [] + # Set custom DNS search domains. With MagicDNS enabled, + # your tailnet base_domain is always the first search domain. + search_domains: [] # Extra DNS records # so far only A-records are supported (on the tailscale side) # See https://github.com/juanfont/headscale/blob/main/docs/dns-records.md#Limitations - # extra_records: + extra_records: [] # - name: "grafana.myvpn.example.com" # type: "A" # value: "100.64.0.3" @@ -268,10 +285,6 @@ dns_config: # # you can also put it in one line # - { name: "prometheus.myvpn.example.com", type: "A", value: "100.64.0.3" } - # Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/). - # Only works if there is at least a nameserver defined. - magic_dns: true - # DEPRECATED # Use the username as part of the DNS name for nodes, with this option enabled: # node1.username.example.com @@ -281,12 +294,6 @@ dns_config: # while in upstream Tailscale, the username is not included. use_username_in_magic_dns: false - # Defines the base domain to create the hostnames for MagicDNS. - # `base_domain` must be a FQDNs, without the trailing dot. - # The FQDN of the hosts will be - # `hostname.user.base_domain` (e.g., _myhost.myuser.example.com_). - base_domain: example.com - # Unix socket used for the CLI to connect without authentication # Note: for production you will want to set this to something like: unix_socket: /var/run/headscale/headscale.sock diff --git a/flake.nix b/flake.nix index ed4f24de..ab608439 100644 --- a/flake.nix +++ b/flake.nix @@ -31,7 +31,7 @@ # When updating go.mod or go.sum, a new sha will need to be calculated, # update this if you have a mismatch after doing a change to thos files. - vendorHash = "sha256-EorT2AVwA3usly/LcNor6r5UIhLCdj3L4O4ilgTIC2o="; + vendorHash = "sha256-08N9ZdUM3Lw0ad89Vpy01e/qJQoMRPj8n4Jd7Aecgjw="; subPackages = ["cmd/headscale"]; diff --git a/go.mod b/go.mod index e96bcc8a..71cd8c44 100644 --- a/go.mod +++ b/go.mod @@ -31,15 +31,15 @@ require ( github.com/samber/lo v1.39.0 github.com/sasha-s/go-deadlock v0.3.1 github.com/spf13/cobra v1.8.0 - github.com/spf13/viper v1.18.2 + github.com/spf13/viper v1.20.0-alpha.6 github.com/stretchr/testify v1.9.0 github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a github.com/tailscale/tailsql v0.0.0-20240418235827-820559f382c1 github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e go4.org/netipx v0.0.0-20231129151722-fdeea329fbba - golang.org/x/crypto v0.23.0 - golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 - golang.org/x/net v0.25.0 + golang.org/x/crypto v0.25.0 + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 + golang.org/x/net v0.27.0 golang.org/x/oauth2 v0.20.0 golang.org/x/sync v0.7.0 google.golang.org/genproto/googleapis/api v0.0.0-20240515191416-fc5f0ca64291 @@ -101,6 +101,7 @@ require ( github.com/go-jose/go-jose/v4 v4.0.1 // indirect github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-viper/mapstructure/v2 v2.0.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/v5 v5.2.1 // indirect @@ -117,7 +118,6 @@ require ( github.com/gorilla/csrf v1.7.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/hashicorp/go-version v1.6.0 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect github.com/hdevalence/ed25519consensus v0.2.0 // indirect github.com/illarion/gonotify v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -137,7 +137,6 @@ require ( github.com/kr/text v0.2.0 // indirect github.com/lib/pq v1.10.7 // 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.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect @@ -166,8 +165,7 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.12.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/sagikazarmark/locafero v0.6.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 @@ -195,16 +193,15 @@ require ( github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.uber.org/multierr v1.11.0 // indirect go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/term v0.20.0 // indirect - golang.org/x/text v0.15.0 // indirect + golang.org/x/mod v0.19.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/term v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.21.0 // indirect + golang.org/x/tools v0.23.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wireguard/windows v0.5.3 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3 // indirect modernc.org/libc v1.50.6 // indirect diff --git a/go.sum b/go.sum index a534a8e4..6bc69456 100644 --- a/go.sum +++ b/go.sum @@ -180,6 +180,8 @@ github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW 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/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc= +github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= @@ -240,11 +242,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1 github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= 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.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/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.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= @@ -312,8 +311,6 @@ 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.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/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -419,10 +416,8 @@ github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWR 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/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= +github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0= @@ -443,8 +438,8 @@ 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.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= -github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/spf13/viper v1.20.0-alpha.6 h1:f65Cr/+2qk4GfHC0xqT/isoupQppwN5+VLRztUGTDbY= +github.com/spf13/viper v1.20.0-alpha.6/go.mod h1:CGBZzv0c9fOUASm6rfus4wdeIjR/04NOLq1P4KRhX3k= 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= @@ -538,11 +533,11 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= -golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a h1:8qmSSA8Gz/1kTrCe0nqR0R3Gb/NDhykzWw2q2mWZydM= golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= @@ -555,8 +550,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= +golang.org/x/mod v0.19.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-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -569,8 +564,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= @@ -615,8 +610,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.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-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -624,8 +619,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -633,8 +628,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 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= @@ -648,8 +643,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= -golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= +golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= 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= @@ -681,8 +676,6 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 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/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/hscontrol/mapper/tail.go b/hscontrol/mapper/tail.go index 92fbed81..d21e4d8d 100644 --- a/hscontrol/mapper/tail.go +++ b/hscontrol/mapper/tail.go @@ -36,8 +36,7 @@ func tailNodes( return tNodes, nil } -// tailNode converts a Node into a Tailscale Node. includeRoutes is false for shared nodes -// as per the expected behaviour in the official SaaS. +// tailNode converts a Node into a Tailscale Node. func tailNode( node *types.Node, capVer tailcfg.CapabilityVersion, diff --git a/hscontrol/mapper/tail_test.go b/hscontrol/mapper/tail_test.go index 47af68fe..ac50d5a6 100644 --- a/hscontrol/mapper/tail_test.go +++ b/hscontrol/mapper/tail_test.go @@ -55,12 +55,14 @@ func TestTailNode(t *testing.T) { { name: "empty-node", node: &types.Node{ - Hostinfo: &tailcfg.Hostinfo{}, + GivenName: "empty", + Hostinfo: &tailcfg.Hostinfo{}, }, pol: &policy.ACLPolicy{}, dnsConfig: &tailcfg.DNSConfig{}, baseDomain: "", want: &tailcfg.Node{ + Name: "empty", StableID: "0", Addresses: []netip.Prefix{}, AllowedIPs: []netip.Prefix{}, diff --git a/hscontrol/noise.go b/hscontrol/noise.go index 360c7045..554be65c 100644 --- a/hscontrol/noise.go +++ b/hscontrol/noise.go @@ -166,7 +166,7 @@ func (ns *noiseServer) earlyNoise(protocolVersion int, writer io.Writer) error { } const ( - MinimumCapVersion tailcfg.CapabilityVersion = 58 + MinimumCapVersion tailcfg.CapabilityVersion = 61 ) // NoisePollNetMapHandler takes care of /machine/:id/map using the Noise protocol diff --git a/hscontrol/types/config.go b/hscontrol/types/config.go index 0c077870..e938768e 100644 --- a/hscontrol/types/config.go +++ b/hscontrol/types/config.go @@ -20,6 +20,7 @@ import ( "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" "tailscale.com/types/dnstype" + "tailscale.com/util/set" ) const ( @@ -88,6 +89,20 @@ type Config struct { Tuning Tuning } +type DNSConfig struct { + MagicDNS bool `mapstructure:"magic_dns"` + BaseDomain string `mapstructure:"base_domain"` + Nameservers Nameservers + SearchDomains []string `mapstructure:"search_domains"` + ExtraRecords []tailcfg.DNSRecord `mapstructure:"extra_records"` + UserNameInMagicDNS bool `mapstructure:"use_username_in_magic_dns"` +} + +type Nameservers struct { + Global []string + Split map[string][]string +} + type SqliteConfig struct { Path string WriteAheadLog bool @@ -201,7 +216,8 @@ func LoadConfig(path string, isFile bool) error { } } - viper.SetEnvPrefix("headscale") + envPrefix := "headscale" + viper.SetEnvPrefix(envPrefix) viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) viper.AutomaticEnv() @@ -213,9 +229,13 @@ func LoadConfig(path string, isFile bool) error { viper.SetDefault("log.level", "info") viper.SetDefault("log.format", TextLogFormat) - viper.SetDefault("dns_config", nil) - viper.SetDefault("dns_config.override_local_dns", true) - viper.SetDefault("dns_config.use_username_in_magic_dns", false) + viper.SetDefault("dns.magic_dns", true) + viper.SetDefault("dns.base_domain", "") + viper.SetDefault("dns.nameservers.global", []string{}) + viper.SetDefault("dns.nameservers.split", map[string]string{}) + viper.SetDefault("dns.search_domains", []string{}) + viper.SetDefault("dns.extra_records", []tailcfg.DNSRecord{}) + viper.SetDefault("dns.use_username_in_magic_dns", false) viper.SetDefault("derp.server.enabled", false) viper.SetDefault("derp.server.stun.enabled", true) @@ -259,17 +279,33 @@ func LoadConfig(path string, isFile bool) error { } if err := viper.ReadInConfig(); err != nil { - log.Warn().Err(err).Msg("Failed to read configuration from disk") - return fmt.Errorf("fatal error reading config file: %w", err) } + depr := deprecator{ + warns: make(set.Set[string]), + fatals: make(set.Set[string]), + } + // Register aliases for backward compatibility // Has to be called _after_ viper.ReadInConfig() // https://github.com/spf13/viper/issues/560 // Alias the old ACL Policy path with the new configuration option. - registerAliasAndDeprecate("policy.path", "acl_policy_path") + depr.warnWithAlias("policy.path", "acl_policy_path") + + // Move dns_config -> dns + depr.warn("dns_config.override_local_dns") + depr.fatalIfNewKeyIsNotUsed("dns.magic_dns", "dns_config.magic_dns") + depr.fatalIfNewKeyIsNotUsed("dns.base_domain", "dns_config.base_domain") + depr.fatalIfNewKeyIsNotUsed("dns.nameservers.global", "dns_config.nameservers") + depr.fatalIfNewKeyIsNotUsed("dns.nameservers.split", "dns_config.restricted_nameservers") + depr.fatalIfNewKeyIsNotUsed("dns.search_domains", "dns_config.domains") + depr.fatalIfNewKeyIsNotUsed("dns.extra_records", "dns_config.extra_records") + depr.warn("dns_config.use_username_in_magic_dns") + depr.warn("dns.use_username_in_magic_dns") + + depr.Log() // Collect any validation errors and return them all at once var errorText string @@ -485,123 +521,131 @@ func GetDatabaseConfig() DatabaseConfig { } } -func GetDNSConfig() (*tailcfg.DNSConfig, string) { - if viper.IsSet("dns_config") { - dnsConfig := &tailcfg.DNSConfig{} +func DNS() (DNSConfig, error) { + var dns DNSConfig - overrideLocalDNS := viper.GetBool("dns_config.override_local_dns") + // TODO: Use this instead of manually getting settings when + // UnmarshalKey is compatible with Environment Variables. + // err := viper.UnmarshalKey("dns", &dns) + // if err != nil { + // return DNSConfig{}, fmt.Errorf("unmarshaling dns config: %w", err) + // } - if viper.IsSet("dns_config.nameservers") { - nameserversStr := viper.GetStringSlice("dns_config.nameservers") + dns.MagicDNS = viper.GetBool("dns.magic_dns") + dns.BaseDomain = viper.GetString("dns.base_domain") + dns.Nameservers.Global = viper.GetStringSlice("dns.nameservers.global") + dns.Nameservers.Split = viper.GetStringMapStringSlice("dns.nameservers.split") + dns.SearchDomains = viper.GetStringSlice("dns.search_domains") - nameservers := []netip.Addr{} - resolvers := []*dnstype.Resolver{} + if viper.IsSet("dns.extra_records") { + var extraRecords []tailcfg.DNSRecord - for _, nameserverStr := range nameserversStr { - // Search for explicit DNS-over-HTTPS resolvers - if strings.HasPrefix(nameserverStr, "https://") { - resolvers = append(resolvers, &dnstype.Resolver{ - Addr: nameserverStr, - }) - - // This nameserver can not be parsed as an IP address - continue - } - - // Parse nameserver as a regular IP - nameserver, err := netip.ParseAddr(nameserverStr) - if err != nil { - log.Error(). - Str("func", "getDNSConfig"). - Err(err). - Msgf("Could not parse nameserver IP: %s", nameserverStr) - } - - nameservers = append(nameservers, nameserver) - resolvers = append(resolvers, &dnstype.Resolver{ - Addr: nameserver.String(), - }) - } - - dnsConfig.Nameservers = nameservers - - if overrideLocalDNS { - dnsConfig.Resolvers = resolvers - } else { - dnsConfig.FallbackResolvers = resolvers - } + err := viper.UnmarshalKey("dns.extra_records", &extraRecords) + if err != nil { + return DNSConfig{}, fmt.Errorf("unmarshaling dns extra records: %w", err) } - if viper.IsSet("dns_config.restricted_nameservers") { - dnsConfig.Routes = make(map[string][]*dnstype.Resolver) - domains := []string{} - restrictedDNS := viper.GetStringMapStringSlice( - "dns_config.restricted_nameservers", - ) - for domain, restrictedNameservers := range restrictedDNS { - restrictedResolvers := make( - []*dnstype.Resolver, - len(restrictedNameservers), - ) - for index, nameserverStr := range restrictedNameservers { - nameserver, err := netip.ParseAddr(nameserverStr) - if err != nil { - log.Error(). - Str("func", "getDNSConfig"). - Err(err). - Msgf("Could not parse restricted nameserver IP: %s", nameserverStr) - } - restrictedResolvers[index] = &dnstype.Resolver{ - Addr: nameserver.String(), - } - } - dnsConfig.Routes[domain] = restrictedResolvers - domains = append(domains, domain) - } - dnsConfig.Domains = domains - } - - if viper.IsSet("dns_config.extra_records") { - var extraRecords []tailcfg.DNSRecord - - err := viper.UnmarshalKey("dns_config.extra_records", &extraRecords) - if err != nil { - log.Error(). - Str("func", "getDNSConfig"). - Err(err). - Msgf("Could not parse dns_config.extra_records") - } - - dnsConfig.ExtraRecords = extraRecords - } - - if viper.IsSet("dns_config.magic_dns") { - dnsConfig.Proxied = viper.GetBool("dns_config.magic_dns") - } - - var baseDomain string - if viper.IsSet("dns_config.base_domain") { - baseDomain = viper.GetString("dns_config.base_domain") - } else { - baseDomain = "headscale.net" // does not really matter when MagicDNS is not enabled - } - - if !viper.GetBool("dns_config.use_username_in_magic_dns") { - dnsConfig.Domains = []string{baseDomain} - } else { - log.Warn().Msg("DNS: Usernames in DNS has been deprecated, this option will be remove in future versions") - log.Warn().Msg("DNS: see 0.23.0 changelog for more information.") - } - - if domains := viper.GetStringSlice("dns_config.domains"); len(domains) > 0 { - dnsConfig.Domains = append(dnsConfig.Domains, domains...) - } - - log.Trace().Interface("dns_config", dnsConfig).Msg("DNS configuration loaded") - return dnsConfig, baseDomain + dns.ExtraRecords = extraRecords } - return nil, "" + dns.UserNameInMagicDNS = viper.GetBool("dns.use_username_in_magic_dns") + + return dns, nil +} + +// GlobalResolvers returns the global DNS resolvers +// defined in the config file. +// If a nameserver is a valid IP, it will be used as a regular resolver. +// If a nameserver is a valid URL, it will be used as a DoH resolver. +// If a nameserver is neither a valid URL nor a valid IP, it will be ignored. +func (d *DNSConfig) GlobalResolvers() []*dnstype.Resolver { + var resolvers []*dnstype.Resolver + + for _, nsStr := range d.Nameservers.Global { + warn := "" + if _, err := netip.ParseAddr(nsStr); err == nil { + resolvers = append(resolvers, &dnstype.Resolver{ + Addr: nsStr, + }) + + continue + } else { + warn = fmt.Sprintf("Invalid global nameserver %q. Parsing error: %s ignoring", nsStr, err) + } + + if _, err := url.Parse(nsStr); err == nil { + resolvers = append(resolvers, &dnstype.Resolver{ + Addr: nsStr, + }) + } else { + warn = fmt.Sprintf("Invalid global nameserver %q. Parsing error: %s ignoring", nsStr, err) + } + + if warn != "" { + log.Warn().Msg(warn) + } + } + + return resolvers +} + +// SplitResolvers returns a map of domain to DNS resolvers. +// If a nameserver is a valid IP, it will be used as a regular resolver. +// If a nameserver is a valid URL, it will be used as a DoH resolver. +// If a nameserver is neither a valid URL nor a valid IP, it will be ignored. +func (d *DNSConfig) SplitResolvers() map[string][]*dnstype.Resolver { + routes := make(map[string][]*dnstype.Resolver) + for domain, nameservers := range d.Nameservers.Split { + var resolvers []*dnstype.Resolver + for _, nsStr := range nameservers { + warn := "" + if _, err := netip.ParseAddr(nsStr); err == nil { + resolvers = append(resolvers, &dnstype.Resolver{ + Addr: nsStr, + }) + + continue + } else { + warn = fmt.Sprintf("Invalid split dns nameserver %q. Parsing error: %s ignoring", nsStr, err) + } + + if _, err := url.Parse(nsStr); err == nil { + resolvers = append(resolvers, &dnstype.Resolver{ + Addr: nsStr, + }) + } else { + warn = fmt.Sprintf("Invalid split dns nameserver %q. Parsing error: %s ignoring", nsStr, err) + } + + if warn != "" { + log.Warn().Msg(warn) + } + } + routes[domain] = resolvers + } + + return routes +} + +func DNSToTailcfgDNS(dns DNSConfig) *tailcfg.DNSConfig { + cfg := tailcfg.DNSConfig{} + + if dns.BaseDomain == "" && dns.MagicDNS { + log.Fatal().Msg("dns.base_domain must be set when using MagicDNS (dns.magic_dns)") + } + + cfg.Proxied = dns.MagicDNS + cfg.ExtraRecords = dns.ExtraRecords + cfg.Resolvers = dns.GlobalResolvers() + + routes := dns.SplitResolvers() + cfg.Routes = routes + if dns.BaseDomain != "" { + cfg.Domains = []string{dns.BaseDomain} + } + cfg.Domains = append(cfg.Domains, dns.SearchDomains...) + + return &cfg } func PrefixV4() (*netip.Prefix, error) { @@ -693,7 +737,11 @@ func GetHeadscaleConfig() (*Config, error) { return nil, fmt.Errorf("config error, prefixes.allocation is set to %s, which is not a valid strategy, allowed options: %s, %s", allocStr, IPAllocationStrategySequential, IPAllocationStrategyRandom) } - dnsConfig, baseDomain := GetDNSConfig() + dnsConfig, err := DNS() + if err != nil { + return nil, err + } + derpConfig := GetDERPConfig() logTailConfig := GetLogTailConfig() randomizeClientPort := viper.GetBool("randomize_client_port") @@ -711,8 +759,23 @@ func GetHeadscaleConfig() (*Config, error) { oidcClientSecret = strings.TrimSpace(string(secretBytes)) } + serverURL := viper.GetString("server_url") + + // BaseDomain cannot be the same as the server URL. + // This is because Tailscale takes over the domain in BaseDomain, + // causing the headscale server and DERP to be unreachable. + // For Tailscale upstream, the following is true: + // - DERP run on their own domains + // - Control plane runs on login.tailscale.com/controlplane.tailscale.com + // - MagicDNS (BaseDomain) for users is on a *.ts.net domain per tailnet (e.g. tail-scale.ts.net) + // + // TODO(kradalby): remove dnsConfig.UserNameInMagicDNS check when removed. + if !dnsConfig.UserNameInMagicDNS && dnsConfig.BaseDomain != "" && strings.Contains(serverURL, dnsConfig.BaseDomain) { + return nil, errors.New("server_url cannot contain the base_domain, this will cause the headscale server and embedded DERP to become unreachable from the Tailscale node.") + } + return &Config{ - ServerURL: viper.GetString("server_url"), + ServerURL: serverURL, Addr: viper.GetString("listen_addr"), MetricsAddr: viper.GetString("metrics_listen_addr"), GRPCAddr: viper.GetString("grpc_listen_addr"), @@ -726,7 +789,7 @@ func GetHeadscaleConfig() (*Config, error) { NoisePrivateKeyPath: util.AbsolutePathFromConfigPath( viper.GetString("noise.private_key_path"), ), - BaseDomain: baseDomain, + BaseDomain: dnsConfig.BaseDomain, DERP: derpConfig, @@ -738,8 +801,8 @@ func GetHeadscaleConfig() (*Config, error) { TLS: GetTLSConfig(), - DNSConfig: dnsConfig, - DNSUserNameInMagicDNS: viper.GetBool("dns_config.use_username_in_magic_dns"), + DNSConfig: DNSToTailcfgDNS(dnsConfig), + DNSUserNameInMagicDNS: dnsConfig.UserNameInMagicDNS, ACMEEmail: viper.GetString("acme_email"), ACMEURL: viper.GetString("acme_url"), @@ -805,19 +868,70 @@ func IsCLIConfigured() bool { return viper.GetString("cli.address") != "" && viper.GetString("cli.api_key") != "" } -// registerAliasAndDeprecate will register an alias between the newKey and the oldKey, +type deprecator struct { + warns set.Set[string] + fatals set.Set[string] +} + +// warnWithAlias will register an alias between the newKey and the oldKey, // and log a deprecation warning if the oldKey is set. -func registerAliasAndDeprecate(newKey, oldKey string) { +func (d *deprecator) warnWithAlias(newKey, oldKey string) { // NOTE: RegisterAlias is called with NEW KEY -> OLD KEY viper.RegisterAlias(newKey, oldKey) if viper.IsSet(oldKey) { - log.Warn().Msgf("The %q configuration key is deprecated. Please use %q instead. %q will be removed in the future.", oldKey, newKey, oldKey) + d.warns.Add(fmt.Sprintf("The %q configuration key is deprecated. Please use %q instead. %q will be removed in the future.", oldKey, newKey, oldKey)) } } -// deprecateAndFatal will log a fatal deprecation warning if the oldKey is set. -func deprecateAndFatal(newKey, oldKey string) { +// fatal deprecates and adds an entry to the fatal list of options if the oldKey is set. +func (d *deprecator) fatal(newKey, oldKey string) { if viper.IsSet(oldKey) { - log.Fatal().Msgf("The %q configuration key is deprecated. Please use %q instead. %q has been removed.", oldKey, newKey, oldKey) + d.fatals.Add(fmt.Sprintf("The %q configuration key is deprecated. Please use %q instead. %q has been removed.", oldKey, newKey, oldKey)) + } +} + +// fatalIfNewKeyIsNotUsed deprecates and adds an entry to the fatal list of options if the oldKey is set and the new key is _not_ set. +// If the new key is set, a warning is emitted instead. +func (d *deprecator) fatalIfNewKeyIsNotUsed(newKey, oldKey string) { + if viper.IsSet(oldKey) && !viper.IsSet(newKey) { + d.fatals.Add(fmt.Sprintf("The %q configuration key is deprecated. Please use %q instead. %q has been removed.", oldKey, newKey, oldKey)) + } else if viper.IsSet(oldKey) { + d.warns.Add(fmt.Sprintf("The %q configuration key is deprecated. Please use %q instead. %q has been removed.", oldKey, newKey, oldKey)) + } +} + +// warn deprecates and adds an option to log a warning if the oldKey is set. +func (d *deprecator) warnNoAlias(newKey, oldKey string) { + if viper.IsSet(oldKey) { + d.warns.Add(fmt.Sprintf("The %q configuration key is deprecated. Please use %q instead. %q has been removed.", oldKey, newKey, oldKey)) + } +} + +// warn deprecates and adds an entry to the warn list of options if the oldKey is set. +func (d *deprecator) warn(oldKey string) { + if viper.IsSet(oldKey) { + d.warns.Add(fmt.Sprintf("The %q configuration key is deprecated and has been removed. Please see the changelog for more details.", oldKey)) + } +} + +func (d *deprecator) String() string { + var b strings.Builder + + for _, w := range d.warns.Slice() { + fmt.Fprintf(&b, "WARN: %s\n", w) + } + + for _, f := range d.fatals.Slice() { + fmt.Fprintf(&b, "FATAL: %s\n", f) + } + + return b.String() +} + +func (d *deprecator) Log() { + if len(d.fatals) > 0 { + log.Fatal().Msg("\n" + d.String()) + } else if len(d.warns) > 0 { + log.Warn().Msg("\n" + d.String()) } } diff --git a/hscontrol/types/config_test.go b/hscontrol/types/config_test.go new file mode 100644 index 00000000..7cf562b1 --- /dev/null +++ b/hscontrol/types/config_test.go @@ -0,0 +1,272 @@ +package types + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "tailscale.com/tailcfg" + "tailscale.com/types/dnstype" +) + +func TestReadConfig(t *testing.T) { + tests := []struct { + name string + configPath string + setup func(*testing.T) (any, error) + want any + wantErr string + }{ + { + name: "unmarshal-dns-full-config", + configPath: "testdata/dns_full.yaml", + setup: func(t *testing.T) (any, error) { + dns, err := DNS() + if err != nil { + return nil, err + } + + return dns, nil + }, + want: DNSConfig{ + MagicDNS: true, + BaseDomain: "example.com", + Nameservers: Nameservers{ + Global: []string{"1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001", "https://dns.nextdns.io/abc123"}, + Split: map[string][]string{"darp.headscale.net": {"1.1.1.1", "8.8.8.8"}, "foo.bar.com": {"1.1.1.1"}}, + }, + ExtraRecords: []tailcfg.DNSRecord{ + {Name: "grafana.myvpn.example.com", Type: "A", Value: "100.64.0.3"}, + {Name: "prometheus.myvpn.example.com", Type: "A", Value: "100.64.0.4"}, + }, + SearchDomains: []string{"test.com", "bar.com"}, + UserNameInMagicDNS: true, + }, + }, + { + name: "dns-to-tailcfg.DNSConfig", + configPath: "testdata/dns_full.yaml", + setup: func(t *testing.T) (any, error) { + dns, err := DNS() + if err != nil { + return nil, err + } + + return DNSToTailcfgDNS(dns), nil + }, + want: &tailcfg.DNSConfig{ + Proxied: true, + Domains: []string{"example.com", "test.com", "bar.com"}, + Resolvers: []*dnstype.Resolver{ + {Addr: "1.1.1.1"}, + {Addr: "1.0.0.1"}, + {Addr: "2606:4700:4700::1111"}, + {Addr: "2606:4700:4700::1001"}, + {Addr: "https://dns.nextdns.io/abc123"}, + }, + Routes: map[string][]*dnstype.Resolver{ + "darp.headscale.net": {{Addr: "1.1.1.1"}, {Addr: "8.8.8.8"}}, + "foo.bar.com": {{Addr: "1.1.1.1"}}, + }, + ExtraRecords: []tailcfg.DNSRecord{ + {Name: "grafana.myvpn.example.com", Type: "A", Value: "100.64.0.3"}, + {Name: "prometheus.myvpn.example.com", Type: "A", Value: "100.64.0.4"}, + }, + }, + }, + { + name: "unmarshal-dns-full-no-magic", + configPath: "testdata/dns_full_no_magic.yaml", + setup: func(t *testing.T) (any, error) { + dns, err := DNS() + if err != nil { + return nil, err + } + + return dns, nil + }, + want: DNSConfig{ + MagicDNS: false, + BaseDomain: "example.com", + Nameservers: Nameservers{ + Global: []string{"1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001", "https://dns.nextdns.io/abc123"}, + Split: map[string][]string{"darp.headscale.net": {"1.1.1.1", "8.8.8.8"}, "foo.bar.com": {"1.1.1.1"}}, + }, + ExtraRecords: []tailcfg.DNSRecord{ + {Name: "grafana.myvpn.example.com", Type: "A", Value: "100.64.0.3"}, + {Name: "prometheus.myvpn.example.com", Type: "A", Value: "100.64.0.4"}, + }, + SearchDomains: []string{"test.com", "bar.com"}, + UserNameInMagicDNS: true, + }, + }, + { + name: "dns-to-tailcfg.DNSConfig", + configPath: "testdata/dns_full_no_magic.yaml", + setup: func(t *testing.T) (any, error) { + dns, err := DNS() + if err != nil { + return nil, err + } + + return DNSToTailcfgDNS(dns), nil + }, + want: &tailcfg.DNSConfig{ + Proxied: false, + Domains: []string{"example.com", "test.com", "bar.com"}, + Resolvers: []*dnstype.Resolver{ + {Addr: "1.1.1.1"}, + {Addr: "1.0.0.1"}, + {Addr: "2606:4700:4700::1111"}, + {Addr: "2606:4700:4700::1001"}, + {Addr: "https://dns.nextdns.io/abc123"}, + }, + Routes: map[string][]*dnstype.Resolver{ + "darp.headscale.net": {{Addr: "1.1.1.1"}, {Addr: "8.8.8.8"}}, + "foo.bar.com": {{Addr: "1.1.1.1"}}, + }, + ExtraRecords: []tailcfg.DNSRecord{ + {Name: "grafana.myvpn.example.com", Type: "A", Value: "100.64.0.3"}, + {Name: "prometheus.myvpn.example.com", Type: "A", Value: "100.64.0.4"}, + }, + }, + }, + { + name: "base-domain-in-server-url-err", + configPath: "testdata/base-domain-in-server-url.yaml", + setup: func(t *testing.T) (any, error) { + return GetHeadscaleConfig() + }, + want: nil, + wantErr: "server_url cannot contain the base_domain, this will cause the headscale server and embedded DERP to become unreachable from the Tailscale node.", + }, + { + name: "base-domain-not-in-server-url", + configPath: "testdata/base-domain-not-in-server-url.yaml", + setup: func(t *testing.T) (any, error) { + cfg, err := GetHeadscaleConfig() + if err != nil { + return nil, err + } + + return map[string]string{ + "server_url": cfg.ServerURL, + "base_domain": cfg.BaseDomain, + }, err + }, + want: map[string]string{ + "server_url": "https://derp.no", + "base_domain": "clients.derp.no", + }, + wantErr: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + viper.Reset() + err := LoadConfig(tt.configPath, true) + assert.NoError(t, err) + + conf, err := tt.setup(t) + + if tt.wantErr != "" { + assert.Equal(t, tt.wantErr, err.Error()) + + return + } + + assert.NoError(t, err) + + if diff := cmp.Diff(tt.want, conf); diff != "" { + t.Errorf("ReadConfig() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestReadConfigFromEnv(t *testing.T) { + tests := []struct { + name string + configEnv map[string]string + setup func(*testing.T) (any, error) + want any + }{ + { + name: "test-random-base-settings-with-env", + configEnv: map[string]string{ + "HEADSCALE_LOG_LEVEL": "trace", + "HEADSCALE_DATABASE_SQLITE_WRITE_AHEAD_LOG": "false", + "HEADSCALE_PREFIXES_V4": "100.64.0.0/10", + }, + setup: func(t *testing.T) (any, error) { + t.Logf("all settings: %#v", viper.AllSettings()) + + assert.Equal(t, "trace", viper.GetString("log.level")) + assert.Equal(t, "100.64.0.0/10", viper.GetString("prefixes.v4")) + assert.False(t, viper.GetBool("database.sqlite.write_ahead_log")) + return nil, nil + }, + want: nil, + }, + { + name: "unmarshal-dns-full-config", + configEnv: map[string]string{ + "HEADSCALE_DNS_MAGIC_DNS": "true", + "HEADSCALE_DNS_BASE_DOMAIN": "example.com", + "HEADSCALE_DNS_NAMESERVERS_GLOBAL": `1.1.1.1 8.8.8.8`, + "HEADSCALE_DNS_SEARCH_DOMAINS": "test.com bar.com", + "HEADSCALE_DNS_USE_USERNAME_IN_MAGIC_DNS": "true", + + // TODO(kradalby): Figure out how to pass these as env vars + // "HEADSCALE_DNS_NAMESERVERS_SPLIT": `{foo.bar.com: ["1.1.1.1"]}`, + // "HEADSCALE_DNS_EXTRA_RECORDS": `[{ name: "prometheus.myvpn.example.com", type: "A", value: "100.64.0.4" }]`, + }, + setup: func(t *testing.T) (any, error) { + t.Logf("all settings: %#v", viper.AllSettings()) + + dns, err := DNS() + if err != nil { + return nil, err + } + + return dns, nil + }, + want: DNSConfig{ + MagicDNS: true, + BaseDomain: "example.com", + Nameservers: Nameservers{ + Global: []string{"1.1.1.1", "8.8.8.8"}, + Split: map[string][]string{ + // "foo.bar.com": {"1.1.1.1"}, + }, + }, + ExtraRecords: []tailcfg.DNSRecord{ + // {Name: "prometheus.myvpn.example.com", Type: "A", Value: "100.64.0.4"}, + }, + SearchDomains: []string{"test.com", "bar.com"}, + UserNameInMagicDNS: true, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for k, v := range tt.configEnv { + t.Setenv(k, v) + } + + viper.Reset() + err := LoadConfig("testdata/minimal.yaml", true) + assert.NoError(t, err) + + conf, err := tt.setup(t) + assert.NoError(t, err) + + if diff := cmp.Diff(tt.want, conf); diff != "" { + t.Errorf("ReadConfig() mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/hscontrol/types/node.go b/hscontrol/types/node.go index 24e36535..04ca9f8d 100644 --- a/hscontrol/types/node.go +++ b/hscontrol/types/node.go @@ -394,40 +394,39 @@ func (node *Node) Proto() *v1.Node { } func (node *Node) GetFQDN(cfg *Config, baseDomain string) (string, error) { - var hostname string - if cfg.DNSConfig != nil && cfg.DNSConfig.Proxied { // MagicDNS - if node.GivenName == "" { - return "", fmt.Errorf("failed to create valid FQDN: %w", ErrNodeHasNoGivenName) - } + if node.GivenName == "" { + return "", fmt.Errorf("failed to create valid FQDN: %w", ErrNodeHasNoGivenName) + } + hostname := node.GivenName + + if baseDomain != "" { hostname = fmt.Sprintf( "%s.%s", node.GivenName, baseDomain, ) + } - if cfg.DNSUserNameInMagicDNS { - if node.User.Name == "" { - return "", fmt.Errorf("failed to create valid FQDN: %w", ErrNodeUserHasNoName) - } - - hostname = fmt.Sprintf( - "%s.%s.%s", - node.GivenName, - node.User.Name, - baseDomain, - ) + if cfg.DNSUserNameInMagicDNS { + if node.User.Name == "" { + return "", fmt.Errorf("failed to create valid FQDN: %w", ErrNodeUserHasNoName) } - if len(hostname) > MaxHostnameLength { - return "", fmt.Errorf( - "failed to create valid FQDN (%s): %w", - hostname, - ErrHostnameTooLong, - ) - } - } else { - hostname = node.GivenName + hostname = fmt.Sprintf( + "%s.%s.%s", + node.GivenName, + node.User.Name, + baseDomain, + ) + } + + if len(hostname) > MaxHostnameLength { + return "", fmt.Errorf( + "failed to create valid FQDN (%s): %w", + hostname, + ErrHostnameTooLong, + ) } return hostname, nil diff --git a/hscontrol/types/node_test.go b/hscontrol/types/node_test.go index 798a54d3..885edf5d 100644 --- a/hscontrol/types/node_test.go +++ b/hscontrol/types/node_test.go @@ -195,7 +195,7 @@ func TestNodeFQDN(t *testing.T) { DNSUserNameInMagicDNS: true, }, domain: "example.com", - want: "test", + want: "test.user.example.com", }, { name: "no-dnsconfig-with-username", @@ -206,7 +206,7 @@ func TestNodeFQDN(t *testing.T) { }, }, domain: "example.com", - want: "test", + want: "test.example.com", }, { name: "all-set", @@ -271,7 +271,7 @@ func TestNodeFQDN(t *testing.T) { DNSUserNameInMagicDNS: false, }, domain: "example.com", - want: "test", + want: "test.example.com", }, { name: "no-dnsconfig", @@ -282,7 +282,7 @@ func TestNodeFQDN(t *testing.T) { }, }, domain: "example.com", - want: "test", + want: "test.example.com", }, } diff --git a/hscontrol/types/testdata/base-domain-in-server-url.yaml b/hscontrol/types/testdata/base-domain-in-server-url.yaml new file mode 100644 index 00000000..683e0218 --- /dev/null +++ b/hscontrol/types/testdata/base-domain-in-server-url.yaml @@ -0,0 +1,16 @@ +noise: + private_key_path: "private_key.pem" + +prefixes: + v6: fd7a:115c:a1e0::/48 + v4: 100.64.0.0/10 + +database: + type: sqlite3 + +server_url: "https://derp.no" + +dns: + magic_dns: true + base_domain: derp.no + use_username_in_magic_dns: false diff --git a/hscontrol/types/testdata/base-domain-not-in-server-url.yaml b/hscontrol/types/testdata/base-domain-not-in-server-url.yaml new file mode 100644 index 00000000..3af345e1 --- /dev/null +++ b/hscontrol/types/testdata/base-domain-not-in-server-url.yaml @@ -0,0 +1,16 @@ +noise: + private_key_path: "private_key.pem" + +prefixes: + v6: fd7a:115c:a1e0::/48 + v4: 100.64.0.0/10 + +database: + type: sqlite3 + +server_url: "https://derp.no" + +dns: + magic_dns: true + base_domain: clients.derp.no + use_username_in_magic_dns: false diff --git a/hscontrol/types/testdata/dns_full.yaml b/hscontrol/types/testdata/dns_full.yaml new file mode 100644 index 00000000..c47e7b0f --- /dev/null +++ b/hscontrol/types/testdata/dns_full.yaml @@ -0,0 +1,37 @@ +# minimum to not fatal +noise: + private_key_path: "private_key.pem" +server_url: "https://derp.no" + +dns: + magic_dns: true + base_domain: example.com + + nameservers: + global: + - 1.1.1.1 + - 1.0.0.1 + - 2606:4700:4700::1111 + - 2606:4700:4700::1001 + - https://dns.nextdns.io/abc123 + + split: + foo.bar.com: + - 1.1.1.1 + darp.headscale.net: + - 1.1.1.1 + - 8.8.8.8 + + search_domains: + - test.com + - bar.com + + extra_records: + - name: "grafana.myvpn.example.com" + type: "A" + value: "100.64.0.3" + + # you can also put it in one line + - { name: "prometheus.myvpn.example.com", type: "A", value: "100.64.0.4" } + + use_username_in_magic_dns: true diff --git a/hscontrol/types/testdata/dns_full_no_magic.yaml b/hscontrol/types/testdata/dns_full_no_magic.yaml new file mode 100644 index 00000000..ac3cc470 --- /dev/null +++ b/hscontrol/types/testdata/dns_full_no_magic.yaml @@ -0,0 +1,37 @@ +# minimum to not fatal +noise: + private_key_path: "private_key.pem" +server_url: "https://derp.no" + +dns: + magic_dns: false + base_domain: example.com + + nameservers: + global: + - 1.1.1.1 + - 1.0.0.1 + - 2606:4700:4700::1111 + - 2606:4700:4700::1001 + - https://dns.nextdns.io/abc123 + + split: + foo.bar.com: + - 1.1.1.1 + darp.headscale.net: + - 1.1.1.1 + - 8.8.8.8 + + search_domains: + - test.com + - bar.com + + extra_records: + - name: "grafana.myvpn.example.com" + type: "A" + value: "100.64.0.3" + + # you can also put it in one line + - { name: "prometheus.myvpn.example.com", type: "A", value: "100.64.0.4" } + + use_username_in_magic_dns: true diff --git a/hscontrol/types/testdata/minimal.yaml b/hscontrol/types/testdata/minimal.yaml new file mode 100644 index 00000000..1d9b1e00 --- /dev/null +++ b/hscontrol/types/testdata/minimal.yaml @@ -0,0 +1,3 @@ +noise: + private_key_path: "private_key.pem" +server_url: "https://derp.no" diff --git a/integration/dns_test.go b/integration/dns_test.go new file mode 100644 index 00000000..60f05199 --- /dev/null +++ b/integration/dns_test.go @@ -0,0 +1,246 @@ +package integration + +import ( + "fmt" + "strings" + "testing" + "time" + + "github.com/juanfont/headscale/integration/hsic" + "github.com/juanfont/headscale/integration/tsic" + "github.com/stretchr/testify/assert" +) + +func TestResolveMagicDNS(t *testing.T) { + IntegrationSkip(t) + t.Parallel() + + scenario, err := NewScenario(dockertestMaxWait()) + assertNoErr(t, err) + defer scenario.Shutdown() + + spec := map[string]int{ + "magicdns1": len(MustTestVersions), + "magicdns2": len(MustTestVersions), + } + + err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("magicdns")) + assertNoErrHeadscaleEnv(t, err) + + allClients, err := scenario.ListTailscaleClients() + assertNoErrListClients(t, err) + + err = scenario.WaitForTailscaleSync() + assertNoErrSync(t, err) + + // assertClientsState(t, allClients) + + // Poor mans cache + _, err = scenario.ListTailscaleClientsFQDNs() + assertNoErrListFQDN(t, err) + + _, err = scenario.ListTailscaleClientsIPs() + assertNoErrListClientIPs(t, err) + + for _, client := range allClients { + for _, peer := range allClients { + // It is safe to ignore this error as we handled it when caching it + peerFQDN, _ := peer.FQDN() + + assert.Equal(t, fmt.Sprintf("%s.headscale.net", peer.Hostname()), peerFQDN) + + command := []string{ + "tailscale", + "ip", peerFQDN, + } + result, _, err := client.Execute(command) + if err != nil { + t.Fatalf( + "failed to execute resolve/ip command %s from %s: %s", + peerFQDN, + client.Hostname(), + err, + ) + } + + ips, err := peer.IPs() + if err != nil { + t.Fatalf( + "failed to get ips for %s: %s", + peer.Hostname(), + err, + ) + } + + for _, ip := range ips { + if !strings.Contains(result, ip.String()) { + t.Fatalf("ip %s is not found in \n%s\n", ip.String(), result) + } + } + } + } +} + +// TestValidateResolvConf validates that the resolv.conf file +// ends up as expected in our Tailscale containers. +// All the containers are based on Alpine, meaning Tailscale +// will overwrite the resolv.conf file. +// On other platform, Tailscale will integrate with a dns manager +// if available (like Systemd-Resolved). +func TestValidateResolvConf(t *testing.T) { + IntegrationSkip(t) + + resolvconf := func(conf string) string { + return strings.ReplaceAll(`# resolv.conf(5) file generated by tailscale +# For more info, see https://tailscale.com/s/resolvconf-overwrite +# DO NOT EDIT THIS FILE BY HAND -- CHANGES WILL BE OVERWRITTEN +`+conf, "\t", "") + } + + tests := []struct { + name string + conf map[string]string + wantConfCompareFunc func(*testing.T, string) + }{ + // New config + { + name: "no-config", + conf: map[string]string{ + "HEADSCALE_DNS_BASE_DOMAIN": "", + "HEADSCALE_DNS_MAGIC_DNS": "false", + "HEADSCALE_DNS_NAMESERVERS_GLOBAL": "", + }, + wantConfCompareFunc: func(t *testing.T, got string) { + assert.NotContains(t, got, "100.100.100.100") + }, + }, + { + name: "global-only", + conf: map[string]string{ + "HEADSCALE_DNS_BASE_DOMAIN": "", + "HEADSCALE_DNS_MAGIC_DNS": "false", + "HEADSCALE_DNS_NAMESERVERS_GLOBAL": "8.8.8.8 1.1.1.1", + }, + wantConfCompareFunc: func(t *testing.T, got string) { + want := resolvconf(` + nameserver 100.100.100.100 + `) + assert.Equal(t, want, got) + }, + }, + { + name: "base-integration-config", + conf: map[string]string{ + "HEADSCALE_DNS_BASE_DOMAIN": "very-unique-domain.net", + }, + wantConfCompareFunc: func(t *testing.T, got string) { + want := resolvconf(` + nameserver 100.100.100.100 + search very-unique-domain.net + `) + assert.Equal(t, want, got) + }, + }, + { + name: "base-magic-dns-off", + conf: map[string]string{ + "HEADSCALE_DNS_MAGIC_DNS": "false", + "HEADSCALE_DNS_BASE_DOMAIN": "very-unique-domain.net", + }, + wantConfCompareFunc: func(t *testing.T, got string) { + want := resolvconf(` + nameserver 100.100.100.100 + search very-unique-domain.net + `) + assert.Equal(t, want, got) + }, + }, + { + name: "base-extra-search-domains", + conf: map[string]string{ + "HEADSCALE_DNS_SEARCH_DOMAINS": "test1.no test2.no", + "HEADSCALE_DNS_BASE_DOMAIN": "with-local-dns.net", + }, + wantConfCompareFunc: func(t *testing.T, got string) { + want := resolvconf(` + nameserver 100.100.100.100 + search with-local-dns.net test1.no test2.no + `) + assert.Equal(t, want, got) + }, + }, + { + name: "base-nameservers-split", + conf: map[string]string{ + "HEADSCALE_DNS_NAMESERVERS_SPLIT": `{foo.bar.com: ["1.1.1.1"]}`, + "HEADSCALE_DNS_BASE_DOMAIN": "with-local-dns.net", + }, + wantConfCompareFunc: func(t *testing.T, got string) { + want := resolvconf(` + nameserver 100.100.100.100 + search with-local-dns.net + `) + assert.Equal(t, want, got) + }, + }, + { + name: "base-full-no-magic", + conf: map[string]string{ + "HEADSCALE_DNS_MAGIC_DNS": "false", + "HEADSCALE_DNS_BASE_DOMAIN": "all-of.it", + "HEADSCALE_DNS_NAMESERVERS_GLOBAL": `8.8.8.8`, + "HEADSCALE_DNS_SEARCH_DOMAINS": "test1.no test2.no", + // TODO(kradalby): this currently isnt working, need to fix it + // "HEADSCALE_DNS_NAMESERVERS_SPLIT": `{foo.bar.com: ["1.1.1.1"]}`, + // "HEADSCALE_DNS_EXTRA_RECORDS": `[{ name: "prometheus.myvpn.example.com", type: "A", value: "100.64.0.4" }]`, + }, + wantConfCompareFunc: func(t *testing.T, got string) { + want := resolvconf(` + nameserver 100.100.100.100 + search all-of.it test1.no test2.no + `) + assert.Equal(t, want, got) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + scenario, err := NewScenario(dockertestMaxWait()) + assertNoErr(t, err) + defer scenario.Shutdown() + + spec := map[string]int{ + "resolvconf1": 3, + "resolvconf2": 3, + } + + err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("resolvconf"), hsic.WithConfigEnv(tt.conf)) + assertNoErrHeadscaleEnv(t, err) + + allClients, err := scenario.ListTailscaleClients() + assertNoErrListClients(t, err) + + err = scenario.WaitForTailscaleSync() + assertNoErrSync(t, err) + + // Poor mans cache + _, err = scenario.ListTailscaleClientsFQDNs() + assertNoErrListFQDN(t, err) + + _, err = scenario.ListTailscaleClientsIPs() + assertNoErrListClientIPs(t, err) + + time.Sleep(30 * time.Second) + + for _, client := range allClients { + b, err := client.ReadFile("/etc/resolv.conf") + assertNoErr(t, err) + + t.Logf("comparing resolv conf of %s", client.Hostname()) + tt.wantConfCompareFunc(t, string(b)) + } + }) + } + +} diff --git a/integration/general_test.go b/integration/general_test.go index c17b977e..2819edb2 100644 --- a/integration/general_test.go +++ b/integration/general_test.go @@ -623,74 +623,6 @@ func TestTaildrop(t *testing.T) { } } -func TestResolveMagicDNS(t *testing.T) { - IntegrationSkip(t) - t.Parallel() - - scenario, err := NewScenario(dockertestMaxWait()) - assertNoErr(t, err) - defer scenario.Shutdown() - - spec := map[string]int{ - "magicdns1": len(MustTestVersions), - "magicdns2": len(MustTestVersions), - } - - err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("magicdns")) - assertNoErrHeadscaleEnv(t, err) - - allClients, err := scenario.ListTailscaleClients() - assertNoErrListClients(t, err) - - err = scenario.WaitForTailscaleSync() - assertNoErrSync(t, err) - - // assertClientsState(t, allClients) - - // Poor mans cache - _, err = scenario.ListTailscaleClientsFQDNs() - assertNoErrListFQDN(t, err) - - _, err = scenario.ListTailscaleClientsIPs() - assertNoErrListClientIPs(t, err) - - for _, client := range allClients { - for _, peer := range allClients { - // It is safe to ignore this error as we handled it when caching it - peerFQDN, _ := peer.FQDN() - - command := []string{ - "tailscale", - "ip", peerFQDN, - } - result, _, err := client.Execute(command) - if err != nil { - t.Fatalf( - "failed to execute resolve/ip command %s from %s: %s", - peerFQDN, - client.Hostname(), - err, - ) - } - - ips, err := peer.IPs() - if err != nil { - t.Fatalf( - "failed to get ips for %s: %s", - peer.Hostname(), - err, - ) - } - - for _, ip := range ips { - if !strings.Contains(result, ip.String()) { - t.Fatalf("ip %s is not found in \n%s\n", ip.String(), result) - } - } - } - } -} - func TestExpireNode(t *testing.T) { IntegrationSkip(t) t.Parallel() diff --git a/integration/hsic/config.go b/integration/hsic/config.go index 7953799e..c4d8b283 100644 --- a/integration/hsic/config.go +++ b/integration/hsic/config.go @@ -2,104 +2,6 @@ package hsic import "github.com/juanfont/headscale/hscontrol/types" -// const ( -// defaultEphemeralNodeInactivityTimeout = time.Second * 30 -// defaultNodeUpdateCheckInterval = time.Second * 10 -// ) - -// TODO(kradalby): This approach doesnt work because we cannot -// serialise our config object to YAML or JSON. -// func DefaultConfig() headscale.Config { -// derpMap, _ := url.Parse("https://controlplane.tailscale.com/derpmap/default") -// -// config := headscale.Config{ -// Log: headscale.LogConfig{ -// Level: zerolog.TraceLevel, -// }, -// ACL: headscale.GetACLConfig(), -// DBtype: "sqlite3", -// EphemeralNodeInactivityTimeout: defaultEphemeralNodeInactivityTimeout, -// NodeUpdateCheckInterval: defaultNodeUpdateCheckInterval, -// IPPrefixes: []netip.Prefix{ -// netip.MustParsePrefix("fd7a:115c:a1e0::/48"), -// netip.MustParsePrefix("100.64.0.0/10"), -// }, -// DNSConfig: &tailcfg.DNSConfig{ -// Proxied: true, -// Nameservers: []netip.Addr{ -// netip.MustParseAddr("127.0.0.11"), -// netip.MustParseAddr("1.1.1.1"), -// }, -// Resolvers: []*dnstype.Resolver{ -// { -// Addr: "127.0.0.11", -// }, -// { -// Addr: "1.1.1.1", -// }, -// }, -// }, -// BaseDomain: "headscale.net", -// -// DBpath: "/tmp/integration_test_db.sqlite3", -// -// PrivateKeyPath: "/tmp/integration_private.key", -// NoisePrivateKeyPath: "/tmp/noise_integration_private.key", -// Addr: "0.0.0.0:8080", -// MetricsAddr: "127.0.0.1:9090", -// ServerURL: "http://headscale:8080", -// -// DERP: headscale.DERPConfig{ -// URLs: []url.URL{ -// *derpMap, -// }, -// AutoUpdate: false, -// UpdateFrequency: 1 * time.Minute, -// }, -// } -// -// return config -// } - -// TODO: Reuse the actual configuration object above. -// Deprecated: use env function instead as it is easier to -// override. -func DefaultConfigYAML() string { - yaml := ` -log: - level: trace -acl_policy_path: "" -database: - type: sqlite3 - sqlite.path: /tmp/integration_test_db.sqlite3 -ephemeral_node_inactivity_timeout: 30m -prefixes: - v6: fd7a:115c:a1e0::/48 - v4: 100.64.0.0/10 -dns_config: - base_domain: headscale.net - magic_dns: true - domains: [] - nameservers: - - 127.0.0.11 - - 1.1.1.1 -private_key_path: /tmp/private.key -noise: - private_key_path: /tmp/noise_private.key -listen_addr: 0.0.0.0:8080 -metrics_listen_addr: 127.0.0.1:9090 -server_url: http://headscale:8080 - -derp: - urls: - - https://controlplane.tailscale.com/derpmap/default - auto_update_enabled: false - update_frequency: 1m -` - - return yaml -} - func MinimumConfigYAML() string { return ` private_key_path: /tmp/private.key @@ -117,10 +19,9 @@ func DefaultConfigEnv() map[string]string { "HEADSCALE_EPHEMERAL_NODE_INACTIVITY_TIMEOUT": "30m", "HEADSCALE_PREFIXES_V4": "100.64.0.0/10", "HEADSCALE_PREFIXES_V6": "fd7a:115c:a1e0::/48", - "HEADSCALE_DNS_CONFIG_BASE_DOMAIN": "headscale.net", - "HEADSCALE_DNS_CONFIG_MAGIC_DNS": "true", - "HEADSCALE_DNS_CONFIG_DOMAINS": "", - "HEADSCALE_DNS_CONFIG_NAMESERVERS": "127.0.0.11 1.1.1.1", + "HEADSCALE_DNS_BASE_DOMAIN": "headscale.net", + "HEADSCALE_DNS_MAGIC_DNS": "true", + "HEADSCALE_DNS_NAMESERVERS_GLOBAL": "127.0.0.11 1.1.1.1", "HEADSCALE_PRIVATE_KEY_PATH": "/tmp/private.key", "HEADSCALE_NOISE_PRIVATE_KEY_PATH": "/tmp/noise_private.key", "HEADSCALE_LISTEN_ADDR": "0.0.0.0:8080", diff --git a/integration/scenario.go b/integration/scenario.go index bd004247..6476fd58 100644 --- a/integration/scenario.go +++ b/integration/scenario.go @@ -51,6 +51,8 @@ var ( tailscaleVersions2021 = map[string]bool{ "head": true, "unstable": true, + "1.70": true, // CapVer: not checked + "1.68": true, // CapVer: not checked "1.66": true, // CapVer: not checked "1.64": true, // CapVer: not checked "1.62": true, // CapVer: not checked @@ -62,10 +64,10 @@ var ( "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, // Oldest supported version, CapVer: 58 + "1.44": false, // CapVer: 63 + "1.42": false, // Oldest supported version, CapVer: 61 + "1.40": false, // CapVer: 61 + "1.38": false, // CapVer: 58 "1.36": false, // CapVer: 56 "1.34": false, // CapVer: 51 "1.32": false, // CapVer: 46 diff --git a/integration/tailscale.go b/integration/tailscale.go index 2ea3faa9..5b1baf1b 100644 --- a/integration/tailscale.go +++ b/integration/tailscale.go @@ -36,6 +36,7 @@ type TailscaleClient interface { Ping(hostnameOrIP string, opts ...tsic.PingOption) error Curl(url string, opts ...tsic.CurlOption) (string, error) ID() string + ReadFile(path string) ([]byte, error) // FailingPeersAsString returns a formatted-ish multi-line-string of peers in the client // and a bool indicating if the clients online count and peer count is equal. diff --git a/integration/tsic/tsic.go b/integration/tsic/tsic.go index 0e3c91f8..e1045ec3 100644 --- a/integration/tsic/tsic.go +++ b/integration/tsic/tsic.go @@ -1,6 +1,8 @@ package tsic import ( + "archive/tar" + "bytes" "context" "encoding/json" "errors" @@ -998,3 +1000,41 @@ func (t *TailscaleInContainer) WriteFile(path string, data []byte) error { func (t *TailscaleInContainer) SaveLog(path string) error { return dockertestutil.SaveLog(t.pool, t.container, path) } + +// ReadFile reads a file from the Tailscale container. +// It returns the content of the file as a byte slice. +func (t *TailscaleInContainer) ReadFile(path string) ([]byte, error) { + tarBytes, err := integrationutil.FetchPathFromContainer(t.pool, t.container, path) + if err != nil { + return nil, fmt.Errorf("reading file from container: %w", err) + } + + var out bytes.Buffer + tr := tar.NewReader(bytes.NewReader(tarBytes)) + for { + hdr, err := tr.Next() + if err == io.EOF { + break // End of archive + } + if err != nil { + return nil, fmt.Errorf("reading tar header: %w", err) + } + + if !strings.Contains(path, hdr.Name) { + return nil, fmt.Errorf("file not found in tar archive, looking for: %s, header was: %s", path, hdr.Name) + } + + if _, err := io.Copy(&out, tr); err != nil { + return nil, fmt.Errorf("copying file to buffer: %w", err) + } + + // Only support reading the first tile + break + } + + if out.Len() == 0 { + return nil, fmt.Errorf("file is empty") + } + + return out.Bytes(), nil +} From fdc034e8ae7a3ac652c108ff8e83c43dc5464a27 Mon Sep 17 00:00:00 2001 From: nadongjun Date: Mon, 19 Aug 2024 18:47:52 +0900 Subject: [PATCH 105/145] Integrate GORM Logger with Zerolog and Add Configuration Options for Logging and Performance (#2040) * Integrate GORM logger with zerolog and add custom GORM configuration options * Add GormConfig struct to group GORM-related settings * Update debug mode instruction in config-example.yaml Co-authored-by: Kristoffer Dalby --------- Co-authored-by: Kristoffer Dalby --- config-example.yaml | 17 +++++++++ hscontrol/db/db.go | 5 +-- hscontrol/types/config.go | 23 ++++++++++++ hscontrol/util/log.go | 75 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 118 insertions(+), 2 deletions(-) diff --git a/config-example.yaml b/config-example.yaml index 40e5c8e4..44e36b82 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -140,6 +140,23 @@ ephemeral_node_inactivity_timeout: 30m database: type: sqlite + # Enable debug mode. This setting requires the log.level to be set to "debug" or "trace". + debug: false + + # GORM configuration settings. + gorm: + # Enable prepared statements. + prepare_stmt: true + + # Enable parameterized queries. + parameterized_queries: true + + # Skip logging "record not found" errors. + skip_err_record_not_found: true + + # Threshold for slow queries in milliseconds. + slow_threshold: 1000 + # SQLite config sqlite: path: /var/lib/headscale/db.sqlite diff --git a/hscontrol/db/db.go b/hscontrol/db/db.go index c1908134..331dba54 100644 --- a/hscontrol/db/db.go +++ b/hscontrol/db/db.go @@ -426,7 +426,7 @@ func openDB(cfg types.DatabaseConfig) (*gorm.DB, error) { // TODO(kradalby): Integrate this with zerolog var dbLogger logger.Interface if cfg.Debug { - dbLogger = logger.Default + dbLogger = util.NewDBLogWrapper(&log.Logger, cfg.Gorm.SlowThreshold, cfg.Gorm.SkipErrRecordNotFound, cfg.Gorm.ParameterizedQueries) } else { dbLogger = logger.Default.LogMode(logger.Silent) } @@ -447,7 +447,8 @@ func openDB(cfg types.DatabaseConfig) (*gorm.DB, error) { db, err := gorm.Open( sqlite.Open(cfg.Sqlite.Path), &gorm.Config{ - Logger: dbLogger, + PrepareStmt: cfg.Gorm.PrepareStmt, + Logger: dbLogger, }, ) diff --git a/hscontrol/types/config.go b/hscontrol/types/config.go index e938768e..bff80998 100644 --- a/hscontrol/types/config.go +++ b/hscontrol/types/config.go @@ -120,11 +120,22 @@ type PostgresConfig struct { ConnMaxIdleTimeSecs int } +type GormConfig struct { + Debug bool + SlowThreshold time.Duration + SkipErrRecordNotFound bool + ParameterizedQueries bool + PrepareStmt bool +} + type DatabaseConfig struct { // Type sets the database type, either "sqlite3" or "postgres" Type string Debug bool + // Type sets the gorm configuration + Gorm GormConfig + Sqlite SqliteConfig Postgres PostgresConfig } @@ -486,6 +497,11 @@ func GetDatabaseConfig() DatabaseConfig { type_ := viper.GetString("database.type") + skipErrRecordNotFound := viper.GetBool("database.gorm.skip_err_record_not_found") + slowThreshold := viper.GetDuration("database.gorm.slow_threshold") * time.Millisecond + parameterizedQueries := viper.GetBool("database.gorm.parameterized_queries") + prepareStmt := viper.GetBool("database.gorm.prepare_stmt") + switch type_ { case DatabaseSqlite, DatabasePostgres: break @@ -499,6 +515,13 @@ func GetDatabaseConfig() DatabaseConfig { return DatabaseConfig{ Type: type_, Debug: debug, + Gorm: GormConfig{ + Debug: debug, + SkipErrRecordNotFound: skipErrRecordNotFound, + SlowThreshold: slowThreshold, + ParameterizedQueries: parameterizedQueries, + PrepareStmt: prepareStmt, + }, Sqlite: SqliteConfig{ Path: util.AbsolutePathFromConfigPath( viper.GetString("database.sqlite.path"), diff --git a/hscontrol/util/log.go b/hscontrol/util/log.go index 41d667d1..12f646b1 100644 --- a/hscontrol/util/log.go +++ b/hscontrol/util/log.go @@ -1,7 +1,14 @@ package util import ( + "context" + "errors" + "time" + + "github.com/rs/zerolog" "github.com/rs/zerolog/log" + "gorm.io/gorm" + gormLogger "gorm.io/gorm/logger" "tailscale.com/types/logger" ) @@ -14,3 +21,71 @@ func TSLogfWrapper() logger.Logf { log.Debug().Caller().Msgf(format, args...) } } + +type DBLogWrapper struct { + Logger *zerolog.Logger + Level zerolog.Level + Event *zerolog.Event + SlowThreshold time.Duration + SkipErrRecordNotFound bool + ParameterizedQueries bool +} + +func NewDBLogWrapper(origin *zerolog.Logger, slowThreshold time.Duration, skipErrRecordNotFound bool, parameterizedQueries bool) *DBLogWrapper { + l := &DBLogWrapper{ + Logger: origin, + Level: origin.GetLevel(), + SlowThreshold: slowThreshold, + SkipErrRecordNotFound: skipErrRecordNotFound, + ParameterizedQueries: parameterizedQueries, + } + + return l +} + +type DBLogWrapperOption func(*DBLogWrapper) + +func (l *DBLogWrapper) LogMode(gormLogger.LogLevel) gormLogger.Interface { + return l +} + +func (l *DBLogWrapper) Info(ctx context.Context, msg string, data ...interface{}) { + l.Logger.Info().Msgf(msg, data...) +} + +func (l *DBLogWrapper) Warn(ctx context.Context, msg string, data ...interface{}) { + l.Logger.Warn().Msgf(msg, data...) +} + +func (l *DBLogWrapper) Error(ctx context.Context, msg string, data ...interface{}) { + l.Logger.Error().Msgf(msg, data...) +} + +func (l *DBLogWrapper) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) { + elapsed := time.Since(begin) + sql, rowsAffected := fc() + fields := map[string]interface{}{ + "duration": elapsed, + "sql": sql, + "rowsAffected": rowsAffected, + } + + if err != nil && !(errors.Is(err, gorm.ErrRecordNotFound) && l.SkipErrRecordNotFound) { + l.Logger.Error().Err(err).Fields(fields).Msgf("") + return + } + + if l.SlowThreshold != 0 && elapsed > l.SlowThreshold { + l.Logger.Warn().Fields(fields).Msgf("") + return + } + + l.Logger.Debug().Fields(fields).Msgf("") +} + +func (l *DBLogWrapper) ParamsFilter(ctx context.Context, sql string, params ...interface{}) (string, []interface{}) { + if l.ParameterizedQueries { + return sql, nil + } + return sql, params +} From f99497340b1971f53b3aefec9c918e74523d0870 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Mon, 19 Aug 2024 12:06:55 +0200 Subject: [PATCH 106/145] add coderabbit config (#2060) Code Rabbit is one of these new fancy LLM code review tools. I am skeptical but we can try it for free and it might provide us with some value to let people get feedback while waiting for other people. Signed-off-by: Kristoffer Dalby --- .coderabbit.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .coderabbit.yaml diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 00000000..614f851b --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,15 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +language: "en-GB" +early_access: false +reviews: + profile: "chill" + request_changes_workflow: false + high_level_summary: true + poem: true + review_status: true + collapse_walkthrough: false + auto_review: + enabled: true + drafts: true +chat: + auto_reply: true From 84cb5d0aed3fe13329a3e28fc9eb1efc587a3b86 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Mon, 19 Aug 2024 13:03:01 +0200 Subject: [PATCH 107/145] make acl_policy_path fatal if policy.path is not set (#2041) --- config-example.yaml | 4 ++-- hscontrol/types/config.go | 2 +- hscontrol/types/config_test.go | 19 +++++++++++++++++++ .../types/testdata/policy-path-is-loaded.yaml | 18 ++++++++++++++++++ integration/hsic/config.go | 2 +- integration/hsic/hsic.go | 2 +- 6 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 hscontrol/types/testdata/policy-path-is-loaded.yaml diff --git a/config-example.yaml b/config-example.yaml index 44e36b82..2735eaf7 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -241,8 +241,8 @@ policy: # - https://tailscale.com/kb/1081/magicdns/ # - https://tailscale.com/blog/2021-09-private-dns-with-magicdns/ # -# Please not that for the DNS configuration to have any effect, -# clients must have the `--accept-ds=true` option enabled. This is the +# Please note that for the DNS configuration to have any effect, +# clients must have the `--accept-dns=true` option enabled. This is the # default for the Tailscale client. This option is enabled by default # in the Tailscale client. # diff --git a/hscontrol/types/config.go b/hscontrol/types/config.go index bff80998..30fa1c6b 100644 --- a/hscontrol/types/config.go +++ b/hscontrol/types/config.go @@ -303,7 +303,7 @@ func LoadConfig(path string, isFile bool) error { // https://github.com/spf13/viper/issues/560 // Alias the old ACL Policy path with the new configuration option. - depr.warnWithAlias("policy.path", "acl_policy_path") + depr.fatalIfNewKeyIsNotUsed("policy.path", "acl_policy_path") // Move dns_config -> dns depr.warn("dns_config.override_local_dns") diff --git a/hscontrol/types/config_test.go b/hscontrol/types/config_test.go index 7cf562b1..2b36e45c 100644 --- a/hscontrol/types/config_test.go +++ b/hscontrol/types/config_test.go @@ -161,6 +161,25 @@ func TestReadConfig(t *testing.T) { }, wantErr: "", }, + { + name: "policy-path-is-loaded", + configPath: "testdata/policy-path-is-loaded.yaml", + setup: func(t *testing.T) (any, error) { + cfg, err := GetHeadscaleConfig() + if err != nil { + return nil, err + } + + return map[string]string{ + "policy.mode": string(cfg.Policy.Mode), + "policy.path": cfg.Policy.Path, + }, err + }, + want: map[string]string{ + "policy.mode": "file", + "policy.path": "/etc/policy.hujson", + }, + }, } for _, tt := range tests { diff --git a/hscontrol/types/testdata/policy-path-is-loaded.yaml b/hscontrol/types/testdata/policy-path-is-loaded.yaml new file mode 100644 index 00000000..da0d29cd --- /dev/null +++ b/hscontrol/types/testdata/policy-path-is-loaded.yaml @@ -0,0 +1,18 @@ +noise: + private_key_path: "private_key.pem" + +prefixes: + v6: fd7a:115c:a1e0::/48 + v4: 100.64.0.0/10 + +database: + type: sqlite3 + +server_url: "https://derp.no" + +acl_policy_path: "/etc/acl_policy.yaml" +policy: + type: file + path: "/etc/policy.hujson" + +dns.magic_dns: false diff --git a/integration/hsic/config.go b/integration/hsic/config.go index c4d8b283..244470f2 100644 --- a/integration/hsic/config.go +++ b/integration/hsic/config.go @@ -13,7 +13,7 @@ noise: func DefaultConfigEnv() map[string]string { return map[string]string{ "HEADSCALE_LOG_LEVEL": "trace", - "HEADSCALE_ACL_POLICY_PATH": "", + "HEADSCALE_POLICY_PATH": "", "HEADSCALE_DATABASE_TYPE": "sqlite", "HEADSCALE_DATABASE_SQLITE_PATH": "/tmp/integration_test_db.sqlite3", "HEADSCALE_EPHEMERAL_NODE_INACTIVITY_TIMEOUT": "30m", diff --git a/integration/hsic/hsic.go b/integration/hsic/hsic.go index 3794e085..0b5a6be3 100644 --- a/integration/hsic/hsic.go +++ b/integration/hsic/hsic.go @@ -82,7 +82,7 @@ type Option = func(c *HeadscaleInContainer) func WithACLPolicy(acl *policy.ACLPolicy) Option { return func(hsic *HeadscaleInContainer) { // TODO(kradalby): Move somewhere appropriate - hsic.env["HEADSCALE_ACL_POLICY_PATH"] = aclPolicyPath + hsic.env["HEADSCALE_POLICY_PATH"] = aclPolicyPath hsic.aclPolicy = acl } From 9bed76d4817ec0d41242974185b06829964fca37 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2024 11:05:43 +0000 Subject: [PATCH 108/145] flake.lock: Update (#2059) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 627b7598..c69f2280 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1723221148, - "narHash": "sha256-7pjpeQlZUNQ4eeVntytU3jkw9dFK3k1Htgk2iuXjaD8=", + "lastModified": 1723856861, + "narHash": "sha256-OTDg91+Zzs2SpU3csK4xVdSQFoG8cK1lNUwKmTqERyE=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "154bcb95ad51bc257c2ce4043a725de6ca700ef6", + "rev": "cd7b95ee3725af7113bacbce91dd6549cee58ca5", "type": "github" }, "original": { From a68854ac33f224e898a01fe4a5dd4c6a6174c757 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 23 Aug 2024 15:28:54 +0200 Subject: [PATCH 109/145] upgrade go (1.23) and tailscale (1.72.1) (#2077) --- Dockerfile.debug | 2 +- Dockerfile.tailscale-HEAD | 2 +- flake.lock | 6 ++-- flake.nix | 6 ++-- go.mod | 27 ++++++++--------- go.sum | 64 ++++++++++++++++++++------------------- integration/route_test.go | 13 ++++---- 7 files changed, 61 insertions(+), 59 deletions(-) diff --git a/Dockerfile.debug b/Dockerfile.debug index 4e63dca8..e5066060 100644 --- a/Dockerfile.debug +++ b/Dockerfile.debug @@ -2,7 +2,7 @@ # and are in no way endorsed by Headscale's maintainers as an # official nor supported release or distribution. -FROM docker.io/golang:1.22-bookworm +FROM docker.io/golang:1.23-bookworm ARG VERSION=dev ENV GOPATH /go WORKDIR /go/src/headscale diff --git a/Dockerfile.tailscale-HEAD b/Dockerfile.tailscale-HEAD index f78d687a..92b0cae5 100644 --- a/Dockerfile.tailscale-HEAD +++ b/Dockerfile.tailscale-HEAD @@ -4,7 +4,7 @@ # This Dockerfile is more or less lifted from tailscale/tailscale # to ensure a similar build process when testing the HEAD of tailscale. -FROM golang:1.22-alpine AS build-env +FROM golang:1.23-alpine AS build-env WORKDIR /go/src diff --git a/flake.lock b/flake.lock index c69f2280..82daf973 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1723856861, - "narHash": "sha256-OTDg91+Zzs2SpU3csK4xVdSQFoG8cK1lNUwKmTqERyE=", + "lastModified": 1724363052, + "narHash": "sha256-Nf/iQWamRVAwAPFccQMfm5Qcf+rLLnU1rWG3f9orDVE=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "cd7b95ee3725af7113bacbce91dd6549cee58ca5", + "rev": "5de1564aed415bf9d0f281461babc2d101dd49ff", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index ab608439..dbf4f38f 100644 --- a/flake.nix +++ b/flake.nix @@ -21,7 +21,7 @@ overlay = _: prev: let pkgs = nixpkgs.legacyPackages.${prev.system}; in rec { - headscale = pkgs.buildGo122Module rec { + headscale = pkgs.buildGo123Module rec { pname = "headscale"; version = headscaleVersion; src = pkgs.lib.cleanSource self; @@ -31,7 +31,7 @@ # When updating go.mod or go.sum, a new sha will need to be calculated, # update this if you have a mismatch after doing a change to thos files. - vendorHash = "sha256-08N9ZdUM3Lw0ad89Vpy01e/qJQoMRPj8n4Jd7Aecgjw="; + vendorHash = "sha256-hmBRtMPqewg4oqu2bc9HtE3wdCdl5v9MoBOOCsjYlE8="; subPackages = ["cmd/headscale"]; @@ -63,7 +63,7 @@ overlays = [self.overlay]; inherit system; }; - buildDeps = with pkgs; [git go_1_22 gnumake]; + buildDeps = with pkgs; [git go_1_23 gnumake]; devDeps = with pkgs; buildDeps ++ [ diff --git a/go.mod b/go.mod index 71cd8c44..a0797844 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,6 @@ module github.com/juanfont/headscale -go 1.22.0 - -toolchain go1.22.2 +go 1.23.0 require ( github.com/AlecAivazis/survey/v2 v2.3.7 @@ -23,14 +21,14 @@ require ( 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.18.0 - github.com/prometheus/common v0.46.0 + github.com/prometheus/client_golang v1.19.1 + github.com/prometheus/common v0.48.0 github.com/pterm/pterm v0.12.79 github.com/puzpuzpuz/xsync/v3 v3.1.0 github.com/rs/zerolog v1.32.0 github.com/samber/lo v1.39.0 github.com/sasha-s/go-deadlock v0.3.1 - github.com/spf13/cobra v1.8.0 + github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.20.0-alpha.6 github.com/stretchr/testify v1.9.0 github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a @@ -49,7 +47,7 @@ require ( gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/postgres v1.5.7 gorm.io/gorm v1.25.10 - tailscale.com v1.66.3 + tailscale.com v1.72.1 ) require ( @@ -81,6 +79,7 @@ require ( github.com/bits-and-blooms/bitset v1.13.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/coder/websocket v1.8.12 // indirect github.com/containerd/console v1.0.4 // indirect github.com/containerd/continuity v0.4.3 // indirect github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect @@ -88,14 +87,14 @@ require ( github.com/dblohm7/wingoes v0.0.0-20240123200102-b75a8a7d7eb0 // indirect github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e // indirect github.com/docker/cli v26.1.3+incompatible // indirect - github.com/docker/docker v26.1.3+incompatible // indirect + github.com/docker/docker v26.1.4+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/felixge/fgprof v0.9.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/fxamacker/cbor/v2 v2.5.0 // indirect - github.com/gaissmai/bart v0.4.1 // indirect + github.com/fxamacker/cbor/v2 v2.6.0 // indirect + github.com/gaissmai/bart v0.11.1 // indirect github.com/glebarez/go-sqlite v1.22.0 // indirect github.com/go-jose/go-jose/v3 v3.0.3 // indirect github.com/go-jose/go-jose/v4 v4.0.1 // indirect @@ -159,6 +158,7 @@ require ( github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus-community/pro-bing v0.4.0 // 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 @@ -174,14 +174,14 @@ require ( 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-20240108194725-7ce1f622c780 // indirect + github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 // indirect github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 // indirect github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 // indirect github.com/tailscale/setec v0.0.0-20240314234648-9da8e7407257 // indirect github.com/tailscale/squibble v0.0.0-20240418235321-9ee0eeb78185 // indirect github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 // indirect - github.com/tailscale/wireguard-go v0.0.0-20240429185444-03c5a0ccf754 // indirect + github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98 // indirect github.com/tcnksm/go-httpstat v0.2.0 // indirect github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e // indirect github.com/vishvananda/netlink v1.2.1-beta.2 // indirect @@ -203,10 +203,9 @@ require ( golang.zx2c4.com/wireguard/windows v0.5.3 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3 // indirect + gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 // indirect modernc.org/libc v1.50.6 // indirect modernc.org/mathutil v1.6.0 // indirect modernc.org/memory v1.8.0 // indirect modernc.org/sqlite v1.29.9 // indirect - nhooyr.io/websocket v1.8.10 // indirect ) diff --git a/go.sum b/go.sum index 6bc69456..fb5b93c0 100644 --- a/go.sum +++ b/go.sum @@ -99,10 +99,12 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= -github.com/cilium/ebpf v0.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4= -github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM= +github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk= +github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso= 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/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= +github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= @@ -113,13 +115,13 @@ github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8 github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU= github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac= 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/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creachadair/mds v0.14.5 h1:2amuO4yCbQkaAyDoLO5iCbwbTRQZz4EpRhOejQbf4+8= github.com/creachadair/mds v0.14.5/go.mod h1:4vrFYUzTXMJpMBU+OA292I6IUxKWCCfZkgXg+/kBZMo= github.com/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.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= -github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0= +github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -134,8 +136,8 @@ github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= github.com/docker/cli v26.1.3+incompatible h1:bUpXT/N0kDE3VUHI2r5VMsYQgi38kYuoC0oL9yt3lqc= github.com/docker/cli v26.1.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v26.1.3+incompatible h1:lLCzRbrVZrljpVNobJu1J2FHk8V0s4BawoZippkc+xo= -github.com/docker/docker v26.1.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v26.1.4+incompatible h1:vuTpXDuoga+Z38m1OZHzl7NKisKWaWlhjQk7IDPSLsU= +github.com/docker/docker v26.1.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -155,10 +157,10 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk 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/gaissmai/bart v0.4.1 h1:G1t58voWkNmT47lBDawH5QhtTDsdqRIO+ftq5x4P9Ls= -github.com/gaissmai/bart v0.4.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg= +github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA= +github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/gaissmai/bart v0.11.1 h1:5Uv5XwsaFBRo4E5VBcb9TzY8B7zxFf+U7isDxqOrRfc= +github.com/gaissmai/bart v0.11.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg= github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I= github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo= github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= @@ -382,13 +384,15 @@ github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Q github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= -github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= +github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4= +github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 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.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y= -github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ= +github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= 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= @@ -434,8 +438,8 @@ 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/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 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.20.0-alpha.6 h1:f65Cr/+2qk4GfHC0xqT/isoupQppwN5+VLRztUGTDbY= @@ -462,8 +466,8 @@ github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP 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-20240108194725-7ce1f622c780 h1:U0J2CUrrTcc2wmr9tSLYEo+USfwNikRRsmxVLD4eZ7E= -github.com/tailscale/golang-x-crypto v0.0.0-20240108194725-7ce1f622c780/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ= +github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 h1:rXZGgEa+k2vJM8xT0PoSKfVXwFGPQ3z3CJfmnHJkZZw= +github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ= github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio= github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8= github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw= @@ -482,10 +486,10 @@ github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 h1:t github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ= github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M= github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y= -github.com/tailscale/wireguard-go v0.0.0-20240429185444-03c5a0ccf754 h1:iazWjqVHE6CbNam7WXRhi33Qad5o7a8LVYgVoILpZdI= -github.com/tailscale/wireguard-go v0.0.0-20240429185444-03c5a0ccf754/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4= -github.com/tailscale/xnet v0.0.0-20240117122442-62b9a7c569f9 h1:81P7rjnikHKTJ75EkjppvbwUfKHDHYk6LJpO5PZy8pA= -github.com/tailscale/xnet v0.0.0-20240117122442-62b9a7c569f9/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg= +github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98 h1:RNpJrXfI5u6e+uzyIzvmnXbhmhdRkVf//90sMBH3lso= +github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4= +github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek= +github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg= github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA= github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk= github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0= @@ -540,8 +544,8 @@ golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a h1:8qmSSA8Gz/1kTrCe0nqR0R3Gb/NDhykzWw2q2mWZydM= golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= -golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= -golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= +golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= +golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -691,8 +695,8 @@ gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= gorm.io/gorm v1.25.10/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-20240306221502-ee1e1f6070e3 h1:/8/t5pz/mgdRXhYOIeqqYhFAQLE4DDGegc0Y4ZjyFJM= -gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3/go.mod h1:NQHVAzMwvZ+Qe3ElSiHmq9RUm1MdNHpUZ52fiEqvn+0= +gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 h1:TU8z2Lh3Bbq77w0t1eG8yRlLcNHzZu3x6mhoH2Mk0c8= +gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/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.4.7 h1:9MDAWxMoSnB6QoSqiVr7P5mtkT9pOc1kSxchzPCnqJs= @@ -725,9 +729,7 @@ modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -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.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.66.3 h1:jpWat+hiobTtCosSV/c8D6S/ubgROf/S59MaIBdM9pY= -tailscale.com v1.66.3/go.mod h1:99BIV4U3UPw36Sva04xK2ZsEpVRUkY9jCdEDSAhaNGM= +tailscale.com v1.72.1 h1:hk82jek36ph2S3Tfsh57NVWKEm/pZ9nfUonvlowpfaA= +tailscale.com v1.72.1/go.mod h1:v7OHtg0KLAnhOVf81Z8WrjNefj238QbFhgkWJQoKxbs= diff --git a/integration/route_test.go b/integration/route_test.go index 48b6c07f..ed371642 100644 --- a/integration/route_test.go +++ b/integration/route_test.go @@ -17,6 +17,7 @@ import ( "github.com/juanfont/headscale/integration/tsic" "github.com/stretchr/testify/assert" "tailscale.com/types/ipproto" + "tailscale.com/types/views" "tailscale.com/wgengine/filter" ) @@ -1146,9 +1147,9 @@ func TestSubnetRouteACL(t *testing.T) { wantClientFilter := []filter.Match{ { - IPProto: []ipproto.Proto{ + IPProto: views.SliceOf([]ipproto.Proto{ ipproto.TCP, ipproto.UDP, ipproto.ICMPv4, ipproto.ICMPv6, - }, + }), Srcs: []netip.Prefix{ netip.MustParsePrefix("100.64.0.1/32"), netip.MustParsePrefix("100.64.0.2/32"), @@ -1178,9 +1179,9 @@ func TestSubnetRouteACL(t *testing.T) { wantSubnetFilter := []filter.Match{ { - IPProto: []ipproto.Proto{ + IPProto: views.SliceOf([]ipproto.Proto{ ipproto.TCP, ipproto.UDP, ipproto.ICMPv4, ipproto.ICMPv6, - }, + }), Srcs: []netip.Prefix{ netip.MustParsePrefix("100.64.0.1/32"), netip.MustParsePrefix("100.64.0.2/32"), @@ -1200,9 +1201,9 @@ func TestSubnetRouteACL(t *testing.T) { Caps: []filter.CapMatch{}, }, { - IPProto: []ipproto.Proto{ + IPProto: views.SliceOf([]ipproto.Proto{ ipproto.TCP, ipproto.UDP, ipproto.ICMPv4, ipproto.ICMPv6, - }, + }), Srcs: []netip.Prefix{ netip.MustParsePrefix("100.64.0.1/32"), netip.MustParsePrefix("100.64.0.2/32"), From 9c4c286696d7eaea3dc613c0112ca237d78232b3 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 23 Aug 2024 17:17:37 +0200 Subject: [PATCH 110/145] fix warning errs from beta2 (#2075) * remove default false for use usernames causing warning Fixes #2065 Signed-off-by: Kristoffer Dalby * Ensure DoH warnings are only emitted if err Fixes #2064 Signed-off-by: Kristoffer Dalby --------- Signed-off-by: Kristoffer Dalby --- hscontrol/types/config.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/hscontrol/types/config.go b/hscontrol/types/config.go index 30fa1c6b..0b7d63b7 100644 --- a/hscontrol/types/config.go +++ b/hscontrol/types/config.go @@ -246,7 +246,6 @@ func LoadConfig(path string, isFile bool) error { viper.SetDefault("dns.nameservers.split", map[string]string{}) viper.SetDefault("dns.search_domains", []string{}) viper.SetDefault("dns.extra_records", []tailcfg.DNSRecord{}) - viper.SetDefault("dns.use_username_in_magic_dns", false) viper.SetDefault("derp.server.enabled", false) viper.SetDefault("derp.server.stun.enabled", true) @@ -600,6 +599,8 @@ func (d *DNSConfig) GlobalResolvers() []*dnstype.Resolver { resolvers = append(resolvers, &dnstype.Resolver{ Addr: nsStr, }) + + continue } else { warn = fmt.Sprintf("Invalid global nameserver %q. Parsing error: %s ignoring", nsStr, err) } @@ -636,6 +637,8 @@ func (d *DNSConfig) SplitResolvers() map[string][]*dnstype.Resolver { resolvers = append(resolvers, &dnstype.Resolver{ Addr: nsStr, }) + + continue } else { warn = fmt.Sprintf("Invalid split dns nameserver %q. Parsing error: %s ignoring", nsStr, err) } From 827e3e83aec0a5f2ced4530c91ad18fd2871a815 Mon Sep 17 00:00:00 2001 From: dragon2611 Date: Tue, 27 Aug 2024 10:03:51 +0100 Subject: [PATCH 111/145] Issue 2045, Feature Request (#2071) Requiring someone to write a design doc/contribute to the feature shouldn't be a requirement for raising a feature request as users may lack the skills required to do this. --- .github/ISSUE_TEMPLATE/feature_request.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index b95cd5e6..70f1a146 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -25,9 +25,9 @@ body: description: Are you willing to contribute to the implementation of this feature? options: - label: I can write the design doc for this feature - required: true + required: false - label: I can contribute this feature - required: true + required: false - type: textarea attributes: label: How can it be implemented? From cf6a606d74313b8b4dd4d5b07ee9b6ea61690624 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Tue, 27 Aug 2024 18:54:28 +0200 Subject: [PATCH 112/145] fix route table migration wiping routes 0.22 -> 0.23 (#2076) --- .github/workflows/test.yml | 2 +- hscontrol/db/db.go | 22 ++- hscontrol/db/db_test.go | 168 ++++++++++++++++++ hscontrol/db/node.go | 7 +- hscontrol/db/node_test.go | 14 +- ...3-to-0-23-0-routes-are-dropped-2063.sqlite | Bin 0 -> 98304 bytes ...0-23-0-routes-fail-foreign-key-2076.sqlite | Bin 0 -> 57344 bytes hscontrol/util/test.go | 6 +- integration/route_test.go | 4 +- 9 files changed, 204 insertions(+), 19 deletions(-) create mode 100644 hscontrol/db/db_test.go create mode 100644 hscontrol/db/testdata/0-22-3-to-0-23-0-routes-are-dropped-2063.sqlite create mode 100644 hscontrol/db/testdata/0-22-3-to-0-23-0-routes-fail-foreign-key-2076.sqlite diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b03fc434..f4659332 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,4 +34,4 @@ jobs: - name: Run tests if: steps.changed-files.outputs.files == 'true' - run: nix develop --check + run: nix develop --command -- gotestsum diff --git a/hscontrol/db/db.go b/hscontrol/db/db.go index 331dba54..3aaa7eeb 100644 --- a/hscontrol/db/db.go +++ b/hscontrol/db/db.go @@ -51,8 +51,8 @@ func NewHeadscaleDatabase( 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 + // New migrations must be added as transactions at the end of this list. + // The initial migration 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. @@ -123,9 +123,21 @@ func NewHeadscaleDatabase( } } - err = tx.AutoMigrate(&types.Route{}) - if err != nil { - return err + // Only run automigrate Route table if it does not exist. It has only been + // changed ones, when machines where renamed to nodes, which is covered + // further up. This whole initial integration is a mess and if AutoMigrate + // is ran on a 0.22 to 0.23 update, it will wipe all the routes. + if tx.Migrator().HasTable(&types.Route{}) && tx.Migrator().HasTable(&types.Node{}) { + err := tx.Exec("delete from routes where node_id not in (select id from nodes)").Error + if err != nil { + return err + } + } + if !tx.Migrator().HasTable(&types.Route{}) { + err = tx.AutoMigrate(&types.Route{}) + if err != nil { + return err + } } err = tx.AutoMigrate(&types.Node{}) diff --git a/hscontrol/db/db_test.go b/hscontrol/db/db_test.go new file mode 100644 index 00000000..b32d93ce --- /dev/null +++ b/hscontrol/db/db_test.go @@ -0,0 +1,168 @@ +package db + +import ( + "fmt" + "io" + "net/netip" + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/juanfont/headscale/hscontrol/types" + "github.com/stretchr/testify/assert" + "gorm.io/gorm" +) + +func TestMigrations(t *testing.T) { + ipp := func(p string) types.IPPrefix { + return types.IPPrefix(netip.MustParsePrefix(p)) + } + r := func(id uint64, p string, a, e, i bool) types.Route { + return types.Route{ + NodeID: id, + Prefix: ipp(p), + Advertised: a, + Enabled: e, + IsPrimary: i, + } + } + tests := []struct { + dbPath string + wantFunc func(*testing.T, *HSDatabase) + wantErr string + }{ + { + dbPath: "testdata/0-22-3-to-0-23-0-routes-are-dropped-2063.sqlite", + wantFunc: func(t *testing.T, h *HSDatabase) { + routes, err := Read(h.DB, func(rx *gorm.DB) (types.Routes, error) { + return GetRoutes(rx) + }) + assert.NoError(t, err) + + assert.Len(t, routes, 10) + want := types.Routes{ + r(1, "0.0.0.0/0", true, true, false), + r(1, "::/0", true, true, false), + r(1, "10.9.110.0/24", true, true, true), + r(26, "172.100.100.0/24", true, true, true), + r(26, "172.100.100.0/24", true, false, false), + r(31, "0.0.0.0/0", true, true, false), + r(31, "0.0.0.0/0", true, false, false), + r(31, "::/0", true, true, false), + r(31, "::/0", true, false, false), + r(32, "192.168.0.24/32", true, true, true), + } + if diff := cmp.Diff(want, routes, cmpopts.IgnoreFields(types.Route{}, "Model", "Node"), cmp.Comparer(func(x, y types.IPPrefix) bool { + return x == y + })); diff != "" { + t.Errorf("TestMigrations() mismatch (-want +got):\n%s", diff) + } + }, + }, + { + dbPath: "testdata/0-22-3-to-0-23-0-routes-fail-foreign-key-2076.sqlite", + wantFunc: func(t *testing.T, h *HSDatabase) { + routes, err := Read(h.DB, func(rx *gorm.DB) (types.Routes, error) { + return GetRoutes(rx) + }) + assert.NoError(t, err) + + assert.Len(t, routes, 4) + want := types.Routes{ + // These routes exists, but have no nodes associated with them + // when the migration starts. + // r(1, "0.0.0.0/0", true, true, false), + // r(1, "::/0", true, true, false), + // r(3, "0.0.0.0/0", true, true, false), + // r(3, "::/0", true, true, false), + // r(5, "0.0.0.0/0", true, true, false), + // r(5, "::/0", true, true, false), + // r(6, "0.0.0.0/0", true, true, false), + // r(6, "::/0", true, true, false), + // r(6, "10.0.0.0/8", true, false, false), + // r(7, "0.0.0.0/0", true, true, false), + // r(7, "::/0", true, true, false), + // r(7, "10.0.0.0/8", true, false, false), + // r(9, "0.0.0.0/0", true, true, false), + // r(9, "::/0", true, true, false), + // r(9, "10.0.0.0/8", true, true, false), + // r(11, "0.0.0.0/0", true, true, false), + // r(11, "::/0", true, true, false), + // r(11, "10.0.0.0/8", true, true, true), + // r(12, "0.0.0.0/0", true, true, false), + // r(12, "::/0", true, true, false), + // r(12, "10.0.0.0/8", true, false, false), + // + // These nodes exists, so routes should be kept. + r(13, "10.0.0.0/8", true, false, false), + r(13, "0.0.0.0/0", true, true, false), + r(13, "::/0", true, true, false), + r(13, "10.18.80.2/32", true, true, true), + } + if diff := cmp.Diff(want, routes, cmpopts.IgnoreFields(types.Route{}, "Model", "Node"), cmp.Comparer(func(x, y types.IPPrefix) bool { + return x == y + })); diff != "" { + t.Errorf("TestMigrations() mismatch (-want +got):\n%s", diff) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.dbPath, func(t *testing.T) { + dbPath, err := testCopyOfDatabase(tt.dbPath) + if err != nil { + t.Fatalf("copying db for test: %s", err) + } + + hsdb, err := NewHeadscaleDatabase(types.DatabaseConfig{ + Type: "sqlite3", + Sqlite: types.SqliteConfig{ + Path: dbPath, + }, + }, "") + if err != nil && tt.wantErr != err.Error() { + t.Errorf("TestMigrations() unexpected error = %v, wantErr %v", err, tt.wantErr) + } + + if tt.wantFunc != nil { + tt.wantFunc(t, hsdb) + } + }) + } +} + +func testCopyOfDatabase(src string) (string, error) { + sourceFileStat, err := os.Stat(src) + if err != nil { + return "", err + } + + if !sourceFileStat.Mode().IsRegular() { + return "", fmt.Errorf("%s is not a regular file", src) + } + + source, err := os.Open(src) + if err != nil { + return "", err + } + defer source.Close() + + tmpDir, err := os.MkdirTemp("", "hsdb-test-*") + if err != nil { + return "", err + } + + fn := filepath.Base(src) + dst := filepath.Join(tmpDir, fn) + + destination, err := os.Create(dst) + if err != nil { + return "", err + } + defer destination.Close() + _, err = io.Copy(destination, source) + return dst, err +} diff --git a/hscontrol/db/node.go b/hscontrol/db/node.go index a2515ebf..a9e78a45 100644 --- a/hscontrol/db/node.go +++ b/hscontrol/db/node.go @@ -5,6 +5,7 @@ import ( "fmt" "net/netip" "sort" + "sync" "time" "github.com/juanfont/headscale/hscontrol/types" @@ -12,7 +13,6 @@ import ( "github.com/patrickmn/go-cache" "github.com/puzpuzpuz/xsync/v3" "github.com/rs/zerolog/log" - "github.com/sasha-s/go-deadlock" "gorm.io/gorm" "tailscale.com/tailcfg" "tailscale.com/types/key" @@ -724,7 +724,7 @@ func ExpireExpiredNodes(tx *gorm.DB, // It is used to delete ephemeral nodes that have disconnected and should be // cleaned up. type EphemeralGarbageCollector struct { - mu deadlock.Mutex + mu sync.Mutex deleteFunc func(types.NodeID) toBeDeleted map[types.NodeID]*time.Timer @@ -752,10 +752,9 @@ func (e *EphemeralGarbageCollector) Close() { // Schedule schedules a node for deletion after the expiry duration. func (e *EphemeralGarbageCollector) Schedule(nodeID types.NodeID, expiry time.Duration) { e.mu.Lock() - defer e.mu.Unlock() - timer := time.NewTimer(expiry) e.toBeDeleted[nodeID] = timer + e.mu.Unlock() go func() { select { diff --git a/hscontrol/db/node_test.go b/hscontrol/db/node_test.go index ad94f064..c83da120 100644 --- a/hscontrol/db/node_test.go +++ b/hscontrol/db/node_test.go @@ -609,12 +609,14 @@ func TestEphemeralGarbageCollectorOrder(t *testing.T) { }) go e.Start() - e.Schedule(1, 1*time.Second) - e.Schedule(2, 2*time.Second) - e.Schedule(3, 3*time.Second) - e.Schedule(4, 4*time.Second) - e.Cancel(2) - e.Cancel(4) + go e.Schedule(1, 1*time.Second) + go e.Schedule(2, 2*time.Second) + go e.Schedule(3, 3*time.Second) + go e.Schedule(4, 4*time.Second) + + time.Sleep(time.Second) + go e.Cancel(2) + go e.Cancel(4) time.Sleep(6 * time.Second) diff --git a/hscontrol/db/testdata/0-22-3-to-0-23-0-routes-are-dropped-2063.sqlite b/hscontrol/db/testdata/0-22-3-to-0-23-0-routes-are-dropped-2063.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..10e1aaec5ed56ab30e47570788d37fa634fa0d82 GIT binary patch literal 98304 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 G_5TC3m>mEB literal 0 HcmV?d00001 diff --git a/hscontrol/db/testdata/0-22-3-to-0-23-0-routes-fail-foreign-key-2076.sqlite b/hscontrol/db/testdata/0-22-3-to-0-23-0-routes-fail-foreign-key-2076.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..dbe969623060bbe12f2daa1cd00008788f80380f GIT binary patch literal 57344 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 Date: Wed, 28 Aug 2024 09:50:09 +0200 Subject: [PATCH 113/145] fix: correct a small spelling mistake (#2081) --- hscontrol/db/db.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hscontrol/db/db.go b/hscontrol/db/db.go index 3aaa7eeb..99c3aa68 100644 --- a/hscontrol/db/db.go +++ b/hscontrol/db/db.go @@ -124,7 +124,7 @@ func NewHeadscaleDatabase( } // Only run automigrate Route table if it does not exist. It has only been - // changed ones, when machines where renamed to nodes, which is covered + // changed once, when machines where renamed to nodes, which is covered // further up. This whole initial integration is a mess and if AutoMigrate // is ran on a 0.22 to 0.23 update, it will wipe all the routes. if tx.Migrator().HasTable(&types.Route{}) && tx.Migrator().HasTable(&types.Node{}) { From 34361c6f827679284d306aafcd4795d17dc08799 Mon Sep 17 00:00:00 2001 From: Mike Poindexter Date: Thu, 29 Aug 2024 23:08:54 -0700 Subject: [PATCH 114/145] Fix FKs on sqlite migrations (#2083) --- hscontrol/db/db.go | 79 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 69 insertions(+), 10 deletions(-) diff --git a/hscontrol/db/db.go b/hscontrol/db/db.go index 99c3aa68..accf439e 100644 --- a/hscontrol/db/db.go +++ b/hscontrol/db/db.go @@ -123,21 +123,16 @@ func NewHeadscaleDatabase( } } - // Only run automigrate Route table if it does not exist. It has only been - // changed once, when machines where renamed to nodes, which is covered - // further up. This whole initial integration is a mess and if AutoMigrate - // is ran on a 0.22 to 0.23 update, it will wipe all the routes. + // Remove any invalid routes associated with a node that does not exist. if tx.Migrator().HasTable(&types.Route{}) && tx.Migrator().HasTable(&types.Node{}) { err := tx.Exec("delete from routes where node_id not in (select id from nodes)").Error if err != nil { return err } } - if !tx.Migrator().HasTable(&types.Route{}) { - err = tx.AutoMigrate(&types.Route{}) - if err != nil { - return err - } + err = tx.AutoMigrate(&types.Route{}) + if err != nil { + return err } err = tx.AutoMigrate(&types.Node{}) @@ -421,7 +416,7 @@ func NewHeadscaleDatabase( }, ) - if err = migrations.Migrate(); err != nil { + if err := runMigrations(cfg, dbConn, migrations); err != nil { log.Fatal().Err(err).Msgf("Migration failed: %v", err) } @@ -545,6 +540,70 @@ func openDB(cfg types.DatabaseConfig) (*gorm.DB, error) { ) } +func runMigrations(cfg types.DatabaseConfig, dbConn *gorm.DB, migrations *gormigrate.Gormigrate) error { + // Turn off foreign keys for the duration of the migration if using sqllite to + // prevent data loss due to the way the GORM migrator handles certain schema + // changes. + if cfg.Type == types.DatabaseSqlite { + var fkEnabled int + if err := dbConn.Raw("PRAGMA foreign_keys").Scan(&fkEnabled).Error; err != nil { + return fmt.Errorf("checking foreign key status: %w", err) + } + if fkEnabled == 1 { + if err := dbConn.Exec("PRAGMA foreign_keys = OFF").Error; err != nil { + return fmt.Errorf("disabling foreign keys: %w", err) + } + defer dbConn.Exec("PRAGMA foreign_keys = ON") + } + } + + if err := migrations.Migrate(); err != nil { + return err + } + + // Since we disabled foreign keys for the migration, we need to check for + // constraint violations manually at the end of the migration. + if cfg.Type == types.DatabaseSqlite { + type constraintViolation struct { + Table string + RowID int + Parent string + ConstraintIndex int + } + + var violatedConstraints []constraintViolation + + rows, err := dbConn.Raw("PRAGMA foreign_key_check").Rows() + if err != nil { + return err + } + + for rows.Next() { + var violation constraintViolation + if err := rows.Scan(&violation.Table, &violation.RowID, &violation.Parent, &violation.ConstraintIndex); err != nil { + return err + } + + violatedConstraints = append(violatedConstraints, violation) + } + _ = rows.Close() + + if len(violatedConstraints) > 0 { + for _, violation := range violatedConstraints { + log.Error(). + Str("table", violation.Table). + Int("row_id", violation.RowID). + Str("parent", violation.Parent). + Msg("Foreign key constraint violated") + } + + return fmt.Errorf("foreign key constraints violated") + } + } + + return nil +} + func (hsdb *HSDatabase) PingDB(ctx context.Context) error { ctx, cancel := context.WithTimeout(ctx, time.Second) defer cancel() From 76515d12d6bbeda26157c49fbffa390536951873 Mon Sep 17 00:00:00 2001 From: Mike Poindexter Date: Thu, 29 Aug 2024 23:20:29 -0700 Subject: [PATCH 115/145] Fix self notification on expiry update via oidc relogin (#2080) --- hscontrol/oidc.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/hscontrol/oidc.go b/hscontrol/oidc.go index fe4d357c..72fefac3 100644 --- a/hscontrol/oidc.go +++ b/hscontrol/oidc.go @@ -526,7 +526,17 @@ func (h *Headscale) validateNodeForOIDCCallback( util.LogErr(err, "Failed to write response") } - ctx := types.NotifyCtx(context.Background(), "oidc-expiry", "na") + ctx := types.NotifyCtx(context.Background(), "oidc-expiry-self", node.Hostname) + h.nodeNotifier.NotifyByNodeID( + ctx, + types.StateUpdate{ + Type: types.StateSelfUpdate, + ChangeNodes: []types.NodeID{node.ID}, + }, + node.ID, + ) + + ctx = types.NotifyCtx(context.Background(), "oidc-expiry-peers", node.Hostname) h.nodeNotifier.NotifyWithIgnore(ctx, types.StateUpdateExpire(node.ID, expiry), node.ID) return nil, true, nil From fffd9d7ee9f17f1aeee1e21d72413b8ca92fa674 Mon Sep 17 00:00:00 2001 From: Roman Zabaluev Date: Fri, 30 Aug 2024 11:20:07 +0400 Subject: [PATCH 116/145] Update ACLs file format docs (#2066) --- docs/acls.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/acls.md b/docs/acls.md index 096dbea0..2330cda9 100644 --- a/docs/acls.md +++ b/docs/acls.md @@ -43,8 +43,7 @@ servers. Note: Users will be created automatically when users authenticate with the Headscale server. -ACLs could be written either on [huJSON](https://github.com/tailscale/hujson) -or YAML. Check the [test ACLs](../tests/acls) for further information. +ACLs have to be written in [huJSON](https://github.com/tailscale/hujson). Check the [test ACLs](../tests/acls) for further information. When registering the servers we will need to add the flag `--advertise-tags=tag:,tag:`, and the user that is From 2b5e52b08b1e36944c3b101b8a365e66c638b3e5 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 30 Aug 2024 16:58:29 +0200 Subject: [PATCH 117/145] validate policy against nodes, error if not valid (#2089) * validate policy against nodes, error if not valid this commit aims to improve the feedback of "runtime" policy errors which would only manifest when the rules are compiled to filter rules with nodes. this change will in; file-based mode load the nodes from the db and try to compile the rules on start up and return an error if they would not work as intended. database-based mode prevent a new ACL being written to the database if it does not compile with the current set of node. Fixes #2073 Fixes #2044 Signed-off-by: Kristoffer Dalby * ensure stderr can be used in err checks Signed-off-by: Kristoffer Dalby * test policy set validation Signed-off-by: Kristoffer Dalby * add new integration test to ghaction Signed-off-by: Kristoffer Dalby * add back defer for cli tst Signed-off-by: Kristoffer Dalby --------- Signed-off-by: Kristoffer Dalby --- .github/workflows/test-integration.yaml | 1 + hscontrol/app.go | 26 +++++++++ hscontrol/grpcv1.go | 29 +++++++++- integration/cli_test.go | 74 +++++++++++++++++++++++++ integration/dockertestutil/execute.go | 2 +- integration/hsic/hsic.go | 2 +- 6 files changed, 129 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-integration.yaml b/.github/workflows/test-integration.yaml index 6203e51b..aa220261 100644 --- a/.github/workflows/test-integration.yaml +++ b/.github/workflows/test-integration.yaml @@ -37,6 +37,7 @@ jobs: - TestNodeRenameCommand - TestNodeMoveCommand - TestPolicyCommand + - TestPolicyBrokenConfigCommand - TestResolveMagicDNS - TestValidateResolvConf - TestDERPServerScenario diff --git a/hscontrol/app.go b/hscontrol/app.go index b66e939b..087d2f2a 100644 --- a/hscontrol/app.go +++ b/hscontrol/app.go @@ -1001,6 +1001,32 @@ func (h *Headscale) loadACLPolicy() error { if err != nil { return fmt.Errorf("failed to load ACL policy from file: %w", err) } + + // Validate and reject configuration that would error when applied + // when creating a map response. This requires nodes, so there is still + // a scenario where they might be allowed if the server has no nodes + // yet, but it should help for the general case and for hot reloading + // configurations. + // Note that this check is only done for file-based policies in this function + // as the database-based policies are checked in the gRPC API where it is not + // allowed to be written to the database. + nodes, err := h.db.ListNodes() + if err != nil { + return fmt.Errorf("loading nodes from database to validate policy: %w", err) + } + + _, err = pol.CompileFilterRules(nodes) + if err != nil { + return fmt.Errorf("verifying policy rules: %w", err) + } + + if len(nodes) > 0 { + _, err = pol.CompileSSHPolicy(nodes[0], nodes) + if err != nil { + return fmt.Errorf("verifying SSH rules: %w", err) + } + } + case types.PolicyModeDB: p, err := h.db.GetPolicy() if err != nil { diff --git a/hscontrol/grpcv1.go b/hscontrol/grpcv1.go index d4e10849..83048bec 100644 --- a/hscontrol/grpcv1.go +++ b/hscontrol/grpcv1.go @@ -4,6 +4,7 @@ package hscontrol import ( "context" "errors" + "fmt" "io" "os" "sort" @@ -721,9 +722,31 @@ func (api headscaleV1APIServer) SetPolicy( p := request.GetPolicy() - valid, err := policy.LoadACLPolicyFromBytes([]byte(p)) + pol, err := policy.LoadACLPolicyFromBytes([]byte(p)) if err != nil { - return nil, err + return nil, fmt.Errorf("loading ACL policy file: %w", err) + } + + // Validate and reject configuration that would error when applied + // when creating a map response. This requires nodes, so there is still + // a scenario where they might be allowed if the server has no nodes + // yet, but it should help for the general case and for hot reloading + // configurations. + nodes, err := api.h.db.ListNodes() + if err != nil { + return nil, fmt.Errorf("loading nodes from database to validate policy: %w", err) + } + + _, err = pol.CompileFilterRules(nodes) + if err != nil { + return nil, fmt.Errorf("verifying policy rules: %w", err) + } + + if len(nodes) > 0 { + _, err = pol.CompileSSHPolicy(nodes[0], nodes) + if err != nil { + return nil, fmt.Errorf("verifying SSH rules: %w", err) + } } updated, err := api.h.db.SetPolicy(p) @@ -731,7 +754,7 @@ func (api headscaleV1APIServer) SetPolicy( return nil, err } - api.h.ACLPolicy = valid + api.h.ACLPolicy = pol ctx := types.NotifyCtx(context.Background(), "acl-update", "na") api.h.nodeNotifier.NotifyAll(ctx, types.StateUpdate{ diff --git a/integration/cli_test.go b/integration/cli_test.go index 088db786..9e7d179f 100644 --- a/integration/cli_test.go +++ b/integration/cli_test.go @@ -1676,3 +1676,77 @@ func TestPolicyCommand(t *testing.T) { assert.Len(t, output.ACLs, 1) assert.Equal(t, output.TagOwners["tag:exists"], []string{"policy-user"}) } + +func TestPolicyBrokenConfigCommand(t *testing.T) { + IntegrationSkip(t) + t.Parallel() + + scenario, err := NewScenario(dockertestMaxWait()) + assertNoErr(t, err) + defer scenario.Shutdown() + + spec := map[string]int{ + "policy-user": 1, + } + + err = scenario.CreateHeadscaleEnv( + spec, + []tsic.Option{}, + hsic.WithTestName("clins"), + hsic.WithConfigEnv(map[string]string{ + "HEADSCALE_POLICY_MODE": "database", + }), + ) + assertNoErr(t, err) + + headscale, err := scenario.Headscale() + assertNoErr(t, err) + + p := policy.ACLPolicy{ + ACLs: []policy.ACL{ + { + // This is an unknown action, so it will return an error + // and the config will not be applied. + Action: "acccept", + Sources: []string{"*"}, + Destinations: []string{"*:*"}, + }, + }, + TagOwners: map[string][]string{ + "tag:exists": {"policy-user"}, + }, + } + + pBytes, _ := json.Marshal(p) + + policyFilePath := "/etc/headscale/policy.json" + + err = headscale.WriteFile(policyFilePath, pBytes) + assertNoErr(t, err) + + // No policy is present at this time. + // Add a new policy from a file. + _, err = headscale.Execute( + []string{ + "headscale", + "policy", + "set", + "-f", + policyFilePath, + }, + ) + assert.ErrorContains(t, err, "verifying policy rules: invalid action") + + // The new policy was invalid, the old one should still be in place, which + // is none. + _, err = headscale.Execute( + []string{ + "headscale", + "policy", + "get", + "--output", + "json", + }, + ) + assert.ErrorContains(t, err, "acl policy not found") +} diff --git a/integration/dockertestutil/execute.go b/integration/dockertestutil/execute.go index 5a8e92b3..1b41e324 100644 --- a/integration/dockertestutil/execute.go +++ b/integration/dockertestutil/execute.go @@ -62,7 +62,7 @@ func ExecuteCommand( exitCode, err := resource.Exec( cmd, dockertest.ExecOptions{ - Env: append(env, "HEADSCALE_LOG_LEVEL=disabled"), + Env: append(env, "HEADSCALE_LOG_LEVEL=info"), StdOut: &stdout, StdErr: &stderr, }, diff --git a/integration/hsic/hsic.go b/integration/hsic/hsic.go index 0b5a6be3..bef05818 100644 --- a/integration/hsic/hsic.go +++ b/integration/hsic/hsic.go @@ -551,7 +551,7 @@ func (t *HeadscaleInContainer) Execute( log.Printf("command stdout: %s\n", stdout) } - return "", err + return stdout, fmt.Errorf("executing command in docker: %w, stderr: %s", err, stderr) } return stdout, nil From cb0e2e44764b7f925aa38b742d5eb42b97814aaf Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 30 Aug 2024 16:59:24 +0200 Subject: [PATCH 118/145] various doc updates in prep for 0.23 (#2091) * various doc updates in prep for 0.23 Signed-off-by: Kristoffer Dalby * add note discouraging postgresql Signed-off-by: Kristoffer Dalby * Update docs/faq.md Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * remove entry for glossary in doc Signed-off-by: Kristoffer Dalby * fix typo Signed-off-by: Kristoffer Dalby --------- Signed-off-by: Kristoffer Dalby Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- config-example.yaml | 5 ++++ docs/faq.md | 2 +- docs/glossary.md | 6 ----- docs/index.md | 7 +----- docs/running-headscale-container.md | 36 +++++------------------------ mkdocs.yml | 1 - 6 files changed, 13 insertions(+), 44 deletions(-) delete mode 100644 docs/glossary.md diff --git a/config-example.yaml b/config-example.yaml index 2735eaf7..37c205e1 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -138,6 +138,9 @@ disable_check_updates: false ephemeral_node_inactivity_timeout: 30m database: + # Database type. Available options: sqlite, postgres + # Please not that using Postgres is highly discouraged as it is only supported for legacy reasons. + # All new development, testing and optimisations are done with SQLite in mind. type: sqlite # Enable debug mode. This setting requires the log.level to be set to "debug" or "trace". @@ -166,6 +169,8 @@ database: write_ahead_log: true # # Postgres config + # Please note that using Postgres is highly discouraged as it is only supported for legacy reasons. + # See database.type for more information. # postgres: # # If using a Unix socket to connect to Postgres, set the socket path in the 'host' field and leave 'port' blank. # host: localhost diff --git a/docs/faq.md b/docs/faq.md index ba30911b..2a459967 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -31,7 +31,7 @@ We are more than happy to exchange emails, or to have dedicated calls before a P ## When/Why is Feature X going to be implemented? -We don't know. We might be working on it. If you want to help, please send us a PR. +We don't know. We might be working on it. If you're interested in contributing, please post a feature request about it. Please be aware that there are a number of reasons why we might not accept specific contributions: diff --git a/docs/glossary.md b/docs/glossary.md deleted file mode 100644 index f42941a6..00000000 --- a/docs/glossary.md +++ /dev/null @@ -1,6 +0,0 @@ -# Glossary - -| Term | Description | -| --------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -| Machine | A machine is a single entity connected to `headscale`, typically an installation of Tailscale. Also known as **Node** | -| Namespace | A namespace was a logical grouping of machines "owned" by the same entity, in Tailscale, this is typically a User (This is now called user) | diff --git a/docs/index.md b/docs/index.md index f0b8bb00..f1b6e1b1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -31,12 +31,7 @@ buttons available in the repo. Headscale is "Open Source, acknowledged contribution", this means that any contribution will have to be discussed with the Maintainers before being submitted. -This model has been chosen to reduce the risk of burnout by limiting the -maintenance overhead of reviewing and validating third-party code. - -Headscale is open to code contributions for bug fixes without discussion. - -If you find mistakes in the documentation, please submit a fix to the documentation. +Please see [CONTRIBUTING.md](https://github.com/juanfont/headscale/blob/main/CONTRIBUTING.md) for more information. ## About diff --git a/docs/running-headscale-container.md b/docs/running-headscale-container.md index 73c1107e..8f5cc7f9 100644 --- a/docs/running-headscale-container.md +++ b/docs/running-headscale-container.md @@ -42,36 +42,12 @@ not work with alternatives like [Podman](https://podman.io). The Docker image ca curl https://raw.githubusercontent.com/juanfont/headscale/main/config-example.yaml -o ./config/config.yaml ``` - - **(Advanced)** If you would like to hand craft a config file **instead** of downloading the example config file, create a blank `headscale` configuration in the headscale directory to edit: + Modify the config file to your preferences before launching Docker container. + Here are some settings that you likely want: - ```shell - touch ./config/config.yaml - ``` - - Modify the config file to your preferences before launching Docker container. - Here are some settings that you likely want: - - ```yaml - # Change to your hostname or host IP - server_url: http://your-host-name:8080 - # Listen to 0.0.0.0 so it's accessible outside the container - metrics_listen_addr: 0.0.0.0:9090 - # The default /var/lib/headscale path is not writable in the container - noise: - private_key_path: /etc/headscale/noise_private.key - # The default /var/lib/headscale path is not writable in the container - derp: - private_key_path: /etc/headscale/private.key - # The default /var/run/headscale path is not writable in the container - unix_socket: /etc/headscale/headscale.sock - # The default /var/lib/headscale path is not writable in the container - database.type: sqlite3 - database.sqlite.path: /etc/headscale/db.sqlite - ``` - - Alternatively, you can mount `/var/lib` and `/var/run` from your host system by adding - `--volume $(pwd)/lib:/var/lib/headscale` and `--volume $(pwd)/run:/var/run/headscale` - in the next step. + Alternatively, you can mount `/var/lib` and `/var/run` from your host system by adding + `--volume $(pwd)/lib:/var/lib/headscale` and `--volume $(pwd)/run:/var/run/headscale` + in the next step. 1. Start the headscale server while working in the host headscale directory: @@ -95,7 +71,7 @@ not work with alternatives like [Podman](https://podman.io). The Docker image ca ```yaml version: "3.7" - + services: headscale: image: headscale/headscale:0.22.3 diff --git a/mkdocs.yml b/mkdocs.yml index 86a15469..2dca103d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -144,4 +144,3 @@ nav: - Proposals: - ACLs: proposals/001-acls.md - Better routing: proposals/002-better-routing.md - - Glossary: glossary.md From 1193a50e9ed260324de76e23a5744f6473ca0386 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 30 Aug 2024 16:59:37 +0200 Subject: [PATCH 119/145] oldest client supported, not latest (#2086) --- CHANGELOG.md | 2 +- README.md | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93898f38..fa5d7f74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,7 @@ after improving the test harness as part of adopting [#1460](https://github.com/ - Adds additional configuration for PostgreSQL for setting max open, idle connection and idle connection lifetime. - 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.42 + - The oldest supported client is 1.42 - 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) diff --git a/README.md b/README.md index 2ee8f4eb..03802e27 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,6 @@ buttons available in the repo. - Taildrop (File Sharing) - [Access control lists](https://tailscale.com/kb/1018/acls/) - [MagicDNS](https://tailscale.com/kb/1081/magicdns) -- Support for multiple IP ranges in the tailnet - Dual stack (IPv4 and IPv6) - Routing advertising (including exit nodes) - Ephemeral nodes From a9a1a07e37ca32ba7d241eef6b96f07a53dfa114 Mon Sep 17 00:00:00 2001 From: nblock Date: Sun, 1 Sep 2024 15:08:06 +0200 Subject: [PATCH 120/145] Use dns: as config key (#2092) --- docs/dns-records.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dns-records.md b/docs/dns-records.md index d049c554..6c8fc42a 100644 --- a/docs/dns-records.md +++ b/docs/dns-records.md @@ -19,7 +19,7 @@ An example use case is to serve apps on the same host via a reverse proxy like N 1. Change the `config.yaml` to contain the desired records like so: ```yaml - dns_config: + dns: ... extra_records: - name: "prometheus.myvpn.example.com" From 976cbfa630599fb772549c3b305d5bda5eb3093c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 1 Sep 2024 13:08:57 +0000 Subject: [PATCH 121/145] flake.lock: Update (#2078) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 82daf973..cd36fb42 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1724363052, - "narHash": "sha256-Nf/iQWamRVAwAPFccQMfm5Qcf+rLLnU1rWG3f9orDVE=", + "lastModified": 1725099143, + "narHash": "sha256-CHgumPZaC7z+WYx72WgaLt2XF0yUVzJS60rO4GZ7ytY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "5de1564aed415bf9d0f281461babc2d101dd49ff", + "rev": "5629520edecb69630a3f4d17d3d33fc96c13f6fe", "type": "github" }, "original": { From ed71d230ebdf8cf1222b27969abf4c9077622502 Mon Sep 17 00:00:00 2001 From: nblock Date: Sun, 1 Sep 2024 15:09:47 +0200 Subject: [PATCH 122/145] Remove references to tests/acls from the documentation (#2088) --- docs/acls.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/acls.md b/docs/acls.md index 2330cda9..0b9a885f 100644 --- a/docs/acls.md +++ b/docs/acls.md @@ -3,7 +3,7 @@ Headscale implements the same policy ACLs as Tailscale.com, adapted to the self- For instance, instead of referring to users when defining groups you must use users (which are the equivalent to user/logins in Tailscale.com). -Please check https://tailscale.com/kb/1018/acls/, and `./tests/acls/` in this repo for working examples. +Please check https://tailscale.com/kb/1018/acls/ for further information. When using ACL's the User borders are no longer applied. All machines whichever the User have the ability to communicate with other hosts as @@ -43,7 +43,7 @@ servers. Note: Users will be created automatically when users authenticate with the Headscale server. -ACLs have to be written in [huJSON](https://github.com/tailscale/hujson). Check the [test ACLs](../tests/acls) for further information. +ACLs have to be written in [huJSON](https://github.com/tailscale/hujson). When registering the servers we will need to add the flag `--advertise-tags=tag:,tag:`, and the user that is From aa0f3d43cc179d14ceae904db035655d1525b126 Mon Sep 17 00:00:00 2001 From: nblock Date: Mon, 2 Sep 2024 08:18:16 +0200 Subject: [PATCH 123/145] Fix typo in example config (#2095) --- config-example.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config-example.yaml b/config-example.yaml index 37c205e1..04a2f342 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -139,7 +139,7 @@ ephemeral_node_inactivity_timeout: 30m database: # Database type. Available options: sqlite, postgres - # Please not that using Postgres is highly discouraged as it is only supported for legacy reasons. + # Please note that using Postgres is highly discouraged as it is only supported for legacy reasons. # All new development, testing and optimisations are done with SQLite in mind. type: sqlite From 3101f895a7375266a76149b2c9d1a02f17295358 Mon Sep 17 00:00:00 2001 From: Mike Poindexter Date: Tue, 3 Sep 2024 00:22:17 -0700 Subject: [PATCH 124/145] Fix 764 (#2093) * Fix KeyExpiration when a zero time value has a timezone When a zero time value is loaded from JSON or a DB in a way that assigns it the local timezone, it does not roudtrip in JSON as a value for which IsZero returns true. This causes KeyExpiry to be treated as a far past value instead of a nilish value. See https://github.com/golang/go/issues/57040 * Fix whitespace * Ensure that postgresql is used for all tests when env var is set * Pass through value of HEADSCALE_INTEGRATION_POSTGRES env var * Add option to set timezone on headscale container * Add test for registration with auth key in alternate timezone --- .github/workflows/test-integration.yaml | 1 + CHANGELOG.md | 2 + hscontrol/mapper/tail.go | 4 +- hscontrol/mapper/tail_test.go | 66 +++++++++++++++++++++++++ integration/general_test.go | 10 +++- integration/hsic/hsic.go | 6 +++ integration/run.sh | 1 + integration/scenario.go | 8 +-- 8 files changed, 91 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test-integration.yaml b/.github/workflows/test-integration.yaml index aa220261..d5b362b7 100644 --- a/.github/workflows/test-integration.yaml +++ b/.github/workflows/test-integration.yaml @@ -45,6 +45,7 @@ jobs: - TestPingAllByIPPublicDERP - TestAuthKeyLogoutAndRelogin - TestEphemeral + - TestEphemeralInAlternateTimezone - TestEphemeral2006DeletedTooQuickly - TestPingAllByHostname - TestTaildrop diff --git a/CHANGELOG.md b/CHANGELOG.md index fa5d7f74..bbb837fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,8 @@ after improving the test harness as part of adopting [#1460](https://github.com/ - Make registration page easier to use on mobile devices - Make write-ahead-log default on and configurable for SQLite [#1985](https://github.com/juanfont/headscale/pull/1985) - Add APIs for managing headscale policy. [#1792](https://github.com/juanfont/headscale/pull/1792) +- Fix for registering nodes using preauthkeys when running on a postgres database in a non-UTC timezone. [#764](https://github.com/juanfont/headscale/issues/764) +- Make sure integration tests cover postgres for all scenarios ## 0.22.3 (2023-05-12) diff --git a/hscontrol/mapper/tail.go b/hscontrol/mapper/tail.go index d21e4d8d..b0878d1a 100644 --- a/hscontrol/mapper/tail.go +++ b/hscontrol/mapper/tail.go @@ -93,7 +93,7 @@ func tailNode( User: tailcfg.UserID(node.UserID), Key: node.NodeKey, - KeyExpiry: keyExpiry, + KeyExpiry: keyExpiry.UTC(), Machine: node.MachineKey, DiscoKey: node.DiscoKey, @@ -102,7 +102,7 @@ func tailNode( Endpoints: node.Endpoints, DERP: derp, Hostinfo: node.Hostinfo.View(), - Created: node.CreatedAt, + Created: node.CreatedAt.UTC(), Online: node.IsOnline, diff --git a/hscontrol/mapper/tail_test.go b/hscontrol/mapper/tail_test.go index ac50d5a6..f744c9c6 100644 --- a/hscontrol/mapper/tail_test.go +++ b/hscontrol/mapper/tail_test.go @@ -1,6 +1,7 @@ package mapper import ( + "encoding/json" "net/netip" "testing" "time" @@ -205,3 +206,68 @@ func TestTailNode(t *testing.T) { }) } } + +func TestNodeExpiry(t *testing.T) { + tp := func(t time.Time) *time.Time { + return &t + } + tests := []struct { + name string + exp *time.Time + wantTime time.Time + wantTimeZero bool + }{ + { + name: "no-expiry", + exp: nil, + wantTimeZero: true, + }, + { + name: "zero-expiry", + exp: &time.Time{}, + wantTimeZero: true, + }, + { + name: "localtime", + exp: tp(time.Time{}.Local()), + wantTimeZero: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + node := &types.Node{ + GivenName: "test", + Expiry: tt.exp, + } + tn, err := tailNode( + node, + 0, + &policy.ACLPolicy{}, + &types.Config{}, + ) + if err != nil { + t.Fatalf("nodeExpiry() error = %v", err) + } + + // Round trip the node through JSON to ensure the time is serialized correctly + seri, err := json.Marshal(tn) + if err != nil { + t.Fatalf("nodeExpiry() error = %v", err) + } + var deseri tailcfg.Node + err = json.Unmarshal(seri, &deseri) + if err != nil { + t.Fatalf("nodeExpiry() error = %v", err) + } + + if tt.wantTimeZero { + if !deseri.KeyExpiry.IsZero() { + t.Errorf("nodeExpiry() = %v, want zero", deseri.KeyExpiry) + } + } else if deseri.KeyExpiry != tt.wantTime { + t.Errorf("nodeExpiry() = %v, want %v", deseri.KeyExpiry, tt.wantTime) + } + }) + } +} diff --git a/integration/general_test.go b/integration/general_test.go index 2819edb2..6de00fd2 100644 --- a/integration/general_test.go +++ b/integration/general_test.go @@ -215,6 +215,14 @@ func TestAuthKeyLogoutAndRelogin(t *testing.T) { } func TestEphemeral(t *testing.T) { + testEphemeralWithOptions(t, hsic.WithTestName("ephemeral")) +} + +func TestEphemeralInAlternateTimezone(t *testing.T) { + testEphemeralWithOptions(t, hsic.WithTestName("ephemeral-tz"), hsic.WithTimezone("America/Los_Angeles")) +} + +func testEphemeralWithOptions(t *testing.T, opts ...hsic.Option) { IntegrationSkip(t) t.Parallel() @@ -227,7 +235,7 @@ func TestEphemeral(t *testing.T) { "user2": len(MustTestVersions), } - headscale, err := scenario.Headscale(hsic.WithTestName("ephemeral")) + headscale, err := scenario.Headscale(opts...) assertNoErrHeadscaleEnv(t, err) for userName, clientCount := range spec { diff --git a/integration/hsic/hsic.go b/integration/hsic/hsic.go index bef05818..b9026225 100644 --- a/integration/hsic/hsic.go +++ b/integration/hsic/hsic.go @@ -211,6 +211,12 @@ func WithTuning(batchTimeout time.Duration, mapSessionChanSize int) Option { } } +func WithTimezone(timezone string) Option { + return func(hsic *HeadscaleInContainer) { + hsic.env["TZ"] = timezone + } +} + // New returns a new HeadscaleInContainer instance. func New( pool *dockertest.Pool, diff --git a/integration/run.sh b/integration/run.sh index 8cad3f02..137bcfb7 100755 --- a/integration/run.sh +++ b/integration/run.sh @@ -26,6 +26,7 @@ run_tests() { --volume "$PWD:$PWD" -w "$PWD"/integration \ --volume /var/run/docker.sock:/var/run/docker.sock \ --volume "$PWD"/control_logs:/tmp/control \ + -e "HEADSCALE_INTEGRATION_POSTGRES" \ golang:1 \ go test ./... \ -failfast \ diff --git a/integration/scenario.go b/integration/scenario.go index 6476fd58..075d1fd5 100644 --- a/integration/scenario.go +++ b/integration/scenario.go @@ -249,6 +249,10 @@ func (s *Scenario) Headscale(opts ...hsic.Option) (ControlServer, error) { return headscale, nil } + if usePostgresForTest { + opts = append(opts, hsic.WithPostgres()) + } + headscale, err := hsic.New(s.pool, s.network, opts...) if err != nil { return nil, fmt.Errorf("failed to create headscale container: %w", err) @@ -465,10 +469,6 @@ func (s *Scenario) CreateHeadscaleEnv( tsOpts []tsic.Option, opts ...hsic.Option, ) error { - if usePostgresForTest { - opts = append(opts, hsic.WithPostgres()) - } - headscale, err := s.Headscale(opts...) if err != nil { return err From d66c5e144f82a6198ee701264a585c959c4c985f Mon Sep 17 00:00:00 2001 From: nblock Date: Tue, 3 Sep 2024 13:04:20 +0200 Subject: [PATCH 125/145] Update documentation for 0.23 (#2096) * docs/acl: fix path to policy file * docs/exit-node: fixup for 0.23 * Add newlines between commands to improve readability * Use nodes instead on name * Remove query parameter from link to Tailscale docs * docs/remote-cli: fix formatting * Indent blocks below line numbers to restore numbering * Fix minor typos * docs/reverse-proxy: remove version information * Websocket support is always required now * s/see detail/see details * docs/exit-node: add warning to manual documentation * Replace the warning section with a warning admonition * Fix TODO link back to the regular linux documentation * docs/openbsd: fix typos * the database is created on-the-fly * docs/sealos: fix typos * docs/container: various fixes * Remove a stray sentence * Remove "headscale" before serve * Indent line continuation * Replace hardcoded 0.22 with * Fix path in debug image to /ko-app/headscale Fixes: #1822 aa --- docs/acls.md | 2 +- docs/exit-node.md | 24 ++++++++------ docs/remote-cli.md | 46 +++++++++++++------------- docs/reverse-proxy.md | 6 ++-- docs/running-headscale-container.md | 27 ++++++--------- docs/running-headscale-linux-manual.md | 12 ++----- docs/running-headscale-openbsd.md | 12 ++----- docs/running-headscale-sealos.md | 4 +-- 8 files changed, 58 insertions(+), 75 deletions(-) diff --git a/docs/acls.md b/docs/acls.md index 0b9a885f..4ab8fb46 100644 --- a/docs/acls.md +++ b/docs/acls.md @@ -52,7 +52,7 @@ a server they can register, the check of the tags is done on headscale server and only valid tags are applied. A tag is valid if the user that is registering it is allowed to do it. -To use ACLs in headscale, you must edit your config.yaml file. In there you will find a `acl_policy_path: ""` parameter. This will need to point to your ACL file. More info on how these policies are written can be found [here](https://tailscale.com/kb/1018/acls/). +To use ACLs in headscale, you must edit your `config.yaml` file. In there you will find a `policy.path` parameter. This will need to point to your ACL file. More info on how these policies are written can be found [here](https://tailscale.com/kb/1018/acls/). Here are the ACL's to implement the same permissions as above: diff --git a/docs/exit-node.md b/docs/exit-node.md index 831652b3..797f42f4 100644 --- a/docs/exit-node.md +++ b/docs/exit-node.md @@ -21,21 +21,23 @@ To use a node as an exit node, IP forwarding must be enabled on the node. Check ```console $ # list nodes $ headscale routes list -ID | Machine | Prefix | Advertised | Enabled | Primary -1 | | 0.0.0.0/0 | false | false | - -2 | | ::/0 | false | false | - -3 | phobos | 0.0.0.0/0 | true | false | - -4 | phobos | ::/0 | true | false | - +ID | Node | Prefix | Advertised | Enabled | Primary +1 | | 0.0.0.0/0 | false | false | - +2 | | ::/0 | false | false | - +3 | phobos | 0.0.0.0/0 | true | false | - +4 | phobos | ::/0 | true | false | - + $ # enable routes for phobos $ headscale routes enable -r 3 $ headscale routes enable -r 4 + $ # Check node list again. The routes are now enabled. $ headscale routes list -ID | Machine | Prefix | Advertised | Enabled | Primary -1 | | 0.0.0.0/0 | false | false | - -2 | | ::/0 | false | false | - -3 | phobos | 0.0.0.0/0 | true | true | - -4 | phobos | ::/0 | true | true | - +ID | Node | Prefix | Advertised | Enabled | Primary +1 | | 0.0.0.0/0 | false | false | - +2 | | ::/0 | false | false | - +3 | phobos | 0.0.0.0/0 | true | true | - +4 | phobos | ::/0 | true | true | - ``` ## On the client @@ -46,4 +48,4 @@ The exit node can now be used with: $ sudo tailscale set --exit-node phobos ``` -Check the official [Tailscale documentation](https://tailscale.com/kb/1103/exit-nodes/?q=exit#step-3-use-the-exit-node) for how to do it on your device. +Check the official [Tailscale documentation](https://tailscale.com/kb/1103/exit-nodes#use-the-exit-node) for how to do it on your device. diff --git a/docs/remote-cli.md b/docs/remote-cli.md index 3d44eabc..14423852 100644 --- a/docs/remote-cli.md +++ b/docs/remote-cli.md @@ -47,40 +47,40 @@ headscale apikeys expire --prefix "" 3. Make `headscale` executable: -```shell -chmod +x /usr/local/bin/headscale -``` + ```shell + chmod +x /usr/local/bin/headscale + ``` -4. Configure the CLI through Environment Variables +4. Configure the CLI through environment variables -```shell -export HEADSCALE_CLI_ADDRESS=":" -export HEADSCALE_CLI_API_KEY="" -``` + ```shell + export HEADSCALE_CLI_ADDRESS=":" + export HEADSCALE_CLI_API_KEY="" + ``` -for example: + for example: -```shell -export HEADSCALE_CLI_ADDRESS="headscale.example.com:50443" -export HEADSCALE_CLI_API_KEY="abcde12345" -``` + ```shell + export HEADSCALE_CLI_ADDRESS="headscale.example.com:50443" + export HEADSCALE_CLI_API_KEY="abcde12345" + ``` -This will tell the `headscale` binary to connect to a remote instance, instead of looking -for a local instance (which is what it does on the server). + This will tell the `headscale` binary to connect to a remote instance, instead of looking + for a local instance (which is what it does on the server). -The API key is needed to make sure that your are allowed to access the server. The key is _not_ -needed when running directly on the server, as the connection is local. + The API key is needed to make sure that you are allowed to access the server. The key is _not_ + needed when running directly on the server, as the connection is local. 5. Test the connection -Let us run the headscale command to verify that we can connect by listing our nodes: + Let us run the headscale command to verify that we can connect by listing our nodes: -```shell -headscale nodes list -``` + ```shell + headscale nodes list + ``` -You should now be able to see a list of your nodes from your workstation, and you can -now control the `headscale` server from your workstation. + You should now be able to see a list of your nodes from your workstation, and you can + now control the `headscale` server from your workstation. ## Behind a proxy diff --git a/docs/reverse-proxy.md b/docs/reverse-proxy.md index 23c61c26..b042b348 100644 --- a/docs/reverse-proxy.md +++ b/docs/reverse-proxy.md @@ -11,9 +11,9 @@ Running headscale behind a reverse proxy is useful when running multiple applica ### WebSockets -The reverse proxy MUST be configured to support WebSockets, as it is needed for clients running Tailscale v1.30+. +The reverse proxy MUST be configured to support WebSockets to communicate with Tailscale clients. -WebSockets support is required when using the headscale embedded DERP server. In this case, you will also need to expose the UDP port used for STUN (by default, udp/3478). Please check our [config-example.yaml](https://github.com/juanfont/headscale/blob/main/config-example.yaml). +WebSockets support is also required when using the headscale embedded DERP server. In this case, you will also need to expose the UDP port used for STUN (by default, udp/3478). Please check our [config-example.yaml](https://github.com/juanfont/headscale/blob/main/config-example.yaml). ### Cloudflare @@ -80,7 +80,7 @@ Sending local reply with details upgrade_failed ### Envoy -You need add a new upgrade_type named `tailscale-control-protocol`. [see detail](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto#extensions-filters-network-http-connection-manager-v3-httpconnectionmanager-upgradeconfig) +You need to add a new upgrade_type named `tailscale-control-protocol`. [see details](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto#extensions-filters-network-http-connection-manager-v3-httpconnectionmanager-upgradeconfig) ### Istio diff --git a/docs/running-headscale-container.md b/docs/running-headscale-container.md index 8f5cc7f9..ef622f4e 100644 --- a/docs/running-headscale-container.md +++ b/docs/running-headscale-container.md @@ -22,12 +22,6 @@ not work with alternatives like [Podman](https://podman.io). The Docker image ca cd ./headscale ``` -1. Create an empty SQlite datebase in the headscale directory: - - ```shell - touch ./config/db.sqlite - ``` - 1. **(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`: @@ -43,7 +37,6 @@ not work with alternatives like [Podman](https://podman.io). The Docker image ca ``` Modify the config file to your preferences before launching Docker container. - Here are some settings that you likely want: Alternatively, you can mount `/var/lib` and `/var/run` from your host system by adding `--volume $(pwd)/lib:/var/lib/headscale` and `--volume $(pwd)/run:/var/run/headscale` @@ -59,7 +52,7 @@ not work with alternatives like [Podman](https://podman.io). The Docker image ca --publish 127.0.0.1:8080:8080 \ --publish 127.0.0.1:9090:9090 \ headscale/headscale: \ - headscale serve + serve ``` Note: use `0.0.0.0:8080:8080` instead of `127.0.0.1:8080:8080` if you want to expose the container externally. @@ -74,16 +67,16 @@ not work with alternatives like [Podman](https://podman.io). The Docker image ca services: headscale: - image: headscale/headscale:0.22.3 + image: headscale/headscale: restart: unless-stopped container_name: headscale ports: - "127.0.0.1:8080:8080" - "127.0.0.1:9090:9090" volumes: - # pls change [config_path] to the fullpath of the config folder just created - - [config_path]:/etc/headscale - command: headscale serve + # Please change to the fullpath of the config folder just created + - :/etc/headscale + command: serve ``` 1. Verify `headscale` is running: @@ -109,7 +102,7 @@ not work with alternatives like [Podman](https://podman.io). The Docker image ca ```shell docker exec headscale \ - headscale users create myfirstuser + headscale users create myfirstuser ``` ### Register a machine (normal login) @@ -124,7 +117,7 @@ To register a machine when running `headscale` in a container, take the headscal ```shell docker exec headscale \ - headscale --user myfirstuser nodes register --key + headscale --user myfirstuser nodes register --key ``` ### Register machine using a pre authenticated key @@ -152,7 +145,7 @@ To run the debug Docker container, use the exact same commands as above, but rep ### Executing commands in the debug container -The default command in the debug container is to run `headscale`, which is located at `/bin/headscale` inside the container. +The default command in the debug container is to run `headscale`, which is located at `/ko-app/headscale` inside the container. Additionally, the debug container includes a minimalist Busybox shell. @@ -162,10 +155,10 @@ To launch a shell in the container, use: docker run -it headscale/headscale:x.x.x-debug sh ``` -You can also execute commands directly, such as `ls /bin` in this example: +You can also execute commands directly, such as `ls /ko-app` in this example: ``` -docker run headscale/headscale:x.x.x-debug ls /bin +docker run headscale/headscale:x.x.x-debug ls /ko-app ``` Using `docker exec` allows you to run commands in an existing container. diff --git a/docs/running-headscale-linux-manual.md b/docs/running-headscale-linux-manual.md index 3651c892..25d47638 100644 --- a/docs/running-headscale-linux-manual.md +++ b/docs/running-headscale-linux-manual.md @@ -1,9 +1,9 @@ # Running headscale on Linux -## Note: Outdated and "advanced" +!!! warning "Outdated and advanced" -This documentation is considered the "legacy"/advanced/manual version of the documentation, you most likely do not -want to use this documentation and rather look at the distro specific documentation (TODO LINK)[]. + This documentation is considered the "legacy"/advanced/manual version of the documentation, you most likely do not + want to use this documentation and rather look at the [distro specific documentation](./running-headscale-linux.md). ## Goal @@ -45,12 +45,6 @@ describing how to make `headscale` run properly in a server environment. headscale ``` -1. Create an empty SQLite database: - - ```shell - touch /var/lib/headscale/db.sqlite - ``` - 1. Create a `headscale` configuration: ```shell diff --git a/docs/running-headscale-openbsd.md b/docs/running-headscale-openbsd.md index 72c7bf79..f3e0548e 100644 --- a/docs/running-headscale-openbsd.md +++ b/docs/running-headscale-openbsd.md @@ -10,7 +10,7 @@ ## Goal This documentation has the goal of showing a user how-to install and run `headscale` on OpenBSD. -In additional to the "get up and running section", there is an optional [rc.d section](#running-headscale-in-the-background-with-rcd) +In addition to the "get up and running section", there is an optional [rc.d section](#running-headscale-in-the-background-with-rcd) describing how to make `headscale` run properly in a server environment. ## Install `headscale` @@ -77,16 +77,10 @@ describing how to make `headscale` run properly in a server environment. mkdir -p /etc/headscale - # Directory for Database, and other variable data (like certificates) + # Directory for database, and other variable data (like certificates) mkdir -p /var/lib/headscale ``` -1. Create an empty SQLite database: - - ```shell - touch /var/lib/headscale/db.sqlite - ``` - 1. Create a `headscale` configuration: ```shell @@ -135,7 +129,7 @@ tailscale up --login-server YOUR_HEADSCALE_URL Register the machine: ```shell -headscale --user myfirstuser nodes register --key +headscale --user myfirstuser nodes register --key ``` ### Register machine using a pre authenticated key diff --git a/docs/running-headscale-sealos.md b/docs/running-headscale-sealos.md index 01aecb0e..1e3fe3ac 100644 --- a/docs/running-headscale-sealos.md +++ b/docs/running-headscale-sealos.md @@ -13,7 +13,7 @@ This documentation has the goal of showing a user how-to run `headscale` on Seal ## Running headscale server -1. Click the following prebuilt template(version [0.23.0-alpha2](https://github.com/juanfont/headscale/releases/tag/v0.23.0-alpha2)): +1. Click the following prebuilt template: [![](https://cdn.jsdelivr.net/gh/labring-actions/templates@main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-template%3FtemplateName%3Dheadscale) @@ -41,7 +41,7 @@ tailscale up --login-server YOUR_HEADSCALE_URL To register a machine when running headscale in [Sealos](https://sealos.io), click on 'Terminal' button on the right side of the headscale application's detail page to access the Terminal of the headscale application, then take the headscale command: ```bash -headscale --user myfirstuser nodes register --key +headscale --user myfirstuser nodes register --key ``` ### Register machine using a pre authenticated key From f039caf1349d1c67d47ecd880d5efca1914f09b6 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 4 Sep 2024 07:55:16 +0200 Subject: [PATCH 126/145] update godeps (#2098) --- CHANGELOG.md | 2 +- CONTRIBUTING.md | 2 +- docs/remote-cli.md | 44 ++-- flake.nix | 33 ++- gen/go/headscale/v1/apikey.pb.go | 22 +- gen/go/headscale/v1/device.pb.go | 26 +- gen/go/headscale/v1/headscale.pb.go | 4 +- gen/go/headscale/v1/headscale.pb.gw.go | 105 ++------ gen/go/headscale/v1/headscale_grpc.pb.go | 150 +++++++----- gen/go/headscale/v1/node.pb.go | 46 ++-- gen/go/headscale/v1/policy.pb.go | 12 +- gen/go/headscale/v1/preauthkey.pb.go | 18 +- gen/go/headscale/v1/routes.pb.go | 26 +- gen/go/headscale/v1/user.pb.go | 26 +- .../headscale/v1/apikey.swagger.json | 1 + .../headscale/v1/device.swagger.json | 1 + .../headscale/v1/headscale.swagger.json | 28 ++- gen/openapiv2/headscale/v1/node.swagger.json | 1 + .../headscale/v1/policy.swagger.json | 1 + .../headscale/v1/preauthkey.swagger.json | 1 + .../headscale/v1/routes.swagger.json | 1 + gen/openapiv2/headscale/v1/user.swagger.json | 1 + go.mod | 97 ++++---- go.sum | 224 +++++++++--------- proto/headscale/v1/headscale.proto | 1 - proto/headscale/v1/policy.proto | 10 +- 26 files changed, 444 insertions(+), 439 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbb837fb..76982608 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,7 +45,7 @@ after improving the test harness as part of adopting [#1460](https://github.com/ - `use_username_in_magic_dns` can be used to turn this behaviour on again, but note that this option _will be removed_ when tags are fixed. - dns.base_domain can no longer be the same as (or part of) server_url. - This option brings Headscales behaviour in line with Tailscale. -- YAML files are no longer supported for headscale policy. [#1792](https://github.com/juanfont/headscale/pull/1792) +- YAML files are no longer supported for headscale policy. [#1792](https://github.com/juanfont/headscale/pull/1792) - HuJSON is now the only supported format for policy. - DNS configuration has been restructured [#2034](https://github.com/juanfont/headscale/pull/2034) - Please review the new [config-example.yaml](./config-example.yaml) for the new structure. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 18d7dfb8..4c3ca130 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,7 +9,7 @@ Headscale has a small maintainer team that tries to balance working on the proje When we work on issues ourselves, we develop first hand knowledge of the code and it makes it possible for us to maintain and own the code as the project develops. -Code contributions are seen as a positive thing. People enjoy and engage with our project, but it also comes with some challenges; we have to understand the code, we have to understand the feature, we might have to become familiar with external libraries or services and we think about security implications. All those steps are required during the reviewing process. After the code has been merged, the feature has to be maintained. Any changes reliant on external services must be updated and expanded accordingly. +Code contributions are seen as a positive thing. People enjoy and engage with our project, but it also comes with some challenges; we have to understand the code, we have to understand the feature, we might have to become familiar with external libraries or services and we think about security implications. All those steps are required during the reviewing process. After the code has been merged, the feature has to be maintained. Any changes reliant on external services must be updated and expanded accordingly. The review and day-1 maintenance adds a significant burden on the maintainers. Often we hope that the contributor will help out, but we found that most of the time, they disappear after their new feature was added. diff --git a/docs/remote-cli.md b/docs/remote-cli.md index 14423852..c641b789 100644 --- a/docs/remote-cli.md +++ b/docs/remote-cli.md @@ -47,40 +47,40 @@ headscale apikeys expire --prefix "" 3. Make `headscale` executable: - ```shell - chmod +x /usr/local/bin/headscale - ``` + ```shell + chmod +x /usr/local/bin/headscale + ``` 4. Configure the CLI through environment variables - ```shell - export HEADSCALE_CLI_ADDRESS=":" - export HEADSCALE_CLI_API_KEY="" - ``` + ```shell + export HEADSCALE_CLI_ADDRESS=":" + export HEADSCALE_CLI_API_KEY="" + ``` - for example: + for example: - ```shell - export HEADSCALE_CLI_ADDRESS="headscale.example.com:50443" - export HEADSCALE_CLI_API_KEY="abcde12345" - ``` + ```shell + export HEADSCALE_CLI_ADDRESS="headscale.example.com:50443" + export HEADSCALE_CLI_API_KEY="abcde12345" + ``` - This will tell the `headscale` binary to connect to a remote instance, instead of looking - for a local instance (which is what it does on the server). + This will tell the `headscale` binary to connect to a remote instance, instead of looking + for a local instance (which is what it does on the server). - The API key is needed to make sure that you are allowed to access the server. The key is _not_ - needed when running directly on the server, as the connection is local. + The API key is needed to make sure that you are allowed to access the server. The key is _not_ + needed when running directly on the server, as the connection is local. 5. Test the connection - Let us run the headscale command to verify that we can connect by listing our nodes: + Let us run the headscale command to verify that we can connect by listing our nodes: - ```shell - headscale nodes list - ``` + ```shell + headscale nodes list + ``` - You should now be able to see a list of your nodes from your workstation, and you can - now control the `headscale` server from your workstation. + You should now be able to see a list of your nodes from your workstation, and you can + now control the `headscale` server from your workstation. ## Behind a proxy diff --git a/flake.nix b/flake.nix index dbf4f38f..8e009c1f 100644 --- a/flake.nix +++ b/flake.nix @@ -20,8 +20,9 @@ { overlay = _: prev: let pkgs = nixpkgs.legacyPackages.${prev.system}; + buildGo = pkgs.buildGo123Module; in rec { - headscale = pkgs.buildGo123Module rec { + headscale = buildGo rec { pname = "headscale"; version = headscaleVersion; src = pkgs.lib.cleanSource self; @@ -31,30 +32,50 @@ # When updating go.mod or go.sum, a new sha will need to be calculated, # update this if you have a mismatch after doing a change to thos files. - vendorHash = "sha256-hmBRtMPqewg4oqu2bc9HtE3wdCdl5v9MoBOOCsjYlE8="; + vendorHash = "sha256-+8dOxPG/Q+wuHgRwwWqdphHOuop0W9dVyClyQuh7aRc="; subPackages = ["cmd/headscale"]; ldflags = ["-s" "-w" "-X github.com/juanfont/headscale/cmd/headscale/cli.Version=v${version}"]; }; - protoc-gen-grpc-gateway = pkgs.buildGoModule rec { + protoc-gen-grpc-gateway = buildGo rec { pname = "grpc-gateway"; - version = "2.19.1"; + version = "2.22.0"; src = pkgs.fetchFromGitHub { owner = "grpc-ecosystem"; repo = "grpc-gateway"; rev = "v${version}"; - sha256 = "sha256-CdGQpQfOSimeio8v1lZ7xzE/oAS2qFyu+uN+H9i7vpo="; + sha256 = "sha256-I1w3gfV06J8xG1xJ+XuMIGkV2/Ofszo7SCC+z4Xb6l4="; }; - vendorHash = "sha256-no7kZGpf/VOuceC3J+izGFQp5aMS3b+Rn+x4BFZ2zgs="; + vendorHash = "sha256-S4hcD5/BSGxM2qdJHMxOkxsJ5+Ks6m4lKHSS9+yZ17c="; nativeBuildInputs = [pkgs.installShellFiles]; subPackages = ["protoc-gen-grpc-gateway" "protoc-gen-openapiv2"]; }; + + golangci-lint = prev.golangci-lint.override { + buildGoModule = buildGo; + }; + + goreleaser = prev.goreleaser.override { + buildGoModule = buildGo; + }; + + gotestsum = prev.gotestsum.override { + buildGoModule = buildGo; + }; + + gotests = prev.gotests.override { + buildGoModule = buildGo; + }; + + gofumpt = prev.gofumpt.override { + buildGoModule = buildGo; + }; }; } // flake-utils.lib.eachDefaultSystem diff --git a/gen/go/headscale/v1/apikey.pb.go b/gen/go/headscale/v1/apikey.pb.go index d1a5f555..e6263522 100644 --- a/gen/go/headscale/v1/apikey.pb.go +++ b/gen/go/headscale/v1/apikey.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 +// protoc-gen-go v1.34.2 // protoc (unknown) // source: headscale/v1/apikey.proto @@ -512,7 +512,7 @@ func file_headscale_v1_apikey_proto_rawDescGZIP() []byte { } var file_headscale_v1_apikey_proto_msgTypes = make([]protoimpl.MessageInfo, 9) -var file_headscale_v1_apikey_proto_goTypes = []interface{}{ +var file_headscale_v1_apikey_proto_goTypes = []any{ (*ApiKey)(nil), // 0: headscale.v1.ApiKey (*CreateApiKeyRequest)(nil), // 1: headscale.v1.CreateApiKeyRequest (*CreateApiKeyResponse)(nil), // 2: headscale.v1.CreateApiKeyResponse @@ -543,7 +543,7 @@ func file_headscale_v1_apikey_proto_init() { return } if !protoimpl.UnsafeEnabled { - file_headscale_v1_apikey_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_apikey_proto_msgTypes[0].Exporter = func(v any, i int) any { switch v := v.(*ApiKey); i { case 0: return &v.state @@ -555,7 +555,7 @@ func file_headscale_v1_apikey_proto_init() { return nil } } - file_headscale_v1_apikey_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_apikey_proto_msgTypes[1].Exporter = func(v any, i int) any { switch v := v.(*CreateApiKeyRequest); i { case 0: return &v.state @@ -567,7 +567,7 @@ func file_headscale_v1_apikey_proto_init() { return nil } } - file_headscale_v1_apikey_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_apikey_proto_msgTypes[2].Exporter = func(v any, i int) any { switch v := v.(*CreateApiKeyResponse); i { case 0: return &v.state @@ -579,7 +579,7 @@ func file_headscale_v1_apikey_proto_init() { return nil } } - file_headscale_v1_apikey_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_apikey_proto_msgTypes[3].Exporter = func(v any, i int) any { switch v := v.(*ExpireApiKeyRequest); i { case 0: return &v.state @@ -591,7 +591,7 @@ func file_headscale_v1_apikey_proto_init() { return nil } } - file_headscale_v1_apikey_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_apikey_proto_msgTypes[4].Exporter = func(v any, i int) any { switch v := v.(*ExpireApiKeyResponse); i { case 0: return &v.state @@ -603,7 +603,7 @@ func file_headscale_v1_apikey_proto_init() { return nil } } - file_headscale_v1_apikey_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_apikey_proto_msgTypes[5].Exporter = func(v any, i int) any { switch v := v.(*ListApiKeysRequest); i { case 0: return &v.state @@ -615,7 +615,7 @@ func file_headscale_v1_apikey_proto_init() { return nil } } - file_headscale_v1_apikey_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_apikey_proto_msgTypes[6].Exporter = func(v any, i int) any { switch v := v.(*ListApiKeysResponse); i { case 0: return &v.state @@ -627,7 +627,7 @@ func file_headscale_v1_apikey_proto_init() { return nil } } - file_headscale_v1_apikey_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_apikey_proto_msgTypes[7].Exporter = func(v any, i int) any { switch v := v.(*DeleteApiKeyRequest); i { case 0: return &v.state @@ -639,7 +639,7 @@ func file_headscale_v1_apikey_proto_init() { return nil } } - file_headscale_v1_apikey_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_apikey_proto_msgTypes[8].Exporter = func(v any, i int) any { switch v := v.(*DeleteApiKeyResponse); i { case 0: return &v.state diff --git a/gen/go/headscale/v1/device.pb.go b/gen/go/headscale/v1/device.pb.go index 40e2e24f..66c31441 100644 --- a/gen/go/headscale/v1/device.pb.go +++ b/gen/go/headscale/v1/device.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 +// protoc-gen-go v1.34.2 // protoc (unknown) // source: headscale/v1/device.proto @@ -925,7 +925,7 @@ func file_headscale_v1_device_proto_rawDescGZIP() []byte { } var file_headscale_v1_device_proto_msgTypes = make([]protoimpl.MessageInfo, 12) -var file_headscale_v1_device_proto_goTypes = []interface{}{ +var file_headscale_v1_device_proto_goTypes = []any{ (*Latency)(nil), // 0: headscale.v1.Latency (*ClientSupports)(nil), // 1: headscale.v1.ClientSupports (*ClientConnectivity)(nil), // 2: headscale.v1.ClientConnectivity @@ -961,7 +961,7 @@ func file_headscale_v1_device_proto_init() { return } if !protoimpl.UnsafeEnabled { - file_headscale_v1_device_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_device_proto_msgTypes[0].Exporter = func(v any, i int) any { switch v := v.(*Latency); i { case 0: return &v.state @@ -973,7 +973,7 @@ func file_headscale_v1_device_proto_init() { return nil } } - file_headscale_v1_device_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_device_proto_msgTypes[1].Exporter = func(v any, i int) any { switch v := v.(*ClientSupports); i { case 0: return &v.state @@ -985,7 +985,7 @@ func file_headscale_v1_device_proto_init() { return nil } } - file_headscale_v1_device_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_device_proto_msgTypes[2].Exporter = func(v any, i int) any { switch v := v.(*ClientConnectivity); i { case 0: return &v.state @@ -997,7 +997,7 @@ func file_headscale_v1_device_proto_init() { return nil } } - file_headscale_v1_device_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_device_proto_msgTypes[3].Exporter = func(v any, i int) any { switch v := v.(*GetDeviceRequest); i { case 0: return &v.state @@ -1009,7 +1009,7 @@ func file_headscale_v1_device_proto_init() { return nil } } - file_headscale_v1_device_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_device_proto_msgTypes[4].Exporter = func(v any, i int) any { switch v := v.(*GetDeviceResponse); i { case 0: return &v.state @@ -1021,7 +1021,7 @@ func file_headscale_v1_device_proto_init() { return nil } } - file_headscale_v1_device_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_device_proto_msgTypes[5].Exporter = func(v any, i int) any { switch v := v.(*DeleteDeviceRequest); i { case 0: return &v.state @@ -1033,7 +1033,7 @@ func file_headscale_v1_device_proto_init() { return nil } } - file_headscale_v1_device_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_device_proto_msgTypes[6].Exporter = func(v any, i int) any { switch v := v.(*DeleteDeviceResponse); i { case 0: return &v.state @@ -1045,7 +1045,7 @@ func file_headscale_v1_device_proto_init() { return nil } } - file_headscale_v1_device_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_device_proto_msgTypes[7].Exporter = func(v any, i int) any { switch v := v.(*GetDeviceRoutesRequest); i { case 0: return &v.state @@ -1057,7 +1057,7 @@ func file_headscale_v1_device_proto_init() { return nil } } - file_headscale_v1_device_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_device_proto_msgTypes[8].Exporter = func(v any, i int) any { switch v := v.(*GetDeviceRoutesResponse); i { case 0: return &v.state @@ -1069,7 +1069,7 @@ func file_headscale_v1_device_proto_init() { return nil } } - file_headscale_v1_device_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_device_proto_msgTypes[9].Exporter = func(v any, i int) any { switch v := v.(*EnableDeviceRoutesRequest); i { case 0: return &v.state @@ -1081,7 +1081,7 @@ func file_headscale_v1_device_proto_init() { return nil } } - file_headscale_v1_device_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_device_proto_msgTypes[10].Exporter = func(v any, i int) any { switch v := v.(*EnableDeviceRoutesResponse); i { case 0: return &v.state diff --git a/gen/go/headscale/v1/headscale.pb.go b/gen/go/headscale/v1/headscale.pb.go index 63e7d536..d6751864 100644 --- a/gen/go/headscale/v1/headscale.pb.go +++ b/gen/go/headscale/v1/headscale.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 +// protoc-gen-go v1.34.2 // protoc (unknown) // source: headscale/v1/headscale.proto @@ -257,7 +257,7 @@ var file_headscale_v1_headscale_proto_rawDesc = []byte{ 0x33, } -var file_headscale_v1_headscale_proto_goTypes = []interface{}{ +var file_headscale_v1_headscale_proto_goTypes = []any{ (*GetUserRequest)(nil), // 0: headscale.v1.GetUserRequest (*CreateUserRequest)(nil), // 1: headscale.v1.CreateUserRequest (*RenameUserRequest)(nil), // 2: headscale.v1.RenameUserRequest diff --git a/gen/go/headscale/v1/headscale.pb.gw.go b/gen/go/headscale/v1/headscale.pb.gw.go index 98c6039b..59a98ce3 100644 --- a/gen/go/headscale/v1/headscale.pb.gw.go +++ b/gen/go/headscale/v1/headscale.pb.gw.go @@ -87,11 +87,7 @@ func request_HeadscaleService_CreateUser_0(ctx context.Context, marshaler runtim var protoReq CreateUserRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -104,11 +100,7 @@ func local_request_HeadscaleService_CreateUser_0(ctx context.Context, marshaler var protoReq CreateUserRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -263,11 +255,7 @@ func request_HeadscaleService_CreatePreAuthKey_0(ctx context.Context, marshaler var protoReq CreatePreAuthKeyRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -280,11 +268,7 @@ func local_request_HeadscaleService_CreatePreAuthKey_0(ctx context.Context, mars var protoReq CreatePreAuthKeyRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -297,11 +281,7 @@ func request_HeadscaleService_ExpirePreAuthKey_0(ctx context.Context, marshaler var protoReq ExpirePreAuthKeyRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -314,11 +294,7 @@ func local_request_HeadscaleService_ExpirePreAuthKey_0(ctx context.Context, mars var protoReq ExpirePreAuthKeyRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -367,11 +343,7 @@ func request_HeadscaleService_DebugCreateNode_0(ctx context.Context, marshaler r var protoReq DebugCreateNodeRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -384,11 +356,7 @@ func local_request_HeadscaleService_DebugCreateNode_0(ctx context.Context, marsh var protoReq DebugCreateNodeRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -453,11 +421,7 @@ func request_HeadscaleService_SetTags_0(ctx context.Context, marshaler runtime.M var protoReq SetTagsRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -487,11 +451,7 @@ func local_request_HeadscaleService_SetTags_0(ctx context.Context, marshaler run var protoReq SetTagsRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -1101,11 +1061,7 @@ func request_HeadscaleService_CreateApiKey_0(ctx context.Context, marshaler runt var protoReq CreateApiKeyRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -1118,11 +1074,7 @@ func local_request_HeadscaleService_CreateApiKey_0(ctx context.Context, marshale var protoReq CreateApiKeyRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -1135,11 +1087,7 @@ func request_HeadscaleService_ExpireApiKey_0(ctx context.Context, marshaler runt var protoReq ExpireApiKeyRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -1152,11 +1100,7 @@ func local_request_HeadscaleService_ExpireApiKey_0(ctx context.Context, marshale var protoReq ExpireApiKeyRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -1257,11 +1201,7 @@ func request_HeadscaleService_SetPolicy_0(ctx context.Context, marshaler runtime var protoReq SetPolicyRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -1274,11 +1214,7 @@ func local_request_HeadscaleService_SetPolicy_0(ctx context.Context, marshaler r var protoReq SetPolicyRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -1291,6 +1227,7 @@ func local_request_HeadscaleService_SetPolicy_0(ctx context.Context, marshaler r // UnaryRPC :call HeadscaleServiceServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. // Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterHeadscaleServiceHandlerFromEndpoint instead. +// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call. func RegisterHeadscaleServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server HeadscaleServiceServer) error { mux.Handle("GET", pattern_HeadscaleService_GetUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { @@ -2024,21 +1961,21 @@ func RegisterHeadscaleServiceHandlerServer(ctx context.Context, mux *runtime.Ser // RegisterHeadscaleServiceHandlerFromEndpoint is same as RegisterHeadscaleServiceHandler but // automatically dials to "endpoint" and closes the connection when "ctx" gets done. func RegisterHeadscaleServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { - conn, err := grpc.Dial(endpoint, opts...) + conn, err := grpc.NewClient(endpoint, opts...) if err != nil { return err } defer func() { if err != nil { if cerr := conn.Close(); cerr != nil { - grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) } return } go func() { <-ctx.Done() if cerr := conn.Close(); cerr != nil { - grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) } }() }() @@ -2056,7 +1993,7 @@ func RegisterHeadscaleServiceHandler(ctx context.Context, mux *runtime.ServeMux, // to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "HeadscaleServiceClient". // Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "HeadscaleServiceClient" // doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in -// "HeadscaleServiceClient" to call the correct interceptors. +// "HeadscaleServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares. func RegisterHeadscaleServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client HeadscaleServiceClient) error { mux.Handle("GET", pattern_HeadscaleService_GetUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { diff --git a/gen/go/headscale/v1/headscale_grpc.pb.go b/gen/go/headscale/v1/headscale_grpc.pb.go index df9cf197..d57aa92e 100644 --- a/gen/go/headscale/v1/headscale_grpc.pb.go +++ b/gen/go/headscale/v1/headscale_grpc.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.2.0 +// - protoc-gen-go-grpc v1.3.0 // - protoc (unknown) // source: headscale/v1/headscale.proto @@ -18,6 +18,38 @@ import ( // Requires gRPC-Go v1.32.0 or later. const _ = grpc.SupportPackageIsVersion7 +const ( + HeadscaleService_GetUser_FullMethodName = "/headscale.v1.HeadscaleService/GetUser" + HeadscaleService_CreateUser_FullMethodName = "/headscale.v1.HeadscaleService/CreateUser" + HeadscaleService_RenameUser_FullMethodName = "/headscale.v1.HeadscaleService/RenameUser" + HeadscaleService_DeleteUser_FullMethodName = "/headscale.v1.HeadscaleService/DeleteUser" + HeadscaleService_ListUsers_FullMethodName = "/headscale.v1.HeadscaleService/ListUsers" + HeadscaleService_CreatePreAuthKey_FullMethodName = "/headscale.v1.HeadscaleService/CreatePreAuthKey" + HeadscaleService_ExpirePreAuthKey_FullMethodName = "/headscale.v1.HeadscaleService/ExpirePreAuthKey" + HeadscaleService_ListPreAuthKeys_FullMethodName = "/headscale.v1.HeadscaleService/ListPreAuthKeys" + HeadscaleService_DebugCreateNode_FullMethodName = "/headscale.v1.HeadscaleService/DebugCreateNode" + HeadscaleService_GetNode_FullMethodName = "/headscale.v1.HeadscaleService/GetNode" + HeadscaleService_SetTags_FullMethodName = "/headscale.v1.HeadscaleService/SetTags" + HeadscaleService_RegisterNode_FullMethodName = "/headscale.v1.HeadscaleService/RegisterNode" + HeadscaleService_DeleteNode_FullMethodName = "/headscale.v1.HeadscaleService/DeleteNode" + HeadscaleService_ExpireNode_FullMethodName = "/headscale.v1.HeadscaleService/ExpireNode" + HeadscaleService_RenameNode_FullMethodName = "/headscale.v1.HeadscaleService/RenameNode" + HeadscaleService_ListNodes_FullMethodName = "/headscale.v1.HeadscaleService/ListNodes" + HeadscaleService_MoveNode_FullMethodName = "/headscale.v1.HeadscaleService/MoveNode" + HeadscaleService_BackfillNodeIPs_FullMethodName = "/headscale.v1.HeadscaleService/BackfillNodeIPs" + HeadscaleService_GetRoutes_FullMethodName = "/headscale.v1.HeadscaleService/GetRoutes" + HeadscaleService_EnableRoute_FullMethodName = "/headscale.v1.HeadscaleService/EnableRoute" + HeadscaleService_DisableRoute_FullMethodName = "/headscale.v1.HeadscaleService/DisableRoute" + HeadscaleService_GetNodeRoutes_FullMethodName = "/headscale.v1.HeadscaleService/GetNodeRoutes" + HeadscaleService_DeleteRoute_FullMethodName = "/headscale.v1.HeadscaleService/DeleteRoute" + HeadscaleService_CreateApiKey_FullMethodName = "/headscale.v1.HeadscaleService/CreateApiKey" + HeadscaleService_ExpireApiKey_FullMethodName = "/headscale.v1.HeadscaleService/ExpireApiKey" + HeadscaleService_ListApiKeys_FullMethodName = "/headscale.v1.HeadscaleService/ListApiKeys" + HeadscaleService_DeleteApiKey_FullMethodName = "/headscale.v1.HeadscaleService/DeleteApiKey" + HeadscaleService_GetPolicy_FullMethodName = "/headscale.v1.HeadscaleService/GetPolicy" + HeadscaleService_SetPolicy_FullMethodName = "/headscale.v1.HeadscaleService/SetPolicy" +) + // HeadscaleServiceClient is the client API for HeadscaleService service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. @@ -69,7 +101,7 @@ func NewHeadscaleServiceClient(cc grpc.ClientConnInterface) HeadscaleServiceClie func (c *headscaleServiceClient) GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*GetUserResponse, error) { out := new(GetUserResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/GetUser", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_GetUser_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -78,7 +110,7 @@ func (c *headscaleServiceClient) GetUser(ctx context.Context, in *GetUserRequest func (c *headscaleServiceClient) CreateUser(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*CreateUserResponse, error) { out := new(CreateUserResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/CreateUser", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_CreateUser_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -87,7 +119,7 @@ func (c *headscaleServiceClient) CreateUser(ctx context.Context, in *CreateUserR func (c *headscaleServiceClient) RenameUser(ctx context.Context, in *RenameUserRequest, opts ...grpc.CallOption) (*RenameUserResponse, error) { out := new(RenameUserResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/RenameUser", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_RenameUser_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -96,7 +128,7 @@ func (c *headscaleServiceClient) RenameUser(ctx context.Context, in *RenameUserR func (c *headscaleServiceClient) DeleteUser(ctx context.Context, in *DeleteUserRequest, opts ...grpc.CallOption) (*DeleteUserResponse, error) { out := new(DeleteUserResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/DeleteUser", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_DeleteUser_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -105,7 +137,7 @@ func (c *headscaleServiceClient) DeleteUser(ctx context.Context, in *DeleteUserR func (c *headscaleServiceClient) ListUsers(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (*ListUsersResponse, error) { out := new(ListUsersResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ListUsers", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_ListUsers_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -114,7 +146,7 @@ func (c *headscaleServiceClient) ListUsers(ctx context.Context, in *ListUsersReq func (c *headscaleServiceClient) CreatePreAuthKey(ctx context.Context, in *CreatePreAuthKeyRequest, opts ...grpc.CallOption) (*CreatePreAuthKeyResponse, error) { out := new(CreatePreAuthKeyResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/CreatePreAuthKey", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_CreatePreAuthKey_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -123,7 +155,7 @@ func (c *headscaleServiceClient) CreatePreAuthKey(ctx context.Context, in *Creat func (c *headscaleServiceClient) ExpirePreAuthKey(ctx context.Context, in *ExpirePreAuthKeyRequest, opts ...grpc.CallOption) (*ExpirePreAuthKeyResponse, error) { out := new(ExpirePreAuthKeyResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ExpirePreAuthKey", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_ExpirePreAuthKey_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -132,7 +164,7 @@ func (c *headscaleServiceClient) ExpirePreAuthKey(ctx context.Context, in *Expir func (c *headscaleServiceClient) ListPreAuthKeys(ctx context.Context, in *ListPreAuthKeysRequest, opts ...grpc.CallOption) (*ListPreAuthKeysResponse, error) { out := new(ListPreAuthKeysResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ListPreAuthKeys", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_ListPreAuthKeys_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -141,7 +173,7 @@ func (c *headscaleServiceClient) ListPreAuthKeys(ctx context.Context, in *ListPr func (c *headscaleServiceClient) DebugCreateNode(ctx context.Context, in *DebugCreateNodeRequest, opts ...grpc.CallOption) (*DebugCreateNodeResponse, error) { out := new(DebugCreateNodeResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/DebugCreateNode", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_DebugCreateNode_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -150,7 +182,7 @@ func (c *headscaleServiceClient) DebugCreateNode(ctx context.Context, in *DebugC func (c *headscaleServiceClient) GetNode(ctx context.Context, in *GetNodeRequest, opts ...grpc.CallOption) (*GetNodeResponse, error) { out := new(GetNodeResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/GetNode", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_GetNode_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -159,7 +191,7 @@ func (c *headscaleServiceClient) GetNode(ctx context.Context, in *GetNodeRequest func (c *headscaleServiceClient) SetTags(ctx context.Context, in *SetTagsRequest, opts ...grpc.CallOption) (*SetTagsResponse, error) { out := new(SetTagsResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/SetTags", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_SetTags_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -168,7 +200,7 @@ func (c *headscaleServiceClient) SetTags(ctx context.Context, in *SetTagsRequest func (c *headscaleServiceClient) RegisterNode(ctx context.Context, in *RegisterNodeRequest, opts ...grpc.CallOption) (*RegisterNodeResponse, error) { out := new(RegisterNodeResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/RegisterNode", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_RegisterNode_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -177,7 +209,7 @@ func (c *headscaleServiceClient) RegisterNode(ctx context.Context, in *RegisterN func (c *headscaleServiceClient) DeleteNode(ctx context.Context, in *DeleteNodeRequest, opts ...grpc.CallOption) (*DeleteNodeResponse, error) { out := new(DeleteNodeResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/DeleteNode", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_DeleteNode_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -186,7 +218,7 @@ func (c *headscaleServiceClient) DeleteNode(ctx context.Context, in *DeleteNodeR func (c *headscaleServiceClient) ExpireNode(ctx context.Context, in *ExpireNodeRequest, opts ...grpc.CallOption) (*ExpireNodeResponse, error) { out := new(ExpireNodeResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ExpireNode", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_ExpireNode_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -195,7 +227,7 @@ func (c *headscaleServiceClient) ExpireNode(ctx context.Context, in *ExpireNodeR func (c *headscaleServiceClient) RenameNode(ctx context.Context, in *RenameNodeRequest, opts ...grpc.CallOption) (*RenameNodeResponse, error) { out := new(RenameNodeResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/RenameNode", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_RenameNode_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -204,7 +236,7 @@ func (c *headscaleServiceClient) RenameNode(ctx context.Context, in *RenameNodeR func (c *headscaleServiceClient) ListNodes(ctx context.Context, in *ListNodesRequest, opts ...grpc.CallOption) (*ListNodesResponse, error) { out := new(ListNodesResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ListNodes", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_ListNodes_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -213,7 +245,7 @@ func (c *headscaleServiceClient) ListNodes(ctx context.Context, in *ListNodesReq func (c *headscaleServiceClient) MoveNode(ctx context.Context, in *MoveNodeRequest, opts ...grpc.CallOption) (*MoveNodeResponse, error) { out := new(MoveNodeResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/MoveNode", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_MoveNode_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -222,7 +254,7 @@ func (c *headscaleServiceClient) MoveNode(ctx context.Context, in *MoveNodeReque func (c *headscaleServiceClient) BackfillNodeIPs(ctx context.Context, in *BackfillNodeIPsRequest, opts ...grpc.CallOption) (*BackfillNodeIPsResponse, error) { out := new(BackfillNodeIPsResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/BackfillNodeIPs", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_BackfillNodeIPs_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -231,7 +263,7 @@ func (c *headscaleServiceClient) BackfillNodeIPs(ctx context.Context, in *Backfi func (c *headscaleServiceClient) GetRoutes(ctx context.Context, in *GetRoutesRequest, opts ...grpc.CallOption) (*GetRoutesResponse, error) { out := new(GetRoutesResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/GetRoutes", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_GetRoutes_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -240,7 +272,7 @@ func (c *headscaleServiceClient) GetRoutes(ctx context.Context, in *GetRoutesReq func (c *headscaleServiceClient) EnableRoute(ctx context.Context, in *EnableRouteRequest, opts ...grpc.CallOption) (*EnableRouteResponse, error) { out := new(EnableRouteResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/EnableRoute", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_EnableRoute_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -249,7 +281,7 @@ func (c *headscaleServiceClient) EnableRoute(ctx context.Context, in *EnableRout func (c *headscaleServiceClient) DisableRoute(ctx context.Context, in *DisableRouteRequest, opts ...grpc.CallOption) (*DisableRouteResponse, error) { out := new(DisableRouteResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/DisableRoute", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_DisableRoute_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -258,7 +290,7 @@ func (c *headscaleServiceClient) DisableRoute(ctx context.Context, in *DisableRo func (c *headscaleServiceClient) GetNodeRoutes(ctx context.Context, in *GetNodeRoutesRequest, opts ...grpc.CallOption) (*GetNodeRoutesResponse, error) { out := new(GetNodeRoutesResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/GetNodeRoutes", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_GetNodeRoutes_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -267,7 +299,7 @@ func (c *headscaleServiceClient) GetNodeRoutes(ctx context.Context, in *GetNodeR func (c *headscaleServiceClient) DeleteRoute(ctx context.Context, in *DeleteRouteRequest, opts ...grpc.CallOption) (*DeleteRouteResponse, error) { out := new(DeleteRouteResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/DeleteRoute", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_DeleteRoute_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -276,7 +308,7 @@ func (c *headscaleServiceClient) DeleteRoute(ctx context.Context, in *DeleteRout func (c *headscaleServiceClient) CreateApiKey(ctx context.Context, in *CreateApiKeyRequest, opts ...grpc.CallOption) (*CreateApiKeyResponse, error) { out := new(CreateApiKeyResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/CreateApiKey", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_CreateApiKey_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -285,7 +317,7 @@ func (c *headscaleServiceClient) CreateApiKey(ctx context.Context, in *CreateApi func (c *headscaleServiceClient) ExpireApiKey(ctx context.Context, in *ExpireApiKeyRequest, opts ...grpc.CallOption) (*ExpireApiKeyResponse, error) { out := new(ExpireApiKeyResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ExpireApiKey", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_ExpireApiKey_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -294,7 +326,7 @@ func (c *headscaleServiceClient) ExpireApiKey(ctx context.Context, in *ExpireApi func (c *headscaleServiceClient) ListApiKeys(ctx context.Context, in *ListApiKeysRequest, opts ...grpc.CallOption) (*ListApiKeysResponse, error) { out := new(ListApiKeysResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ListApiKeys", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_ListApiKeys_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -303,7 +335,7 @@ func (c *headscaleServiceClient) ListApiKeys(ctx context.Context, in *ListApiKey func (c *headscaleServiceClient) DeleteApiKey(ctx context.Context, in *DeleteApiKeyRequest, opts ...grpc.CallOption) (*DeleteApiKeyResponse, error) { out := new(DeleteApiKeyResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/DeleteApiKey", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_DeleteApiKey_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -312,7 +344,7 @@ func (c *headscaleServiceClient) DeleteApiKey(ctx context.Context, in *DeleteApi func (c *headscaleServiceClient) GetPolicy(ctx context.Context, in *GetPolicyRequest, opts ...grpc.CallOption) (*GetPolicyResponse, error) { out := new(GetPolicyResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/GetPolicy", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_GetPolicy_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -321,7 +353,7 @@ func (c *headscaleServiceClient) GetPolicy(ctx context.Context, in *GetPolicyReq func (c *headscaleServiceClient) SetPolicy(ctx context.Context, in *SetPolicyRequest, opts ...grpc.CallOption) (*SetPolicyResponse, error) { out := new(SetPolicyResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/SetPolicy", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_SetPolicy_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -484,7 +516,7 @@ func _HeadscaleService_GetUser_Handler(srv interface{}, ctx context.Context, dec } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/GetUser", + FullMethod: HeadscaleService_GetUser_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).GetUser(ctx, req.(*GetUserRequest)) @@ -502,7 +534,7 @@ func _HeadscaleService_CreateUser_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/CreateUser", + FullMethod: HeadscaleService_CreateUser_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).CreateUser(ctx, req.(*CreateUserRequest)) @@ -520,7 +552,7 @@ func _HeadscaleService_RenameUser_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/RenameUser", + FullMethod: HeadscaleService_RenameUser_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).RenameUser(ctx, req.(*RenameUserRequest)) @@ -538,7 +570,7 @@ func _HeadscaleService_DeleteUser_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/DeleteUser", + FullMethod: HeadscaleService_DeleteUser_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).DeleteUser(ctx, req.(*DeleteUserRequest)) @@ -556,7 +588,7 @@ func _HeadscaleService_ListUsers_Handler(srv interface{}, ctx context.Context, d } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/ListUsers", + FullMethod: HeadscaleService_ListUsers_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).ListUsers(ctx, req.(*ListUsersRequest)) @@ -574,7 +606,7 @@ func _HeadscaleService_CreatePreAuthKey_Handler(srv interface{}, ctx context.Con } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/CreatePreAuthKey", + FullMethod: HeadscaleService_CreatePreAuthKey_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).CreatePreAuthKey(ctx, req.(*CreatePreAuthKeyRequest)) @@ -592,7 +624,7 @@ func _HeadscaleService_ExpirePreAuthKey_Handler(srv interface{}, ctx context.Con } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/ExpirePreAuthKey", + FullMethod: HeadscaleService_ExpirePreAuthKey_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).ExpirePreAuthKey(ctx, req.(*ExpirePreAuthKeyRequest)) @@ -610,7 +642,7 @@ func _HeadscaleService_ListPreAuthKeys_Handler(srv interface{}, ctx context.Cont } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/ListPreAuthKeys", + FullMethod: HeadscaleService_ListPreAuthKeys_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).ListPreAuthKeys(ctx, req.(*ListPreAuthKeysRequest)) @@ -628,7 +660,7 @@ func _HeadscaleService_DebugCreateNode_Handler(srv interface{}, ctx context.Cont } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/DebugCreateNode", + FullMethod: HeadscaleService_DebugCreateNode_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).DebugCreateNode(ctx, req.(*DebugCreateNodeRequest)) @@ -646,7 +678,7 @@ func _HeadscaleService_GetNode_Handler(srv interface{}, ctx context.Context, dec } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/GetNode", + FullMethod: HeadscaleService_GetNode_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).GetNode(ctx, req.(*GetNodeRequest)) @@ -664,7 +696,7 @@ func _HeadscaleService_SetTags_Handler(srv interface{}, ctx context.Context, dec } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/SetTags", + FullMethod: HeadscaleService_SetTags_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).SetTags(ctx, req.(*SetTagsRequest)) @@ -682,7 +714,7 @@ func _HeadscaleService_RegisterNode_Handler(srv interface{}, ctx context.Context } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/RegisterNode", + FullMethod: HeadscaleService_RegisterNode_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).RegisterNode(ctx, req.(*RegisterNodeRequest)) @@ -700,7 +732,7 @@ func _HeadscaleService_DeleteNode_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/DeleteNode", + FullMethod: HeadscaleService_DeleteNode_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).DeleteNode(ctx, req.(*DeleteNodeRequest)) @@ -718,7 +750,7 @@ func _HeadscaleService_ExpireNode_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/ExpireNode", + FullMethod: HeadscaleService_ExpireNode_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).ExpireNode(ctx, req.(*ExpireNodeRequest)) @@ -736,7 +768,7 @@ func _HeadscaleService_RenameNode_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/RenameNode", + FullMethod: HeadscaleService_RenameNode_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).RenameNode(ctx, req.(*RenameNodeRequest)) @@ -754,7 +786,7 @@ func _HeadscaleService_ListNodes_Handler(srv interface{}, ctx context.Context, d } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/ListNodes", + FullMethod: HeadscaleService_ListNodes_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).ListNodes(ctx, req.(*ListNodesRequest)) @@ -772,7 +804,7 @@ func _HeadscaleService_MoveNode_Handler(srv interface{}, ctx context.Context, de } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/MoveNode", + FullMethod: HeadscaleService_MoveNode_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).MoveNode(ctx, req.(*MoveNodeRequest)) @@ -790,7 +822,7 @@ func _HeadscaleService_BackfillNodeIPs_Handler(srv interface{}, ctx context.Cont } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/BackfillNodeIPs", + FullMethod: HeadscaleService_BackfillNodeIPs_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).BackfillNodeIPs(ctx, req.(*BackfillNodeIPsRequest)) @@ -808,7 +840,7 @@ func _HeadscaleService_GetRoutes_Handler(srv interface{}, ctx context.Context, d } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/GetRoutes", + FullMethod: HeadscaleService_GetRoutes_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).GetRoutes(ctx, req.(*GetRoutesRequest)) @@ -826,7 +858,7 @@ func _HeadscaleService_EnableRoute_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/EnableRoute", + FullMethod: HeadscaleService_EnableRoute_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).EnableRoute(ctx, req.(*EnableRouteRequest)) @@ -844,7 +876,7 @@ func _HeadscaleService_DisableRoute_Handler(srv interface{}, ctx context.Context } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/DisableRoute", + FullMethod: HeadscaleService_DisableRoute_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).DisableRoute(ctx, req.(*DisableRouteRequest)) @@ -862,7 +894,7 @@ func _HeadscaleService_GetNodeRoutes_Handler(srv interface{}, ctx context.Contex } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/GetNodeRoutes", + FullMethod: HeadscaleService_GetNodeRoutes_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).GetNodeRoutes(ctx, req.(*GetNodeRoutesRequest)) @@ -880,7 +912,7 @@ func _HeadscaleService_DeleteRoute_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/DeleteRoute", + FullMethod: HeadscaleService_DeleteRoute_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).DeleteRoute(ctx, req.(*DeleteRouteRequest)) @@ -898,7 +930,7 @@ func _HeadscaleService_CreateApiKey_Handler(srv interface{}, ctx context.Context } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/CreateApiKey", + FullMethod: HeadscaleService_CreateApiKey_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).CreateApiKey(ctx, req.(*CreateApiKeyRequest)) @@ -916,7 +948,7 @@ func _HeadscaleService_ExpireApiKey_Handler(srv interface{}, ctx context.Context } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/ExpireApiKey", + FullMethod: HeadscaleService_ExpireApiKey_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).ExpireApiKey(ctx, req.(*ExpireApiKeyRequest)) @@ -934,7 +966,7 @@ func _HeadscaleService_ListApiKeys_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/ListApiKeys", + FullMethod: HeadscaleService_ListApiKeys_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).ListApiKeys(ctx, req.(*ListApiKeysRequest)) @@ -952,7 +984,7 @@ func _HeadscaleService_DeleteApiKey_Handler(srv interface{}, ctx context.Context } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/DeleteApiKey", + FullMethod: HeadscaleService_DeleteApiKey_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).DeleteApiKey(ctx, req.(*DeleteApiKeyRequest)) @@ -970,7 +1002,7 @@ func _HeadscaleService_GetPolicy_Handler(srv interface{}, ctx context.Context, d } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/GetPolicy", + FullMethod: HeadscaleService_GetPolicy_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).GetPolicy(ctx, req.(*GetPolicyRequest)) @@ -988,7 +1020,7 @@ func _HeadscaleService_SetPolicy_Handler(srv interface{}, ctx context.Context, d } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/SetPolicy", + FullMethod: HeadscaleService_SetPolicy_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).SetPolicy(ctx, req.(*SetPolicyRequest)) diff --git a/gen/go/headscale/v1/node.pb.go b/gen/go/headscale/v1/node.pb.go index b961ca73..61ed4064 100644 --- a/gen/go/headscale/v1/node.pb.go +++ b/gen/go/headscale/v1/node.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 +// protoc-gen-go v1.34.2 // protoc (unknown) // source: headscale/v1/node.proto @@ -1389,7 +1389,7 @@ func file_headscale_v1_node_proto_rawDescGZIP() []byte { var file_headscale_v1_node_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_headscale_v1_node_proto_msgTypes = make([]protoimpl.MessageInfo, 21) -var file_headscale_v1_node_proto_goTypes = []interface{}{ +var file_headscale_v1_node_proto_goTypes = []any{ (RegisterMethod)(0), // 0: headscale.v1.RegisterMethod (*Node)(nil), // 1: headscale.v1.Node (*RegisterNodeRequest)(nil), // 2: headscale.v1.RegisterNodeRequest @@ -1446,7 +1446,7 @@ func file_headscale_v1_node_proto_init() { file_headscale_v1_preauthkey_proto_init() file_headscale_v1_user_proto_init() if !protoimpl.UnsafeEnabled { - file_headscale_v1_node_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[0].Exporter = func(v any, i int) any { switch v := v.(*Node); i { case 0: return &v.state @@ -1458,7 +1458,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[1].Exporter = func(v any, i int) any { switch v := v.(*RegisterNodeRequest); i { case 0: return &v.state @@ -1470,7 +1470,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[2].Exporter = func(v any, i int) any { switch v := v.(*RegisterNodeResponse); i { case 0: return &v.state @@ -1482,7 +1482,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[3].Exporter = func(v any, i int) any { switch v := v.(*GetNodeRequest); i { case 0: return &v.state @@ -1494,7 +1494,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[4].Exporter = func(v any, i int) any { switch v := v.(*GetNodeResponse); i { case 0: return &v.state @@ -1506,7 +1506,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[5].Exporter = func(v any, i int) any { switch v := v.(*SetTagsRequest); i { case 0: return &v.state @@ -1518,7 +1518,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[6].Exporter = func(v any, i int) any { switch v := v.(*SetTagsResponse); i { case 0: return &v.state @@ -1530,7 +1530,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[7].Exporter = func(v any, i int) any { switch v := v.(*DeleteNodeRequest); i { case 0: return &v.state @@ -1542,7 +1542,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[8].Exporter = func(v any, i int) any { switch v := v.(*DeleteNodeResponse); i { case 0: return &v.state @@ -1554,7 +1554,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[9].Exporter = func(v any, i int) any { switch v := v.(*ExpireNodeRequest); i { case 0: return &v.state @@ -1566,7 +1566,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[10].Exporter = func(v any, i int) any { switch v := v.(*ExpireNodeResponse); i { case 0: return &v.state @@ -1578,7 +1578,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[11].Exporter = func(v any, i int) any { switch v := v.(*RenameNodeRequest); i { case 0: return &v.state @@ -1590,7 +1590,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[12].Exporter = func(v any, i int) any { switch v := v.(*RenameNodeResponse); i { case 0: return &v.state @@ -1602,7 +1602,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[13].Exporter = func(v any, i int) any { switch v := v.(*ListNodesRequest); i { case 0: return &v.state @@ -1614,7 +1614,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[14].Exporter = func(v any, i int) any { switch v := v.(*ListNodesResponse); i { case 0: return &v.state @@ -1626,7 +1626,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[15].Exporter = func(v any, i int) any { switch v := v.(*MoveNodeRequest); i { case 0: return &v.state @@ -1638,7 +1638,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[16].Exporter = func(v any, i int) any { switch v := v.(*MoveNodeResponse); i { case 0: return &v.state @@ -1650,7 +1650,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[17].Exporter = func(v any, i int) any { switch v := v.(*DebugCreateNodeRequest); i { case 0: return &v.state @@ -1662,7 +1662,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[18].Exporter = func(v any, i int) any { switch v := v.(*DebugCreateNodeResponse); i { case 0: return &v.state @@ -1674,7 +1674,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[19].Exporter = func(v any, i int) any { switch v := v.(*BackfillNodeIPsRequest); i { case 0: return &v.state @@ -1686,7 +1686,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[20].Exporter = func(v any, i int) any { switch v := v.(*BackfillNodeIPsResponse); i { case 0: return &v.state diff --git a/gen/go/headscale/v1/policy.pb.go b/gen/go/headscale/v1/policy.pb.go index 31ecffdf..62a079be 100644 --- a/gen/go/headscale/v1/policy.pb.go +++ b/gen/go/headscale/v1/policy.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 +// protoc-gen-go v1.34.2 // protoc (unknown) // source: headscale/v1/policy.proto @@ -259,7 +259,7 @@ func file_headscale_v1_policy_proto_rawDescGZIP() []byte { } var file_headscale_v1_policy_proto_msgTypes = make([]protoimpl.MessageInfo, 4) -var file_headscale_v1_policy_proto_goTypes = []interface{}{ +var file_headscale_v1_policy_proto_goTypes = []any{ (*SetPolicyRequest)(nil), // 0: headscale.v1.SetPolicyRequest (*SetPolicyResponse)(nil), // 1: headscale.v1.SetPolicyResponse (*GetPolicyRequest)(nil), // 2: headscale.v1.GetPolicyRequest @@ -282,7 +282,7 @@ func file_headscale_v1_policy_proto_init() { return } if !protoimpl.UnsafeEnabled { - file_headscale_v1_policy_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_policy_proto_msgTypes[0].Exporter = func(v any, i int) any { switch v := v.(*SetPolicyRequest); i { case 0: return &v.state @@ -294,7 +294,7 @@ func file_headscale_v1_policy_proto_init() { return nil } } - file_headscale_v1_policy_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_policy_proto_msgTypes[1].Exporter = func(v any, i int) any { switch v := v.(*SetPolicyResponse); i { case 0: return &v.state @@ -306,7 +306,7 @@ func file_headscale_v1_policy_proto_init() { return nil } } - file_headscale_v1_policy_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_policy_proto_msgTypes[2].Exporter = func(v any, i int) any { switch v := v.(*GetPolicyRequest); i { case 0: return &v.state @@ -318,7 +318,7 @@ func file_headscale_v1_policy_proto_init() { return nil } } - file_headscale_v1_policy_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_policy_proto_msgTypes[3].Exporter = func(v any, i int) any { switch v := v.(*GetPolicyResponse); i { case 0: return &v.state diff --git a/gen/go/headscale/v1/preauthkey.pb.go b/gen/go/headscale/v1/preauthkey.pb.go index 35a0dfe0..ede617f2 100644 --- a/gen/go/headscale/v1/preauthkey.pb.go +++ b/gen/go/headscale/v1/preauthkey.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 +// protoc-gen-go v1.34.2 // protoc (unknown) // source: headscale/v1/preauthkey.proto @@ -522,7 +522,7 @@ func file_headscale_v1_preauthkey_proto_rawDescGZIP() []byte { } var file_headscale_v1_preauthkey_proto_msgTypes = make([]protoimpl.MessageInfo, 7) -var file_headscale_v1_preauthkey_proto_goTypes = []interface{}{ +var file_headscale_v1_preauthkey_proto_goTypes = []any{ (*PreAuthKey)(nil), // 0: headscale.v1.PreAuthKey (*CreatePreAuthKeyRequest)(nil), // 1: headscale.v1.CreatePreAuthKeyRequest (*CreatePreAuthKeyResponse)(nil), // 2: headscale.v1.CreatePreAuthKeyResponse @@ -551,7 +551,7 @@ func file_headscale_v1_preauthkey_proto_init() { return } if !protoimpl.UnsafeEnabled { - file_headscale_v1_preauthkey_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_preauthkey_proto_msgTypes[0].Exporter = func(v any, i int) any { switch v := v.(*PreAuthKey); i { case 0: return &v.state @@ -563,7 +563,7 @@ func file_headscale_v1_preauthkey_proto_init() { return nil } } - file_headscale_v1_preauthkey_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_preauthkey_proto_msgTypes[1].Exporter = func(v any, i int) any { switch v := v.(*CreatePreAuthKeyRequest); i { case 0: return &v.state @@ -575,7 +575,7 @@ func file_headscale_v1_preauthkey_proto_init() { return nil } } - file_headscale_v1_preauthkey_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_preauthkey_proto_msgTypes[2].Exporter = func(v any, i int) any { switch v := v.(*CreatePreAuthKeyResponse); i { case 0: return &v.state @@ -587,7 +587,7 @@ func file_headscale_v1_preauthkey_proto_init() { return nil } } - file_headscale_v1_preauthkey_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_preauthkey_proto_msgTypes[3].Exporter = func(v any, i int) any { switch v := v.(*ExpirePreAuthKeyRequest); i { case 0: return &v.state @@ -599,7 +599,7 @@ func file_headscale_v1_preauthkey_proto_init() { return nil } } - file_headscale_v1_preauthkey_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_preauthkey_proto_msgTypes[4].Exporter = func(v any, i int) any { switch v := v.(*ExpirePreAuthKeyResponse); i { case 0: return &v.state @@ -611,7 +611,7 @@ func file_headscale_v1_preauthkey_proto_init() { return nil } } - file_headscale_v1_preauthkey_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_preauthkey_proto_msgTypes[5].Exporter = func(v any, i int) any { switch v := v.(*ListPreAuthKeysRequest); i { case 0: return &v.state @@ -623,7 +623,7 @@ func file_headscale_v1_preauthkey_proto_init() { return nil } } - file_headscale_v1_preauthkey_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_preauthkey_proto_msgTypes[6].Exporter = func(v any, i int) any { switch v := v.(*ListPreAuthKeysResponse); i { case 0: return &v.state diff --git a/gen/go/headscale/v1/routes.pb.go b/gen/go/headscale/v1/routes.pb.go index d2273047..76806db8 100644 --- a/gen/go/headscale/v1/routes.pb.go +++ b/gen/go/headscale/v1/routes.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 +// protoc-gen-go v1.34.2 // protoc (unknown) // source: headscale/v1/routes.proto @@ -643,7 +643,7 @@ func file_headscale_v1_routes_proto_rawDescGZIP() []byte { } var file_headscale_v1_routes_proto_msgTypes = make([]protoimpl.MessageInfo, 11) -var file_headscale_v1_routes_proto_goTypes = []interface{}{ +var file_headscale_v1_routes_proto_goTypes = []any{ (*Route)(nil), // 0: headscale.v1.Route (*GetRoutesRequest)(nil), // 1: headscale.v1.GetRoutesRequest (*GetRoutesResponse)(nil), // 2: headscale.v1.GetRoutesResponse @@ -679,7 +679,7 @@ func file_headscale_v1_routes_proto_init() { } file_headscale_v1_node_proto_init() if !protoimpl.UnsafeEnabled { - file_headscale_v1_routes_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_routes_proto_msgTypes[0].Exporter = func(v any, i int) any { switch v := v.(*Route); i { case 0: return &v.state @@ -691,7 +691,7 @@ func file_headscale_v1_routes_proto_init() { return nil } } - file_headscale_v1_routes_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_routes_proto_msgTypes[1].Exporter = func(v any, i int) any { switch v := v.(*GetRoutesRequest); i { case 0: return &v.state @@ -703,7 +703,7 @@ func file_headscale_v1_routes_proto_init() { return nil } } - file_headscale_v1_routes_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_routes_proto_msgTypes[2].Exporter = func(v any, i int) any { switch v := v.(*GetRoutesResponse); i { case 0: return &v.state @@ -715,7 +715,7 @@ func file_headscale_v1_routes_proto_init() { return nil } } - file_headscale_v1_routes_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_routes_proto_msgTypes[3].Exporter = func(v any, i int) any { switch v := v.(*EnableRouteRequest); i { case 0: return &v.state @@ -727,7 +727,7 @@ func file_headscale_v1_routes_proto_init() { return nil } } - file_headscale_v1_routes_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_routes_proto_msgTypes[4].Exporter = func(v any, i int) any { switch v := v.(*EnableRouteResponse); i { case 0: return &v.state @@ -739,7 +739,7 @@ func file_headscale_v1_routes_proto_init() { return nil } } - file_headscale_v1_routes_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_routes_proto_msgTypes[5].Exporter = func(v any, i int) any { switch v := v.(*DisableRouteRequest); i { case 0: return &v.state @@ -751,7 +751,7 @@ func file_headscale_v1_routes_proto_init() { return nil } } - file_headscale_v1_routes_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_routes_proto_msgTypes[6].Exporter = func(v any, i int) any { switch v := v.(*DisableRouteResponse); i { case 0: return &v.state @@ -763,7 +763,7 @@ func file_headscale_v1_routes_proto_init() { return nil } } - file_headscale_v1_routes_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_routes_proto_msgTypes[7].Exporter = func(v any, i int) any { switch v := v.(*GetNodeRoutesRequest); i { case 0: return &v.state @@ -775,7 +775,7 @@ func file_headscale_v1_routes_proto_init() { return nil } } - file_headscale_v1_routes_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_routes_proto_msgTypes[8].Exporter = func(v any, i int) any { switch v := v.(*GetNodeRoutesResponse); i { case 0: return &v.state @@ -787,7 +787,7 @@ func file_headscale_v1_routes_proto_init() { return nil } } - file_headscale_v1_routes_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_routes_proto_msgTypes[9].Exporter = func(v any, i int) any { switch v := v.(*DeleteRouteRequest); i { case 0: return &v.state @@ -799,7 +799,7 @@ func file_headscale_v1_routes_proto_init() { return nil } } - file_headscale_v1_routes_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_routes_proto_msgTypes[10].Exporter = func(v any, i int) any { switch v := v.(*DeleteRouteResponse); i { case 0: return &v.state diff --git a/gen/go/headscale/v1/user.pb.go b/gen/go/headscale/v1/user.pb.go index 17cb4b54..ff1a5689 100644 --- a/gen/go/headscale/v1/user.pb.go +++ b/gen/go/headscale/v1/user.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 +// protoc-gen-go v1.34.2 // protoc (unknown) // source: headscale/v1/user.proto @@ -607,7 +607,7 @@ func file_headscale_v1_user_proto_rawDescGZIP() []byte { } var file_headscale_v1_user_proto_msgTypes = make([]protoimpl.MessageInfo, 11) -var file_headscale_v1_user_proto_goTypes = []interface{}{ +var file_headscale_v1_user_proto_goTypes = []any{ (*User)(nil), // 0: headscale.v1.User (*GetUserRequest)(nil), // 1: headscale.v1.GetUserRequest (*GetUserResponse)(nil), // 2: headscale.v1.GetUserResponse @@ -640,7 +640,7 @@ func file_headscale_v1_user_proto_init() { return } if !protoimpl.UnsafeEnabled { - file_headscale_v1_user_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_user_proto_msgTypes[0].Exporter = func(v any, i int) any { switch v := v.(*User); i { case 0: return &v.state @@ -652,7 +652,7 @@ func file_headscale_v1_user_proto_init() { return nil } } - file_headscale_v1_user_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_user_proto_msgTypes[1].Exporter = func(v any, i int) any { switch v := v.(*GetUserRequest); i { case 0: return &v.state @@ -664,7 +664,7 @@ func file_headscale_v1_user_proto_init() { return nil } } - file_headscale_v1_user_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_user_proto_msgTypes[2].Exporter = func(v any, i int) any { switch v := v.(*GetUserResponse); i { case 0: return &v.state @@ -676,7 +676,7 @@ func file_headscale_v1_user_proto_init() { return nil } } - file_headscale_v1_user_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_user_proto_msgTypes[3].Exporter = func(v any, i int) any { switch v := v.(*CreateUserRequest); i { case 0: return &v.state @@ -688,7 +688,7 @@ func file_headscale_v1_user_proto_init() { return nil } } - file_headscale_v1_user_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_user_proto_msgTypes[4].Exporter = func(v any, i int) any { switch v := v.(*CreateUserResponse); i { case 0: return &v.state @@ -700,7 +700,7 @@ func file_headscale_v1_user_proto_init() { return nil } } - file_headscale_v1_user_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_user_proto_msgTypes[5].Exporter = func(v any, i int) any { switch v := v.(*RenameUserRequest); i { case 0: return &v.state @@ -712,7 +712,7 @@ func file_headscale_v1_user_proto_init() { return nil } } - file_headscale_v1_user_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_user_proto_msgTypes[6].Exporter = func(v any, i int) any { switch v := v.(*RenameUserResponse); i { case 0: return &v.state @@ -724,7 +724,7 @@ func file_headscale_v1_user_proto_init() { return nil } } - file_headscale_v1_user_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_user_proto_msgTypes[7].Exporter = func(v any, i int) any { switch v := v.(*DeleteUserRequest); i { case 0: return &v.state @@ -736,7 +736,7 @@ func file_headscale_v1_user_proto_init() { return nil } } - file_headscale_v1_user_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_user_proto_msgTypes[8].Exporter = func(v any, i int) any { switch v := v.(*DeleteUserResponse); i { case 0: return &v.state @@ -748,7 +748,7 @@ func file_headscale_v1_user_proto_init() { return nil } } - file_headscale_v1_user_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_user_proto_msgTypes[9].Exporter = func(v any, i int) any { switch v := v.(*ListUsersRequest); i { case 0: return &v.state @@ -760,7 +760,7 @@ func file_headscale_v1_user_proto_init() { return nil } } - file_headscale_v1_user_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_user_proto_msgTypes[10].Exporter = func(v any, i int) any { switch v := v.(*ListUsersResponse); i { case 0: return &v.state diff --git a/gen/openapiv2/headscale/v1/apikey.swagger.json b/gen/openapiv2/headscale/v1/apikey.swagger.json index 0d4ebbe9..8c8596a9 100644 --- a/gen/openapiv2/headscale/v1/apikey.swagger.json +++ b/gen/openapiv2/headscale/v1/apikey.swagger.json @@ -34,6 +34,7 @@ "details": { "type": "array", "items": { + "type": "object", "$ref": "#/definitions/protobufAny" } } diff --git a/gen/openapiv2/headscale/v1/device.swagger.json b/gen/openapiv2/headscale/v1/device.swagger.json index 5360527a..99d20deb 100644 --- a/gen/openapiv2/headscale/v1/device.swagger.json +++ b/gen/openapiv2/headscale/v1/device.swagger.json @@ -34,6 +34,7 @@ "details": { "type": "array", "items": { + "type": "object", "$ref": "#/definitions/protobufAny" } } diff --git a/gen/openapiv2/headscale/v1/headscale.swagger.json b/gen/openapiv2/headscale/v1/headscale.swagger.json index 9c1cf0e9..9530ea4d 100644 --- a/gen/openapiv2/headscale/v1/headscale.swagger.json +++ b/gen/openapiv2/headscale/v1/headscale.swagger.json @@ -449,15 +449,7 @@ "in": "body", "required": true, "schema": { - "type": "object", - "properties": { - "tags": { - "type": "array", - "items": { - "type": "string" - } - } - } + "$ref": "#/definitions/HeadscaleServiceSetTagsBody" } } ], @@ -914,6 +906,17 @@ } }, "definitions": { + "HeadscaleServiceSetTagsBody": { + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "protobufAny": { "type": "object", "properties": { @@ -936,6 +939,7 @@ "details": { "type": "array", "items": { + "type": "object", "$ref": "#/definitions/protobufAny" } } @@ -1134,6 +1138,7 @@ "routes": { "type": "array", "items": { + "type": "object", "$ref": "#/definitions/v1Route" } } @@ -1157,6 +1162,7 @@ "routes": { "type": "array", "items": { + "type": "object", "$ref": "#/definitions/v1Route" } } @@ -1176,6 +1182,7 @@ "apiKeys": { "type": "array", "items": { + "type": "object", "$ref": "#/definitions/v1ApiKey" } } @@ -1187,6 +1194,7 @@ "nodes": { "type": "array", "items": { + "type": "object", "$ref": "#/definitions/v1Node" } } @@ -1198,6 +1206,7 @@ "preAuthKeys": { "type": "array", "items": { + "type": "object", "$ref": "#/definitions/v1PreAuthKey" } } @@ -1209,6 +1218,7 @@ "users": { "type": "array", "items": { + "type": "object", "$ref": "#/definitions/v1User" } } diff --git a/gen/openapiv2/headscale/v1/node.swagger.json b/gen/openapiv2/headscale/v1/node.swagger.json index 8271250e..16321347 100644 --- a/gen/openapiv2/headscale/v1/node.swagger.json +++ b/gen/openapiv2/headscale/v1/node.swagger.json @@ -34,6 +34,7 @@ "details": { "type": "array", "items": { + "type": "object", "$ref": "#/definitions/protobufAny" } } diff --git a/gen/openapiv2/headscale/v1/policy.swagger.json b/gen/openapiv2/headscale/v1/policy.swagger.json index 63afc575..63057ed0 100644 --- a/gen/openapiv2/headscale/v1/policy.swagger.json +++ b/gen/openapiv2/headscale/v1/policy.swagger.json @@ -34,6 +34,7 @@ "details": { "type": "array", "items": { + "type": "object", "$ref": "#/definitions/protobufAny" } } diff --git a/gen/openapiv2/headscale/v1/preauthkey.swagger.json b/gen/openapiv2/headscale/v1/preauthkey.swagger.json index ef16319c..17a2be1a 100644 --- a/gen/openapiv2/headscale/v1/preauthkey.swagger.json +++ b/gen/openapiv2/headscale/v1/preauthkey.swagger.json @@ -34,6 +34,7 @@ "details": { "type": "array", "items": { + "type": "object", "$ref": "#/definitions/protobufAny" } } diff --git a/gen/openapiv2/headscale/v1/routes.swagger.json b/gen/openapiv2/headscale/v1/routes.swagger.json index 34eda676..11087f2a 100644 --- a/gen/openapiv2/headscale/v1/routes.swagger.json +++ b/gen/openapiv2/headscale/v1/routes.swagger.json @@ -34,6 +34,7 @@ "details": { "type": "array", "items": { + "type": "object", "$ref": "#/definitions/protobufAny" } } diff --git a/gen/openapiv2/headscale/v1/user.swagger.json b/gen/openapiv2/headscale/v1/user.swagger.json index 1355a9cc..008ca3e8 100644 --- a/gen/openapiv2/headscale/v1/user.swagger.json +++ b/gen/openapiv2/headscale/v1/user.swagger.json @@ -34,6 +34,7 @@ "details": { "type": "array", "items": { + "type": "object", "$ref": "#/definitions/protobufAny" } } diff --git a/go.mod b/go.mod index a0797844..18089bbd 100644 --- a/go.mod +++ b/go.mod @@ -4,30 +4,30 @@ go 1.23.0 require ( github.com/AlecAivazis/survey/v2 v2.3.7 - github.com/coreos/go-oidc/v3 v3.10.0 + github.com/coreos/go-oidc/v3 v3.11.0 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/deckarep/golang-set/v2 v2.6.0 github.com/glebarez/sqlite v1.11.0 github.com/go-gormigrate/gormigrate/v2 v2.1.2 - github.com/gofrs/uuid/v5 v5.2.0 + github.com/gofrs/uuid/v5 v5.3.0 github.com/google/go-cmp v0.6.0 github.com/gorilla/mux v1.8.1 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 - github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 github.com/jagottsicher/termcolor v1.0.2 - github.com/klauspost/compress v1.17.8 + github.com/klauspost/compress v1.17.9 github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 - github.com/ory/dockertest/v3 v3.10.0 + github.com/ory/dockertest/v3 v3.11.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.19.1 - github.com/prometheus/common v0.48.0 + github.com/prometheus/client_golang v1.20.2 + github.com/prometheus/common v0.58.0 github.com/pterm/pterm v0.12.79 - github.com/puzpuzpuz/xsync/v3 v3.1.0 - github.com/rs/zerolog v1.32.0 - github.com/samber/lo v1.39.0 - github.com/sasha-s/go-deadlock v0.3.1 + github.com/puzpuzpuz/xsync/v3 v3.4.0 + github.com/rs/zerolog v1.33.0 + github.com/samber/lo v1.47.0 + github.com/sasha-s/go-deadlock v0.3.5 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.20.0-alpha.6 github.com/stretchr/testify v1.9.0 @@ -35,18 +35,18 @@ require ( github.com/tailscale/tailsql v0.0.0-20240418235827-820559f382c1 github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e go4.org/netipx v0.0.0-20231129151722-fdeea329fbba - golang.org/x/crypto v0.25.0 - golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 - golang.org/x/net v0.27.0 - golang.org/x/oauth2 v0.20.0 - golang.org/x/sync v0.7.0 - google.golang.org/genproto/googleapis/api v0.0.0-20240515191416-fc5f0ca64291 - google.golang.org/grpc v1.64.0 - google.golang.org/protobuf v1.34.1 + golang.org/x/crypto v0.26.0 + golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 + golang.org/x/net v0.28.0 + golang.org/x/oauth2 v0.22.0 + golang.org/x/sync v0.8.0 + google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 + google.golang.org/grpc v1.66.0 + google.golang.org/protobuf v1.34.2 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c gopkg.in/yaml.v3 v3.0.1 - gorm.io/driver/postgres v1.5.7 - gorm.io/gorm v1.25.10 + gorm.io/driver/postgres v1.5.9 + gorm.io/gorm v1.25.11 tailscale.com v1.72.1 ) @@ -54,7 +54,7 @@ require ( atomicgo.dev/cursor v0.2.0 // indirect atomicgo.dev/keyboard v0.2.9 // indirect atomicgo.dev/schedule v0.1.0 // indirect - dario.cat/mergo v1.0.0 // indirect + dario.cat/mergo v1.0.1 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect @@ -78,7 +78,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.13.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/coder/websocket v1.8.12 // indirect github.com/containerd/console v1.0.4 // indirect github.com/containerd/continuity v0.4.3 // indirect @@ -86,21 +86,21 @@ require ( github.com/creachadair/mds v0.14.5 // indirect github.com/dblohm7/wingoes v0.0.0-20240123200102-b75a8a7d7eb0 // indirect github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e // indirect - github.com/docker/cli v26.1.3+incompatible // indirect - github.com/docker/docker v26.1.4+incompatible // indirect + github.com/docker/cli v27.2.0+incompatible // indirect + github.com/docker/docker v27.2.0+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/felixge/fgprof v0.9.4 // indirect + github.com/felixge/fgprof v0.9.5 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.6.0 // indirect github.com/gaissmai/bart v0.11.1 // indirect github.com/glebarez/go-sqlite v1.22.0 // indirect github.com/go-jose/go-jose/v3 v3.0.3 // indirect - github.com/go-jose/go-jose/v4 v4.0.1 // indirect + github.com/go-jose/go-jose/v4 v4.0.2 // indirect github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect - github.com/go-viper/mapstructure/v2 v2.0.0 // indirect + github.com/go-viper/mapstructure/v2 v2.1.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/v5 v5.2.1 // indirect @@ -110,20 +110,20 @@ require ( github.com/google/go-github v17.0.0+incompatible // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect - github.com/google/pprof v0.0.0-20240509144519-723abb6459b7 // indirect + github.com/google/pprof v0.0.0-20240829160300-da1f7e9f2b25 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gookit/color v1.5.4 // indirect github.com/gorilla/csrf v1.7.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect - github.com/hashicorp/go-version v1.6.0 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect github.com/hdevalence/ed25519consensus v0.2.0 // indirect github.com/illarion/gonotify v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/insomniacslk/dhcp v0.0.0-20240129002554-15c9b8791914 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect - github.com/jackc/pgx/v5 v5.5.5 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.6.0 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect @@ -134,11 +134,10 @@ require ( 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.8 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mdlayher/genetlink v1.3.2 // indirect github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/sdnotify v1.0.0 // indirect @@ -146,21 +145,21 @@ require ( github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/miekg/dns v1.1.58 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/term v0.5.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect - github.com/opencontainers/runc v1.1.12 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect - github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect + github.com/opencontainers/runc v1.1.14 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus-community/pro-bing v0.4.0 // indirect - github.com/prometheus/client_model v0.5.0 // indirect - github.com/prometheus/procfs v0.12.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect @@ -169,7 +168,7 @@ require ( 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/cast v1.7.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect @@ -193,19 +192,19 @@ require ( github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.uber.org/multierr v1.11.0 // indirect go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect - golang.org/x/mod v0.19.0 // indirect - golang.org/x/sys v0.22.0 // indirect - golang.org/x/term v0.22.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/mod v0.20.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/term v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.23.0 // indirect + golang.org/x/tools v0.24.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wireguard/windows v0.5.3 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 // indirect - modernc.org/libc v1.50.6 // indirect + modernc.org/libc v1.60.1 // indirect modernc.org/mathutil v1.6.0 // indirect modernc.org/memory v1.8.0 // indirect - modernc.org/sqlite v1.29.9 // indirect + modernc.org/sqlite v1.32.0 // indirect ) diff --git a/go.sum b/go.sum index fb5b93c0..2213f423 100644 --- a/go.sum +++ b/go.sum @@ -7,8 +7,8 @@ atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtE 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= -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc= @@ -88,8 +88,8 @@ github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6 github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= @@ -112,8 +112,8 @@ github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7b github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0= github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= -github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU= -github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac= +github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI= +github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creachadair/mds v0.14.5 h1:2amuO4yCbQkaAyDoLO5iCbwbTRQZz4EpRhOejQbf4+8= @@ -134,10 +134,10 @@ github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yez github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= -github.com/docker/cli v26.1.3+incompatible h1:bUpXT/N0kDE3VUHI2r5VMsYQgi38kYuoC0oL9yt3lqc= -github.com/docker/cli v26.1.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v26.1.4+incompatible h1:vuTpXDuoga+Z38m1OZHzl7NKisKWaWlhjQk7IDPSLsU= -github.com/docker/docker v26.1.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/cli v27.2.0+incompatible h1:yHD1QEB1/0vr5eBNpu8tncu8gWxg8EydFPOSKHzXSMM= +github.com/docker/cli v27.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4= +github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -151,8 +151,8 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= -github.com/felixge/fgprof v0.9.4 h1:ocDNwMFlnA0NU0zSB3I52xkO4sFXk80VK9lXjLClu88= -github.com/felixge/fgprof v0.9.4/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= +github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY= +github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= @@ -171,27 +171,27 @@ github.com/go-gormigrate/gormigrate/v2 v2.1.2 h1:F/d1hpHbRAvKezziV2CC5KUE82cVe9z github.com/go-gormigrate/gormigrate/v2 v2.1.2/go.mod h1:9nHVX6z3FCMCQPA7PThGcA55t22yKQfK/Dnsf5i7hUo= github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= -github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U= -github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= +github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= +github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 h1:ymLjT4f35nQbASLnvxEde4XOBL+Sn7rFuV+FOJqkljg= github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0/go.mod h1:6daplAwHHGbUGib4990V3Il26O0OC4aRyvewaaAihaA= github.com/go-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-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-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc= -github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.1.0 h1:gHnMa2Y/pIxElCH2GlZZ1lZSsn6XMtufpGyP1XxdC/w= +github.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= github.com/godbus/dbus/v5 v5.0.4/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.2.0 h1:qw1GMx6/y8vhVsx626ImfKMuS5CvJmhIKKtuyvfajMM= -github.com/gofrs/uuid/v5 v5.2.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= +github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk= +github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= @@ -222,8 +222,8 @@ github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdF github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= -github.com/google/pprof v0.0.0-20240509144519-723abb6459b7 h1:velgFPYr1X9TDwLIfkV7fWqsFlf7TeP11M/7kPd/dVI= -github.com/google/pprof v0.0.0-20240509144519-723abb6459b7/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/pprof v0.0.0-20240829160300-da1f7e9f2b25 h1:sEDPKUw6iPjczdu33njxFjO6tYa9bfc0z/QyB/zSsBw= +github.com/google/pprof v0.0.0-20240829160300-da1f7e9f2b25/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -240,10 +240,10 @@ github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kX github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= -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/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= @@ -260,10 +260,10 @@ github.com/insomniacslk/dhcp v0.0.0-20240129002554-15c9b8791914 h1:kD8PseueGeYii github.com/insomniacslk/dhcp v0.0.0-20240129002554-15c9b8791914/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI= 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-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= -github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= -github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jagottsicher/termcolor v1.0.2 h1:fo0c51pQSuLBN1+yVX2ZE+hE+P7ULb/TY8eRowJnrsM= @@ -288,13 +288,13 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= -github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 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.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= -github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +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= @@ -308,9 +308,11 @@ 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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= -github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= -github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= @@ -323,8 +325,8 @@ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D 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.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= @@ -340,12 +342,12 @@ github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= 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/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= @@ -356,18 +358,18 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/opencontainers/runc v1.1.12 h1:BOIssBaW1La0/qbNZHXOOa71dZfZEQOzW7dqQf3phss= -github.com/opencontainers/runc v1.1.12/go.mod h1:S+lQwSfncpBha7XTy/5lBwWgm5+y5Ma/O44Ekby9FK8= +github.com/opencontainers/runc v1.1.14 h1:rgSuzbmgz5DUJjeSnw337TxDbRuqjs6iqQck/2weR6w= +github.com/opencontainers/runc v1.1.14/go.mod h1:E4C2z+7BxR7GHXp0hAY53mek+x49X1LjPNeMTfRGvOA= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= -github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4= -github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg= +github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA= +github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI= github.com/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.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= -github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ= -github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw= +github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/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= @@ -386,15 +388,15 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4= github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4= -github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= -github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg= +github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -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.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= -github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.58.0 h1:N+N8vY4/23r6iYfD3UQZUoJPnUYAo7v6LG5XZxjZTXo= +github.com/prometheus/common v0.58.0/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= @@ -404,8 +406,8 @@ github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5b github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= github.com/pterm/pterm v0.12.79 h1:lH3yrYMhdpeqX9y5Ep1u7DejyHy7NSQg9qrBjF9dFT4= github.com/pterm/pterm v0.12.79/go.mod h1:1v/gzOF1N0FsjbgTHZ1wVycRkKiatFvJSJC4IGaQAAo= -github.com/puzpuzpuz/xsync/v3 v3.1.0 h1:EewKT7/LNac5SLiEblJeUu8z5eERHrmRLnMQL2d7qX4= -github.com/puzpuzpuz/xsync/v3 v3.1.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4= +github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 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= @@ -415,17 +417,17 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= -github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= 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.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= -github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= -github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= -github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0= -github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM= +github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= +github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= +github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU= +github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= @@ -436,8 +438,8 @@ github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9yS 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/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -447,7 +449,6 @@ github.com/spf13/viper v1.20.0-alpha.6/go.mod h1:CGBZzv0c9fOUASm6rfus4wdeIjR/04N 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= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -457,7 +458,6 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= @@ -537,11 +537,11 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA= +golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a h1:8qmSSA8Gz/1kTrCe0nqR0R3Gb/NDhykzWw2q2mWZydM= golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= @@ -554,8 +554,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= -golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -568,11 +568,11 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= -golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= +golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -581,8 +581,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.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= @@ -614,8 +614,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.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-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -623,8 +623,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= -golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -632,8 +632,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -647,8 +647,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= -golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= 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= @@ -662,19 +662,19 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto/googleapis/api v0.0.0-20240515191416-fc5f0ca64291 h1:4HZJ3Xv1cmrJ+0aFo304Zn79ur1HMxptAE7aCPNLSqc= -google.golang.org/genproto/googleapis/api v0.0.0-20240515191416-fc5f0ca64291/go.mod h1:RGnPtTG7r4i8sPlNyDeikXF99hMM+hN6QMm4ooG9g2g= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 h1:AgADTJarZTBqgjiUzRgfaBchgYB3/WFTC80GPwsMcRI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= -google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= +google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 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= @@ -689,12 +689,12 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM= -gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA= -gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= -gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= -gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= -gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= +gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8= +gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= +gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg= +gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 h1:TU8z2Lh3Bbq77w0t1eG8yRlLcNHzZu3x6mhoH2Mk0c8= gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -703,18 +703,18 @@ honnef.co/go/tools v0.4.7 h1:9MDAWxMoSnB6QoSqiVr7P5mtkT9pOc1kSxchzPCnqJs= honnef.co/go/tools v0.4.7/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/cc/v4 v4.21.2 h1:dycHFB/jDc3IyacKipCNSDrjIC0Lm1hyoWOZTRR20Lk= -modernc.org/cc/v4 v4.21.2/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= -modernc.org/ccgo/v4 v4.17.7 h1:+MG+Np7uYtsuPvtoH3KtZ1+pqNiJAOqqqVIxggE1iIo= -modernc.org/ccgo/v4 v4.17.7/go.mod h1:x87xuLLXuJv3Nn5ULTUqJn/HsTMMMiT1Eavo6rz1NiY= +modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= +modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4= +modernc.org/ccgo/v4 v4.21.0/go.mod h1:h6kt6H/A2+ew/3MW/p6KEoQmrq/i3pr0J/SiwiaF/g0= modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= -modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= -modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= +modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M= +modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= -modernc.org/libc v1.50.6 h1:72NPEFMyKP01RJrKXS2eLXv35UklKqlJZ1b9P7gSo6I= -modernc.org/libc v1.50.6/go.mod h1:8lr2m1THY5Z3ikGyUc3JhLEQg1oaIBz/AQixw8/eksQ= +modernc.org/libc v1.60.1 h1:at373l8IFRTkJIkAU85BIuUoBM4T1b51ds0E1ovPG2s= +modernc.org/libc v1.60.1/go.mod h1:xJuobKuNxKH3RUatS7GjR+suWj+5c2K7bi4m/S5arOY= 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.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= @@ -723,8 +723,8 @@ modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= -modernc.org/sqlite v1.29.9 h1:9RhNMklxJs+1596GNuAX+O/6040bvOwacTxuFcRuQow= -modernc.org/sqlite v1.29.9/go.mod h1:ItX2a1OVGgNsFh6Dv60JQvGfJfTPHPVpV6DF59akYOA= +modernc.org/sqlite v1.32.0 h1:6BM4uGza7bWypsw4fdLRsLxut6bHe4c58VeqjRgST8s= +modernc.org/sqlite v1.32.0/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA= modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= diff --git a/proto/headscale/v1/headscale.proto b/proto/headscale/v1/headscale.proto index 183927ed..7324b65a 100644 --- a/proto/headscale/v1/headscale.proto +++ b/proto/headscale/v1/headscale.proto @@ -209,7 +209,6 @@ service HeadscaleService { } // --- Policy end --- - // Implement Tailscale API // rpc GetDevice(GetDeviceRequest) returns(GetDeviceResponse) { // option(google.api.http) = { diff --git a/proto/headscale/v1/policy.proto b/proto/headscale/v1/policy.proto index 3c929385..995f3af8 100644 --- a/proto/headscale/v1/policy.proto +++ b/proto/headscale/v1/policy.proto @@ -5,17 +5,17 @@ option go_package = "github.com/juanfont/headscale/gen/go/v1"; import "google/protobuf/timestamp.proto"; message SetPolicyRequest { - string policy = 1; + string policy = 1; } message SetPolicyResponse { - string policy = 1; - google.protobuf.Timestamp updated_at = 2; + string policy = 1; + google.protobuf.Timestamp updated_at = 2; } message GetPolicyRequest {} message GetPolicyResponse { - string policy = 1; - google.protobuf.Timestamp updated_at = 2; + string policy = 1; + google.protobuf.Timestamp updated_at = 2; } \ No newline at end of file From e43d6a0361c2f0567f2ae79852ca86db13ddc6bb Mon Sep 17 00:00:00 2001 From: nblock Date: Wed, 4 Sep 2024 14:38:38 +0200 Subject: [PATCH 127/145] Move flags after the command (#2100) The built-in help also shows flags to given after the command. Align documentation examples accordingly. --- docs/running-headscale-container.md | 4 ++-- docs/running-headscale-linux-manual.md | 4 ++-- docs/running-headscale-linux.md | 4 ++-- docs/running-headscale-openbsd.md | 4 ++-- docs/running-headscale-sealos.md | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/running-headscale-container.md b/docs/running-headscale-container.md index ef622f4e..087dae30 100644 --- a/docs/running-headscale-container.md +++ b/docs/running-headscale-container.md @@ -117,7 +117,7 @@ To register a machine when running `headscale` in a container, take the headscal ```shell docker exec headscale \ - headscale --user myfirstuser nodes register --key + headscale nodes register --user myfirstuser --key ``` ### Register machine using a pre authenticated key @@ -126,7 +126,7 @@ Generate a key using the command line: ```shell docker exec headscale \ - headscale --user myfirstuser preauthkeys create --reusable --expiration 24h + headscale preauthkeys create --user myfirstuser --reusable --expiration 24h ``` This will return a pre-authenticated key that can be used to connect a node to `headscale` during the `tailscale` command: diff --git a/docs/running-headscale-linux-manual.md b/docs/running-headscale-linux-manual.md index 25d47638..720390d8 100644 --- a/docs/running-headscale-linux-manual.md +++ b/docs/running-headscale-linux-manual.md @@ -92,7 +92,7 @@ tailscale up --login-server YOUR_HEADSCALE_URL Register the machine: ```shell -headscale --user myfirstuser nodes register --key +headscale nodes register --user myfirstuser --key ``` ### Register machine using a pre authenticated key @@ -100,7 +100,7 @@ headscale --user myfirstuser nodes register --key Generate a key using the command line: ```shell -headscale --user myfirstuser preauthkeys create --reusable --expiration 24h +headscale preauthkeys create --user myfirstuser --reusable --expiration 24h ``` This will return a pre-authenticated key that can be used to connect a node to `headscale` during the `tailscale` command: diff --git a/docs/running-headscale-linux.md b/docs/running-headscale-linux.md index f08789c4..4be2e693 100644 --- a/docs/running-headscale-linux.md +++ b/docs/running-headscale-linux.md @@ -78,7 +78,7 @@ tailscale up --login-server Register the machine: ```shell -headscale --user myfirstuser nodes register --key +headscale nodes register --user myfirstuser --key ``` ### Register machine using a pre authenticated key @@ -86,7 +86,7 @@ headscale --user myfirstuser nodes register --key Generate a key using the command line: ```shell -headscale --user myfirstuser preauthkeys create --reusable --expiration 24h +headscale preauthkeys create --user myfirstuser --reusable --expiration 24h ``` This will return a pre-authenticated key that is used to diff --git a/docs/running-headscale-openbsd.md b/docs/running-headscale-openbsd.md index f3e0548e..449034ba 100644 --- a/docs/running-headscale-openbsd.md +++ b/docs/running-headscale-openbsd.md @@ -129,7 +129,7 @@ tailscale up --login-server YOUR_HEADSCALE_URL Register the machine: ```shell -headscale --user myfirstuser nodes register --key +headscale nodes register --user myfirstuser --key ``` ### Register machine using a pre authenticated key @@ -137,7 +137,7 @@ headscale --user myfirstuser nodes register --key Generate a key using the command line: ```shell -headscale --user myfirstuser preauthkeys create --reusable --expiration 24h +headscale preauthkeys create --user myfirstuser --reusable --expiration 24h ``` This will return a pre-authenticated key that can be used to connect a node to `headscale` during the `tailscale` command: diff --git a/docs/running-headscale-sealos.md b/docs/running-headscale-sealos.md index 1e3fe3ac..52f5c7ec 100644 --- a/docs/running-headscale-sealos.md +++ b/docs/running-headscale-sealos.md @@ -41,7 +41,7 @@ tailscale up --login-server YOUR_HEADSCALE_URL To register a machine when running headscale in [Sealos](https://sealos.io), click on 'Terminal' button on the right side of the headscale application's detail page to access the Terminal of the headscale application, then take the headscale command: ```bash -headscale --user myfirstuser nodes register --key +headscale nodes register --user myfirstuser --key ``` ### Register machine using a pre authenticated key @@ -49,7 +49,7 @@ headscale --user myfirstuser nodes register --key click on 'Terminal' button on the right side of the headscale application's detail page to access the Terminal of the headscale application, then generate a key using the command line: ```bash -headscale --user myfirstuser preauthkeys create --reusable --expiration 24h +headscale preauthkeys create --user myfirstuser --reusable --expiration 24h ``` This will return a pre-authenticated key that can be used to connect a node to `headscale` during the `tailscale` command: From 35bfe7ced04079af7017c044140451c5f6622af5 Mon Sep 17 00:00:00 2001 From: nblock Date: Thu, 5 Sep 2024 12:08:50 +0200 Subject: [PATCH 128/145] Add support for service reload and sync service file (#2102) * Add support for service reload and sync service file * Copy the systemd.service file to the manual linux docs and adjust the path to the headscale binary to match with the previous documentation blocks. Unfortunately, there seems to be no easy way to include a file in mkdocs. * Remove a redundant "deprecation" block. The beginning of the documentation already states that. * Add `ExecReload` to the systemd.service file. Fixes: #2016 * Its called systemd * Fix link to systemd homepage --- docs/packaging/headscale.systemd.service | 1 + docs/running-headscale-linux-manual.md | 44 +++++------------------- docs/running-headscale-linux.md | 2 +- integration/dns_test.go | 2 +- 4 files changed, 11 insertions(+), 38 deletions(-) diff --git a/docs/packaging/headscale.systemd.service b/docs/packaging/headscale.systemd.service index 14e31618..37d5f5d3 100644 --- a/docs/packaging/headscale.systemd.service +++ b/docs/packaging/headscale.systemd.service @@ -9,6 +9,7 @@ Type=simple User=headscale Group=headscale ExecStart=/usr/bin/headscale serve +ExecReload=/usr/bin/kill -HUP $MAINPID Restart=always RestartSec=5 diff --git a/docs/running-headscale-linux-manual.md b/docs/running-headscale-linux-manual.md index 720390d8..3a0d91e0 100644 --- a/docs/running-headscale-linux-manual.md +++ b/docs/running-headscale-linux-manual.md @@ -8,7 +8,7 @@ ## Goal This documentation has the goal of showing a user how-to set up and run `headscale` on Linux. -In additional to the "get up and running section", there is an optional [SystemD section](#running-headscale-in-the-background-with-systemd) +In additional to the "get up and running section", there is an optional [systemd section](#running-headscale-in-the-background-with-systemd) describing how to make `headscale` run properly in a server environment. ## Configure and run `headscale` @@ -66,7 +66,7 @@ describing how to make `headscale` run properly in a server environment. To continue the tutorial, open a new terminal and let it run in the background. Alternatively use terminal emulators like [tmux](https://github.com/tmux/tmux) or [screen](https://www.gnu.org/software/screen/). - To run `headscale` in the background, please follow the steps in the [SystemD section](#running-headscale-in-the-background-with-systemd) before continuing. + To run `headscale` in the background, please follow the steps in the [systemd section](#running-headscale-in-the-background-with-systemd) before continuing. 1. Verify `headscale` is running: Verify `headscale` is available: @@ -109,42 +109,14 @@ This will return a pre-authenticated key that can be used to connect a node to ` tailscale up --login-server --authkey ``` -## Running `headscale` in the background with SystemD +## Running `headscale` in the background with systemd -:warning: **Deprecated**: This part is very outdated and you should use the [pre-packaged Headscale for this](./running-headscale-linux.md) - -This section demonstrates how to run `headscale` as a service in the background with [SystemD](https://www.freedesktop.org/wiki/Software/systemd/). +This section demonstrates how to run `headscale` as a service in the background with [systemd](https://systemd.io/). This should work on most modern Linux distributions. -1. Create a SystemD service configuration at `/etc/systemd/system/headscale.service` containing: - - ```systemd - [Unit] - Description=headscale controller - After=syslog.target - After=network.target - - [Service] - Type=simple - User=headscale - Group=headscale - ExecStart=/usr/local/bin/headscale serve - Restart=always - RestartSec=5 - - # Optional security enhancements - NoNewPrivileges=yes - PrivateTmp=yes - ProtectSystem=strict - ProtectHome=yes - WorkingDirectory=/var/lib/headscale - ReadWritePaths=/var/lib/headscale /var/run/headscale - AmbientCapabilities=CAP_NET_BIND_SERVICE - RuntimeDirectory=headscale - - [Install] - WantedBy=multi-user.target - ``` +1. Copy [headscale's systemd service file](./packaging/headscale.systemd.service) to + `/etc/systemd/system/headscale.service` and adjust it to suit your local setup. The following parameters likely need + to be modified: `ExecStart`, `WorkingDirectory`, `ReadWritePaths`. Note that when running as the headscale user ensure that, either you add your current user to the headscale group: @@ -164,7 +136,7 @@ This should work on most modern Linux distributions. unix_socket: /var/run/headscale/headscale.sock ``` -1. Reload SystemD to load the new configuration file: +1. Reload systemd to load the new configuration file: ```shell systemctl daemon-reload diff --git a/docs/running-headscale-linux.md b/docs/running-headscale-linux.md index 4be2e693..ffa510a6 100644 --- a/docs/running-headscale-linux.md +++ b/docs/running-headscale-linux.md @@ -8,7 +8,7 @@ Get Headscale up and running. -This includes running Headscale with SystemD. +This includes running Headscale with systemd. ## Migrating from manual install diff --git a/integration/dns_test.go b/integration/dns_test.go index 60f05199..f7973300 100644 --- a/integration/dns_test.go +++ b/integration/dns_test.go @@ -86,7 +86,7 @@ func TestResolveMagicDNS(t *testing.T) { // All the containers are based on Alpine, meaning Tailscale // will overwrite the resolv.conf file. // On other platform, Tailscale will integrate with a dns manager -// if available (like Systemd-Resolved). +// if available (like systemd-resolved). func TestValidateResolvConf(t *testing.T) { IntegrationSkip(t) From 6609f60938ad5410f4229db339c7c65394f36293 Mon Sep 17 00:00:00 2001 From: greizgh Date: Thu, 5 Sep 2024 13:37:05 +0200 Subject: [PATCH 129/145] actually lint file on CI (#2018) * replace deprecated golangci-lint output format CI was producing this kind of messages: > [config_reader] The output format `github-actions` is deprecated, please use `colored-line-number` * Actually lint files on CI --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8f38f9d7..94953fbc 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -31,7 +31,7 @@ jobs: - name: golangci-lint if: steps.changed-files.outputs.files == 'true' - run: nix develop --command -- golangci-lint run --new-from-rev=${{github.event.pull_request.base.sha}} --out-format=github-actions . + run: nix develop --command -- golangci-lint run --new-from-rev=${{github.event.pull_request.base.sha}} --out-format=colored-line-number prettier-lint: runs-on: ubuntu-latest From 1c34101e72683515bfb7ed44fc6999f3211cddee Mon Sep 17 00:00:00 2001 From: nblock Date: Thu, 5 Sep 2024 13:50:10 +0200 Subject: [PATCH 130/145] Remove outdated proposals (#2104) Fixes: #2101 --- docs/proposals/001-acls.md | 362 --------------------------- docs/proposals/002-better-routing.md | 48 ---- mkdocs.yml | 3 - 3 files changed, 413 deletions(-) delete mode 100644 docs/proposals/001-acls.md delete mode 100644 docs/proposals/002-better-routing.md diff --git a/docs/proposals/001-acls.md b/docs/proposals/001-acls.md deleted file mode 100644 index 74bcd13e..00000000 --- a/docs/proposals/001-acls.md +++ /dev/null @@ -1,362 +0,0 @@ -# ACLs - -A key component of tailscale is the notion of Tailnet. This notion is hidden -but the implications that it have on how to use tailscale are not. - -For tailscale an [tailnet](https://tailscale.com/kb/1136/tailnet/) is the -following: - -> For personal users, you are a tailnet of many devices and one person. Each -> device gets a private Tailscale IP address in the CGNAT range and every -> device can talk directly to every other device, wherever they are on the -> internet. -> -> For businesses and organizations, a tailnet is many devices and many users. -> It can be based on your Microsoft Active Directory, your Google Workspace, a -> GitHub organization, Okta tenancy, or other identity provider namespace. All -> of the devices and users in your tailnet can be seen by the tailnet -> administrators in the Tailscale admin console. There you can apply -> tailnet-wide configuration, such as ACLs that affect visibility of devices -> inside your tailnet, DNS settings, and more. - -## Current implementation and issues - -Currently in headscale, the namespaces are used both as tailnet and users. The -issue is that if we want to use the ACL's we can't use both at the same time. - -Tailnet's cannot communicate with each others. So we can't have an ACL that -authorize tailnet (namespace) A to talk to tailnet (namespace) B. - -We also can't write ACLs based on the users (namespaces in headscale) since all -devices belong to the same user. - -With the current implementation the only ACL that we can user is to associate -each headscale IP to a host manually then write the ACLs according to this -manual mapping. - -```json -{ - "hosts": { - "host1": "100.64.0.1", - "server": "100.64.0.2" - }, - "acls": [ - { "action": "accept", "users": ["host1"], "ports": ["host2:80,443"] } - ] -} -``` - -While this works, it requires a lot of manual editing on the configuration and -to keep track of all devices IP address. - -## Proposition for a next implementation - -In order to ease the use of ACL's we need to split the tailnet and users -notion. - -A solution could be to consider a headscale server (in it's entirety) as a -tailnet. - -For personal users the default behavior could either allow all communications -between all namespaces (like tailscale) or disallow all communications between -namespaces (current behavior). - -For businesses and organisations, viewing a headscale instance a single tailnet -would allow users (namespace) to talk to each other with the ACLs. As described -in tailscale's documentation [[1]], a server should be tagged and personal -devices should be tied to a user. Translated in headscale's terms each user can -have multiple devices and all those devices should be in the same namespace. -The servers should be tagged and used as such. - -This implementation would render useless the sharing feature that is currently -implemented since an ACL could do the same. Simplifying to only one user -interface to do one thing is easier and less confusing for the users. - -To better suit the ACLs in this proposition, it's advised to consider that each -namespaces belong to one person. This person can have multiple devices, they -will all be considered as the same user in the ACLs. OIDC feature wouldn't need -to map people to namespace, just create a namespace if the person isn't -registered yet. - -As a sidenote, users would like to write ACLs as YAML. We should offer users -the ability to rules in either format (HuJSON or YAML). - -[1]: https://tailscale.com/kb/1068/acl-tags/ - -## Example - -Let's build an example use case for a small business (It may be the place where -ACL's are the most useful). - -We have a small company with a boss, an admin, two developer and an intern. - -The boss should have access to all servers but not to the users hosts. Admin -should also have access to all hosts except that their permissions should be -limited to maintaining the hosts (for example purposes). The developers can do -anything they want on dev hosts, but only watch on productions hosts. Intern -can only interact with the development servers. - -Each user have at least a device connected to the network and we have some -servers. - -- database.prod -- database.dev -- app-server1.prod -- app-server1.dev -- billing.internal - -### Current headscale implementation - -Let's create some namespaces - -```bash -headscale namespaces create prod -headscale namespaces create dev -headscale namespaces create internal -headscale namespaces create users - -headscale nodes register -n users boss-computer -headscale nodes register -n users admin1-computer -headscale nodes register -n users dev1-computer -headscale nodes register -n users dev1-phone -headscale nodes register -n users dev2-computer -headscale nodes register -n users intern1-computer - -headscale nodes register -n prod database -headscale nodes register -n prod app-server1 - -headscale nodes register -n dev database -headscale nodes register -n dev app-server1 - -headscale nodes register -n internal billing - -headscale nodes list -ID | Name | Namespace | IP address -1 | boss-computer | users | 100.64.0.1 -2 | admin1-computer | users | 100.64.0.2 -3 | dev1-computer | users | 100.64.0.3 -4 | dev1-phone | users | 100.64.0.4 -5 | dev2-computer | users | 100.64.0.5 -6 | intern1-computer | users | 100.64.0.6 -7 | database | prod | 100.64.0.7 -8 | app-server1 | prod | 100.64.0.8 -9 | database | dev | 100.64.0.9 -10 | app-server1 | dev | 100.64.0.10 -11 | internal | internal | 100.64.0.11 -``` - -In order to only allow the communications related to our description above we -need to add the following ACLs - -```json -{ - "hosts": { - "boss-computer": "100.64.0.1", - "admin1-computer": "100.64.0.2", - "dev1-computer": "100.64.0.3", - "dev1-phone": "100.64.0.4", - "dev2-computer": "100.64.0.5", - "intern1-computer": "100.64.0.6", - "prod-app-server1": "100.64.0.8" - }, - "groups": { - "group:dev": ["dev1-computer", "dev1-phone", "dev2-computer"], - "group:admin": ["admin1-computer"], - "group:boss": ["boss-computer"], - "group:intern": ["intern1-computer"] - }, - "acls": [ - // boss have access to all servers but no users hosts - { - "action": "accept", - "users": ["group:boss"], - "ports": ["prod:*", "dev:*", "internal:*"] - }, - - // admin have access to administration port (lets only consider port 22 here) - { - "action": "accept", - "users": ["group:admin"], - "ports": ["prod:22", "dev:22", "internal:22"] - }, - - // dev can do anything on dev servers and check access on prod servers - { - "action": "accept", - "users": ["group:dev"], - "ports": ["dev:*", "prod-app-server1:80,443"] - }, - - // interns only have access to port 80 and 443 on dev servers (lame internship) - { "action": "accept", "users": ["group:intern"], "ports": ["dev:80,443"] }, - - // users can access their own devices - { - "action": "accept", - "users": ["dev1-computer"], - "ports": ["dev1-phone:*"] - }, - { - "action": "accept", - "users": ["dev1-phone"], - "ports": ["dev1-computer:*"] - }, - - // internal namespace communications should still be allowed within the namespace - { "action": "accept", "users": ["dev"], "ports": ["dev:*"] }, - { "action": "accept", "users": ["prod"], "ports": ["prod:*"] }, - { "action": "accept", "users": ["internal"], "ports": ["internal:*"] } - ] -} -``` - -Since communications between namespace isn't possible we also have to share the -devices between the namespaces. - -```bash - -// add boss host to prod, dev and internal network -headscale nodes share -i 1 -n prod -headscale nodes share -i 1 -n dev -headscale nodes share -i 1 -n internal - -// add admin computer to prod, dev and internal network -headscale nodes share -i 2 -n prod -headscale nodes share -i 2 -n dev -headscale nodes share -i 2 -n internal - -// add all dev to prod and dev network -headscale nodes share -i 3 -n dev -headscale nodes share -i 4 -n dev -headscale nodes share -i 3 -n prod -headscale nodes share -i 4 -n prod -headscale nodes share -i 5 -n dev -headscale nodes share -i 5 -n prod - -headscale nodes share -i 6 -n dev -``` - -This fake network have not been tested but it should work. Operating it could -be quite tedious if the company grows. Each time a new user join we have to add -it to a group, and share it to the correct namespaces. If the user want -multiple devices we have to allow communication to each of them one by one. If -business conduct a change in the organisations we may have to rewrite all acls -and reorganise all namespaces. - -If we add servers in production we should also update the ACLs to allow dev -access to certain category of them (only app servers for example). - -### example based on the proposition in this document - -Let's create the namespaces - -```bash -headscale namespaces create boss -headscale namespaces create admin1 -headscale namespaces create dev1 -headscale namespaces create dev2 -headscale namespaces create intern1 -``` - -We don't need to create namespaces for the servers because the servers will be -tagged. When registering the servers we will need to add the flag -`--advertised-tags=tag:,tag:`, and the user (namespace) that is -registering the server should be allowed to do it. Since anyone can add tags to -a server they can register, the check of the tags is done on headscale server -and only valid tags are applied. A tag is valid if the namespace that is -registering it is allowed to do it. - -Here are the ACL's to implement the same permissions as above: - -```json -{ - // groups are simpler and only list the namespaces name - "groups": { - "group:boss": ["boss"], - "group:dev": ["dev1", "dev2"], - "group:admin": ["admin1"], - "group:intern": ["intern1"] - }, - "tagOwners": { - // the administrators can add servers in production - "tag:prod-databases": ["group:admin"], - "tag:prod-app-servers": ["group:admin"], - - // the boss can tag any server as internal - "tag:internal": ["group:boss"], - - // dev can add servers for dev purposes as well as admins - "tag:dev-databases": ["group:admin", "group:dev"], - "tag:dev-app-servers": ["group:admin", "group:dev"] - - // interns cannot add servers - }, - "acls": [ - // boss have access to all servers - { - "action": "accept", - "users": ["group:boss"], - "ports": [ - "tag:prod-databases:*", - "tag:prod-app-servers:*", - "tag:internal:*", - "tag:dev-databases:*", - "tag:dev-app-servers:*" - ] - }, - - // admin have only access to administrative ports of the servers - { - "action": "accept", - "users": ["group:admin"], - "ports": [ - "tag:prod-databases:22", - "tag:prod-app-servers:22", - "tag:internal:22", - "tag:dev-databases:22", - "tag:dev-app-servers:22" - ] - }, - - { - "action": "accept", - "users": ["group:dev"], - "ports": [ - "tag:dev-databases:*", - "tag:dev-app-servers:*", - "tag:prod-app-servers:80,443" - ] - }, - - // servers should be able to talk to database. Database should not be able to initiate connections to server - { - "action": "accept", - "users": ["tag:dev-app-servers"], - "ports": ["tag:dev-databases:5432"] - }, - { - "action": "accept", - "users": ["tag:prod-app-servers"], - "ports": ["tag:prod-databases:5432"] - }, - - // interns have access to dev-app-servers only in reading mode - { - "action": "accept", - "users": ["group:intern"], - "ports": ["tag:dev-app-servers:80,443"] - }, - - // we still have to allow internal namespaces communications since nothing guarantees that each user have their own namespaces. This could be talked over. - { "action": "accept", "users": ["boss"], "ports": ["boss:*"] }, - { "action": "accept", "users": ["dev1"], "ports": ["dev1:*"] }, - { "action": "accept", "users": ["dev2"], "ports": ["dev2:*"] }, - { "action": "accept", "users": ["admin1"], "ports": ["admin1:*"] }, - { "action": "accept", "users": ["intern1"], "ports": ["intern1:*"] } - ] -} -``` - -With this implementation, the sharing step is not necessary. Maintenance cost -of the ACL file is lower and less tedious (no need to map hostname and IP's -into it). diff --git a/docs/proposals/002-better-routing.md b/docs/proposals/002-better-routing.md deleted file mode 100644 index c56a38ff..00000000 --- a/docs/proposals/002-better-routing.md +++ /dev/null @@ -1,48 +0,0 @@ -# Better route management - -As of today, route management in Headscale is very basic and does not allow for much flexibility, including implementing subnet HA, 4via6 or more advanced features. We also have a number of bugs (e.g., routes exposed by ephemeral nodes) - -This proposal aims to improve the route management. - -## Current situation - -Routes advertised by the nodes are read from the Hostinfo struct. If approved from the the CLI or via autoApprovers, the route is added to the EnabledRoutes field in `Machine`. - -This means that the advertised routes are not persisted in the database, as Hostinfo is always replaced. In the same way, EnabledRoutes can get out of sync with the actual routes in the node. - -In case of colliding routes (i.e., subnets that are exposed from multiple nodes), we are currently just sending all of them in `PrimaryRoutes`... and hope for the best. (`PrimaryRoutes` is the field in `Node` used for subnet failover). - -## Proposal - -The core part is to create a new `Route` struct (and DB table), with the following fields: - -```go -type Route struct { - ID uint64 `gorm:"primary_key"` - - Machine *Machine - Prefix IPPrefix - - Advertised bool - Enabled bool - IsPrimary bool - - - CreatedAt *time.Time - UpdatedAt *time.Time - DeletedAt *time.Time -} -``` - -- The `Advertised` field is set to true if the route is being advertised by the node. It is set to false if the route is removed. This way we can indicate if a later enabled route has stopped being advertised. A similar behaviour happens in the Tailscale.com control panel. - -- The `Enabled` field is set to true if the route is enabled - via CLI or autoApprovers. - -- `IsPrimary` indicates if Headscale has selected this route as the primary route for that particular subnet. This allows us to implement subnet failover. This would be fully automatic if there is more than subnet routers advertising the same network - which is the behaviour of Tailscale.com. - -## Stuff to bear in mind - -- We need to make sure to migrate the current `EnabledRoutes` of `Machine` into the new table. -- When a node stops sharing a subnet, I reckon we should mark it both as not `Advertised` and not `Enabled`. Users should re-enable it if the node advertises it again. -- If only one subnet router is advertising a subnet, we should mark it as primary. -- Regarding subnet failover, the current behaviour of Tailscale.com is to perform the failover after 15 seconds from the node disconnecting from their control panel. I reckon we cannot do the same currently. Our maximum granularity is the keep alive period. diff --git a/mkdocs.yml b/mkdocs.yml index 2dca103d..c14fd716 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -141,6 +141,3 @@ nav: - Android: android-client.md - Windows: windows-client.md - iOS: iOS-client.md - - Proposals: - - ACLs: proposals/001-acls.md - - Better routing: proposals/002-better-routing.md From 42d2c27853ce98cf50df84a4e4c14409c618f15a Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 5 Sep 2024 14:00:19 +0200 Subject: [PATCH 131/145] fix goreleaser warnings (#2106) * add version to goreleaser config Signed-off-by: Kristoffer Dalby * rename deprected setting Signed-off-by: Kristoffer Dalby --------- Signed-off-by: Kristoffer Dalby --- .goreleaser.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 4e91c74d..4aabde4b 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,4 +1,5 @@ --- +version: 2 before: hooks: - go mod tidy -compat=1.22 @@ -184,7 +185,7 @@ kos: checksum: name_template: "checksums.txt" snapshot: - name_template: "{{ .Tag }}-next" + version_template: "{{ .Tag }}-next" changelog: sort: asc filters: From adc084f20f843d7963c999764fa83939668d2d2c Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 5 Sep 2024 14:00:36 +0200 Subject: [PATCH 132/145] add no stalebot exception (#2107) Signed-off-by: Kristoffer Dalby --- .github/workflows/stale.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index f7c4ae75..592929cb 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -20,4 +20,5 @@ jobs: 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 + exempt-issue-labels: "no-stale-bot" repo-token: ${{ secrets.GITHUB_TOKEN }} From f368ed01ed18b1d9388879f17f4e78d29218fbd9 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 5 Sep 2024 16:46:20 +0200 Subject: [PATCH 133/145] 2068 AutoApprovers tests (#2105) * replace old suite approved routes test with table driven Signed-off-by: Kristoffer Dalby * add test to reproduce issue Signed-off-by: Kristoffer Dalby * add integration test for 2068 Signed-off-by: Kristoffer Dalby --------- Signed-off-by: Kristoffer Dalby --- .github/workflows/test-integration.yaml | 1 + hscontrol/db/node_test.go | 152 ++++++++++++++++-------- hscontrol/poll.go | 23 +--- hscontrol/util/net.go | 19 +++ integration/route_test.go | 90 ++++++++++++++ 5 files changed, 215 insertions(+), 70 deletions(-) diff --git a/.github/workflows/test-integration.yaml b/.github/workflows/test-integration.yaml index d5b362b7..ed194da1 100644 --- a/.github/workflows/test-integration.yaml +++ b/.github/workflows/test-integration.yaml @@ -55,6 +55,7 @@ jobs: - TestEnablingRoutes - TestHASubnetRouterFailover - TestEnableDisableAutoApprovedRoute + - TestAutoApprovedSubRoute2068 - TestSubnetRouteACL - TestHeadscale - TestCreateTailscale diff --git a/hscontrol/db/node_test.go b/hscontrol/db/node_test.go index c83da120..94cce13b 100644 --- a/hscontrol/db/node_test.go +++ b/hscontrol/db/node_test.go @@ -6,6 +6,7 @@ import ( "math/big" "net/netip" "regexp" + "sort" "strconv" "sync" "testing" @@ -518,8 +519,37 @@ func TestHeadscale_generateGivenName(t *testing.T) { } } -func (s *Suite) TestAutoApproveRoutes(c *check.C) { - acl := []byte(` +func TestAutoApproveRoutes(t *testing.T) { + tests := []struct { + name string + acl string + routes []netip.Prefix + want []netip.Prefix + }{ + { + name: "2068-approve-issue-sub", + acl: ` +{ + "groups": { + "group:k8s": ["test"] + }, + + "acls": [ + {"action": "accept", "users": ["*"], "ports": ["*:*"]}, + ], + + "autoApprovers": { + "routes": { + "10.42.0.0/16": ["test"], + } + } +}`, + routes: []netip.Prefix{netip.MustParsePrefix("10.42.7.0/24")}, + want: []netip.Prefix{netip.MustParsePrefix("10.42.7.0/24")}, + }, + { + name: "2068-approve-issue-sub", + acl: ` { "tagOwners": { "tag:exit": ["test"], @@ -540,61 +570,83 @@ func (s *Suite) TestAutoApproveRoutes(c *check.C) { "10.11.0.0/16": ["test"], } } -} - `) - - pol, err := policy.LoadACLPolicyFromBytes(acl) - c.Assert(err, check.IsNil) - c.Assert(pol, check.NotNil) - - 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) - - nodeKey := key.NewNode() - machineKey := key.NewMachine() - - defaultRouteV4 := netip.MustParsePrefix("0.0.0.0/0") - defaultRouteV6 := netip.MustParsePrefix("::/0") - route1 := netip.MustParsePrefix("10.10.0.0/16") - // Check if a subprefix of an autoapproved route is approved - route2 := netip.MustParsePrefix("10.11.0.0/24") - - v4 := netip.MustParseAddr("100.64.0.1") - node := types.Node{ - ID: 0, - MachineKey: machineKey.Public(), - NodeKey: nodeKey.Public(), - Hostname: "test", - UserID: user.ID, - RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: ptr.To(pak.ID), - Hostinfo: &tailcfg.Hostinfo{ - RequestTags: []string{"tag:exit"}, - RoutableIPs: []netip.Prefix{defaultRouteV4, defaultRouteV6, route1, route2}, +}`, + routes: []netip.Prefix{ + netip.MustParsePrefix("0.0.0.0/0"), + netip.MustParsePrefix("::/0"), + netip.MustParsePrefix("10.10.0.0/16"), + netip.MustParsePrefix("10.11.0.0/24"), + }, + want: []netip.Prefix{ + netip.MustParsePrefix("::/0"), + netip.MustParsePrefix("10.11.0.0/24"), + netip.MustParsePrefix("10.10.0.0/16"), + netip.MustParsePrefix("0.0.0.0/0"), + }, }, - IPv4: &v4, } - trx := db.DB.Save(&node) - c.Assert(trx.Error, check.IsNil) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + adb, err := newTestDB() + assert.NoError(t, err) + pol, err := policy.LoadACLPolicyFromBytes([]byte(tt.acl)) - sendUpdate, err := db.SaveNodeRoutes(&node) - c.Assert(err, check.IsNil) - c.Assert(sendUpdate, check.Equals, false) + assert.NoError(t, err) + assert.NotNil(t, pol) - node0ByID, err := db.GetNodeByID(0) - c.Assert(err, check.IsNil) + user, err := adb.CreateUser("test") + assert.NoError(t, err) - // TODO(kradalby): Check state update - err = db.EnableAutoApprovedRoutes(pol, node0ByID) - c.Assert(err, check.IsNil) + pak, err := adb.CreatePreAuthKey(user.Name, false, false, nil, nil) + assert.NoError(t, err) - enabledRoutes, err := db.GetEnabledRoutes(node0ByID) - c.Assert(err, check.IsNil) - c.Assert(enabledRoutes, check.HasLen, 4) + nodeKey := key.NewNode() + machineKey := key.NewMachine() + + v4 := netip.MustParseAddr("100.64.0.1") + node := types.Node{ + ID: 0, + MachineKey: machineKey.Public(), + NodeKey: nodeKey.Public(), + Hostname: "test", + UserID: user.ID, + RegisterMethod: util.RegisterMethodAuthKey, + AuthKeyID: ptr.To(pak.ID), + Hostinfo: &tailcfg.Hostinfo{ + RequestTags: []string{"tag:exit"}, + RoutableIPs: tt.routes, + }, + IPv4: &v4, + } + + trx := adb.DB.Save(&node) + assert.NoError(t, trx.Error) + + sendUpdate, err := adb.SaveNodeRoutes(&node) + assert.NoError(t, err) + assert.False(t, sendUpdate) + + node0ByID, err := adb.GetNodeByID(0) + assert.NoError(t, err) + + // TODO(kradalby): Check state update + err = adb.EnableAutoApprovedRoutes(pol, node0ByID) + assert.NoError(t, err) + + enabledRoutes, err := adb.GetEnabledRoutes(node0ByID) + assert.NoError(t, err) + assert.Len(t, enabledRoutes, len(tt.want)) + + sort.Slice(enabledRoutes, func(i, j int) bool { + return util.ComparePrefix(enabledRoutes[i], enabledRoutes[j]) > 0 + }) + + if diff := cmp.Diff(tt.want, enabledRoutes, util.Comparers...); diff != "" { + t.Errorf("unexpected enabled routes (-want +got):\n%s", diff) + } + }) + } } func TestEphemeralGarbageCollectorOrder(t *testing.T) { diff --git a/hscontrol/poll.go b/hscontrol/poll.go index b9bf65a2..d7ba682e 100644 --- a/hscontrol/poll.go +++ b/hscontrol/poll.go @@ -1,12 +1,10 @@ package hscontrol import ( - "cmp" "context" "fmt" "math/rand/v2" "net/http" - "net/netip" "sort" "strings" "time" @@ -14,6 +12,7 @@ import ( "github.com/juanfont/headscale/hscontrol/db" "github.com/juanfont/headscale/hscontrol/mapper" "github.com/juanfont/headscale/hscontrol/types" + "github.com/juanfont/headscale/hscontrol/util" "github.com/rs/zerolog/log" "github.com/sasha-s/go-deadlock" xslices "golang.org/x/exp/slices" @@ -742,10 +741,10 @@ func hostInfoChanged(old, new *tailcfg.Hostinfo) (bool, bool) { newRoutes := new.RoutableIPs sort.Slice(oldRoutes, func(i, j int) bool { - return comparePrefix(oldRoutes[i], oldRoutes[j]) > 0 + return util.ComparePrefix(oldRoutes[i], oldRoutes[j]) > 0 }) sort.Slice(newRoutes, func(i, j int) bool { - return comparePrefix(newRoutes[i], newRoutes[j]) > 0 + return util.ComparePrefix(newRoutes[i], newRoutes[j]) > 0 }) if !xslices.Equal(oldRoutes, newRoutes) { @@ -764,19 +763,3 @@ func hostInfoChanged(old, new *tailcfg.Hostinfo) (bool, bool) { return false, false } - -// TODO(kradalby): Remove after go 1.23, will be in stdlib. -// Compare returns an integer comparing two prefixes. -// The result will be 0 if p == p2, -1 if p < p2, and +1 if p > p2. -// Prefixes sort first by validity (invalid before valid), then -// address family (IPv4 before IPv6), then prefix length, then -// address. -func comparePrefix(p, p2 netip.Prefix) int { - if c := cmp.Compare(p.Addr().BitLen(), p2.Addr().BitLen()); c != 0 { - return c - } - if c := cmp.Compare(p.Bits(), p2.Bits()); c != 0 { - return c - } - return p.Addr().Compare(p2.Addr()) -} diff --git a/hscontrol/util/net.go b/hscontrol/util/net.go index b704c936..c44b7287 100644 --- a/hscontrol/util/net.go +++ b/hscontrol/util/net.go @@ -1,8 +1,10 @@ package util import ( + "cmp" "context" "net" + "net/netip" ) func GrpcSocketDialer(ctx context.Context, addr string) (net.Conn, error) { @@ -10,3 +12,20 @@ func GrpcSocketDialer(ctx context.Context, addr string) (net.Conn, error) { return d.DialContext(ctx, "unix", addr) } + + +// TODO(kradalby): Remove after go 1.24, will be in stdlib. +// Compare returns an integer comparing two prefixes. +// The result will be 0 if p == p2, -1 if p < p2, and +1 if p > p2. +// Prefixes sort first by validity (invalid before valid), then +// address family (IPv4 before IPv6), then prefix length, then +// address. +func ComparePrefix(p, p2 netip.Prefix) int { + if c := cmp.Compare(p.Addr().BitLen(), p2.Addr().BitLen()); c != 0 { + return c + } + if c := cmp.Compare(p.Bits(), p2.Bits()); c != 0 { + return c + } + return p.Addr().Compare(p2.Addr()) +} diff --git a/integration/route_test.go b/integration/route_test.go index a92258af..0252e702 100644 --- a/integration/route_test.go +++ b/integration/route_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" v1 "github.com/juanfont/headscale/gen/go/headscale/v1" "github.com/juanfont/headscale/hscontrol/policy" "github.com/juanfont/headscale/hscontrol/util" @@ -957,6 +958,95 @@ func TestEnableDisableAutoApprovedRoute(t *testing.T) { assert.Equal(t, true, reAdvertisedRoutes[0].GetIsPrimary()) } +func TestAutoApprovedSubRoute2068(t *testing.T) { + IntegrationSkip(t) + t.Parallel() + + expectedRoutes := "10.42.7.0/24" + + user := "subroute" + + scenario, err := NewScenario(dockertestMaxWait()) + assertNoErrf(t, "failed to create scenario: %s", err) + defer scenario.Shutdown() + + spec := map[string]int{ + user: 1, + } + + err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{tsic.WithTags([]string{"tag:approve"})}, hsic.WithTestName("clienableroute"), hsic.WithACLPolicy( + &policy.ACLPolicy{ + ACLs: []policy.ACL{ + { + Action: "accept", + Sources: []string{"*"}, + Destinations: []string{"*:*"}, + }, + }, + TagOwners: map[string][]string{ + "tag:approve": {user}, + }, + AutoApprovers: policy.AutoApprovers{ + Routes: map[string][]string{ + "10.42.0.0/16": {"tag:approve"}, + }, + }, + }, + )) + assertNoErrHeadscaleEnv(t, err) + + allClients, err := scenario.ListTailscaleClients() + assertNoErrListClients(t, err) + + err = scenario.WaitForTailscaleSync() + assertNoErrSync(t, err) + + headscale, err := scenario.Headscale() + assertNoErrGetHeadscale(t, err) + + subRouter1 := allClients[0] + + // Initially advertise route + command := []string{ + "tailscale", + "set", + "--advertise-routes=" + expectedRoutes, + } + _, _, err = subRouter1.Execute(command) + assertNoErrf(t, "failed to advertise route: %s", err) + + time.Sleep(10 * time.Second) + + var routes []*v1.Route + err = executeAndUnmarshal( + headscale, + []string{ + "headscale", + "routes", + "list", + "--output", + "json", + }, + &routes, + ) + assertNoErr(t, err) + assert.Len(t, routes, 1) + + want := []*v1.Route{ + { + Id: 1, + Prefix: expectedRoutes, + Advertised: true, + Enabled: true, + IsPrimary: true, + }, + } + + if diff := cmp.Diff(want, routes, cmpopts.IgnoreUnexported(v1.Route{}), cmpopts.IgnoreFields(v1.Route{}, "Node", "CreatedAt", "UpdatedAt", "DeletedAt")); diff != "" { + t.Errorf("unexpected routes (-want +got):\n%s", diff) + } +} + // TestSubnetRouteACL verifies that Subnet routes are distributed // as expected when ACLs are activated. // It implements the issue from From 8a3a0fee3ccbca7dd67b0d2965b523c8b6cb5451 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sat, 7 Sep 2024 09:23:58 +0200 Subject: [PATCH 134/145] Only load needed part of configuration (#2109) --- .gitignore | 1 + CHANGELOG.md | 2 + cmd/headscale/cli/api_key.go | 33 +------ cmd/headscale/cli/configtest.go | 2 +- cmd/headscale/cli/debug.go | 14 +-- cmd/headscale/cli/nodes.go | 34 ++----- cmd/headscale/cli/policy.go | 27 +++--- cmd/headscale/cli/preauthkeys.go | 28 +----- cmd/headscale/cli/root.go | 16 ++-- cmd/headscale/cli/routes.go | 40 +------- cmd/headscale/cli/{server.go => serve.go} | 2 +- cmd/headscale/cli/users.go | 22 +---- cmd/headscale/cli/utils.go | 27 +++--- cmd/headscale/headscale_test.go | 58 ------------ hscontrol/grpcv1.go | 8 +- hscontrol/types/config.go | 110 ++++++++++++---------- hscontrol/types/config_test.go | 68 +++++++++++-- integration/cli_test.go | 28 ++---- 18 files changed, 196 insertions(+), 324 deletions(-) rename cmd/headscale/cli/{server.go => serve.go} (92%) diff --git a/.gitignore b/.gitignore index f6e506bc..1662d7f2 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ dist/ /headscale config.json config.yaml +config*.yaml derp.yaml *.hujson *.key diff --git a/CHANGELOG.md b/CHANGELOG.md index 76982608..91aed9ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,8 @@ after improving the test harness as part of adopting [#1460](https://github.com/ - Add APIs for managing headscale policy. [#1792](https://github.com/juanfont/headscale/pull/1792) - Fix for registering nodes using preauthkeys when running on a postgres database in a non-UTC timezone. [#764](https://github.com/juanfont/headscale/issues/764) - Make sure integration tests cover postgres for all scenarios +- CLI commands (all except `serve`) only requires minimal configuration, no more errors or warnings from unset settings [#2109](https://github.com/juanfont/headscale/pull/2109) +- CLI results are now concistently sent to stdout and errors to stderr [#2109](https://github.com/juanfont/headscale/pull/2109) ## 0.22.3 (2023-05-12) diff --git a/cmd/headscale/cli/api_key.go b/cmd/headscale/cli/api_key.go index 372ec390..bd839b7b 100644 --- a/cmd/headscale/cli/api_key.go +++ b/cmd/headscale/cli/api_key.go @@ -54,7 +54,7 @@ var listAPIKeys = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { output, _ := cmd.Flags().GetString("output") - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -67,14 +67,10 @@ var listAPIKeys = &cobra.Command{ fmt.Sprintf("Error getting the list of keys: %s", err), output, ) - - return } if output != "" { SuccessOutput(response.GetApiKeys(), "", output) - - return } tableData := pterm.TableData{ @@ -102,8 +98,6 @@ var listAPIKeys = &cobra.Command{ fmt.Sprintf("Failed to render pterm table: %s", err), output, ) - - return } }, } @@ -119,9 +113,6 @@ If you loose a key, create a new one and revoke (expire) the old one.`, Run: func(cmd *cobra.Command, args []string) { output, _ := cmd.Flags().GetString("output") - log.Trace(). - Msg("Preparing to create ApiKey") - request := &v1.CreateApiKeyRequest{} durationStr, _ := cmd.Flags().GetString("expiration") @@ -133,19 +124,13 @@ If you loose a key, create a new one and revoke (expire) the old one.`, fmt.Sprintf("Could not parse duration: %s\n", err), output, ) - - return } expiration := time.Now().UTC().Add(time.Duration(duration)) - log.Trace(). - Dur("expiration", time.Duration(duration)). - Msg("expiration has been set") - request.Expiration = timestamppb.New(expiration) - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -156,8 +141,6 @@ If you loose a key, create a new one and revoke (expire) the old one.`, fmt.Sprintf("Cannot create Api Key: %s\n", err), output, ) - - return } SuccessOutput(response.GetApiKey(), response.GetApiKey(), output) @@ -178,11 +161,9 @@ var expireAPIKeyCmd = &cobra.Command{ fmt.Sprintf("Error getting prefix from CLI flag: %s", err), output, ) - - return } - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -197,8 +178,6 @@ var expireAPIKeyCmd = &cobra.Command{ fmt.Sprintf("Cannot expire Api Key: %s\n", err), output, ) - - return } SuccessOutput(response, "Key expired", output) @@ -219,11 +198,9 @@ var deleteAPIKeyCmd = &cobra.Command{ fmt.Sprintf("Error getting prefix from CLI flag: %s", err), output, ) - - return } - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -238,8 +215,6 @@ var deleteAPIKeyCmd = &cobra.Command{ fmt.Sprintf("Cannot delete Api Key: %s\n", err), output, ) - - return } SuccessOutput(response, "Key deleted", output) diff --git a/cmd/headscale/cli/configtest.go b/cmd/headscale/cli/configtest.go index 72744a7b..d469885b 100644 --- a/cmd/headscale/cli/configtest.go +++ b/cmd/headscale/cli/configtest.go @@ -14,7 +14,7 @@ var configTestCmd = &cobra.Command{ Short: "Test the configuration.", Long: "Run a test of the configuration and exit.", Run: func(cmd *cobra.Command, args []string) { - _, err := getHeadscaleApp() + _, err := newHeadscaleServerWithConfig() if err != nil { log.Fatal().Caller().Err(err).Msg("Error initializing") } diff --git a/cmd/headscale/cli/debug.go b/cmd/headscale/cli/debug.go index 054fc07f..72cde32d 100644 --- a/cmd/headscale/cli/debug.go +++ b/cmd/headscale/cli/debug.go @@ -64,11 +64,9 @@ var createNodeCmd = &cobra.Command{ user, err := cmd.Flags().GetString("user") if err != nil { ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output) - - return } - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -79,8 +77,6 @@ var createNodeCmd = &cobra.Command{ fmt.Sprintf("Error getting node from flag: %s", err), output, ) - - return } machineKey, err := cmd.Flags().GetString("key") @@ -90,8 +86,6 @@ var createNodeCmd = &cobra.Command{ fmt.Sprintf("Error getting key from flag: %s", err), output, ) - - return } var mkey key.MachinePublic @@ -102,8 +96,6 @@ var createNodeCmd = &cobra.Command{ fmt.Sprintf("Failed to parse machine key from flag: %s", err), output, ) - - return } routes, err := cmd.Flags().GetStringSlice("route") @@ -113,8 +105,6 @@ var createNodeCmd = &cobra.Command{ fmt.Sprintf("Error getting routes from flag: %s", err), output, ) - - return } request := &v1.DebugCreateNodeRequest{ @@ -131,8 +121,6 @@ var createNodeCmd = &cobra.Command{ fmt.Sprintf("Cannot create node: %s", status.Convert(err).Message()), output, ) - - return } SuccessOutput(response.GetNode(), "Node created", output) diff --git a/cmd/headscale/cli/nodes.go b/cmd/headscale/cli/nodes.go index 4de7b969..b9e97a33 100644 --- a/cmd/headscale/cli/nodes.go +++ b/cmd/headscale/cli/nodes.go @@ -116,11 +116,9 @@ var registerNodeCmd = &cobra.Command{ user, err := cmd.Flags().GetString("user") if err != nil { ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output) - - return } - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -131,8 +129,6 @@ var registerNodeCmd = &cobra.Command{ fmt.Sprintf("Error getting node key from flag: %s", err), output, ) - - return } request := &v1.RegisterNodeRequest{ @@ -150,8 +146,6 @@ var registerNodeCmd = &cobra.Command{ ), output, ) - - return } SuccessOutput( @@ -169,17 +163,13 @@ var listNodesCmd = &cobra.Command{ user, err := cmd.Flags().GetString("user") if err != nil { ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output) - - return } showTags, err := cmd.Flags().GetBool("tags") if err != nil { ErrorOutput(err, fmt.Sprintf("Error getting tags flag: %s", err), output) - - return } - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -194,21 +184,15 @@ var listNodesCmd = &cobra.Command{ fmt.Sprintf("Cannot get nodes: %s", status.Convert(err).Message()), output, ) - - return } if output != "" { SuccessOutput(response.GetNodes(), "", output) - - return } tableData, err := nodesToPtables(user, showTags, response.GetNodes()) if err != nil { ErrorOutput(err, fmt.Sprintf("Error converting to table: %s", err), output) - - return } err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render() @@ -218,8 +202,6 @@ var listNodesCmd = &cobra.Command{ fmt.Sprintf("Failed to render pterm table: %s", err), output, ) - - return } }, } @@ -243,7 +225,7 @@ var expireNodeCmd = &cobra.Command{ return } - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -286,7 +268,7 @@ var renameNodeCmd = &cobra.Command{ return } - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -335,7 +317,7 @@ var deleteNodeCmd = &cobra.Command{ return } - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -435,7 +417,7 @@ var moveNodeCmd = &cobra.Command{ return } - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -508,7 +490,7 @@ be assigned to nodes.`, return } if confirm { - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -681,7 +663,7 @@ var tagCmd = &cobra.Command{ Aliases: []string{"tags", "t"}, Run: func(cmd *cobra.Command, args []string) { output, _ := cmd.Flags().GetString("output") - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() diff --git a/cmd/headscale/cli/policy.go b/cmd/headscale/cli/policy.go index 00c4566d..d1349b5a 100644 --- a/cmd/headscale/cli/policy.go +++ b/cmd/headscale/cli/policy.go @@ -1,6 +1,7 @@ package cli import ( + "fmt" "io" "os" @@ -30,7 +31,8 @@ var getPolicy = &cobra.Command{ Short: "Print the current ACL Policy", Aliases: []string{"show", "view", "fetch"}, Run: func(cmd *cobra.Command, args []string) { - ctx, client, conn, cancel := getHeadscaleCLIClient() + output, _ := cmd.Flags().GetString("output") + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -38,13 +40,13 @@ var getPolicy = &cobra.Command{ response, err := client.GetPolicy(ctx, request) if err != nil { - log.Fatal().Err(err).Msg("Failed to get the policy") - - return + ErrorOutput(err, fmt.Sprintf("Failed loading ACL Policy: %s", err), output) } // TODO(pallabpain): Maybe print this better? - SuccessOutput("", response.GetPolicy(), "hujson") + // This does not pass output as we dont support yaml, json or json-line + // output for this command. It is HuJSON already. + SuccessOutput("", response.GetPolicy(), "") }, } @@ -56,33 +58,28 @@ var setPolicy = &cobra.Command{ This command only works when the acl.policy_mode is set to "db", and the policy will be stored in the database.`, Aliases: []string{"put", "update"}, Run: func(cmd *cobra.Command, args []string) { + output, _ := cmd.Flags().GetString("output") policyPath, _ := cmd.Flags().GetString("file") f, err := os.Open(policyPath) if err != nil { - log.Fatal().Err(err).Msg("Error opening the policy file") - - return + ErrorOutput(err, fmt.Sprintf("Error opening the policy file: %s", err), output) } defer f.Close() policyBytes, err := io.ReadAll(f) if err != nil { - log.Fatal().Err(err).Msg("Error reading the policy file") - - return + ErrorOutput(err, fmt.Sprintf("Error reading the policy file: %s", err), output) } request := &v1.SetPolicyRequest{Policy: string(policyBytes)} - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() if _, err := client.SetPolicy(ctx, request); err != nil { - log.Fatal().Err(err).Msg("Failed to set ACL Policy") - - return + ErrorOutput(err, fmt.Sprintf("Failed to set ACL Policy: %s", err), output) } SuccessOutput(nil, "Policy updated.", "") diff --git a/cmd/headscale/cli/preauthkeys.go b/cmd/headscale/cli/preauthkeys.go index cc3b1b76..0074e029 100644 --- a/cmd/headscale/cli/preauthkeys.go +++ b/cmd/headscale/cli/preauthkeys.go @@ -60,11 +60,9 @@ var listPreAuthKeys = &cobra.Command{ user, err := cmd.Flags().GetString("user") if err != nil { ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output) - - return } - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -85,8 +83,6 @@ var listPreAuthKeys = &cobra.Command{ if output != "" { SuccessOutput(response.GetPreAuthKeys(), "", output) - - return } tableData := pterm.TableData{ @@ -134,8 +130,6 @@ var listPreAuthKeys = &cobra.Command{ fmt.Sprintf("Failed to render pterm table: %s", err), output, ) - - return } }, } @@ -150,20 +144,12 @@ var createPreAuthKeyCmd = &cobra.Command{ user, err := cmd.Flags().GetString("user") if err != nil { ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output) - - return } reusable, _ := cmd.Flags().GetBool("reusable") ephemeral, _ := cmd.Flags().GetBool("ephemeral") tags, _ := cmd.Flags().GetStringSlice("tags") - log.Trace(). - Bool("reusable", reusable). - Bool("ephemeral", ephemeral). - Str("user", user). - Msg("Preparing to create preauthkey") - request := &v1.CreatePreAuthKeyRequest{ User: user, Reusable: reusable, @@ -180,8 +166,6 @@ var createPreAuthKeyCmd = &cobra.Command{ fmt.Sprintf("Could not parse duration: %s\n", err), output, ) - - return } expiration := time.Now().UTC().Add(time.Duration(duration)) @@ -192,7 +176,7 @@ var createPreAuthKeyCmd = &cobra.Command{ request.Expiration = timestamppb.New(expiration) - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -203,8 +187,6 @@ var createPreAuthKeyCmd = &cobra.Command{ fmt.Sprintf("Cannot create Pre Auth Key: %s\n", err), output, ) - - return } SuccessOutput(response.GetPreAuthKey(), response.GetPreAuthKey().GetKey(), output) @@ -227,11 +209,9 @@ var expirePreAuthKeyCmd = &cobra.Command{ user, err := cmd.Flags().GetString("user") if err != nil { ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output) - - return } - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -247,8 +227,6 @@ var expirePreAuthKeyCmd = &cobra.Command{ fmt.Sprintf("Cannot expire Pre Auth Key: %s\n", err), output, ) - - return } SuccessOutput(response, "Key expired", output) diff --git a/cmd/headscale/cli/root.go b/cmd/headscale/cli/root.go index b0d9500e..7bac79ce 100644 --- a/cmd/headscale/cli/root.go +++ b/cmd/headscale/cli/root.go @@ -9,6 +9,7 @@ import ( "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/cobra" + "github.com/spf13/viper" "github.com/tcnksm/go-latest" ) @@ -49,11 +50,6 @@ func initConfig() { } } - cfg, err := types.GetHeadscaleConfig() - if err != nil { - log.Fatal().Err(err).Msg("Failed to read headscale configuration") - } - machineOutput := HasMachineOutputFlag() // If the user has requested a "node" readable format, @@ -62,11 +58,13 @@ func initConfig() { zerolog.SetGlobalLevel(zerolog.Disabled) } - if cfg.Log.Format == types.JSONLogFormat { - log.Logger = log.Output(os.Stdout) - } + // logFormat := viper.GetString("log.format") + // if logFormat == types.JSONLogFormat { + // log.Logger = log.Output(os.Stdout) + // } - if !cfg.DisableUpdateCheck && !machineOutput { + disableUpdateCheck := viper.GetBool("disable_check_updates") + if !disableUpdateCheck && !machineOutput { if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") && Version != "dev" { githubTag := &latest.GithubTag{ diff --git a/cmd/headscale/cli/routes.go b/cmd/headscale/cli/routes.go index 86ef295c..96227b31 100644 --- a/cmd/headscale/cli/routes.go +++ b/cmd/headscale/cli/routes.go @@ -64,11 +64,9 @@ var listRoutesCmd = &cobra.Command{ fmt.Sprintf("Error getting machine id from flag: %s", err), output, ) - - return } - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -82,14 +80,10 @@ var listRoutesCmd = &cobra.Command{ fmt.Sprintf("Cannot get nodes: %s", status.Convert(err).Message()), output, ) - - return } if output != "" { SuccessOutput(response.GetRoutes(), "", output) - - return } routes = response.GetRoutes() @@ -103,14 +97,10 @@ var listRoutesCmd = &cobra.Command{ fmt.Sprintf("Cannot get routes for node %d: %s", machineID, status.Convert(err).Message()), output, ) - - return } if output != "" { SuccessOutput(response.GetRoutes(), "", output) - - return } routes = response.GetRoutes() @@ -119,8 +109,6 @@ var listRoutesCmd = &cobra.Command{ tableData := routesToPtables(routes) if err != nil { ErrorOutput(err, fmt.Sprintf("Error converting to table: %s", err), output) - - return } err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render() @@ -130,8 +118,6 @@ var listRoutesCmd = &cobra.Command{ fmt.Sprintf("Failed to render pterm table: %s", err), output, ) - - return } }, } @@ -150,11 +136,9 @@ var enableRouteCmd = &cobra.Command{ fmt.Sprintf("Error getting machine id from flag: %s", err), output, ) - - return } - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -167,14 +151,10 @@ var enableRouteCmd = &cobra.Command{ fmt.Sprintf("Cannot enable route %d: %s", routeID, status.Convert(err).Message()), output, ) - - return } if output != "" { SuccessOutput(response, "", output) - - return } }, } @@ -193,11 +173,9 @@ var disableRouteCmd = &cobra.Command{ fmt.Sprintf("Error getting machine id from flag: %s", err), output, ) - - return } - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -210,14 +188,10 @@ var disableRouteCmd = &cobra.Command{ fmt.Sprintf("Cannot disable route %d: %s", routeID, status.Convert(err).Message()), output, ) - - return } if output != "" { SuccessOutput(response, "", output) - - return } }, } @@ -236,11 +210,9 @@ var deleteRouteCmd = &cobra.Command{ fmt.Sprintf("Error getting machine id from flag: %s", err), output, ) - - return } - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -253,14 +225,10 @@ var deleteRouteCmd = &cobra.Command{ fmt.Sprintf("Cannot delete route %d: %s", routeID, status.Convert(err).Message()), output, ) - - return } if output != "" { SuccessOutput(response, "", output) - - return } }, } diff --git a/cmd/headscale/cli/server.go b/cmd/headscale/cli/serve.go similarity index 92% rename from cmd/headscale/cli/server.go rename to cmd/headscale/cli/serve.go index a1d19600..9f0fa35e 100644 --- a/cmd/headscale/cli/server.go +++ b/cmd/headscale/cli/serve.go @@ -16,7 +16,7 @@ var serveCmd = &cobra.Command{ return nil }, Run: func(cmd *cobra.Command, args []string) { - app, err := getHeadscaleApp() + app, err := newHeadscaleServerWithConfig() if err != nil { log.Fatal().Caller().Err(err).Msg("Error initializing") } diff --git a/cmd/headscale/cli/users.go b/cmd/headscale/cli/users.go index e6463d6f..d04d7568 100644 --- a/cmd/headscale/cli/users.go +++ b/cmd/headscale/cli/users.go @@ -44,7 +44,7 @@ var createUserCmd = &cobra.Command{ userName := args[0] - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -63,8 +63,6 @@ var createUserCmd = &cobra.Command{ ), output, ) - - return } SuccessOutput(response.GetUser(), "User created", output) @@ -91,7 +89,7 @@ var destroyUserCmd = &cobra.Command{ Name: userName, } - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -102,8 +100,6 @@ var destroyUserCmd = &cobra.Command{ fmt.Sprintf("Error: %s", status.Convert(err).Message()), output, ) - - return } confirm := false @@ -134,8 +130,6 @@ var destroyUserCmd = &cobra.Command{ ), output, ) - - return } SuccessOutput(response, "User destroyed", output) } else { @@ -151,7 +145,7 @@ var listUsersCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { output, _ := cmd.Flags().GetString("output") - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -164,14 +158,10 @@ var listUsersCmd = &cobra.Command{ fmt.Sprintf("Cannot get users: %s", status.Convert(err).Message()), output, ) - - return } if output != "" { SuccessOutput(response.GetUsers(), "", output) - - return } tableData := pterm.TableData{{"ID", "Name", "Created"}} @@ -192,8 +182,6 @@ var listUsersCmd = &cobra.Command{ fmt.Sprintf("Failed to render pterm table: %s", err), output, ) - - return } }, } @@ -213,7 +201,7 @@ var renameUserCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { output, _ := cmd.Flags().GetString("output") - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -232,8 +220,6 @@ var renameUserCmd = &cobra.Command{ ), output, ) - - return } SuccessOutput(response.GetUser(), "User renamed", output) diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index 409e3dc4..ff1137be 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -23,8 +23,8 @@ const ( SocketWritePermissions = 0o666 ) -func getHeadscaleApp() (*hscontrol.Headscale, error) { - cfg, err := types.GetHeadscaleConfig() +func newHeadscaleServerWithConfig() (*hscontrol.Headscale, error) { + cfg, err := types.LoadServerConfig() if err != nil { return nil, fmt.Errorf( "failed to load configuration while creating headscale instance: %w", @@ -40,8 +40,8 @@ func getHeadscaleApp() (*hscontrol.Headscale, error) { return app, nil } -func getHeadscaleCLIClient() (context.Context, v1.HeadscaleServiceClient, *grpc.ClientConn, context.CancelFunc) { - cfg, err := types.GetHeadscaleConfig() +func newHeadscaleCLIWithConfig() (context.Context, v1.HeadscaleServiceClient, *grpc.ClientConn, context.CancelFunc) { + cfg, err := types.LoadCLIConfig() if err != nil { log.Fatal(). Err(err). @@ -130,7 +130,7 @@ func getHeadscaleCLIClient() (context.Context, v1.HeadscaleServiceClient, *grpc. return ctx, client, conn, cancel } -func SuccessOutput(result interface{}, override string, outputFormat string) { +func output(result interface{}, override string, outputFormat string) string { var jsonBytes []byte var err error switch outputFormat { @@ -151,21 +151,26 @@ func SuccessOutput(result interface{}, override string, outputFormat string) { } default: // nolint - fmt.Println(override) - - return + return override } - // nolint - fmt.Println(string(jsonBytes)) + return string(jsonBytes) } +// SuccessOutput prints the result to stdout and exits with status code 0. +func SuccessOutput(result interface{}, override string, outputFormat string) { + fmt.Println(output(result, override, outputFormat)) + os.Exit(0) +} + +// ErrorOutput prints an error message to stderr and exits with status code 1. func ErrorOutput(errResult error, override string, outputFormat string) { type errOutput struct { Error string `json:"error"` } - SuccessOutput(errOutput{errResult.Error()}, override, outputFormat) + fmt.Fprintf(os.Stderr, "%s\n", output(errOutput{errResult.Error()}, override, outputFormat)) + os.Exit(1) } func HasMachineOutputFlag() bool { diff --git a/cmd/headscale/headscale_test.go b/cmd/headscale/headscale_test.go index 580caf17..00c4a276 100644 --- a/cmd/headscale/headscale_test.go +++ b/cmd/headscale/headscale_test.go @@ -4,7 +4,6 @@ import ( "io/fs" "os" "path/filepath" - "strings" "testing" "github.com/juanfont/headscale/hscontrol/types" @@ -113,60 +112,3 @@ func (*Suite) TestConfigLoading(c *check.C) { c.Assert(viper.GetBool("logtail.enabled"), check.Equals, false) c.Assert(viper.GetBool("randomize_client_port"), check.Equals, false) } - -func writeConfig(c *check.C, tmpDir string, configYaml []byte) { - // Populate a custom config file - configFile := filepath.Join(tmpDir, "config.yaml") - err := os.WriteFile(configFile, configYaml, 0o600) - if err != nil { - c.Fatalf("Couldn't write file %s", configFile) - } -} - -func (*Suite) TestTLSConfigValidation(c *check.C) { - tmpDir, err := os.MkdirTemp("", "headscale") - if err != nil { - c.Fatal(err) - } - // defer os.RemoveAll(tmpDir) - configYaml := []byte(`--- -tls_letsencrypt_hostname: example.com -tls_letsencrypt_challenge_type: "" -tls_cert_path: abc.pem -noise: - private_key_path: noise_private.key`) - writeConfig(c, tmpDir, configYaml) - - // Check configuration validation errors (1) - err = types.LoadConfig(tmpDir, false) - c.Assert(err, check.NotNil) - // check.Matches can not handle multiline strings - tmp := strings.ReplaceAll(err.Error(), "\n", "***") - c.Assert( - tmp, - check.Matches, - ".*Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both.*", - ) - c.Assert( - tmp, - check.Matches, - ".*Fatal config error: the only supported values for tls_letsencrypt_challenge_type are.*", - ) - c.Assert( - tmp, - check.Matches, - ".*Fatal config error: server_url must start with https:// or http://.*", - ) - - // Check configuration validation errors (2) - configYaml = []byte(`--- -noise: - private_key_path: noise_private.key -server_url: http://127.0.0.1:8080 -tls_letsencrypt_hostname: example.com -tls_letsencrypt_challenge_type: TLS-ALPN-01 -`) - writeConfig(c, tmpDir, configYaml) - err = types.LoadConfig(tmpDir, false) - c.Assert(err, check.IsNil) -} diff --git a/hscontrol/grpcv1.go b/hscontrol/grpcv1.go index 83048bec..3f985d98 100644 --- a/hscontrol/grpcv1.go +++ b/hscontrol/grpcv1.go @@ -684,7 +684,7 @@ func (api headscaleV1APIServer) GetPolicy( case types.PolicyModeDB: p, err := api.h.db.GetPolicy() if err != nil { - return nil, err + return nil, fmt.Errorf("loading ACL from database: %w", err) } return &v1.GetPolicyResponse{ @@ -696,20 +696,20 @@ func (api headscaleV1APIServer) GetPolicy( absPath := util.AbsolutePathFromConfigPath(api.h.cfg.Policy.Path) f, err := os.Open(absPath) if err != nil { - return nil, err + return nil, fmt.Errorf("reading policy from path %q: %w", absPath, err) } defer f.Close() b, err := io.ReadAll(f) if err != nil { - return nil, err + return nil, fmt.Errorf("reading policy from file: %w", err) } return &v1.GetPolicyResponse{Policy: string(b)}, nil } - return nil, nil + return nil, fmt.Errorf("no supported policy mode found in configuration, policy.mode: %q", api.h.cfg.Policy.Mode) } func (api headscaleV1APIServer) SetPolicy( diff --git a/hscontrol/types/config.go b/hscontrol/types/config.go index 0b7d63b7..8767077e 100644 --- a/hscontrol/types/config.go +++ b/hscontrol/types/config.go @@ -212,6 +212,12 @@ type Tuning struct { NodeMapSessionBufferedChanSize int } +// LoadConfig prepares and loads the Headscale configuration into Viper. +// This means it sets the default values, reads the configuration file and +// environment variables, and handles deprecated configuration options. +// It has to be called before LoadServerConfig and LoadCLIConfig. +// The configuration is not validated and the caller should check for errors +// using a validation function. func LoadConfig(path string, isFile bool) error { if isFile { viper.SetConfigFile(path) @@ -284,14 +290,14 @@ func LoadConfig(path string, isFile bool) error { viper.SetDefault("prefixes.allocation", string(IPAllocationStrategySequential)) - if IsCLIConfigured() { - return nil - } - if err := viper.ReadInConfig(); err != nil { return fmt.Errorf("fatal error reading config file: %w", err) } + return nil +} + +func validateServerConfig() error { depr := deprecator{ warns: make(set.Set[string]), fatals: make(set.Set[string]), @@ -360,12 +366,12 @@ func LoadConfig(path string, isFile bool) error { if errorText != "" { // nolint return errors.New(strings.TrimSuffix(errorText, "\n")) - } else { - return nil } + + return nil } -func GetTLSConfig() TLSConfig { +func tlsConfig() TLSConfig { return TLSConfig{ LetsEncrypt: LetsEncryptConfig{ Hostname: viper.GetString("tls_letsencrypt_hostname"), @@ -384,7 +390,7 @@ func GetTLSConfig() TLSConfig { } } -func GetDERPConfig() DERPConfig { +func derpConfig() DERPConfig { serverEnabled := viper.GetBool("derp.server.enabled") serverRegionID := viper.GetInt("derp.server.region_id") serverRegionCode := viper.GetString("derp.server.region_code") @@ -445,7 +451,7 @@ func GetDERPConfig() DERPConfig { } } -func GetLogTailConfig() LogTailConfig { +func logtailConfig() LogTailConfig { enabled := viper.GetBool("logtail.enabled") return LogTailConfig{ @@ -453,7 +459,7 @@ func GetLogTailConfig() LogTailConfig { } } -func GetPolicyConfig() PolicyConfig { +func policyConfig() PolicyConfig { policyPath := viper.GetString("policy.path") policyMode := viper.GetString("policy.mode") @@ -463,7 +469,7 @@ func GetPolicyConfig() PolicyConfig { } } -func GetLogConfig() LogConfig { +func logConfig() LogConfig { logLevelStr := viper.GetString("log.level") logLevel, err := zerolog.ParseLevel(logLevelStr) if err != nil { @@ -473,9 +479,9 @@ func GetLogConfig() LogConfig { logFormatOpt := viper.GetString("log.format") var logFormat string switch logFormatOpt { - case "json": + case JSONLogFormat: logFormat = JSONLogFormat - case "text": + case TextLogFormat: logFormat = TextLogFormat case "": logFormat = TextLogFormat @@ -491,7 +497,7 @@ func GetLogConfig() LogConfig { } } -func GetDatabaseConfig() DatabaseConfig { +func databaseConfig() DatabaseConfig { debug := viper.GetBool("database.debug") type_ := viper.GetString("database.type") @@ -543,7 +549,7 @@ func GetDatabaseConfig() DatabaseConfig { } } -func DNS() (DNSConfig, error) { +func dns() (DNSConfig, error) { var dns DNSConfig // TODO: Use this instead of manually getting settings when @@ -575,12 +581,12 @@ func DNS() (DNSConfig, error) { return dns, nil } -// GlobalResolvers returns the global DNS resolvers +// globalResolvers returns the global DNS resolvers // defined in the config file. // If a nameserver is a valid IP, it will be used as a regular resolver. // If a nameserver is a valid URL, it will be used as a DoH resolver. // If a nameserver is neither a valid URL nor a valid IP, it will be ignored. -func (d *DNSConfig) GlobalResolvers() []*dnstype.Resolver { +func (d *DNSConfig) globalResolvers() []*dnstype.Resolver { var resolvers []*dnstype.Resolver for _, nsStr := range d.Nameservers.Global { @@ -613,11 +619,11 @@ func (d *DNSConfig) GlobalResolvers() []*dnstype.Resolver { return resolvers } -// SplitResolvers returns a map of domain to DNS resolvers. +// splitResolvers returns a map of domain to DNS resolvers. // If a nameserver is a valid IP, it will be used as a regular resolver. // If a nameserver is a valid URL, it will be used as a DoH resolver. // If a nameserver is neither a valid URL nor a valid IP, it will be ignored. -func (d *DNSConfig) SplitResolvers() map[string][]*dnstype.Resolver { +func (d *DNSConfig) splitResolvers() map[string][]*dnstype.Resolver { routes := make(map[string][]*dnstype.Resolver) for domain, nameservers := range d.Nameservers.Split { var resolvers []*dnstype.Resolver @@ -653,7 +659,7 @@ func (d *DNSConfig) SplitResolvers() map[string][]*dnstype.Resolver { return routes } -func DNSToTailcfgDNS(dns DNSConfig) *tailcfg.DNSConfig { +func dnsToTailcfgDNS(dns DNSConfig) *tailcfg.DNSConfig { cfg := tailcfg.DNSConfig{} if dns.BaseDomain == "" && dns.MagicDNS { @@ -662,9 +668,9 @@ func DNSToTailcfgDNS(dns DNSConfig) *tailcfg.DNSConfig { cfg.Proxied = dns.MagicDNS cfg.ExtraRecords = dns.ExtraRecords - cfg.Resolvers = dns.GlobalResolvers() + cfg.Resolvers = dns.globalResolvers() - routes := dns.SplitResolvers() + routes := dns.splitResolvers() cfg.Routes = routes if dns.BaseDomain != "" { cfg.Domains = []string{dns.BaseDomain} @@ -674,7 +680,7 @@ func DNSToTailcfgDNS(dns DNSConfig) *tailcfg.DNSConfig { return &cfg } -func PrefixV4() (*netip.Prefix, error) { +func prefixV4() (*netip.Prefix, error) { prefixV4Str := viper.GetString("prefixes.v4") if prefixV4Str == "" { @@ -698,7 +704,7 @@ func PrefixV4() (*netip.Prefix, error) { return &prefixV4, nil } -func PrefixV6() (*netip.Prefix, error) { +func prefixV6() (*netip.Prefix, error) { prefixV6Str := viper.GetString("prefixes.v6") if prefixV6Str == "" { @@ -723,27 +729,37 @@ func PrefixV6() (*netip.Prefix, error) { return &prefixV6, nil } -func GetHeadscaleConfig() (*Config, error) { - if IsCLIConfigured() { - return &Config{ - CLI: CLIConfig{ - Address: viper.GetString("cli.address"), - APIKey: viper.GetString("cli.api_key"), - Timeout: viper.GetDuration("cli.timeout"), - Insecure: viper.GetBool("cli.insecure"), - }, - }, nil +// LoadCLIConfig returns the needed configuration for the CLI client +// of Headscale to connect to a Headscale server. +func LoadCLIConfig() (*Config, error) { + return &Config{ + DisableUpdateCheck: viper.GetBool("disable_check_updates"), + UnixSocket: viper.GetString("unix_socket"), + CLI: CLIConfig{ + Address: viper.GetString("cli.address"), + APIKey: viper.GetString("cli.api_key"), + Timeout: viper.GetDuration("cli.timeout"), + Insecure: viper.GetBool("cli.insecure"), + }, + }, nil +} + +// LoadServerConfig returns the full Headscale configuration to +// host a Headscale server. This is called as part of `headscale serve`. +func LoadServerConfig() (*Config, error) { + if err := validateServerConfig(); err != nil { + return nil, err } - logConfig := GetLogConfig() + logConfig := logConfig() zerolog.SetGlobalLevel(logConfig.Level) - prefix4, err := PrefixV4() + prefix4, err := prefixV4() if err != nil { return nil, err } - prefix6, err := PrefixV6() + prefix6, err := prefixV6() if err != nil { return nil, err } @@ -763,13 +779,13 @@ func GetHeadscaleConfig() (*Config, error) { return nil, fmt.Errorf("config error, prefixes.allocation is set to %s, which is not a valid strategy, allowed options: %s, %s", allocStr, IPAllocationStrategySequential, IPAllocationStrategyRandom) } - dnsConfig, err := DNS() + dnsConfig, err := dns() if err != nil { return nil, err } - derpConfig := GetDERPConfig() - logTailConfig := GetLogTailConfig() + derpConfig := derpConfig() + logTailConfig := logtailConfig() randomizeClientPort := viper.GetBool("randomize_client_port") oidcClientSecret := viper.GetString("oidc.client_secret") @@ -806,7 +822,7 @@ func GetHeadscaleConfig() (*Config, error) { MetricsAddr: viper.GetString("metrics_listen_addr"), GRPCAddr: viper.GetString("grpc_listen_addr"), GRPCAllowInsecure: viper.GetBool("grpc_allow_insecure"), - DisableUpdateCheck: viper.GetBool("disable_check_updates"), + DisableUpdateCheck: false, PrefixV4: prefix4, PrefixV6: prefix6, @@ -823,11 +839,11 @@ func GetHeadscaleConfig() (*Config, error) { "ephemeral_node_inactivity_timeout", ), - Database: GetDatabaseConfig(), + Database: databaseConfig(), - TLS: GetTLSConfig(), + TLS: tlsConfig(), - DNSConfig: DNSToTailcfgDNS(dnsConfig), + DNSConfig: dnsToTailcfgDNS(dnsConfig), DNSUserNameInMagicDNS: dnsConfig.UserNameInMagicDNS, ACMEEmail: viper.GetString("acme_email"), @@ -870,7 +886,7 @@ func GetHeadscaleConfig() (*Config, error) { LogTail: logTailConfig, RandomizeClientPort: randomizeClientPort, - Policy: GetPolicyConfig(), + Policy: policyConfig(), CLI: CLIConfig{ Address: viper.GetString("cli.address"), @@ -890,10 +906,6 @@ func GetHeadscaleConfig() (*Config, error) { }, nil } -func IsCLIConfigured() bool { - return viper.GetString("cli.address") != "" && viper.GetString("cli.api_key") != "" -} - type deprecator struct { warns set.Set[string] fatals set.Set[string] diff --git a/hscontrol/types/config_test.go b/hscontrol/types/config_test.go index 2b36e45c..e6e8d6c2 100644 --- a/hscontrol/types/config_test.go +++ b/hscontrol/types/config_test.go @@ -1,6 +1,8 @@ package types import ( + "os" + "path/filepath" "testing" "github.com/google/go-cmp/cmp" @@ -22,7 +24,7 @@ func TestReadConfig(t *testing.T) { name: "unmarshal-dns-full-config", configPath: "testdata/dns_full.yaml", setup: func(t *testing.T) (any, error) { - dns, err := DNS() + dns, err := dns() if err != nil { return nil, err } @@ -48,12 +50,12 @@ func TestReadConfig(t *testing.T) { name: "dns-to-tailcfg.DNSConfig", configPath: "testdata/dns_full.yaml", setup: func(t *testing.T) (any, error) { - dns, err := DNS() + dns, err := dns() if err != nil { return nil, err } - return DNSToTailcfgDNS(dns), nil + return dnsToTailcfgDNS(dns), nil }, want: &tailcfg.DNSConfig{ Proxied: true, @@ -79,7 +81,7 @@ func TestReadConfig(t *testing.T) { name: "unmarshal-dns-full-no-magic", configPath: "testdata/dns_full_no_magic.yaml", setup: func(t *testing.T) (any, error) { - dns, err := DNS() + dns, err := dns() if err != nil { return nil, err } @@ -105,12 +107,12 @@ func TestReadConfig(t *testing.T) { name: "dns-to-tailcfg.DNSConfig", configPath: "testdata/dns_full_no_magic.yaml", setup: func(t *testing.T) (any, error) { - dns, err := DNS() + dns, err := dns() if err != nil { return nil, err } - return DNSToTailcfgDNS(dns), nil + return dnsToTailcfgDNS(dns), nil }, want: &tailcfg.DNSConfig{ Proxied: false, @@ -136,7 +138,7 @@ func TestReadConfig(t *testing.T) { name: "base-domain-in-server-url-err", configPath: "testdata/base-domain-in-server-url.yaml", setup: func(t *testing.T) (any, error) { - return GetHeadscaleConfig() + return LoadServerConfig() }, want: nil, wantErr: "server_url cannot contain the base_domain, this will cause the headscale server and embedded DERP to become unreachable from the Tailscale node.", @@ -145,7 +147,7 @@ func TestReadConfig(t *testing.T) { name: "base-domain-not-in-server-url", configPath: "testdata/base-domain-not-in-server-url.yaml", setup: func(t *testing.T) (any, error) { - cfg, err := GetHeadscaleConfig() + cfg, err := LoadServerConfig() if err != nil { return nil, err } @@ -165,7 +167,7 @@ func TestReadConfig(t *testing.T) { name: "policy-path-is-loaded", configPath: "testdata/policy-path-is-loaded.yaml", setup: func(t *testing.T) (any, error) { - cfg, err := GetHeadscaleConfig() + cfg, err := LoadServerConfig() if err != nil { return nil, err } @@ -245,7 +247,7 @@ func TestReadConfigFromEnv(t *testing.T) { setup: func(t *testing.T) (any, error) { t.Logf("all settings: %#v", viper.AllSettings()) - dns, err := DNS() + dns, err := dns() if err != nil { return nil, err } @@ -289,3 +291,49 @@ func TestReadConfigFromEnv(t *testing.T) { }) } } + +func TestTLSConfigValidation(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "headscale") + if err != nil { + t.Fatal(err) + } + // defer os.RemoveAll(tmpDir) + configYaml := []byte(`--- +tls_letsencrypt_hostname: example.com +tls_letsencrypt_challenge_type: "" +tls_cert_path: abc.pem +noise: + private_key_path: noise_private.key`) + + // Populate a custom config file + configFilePath := filepath.Join(tmpDir, "config.yaml") + err = os.WriteFile(configFilePath, configYaml, 0o600) + if err != nil { + t.Fatalf("Couldn't write file %s", configFilePath) + } + + // Check configuration validation errors (1) + err = LoadConfig(tmpDir, false) + assert.NoError(t, err) + + err = validateServerConfig() + assert.Error(t, err) + assert.Contains(t, err.Error(), "Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both") + assert.Contains(t, err.Error(), "Fatal config error: the only supported values for tls_letsencrypt_challenge_type are") + assert.Contains(t, err.Error(), "Fatal config error: server_url must start with https:// or http://") + + // Check configuration validation errors (2) + configYaml = []byte(`--- +noise: + private_key_path: noise_private.key +server_url: http://127.0.0.1:8080 +tls_letsencrypt_hostname: example.com +tls_letsencrypt_challenge_type: TLS-ALPN-01 +`) + err = os.WriteFile(configFilePath, configYaml, 0o600) + if err != nil { + t.Fatalf("Couldn't write file %s", configFilePath) + } + err = LoadConfig(tmpDir, false) + assert.NoError(t, err) +} diff --git a/integration/cli_test.go b/integration/cli_test.go index 9e7d179f..fd7a8c1b 100644 --- a/integration/cli_test.go +++ b/integration/cli_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "sort" + "strings" "testing" "time" @@ -735,13 +736,7 @@ func TestNodeTagCommand(t *testing.T) { assert.Equal(t, []string{"tag:test"}, node.GetForcedTags()) - // try to set a wrong tag and retrieve the error - type errOutput struct { - Error string `json:"error"` - } - var errorOutput errOutput - err = executeAndUnmarshal( - headscale, + _, err = headscale.Execute( []string{ "headscale", "nodes", @@ -750,10 +745,8 @@ func TestNodeTagCommand(t *testing.T) { "-t", "wrong-tag", "--output", "json", }, - &errorOutput, ) - assert.Nil(t, err) - assert.Contains(t, errorOutput.Error, "tag must start with the string 'tag:'") + assert.ErrorContains(t, err, "tag must start with the string 'tag:'") // Test list all nodes after added seconds resultMachines := make([]*v1.Node, len(machineKeys)) @@ -1398,18 +1391,17 @@ func TestNodeRenameCommand(t *testing.T) { assert.Contains(t, listAllAfterRename[4].GetGivenName(), "node-5") // Test failure for too long names - result, err := headscale.Execute( + _, err = headscale.Execute( []string{ "headscale", "nodes", "rename", "--identifier", fmt.Sprintf("%d", listAll[4].GetId()), - "testmaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaachine12345678901234567890", + strings.Repeat("t", 64), }, ) - assert.Nil(t, err) - assert.Contains(t, result, "not be over 63 chars") + assert.ErrorContains(t, err, "not be over 63 chars") var listAllAfterRenameAttempt []v1.Node err = executeAndUnmarshal( @@ -1536,7 +1528,7 @@ func TestNodeMoveCommand(t *testing.T) { assert.Equal(t, allNodes[0].GetUser(), node.GetUser()) assert.Equal(t, allNodes[0].GetUser().GetName(), "new-user") - moveToNonExistingNSResult, err := headscale.Execute( + _, err = headscale.Execute( []string{ "headscale", "nodes", @@ -1549,11 +1541,9 @@ func TestNodeMoveCommand(t *testing.T) { "json", }, ) - assert.Nil(t, err) - - assert.Contains( + assert.ErrorContains( t, - moveToNonExistingNSResult, + err, "user not found", ) assert.Equal(t, node.GetUser().GetName(), "new-user") From 5597edac1ec70c1a623d6dd9c709b8ca97fb71a5 Mon Sep 17 00:00:00 2001 From: nblock Date: Mon, 9 Sep 2024 08:57:50 +0200 Subject: [PATCH 135/145] Remove version and update setup instructions for Android (#2112) --- docs/android-client.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/docs/android-client.md b/docs/android-client.md index 21dd8d21..044b9fcf 100644 --- a/docs/android-client.md +++ b/docs/android-client.md @@ -8,12 +8,9 @@ This documentation has the goal of showing how a user can use the official Andro Install the official Tailscale Android client from the [Google Play Store](https://play.google.com/store/apps/details?id=com.tailscale.ipn) or [F-Droid](https://f-droid.org/packages/com.tailscale.ipn/). -Ensure that the installed version is at least 1.30.0, as that is the first release to support custom URLs. - ## Configuring the headscale URL -After opening the app: - -- Open setting and go into account settings -- In the kebab menu icon (three dots) on the top bar on the right select “Use an alternate server” -- Enter your server URL and follow the instructions +- Open the app and select the settings menu in the upper-right corner +- Tap on `Accounts` +- In the kebab menu icon (three dots) in the upper-right corner select `Use an alternate server` +- Enter your server URL (e.g `https://headscale.example.com`) and follow the instructions From bac7ea67f4314870d1d8459c624c6ea10e352448 Mon Sep 17 00:00:00 2001 From: nblock Date: Mon, 9 Sep 2024 13:18:16 +0200 Subject: [PATCH 136/145] Simplify windows setup instructions (#2114) * Simplify /windows to the bare minimum. Also remove the /windows/tailscale.reg endpoint as its generated file is no longer valid for current Tailscale versions. * Update and simplify the windows documentation accordingly. * Add a "Unattended mode" section to the troubleshooting section explaining how to enable "Unattended mode" in the via the Tailscale tray icon. * Add infobox about /windows to the docs Tested on Windows 10, 22H2 with Tailscale 1.72.0 Replaces: #1995 See: #2096 --- docs/images/windows-registry.png | Bin 103356 -> 0 bytes docs/windows-client.md | 61 ++++++++++++------------- hscontrol/app.go | 2 - hscontrol/platform_config.go | 52 ---------------------- hscontrol/templates/windows.html | 74 +++++-------------------------- mkdocs.yml | 2 +- 6 files changed, 42 insertions(+), 149 deletions(-) delete mode 100644 docs/images/windows-registry.png diff --git a/docs/images/windows-registry.png b/docs/images/windows-registry.png deleted file mode 100644 index 1324ca6c4d8e8e486d46569aabce5e2acd870ebc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 103356 zcmY(qb8u$C);&BEKCv+|p4hf+Op+%yCz{x{ZB1<3wlOg$wyke|RqwrXzuHx&PychO z&+hKkYp=aKOi^9}0Tu`L%a<<*Qj(&|U%r5o{e3?MfcoV2_3K$D zOpCFq8~t0Q#ifqwVP-mauBu@U7^p>|#mM{Ces>dbU`}Aw2j#Kajx%sxZ-O~KRN@Gal zlDiB3qQ~at>n540VaLOZR^yDcBu@ziIk_oR;gl?37LFv3lJW`)Me2rJJ)C4*XmzB+>g$YHE z`W8r{8$&4Nl?4@l5ik|R9Jp^pyB|!=>k{+`v+sn$ggjT?ITMQT#uv616k4lVja-b>`sbiy# zI<6Ig;9iMR17LrxG?0=4wEJvzGaeo-FA(_K}L#V>L%xp85OD-`UBR%64zNL$XtaO&Bh()(jT!m26W_4xt) zTmd^mO$EOsnI$2)Q41sE3ouw|OzeD~cD2(0eEle_yyU2-`Mbv5gjbh>}F0$dsy!y^Wi>8}Lhu1mc)fJ#ztFHEJ5G)~JisBP}9>e4Se#;Yl1wGCN?_ z(B&G=##_Xi0v*^t>sA6l2<`uL|6!k3HD*6E&0ML%j+gAjhOR8ZB!lOwz72pIDu#?x zN>io|{M88G>$L-CCI-vot_ZE>B`Vunx1jACiJ=PAu)HiJg&Fyb0m9W83-S_s*G?h+1%P8c$!5C;vrH~`#fPpvm{pVu8vch9pi{r=e& zCq%(*EhX%srBojmXz6}nkL2_i!Pax06D17XTZ#wgHp;YMqB*j55tdjOzTaTW_ywHM zH6=S?bm1q!hFJP1`{0^xteKKDibWu1tCCa!*yZr#P^;crpmWBH(ht*&LjhtQA#4c* zYq^-Avdts(H`&!SFi84FmPfA(vUBGM!T(y%cPKv~mS3K$JcuIw+mlPz=>~6)FYkjW z(Nz}$NxsgYl%~=Ua=k8nBQ`u#STqq9+zGnG))gYG8nFQlHVV2rNc4bLDjYVk0H)mT zL=7yJWv1C}_u%WqpK8jcB zvr8(O4rNWk!i4BsIAUOES%?=i!#ar)9XvSO%^dH-PR!Qi|LYyWh@NUBunE|_MIhx6 zb|TngJOl`U8kbaz%o#yr;%bY}moSmqgV*sW3^1Pb7}r!SDgTPv)nN%W5q6^y>$Y%a zXc8JpqZ2+QbZ;1K*a?S@9~5pSw#c$2+zX6z$S-OUyX4cO8NS@K4Z%c(^e)W43R-SR zPT0!$;Hx4;A)aBhA$COq8{G|Iz-oh_ydpTW>cW*ImxtRNhID(_U9)Mv++|sf`scu6 zelUIFHyqo7n9bJ#h(9AV`n>o8rN0W+qNGM1*oQh-oz3($lfofM#j4pv>F6Lks>Q1h zgpq4nU*Me`_K-=`z$Yo$2!`v{1^43O!Cu)4&~I_#WMDaw!HzoujxrpWg?GxR8GqS6BCijuy@Q5MicA` z-e*~hY<$;KQjos{>l4F?B@6u@rPGl8n?Ck>s;Y%H<_KJ49@UKxZ_>Eh457L(#ARix zHnu}3-AdHTc`6o;aeBwA16`O60k{QN3*R6RuF(dAh!ubKx~c9J6a+kp@hb+SLBJr> z*VJGeM+!?PH#iz_#?dGfu)&yX7AO{uJU>ZGuhYXaYZ!FSOJ~r^Xv<5&z?2@t)%Tcs z+=(cfoKEbm?(suWE$A;_I(g*x$K0C50-3E*Yt_Vg4jUHfMLG2)HGU^|GJbK_QFY7a zNKDK?BHrdfmx5Id!0L&#Hkag z5>k5-qQsM3<~pU(F z=Vv%|XMKbuoj?qf@xQ@4i)CptL;g3Wts|1J8RNiTj{L)zUK80q!r{KeifVd zugWI$pc#guC#g)hrjOoy$}`4=2JbTt#HLj=Df$EBSBY;Vr&^?W;V}{Rf4)wO2q*N> zkO^Ir&nGL;ao~AFvTxI`Sl|Q&c#QPre6awev697!dx*Wfq8Tw z_lLfVCRwP7KlAV+UrRo+b zn6G>c5;yW%J#6Z*ORKDth3sW;TZMcQV|VA9R>)eIh$@C};MFC$W!%pOB#;?KZPn=% zY4sDrDz5v&0kTsj)=bd&-QIuJS#wx|FPs0YINqptMz1fj1I`BDcisE0G}-;l>LPy%W(sk&^_3uFpHdx>Q!Bv+AvkB0c|@OviFezZt^I zp-+UjAe{xwbL=x(IIk6FiE%0z$O|F#e#(^~@V=Wxz-6~u?r^)kyL0T`B7}#6gDP}; zI@52{cQeg}fYz3QMv+3bcD!r)yx(bWmoXLYqP<((Xr995a(rKhkIM31BK+uu6zF;w zX#U`L!m9S14a4$0X4zWxx^WsN!0enKiSi#y#)1q=wE$>Lxo1(4EBun1i4s0TSIlmD zR;CIJN&gaa*M{5}-|GhK_(ZMV4)Ib&52Tk&I2N2;)HS10LPY506A_B)Z+d)hqi*9P z9)Y4kr)2b);Ru}u_okh)HUo5va;$yV`Y)aGW~WyjZ2tZoe-bzPG|CWWgApQ_=ZWa!43dK>iaxvjbtf zH3s_&M8%LZ8n2%*UTcX=Zb4!RjG{7~_D+aHQqwHGjA?cj5p0$c5j3^*sfhCc&E{1H zsNVVs5$)LLTzY>zsYo%@+0#u zP|v@rD|>ytX#bq9dVR*{*r+?yrLpPWhtl#xmq{vYBE_Wnu?oNJs{wj}2F^>Z4K!@5 z{M&OX5`x450N9Aw(5l*Q<(ftNR-cN>tn6$=;)evV0SeyISaT!P_O_F;@@SrmmeZIyLqloudRUR`B#61SUu%1!y7Gi{lH$A0ZKbv>9u&2=Tjvj z8XA%yn(*y*fBU?6^VEA&$hmJQELa*N!8%W*z z=prgIC_><5tj*Yte5uNRUO{h;A|(v(n~$G&kDsS)liqmi9k(^of=4I zC`5cI_`DK~d;0FDwz9I2-v{#w0vNp03Bmb;L>xl<-*I2>BF)Zy9^YLE9k05$7pC10 zlmwnf&!yn@Dp=UB|3GlyR+jU*g_CK;7UDH9L0WSBPZW@a>>CN*|21cGepsB{HSdK= zLl{PvvPnz9$uxiHRvrae#?a5O;TGniC(|^`Wz3xLEjNVkvaZoGn)77D(%T(jB<$DU zW)ejL-)&`;Erj5KmylN*u4Bzv!IX-4F5a|rU7`$moL5d6Q~f~A$tjb8u$B_3+mx+S z!w+dxO^DJWER&TfmPth`MeQ~%RTF5{(3mP-_3iy)P3^M%k-(PI*;b^nSoUXyg0zOm z7?mQ&cc<>}cVM%`G%*@oYZn|F?)_u^hylmc=+vlOYGie}*@>Y?QesRe$fHEQN1;~8 zKu3adN){AJC12dslHh*O#z^`oZq9zS!k~S%`Viux_dwHNfP*wH_kH&&A)pVN( z$i*?(vWNBbIPD|3E5#I6@9&1RE>x)spEb)=W3cCpaLcu$kXZ#pH@TzYxan_~oR^FJ z`aj!V!tg8LqAJ{G9p&PhpPx@iNKjDm!ObT-93n{LCj%qa=%Nkp)3k&in=KIawX|Bi z7j@DX;?X>H9B!*Z;@XRiVCZuFU8b6%Z4ZM|so8k)CxU^0FIMEFO4y*)YCStZ$N?-$lG;)pP3hz6jZpV)L#FGqlW_KKd-l}(;i_?o&vBnE zQ33A`ns8&+MZoolhyNBU=luqmM(va~en(jX(G1m1rhhUEdm|Vj{iq$J&5%qda1#5D zIXe-&cCleJJJn{iFtETEV-r``^4XN(lFck^a_Rl~n)4Bvw506~T}fE6zWWXIahFa! zOI4ghZk$YW`#ha}|Ms5kw$}OBc^5R?efQfo*H6It6WsgI#pmxNB68o<*k=3CM^fYI zc-{YoCV1;G_=Rc+5*Kd$cp)_Cj&Xw{$g7hwb3?ScIN1ORHlQNQe@{? zmc!`in}GN5unqAy(x1WntVN_Tyf=Sm$D6;KzU%4F-=4i^Xv(r%UC}k9XT&S*5R8f%?iR8+JhmU5%^#a`OPi9K@J&|QU5D9^)-9i-?us$*)<0sJ01f|V6B7DoaE9eqqHKB>dacri_v_{ht1dJo;REITCNGCgTaTP*<>CrR3?9%!`7{`4)4m>Zv#Npk88T+Qd`I^9=td|ZVH+$Evy5$ZNy zbU}ODO_X8%$k%lpxV3pZF67wqoM%DgeJGfpik!`IJCS+ZuwD}WapNuHb2K<`p|EwB z2iJYi;qwj>nYzh4mX3$*|N6(3yd-SO!q(Li=zbkXv$Szzi^O}^^U~IK`~j_Zy~={F zgt$_cBc(JNCPUxqq-b+y>t-y4(q!f7`mFofeKToGKid5)SM4)3|40iZVy|>!C^=M` zozg}y;b<}5-~3*Yz`$jCU#f(upjW#^*zw$mv|-O@ycBp#yqd{jwSyZ_q|OP)%wVlTFgh)m8z-z_x^ztVZ_f+ghWA1nA%DN=C&B zPS5)()w-ST2;FMcx?X=f#qVeGka8DNC!^Y45zTSW7~eGreUhoxyb?nR>xAPDb_XGf zy`J0XrKESmq|5Uc@W3&dZu-Ku39lCGM0J&vMo3PfyA7vDW8NO5GBY#pdgYO3L@5}AzbNEW{?#E1)OapB`tK$tF51uQSaY7{Ql<+_ zOWUe)n);QA&`&7)WHsMPXg|+lKAsXj?-ca-4l~X0J!{6k?-1G71tz9Z)l@h~i4i%d z%07m0vw+K_U!4V1b~;@hk)$~lu|K?~!H%cM#Xe?L)O)|XTQFAb)Fg}c@lc2VP%q=% zs`6M)xVu|5{YMI$0(!tSEZ>y++dXoL57AOZF^noKm#s0yzMeuOlG^)aPt;XZWt7Ih zb7mwVG_&|qm~zCANJOydIZU#ZgQ~l(`rwfEmx8c#TiowYEUQ|f;Qg^&yus|$XFTZf z)N7Cu%VDVO2|x*+NH#({>$lx@o6X&i=eLV<$vZ__wj1`3I*%Lfxci+iFBwOx8D=(J zd^XurVd%xv^{cx?FWZJ!&67@S$jk1hROQ_tr7=1l<85Rfe9eSq3Lm>6R8PV#%nrp*bWd47Wn=Y6}6fBs$nm&kXQP$(_p<#=T17@ z^fTzPoWc9p(&gGsJ-P3Fmc!xnRyv!<_KUl@c?GZgv}%vRG}}z|hW=AMpVc(8u7k*> zF8fQqNB=Af)g7=v5~EN=Pq^ke(;pf?F@`_>yndW)*t(9du_L~VrRcm}D|qG2Xu>)T zmD*AQz20f`ICmRur|#--ZQh=zXq*X$Bs;@?6-NG6U8s(6F23|Wx<$O)+fWT5Ft2g+{oRJF@(b7(C<4Nd#U*ZZ zyVUclDG#sjerj#bbURR~J)N%Vbc^wFsW+9}45}cN#1N&(@jQzwkLJCd{ERs^)#Lj- z6L{Ng#%NtRJ@&|U*>o%Vt{6KwFklQ_u0N&b$}go*VcqnP2?_Z6D*uqJQI2G&qEwqX!Oj53CXB`3%w{27{u44DgXhgJTTIr%G9EFsXwQbO&;5*I;mE`bT^`P&| z8cc#C@m`&jSMwkJ3J5J(J6EF;eF$_WVjJ>CwqPO2@p6Q#p_T9cff^4MM;^Z!z8wfF zLc{kvp|yghOOp8H5KJlMmlWrt=)g_k(3Zi#pvG4<@xBoaTyP_%z%{BJa5`OYzCgGdn_W;wwZ?U zzW1%jC>M0UBhOC=f;OEMVA3TC-(G#5wk7yaTKD;~JC}5{7L^82mvnn5lQ8KzpOSg9 z;@I*Bz%91^hqcS0NWQ3rUl#&a(NY;=R-C5h7>w9X1>hnQJCNSZwIZNlh!Odpwy$%q zM-^Ocd&$MqZl1o?nmgYLEAv)Q!jq4Dy6=%|CoQoggv)l;f`+X)Gl}AV%VJVt?a^c$ zb0e!RYSeWsBg$y?N$hmaD4C=1O*9lZg2_sd!63-Slp;j_2~f!+SSV=Ac^aT_SeSF;h|Ww0wjTaH`a7-v`aM8Z zB5d;=V>WUr*`}7sa22sD$3d70sSF<2CoeaoEtSlX4@1915-O)a8Y>*|#|kgY|EdxH zFU#@;45z_Bv?VkaKW2f>yZ6_Guhl^4E%JPQ0L|39rrzP~{FIQc-33P`g8Wlfqvma8 zwQE^P309C?vZR59FlV%LP*|p2P^04rJG_Ks#a{m`2h)RvNm)n|?D$7oagO(g+m@4P z*?jByhlD$t6($n6%3ci|^9Z!%OB(NwB!vz@|_T$j(R7)UP%O9f^Q@>p3p z%Gl>8i9u_k;`K=Cb(94;&t~2nqsCSdGO_1L!VvV9sy@k+H^Lzto^#SKU(!3&;B$}v z(XIs}z8oc54d8r(SX|h_aHZ78$i@38_G>7Zdl3`Qy3hS&k}Elhpgk|hlTsn_SMsxP z&GR=`5-CvOXO@;LW*Pj!+={9sr0IoLYlS+fC|Giz(tRg3<`D0Ok0O{UVpKzGeA8m+ z1!oK=7W@XB_ZS+C)q8z_1V({WZjLG{8`Ijc*(FHaR!)NS!0wcx=zC>PK=UF`h6Oy^ zu**9EEm0q%qOAw_P6p%WhyNsOOusE@2-e~5^v23GaJCoBY7UsZd@?tXOH^UuM&w)AOS49}j<#llUNPl>Dp6{Se+9jfZG`OTb?nG=R3Fi6x ze5Xz~hOnMTfA8cIrrZ*-3X(u8x^>1H!QfvMN(Tw#A+@6o`lKlaO+tgS%?0oOWQG|M z#wl$ubh`rET5SmS-Oi9@QO$AW%4T=w@BV08A_XvVT|@SMaeorSY_=V-v{y2eRjC!<3;_%@56=)W%wuDYy@@3_(!fNO$5;+5(~1<7x6Ish zcX{+Q5@+;6B)96=p?ZBnb`e%)Ot7l&HF+wS`M)TjUgdk92JJ4IL6xlRU zjTyf_QiT+{K}52t6Q0EVlpUSv8cUt(5$g50q22@grk$EFD=SKnEOCQbcQa3OGuxf| zpIU%4p_&!yk(^&Df3P|b3GicEWjUy0+8k`^o z7+I(IoBxPn?GPkjO9rwvFkJ~5S6)ks4b+K^NbWMrZwizn8m>qWMQx>aZSO&WX-Ci# zzeksIf}@P#r*m41MD{?MF%bF<#h531#vE(flkdW+isOu0%hiGvZ`nz#db9Q^o( z^6?=|_N)ljyFjG8?%J_l`cV0gR&c=h1=AFUWfUJTyt10~7v&s+VuP^F1R{e+3Jel+ zAa=`9tS$CJ?ZgP?F}q;T*$N;iSd4JlmKNCSSpCDi7I2~+1c1|>sUsOS#MgK)%0Gz1 zNVfI#DKkac+KOeXwDM19?Bg4hMN$_t%F~=9r}{tcVq%IKyx6tnbo#zqp8CgQH zly?iQ__rkS(xx_i&34?qMmEb4ofjNyCA{xmN%ICeg(uZ4-(`LUm8O5W%Bw(qo$(I9 z&?mHx#vv5I@>ZU7__kr`_z`M1)wH0^Z&RtnYPX}#Wx3F6>dT3tz<^LsY?FU#RxRwK zPKdwj&o0r4?^N+Wh;@Y<)w8aF_402@Mc`z*tyD52~=K`l%We+_BM^R~9&q0z>^n0TQ3&SEC@H z7y{FGPpiBJ_zJWUi+-a&h((Mff#QH<7__EYg=v&5=zC+Gs2mm4N8p?3&feGicq&eg z8G!xUzCjV6X|qGFRr5a-$vy$F((3mzGU;$Q@Sq{`Pt4OmR2KMc3+8)A6%;Eeqi2MZ zTNf#*VK#K1$Ars|o%Pt+*oK7Q24 z)@-741tgNuBwA;-t77Ttg>R+>V5h#tulbf0DP+k5dllc_kEqD^BU2EEy zOeB~R`KK{-wWHL0?y{?m!I-T%6k(b`<+LudAru}1c&?JVUT6rS;crC<<48Xwp&P%8 zq#nCtR%g(CiHNZPjCQ1qor@rJG8zhw7;cLWj!*w(4^nS)ED*YSfe5~sk2pcEw*+hj z)WYTARfxSVX<*0}t(eI8!;w`bnvvXnuvPh&HfkarThhxc?_$9@6Nm6L=Qp_GC|a{w zzuE8pKd=jjuE3uXAWTI0s9MlH&Sd<_wBkQvS=bHN-tKVq8V)jonCht);r)2Fx*Gqh z8oezN_0-=uOYo%EC#awqbj21FwSx4Avi5l}jZG545`iVd9TE=6yhwNejcYl2kONYQ zoRm2ZT3zs;E936p&kpTP_s%C8;a{d6;NVg504@qvr3RY3Rf)~fazh3SyNL^I9&r3F zq5>m!oJlc%1Pw)7Ae|v6n@USN8{i@c{7PAqVs?4C&nz<4a7n^mWoSu8hUZ_7?oE{dIN-?O!?RVshjil%;?5n<(vWJq=4CtEtN0J}{w$IJh9Ch8f) zl72_D`uLA-{;inUrE>NDRrc-aEk83LaUgqTl~0n;!hJZfOag{4hO97BDJm?gU|NbE zjPKeiZ%mY8REam@){tB31m>7NKOihW((G1AZRLqw%eEv~Z;;=B>2v_XarKyZrc_dr z2={v+6aYDBRwqO4xk(fv?jm{=jT2d%;hoi|bHEXC#-j@5z|Anl(8PJMU9U+?A7_iu1lmpgHlWH* zC>{SK41^!Dm(DiWsHd!d*v5fZFu?8Z&lR4n_w2{(UcrXz)re`g`*J;(acf-XwP#K5-KYJ#!`V5y-Gtqs^a)RvS$kX#v3T;;5o01o1l1j}}22{=C${^&QY>c7%`r$enI zENOxJquz%c`kqedNVI$S5M-+$y}kJ~>hGg!GMwPDXgsTd=p*FcWV<8l-lvlu7frWG zmJBiqW6axtB9Kkq)b|82L4@sEPeIEk7w6IcmfV&s z1S3m~uHhLo_v$=4?vyt=F^O+rTB`vyn2fB^3zQRTYdlETW8*6{{ue($_}JCM>&2Bc zQfH#laQY;Nh-7d^ent80z9()~*)H7$w!%Q>6Dg2ttgvgZu5ntYbq;MG(2tO&)uWsk zyl<>Kq0;~EykYU4f6AG-phA?Fp&8nxCd-g+6p^3EP5a+2{}$%?Dfo{N5^GjG{R{OJlpBXOu%PRDMSF* zPv$4HAFB&OdI^iUlC<)E$7pz2og4qdA>lXY$n3EDg6(x`_kz?+OpCNr3&geTkr~sJP1Z`dda|$i=-p8d9X#wJk$m^&hs!?qVqHqN|^q;RcBE@47+Vpj1l)1X*=Ve z@K~0N4t=t>4b?z=F|vYCvB60z>^JLQ^bEEmFqsJ}lkc?_AkI$jwuSbW`dOVL3!vroscb(Ys9rB{ibEU$O-#F#O z?hwy-gI&M4Ma|_~XX8)MFER+)u#*<|0xC~nwE!W}fwfQpe&`}7fnoSOff7lQtruVw ziy9}+7-{j2CXN!Qt!?O{s<35HVuhp}K-djL;qU^Cs?rk4Z)V)2OrbWKD!H*}EPg*K zz!`}I=*U(C*#rP$ds)A6XjBhcudCz>%+Rr%M~5Ph8Mr4uG^4dK46}etnR*(U9R*dV z)WWh8%(@4tG63Iv%gR@lq-wBI!7h6OXM?QBJTfJ0RF&sMAp3`S1Ma_UK{p+M^;l#g z;2<`)GK7%tk4x{PJ6|G%eKOj8$n=WsXO6ZD!Z$Qh=>^K=cH4HhkgSyQDbZr}o00rX zcK+%{phe*}f%$%T} z$yWPa%OUS!SI8yz!I3IOj=gCfXbLjIRU=PbE^n?QBSEWatqwe=^-v5;j~mT=QneYc zp*K$SA2{|1AT5qS|-WtnUc6t$mya|u87)9?&}eC;ObG9&&Q!SW|q}W z1E@ROnDe;+?c-i%3lJB{o}%{Cuofb8{;&Qp= zwzif+yXN)&9)s8N$)8f$YFzF0*!q~Qhv&VFV(Y!D)p+&zcVHgpH3>>9@nY>(%Pkh_ zttmM-W4x!>XCQkBW@|69EWUup%+Y)#*rW`7rnL$R*z)Gn3 zYF>l;=fAm0znzIff!2eB&#Ra)_lYsNeA8ipj3?AoP%M1~_sg?PI}d|a!+2W=!JJyx z-4mL?-E2&mRp#mh|49b96c%q^%_fTWJXjECb?;BT3&l~ z9@js^8$sC)ivr4>*M-$?^Bi6EL#r<#TU>{?pKl5vJ+H=um7Z4QlclT=YN44<<))0h zU$G1^+ds#Apf_$7RYiP@8l!fu%+8y6*DUN@@`@>(*W4a)FFNM28A9dVp3A2NK6#Sx zb+0yTy)MpdClkkF&%@%!EQDTk2;T%A1)gRCICNi+yFX6}^}OCO**ic~{Z>=A<{bY5 zEpJ@pR8>PxJ$}eUPF*ZUq-m&K!S=jx!*kUoe%?-RBcFFmc@HiN3`>e6s?iMI+r`Os zhs&Y+jA-ts8l4p0^HdR}_vdb%trjt&81p73BA!pQv8y~q|Ejl9)eNZaE*+~l+Frr?rVf|QJQ)SWprFy(z;uZyr%yMxHJ9`+f}cxcr$QkN?C77;tmNvvu~ z*UfE*Z+o?`X?9=5LvPUZkNf`aFHU z8O;{rKdhe9nPooo1mL~~jvTNrzi09{Wc&kq1yu-T)ERbh3Qjl6kFUgqRebToJfT*< zMf+1jIx_P6YHw;0#aw3ROnAIJCZ}_n#^(@~D9AJ?Z3~|lj!Y#n4ZnNPI#dMa5h;36 zNItEm1RPb|p0yo!PBk<(&It#a(@2enjae`J>8ym57GZtp=?|5 zI2ZX`B~6zdIcs;}&fevfhRKGv*_NibVA8dDY>?MOWL?IoI=dg|ZIUf*c>lJ3^xEYc z)^plfqVL|hSmT)Ap~*S6q*cb2L=0o*tbq1&LP~hH#8H85hYDX+;uw(%JRk7wE7Ggb z@;R~Gdh2Q1K}7pAv6S^^5UBLf_4xMa^D?kiwd|B4f+Wzib~+b;TM4fwtw=cg4IT<} z^d?LAaMhL3gmC0oXzN=RE`CJ@ag@0&g;v8yS39{g32J`{ceeSv2$JUxoZbxg=bnJi ziOc2-d4_XMe`|`03c3UTOm zBQ1cB-2{>pJ5>U{+YI&vC3=aZiF^rmZ?obD1*wU>BB&OzCS*Qs~XF}oG+TN>SlQH<7Q%dN-Ovl`&8ks-@# z7(7N)3kyA<`KVXLrJg3}Pf*%U%Am%La2$L6Mb`Q!xfpzQX7B`w{Yer9;Ul!I$EgF- z@}mXM9f^e*ekqaT2ZXM zxTc-~qGm1&(%5}Z1=U(EoizjL42?)kP)w09m3U?veol8bU$(wpWXbZof9j;@b~Po) zS>zoO-c&RSd~7y%y#_RQy(@(7O|_qAT690m5yt1g+IHRgUwU1(V0DN3)9RcV%Zd5Y zlINX?D)iZ2n08eR61?I83tq3bItjDBGuJR#R2|)$b$p=Ze7x~J3ZQ~zpS5V`qOfE@ zM_r19`mJRCA|mIG2!0o`-}^$syyu45xok45(-@ZXwjiN2TIwJ*x75_+ZGWs>R@Qr0 zbm{eqN7D9KrKjz42tHiN9gHwJLPm$&j8}Xx?LVlswl@9#1GvO>IKlCT-ORQc#d2uF zK@r(Czt%-cXkGP(@Qi*)FK_td`RQP7PW6z$Q+v&**RFTPVJJH~GMM8Kl$2vq?^Aw@ z)v)CKpOx{1fsDN(97T^W0XP5Ic__znjMtKoPt7PPB`&i^%ylGa;e}hQ&pg$H!c59w z3Em7g+Y;n&9Cy6GFqv02@spcyM%4Rw-}3WX4MUFLazO+l-}Y&S-uZ~)es zov@*abYdl#)_PVq&i{IiL_6Ikb5oj<{V-JGK@%ge^TBiBwF1MCznMoGt@D0fo$d7! zHhgGBQ|7iK2tFSITLPi0i0UU7GLknkVy5DfL|Zq}-U6duCY3iBLf}1N!{h9{EQIfK zA~ntTBFR*vqwspsx->VM9pgTGQ%L4ySy|QgwB~bnFpnvBm+dez=JWh^n0!_fS(leg zt>Sf#hw*Gm(K*5WEz5cA7DVun$)fu?48GKQH0Qv7J$*T=DW%bIz*ep9uoAPw66dsa z9sT+I+WkpUquu}bwRb7l>3TiW)O1J5#yqSe8a;v=y>*HOSydRNu+&O?6BBLdZLbba z;AZEuupyS*FDiT!PVeW5!!Pn7maH0R2^4^^umJv5Z5U~VSUCjLvqJn zHT0)=0W9KPbJUsFERGG|%eaxsJl7B4JISOQp^N21OcIv_;k0RhpolcusHOPGsY%pY zk37NB(IsqV#*~%!wJ(MC{Qogv(lwxet>8+wBm&Ue*2np zpB7j{_JQTQ)3#k6N64Wx);dS7Q=sXW?bj`ukq|%_1q8G5Yn}2;l<1@k_ON}dQLhQ5 z)FM?iuw5!H8u2zUk)N_Sw%sK4z-q%~Cg~`*Ua%tGHHErz|GPvMBqt#8x8v++CYiOp zx_|C1y(mmEMPA#gK6h3)cVoU3?Y>p{mqy57WCU`682Pv(j2yHv;Ij;8L8wgg4lEhA z4@We+L1;=95kheoR2@Gvbv`y)L+Vw zpe4*d_%U1ERsN$K=o{iXmO990W{!Qy+wj*yBE=|CkK7-sC?+wb>G$gO_6C>Z_Oyec zB)(8NdW39(y;^38Y;4*GOb5OP!(LI#hMhXxc;CX$lLnlH7mr#h6b;70`}`TY+J|1p zgN95YR0Y@PD@ot3^RqRL1U{D)S$O7yY0HwR(c`dSd<=2gDBkAaB6jlo@_((LFSN%dH+ zA3WwRJCY=|XsBMH)$N_HnUR$cIqR2-Hx{1J=&bHHuu-o#6X z%u)43J^U?jTclnrIo5a{kSmCe&@-8a1rsoeVR6i_sD>>o)SjyM|qQ)PSeTd9=R zntYWJz(m#m2W%`JE6Fd{ZPKYbAT3O6WkdfWBC*jWk@}a-z(J>k zrg&|tb%VvrxPo&Zkz;q8ktpeP>jwgvcX>=!>D>&6ma{cW%V-p`&s8t1Kaf)kz`_785kJYZA+E(K!!K5#}M}A zYYSyoE}kcbU2FwZlOI=3H=thWA!W%LF{lR#*XluGBJa5Vxb2|9D6&`>Gbn;G`y*D7 z@i`rXdDkyN9Ow`So<^e1z>?3+7vAv$k@g(2*aQYP{CC-QCx*0T@@z$Z#}|`PS*71f zCc;i|xRn5cV|gw)@G0l0+JAcz)rvAXDNK9RsKEh$81>0xSFbf(ZH`L&;mx)-Wavq+~!{>7NMu+C=k`a z*>qiTw-S0Z0wu3((Q!si>Ox%{cp3f9dYi?!n0c#1McYEnxqOi3KwoxDt#Ye-V2XW3 zZlNh-%>B8rGre%3pzD}_q;*Tm?#(%vLZTzERkt?Ku(6T9=YJ=-oK|XubsvU_p40(8 zk{>sX#$qcl{~o+aR}^f~@W$KS-IO4d8Ui?KykwnU@1Dzh=>A{D#20lT6#%Y(e?hk~ zht={Gr(f>^TF{JUl4@b$M14mUT@eoZrYuPyGi*eUaWHQlW-s?x4ZV!cl15R$qjET} z#Di-j*if;i{Q$zPBTL40&TGfU5yqmX5xlE^DD$|LMmkttJP}#YKx`r)8u-B(Gl9!J zky26xcorStmH2KGSSe*HDb5T#CsV;|$vkl^SRzQ(Bl-M5o#3UVfHPuTTPK|UWna`1 zLLNK>e}_lRNv^D?Vmw@>gw%|QNh|!T3|786f#H34GtuYV*W|?GR=hyc=+vlZNdtU9>)KW^S zBG6bDM2vwNiA!($+P32scftNYrrv?Q5@-q63_I%BwrzH7XUFc2ZQIzfZQFJ_wr$(y z^qG6lnfVLrDXprn-YPb#K_VRoE7J-w$Y0sWVK6Or)oR>82$e7EOZFEpSFH@SS&c8U zkr9}VpwB3gQ+we`FkVZQ_vOM$-buly|DlR4#|b?q9Cd=1-Ana@j1;3PQfoj34@*i_ z$xT$cU{2-XJ+GRUTqN$)r)T^0;cO?=sA*~bb7k_5Z(4@;ral=U&RHoFkz32mTxG8TACE|yu2OV8x^xGe@o>VI4Fr#!d4xsq6X_-a( zH{=zZ3uZ659nRBk@MA^*G$?K5y_S3@rfmKnu`n!HQPZ|3Z~<7fV5DvsH3Q$P*;-5p z8#o@ZiBe%Sgvj!^UL!POBEqP|Vy(i6etf}4OGriZEl zv=+yE7uWwAoqAo6?53PMQ2Fa}J+v%lF9V9KjvshOL<>fl640@eT<3sda zHqAcc#pL73&lJU(_u%Rh+Bk+Z*YFtBWW1SW6%M2EMXW_)Y6;J2N~l=|TwSZY0=4od zmEzeh(v@kSJzjfyL#a`?oD90{PP2POQ{1UADpLpT>z7)VhG?HY0xuR04lLu8%~k|P zni7($*9r=9a*IXu6ROA|uo8*Gg)pDiq}K6=}IFQ|rg8GHZTxm|VmQ&GA4B z+!O}wZufl*Y~!U%DdJ97@$~$R$%%sy;jps7L3geY){T7aS6QF_68XgtpjT^UGfkRwA6}q z%~`Djk^|Suk&-wfIy}aUC6bsw%YRNC4J~Xz#;dW+@9Et6fs|H$Fu}&9^}ln?U%N)w zs6NYOt1nrS7a+PX(OPdGv{zf&+2Ed)k41!6GBKzf+7U&D3k!iaa=6u~TsS*eS|0{3 zu(YBG4n|h>^iuLcNFpnCmU<&y5W;n%qzV)<*%2k0lT|A={GeC`pSab>ULjN#l8kQ+ z0QnM?cho=wy$M6(wZCJe;XEZ>6Yf?I+32H)Pp6n3G` ziukt#KevFU2Qj}l(5qC)qI>(YHLMvDJ@aQuX!hK zo1LlI*NV<{1%s@d4$-}L-=;%C#h2p>jhB~D3U=-lbS=BA1OO$|a2V*R4XeDwuPhmOUVDFj4+ zWT_6hqmyZA##zpvejG9}wi|1@cn(h3iPO4l8?=#A(<)ks!lsIyz>-<&u_?|X;b7IW zkvZ4~mcAu)h5L(~qR z3#i%PfyA`~EP6IdrrIzswhbmbYOrKnOfFE!ctsMaz>Of41T&=kDE4m1*f6d!pd+A- zXatL-JKXlqFXu+M?!`Vr2DjaJjV$kAOBe{__Jgb`FK_JC+$DFW1D_eOUZ%>qrX*&MfPmCeT>W zh%I&s&2kWxZ;y)@gs0aZg03u4B8%ckq%Q;-jV79U6!s6dQEMO$#K9&}LfZIw?6Dp% zT?CsraeJcRQI(3gcmPT8AChtvViCeX5m-DE6{s3;1y!;dN#a5Iqmf})Dl9Q2@&Rfu z89`_!2TdO0+8;>fwrwGD?$CM65;@qbr+&t9Im|NMCaTc@ZO(1;`JBz{x8I?C7h&mag#po63%i75VZwdL; zA-X_H9(u6D9Mp?D(Rf5)8r9{_o45*YqH)&ok3@7`GQxm(cWx)KK0bh=D*XKh(Y(A} z{q9TJR|l>?A%c%}=HrZw#0HvrapTDB^R~Mwx@6}=EAl_gz-CJ#ws4*3FSbgh-cQ zZFll8r+9mo;-ooQL^o!0|7b}TF1}`15Gt@!ZP=e(0yfvTR=LI-Q^Lu9=oYBG*!tsO ze_v3^^5>GJO^CFk4yi@@@M!7LG?KPXGQkBI7N9lE3(Er~h^VLvYQ~FsCg13$^Kgm6 z^^z7r;>Si9`i2#?h9Rw(!s*{xZ#1Cwr14?m(M0*Fy|`sZkICQ5M8f)b#Qb>b8PzZ8 z!-B|6iTV953>cuw?uc4It;Kyh|Qj86S6y zPN+@-I3q*t)focnUN>6%a7=5YNn@nl)6l3(dC`SRsIU!eL4*yJs2VI3kjj7P;(`%c zYW^E9z9Ar(I!M&OP7RXH?b~7b^MSFd89yh;u}VoX9CKBYOAqee@)L$VdbQB%!|}fj zAPUi2lU62KWNBJp1S zrk^{OI++r>hVkg?CK^8WBOgMlL>RtCF$^muTIUu?xT)AkDu~)`jc3Gh(~k6L7Tu8? z&bkuBXo^{Q>IJ8Ly6Eg5-lr{yy4WX zstlgsqm<%t$>NU-G9=sp7b}rH@>XXz{?5|{NU5tHTBh`YkN3~`M>R<*>Y0U;_cTFW z)K$9~lj6CC7v;6XyAZmqX&LwNR7xj`fBsgO@$>Wj-Hw0o; zs?fMmiyjFz*5*m$!8fYQ z*F380$ElyU*o04lwmgMXisv98M3IA!?_q#9Ah@M|Bs<`s=%7Mzp@?Ed)G_%^0YNB4 z#1D!h!Z74yZHI}vYOg)a%iNs)rk{Ps-?usUhaKF;*G5dc3j=R+HSLg!z!#N$pO>l?IJ9w7Kf>C6b`|g_q3`{t7@X zgfFN;=aW>hSEwm-t@DW~NgiMPyy$rpXe3<1qHBU|!`NWKpp1N>%y0?@7n(o1`0aK4c4&L4ZrhO!>49;gnQQ9bvkeA%wsTGO zCPWJA5XB#xSCKs+rs>3oCzc_iBrohqfQprG8zQ@ea)a4d1=7v@y)Vw^d+X|oc$oo^ zw7ttTboy?9e7Rj=CxLYQ98LT^K_vz)?{CA9g}#*>=5eP9CI{nCMC<4^ILr0dUNC$l zzqe+?99`ssM$$sdaAyVusV~Mb>VxfvcI9EX3o+krnRR9)ktzr*j+bY)4z-t!bgAdK zMxx?oN|J@Z{K%S-wsckv;KUe$Yhv0C6ORA z{AAixkUYBXcKvaSvw%R7hZ>xV!&=?HF{B;EGjSt@VgX;LU`p1DX}8L>n_|Qw6^jB{ zj3ipcQC!D`Gr6HqNGCVciGlGzx2Bkcxd36D>6I-;15o-IgKbIR{(z$aELRIIwsMa1_DnOls!bdf=LG?yc70#+EC=!abpHOG048hyf6VKSL? znw>*-Z~=l5g^>F!1`a>>zsXE-k-S`d1A(kcy zKm!R3c~o0rS*F?nt>#G`wk0yiDxOwKZVhO-0l}iwI=DzM5zS~ia6wZ2H;nfIyVor1 zzdyp9e>lsbIRmk#OgIQx7Q!lUO0p7mfBs+*0W{+)5;7R7N?q@af)j4+OB)gBDv=Oh z7Crqm*zIwgDlhjt3`Qj()hF^t>lUAu!X1)iFx12-%yi`*N{z(iOb^h(3$l@t#!rAc zuL23tm;IurMSc+Dn>ud{k>=7#)QL44YbVzAGfOF01C#hH)rzXTmln%bXPinYy`e&C z_`%@&J~@Mbk?N>zyeE z>LOo}3?kUH)3IuapKyybYG1Ngx#R*`0@XYc1t+n&!dWCI$_EK-#Agu$@*V?5;^^R;b39Wd7|9!U!mu>5cGS@kKOwUF9?3H zd!k6vB>05&)5Gua%w@oq?DqNm-Td0<#yo?Ub4tAL+d%%!krh=c;O9Bqs zb`m`iZT3D?~RPzv-$BvYzBp8`t*4XGiQl3DWt0MeS>;R26}jcztT?EO!cD zT1}!SM>z2yAujAOZ}ZCr-*SUwvgD6bp{w}I&0N31{4nG_`IbJ;Y4#M zsNFhF3kbMJsha%qvgO6qGrTiF{PnVNQgzeP<)2Z{-szS#nlb7Au~?tmsY^7ICR$ur ze=|Zlo4U>IEeHM6caV}_4od)|qPSERV1*#VRGdAKp$4(Nt?G_rx{x+?1gdR#?&+{vIG=Y-v zX?5CosQLb4t)eEC-x{Y?L@igCAdGv&i}xkA{KGnyq#fx zFIP>`RYf_OaXl-(=Y-Pw^^O0wt}DJdx)kSSj4NVB<*bd@h2Lk-%j#doo`)U`1cc_F zEFjb0v+mn`uV@tRyC*jWXhd5z^?Sf7F#itd$^zlGVAkTz+QF;P4HnC4d!O5id;PTc z30H+rf)Yy;Mgexuq;T2`r<*#sEY8=NtnbhJPy!E0Dk&ScQCbB7uWxXEbwmM{8b3nn zWhw5NWr3Hsuz0k=vG{3XPkre4L%WE08f^8(hQTPYm|qQkXz0PqYe`YTC0TCm(@B>L zR4p}=o=0(-&po$m0TKimwqfy9Nh;ke*Q-gC^v|aXgiBN`%0`^qZkLlSTbbOvb5BZh zh!Ym9nXKl7j*9!rV6f)xeG7(J3XNp7Wgtc0kx+J|rVQ%e#=}c!w!p zaN+ETSa{g}LmrfgiHq4<<9Q6%-ARg`c|w&$@plYsHZ~n5&-&NHpZCl9_anrqZg_Yc zscwfMZUKhLfP6eH`?+-1E6>hNy@$sd!kWGs+B&kE#wV@w9=7j6IZl0<5fUkjc$!Un zT=8f9&xkAWxT|a2@1=|=s(iBGgA7OcKlmhtDC8QSuOmAdNv#H~GRC{IuftFTgdD5W zQ@rn~jCdRxXl;4Yag1Jnu9B9w=lkh3Ei9z49offw)U1F{JCe}q6o_i&jIpYw>+5(7($*k) zKUNHr;;ARir&nn_;?i&~qTfNkP=2fXV}S_3Lk{ zsN~yCk4F;)c+N8;7{4dqv}HL$%DtQ5rnw)FF|+QMGHT-I^m)D>=BHm*DZdZ>z=Eka zfwYmEhAo%&PI*^dmT%P^SDnLp9ox>szrc{oTu{RE24yIs^|SFb<^tx0441GX-V8K2 z$D!iSD9$Ti*DGp3JmpJ7zuG9X*W9umDd>B=v?_kI>@=D5ye2UWA_3^2HHNts2WAVT zG&tl2F^x;G`whX2h?tX^@flZ{Uqv-u_f6`0p|zsx|K#zyo|jEfBMN|xk9UEK#{H+9 zkAo7Mu2*@Dbz?Gk$v1MUqjf7@=eEOS&(FnWbEb+x_VZn3F`Lr(?U%ba&X1=QJBy(; zTu5$OQ9C*EE6$mgT(S@#WT67d*y=M)=dAGI42~R~(>4>(9ao;=gbtkxXQYKShEvb^ zw-T5!RF)bhqd5bok2^&ldyt(l$%HOXkVk=}_So5{w-!0j(n)Q;ZqA;$==qCy&a%$b|34M>%l8Ht9?j2ea#v(Kn7sRS0C3}6*4@|n$_mPWbm(9cdtpuSVU1BOI)3p~)J)PSA z-qiFkTBU~!%c~LA{G$;L%gVd+3ZG@pKZ~Np>bfqk|HQMVDrM5lPrM~Q+lsWbAsjlU z^A!voF2P@i2AZ!&)R=vcEcI^VqZM2CGk7z;^?mfq`o?cv&ovEKCXV`U_Urg9o9{cM z4*8JNU5y^~8v~`jRy=>cMgqhZbbPbed+ve@-rtu~z|R@HocokxIu#}JfyG=jR+>>n zBBNhPLFM~edbn90rcl5oR988H=X_cpr_)7d6gXTokx)9WF5z+#sqeuW{QHql*TX;& z${hPEe!}k*o>@Vv)=ILuvwxJM@^Uw1P_ur@!ga>z36Go6uICMeRPYI7c6-2Ep7oN) z=^*#?gy(&8D3t5kF_Ku2hs;ncQo4g2dD9%OP3!UB_v=UhU#^|!&Duud)TeV8P14@2 zi)=7$iC(*gn1jiTVPjH112yEowh{PFjw-74m@}#1uevo%F6r$Cu=X&FG{fn?EJQ#A2U!)Pv8_84PJ}P2rD^zC$Ka zPXaW+no|fhLw$vjSRhJ_nVtB~q>snwddt2oWK#Z+`hMozaNVm+RH zJ+JA2Ej<|8avy%^dj4g1Uba5neeM55gaSXpo;7Wl7%qF&WxEvQVVogHnXrTa+ddgz z8eX89oKwpOkFd>qKT476%41|ZG6#E%`lob4;d{=Mbd_@dEE@_RDlm$zMbmlmozQ)O z!Y~3LCam1>c^Y$7ZCr`+nA6jagY2eF(E6+whi0DnaRl7PSi*l!DyUG+DnCJeC$JLp zw!cbP3#wct+17cOLroZpNnp9e?fLpxRxAiV3~=@b(}HE=FWtzXGO$+$Hp)=rh9_#1 znEiG{75uLjU=e<4+wdNqk?#~&G)ZSU3cU_Yv+DWmKW&cx*Y|pA1%H={WX7k-%O?33 zfRJnng7j)vP6wkq`~wm^QRS)O?_k;Dxa>$cAU*bXMqoFy4f5bxX_fuO4S3_0JRTd3 zQCwZIIY-oS>zmFnIjLJk^t!AV{>AYwb&!fa=sNxK)tRE#e*I*g^Er`sujgJhYa`e5 zIdz@<o^Jj5^wB{V!{NEMs(jU1T0JevFE-n*75>mXs1?_R|_hIizM}yZ_lbOW;hm zzn-wH>0C)1?Do8RA6_r%X>)sd;myvL`^w=pHh$Me(dp$QU;w1 z?@H0OZ95dyxt4jOq0y`s(7GApqcEUJ;g7bQFzS|FeDx^-!2Z zVN+}NXPu_$Lz>p6{wQ7G>uVJ|ybJNIR1h*VGxlhZfm;ksq7Kuz1Cq6&q;__sdCkFM zLK=ce@2ZqaC2i98G`Lku*EOLkD1U%?8@LD|y-db3xeSHLFt(&++m)oQ`zHyVVj}Yv z%Zb4#{2LG=4a82moc8^?=_r;n+${;B`E)!Ic;4xnKU2B{j@PIO$E14g(O5pg|4x*G z%B$U&-P;}7XmgYT#`TGHSa(d`ofdYF-SxD7Xti>7<)IZp^JpT^RoCqeyiW67R=K&` zHxD8xKd+iTr|7#K#l4$Ubv%hD42i4mZh5^f21kf5ZT~JOm3R4wZh=2LMWr5f6FJR*=-IMh?u69**% zJIAFCD6_o9PeU*e*a_SoZqs7ugu89@8vD9p6sa=@5*kdUdtdrUrV-v3gobAH2)Tcc zsqrQj*&@IbINg9s&p!8j7Ys@A;!b6I8BBFG5R3)a69jx1t#@2E|DA04$TM%C^>N({ zPWjlVeG4VCmM%eq7#&#=HMhJg&ePX*4y;~k*)m`iYZfvnq*D;MJ#W$XJ`9YR!kM!6 zo>)=a+EW^9@w`^}-xq4z5_G5h#=uss#O8WkNApjFVDPm~|6*@up#wu zYzI|%xt7>;*Nr5tNIZVUti)22)X%QRvw3?SmtjPLow2Vb&P6~O z;Il;hr*I7OrufFO=9*}8?VOdPoz7v*&ZnUx-b*cP^-YiC%j}$chyjp+JI^cUR2u4L zn~XeJPvOmh2UC({PP!Zy4(Tx#ayBRgrJ!f3H%@OsLvRYqzIl51e1<`J| zv+7VX!m(92CH-3-&zo?Q=Mk8{*nM)rq*%~D&--$C?jNqd#*e>}P@>ZvPLR3<@P_7Y z>`15#5|xS1Hze!YK*!@(FcdEhDY37s=nDCpf!w&-QDnx=CSF0>ZX%}Yijg?HJypXli8!9Z>=62Yw?;77Ac ze%cx^LWkrj8+=x=WGe4lyQ&qBDHF0*(^4;1QQZlYb*k;_%LkZGsP8r# zGu2VJ$~KljzfM-hU;MrFex;{F$4ojcuft!|$m5){Ntk9IGgUf49_|B7>9~7tLHOnQ zz0Ci7oAde9?$W*|MO+dVY z1jhL25_I5Z}Q`0o+G1kQ1WZSksRV7d> zgEw5JM!4y^ozUY=Q^wDD{&?nqDCc_+t{F3t&3)b^mZne}UrIKcg~(s~`{r?P@>m@Y ziwNJkb>GF2Aj3R8k)oel5hZv*JW)JRCDpWKx>O8&{PCH6+pUOdz3{QOH`T*;Tah1c zSc|r9uMoXhF5~M`dAc#S5MT3VE|W4l^O^DcN%f>r$joX$`TWWJ+3eTMvVIol_iC$F z_#T%nCtF*bJ0m>&T2--Et{QI3Hk7N6{g;lsTTlT{@)#1tKfPH5Cj3>?`BcS&9M?rD zl8GktdovIluk=T3&j8uZq4v zx!Vs*~mq@@Hv4__aj-!eOeMJxVSVksPlTQZwFBX1nEURVks6$BjrN`&}31$L~!53+Hm! zL*8g-*cOph-{wt^`IkAWCQQ^n_c%Z8bCAnqEzsideRLn^*DB=O&z6_-@Ba~l`vaiG zkYGX^DAk*FV~i>bi1*cWZ9g+?S{+KNII+!ZT>=6>7E|lIPfA3@(E28do&V8bKG`X1 zx`!-;)PHjO;VlEjAWlQNTdBk?Qn|`tmu}Y+?Kr+Wc9qW zCOKDruUDz1xeTkCVb!tXj{=G2v2-4~;26J#uuUzMq=5tQ%Pq z*lFVd3u-7+SU{s$hJQClP|FvTDiq3-)UX@g2JAU?pVwCv_}hj-p%g7e6UxYl7`iZh2rneY41o{p#Fa8wHUO|Sh7+fD-A-b3NpFJ7!}^Z&IXZvDU!>_mBOI<`978W`UuYXt7X zMUdKY^}ODmr+H5wu7{JpM1iFI4cC$8oLzOLt1h3KWuq1BoLesgs6Dp@_tVEe5o2L= zsTxO}+VB>M-__EX?9Q8(nal~C%i=WhrKob~b=*esa@??>Z* zzpV({evToL)uV8EkA=Ee_uRh8sq52|U`Lg)>L*-3Z{_@Ng3_}LcD;wa_Gnw@LEf7z zqY=@^z0CnFtq~7{P40A9F-++Gg2{iIQDG}hQf_uT*O^k&_0$bhVNERdwbQkn;5g0q z`jEuA(e*c!U!KK=Gx-F19;HZ{szm84rc~`*=A47odVL~dSNq=>uU{ z23h*UB4L;Vvxnl>ubbY$n9dgmf+)tw6UOcG#_f@;Gi~>(MheF7MTL8lK4+MuJd%P+ zdhM-b3nCIFpW7404d>;}NM|7Om=h#r*hzkUp|{EX(hZ8OoP62r?2z*E>#D-^*7;x0 zW)_mPa_J`S5Aps18mg)Q(Hy1xyH0ERbBnrZ*a6#KRG8d&#I#byYW#*3S{)19`6t2oL>*9Df;&0VanNh*&RF4=f0etOW?-d z8ziiNwf+p2@$Hw9tM#wlsrS&b_$PR-dGYQ0WFToqAsMtNx16!RbZ8q=QRu?FdNj}7 zJQRNWBm%FvGyIV<{p7kx%eJGw@mzXMQ$6`>{zUg_LyAEA{nPUHtq#K+YR_Gb{@X>~ z^5+!}(iq(Rm<*%7ymrX4cD{8E;= zDN-2DYT~w!weu%+%WaSN=Cq>n7)s+4%;-g0Dk;t0c{gdZZ4HYKSTKC5GhvYM#=VV&8Jq#n!fEp!98k9rL2`V_-#Tp4{|N0#z4DYXu z1nzfDs%qN4UCwGRH4)Q6fON%R8NI9?ySaf|z$#=-_rq|#&Lf6tmRx0!R1I4|AY3^LFCpN$ zyU$rNy6CswMS-5>SLZ43N4Oj7GUp*&X50L`nn^?h!P!LL;iD>nVw*?{NREYDg6 z7K)gWy_W?FB%O3`Z@GPH3UE1Yynxc@E8x3m#3eyUXb>wFK@G-HR@ccF%zEtf z9hasbq_1hBlAjQ1{VC%Z8vZnd8sauI#zXIqr9u39pnO_0UcPKuZR6gsfV<7pb;X;p z|CM&r!N+Nfv-_U?CbMbH=bUN8D6mnG&_n=;$J^O zr>uG1yv~*fM;}@`&F_n=YT2+7=AlkP^|k9Os+w~@R@d}KKi%*~oYXMwa(-U*oCd0q zq?cO$o8`@=02H*ZY8M+%U1jwQn^Y=9m#6veJt^^gf8zO>FP=AU;)RD>?T&g@p#o7I ze&ps^HKZ_6=x#dZcyq3~1zr|cUssh#d-r%Ft{9vpzG12CbbbxtK5e*O<1D;zgA1;7 zJ*^q8HJVE+t#gQ8P$h$Zf=l>*h4~8H!KoOvsdUJ1dbE+r(HhEgo$_X12Zz(j>onT+ zIZk(t7}T(oEd)~~u_?Q5Hre(U9wz5BPG@^6YGdyNi$Ybq}?w_l?|;RF9|5P+wsSm^L(Q^|=-v-CXXw! zJ?PxP>KJMl=-%NIV=w{L+p2vo`Cp;&{C+9_e6mZ*QO zY05V~AVZ)clmV_06Q7uSNcsR5V?6+7cLrEejI`DcF^a+=v7$VhtR6D{^+;MpLdhxI z&LYV!AL~TbjjB`7-!djF!Jf$CWCVAI3;+*uiZ|NIN27vjX3(K9wC)OP-65GNu|8{6 zOQuuag+0lXGFAmjvlOfFNRN~)&Fmxo@>W`e2`Hwb%xO#;unEqm&38hvRll|Vl26jGXt7sM#DjHgPD zoy!*(#kcU|Q9NcjmN7hsv7D7V6|Ky7sS1l0KvSSPCEjZcXy8%=M0`nBYkC`oD^_!sO zIDjy9SrP!K+@Hb?(NL|3TPRZq?=QyGqr6`a-3N#EO9)xlwg7rQ-oCJPu6{6c>z9v? zP#?0@ggj26WW2o^c0x4%wz!y(3!4zB^gtW3{KI1mxK2coFh)*^G`un+5bszu%MGzM zSiR`SbPS?c1#_^}Wv63vEQ|*ykFTf>zx;LQI0fN~c>)+!u*@&VR(eKRK#gZ8f+v?B z)2Z7MP6Zj4XHBByJKw4(=+tc@*?wWP0V8+wu=->iJm<#Et9r=K!E6%(rtY+I<) zuj@Y8eHh$+vk=iOE#J?_qwbG%m9mz6y>m~OuLi^6M@p0wg|JpCnFBFl0|DYZ*{xEF zrp#FBF*TqhVxkev%-sxzab}^o$~d-Vyj|XEDlEHIVM2J`1iEGsAg~svB4>@R&>`&d6jni_AC*MkXG&d@cm*`qoFUmVt*T@WG_&(CKF6j{iz2p8 zoM=#brigTKGD5fk+P`zrY6LVi8uC*NYImp2d5kQc^I)pe7Ljr;?^MBi}yN2-lBcXYM`17_5`x0Lmzi;NX*7-MNhaM^ktcd*ChQlO(;V%pgBZK~ zNc>1w1<0q+xehb;Ca?L=1@+*k088Q&zS&*3ljPq5X-Ii{`HDZGe*GWICtyZoeHLZLT<07@cUaRoXI+ufU? zqb8Zh$KL*-01qMNA{jp4gE*0Fpu~MUo+#`n^oIo_J1dXF#0L!=dhGXmk zlakFI@u$F&#AXr3IxNr-T7(;LxhQe|dCCm2FWOs2MI8i~@p1uu-6`zc+ndgVaB~$t zBIuWZQuuk?=T8!SWQfg+=94H$m4(2Y#uPdDBz76rH0qytK@YE>uQg!CpNUI^ z0SZz`w;MJaZ4Q@D3_zj*WE2A5$2=nf=>tE4q0Ubg`XSrLFN}xiuXdGpEDPl{NnWTT zsz&l8&Jv8zglET{34&xwK|sUjpx#*RmXx`U_5-*Jjy*SW3v!XD z3X*Am5DdtAA%6=*odFOvAi*>B-+@?g@$#c3qpc( zFI|5?YY8sWRpgf{!=cM@-PPua8A~QQLoA(}J-WoKzJLF!7kswJzp+H4wybmUj9I&4 zZ&(2P#mm?vcD*Su3N{0~^Qzk0aj1c(N0f1GT{ETpVZD6|H6a~V0zI1K)$9JW#arDu z4qem%2Lj4<-G?|7jp;QK1-&AKa~}C9HK9yFMqRFgjwryIfM!f? zK14W`NN|NT$HH;I2BxAE4=)KpGmAz%>esTP!irV#WN?XAFl`>*(I9e!_Qu{98ycv}jA>2b9%{-O~oxz%jP5 zJt-er0FuDM%WYUc`a|Vwd2GNMDEUfDU|TCCNM+uF5tPVn%+FY1@Smk_IShJmgulhE z5T=UW_iT1Vo~iI!Aok$HOw%aZH;2%IpTS;oJ(;fft|8BG-Vk@Dv)iunu*95(KoLf| zPMsio%s~~yH+e&cqVs@gZom`jXn;W@>l{MlU6ESv5*Avk3Kp5y&J<{S)=pj!;vesT z`6FkkTBaL92G3TD-lsnTM{;+@dq+%fNY?}M_#L-5Csy$1K^d?S*)uFHe~C4%3SU8z zHzzt#$8tgf_Y4tjAsc#1e8HiieP5BKcdtD?V)YC^$WXI-e(^s47!a5RL)oP$K|btr z_h|W#J;AV*Ibdhi|+DvEt;M;dqF!2W=e9x)6j5-3m_x&!td{p z9OW{h3W8-r_#pTrBnyD{^7lNj5|f}aIqjm|{UIofqj1pXuPhQ9qqt1o3Kc{el+{0g zZh~wzuqR2xASIjjM>+}eG?UB|bI1K`!WjkL+^=EED@ZEE&{gOaq(kJ6X2Lc%uTPn{ zGcq+9@vk5!xITs+PF~O)-M}DgJ{o}B2x3J$*UsHv62VPMRA&M5tP$_o-^|1HEQGqF zx*}I`oXVd*l4h~sB)Ed;52tL0;&(i3$;3|<@eF*cJ^@!_x>RKJ8;2L}52R|s_1K;r z{(iJRubTu&MER0fZiRiYU>4QXp1+-2+;CsLf6I8=;6$igBW^jm1YWZz3d%=02u;KXeI80`JsYYG!jdda!-K zv7h$z-713YhlH!pgOr^*@=1w$a``62I9Yx5O)Y#L6zBuX>uy+zx9lS>Ep8I$&^sQ? zE58bzJvDfM(;M=XHScaP6BiOiKTVM5Lq&9-MYtkCDD#_$Vh>?b!x8@pa4YLFtb@wM zqs3>e$@;z3amO&1KRZF=oe+XsMHY3o=ysdGoS@ZYZH=ZL6t1g8#S9kRCg()1gxsVR z<*(Jn%@;q?3Miw~VbQoew`I=J!9_)7?JZ(t2B$qW^d~aOXq%aX)in5#Y+sysQ)H7= z-ze{1(z`CShM&VyY(?^C#y7(DuGFtM37nRyN!(*B*r7KMKbN#CLP)U$!&1AjZT>*! zuo5Ns9&q+WW!3H-(-Tq1}Xi4M7JC4&8|yYlP=hh3_wo8qGnE4T$=x*7~;c23s?GAQ%LVv zI!4?FQu9)7P=i)fX84CrIoPj*zQ{}{#w1JxQesCkQf2T}9d;$NQIhi|khDbfWwc(A zPjN{dNQbznmt-nM1}IvPBCGyq#NTaV6#n2tw}RZDuTtB$OxWn1_8acvW^*zUBv31j zb+o;~M!f`91SEg;Fyy&t8y!#{xnPna?S@Dqg@Qu{dB3oVRy zChnZz2d0KknmCj{d#TD}z^N^cJBiq!8DXc$g*k#m;3<%K_2Y~Hwn>g57UH59@}f1M zfqwLVM1rFI(G%y#@@n`QI_=hmU`Mdlu`kF2N!lf7%IAAg0Wv%E1oc$k1-gk?r$K01 zV07w|{VX^ttnJGm{ zMalA9MRH;ua{U^hbH^MGSpIB z!Kpu|2EUTo4Y899?2$YYXm>oRYV*r>vipexKFRm#pZug`#{M3T6bg@IBZmV#FtLr8 zMB4>q`%z0ztJGSA%;&O0)#@caKku)XHcgn z2$~>oQ62XEZC6N%%9!-Tz-+*-gVl$S6d*>rU5&N1%Jy)el7vgLsCDw|XrM&n$SHgm zL_`-5v7L(*+lgj>zsXHV?8|9lgJKa?-UN?mA_)o6vARhohd=naPwG2dlptCEKLD{n zPQSQA2wI)e8{v)fDMyV!ezGBU2#|oI!Rz44cDr4#^|Lf6U3mTlef3JSF|35GC23l% z9C^&=6NSUMq6wrMS1gAbc!K11I4%?{)D7}LB_Yg*U4|TkN8X&l3cp+S5AVrgNVJ4_ z>F9ilOKLcP35ixXOZbw-3}#rb9oQ)}hX2CZai}f44SXQjQy?6v$)z?1fw9g7uB)F3}E}ME3_v7pC;3@cop?C21&B9rSUDzkr=_bw;t#MBsk53G* z&|D{^jZdL94qt}wBoGpxGY-_%GsD;m!9Jq6+a-AR&_$zw(KSl}htuphw9o?BdrShu zdwy|KJWPO+!zml4@GJNxok3x4WQZ`T3=NiC7qN3KI6{i=3kIGX&~?mu@A{`h1h&&Q z4$7X+&heKua%V6=eZ%1tLMQohbhqyP|86aiWkuSlK>BW*>h1p*EYP#_UocMZr~e%b zc%riOkp@{F9wVYN&pao%xBr$7@FO0xS>VgV&%*pK#}%n9B1ax#RE)R4J!g*>7W|qS zHV7F3w(#tSDFm@A@SVcJC7fn(*(gg+9DB@{E;#QzPEo@=pIbxlrPVk=k2yV<3{9+b z9Ah#$_UPk&bm6&W;Z-cT@-rz)K!b{7rmZ)LQ0$p!A&&LPz-4Fh4&rq6M2te6H*Ayg8vlY%c!02~r33?3p^FjN;{kZ?#{=rFb^aoO3A!cH0iopu0GA*qb`dscMg-hr@(5d=fyWcf=5Q0#Bx z+K@=P#OX2$;St4V!TDkY;qvgt_|`(Q$_dL9L#U}~DD4v^-YU;xWde?_pwWPn;I#(_ zDG8aRfTfWU5TcM!HRI9J@zmisEO&;Hf_nfXSwQ*Bc@JM8kCI?C40gVBSVHjmhcI3j z{e<%bZtAE!co!n!80q1%K~xy;L?oD@1ZsK;j z#>(3WfgyefNYKTHY$xVh;0Y;03Lzq8-lJSM6_O%{=O_{pB!Cy@JIM(JgG+{0rCt^Y zWpSmVnz5xInU#p&RpO|tWdLTDD?Ri#1K7g!51}XkQ|>_kHn25s!00QBdTE`26 zU)g06QUurzz*;>0W5NyqsgtXRGvZlT=<5tljOwnfI)1`{RFE4Ipd2^bZJ{$bI%p{TM4pVfxBHu}!rn@7Rvw!e`M@M(a zyz}^B^&0lUHep5f&aM9c{G8|cvWD&0dDOmalupQ^^K1;ODgNQ@248C6(cQxaOMm$P z)5g8C{wo&1qt{cK0&H1pC4^0*WoGgaVB(3L=bCpkhma9Jn~i6!0_m0TmI=CH)_4cs zJ}j9TvM_}n%`H2W`6=I>Z#oYbsCq%$+Wh;!A9vx8FDMHWp%h#M4}x_{OBC3NwBr7G z?_+R;oV7v;AtUA=A;l_*Si2}V9&|+c@Vs@`7*D>!iSU?bB+~ty6`2%mAsWxmeR74> zcMd1aemCfNBI1P^ufV-%Ba|R4+A)Mv!Fur}l&*5jj9#TttJR8CtYid*$yhg=StUtH zM`e~JQG^CO;w*G4!6(V7kdVwL z0VbRXZVe?#T0(jYL-Df># zHFkyqenJqjbJg%|Aoq`6S^U!VmGDjsSGwO|FmN{ldh5$GJBQQ6Spb9J2M&sk_P^65{KrD2VS_O9bqv!Tx9rba zdJ5f^A&<)t_-$==dOOL#Mk( z?3`x!*g>p(a(p`BqI9xM>9TaYQ%HBJ;)g_7R4?79`)=9q zfdScp6)An;g%>#QqC^{Oxc0ErC})hqf?1vgKaCR9Xf)skP{Ia|80cS}$~`zjkOv^6 zeMXcau!vN}w)Aa6eV#&!Qiz|9pc~+EG>6jDWAnppkQV6ZGE)-`W(lbWv^Q2edoWW=%xGcS^7;F7|ZNC+9n^g`$Kof(Vi^xhCPm> z;9!}N_9t5Cw#R3|AA;;d*HhT}K5Vw^U$lkK?8EAw>^pk3^kes3XaUb?Jf`2-wPnts z^IK>8xBam6!!o}vMM|STFY_zDd)s-1-?&%Zu64Fqx>vxzOF#4JF5Bm2ei}V!@6CH+ z0mn#+@6W*ga}wNn+}IHCRbc<&dO*2_-ylAK0D8wK9qwXwX9W)nZfZQkgM;!t>Ervf zkx)HDD{GP945XFgdD{U@kXVr_Wl}7rL_Bc2!Y}M~$8}+vCvvM{qK?H!Z?n$zc%NFY zcUph91^(=Sa2QB`<@NU2@vm(BfAe$5kq(+*M;>`3i>W%Tbrf}c2SovJkt9ioHxW_2 zj*=|=2@XW9}0r|(kFCq0CULD+OI4!1l7yTi)-?r^1^(i?GE4jNws)?h z55U1#?ip0R5u)V8gCv6fJXwk(Sy)%1^p7-Usl2bq#0Q#sgo?vZ8Gv%XOxV@Ij|4dy z)ly_4sBf+=m|QH)X6qc`=#ff+V5kByteR_mht!BbMK>%JftK&eD{63?7H8y8TY{p zq^R9&wHj@2snu#FNn$NzU=yy_XssRX5N<0qD6QRfA)VC^jnMgavP*+6I8dd<@o2(i#Gn^uHZ3rUq>8qL{O%Y67sAemB_`PW!=(SE-E)XPx`iXduZ>%N zJsYh=j@DRf)q_5|g)>7ZpMCL9e*K4Sr)*nfBib41{Mhjmyj0a{)mob*$uY+qlV#B2 zGRCB+U;QUuprzyZT=tJ|RF(`tM!&ZOLQmBsjch%YVA)e?Q2u!1={L5ed@`Mvk@ZM{so6RNwhNDwY zJ@r?=`PJw)qi;ft61kEn2aOk&h#vJHu?>I8@GE%c!P&?6Qf|23`ft4U+Ii<*fEx?z zqSm~*&&+;u{x&=BX{_&0W0z;Kj*#T^kcXY4!v1fsv1>P$Q1e!crPke_F=_v#lTSap%SYwonMPN4%_m*>hSxm9Z!_CHd%Ac zT3;=a)tVMR`SH0;PdOZgP(aRXnfoF}4pdI4a4TB{=9^BDG8o!(jx(3*2v1$usJBdg zzm1gtN6x4Fi1>$fCj<%u20Hraql14ac(#l&2&Z>zYZpcF%rno-oH_IOvT4&QVp$JuzO)O*N7+RZDVx`n^#~l~qn*n&GX*xt!S{c28Zf#D$@RfD2-&hu|#QBBn`L<&s?E8 z8dCf`0LcTDfSBvWi?(-mf+c8Ws3y5=2i~z=?_x&S}|5C0fKLYXD&tt*zEu_sM7HHVkTXQQ4Mc zJcP5gmAd0$#0r?%Ae^rFelu6^eE!)M_=n>y&NNh^e9r|JT#)Cv);jQ?0@4carIc|T zpMLu3n{U4PZzwUo6WDEaN+|G+`r|PTb_OPSJv z(MQK|97WMN=bV$KX%t1D`OIemk!#tGEc2dqO_%wMw;^-EdP=HUyLrrUhyUW2KWnyl zeH&G^7pP8ae5`PrhdUjLyb5{_T2)e&XG1kbO@qac#tqxVJ0Z!}P&SOp zkl>8|cnBzC!A7WH1tG!rqL=UIHmrvw3N$p=n$XB@d$u#!8k}$5u~+xvzMsw4JGcK;3;b{3tx* z2>z|$B^qUM1tQmf{KuJ7rcAl}?z=z!@q-_H@WJi2+wP3-p23O|T;Y+^5-sNwvO^2% z#AUy>dE%*`njL?Cuk*=<^QvISIeEX7L-86pn zRd<}a!JRixbu(_gMW zrmtT{mtJ<|rI%kB>p0fYX{Y@sBr;5E@Z~w<#`H;5Py1F`@Fqt&ea?B2GRsUi)cEH+ z+xw+wfu054l?9k8gchL5;sn_4!YOh!@>NP?Q#2E^@Z%rZ?}qEAWBFqTAGz=5^`>mG zph@emGWCYbZdhr2pHSk<-~NHYBV({V@6o=rKZ%yBO<4QIg{|I-=`UU!PqMH!kJ5hV z6xzNOR^4d3b=H0O_8UpGzWwzh#>CaWsJiz-hiAIB@oH<$d+CJ_ZNJO@2OmKuKlLle zRAic{X!rd;8-%;~K&;WlMn_$@9v|Z`TEF+>-)n(?INl2STLDX5c;SUXTj9^<_aR+T zf8!e`?zGcRLWs>a+w8Q{PGdqNHSN__ zozQH*PSh~j2-Yu>s4!&9#~z*iz`YMq3B`r$Z@hJrO}ElSU-|OqzjV~UE?GRBqk>W5 zl&)2)CeQLb|KSgRSQJGFBc5@_8U6kJK_To<*&5_o94gQ^J6mw7izF&DQc|EYHht@) zldrn+R}as4oRl1CHLc5IqVIn9jG@-BxAuugAH8SR-7b+?+h20g&*!~7f7+ebeeU0m zc zyzgK9?40MHf8mKsFS~TcLyuFtxzFw&ynn{Mi?W5+{{GerfBIiN|JGl*Mz*Y^>065i zIUb98yxO@M*c`=o&OWVSU(fQ^!;j5-aONwIzxcwcE39zSj0dl|;$jl?t?z!fZ5l&) z{`gZb+&A+z93>EBmvJd})H?o34|+fMEbzBl;2(~+{+#~SpX;EtzOS!#^2sM3aKHg) zopqLu6r(DqIdP}RgHVcuw7QBYhVVC-;-UVYM4U~+zh$#_Lu%J-y-C#6X}1h?tSJ)f zZm`bhK6&7nv@X)gO*!d|lfTqQwA0R8UwP#(S6X30q-n$o8&yr#R;K*vPc97QdIEpH zTCJXS)>(fso`^H+)W+c@5+@vLYcom&xwdtr#rQGNm`i?k!4H0L8p_iUjUU(7a{i&m z9u<9H>{tV`^HB;xLH-cQ5#6UckV^CaVxK}`i7r4Xzy3& zJ=t8ac<#Jx<1IEDUmw5!jyunJ>iKuu#rOCAuyvxGto8LnP)C(St+cPU`f6*Dp(pRU zw~|bf>Dbp_elwPew5?V_h6Mw|x%snZrm8-WREGzLaT%9&Br@>oJv!gtW9Of8cHNA& zf8@af@5+0B(M1AAANIZj zT&}8Wd+l=C%q?jINJycV07_7rAT|UMRC>`*1w{n07X(ynpCSqtAQZ8peu|2UiUkWA z3?=m56CjBpA?@BfGpFqGKkwRSZf*iWqVRn`aOb(totZPI?6c0^Yp=b^`=XXm2I5eR zdyHB~D+jWu5j7_WickCS2^W0zjLo*#^2NKnqM2%a-u>~N_eeyKm@qj=dbKW@9h6T0;{{% zvvl!w;cu<$a)EKsIvYidKgIS|Ugp+WoJxTh7Cb*BqbQ-Qmt+KzMIf;&p&}t;l9>?A zqDZXGh*Afog#;-PbKbg5NMs#FZ4Cgch9_Nt^vst~mu)*lR3)Yz+p_X9cdC<@Ns=_1 z&9W@5wMwY~F~5%X*D01R$Q61Qd=nMuBb_U9;FZR4(&%U2;_8C)zxMtQe{lYzkBsf@ zd34^qp8mdU%$Q{lJaE%>*T$s1rKQUj%btXEn&&1}Nh8vcimkPcSh=dxyU8Zbiwn;G zwq;npEM|o2NRn5nj1g6QVuisv+C92?R05+CSYHWvtnMl>TQHe}g)YHD3*Ii};dGA@ z^4W?O0O((}#}2n#cjdQ#^rM%(_6>r@0{g|b=luOQL__yKaQ8KH9^P-?H)oSKUNyAx z?6bd;FlIqB>iW;tw`DJo{e_|4NT{OKT9y!%on)U^9`@yLe64>{lTYJRN$`=eCm8Ia?8q1Sij~Hy$|~!WvC3L$ZDdSr zOyYgNv%Q{rq#no2zYIF$iwL1kU}V*&6rRe8qO5I5 zNRXFJ+j7cw+iun~apLwbohd2OR%TiA{)Zk+qTU_1pYf$LzOcatz^?hr)mL5oo$vNX zX}=nK#G%L3i_ZVFozK(bg`{?Suqt2w%Dpy9dd7{J_{0CX^oS$(H$*SpYwz(JPaZpV zgEzhYbuZg(rxh!gkeB-(eCVrooq6~XZ+X$~dv5#UU3$lDc;JDr-+9JX!2^j`001BW zNklPt*Eh$`XkszKUbIWS-+6t^KT{~OwNXt^McZnCV zwKCG2Gp}X4qBt>%T-mm9)(|npq;1hwDoS-mL@NdsHhP<>jyh#A)NVDK%|JsG{99#N z#&KMmQENRlfEfdaK|46l*so?mj7>^f?FudXHoEPhhdKy=<|vk|^c()`%|mZ{yO)t8 z`rT!}5T;^u2UjJr6LiO&*FYAYBAwj((9OJXu6i2kj=Selj=VrfTG1&$!l z9>vI#cS;LbW~16-tp_yvbyNb+oCE?HM7{dMX+aMUC53g4q_~%)y=mUOcElAdWgaE* zs}Dc8O*EFA7IB$J#~$~QS>HW}v@1!2=dU?o{N`g9EqJ^j89Rzb$L~`=EE&SB6 zxYaR`d~)#HS|V`As^}NL{k<(q7593F=R%_Rk(Z zn--u72ry{Fxfop=`l%Z;`s=6!{*?rt%I^7B$3ETP1J0HZBH(W&N%9mX7mNg$EWnsj zQdr|u1YURRtPVt@;j&RS?Io`v)gfxIuPfxyYfe=Sw1a{=j(Wxu^%KQFcc)c|M5=LF zGCK1j`oY^gZ4i+og7KJDqPOqG{b$ElRA;+K?CYaPlhIV0^!bRicf0_Gy1 z%yUbjY5_GbnR*&Y9!gSM6Vq774U)_d>7c3s(CQ6wcvNHX!(IXcD|1~#9T<|*OQ8|9 zNEKO!znEeq-E(Mz662794`vXdoOfLh*ArDn@Vf<1;fO~$tYoka`-CNXxZB`mukU<~ zFC)${`f*eOFEj~2W$|c*_Hv$tDPhXj z`W8t5u>gs+Q-VZ9z=#?V)q28c#r53CvB@G2W!Nti=`*;smP&b3YAI#Pn#oe4*fS-R ziW4X?DXWsAI5Dmgf#(udRbs&!hfH}1p3CqA8Sk`~ykeypG$$9Gvs~BE`|Hwy-R)~= z)9&-c8Tf~_eyts)%!E7gD)q_28{3890S|8&v2A1rs3R-gj?q>1+;f6rA9l3 zKKuns{_x`cM_!`)L;cQI47K?z&yhdZV374CN3^ZJ)`+XrH_S(~jCgkAtf0_JzG41l zB#l3WQ`Pc8mbL;SjbsqDt_gLqlPy2^;;cq?Z381c= zldwJPiSIc{MY1XlXvKg5!Io8xagdSPoM>C3&B#wB+JfBl51nup`OTf#kmO{dDb(1U zXri?lQmCj!*3`9u#B%_lLB(j`QX3iVI&kCIqIR5&FHAv#`Y>Je4s0YkyA zHKrsQMwXAFC;*9ty9;4c=Z%*gFLSuLYOW4N0Q7=m+AD59>6m&XrKBU0&I8B76o3e0 zYNEiV2HtB#CUCEz^D!B=l<1o zY!$I5?Zm*)+uwB5gsognJa4X?9gCg=XAA(oA28QctvfIQ{|7#j8S!sq&9y#pwof|~8BY3yDfg#&{@6Rt^IxnH+~@1xtKT1RtI zVQ3DYw(dM=59XcK*d_Hob$j@mLzfF_iQTD!W3J4_9>LcS?(NqCLM9FO_WEu)y2N+6dPMMLSIcMk2?Y^%$+IxihAkeW zX|-ra+{IIWWP>r>dw0h*;h#bdH-4+@9azZH6$@Kt4^D1}|bT};Wy6opoiuPM6uJYh{B0N5H#wmMMgL9fOm-wG_! zR$J?VOK~lc;anq9LA&b%994JyZma9}uoapJNe%VNB(!s_Jx9NeO5lYj0UjU9I(U(T zGX(1~*iOP*GTxC^mMWDdm;C&)uY08ukrLUwMQI%M_1|&-T;R>4rZLR>-QnpRMo#`jTY>%uE`=8rzhA(Pn2wZo~~`Mw#3)9eeL~%&L5$rVWA;DG2*ql{h=|# z9e`UEetlvCF;QSETC&C&!r|znK-tE1>KO#ij1ev27C>V4wyy11x2!vNgx$hPx-P5t88&^+5*ihe95F>WuGjw;wd%cmbvBnecUbQ|OWWdL9`vF3Z*BY(w z9`WzKj~t~V&+$ZKtLtDKxz{uOd42J>)_Z@ckn+w#J@Lf%u<0Jax}dXyY+(%xILtPf zICjdk3HY13(UhqhPM$V#@{|pxOq)1m>cmZ^k1wi~q>LbAO*@K7NK;lrQc~^UTJue7 zog|4;Dx{_b!fal9N#OtphVIVeK*3ny)mA8fVQnmg$DdcRYKj62G0F=`(vVh34**xe z27){U#;dMR#YpgMRUL4ln5|%;j-mWF75o7ehzN$zVAMCnrb~n$fjC8*S((?1_F$B-n zaD*Yi40W|E?v(M*EQx|dRf`X48d$kvbqn2C1yzX5R)`$jXv6cbB#mk+2bRkx=r>eB z1x*Uchhre>TD1mcTpKg>8-C)SyK@1P_>Mea_3`zC;@*MJ!PE#yZro?W4p7L9o?Y*c zytLs<<7xn3`RAdl3Sr18MD1YSVE>Z_J>Y{cG@72e9DR>$PPS`b8VSM({f1?t3p-BL9STL}Ho{^jm zA!REsvNVZRgc;pdBr*3OR^(k0NA;v50N5f(N2~)_blnjeW=$T|7?{B=jsFr_t2=99 z432O@Jimos%bD{!q?7?^2s)7GYDU;$!N3fHo(f)!Xm!;Ptf(O0U<22*wrmYxria4R zypba!tPpEW$&#%#(Tdh9BarM(2d6KRM4mx~LS!(!-u13`Ir!=k|65nn9Ewla2_@w5 z?>aH~jDt@K{fi~4pq5-wc90!U1^EI)H>6Hc<$dLWmY2t{Qmi1CB`KS2NPF$JW$pJU zisCpHLNFW`NEPu2;X&$-mti9sZQ!m5YlVGGf-zi$;uv5>af~Rp2U|AR78uS+0ET{j z*Y&BwatKQV%PR$FVIFGSu_FiL9U)ejZDHi_?c-m41~&6+b;K%x59qqh>L5Dy_s9+4 z^?F!Bh5m|0h91l(u3IV{amR%L1x7 zXOWE%DSw0qpgToj!?0n^zXm&yKZXnA9{E0b+cW)dWJA_I_wcw~`-f-h`}*Q<{X6@! z`f=V}c+o6&LrUX9fv>Zy^4JsO(m3KmKI75-x@#ivEn1Is)H2pYMNTA*afSyeR#6z; zPx1;yi$zfk+kzpT3kkFy3n|!L`0GB`5o@1XwqXYaFSC<^lIEB&V zp>45L!%78DtyZ)neXZCYe&w8mFpl~9p_}nNLsx~BA6tK+--T?#x}#u#VQ|)ixL%ip z!|JEchwQ- zlu%2AMRPr)a#W#C(5mNH90p6GmTs+MyLz%#f(W!>wO)V4dp8!wnlP-_pU~>=?)*bn z^>g!{M00kVjh&(1M;r`?S@_DW#mxi-%r?r%%XSSxoWFZQwo$qg2s?Lw>h=(}if{$= znAHXxU$fipVE1(WVfZOXvv#Kqw}K~&;cubY_zLHT#UG!L4YJ|A!iNzOu5ZtwLO+Ol zb*qMZHs3|)?%igwXzIHgZn#4Lx$C3}ans>*)J>?L{#p(C&%Um{^>v-6>p@x9@Bi`k z^~K*>-~A;C281Q45uqoAK)AOs%dj+2Y~JyXsDIWt zzy{ar$YI=7#-JJtH&R=Jc&mM}^$j8nkChOE<8%HQ)_gGMx|LMJi-zu5heb2`VN?Py zBnk92dxDAy!3u?u?o$$-s-P& zSRKeFK^ZvvKcK0?B9?6}cpJkO?9Lbv!0}n_UkQ?g4&(@1c*yJuf;|T{&QF9x8rTO{ z=^}K5sW%+UpV@lg*~HgnkM)cn_JjrwEWcbx6`tGFM9^^faU$!_>os(iII$Op<7k*Ul%Ln`L-T;&2K;N#MTXwFF#ep%J)7 zaJcd(m~{314qrWds4dI-uCde)2{SH8DOkmv#?m#aIJO>UE|iINU5_EU$|l!{DJd9R zn2XUuE@g4-S01*v*J{uDf2}*=(>m~3x3|zKIiB}4@wfhs{WVhNQFM6DxnC=bDoSjNH$wPElAtC736c^^P`J2izbS@WoK&6VRFx!MncFn!@u+95cmS^MLc>CJ z&ITJxYt7+77+T%|JO|fBOrbV|IGIb`dFS02PMqz?p=@cn_bVz$7(@#U{bTkG?>q5W zfppr??HsSe_?qZPXnD5*o|i8FpNEPl$UI z>;UI$tR5U|_uvtY=pH-zJ}QA1ssx0R5!bSHoeE6qfKirVvB%>G>p6S^>#8TzBMX-# z5s{5JP1AcMC*y_ml&&N(@E9l+u^1l4@Buh4U_R8g7sh;@(Z%Dmd+hV>J!EbK|5ly6 zfn{=i(~w5j*kI=t9FAOn$N4%V){| zD`CF&utE+kSmPmt7S)B0!;f|Kqrp7Dn}hDf58dPI({_JFuLx%g2GZ(t)Sp=458E^& zcIHmU;a1)=Od&`z9CrV}C=C(GZps^*T!P(%!DY(LKz5CC$hl$RgyvvnAG#@WY1n2| zY#fCtDcnxDb$0sJ%?)qih~ep}{tb7C1pn0op*yjybuhl7sr(u?l=<7pr&3#xp`7?3b#KCX>E))_H)P2R;hiUP$c-BP=vENI_7Ad;{sIL@hI4%5r}~hNjJ- zf>G9p4|#+C@*^Gw%q-hgP!DThMDU?v#Hs9Rnbz@yf1PR`51 z)+Sd4AY?2V11DGk5IjRn2g}J0hOv#Xi(x`p4AgO-1yluE$(TlL z%CkPN69RM4+>v#-eRee&{J@xD$h`H1cLiD=kiT$VcosOlxSkXw;7V#>Fza~s2w8Fb zA86w^KcE(-eXtm|oCw+>7j>Qk1K3S$c4rIUrzR zhWAvKhRr_6tG;+yuPlnZ6Z$@;(;yWujj_Q6hzfI=k36}8`&eC6Vm0tFfJ5)R_CgnB6@$7OFH>1~-AR^ZVJn*j zvPE=3t4Nl4r6Z+Iys@l)RlGO1OJ3Q&ctQ{+?#-*J_u1ap6w{u`>a zmGBfJbYO>5tBs?waNzSWMzbRfas_Ee73rvxcY?zP4GKV?p~U%7S~v%sS2|XL8AA;00-gZ4 zHSJC?m-51B4nTPC**OXim4%^VtVf*$`jG*@ffPmq%m}Av=vGM})VS7dY=vze%?(!g5Pl{2WN3K5nH*(^xffmI}<y6&hOvfa zKcxjMGf6^NQ^70(W*qWha67RMCMJHCn6?~Dzk}zW)*Zh`6CzzR9CncDS`C4@q zM6l10L8BFd$Cjn)J8gy39#T;}e4El!X?(ka5h*kcwT9Rq_tgFC>HgYkwVZQ3Jw3sI zksRj=2qq&>v4*}F`NK2ydA;$plnUADM0COlCj`ueb%CRXMh-{xKk)t!64A*YKUqro z?i1ckMDKgw`_Q@J=HU*KBy8Xs5-3O{Q311x@mf|bc}xoX*rRuoTV<;@AR&BdVT`Z$ zi!n)IBh|SAJn9`$-Y(FmIxWiEx0F>d_DXMCk- zm4jih z+F{kKqBzr%OP>U_+sGXSyHS#06_n>nXcEF;A#P%&EQ~kKI1LL8*61PEe!WFAdcddz zo=*~}*YOAkRCkOO<4BH>(47CEnq5-l?z zZLkGE%H@=Mt)ze*!f7N(>nIspAWmeJcd&G|WQ?zj6>)?}qj$|TL!qgIMOzuN&QlI9 z-=vu!4(Q32P{AAluz=%s-c&Z$8b%)=?Ui*Ud7EWD(AL6tIzxylD|m)v5*tprFUtl=L!YMbL3ume4HvWTrb z77tP8DXlD}v848LGHvS=%pthYtP<9FV;%EDDJ2ycOMt>&<>cWY%3-%fG025mM1=-` z39L83{E9C0yD+9&Np^@A4;Ce04()&i?BJG-wea4+9IhPsDos&RDuhHOnBg>KN!C@Q zEJe7!b#OW=L2;azyhF}aR%o4REIfE}gQpchTS%-{l32YvvG|ccjlRAlBdsq?hBIS1?OK-6va8`p4)D^d73ZCbS=_Qe}s_uBnlzu&80`|2obBw52TN~$Aw>8~%}|KNjY zaB$}I&6@pVk|<84`243nQ5hL$O)3Cq!S*zBPDUx^`E6Ivj^+4W_dZ~#HRNm=N4)rT zJSf>aC6z4NZ6eAVh%mNWtG3(b#n=DiHc~M;KbXVi_2Kuu=QF2$oWM5{UYraDo<~j} zL3gVc+v^g@==MD6Ztbk?H6r2y|KYqp&O~_z2dUVTUERC<)%}%=uO8& z65)qSfA$*{QPt_JSp4|dF=KSn>}~Y!y2~Dg1w@UI5-xciVVsra4Ojg|MoE^WW5#d3 zd`bIf-}`=_8n2Z~k~GemN!)+%VMnf?wi?;u=jvz2ggvIR`jDfKjiX2lF>{yQSF}rx zdpPPWd+GL@En2!fM;RzsL1@Lw9j8vyI_}NJj_aE|_u5;cEQL9wDAFR*D!5X7Rk_N* ztWY#GR74TTd;#18GftWcJ{HB3X*;h-C>T1AD=gW!Nd(VyZ6q!X!+wax7d8n+?(N;L$uDj zs!&|XMMOzV99Kk)Ixj+44=Z{iy6~b|haP+g>RJIkq;rmeB#O#TYwk^VG}Cd-IQsNy zpT?pbRkYD5C!eOHsL?lO&YWvM_>qsE`svRB6}0NSWX87hmMkwqpwtwfJoS`R^=C55 zWTa&pC1XDKnKNUJ1$ka{bSxi!1}XvE7oDk9DZr zc+zHLdpBsPw6C{$-Q2r79ipn5Ib-_+4?G-xM$$r{W@6WExBJ`dn<}DqZbXFj$?Bl) zf876rTdOFFIF6N4T&Tw(wKdQDtlL}9HvX2aOgE%RpZw$}3Eb2e2iDmL5ix2sdfE6P z&EajhaNxuQwkZCi$6>5UFB|>+7Up>_g#{WtJj?6ro(~A3s9}MBEBp_>R%=C7(a_58 zp7N3Z`RE5OIrU?ducTsmq&mL!_!Xy}`rY@wmsYGGTOb^?001BWNkllyhnN16q9~T)k5^r@xKnz&{L=58`=-O+f*3V-U_~A4huWmQ&!|(p z`{*}+>(ZZIdG&9yWQ^7W`>v~QxV=^9=OPG?$;I+|f(5s}0$Q{CjeZ-Iz;jswSX@DcVx;p(@FDBR z5k-m5tKVJo_s3oPhS}G@`-r!#7+Tp9Hfi=>_2)UJ^0vs|c=!QExW^xUY>yZ3dBvY+ z51Cfc9(?2LU$<;%Ig0B%X%#E*n7O6*-uu=gk6yfN`Quf2>#f&acg^1qefx0()lgXt ztsGo2=ep~>j?O)2R+sJTxh&H4YkNgBu;9Tv#%{RvYxjR^=_=#$pZ(-#*I##Ifg3NomvIVoOofP4GSKOS{E*%>Dk?ILol=}I1t$k!hFqi)-v;mkru#Spq&tF@Rwq%- zEW?49SSwSSTjt(sZQ)$?g)g45$<$4jKK9Uz7i~uw7RpX1W$0&8NF90b$WdWDGRPv~ zfo`28X$+@Nj7ZtNcRzU9rN92+CEr2V!g4CFl57#STxaOW<4(Ks>O0C6kNp0}U%&Z| z8%jaPzw<+h>T|wYF|c&kJ@)wA=g)ZX&WEnM`Z|*Kz&9Rn)z!BkH%mBD?)1-}k^6S1 zwS3Dd8_&7%wxMF>Cr|lUaH~d1W{IY5w)y-;OLyFLS8}eQWR_(lsl@6*=qN*BAh3Vo zmNdRhbORY&a3Nv^UwSO;l}d?x4??jBKXHw3#-)x3apo2EWMJwe=ajcjRF-I?$(#N4 zFaKz_mVD=%-+0gaKY;0dWxG!$k1bvF{Y!s^%(Y6=&?=h`^!AOjL`kN34nt+-`gON; z@9jO(zJ-97&pB5iA0R4!Ttm8}Cj$>JKEPg(T$xC~aDe*&nm}d04V8xYoiq|Esj_%E1d7AX>$voj zZU$T)=Q`DrWq5YjH_l*lq~Ct>N00i%$9sxZKls$i!L0>54BPA@pP?kpIO43=Ya7b+Pl_V8sO=G;_wc5b)l@w)dPFDEP`%e1G zSzjM6qa2N$5c*eTylge{+VhBqaN~`}F^~QK*V20C8)wZ2-uJ!>F1+Zi#~e@; zISIGbrjuX2=k7_Uyq9$B@yGr0vR|Xl6Di=1T6xtjEQbUB>&U}gk>7sb9R)c!wit9; zM{&g&;_wu8xuX$_a2=;a4m(a$Nl7EAsIrAq7TH-sqb>;nlhVYIcG4SRkxiu|WSCP4 zfNidLPzYU8ha;5?9b(HW@9*g!SUG45L_s@ciRyW1bVy5ip1=_&0$VV7$~&bH@Wxr^ zV6HHfC^Ft*qdQY}Zn*Z&J$HXi(ul}+;21*?lPk~34-T!EwBbhEZnZ7N*_#e{{att6 zR20R)VE*w_K8<2M6?GgXY4nlzoH*<3uTUJn`5o`P{8v{Xr4UoF)KL%S#Ik522B`~A zaYRZXOT{1%sKJ)QInqSF7+Nu~YFR1?DJ|Ums$_CVB=aaOnpl-+Nv0r?gV}{y7Ai1? z>sTat)iQ$Ed4j7WvxwmBwZ>sKSClC5ofNKIzM^G}r$ilc$bpY7U2^wbk5FT5HQ0I2 zN$>vVcfYl=O-f0MRWnhY5^0hlyR0-%&W!x;bx|qh(9l5eY^7;xjESPC(P*r7w*L)# zYna#hq?1nK2WOrlA1m;ZhWz)ZvwygsBuR>*`p}0yYz?Y)(l|vGA_uuWV{VX+v{an8 zsL3K{9IFEWaj}QfYvB^IGUCj7NKI!_#8H&3C9kLR71fGiy&K9Q*vkcvbD5JXe{sr3 z=6veZUX$CEOGs6T>hw0Dc*phjOGdztg>#(_N8?i(%zyv0%Z_;Cfwm%wGwH)cx@(bO5(dGkqspL=~F6H}42cCW3sTDWxKzPs*m>^n}P zqP5ZFjRqedXcsW*jPPJ}2(feSINjLk(~-*PKBE$N9!g*h|8BR?5YFu+lGgVyKFrm( z-V7g6j;=gHnm3jk+Wq{4q9nSDz@9= zMJK%T!_B@;9$%FYEL|~m?6_1XO39!6^v{@|^MM8P@7`eAq^#M0#)rPN%Qm}Sb<^Kg z(u$Y9c*Y5DdvBtrE?GgRp7!yk>dDgRkb@6(R28mx$u_%Pe#tNUdK)Tfe(SrJee2?% z_eM!7qmQ3{CKko$I+!6MrH2LwCyX7ZBF#a|c-4^Ee#Tz6&$*e34mK$Og4}iIeOpYP zoN6^~%9O>67aLa|a>N^&u}bvBpZ)Bw=rpnts;y)C?d7vy_v*u{p%sT8zTau5pH>uf z&EM{y`Qq2KI;MYvahr{`i|@OU?3l~`c*{Zizv}jD{;+7#!ku>6Q_Fbg-S+4>kdtgW zp{Kp{pVlWEZLy;$^Bb?ZpL9%~y=R`N(>Cqgfcb?^I^no6iAv)5phJ(SC|=$+FPZuB z`>(m1T+!E%JsE_z&%XOeDReaMvdex$)TVsZp>I4)i@v7Za@?gy@g?*RN&+}_uO-fqR<*(q-(WfYjTw=%?)Mgin5)ngeW89dr)!1eC<{|c+UgVrcFsB zqO$O&>S?BkVMHks3Q?JBC?DK^zy1FFr$6O+`Mcl$`I{Gi|8KY7zWFAb{`rRMPe1tt zqVmkIe0jjE7#JA3`@#9wUAus)A`{-}2(iejN>D@{wT(*Brk2s{J8$1^>Sn+D`|R(0 z`&?>f8&2AI<>QZ+gSZ*y*X6rnu*Faf5aK5zAK&QbQ3`U_vU_0F66(;ioKOkV7|*DfO2WWvO0%N9M{&}xgVw~rbbIr{MrzL$tb zUt{Arb8kYH02fvPD4a!++V_9?^KAT>9k$(C*{T>Srf;(8Kx;viik)Y^?7Ugu&8yaf^Y45A2Tq)O z?d{~mey`f+%U}8GVBXnr+b!lldX8X!=?nk;g_c>>87yzR?Y3)g znj?f*HJE?ptaBDGo>||_m2C%apA?*mixS7&(D709Fnxj^i7v7Uik2mRhHz(-gC^=fB1!w zWOZ!uCMn6r_BP9^UpT$AO9rdruDkBL?jH})K;Fz6QS4e( z`MQ1g{rg|9DxJOT*H>Kdt?$(?sj?#HdYgT1hK-QInKCDz{osec=;>*^WalkHf)MHG zv`>BVyWhR=mYeUPvWydCoxxjC4-;wNMO?r6ou66gh;`NwPxkJj3orQ5_rG`XMYCp| z_x0~w{7qMtTI&S$d277>-HWYpl7I1Kz3{X!uR{uOygx1cEnJ-4m5$!`o)0kPjR6X} zaK`3Yso=vU$miG%A4? zk_1B32YHR4-eYYFTx^RZ$FxUZHFx%P58QL>jkhgZ+>#)EkZIKSx8Ggk?1Dv)-7|jU ziLr?9yYs%G7L`7G^D)Qyl@ISYrRn3e9mps+`NkssXAJrDvde$8@V@KbbNul?`^n`< zY#}=GN&l-Kk02OTKrE*DC<^@zH=`6R--X)w;gxc zbjtKMyy=Z3{mhrn++yNJi|=}nX~*HNv6WLj8~o*OH@C{dyH#&J>UG|%h@xh@r3g9T zS=nB?YT44okNtDkSG+um;+OCJsvB>*b^5f;{`99m{r49?j|`Lm?aDkUgdkmp(2kE~ zlK$u$pWAB6h9`aYGdpy^Sna z9(CZWZ@&E=tiz4icFP&_A6ssS_I&ZAdvE#kq86g%?aZh zV;{Z$K__jgD%DI%#CL?CII_uR8*g#t6@U8dXTAUWKT^)yTC8jVDS9wJ$ zt;m>S$!qrAGmAHR?}yI3;-S>Fu{vuLh6mUmz49)V_J4lxtxJD&(Jy}TEpi2lR}GLXR~Ibvb)I#6d24OJmtPBi zYh5A5_1&MbMyWCY_V~uRUn5UNktc}Wd(DLpmQYrzmE30cmy;Y|yxn13w2*7X9!0M} z!ciqKQQ(#Lkrz6SOIxC%@Ch%@!vK+Xunq$C>lOa?yG3|I|C3SJuff{k>&DSr!o`jpq3N zX7cF#c@rjY3c4;~gy{@*+JNc;P7~$^&dcV?I9W`bHtE7|ee1_RzVyupzx>A+Uv&S% zMc@4CM)?f&l--SpjXH_SI^yPmzVpfL1OtE z58D3|U;5$uKfa4KIbcO1lEQLHK9P#lZE2@$ro&eHj z+HNghxMazpM<2al;Xfyg+33il-wcW>kq)*8lSabXWIC35i|OMARxSVL#oy`4#((EK z7rpVt(<2r4^z`H<^)V{hiUkV}JN&2xk3HHLyUDSKpA@H>dV2;32bDy;x38yn-aYfg zxL%X0P&8m1B|vnjbew3N#>fGy{&wY`cYOH{Z3_dtY!3qOC{2gjMMSjQE<4ULLzjR5 z5|w0S32y}!L$2~Z(d)T92}^()mA+dH4BL{t5+V*<2F$-LypMInrTlA*#%JBX^=#vB z2}uQfcaYo%-~VB9lqOj>J&Rzy5m>6RT*AEu;-|2P8u`B>*MZTKG@X~6^``SG@8qoj zPVvP55x+u70!9tnWX{;wxd>JlKbo543=umoQi_{Ec!(0}A4f@(`Zg>mqn=`G3DW?M{s@S#T=aZH0PrG;=sOf=Lod%o;7XMg#Osgrwqr*5Ph zjkCUX9^h3%crTW!k+p^f5~2qmexx~eY=2Mh%U3K-dQys&OLR|j0=kLmEP3R?aT{$C_Y)Go0<=j;O2a)fRQ3x*yRR{7 zM!$?o01^PWd0;JM`0ZMmVnKq^oV0gr+r|aaJC54_r{DhSwKv>S=-$Ftc{Pw=U9C$R zlaAl`^iw}@z$~8{gU&)-T>n&d+{0Z_?gK@`9FHRg6p7blQAe37*8CefcgdqsOtVPzn-KlJ@s?{ z{qFyI+hv#j&H(j5+&h2X&#(CH-0N=MW0yTw4s=9<8ToA|9RG*kUJ=9VQ`sVyov186a_2puw+cr-nYPii-qc)o_3w+7oyQgq%)02(eGWLNcjM@uhpzkm6~7aLe*TL; z+%$g?r5adiLP!-kSKagA-BY*TIvq3a(FF_V%$Z}Y)mnEttr*2k08+QWWHN#H1Y{(_ zF-akaKpLvi6KmUP^V&Qjt1{K}rUMjip|Z5EsheXPV-u-oo%=N+mw;>+6O5$zxOX0V z&Nsex=bd-5$1jo8QSkrzDSELb;PDyyySPk{fmQ- zsOOTO{(8IJU$XP=TQ9l$%K3AzAxWox?h8j9yq_YP_t?U0!wn}i;taq^{)7*n^oPqY zO^CDCeMylTEc4#p=)wDLPkXc8@#7ag^4RRZ{jHhwRIW-r*`W^bx8O<=SUM0Lcib^& zo_o#%^B)>JefoAYb~xkHpX-kR>~9r4y7cj}6DKPbJ+@@|@2~pv%BrBM%KFAdx-o6a zbRpE)U;Rp!0DOb9@hOoEJ{QSW(P2TmRLoL`)K-mH&( z;3EqVN}yr~mlH5iMFZI1tX{1(5rh?2k}=x#sIxx3{2``1LH|Z+Y@@ z6kCptef1k>l_ly7jRT#cF@~)@c&1I+e&B{VkKXW)`8QlM?}lq0ntSa-H_Ulx?wkka z&c5%4*$><>`{C>UzToETiIjtdM%GqZ%e2t|*W#1CthFV-EJOSTFBd!Q5Ml`YiSRx0 z11FMK&$)iq#ozzlr9Zvz;YVFzDQa|b@=sDB0}i4|aan<-cZ+8vQmZfftqphIJ~V z?=cp)Kz#A>!A>Jez_ColsUQEydr$h%;80Og`O>{-COVQro&NbVDxyQ)a>$i`{Nb(4= zyKI;=Qr#z1-%Zvm1Ofe%fSP*xRLuc$hC zoHc*-i=Ui$!ZG8=joEzD&91!i%AWoPjT!foU;N_DM;w&t-dpatbE~blG+f0VH+9># zg;cNH`?WevvflK-HyyCujIFXbb5+@BP+qPgl3AlSPJLMoa+pmM@E?#8>wx4Q8)J*k zfKnCi7e+-&K<6nzha+j(${|P%P?3<) zCr&;2m}A~XN%Vu?{OZS-elLl~_B6(S=X*ccarfOen6k;n6E-^N4F~S9{kHvMdq~i- zR*Nd%5b0m8`s;>9+@-Q~b}%2{`ctMb@TUYcQUpW$nW!uwlpy`Ly`&Pyr0D}QjwH#gdF+-WC&1UrS41n&9_RsqP5;e(&f zhk(Bo_I%g7-gUtR7d){MBhRzG`+I<=mqM#ZM7HB}BEd-j5Cl#*S5ZirSG0WoP2Zh9 zy$|{F+^>PW1)O1k-wHC3_m4i(p7N5n5RJR>_S=i1h)8M`C^|+qu>P!66hewyZn^{L=BqAJ36f=cM??1s9zB(T|0U@*pn|OL(swedpWX?#p@)I_MxTq)OC<7hkMM zWJ*=0IO^D=g{IWU7tNY={PD*jxLg&B7q2|vtsni(?=IUYEeO!leHF4q$rM!*N3er1 zXuVgmVhV~EmxF8C~?97H=-!KYxoSoX3-%bTO|^> zV%a+UzyscW`Z+JzZF^o`80}HYP7)MZ9zhITGW5bHkMNk@k*? zs-2`!?u?h2a+Fkqq@LIYE?OIVZGtPUnKsc%|&5q+9dn?Y7JAzrXeN4K|(xcpX9Bc2X5)VQGz# zT645eNF%j`my)EfENKKInLxlN?4-k8!jGjjIlzm05unyBS?xW8e%Tf&pNovNO1u@$ zkdjqdMR5uSpIY@+l`mqU6a`RJKX2ZBZ-4vS|8~{2TgiwG# zL&mf9UaAP>L#3e@hBX3G6~)>C(!P`=Op*B(RpY1+gjg2k5ZrN?cet``1-F=FGmA+Z znnqLGz*^Jfs!o-q$@@MhGQ^Htf(7QlcxAEs_ngsIT=vgW%_ipjZV@e53{jms8~}AE=axlyt@lxP71G z#cnMh(zfu7Ii4759~g3L^=JsoU#6P}pu(}oz2nxKZaHVxc^izI@YvF&uRrjBZD(M$ zbjKaH{QT#a9dgLQmC0er#h@=Ftv1?t%-fE5%ZbOn_j^D1l4e515Ecf7mIFIilr{NI zV9_a(d12BhCV_0kc2Ptzxzg%L>WC`Sj&xia8OIqstHy~)dQa_E+3e{BeUJ6Qr+1Tan&5N_paN6b`{)YjI9wAOIIYL##UDnqwg<72?zvEFcZ{S9nb{`T3{LHd?P^w z%o?QUFa(lE1S;ZPN*|}*fD|Ss53--ac;KUZFI=_5=9}KJcoEz;6!$j!ZoT`iNgHoK zR7S8!SSiJs=W&e-Yv?i%$^;^q=k#+3CKs!fU~Gj&-ILrC6p^sjhx}mh6C&3N@`z=u zqOcGJihF5&tmIyM?y=XNdr-BKk~9@;2jD7_JfH-;s=)c|eGGIo5C}4&JkF2p!O?~=jHH-d zG{#ym9V(3yw#wvDtQjIwOXTf(7|t-w9qCwWHROCG1$qhGBC;&YC{7FGr3M^Uj9g)O z2CK>{={MbU%74A}=!4#L*e@>oB~aq?LT5Z5DG_T8ymy>O7Ogz)Nm+{8S5_p-+m2S% zXHNT6t5v*wubB{~@m?m8kgwQluUG83ce`*IB6o>5c@ifTS>-@aBx14_fB;bxbt(s( zNF}_j$V)Ahqbg$1G8KtZpa#H_5hOFDD#M}$FePFv!ZXS|h#G~7q6Q@3JaCJUJy!ya zOT*IuoI1|^s+?=c#^B&!Z%=Q;v~(a#DXrFuQk@Cv6fREK3AYz4_9ban6jfeTY1V5PFhi}m{CRStXT7fX)H@3yhAF5|I_acy&pr1k zjICg}tk?eFDi=O4%4X6G1X9jBDRfAnBIk7+QoT*2iOLr9DApB(pg9+gz$94up(4_P zbPTnKHdj_e5;=Rr;es`8)O!(dE6TF0szeA8$8abW&d2~b5fldltfJo{crTqRwbV0b zZa;J8_6T1qBuD1$PSS{W*l9->(IuDsh)mTpW*kwJw~I7|26^wX2fzER2Mg&O70wr$ zL)g54_E94yVOpI$&3c@3k)$-$olcR&iSpE&fa*)t*Qxlv=m{X9ti?Ll7)jqi?x`v!azl=)Y`7D7j%AsN~ zDzL`KBrcqaWWdjaH(193Qi}{?3`$~&E@DBxj1*inz~445jTq2egZ1t9`45$mFcc>M zB!~N>f#_MoqQMDE34JA58ZJ?AC$%-g&GPotNt z;2n%c7QXrH>7SB~an4enn>5Q%gkHxqqynfMX)lD#jI*&+xPDavQ8ZEmRUSvFh@{2J zAEr{G!GG<_3T_?=m~jJ8Zb{ZiS4r5KN|cHBRz$dp@hA|9;+PqcsNJb_tRn!d7Vs|w zu5l#tv70H0GwviwZ*O>}NW`-aY}is(1`DAEvc0GP2yKKIZGln)78+c6{2?6r^#89J z(czRKhVoR#y4Fx3qjZ2b7~WhY z|KM24lFV2W3FhoUi3%zQy>%egbR;B{6qr3>*E@DavEdB26!<)a08AcI8#xLvdE{GQY5AIB|0dWPaP;7HM$*e zrU9-6Rk`gpGhV#?&RD0io=D*0;wBl2BC0G9c$MJcj~>nrI@HX>X@*p-NNP}~R#lv3 z&Uy*WA)J@SyM*zSt#OHta$_T{T!keZAljq?_bLFd1(%;AN~pXct)2BuD27&`-F!Q& z0okD%v^#4z{$=#vs03bU5`Z>mKp!lw4V)4o-@@6l)G;V|0Wcp?t8B+{s=!suhX|_L zov0zLah{oAG^XO922jvX5@}14DiF9Jmxr~56)3?&d6kt4xRh{gzPmB@(FNG>iB>}QnfgDo6G}meBT_H$DI25*3*3$!{55a8YStDGh ze4~-JJ2;PSXjc`I*oMR0V8L+|9t6_UmGzA08{vy5pG3fhtbCOUg}k6RuF4{cW5vXJ zR8^FMi`#h9PzdB<2*fES9)i;e1(G~%g11|PBRpxsD>zt{%_PA?sh#B4dB`Bf8D@l|yR%z6;1+to$(}MO|AN(yr;;uXIy6W#&@qy?!!(gfiBM!f$ z0%4+OzG}zz1-N~f;0AGVtP>-pz{wy0&}s}g*rn9Lkb2_(0Gb~-lA;KrbB-4{Tn@^J z5Ue0rhlX}AZ8jPtgaRo61K)$17PJIPd7WwW*W6ZK0`s^Yne1#e1KyqV`9aF1!>I=5HnTOh)5(A zB2`W!DA`pP44}k$mB_x~fE5_>BI~VErHtT36R~tAM#g4`r~_Y#fY~up;XNmxNk~az zbvbNo$&itTbusSAfi%Z4mmv&`PgN>0SO|s? zkupNM2BQZlT+7k_WA8n{C9A6J@qN+_72fMa2fFD%XqqS>AQB`fhQDbT)0jscQIcT> z%vlEmMrdh7Q2(Q2LNUUVZh| zt5YW845}QCLsu3kSqj+6hRGmmQcG8-n?3hlY##(VQDX3 zMgYj5p`wr?$Au8sE+{xoAVnU6qr_2zmkUB_Ol6)Uhdb8?eU34L@1;Xy=yj^txg-J&x2MFw|5L1|i`U*IHHH;xC%4?Lwp; z%~LSJFnZRclM+={dF_leN#r!dJlE1m=Ki9XBszHT4iY8_>Vica_8==vd@x~wCzA!> zJmJo+5JrjYO}HC)Iga^2k+ZIlD1J!RdadA+D!XlPO|`NRTk@WZj{z7ku_q|Zw!FL{ zk78a{vs=Dvd5YjA8rd$93t&W`Y;S+|eVm&<&YS!ie?VxA`#hIM$yRn%VwVDZZ}z1`f(y`A=#^*=sDId3)#3W&s4mA)Mxfp< z954zjTLdh*>a;~_1x9s~WL4!nKnt!<3ea#Pq(B}RP@;)=GFS(-!~lpM1&};gG$uyH z;3_35g$uX@34OQ_5oSF3fHPpg3sFxT!EtH_WR;=@BMDW-(v)1x3MuS`BUUAKz=lW+ zaq&U|m0b^&SJ2F(#s^<4r+9c7I<_mxr?_X(>;#l`(36iC#bN4dE7Ffp%~|NVn7DiP3GwEl-?s$|;K% zFZSL)99cdFA7k-EA+~G#;QTXBIPscmue$f{d#>raV^26P=2337dF;TzS`4###4dIk zqwNNc^2fEIC)p+HO1ORjwlR*Pm*3B1WTP0zII_Ic8!Y{haKiv<7PjPg*BRIIb@(@Wxd|kwfApY6NmJ zS@aX)E`j;fbLS%#OUSI_+s6k*+(A*w&K``g5U9suRXMQZl{c_4`@MXYh%Si{ZxbPn z7AjZ;<_gAVKqZ_!g2|poa4e>AbSZ^IDFQjI zxU-JxO%hfHEkum+j_u@;-vAl))oV$~fDofN*d$d+-fNoio z27W%@ZrijDhaV?O#3&$6ECXAu3K9WwA(9L|fkz4g0}OpvBL}40t^$C_98%hDa<*lBJGvDEju{|w_uBcOwC(X!eROa-w2X)&YKWZhQXP1S>FpejAq`htB#cRj3PnzjbP(a- z^B1NntrS|Dl+e5dLChZ!R&W+$iu{8o9i9ZEk_2CM1Y2kUV~wh34NTJU_ViJ~n3=Zc zO~iH&xn-nr@mDx1dD-*LZQXjOcK^-t&&Hocp=AyzR}T zbS{~{iR2Ba!AQp`9_(d;ehQXgJurvsa8fAMtRY7U{@8?Fzd_Y%I3onyWf0)=!efn# z>K2qpC%I}hDkNyt>jc4o1c4ypa8=Z6kb!}Zs~6kgQrV#(*L76uvAwq6KoeU|Sl|g| z0UjL!FZ^*`1}+JQkiJ&NA`1t!t!}5C*XtbBsq)0Y)6sV?1x=Tohy zvOxw?#z+%M0_fgO{zoQec4iIPEiRh|_Icog&wuKW4HA1oJLX7Q9}-yV1kM%kPL=NN9|t z9!pSTm{x_rp=Xx8LRlGCc7~H=fFQnOlN@Aq92XOMjG2%C<`XfhfNMl_k!K$k*gRf)=np>7m&Kg;L&{J@(3bqJEQCdGLq$2_CII~QYIl2M+)&RK3_yU zIb^CSppFrsRTV5HiFSfIU6;e5hTJh76Cv4a2eaYYU}C1rbHhQW>anKzf3{md)RN!z zr*Qxv7ctmYhsDFTylsDhs;OhZk3oFTxyAlAj{A;%-QUwr$6Gt9r?sQo0~2HM;C}nt z-;(fi=gxU3u!YNIw^?kJpucmYXNNOmqO#K+oU+TE8}FG(qbT?6$MNF&#@e30xQlQm z_t+FLI3pxzL4<-3ZfC^P?a)A;Ce_zZ|A(CD?>=`P#_UjDe(Ar>-*e$tF8IbO@;pc1bES~TK28+WP_gQU?*cUE2MzL%bKCbo2c;&lWHShxLeNsw znl#cOqlGO=!EKVlJ18l%VJlFU49SA2KH!bOMu;}CYY2+Xaoq_QQjc_pe8o1*7ZAk- zLW*!lcw+-;8(H2WXCgYZ94dq|ou}?F2~`6iiBx`G#s}u~N2)BMa2&ftIU)`@3I`;? zi(>@Zlt$F7a#WT`j8d99Ee;7vp;%I-sc?cy zsA;IuSICOw^@Zd=T(48!pw0wd%TShJ)LsF|}~kOsR(uicPIe8#dM^4JGr#)Pf307?e$v3ep@$yIk!P4283R32jT0Q-cQLtOhwg=w7(T%c-~e{dxNvS+Pn|@R@G3=Uj--(mwKwlJy1HgPq2QE# z_6tuSovz=@*VT?e^E~miv zPix}ugaw{d7Knx01B*BCh4YfvP$;O_wEyn={&`JBGAHl9|HE(p&5Mo$Cw zWGQQrxcIx5yyg`rQn`NJ^4s@0=GmJzR@zTL^w2{t{rT4?PaaS#(FGaEvO_{#cjc8w zyx@hDHpooAXX&4By!4y@aPp}w1w~8>b!qbFob=oueD9mRY`(|yUBH|kgNYowVd4=m zEOHPqTqAo8?wNME`iDPS|EvF6f9qcop#}#AAZ=Q-)5Ol%eb03AT#LM?uD8~e)>f5> z#q6-u=8SX{zhSX0n@g`0_FM(obuu_gk;E6Y2;RBY@bK`ebtAKPoj+shbRUXU8cE=LX3LE%s`XP$uUj09*pv) zB2j8D4e4L}{K~_RIcCR4T)DLDRzcgWo{f%< zmW7=@b7pI_`1k+#*-=M5eYZK&R<2x;YoH>5rf5l`kYor=&eiCIs+1yElG?)?*4}%U z9UT_BRm+Hcp;w*rF_PWM zp{={+6=%L<%cL2v_}GUjNvBPoDwKf(v_vCHoZsB)z$FdBRU}rT#3M)fcr6>7668s` zN<*(N(bAPPbKbnQo7Z;HP80mFB8`GQ(DKP8mrO*e4^&tLb%XChyG3LaLQR`rUVr`m zhdg!Zf8X@{XC1xtzFP*Hv#OSuluae_C6$Tc3MEwl=9+j+SYW~eY=JoX!uiA*B@HF7 zyzci09J>E4zq$GOCq8}oy*Ev5Hk-An-~Po<4ms{{r_sM9(YocUjywL@KmF-X4?6VN zw4x7v=!<0u-N*i75ram1*|Imj?zPJ|4Ce!l70Wl?{JY=%?dxCrw}1a~P~`Q%)z|*{ zc`rQa+;bQI^;5`W=njs{PwLNBEQupaXLFSzR^^F0#mcXL>&EYVXOFU&KRn#1)!oJ| z!uncjy2Wt!o;4dO_sR$arYyMW5L{rTL7tec@=gY3aY<`))x%rXDIgUukW?fB!g)&A zrqMG;%KA`uOL41p?MB+(H&k@pjR$tUzWLVMZ~BADozQaQaFNv-n^AaICtf;RRH7B& zZ)>0&wl6rsQYWpP?s#8GCyY=}an!l?*SG!GuTdzoaxKhA!3ntq!ie5VS0ZUWLvTh_ zL*O1M>%)BtMGKxPNZj=M+pb;TY}Q9BVvGpud0AjDp(cWqu2aiQw__o*CKqir7K|B)9Y43xR-m;1b#qcO??!i?&WlN;{}=!3|3ylc zwf1dqd)xWvpI?^cLrFGlJ?xHcFKEvE-Tb}om}8D%yCRE$i_!!N70JZGrw2D~w{j%1 z;88FG5$@=4M(;wtY7GDe=l}p907*naROuuM1C&o_V0n_aw-hlmV(Xbb@;f+-(FmCp z79lq;GGvWFjwNAF{@g!PQC-Z$J6-=T;-@ zjVvG~r#)@%<_-lw0Nx|NN)4mLgw3TkHs(OUV`iz-5~+5qOrw3Ik$IA(P#GE{(-s5* zj!7Y*Rh|31tW;8i57rRrq)A?(p1HRgfE9T0@B{aoHFeVR)%PtL+*OeA@Q+EU5=#(e z23eBBB2Tyg#v|Y6i7h59u+;*wBlJdL3lN1#TM5d_0Fw0)2QHd3wXuBFs`}nfwdG_Z zr*bk)ky8<@dgEyy`^4vdboimi0vQPXv5&ulRRO5rOgTkDDRJMbd#6m9Vsyh0?LTMg zp>vPHY7BF0%lX#J-|+g+pMSyJ8H0M9OMmO>J+9uxzyzvIhk%7ELbM=PX#tKY#v7rj zwRz*0FTC?x-#-u~+X|3^iWaIUYawlsOKq)d%EE`rJ47*}^COa%qdx?Ci@k)wAbioE zGa*@vcq8s#8P#sEMlZ^#LeS`z6ihFV(GxhgOi5BXjQWWkxXv;mL#G8{&9aIN9u(z) zen$b$7stRTbiofV+3|u>Cze?*h2t}TSBaxDUQi8THH0DgLmp?n56JZ5GmL2fK~k%m zNlh&rg3)`t+2AaBYDq4MOl?==9bk4VSq~1;pDwEFLLI(la?R>tZMIArJo-&anJfw2(CC{8OIOdi=S_*z4V5tVpns;b`Jt6?z{Bwm|ns-|v5fTMQl&pfbp0 zK*ta0d=LEp0bl>m{~Ze^?kxQ+L38KMIsRG4Yh!G|)j$CQAU}#oFsvJCbW$g^vRiqe z3l%(sM!Yx9o5~yKlr#hF;Si7wn+Fduh$M~fuJIw-N47_#<+CU4ez&tMItVI`j9hxw znZN$f`y0MctzoKcO*eg|sOtuGmXsBBI!P_JRiTVW^&u@;%MJYi%>*~JT6M?L*)!)# zQA{6W>!vaDQla1z!}t{luUPy7J4KuEvgPk)&@W7eWY3l(DY zDy9daN|!HS2wuoPvPCAgn6SVT*#hYCJV}E(!#sj0FJ!Fs0~usbZ@%R()28pbXwgC; zLsfdCbBQEw)gmsSwgH9AoRkcj^4O&!&g4{NgW8e zcknGFt_md0*Dn0(A%`CR>Q_G-wJIL}b`HHHL_nGe5^BqcH0p;ocx{0ndY z@n!p&WJ;@5^FG0XQ$ni0ENls7gG{AK2uW}zz+q!TNZ_z?kUu47f=j*2T!_y!SkrrA z`_zZjV}mrt>RJ$ zH8L2)+^QYCh|&R70a!Zt5?KrqAP=SNMDW^EW_@OTylUes<10Ql2!$Evb+9^=sk8N} zn$j(%w~HCAYG%8bxn*?jXlKsmkzLuck{FM)euD##(krw~!f%bY27WwF>==DDwkBG;8_P_cz>TgAfhf_{D zCF-z0qW;$QZ*QGNfvOOyvV^!4pgPBt5l`%C3xi#1gB0f8`_?SK`@ZGNSKocln!E2_ zf7kML%a?Dw`|hE;?%rIL$;OSP64Qukj#9Xev?$95)<1eA+hAeA^&?8CSD`9<iH;>Byy!!fKyYauTz4UYEe16r6HdQ1lwD^Q%8A_cdR*5a$4(YtGbi{!NPEW)~ z&slu+jW;%xNUJuAvGamU!o=X6I>#~e`!%^oGS9^K6Bc+PSpW-{tpm40-{%}9krEdD z-6Id&eY$Rb_?)j?b;C`W*2WHNIdbT+2T3`tIk4-dwX4gby~o1+c9}&|7XNzjdCf*+ zAenys%`00LlY|UiNy@vFMEaMzZ(nxfzfV!(Yv28TOKFdiz|Hk{tUTvqpFQiH|L7p( zMFFXBTL=BeouvgKYY0<5lu5l22HbI@c=)sAN2%zn{>*1rT=H+Thr51s)Osf|+b4-P zMx;h1S|?IM^O+IARs$gJ;DJ1VW_xg9yWRw0y|h;O-cx#uCyN;(@J5v)D`k8FWuOHx zd1yOcJ+kLCC!3pql=H2x;KGMQcq4%ojqr)!SLyM2{Pq!F84UH#%}U^*2#LIqpxqF% zI)U@ihNTdSrO+cI8k(Yg^WFb)Af?fJ!tWKWA%IMgeI;4*SHb@9FF{&fCa(8H0U*qV ztl}l{2F6t0OV2>Be?b1rW%0-qMcszF&bwBv5oUO|ImcbRgqjK5oE*CY4~zY7m}euC z{(if8Z2j|S@V6p(+DZCbT4{$gVAU&&Xjwc(L_IlR6nLfuX;7+ez8rn`k{L{pUZ(EHFTl}Xjtecqn?1}M$d7{S432Qq_UwCJH$n`X`)^5kW5 z<>$}a5=@y55~ZV)XNa0EYeb)2;hG_Lk&wtMD`&_1ULSE-Om5JFjx@F4b znIHbZM~VEQhaJ4zv>Bu8x6teXz$T6^r$U$Ih)iS=92HesYh3rUO9ywIf9z2wt+;Q~ z9&?OR!oqLQ>u%|d%D^LdG}_MgSbbuf2@CumSzs)rzV-Nh9mN8Pgx6p9%Yj*Y9dpbJ z?p-;wV78Xp3$3oX;_5@5xvy8G+zRi9Zu-OT)@`8qbHuCPbn>fDI{l!7pE26*Lh}fC z3t|1T^huVJ3E#ftdtX?)VUMT1s1oU$U;KP3+iyAj-LHAW8Al(nh}{vGIv?j0E|gk> zDudKY_S5@=7g+aN8W~=H-i52L_{H>*R?Su@mYdYb1`h!%l*G_Nc?7k*wWUf-KhEP- z4DDb}fQO_1`PO^3jdx=;TfBhxX6(0P&K`!heay`s9h8V>%=L(`(Qh<%^s$}u=a@jS zu4Rr9A0T>G@GALQ9{H7(fS?^8AB>=&5h=VDh7p&9kUb9UMNk6l{+`+6B|(TN9(L?3 z8PIb82X%xL82a##agHvWUf@ghazC+Y4B+x*z^#N+8N+hyJ&S?2@v%2t7dtcs`3$^$ z-w}SK(Wv&Ef9zKmQ#~gLVIYYi+ce?F#b5YipCo~{F=kMs4ay%0I`!;OcX`Yxzd9sGlfFEuq|r~TY0S% zGrD%*hQV#iRd+yh<;5;HGO6?FU6r47?w8T%p!fy`0p;o$=Y0txhDAzgjuj43rz1v( zTbv>*sj^0)G@6CL98uSz)VEYw%1S$x(gAXGL6?>9fO{6C5lN?A6t*H!>y(uhs?it} z-C^x5DmIvU>P5A2GwnI`po0!{lw$D`+QVBmY-(@bW50Q1x;n{*e6n!Y1!uqK9pC%P zxo3X-tQJcB*erA*{^GhurSR}i@a1=)T}*8HM7Dr}<`X$^72sFLc%r3AlM$CJ+->SV zzxQq5{mPf#`>}V7Slwh;V; zo?@@=i^Jj=i{_!>4d| z`NTpQRw{NvZkLfjyXfVb5el+Jywzhs_vUr}({lw0BtKgGrVo`OV0yS$1VeiJ--^u` z$w8|EMd2`u^ zWrNA{x(lT*3pmeq$iIb!6c(i+dQ%zdvD1re>3vdbXJmcZfYK;+3a2F5ydoK-X&9BZ zT+4f3OB(eI>o&An?+a&ZS(-tHfwD}R8M|Zyjh|osqnEt$73)_GU2@qkE`Q&9C+{+S z_3}Sm@sl6D;$=tw>-R5Rziu5CDN3nYt<^11pV7N+)g_evD5zUaYc|QcISb~!|LGs!eB99oEL(j~Hn~ad5iexIkYLhagBOZO?)lBHuGS_y?1-bKpkH2b^$`ah zK*O89@P*Hw_k&;S49mv`ufMv5$R5vMd%!D8)dIEvRALqbbm2>}T&jdMx;VhflL^a5VaM41 z5nsX9!p;D;Bu+|3UOTeX%M#{7gl zL~z7;fcVa{%Pd+6#|JMgVp5G{H6#Jxf6f5KJ!c1Gv?{)jh!!P<)kas2raFDfe*2t% z9;L>bv?>vFj^$f8rWpGi#B3k-Mm}b~*y*Q;!p|Q5Mj|@>^wY8QvvwQ&tsUMTJqkPT zbc)kYI}J6iP{$l@WoXxq;YlT&D|VebYyN_H^XJdqeg53}3wE8q`;2)Dc3H4smw5}O zO`fL8YKTcvR1_WODvMoU&)ffCfX~|U(8}XJ)LZ$`@2UkK4CDYY=zMbK`#poAuep9&o07tdyuL!Z){N;RwT7+Lb)Fbyz3VvJ_O9by z;mV@w7JzI_7WW~sA*t}pENSgTVx4x6{7kGfHb@^qiGy4hKb3dN^`6>0ewNzDC-`K- zY2nQSUS¨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 W(P=HGmj~wn0000` as a `string` type, to ensure Tailscale contacts the correct control server. - -You can set these using the Windows Registry Editor: - -![windows-registry](./images/windows-registry.png) - -Or via the following Powershell commands (right click Powershell icon and select "Run as administrator"): - -``` -New-Item -Path "HKLM:\SOFTWARE\Tailscale IPN" -New-ItemProperty -Path 'HKLM:\Software\Tailscale IPN' -Name UnattendedMode -PropertyType String -Value always -New-ItemProperty -Path 'HKLM:\Software\Tailscale IPN' -Name LoginURL -PropertyType String -Value https://YOUR-HEADSCALE-URL -``` - -The Tailscale Windows client has been observed to reset its configuration on logout/reboot and these two keys [resolves that issue](https://github.com/tailscale/tailscale/issues/2798). - -For a guide on how to edit registry keys, [check out Computer Hope](https://www.computerhope.com/issues/ch001348.htm). - ## Installation Download the [Official Windows Client](https://tailscale.com/download/windows) and install it. -When the installation has finished, start Tailscale and log in (you might have to click the icon in the system tray). +## Configuring the headscale URL -The log in should open a browser Window and direct you to your `headscale` instance. +!!! info "Instructions on your headscale instance" + + An endpoint with information on how to connect your Windows device + is also available at `/windows` on your running instance. + +Open a Command Prompt or Powershell and use Tailscale's login command to connect with your headscale instance (e.g +`https://headscale.example.com`): + +``` +tailscale login --login-server +``` + +Follow the instructions in the opened browser window to finish the configuration. ## Troubleshooting +### Unattended mode + +By default, Tailscale's Windows client is only running when the user is logged in. If you want to keep Tailscale running +all the time, please enable "Unattended mode": + +- Click on the Tailscale tray icon and select `Preferences` +- Enable `Run unattended` +- Confirm the "Unattended mode" message + +See also [Keep Tailscale running when I'm not logged in to my computer](https://tailscale.com/kb/1088/run-unattended) + +### Failing node registration + If you are seeing repeated messages like: ``` @@ -53,8 +55,7 @@ This typically means that the registry keys above was not set appropriately. To reset and try again, it is important to do the following: -1. Ensure the registry keys from the previous guide is correctly set. -2. Shut down the Tailscale service (or the client running in the tray) -3. Delete Tailscale Application data folder, located at `C:\Users\\AppData\Local\Tailscale` and try to connect again. -4. Ensure the Windows node is deleted from headscale (to ensure fresh setup) -5. Start Tailscale on the windows machine and retry the login. +1. Shut down the Tailscale service (or the client running in the tray) +2. Delete Tailscale Application data folder, located at `C:\Users\\AppData\Local\Tailscale` and try to connect again. +3. Ensure the Windows node is deleted from headscale (to ensure fresh setup) +4. Start Tailscale on the Windows machine and retry the login. diff --git a/hscontrol/app.go b/hscontrol/app.go index 087d2f2a..1732135a 100644 --- a/hscontrol/app.go +++ b/hscontrol/app.go @@ -437,8 +437,6 @@ func (h *Headscale) createRouter(grpcMux *grpcRuntime.ServeMux) *mux.Router { router.HandleFunc("/apple/{platform}", h.ApplePlatformConfig). Methods(http.MethodGet) router.HandleFunc("/windows", h.WindowsConfigMessage).Methods(http.MethodGet) - router.HandleFunc("/windows/tailscale.reg", h.WindowsRegConfig). - Methods(http.MethodGet) // TODO(kristoffer): move swagger into a package router.HandleFunc("/swagger", headscale.SwaggerUI).Methods(http.MethodGet) diff --git a/hscontrol/platform_config.go b/hscontrol/platform_config.go index 0404f546..9844a606 100644 --- a/hscontrol/platform_config.go +++ b/hscontrol/platform_config.go @@ -59,46 +59,6 @@ func (h *Headscale) WindowsConfigMessage( } } -// WindowsRegConfig generates and serves a .reg file configured with the Headscale server address. -func (h *Headscale) WindowsRegConfig( - writer http.ResponseWriter, - req *http.Request, -) { - config := WindowsRegistryConfig{ - URL: h.cfg.ServerURL, - } - - var content bytes.Buffer - if err := windowsRegTemplate.Execute(&content, config); err != nil { - log.Error(). - Str("handler", "WindowsRegConfig"). - Err(err). - Msg("Could not render Apple macOS template") - - writer.Header().Set("Content-Type", "text/plain; charset=utf-8") - writer.WriteHeader(http.StatusInternalServerError) - _, err := writer.Write([]byte("Could not render Windows registry template")) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Failed to write response") - } - - return - } - - writer.Header().Set("Content-Type", "text/x-ms-regedit; charset=utf-8") - writer.WriteHeader(http.StatusOK) - _, err := writer.Write(content.Bytes()) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Failed to write response") - } -} - // AppleConfigMessage shows a simple message in the browser to point the user to the iOS/MacOS profile and instructions for how to install it. func (h *Headscale) AppleConfigMessage( writer http.ResponseWriter, @@ -305,10 +265,6 @@ func (h *Headscale) ApplePlatformConfig( } } -type WindowsRegistryConfig struct { - URL string -} - type AppleMobileConfig struct { UUID uuid.UUID URL string @@ -320,14 +276,6 @@ type AppleMobilePlatformConfig struct { URL string } -var windowsRegTemplate = textTemplate.Must( - textTemplate.New("windowsconfig").Parse(`Windows Registry Editor Version 5.00 - -[HKEY_LOCAL_MACHINE\SOFTWARE\Tailscale IPN] -"UnattendedMode"="always" -"LoginURL"="{{.URL}}" -`)) - var commonTemplate = textTemplate.Must( textTemplate.New("mobileconfig").Parse(` diff --git a/hscontrol/templates/windows.html b/hscontrol/templates/windows.html index c590494f..34aaa0ae 100644 --- a/hscontrol/templates/windows.html +++ b/hscontrol/templates/windows.html @@ -25,75 +25,21 @@

headscale: Windows configuration

-

Recent Tailscale versions (1.34.0 and higher)

- Tailscale added Fast User Switching in version 1.34 and you can now use - the new login command to connect to one or more headscale (and Tailscale) - servers. The previously used profiles does not have an effect anymore. -

-

Use Tailscale's login command to add your profile:

-
tailscale login --login-server {{.URL}}
- -

Windows registry configuration (1.32.0 and lower)

-

- This page provides Windows registry information for the official Windows - Tailscale client. -

- -

-

- The registry file will configure Tailscale to use {{.URL}} as - its control server. -

- -

-

Caution

-

- You should always download and inspect the registry file before installing - it: -

-
curl {{.URL}}/windows/tailscale.reg
- -

Installation

-

- Headscale can be set to the default server by running the registry file: -

- -

- Windows registry fileTailscale for Windows + and install it.

-
    -
  1. Download the registry file, then run it
  2. -
  3. Follow the prompts
  4. -
  5. Install and run the official windows Tailscale client
  6. -
  7. - When the installation has finished, start Tailscale, and log in by - clicking the icon in the system tray -
  8. -
-

Or using REG:

- Open command prompt with Administrator rights. Issue the following - commands to add the required registry entries: + Open a Command Prompt or Powershell and use Tailscale's login command to + connect with headscale:

-
-    REG ADD "HKLM\Software\Tailscale IPN" /v UnattendedMode /t REG_SZ /d always
-      REG ADD "HKLM\Software\Tailscale IPN" /v LoginURL /t REG_SZ /d "{{.URL}}"
-  
-

Or using Powershell

-

- Open Powershell with Administrator rights. Issue the following commands to - add the required registry entries: -

-
-    New-ItemProperty -Path 'HKLM:\Software\Tailscale IPN' -Name UnattendedMode -PropertyType String -Value always
-      New-ItemProperty -Path 'HKLM:\Software\Tailscale IPN' -Name LoginURL -PropertyType String -Value "{{.URL}}"
-  
-

Finally, restart Tailscale and log in.

- -

+
tailscale login --login-server {{.URL}}
diff --git a/mkdocs.yml b/mkdocs.yml index c14fd716..b88cfcc4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -10,7 +10,7 @@ repo_name: juanfont/headscale repo_url: https://github.com/juanfont/headscale # Copyright -copyright: Copyright © 2023 Headscale authors +copyright: Copyright © 2024 Headscale authors # Configuration theme: From 60b94b04675b41438ab679f3f2f4b0a0310179a4 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Mon, 9 Sep 2024 14:10:22 +0200 Subject: [PATCH 137/145] Fix slow shutdown (#2113) * rearrange shutdown Signed-off-by: Kristoffer Dalby * http closed is fine Signed-off-by: Kristoffer Dalby * update changelog Signed-off-by: Kristoffer Dalby * logging while shutting Signed-off-by: Kristoffer Dalby --------- Signed-off-by: Kristoffer Dalby --- CHANGELOG.md | 1 + cmd/headscale/cli/serve.go | 7 +++++-- hscontrol/app.go | 36 +++++++++++++++++----------------- hscontrol/notifier/notifier.go | 34 +++++++++++++++++++++++++++++++- 4 files changed, 57 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91aed9ef..d9818217 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,7 @@ after improving the test harness as part of adopting [#1460](https://github.com/ - Make sure integration tests cover postgres for all scenarios - CLI commands (all except `serve`) only requires minimal configuration, no more errors or warnings from unset settings [#2109](https://github.com/juanfont/headscale/pull/2109) - CLI results are now concistently sent to stdout and errors to stderr [#2109](https://github.com/juanfont/headscale/pull/2109) +- Fix issue where shutting down headscale would hang [#2113](https://github.com/juanfont/headscale/pull/2113) ## 0.22.3 (2023-05-12) diff --git a/cmd/headscale/cli/serve.go b/cmd/headscale/cli/serve.go index 9f0fa35e..91597400 100644 --- a/cmd/headscale/cli/serve.go +++ b/cmd/headscale/cli/serve.go @@ -1,6 +1,9 @@ package cli import ( + "errors" + "net/http" + "github.com/rs/zerolog/log" "github.com/spf13/cobra" ) @@ -22,8 +25,8 @@ var serveCmd = &cobra.Command{ } err = app.Serve() - if err != nil { - log.Fatal().Caller().Err(err).Msg("Error starting server") + if err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Fatal().Caller().Err(err).Msg("Headscale ran into an error and had to shut down.") } }, } diff --git a/hscontrol/app.go b/hscontrol/app.go index 1732135a..4a5b4679 100644 --- a/hscontrol/app.go +++ b/hscontrol/app.go @@ -770,7 +770,7 @@ func (h *Headscale) Serve() error { }) } default: - trace := log.Trace().Msgf + info := func(msg string) { log.Info().Msg(msg) } log.Info(). Str("signal", sig.String()). Msg("Received signal to stop, shutting down gracefully") @@ -778,55 +778,55 @@ func (h *Headscale) Serve() error { expireNodeCancel() h.ephemeralGC.Close() - trace("waiting for netmap stream to close") - h.pollNetMapStreamWG.Wait() - // Gracefully shut down servers ctx, cancel := context.WithTimeout( context.Background(), types.HTTPShutdownTimeout, ) - trace("shutting down debug http server") + info("shutting down debug http server") if err := debugHTTPServer.Shutdown(ctx); err != nil { - log.Error().Err(err).Msg("Failed to shutdown prometheus http") + log.Error().Err(err).Msg("failed to shutdown prometheus http") } - trace("shutting down main http server") + info("shutting down main http server") if err := httpServer.Shutdown(ctx); err != nil { - log.Error().Err(err).Msg("Failed to shutdown http") + log.Error().Err(err).Msg("failed to shutdown http") } - trace("shutting down grpc server (socket)") + info("closing node notifier") + h.nodeNotifier.Close() + + info("waiting for netmap stream to close") + h.pollNetMapStreamWG.Wait() + + info("shutting down grpc server (socket)") grpcSocket.GracefulStop() if grpcServer != nil { - trace("shutting down grpc server (external)") + info("shutting down grpc server (external)") grpcServer.GracefulStop() grpcListener.Close() } if tailsqlContext != nil { - trace("shutting down tailsql") + info("shutting down tailsql") tailsqlContext.Done() } - trace("closing node notifier") - h.nodeNotifier.Close() - // Close network listeners - trace("closing network listeners") + info("closing network listeners") debugHTTPListener.Close() httpListener.Close() grpcGatewayConn.Close() // Stop listening (and unlink the socket if unix type): - trace("closing socket listener") + info("closing socket listener") socketListener.Close() // Close db connections - trace("closing database connection") + info("closing database connection") err = h.db.Close() if err != nil { - log.Error().Err(err).Msg("Failed to close db") + log.Error().Err(err).Msg("failed to close db") } log.Info(). diff --git a/hscontrol/notifier/notifier.go b/hscontrol/notifier/notifier.go index 0b663776..ceede6ba 100644 --- a/hscontrol/notifier/notifier.go +++ b/hscontrol/notifier/notifier.go @@ -36,6 +36,7 @@ type Notifier struct { connected *xsync.MapOf[types.NodeID, bool] b *batcher cfg *types.Config + closed bool } func NewNotifier(cfg *types.Config) *Notifier { @@ -43,6 +44,7 @@ func NewNotifier(cfg *types.Config) *Notifier { nodes: make(map[types.NodeID]chan<- types.StateUpdate), connected: xsync.NewMapOf[types.NodeID, bool](), cfg: cfg, + closed: false, } b := newBatcher(cfg.Tuning.BatchChangeDelay, n) n.b = b @@ -51,9 +53,19 @@ func NewNotifier(cfg *types.Config) *Notifier { return n } -// Close stops the batcher inside the notifier. +// Close stops the batcher and closes all channels. func (n *Notifier) Close() { + notifierWaitersForLock.WithLabelValues("lock", "close").Inc() + n.l.Lock() + defer n.l.Unlock() + notifierWaitersForLock.WithLabelValues("lock", "close").Dec() + + n.closed = true n.b.close() + + for _, c := range n.nodes { + close(c) + } } func (n *Notifier) tracef(nID types.NodeID, msg string, args ...any) { @@ -70,6 +82,10 @@ func (n *Notifier) AddNode(nodeID types.NodeID, c chan<- types.StateUpdate) { notifierWaitersForLock.WithLabelValues("lock", "add").Dec() notifierWaitForLock.WithLabelValues("add").Observe(time.Since(start).Seconds()) + if n.closed { + return + } + // If a channel exists, it means the node has opened a new // connection. Close the old channel and replace it. if curr, ok := n.nodes[nodeID]; ok { @@ -96,6 +112,10 @@ func (n *Notifier) RemoveNode(nodeID types.NodeID, c chan<- types.StateUpdate) b notifierWaitersForLock.WithLabelValues("lock", "remove").Dec() notifierWaitForLock.WithLabelValues("remove").Observe(time.Since(start).Seconds()) + if n.closed { + return true + } + if len(n.nodes) == 0 { return true } @@ -154,6 +174,10 @@ func (n *Notifier) NotifyWithIgnore( update types.StateUpdate, ignoreNodeIDs ...types.NodeID, ) { + if n.closed { + return + } + notifierUpdateReceived.WithLabelValues(update.Type.String(), types.NotifyOriginKey.Value(ctx)).Inc() n.b.addOrPassthrough(update) } @@ -170,6 +194,10 @@ func (n *Notifier) NotifyByNodeID( notifierWaitersForLock.WithLabelValues("lock", "notify").Dec() notifierWaitForLock.WithLabelValues("notify").Observe(time.Since(start).Seconds()) + if n.closed { + return + } + if c, ok := n.nodes[nodeID]; ok { select { case <-ctx.Done(): @@ -205,6 +233,10 @@ func (n *Notifier) sendAll(update types.StateUpdate) { notifierWaitersForLock.WithLabelValues("lock", "send-all").Dec() notifierWaitForLock.WithLabelValues("send-all").Observe(time.Since(start).Seconds()) + if n.closed { + return + } + for id, c := range n.nodes { // Whenever an update is sent to all nodes, there is a chance that the node // has disconnected and the goroutine that was supposed to consume the update From c3b260a6f7190105e64e48cacd85db3f7d53317c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 14:16:35 +0200 Subject: [PATCH 138/145] flake.lock: Update (#2111) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index cd36fb42..9b66e4e0 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1725099143, - "narHash": "sha256-CHgumPZaC7z+WYx72WgaLt2XF0yUVzJS60rO4GZ7ytY=", + "lastModified": 1725534445, + "narHash": "sha256-Yd0FK9SkWy+ZPuNqUgmVPXokxDgMJoGuNpMEtkfcf84=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "5629520edecb69630a3f4d17d3d33fc96c13f6fe", + "rev": "9bb1e7571aadf31ddb4af77fc64b2d59580f9a39", "type": "github" }, "original": { From 99f18f9cd90c5b806d390473c3aaa89a5aca3ad2 Mon Sep 17 00:00:00 2001 From: curlwget Date: Mon, 9 Sep 2024 20:17:25 +0800 Subject: [PATCH 139/145] chore: fix some comments (#2069) --- hscontrol/mapper/mapper.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hscontrol/mapper/mapper.go b/hscontrol/mapper/mapper.go index 702b7845..8593e167 100644 --- a/hscontrol/mapper/mapper.go +++ b/hscontrol/mapper/mapper.go @@ -227,7 +227,7 @@ func (m *Mapper) FullMapResponse( return m.marshalMapResponse(mapRequest, resp, node, mapRequest.Compress, messages...) } -// ReadOnlyResponse returns a MapResponse for the given node. +// ReadOnlyMapResponse returns a MapResponse for the given node. // Lite means that the peers has been omitted, this is intended // to be used to answer MapRequests with OmitPeers set to true. func (m *Mapper) ReadOnlyMapResponse( @@ -552,7 +552,7 @@ func appendPeerChanges( } // If there are filter rules present, see if there are any nodes that cannot - // access eachother at all and remove them from the peers. + // access each-other at all and remove them from the peers. if len(packetFilter) > 0 { changed = policy.FilterNodesByACL(node, changed, packetFilter) } @@ -596,7 +596,7 @@ func appendPeerChanges( } else { // This is a hack to avoid sending an empty list of packet filters. // Since tailcfg.PacketFilter has omitempty, any empty PacketFilter will - // be omitted, causing the client to consider it unchange, keeping the + // be omitted, causing the client to consider it unchanged, keeping the // previous packet filter. Worst case, this can cause a node that previously // has access to a node to _not_ loose access if an empty (allow none) is sent. reduced := policy.ReduceFilterRules(node, packetFilter) From 7be8796d87d2f65cdac200e3fa26febe1260bf72 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Mon, 9 Sep 2024 14:29:09 +0200 Subject: [PATCH 140/145] dont override golangci go (#2116) Signed-off-by: Kristoffer Dalby --- flake.nix | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/flake.nix b/flake.nix index 8e009c1f..79dd58e8 100644 --- a/flake.nix +++ b/flake.nix @@ -57,9 +57,11 @@ subPackages = ["protoc-gen-grpc-gateway" "protoc-gen-openapiv2"]; }; - golangci-lint = prev.golangci-lint.override { - buildGoModule = buildGo; - }; + # Upstream does not override buildGoModule properly, + # importing a specific module, so comment out for now. + # golangci-lint = prev.golangci-lint.override { + # buildGoModule = buildGo; + # }; goreleaser = prev.goreleaser.override { buildGoModule = buildGo; From 4b02dc95653f8c24be1effa8c94e9b3646595b68 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 11 Sep 2024 10:43:22 +0200 Subject: [PATCH 141/145] make cli mode respect log.level (#2124) Fixes #2119 Signed-off-by: Kristoffer Dalby --- hscontrol/types/config.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hscontrol/types/config.go b/hscontrol/types/config.go index 8767077e..50ce2f07 100644 --- a/hscontrol/types/config.go +++ b/hscontrol/types/config.go @@ -732,6 +732,9 @@ func prefixV6() (*netip.Prefix, error) { // LoadCLIConfig returns the needed configuration for the CLI client // of Headscale to connect to a Headscale server. func LoadCLIConfig() (*Config, error) { + logConfig := logConfig() + zerolog.SetGlobalLevel(logConfig.Level) + return &Config{ DisableUpdateCheck: viper.GetBool("disable_check_updates"), UnixSocket: viper.GetString("unix_socket"), @@ -741,6 +744,7 @@ func LoadCLIConfig() (*Config, error) { Timeout: viper.GetDuration("cli.timeout"), Insecure: viper.GetBool("cli.insecure"), }, + Log: logConfig, }, nil } From 64319f79ff1934865805fc73be2228dddce0ec80 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 11 Sep 2024 12:00:32 +0200 Subject: [PATCH 142/145] make stream shutdown if self-node has been removed (#2125) * add shutdown that asserts if headscale had panics Signed-off-by: Kristoffer Dalby * add test case producing 2118 panic Signed-off-by: Kristoffer Dalby * make stream shutdown if self-node has been removed Currently we will read the node from database, and since it is deleted, the id might be set to nil. Keep the node around and just shutdown, so it is cleanly removed from notifier. Fixes #2118 Signed-off-by: Kristoffer Dalby --------- Signed-off-by: Kristoffer Dalby --- .github/workflows/test-integration.yaml | 1 + hscontrol/poll.go | 7 ++ integration/control.go | 4 +- integration/dockertestutil/logs.go | 18 +++-- integration/general_test.go | 99 +++++++++++++++++++++++++ integration/hsic/hsic.go | 8 +- integration/scenario.go | 28 +++++-- integration/tsic/tsic.go | 4 +- 8 files changed, 148 insertions(+), 21 deletions(-) diff --git a/.github/workflows/test-integration.yaml b/.github/workflows/test-integration.yaml index ed194da1..d6c7eff2 100644 --- a/.github/workflows/test-integration.yaml +++ b/.github/workflows/test-integration.yaml @@ -52,6 +52,7 @@ jobs: - TestExpireNode - TestNodeOnlineStatus - TestPingAllByIPManyUpDown + - Test2118DeletingOnlineNodePanics - TestEnablingRoutes - TestHASubnetRouterFailover - TestEnableDisableAutoApprovedRoute diff --git a/hscontrol/poll.go b/hscontrol/poll.go index d7ba682e..82a5295f 100644 --- a/hscontrol/poll.go +++ b/hscontrol/poll.go @@ -5,6 +5,7 @@ import ( "fmt" "math/rand/v2" "net/http" + "slices" "sort" "strings" "time" @@ -273,6 +274,12 @@ func (m *mapSession) serveLongPoll() { return } + // If the node has been removed from headscale, close the stream + if slices.Contains(update.Removed, m.node.ID) { + m.tracef("node removed, closing stream") + return + } + m.tracef("received stream update: %s %s", update.Type.String(), update.Message) mapResponseUpdateReceived.WithLabelValues(update.Type.String()).Inc() diff --git a/integration/control.go b/integration/control.go index 8a34bde8..b5699577 100644 --- a/integration/control.go +++ b/integration/control.go @@ -6,8 +6,8 @@ import ( ) type ControlServer interface { - Shutdown() error - SaveLog(string) error + Shutdown() (string, string, error) + SaveLog(string) (string, string, error) SaveProfile(string) error Execute(command []string) (string, error) WriteFile(path string, content []byte) error diff --git a/integration/dockertestutil/logs.go b/integration/dockertestutil/logs.go index 98ba970a..64c3c9ac 100644 --- a/integration/dockertestutil/logs.go +++ b/integration/dockertestutil/logs.go @@ -17,10 +17,10 @@ func SaveLog( pool *dockertest.Pool, resource *dockertest.Resource, basePath string, -) error { +) (string, string, error) { err := os.MkdirAll(basePath, os.ModePerm) if err != nil { - return err + return "", "", err } var stdout bytes.Buffer @@ -41,28 +41,30 @@ func SaveLog( }, ) if err != nil { - return err + return "", "", err } log.Printf("Saving logs for %s to %s\n", resource.Container.Name, basePath) + stdoutPath := path.Join(basePath, resource.Container.Name+".stdout.log") err = os.WriteFile( - path.Join(basePath, resource.Container.Name+".stdout.log"), + stdoutPath, stdout.Bytes(), filePerm, ) if err != nil { - return err + return "", "", err } + stderrPath := path.Join(basePath, resource.Container.Name+".stderr.log") err = os.WriteFile( - path.Join(basePath, resource.Container.Name+".stderr.log"), + stderrPath, stderr.Bytes(), filePerm, ) if err != nil { - return err + return "", "", err } - return nil + return stdoutPath, stderrPath, nil } diff --git a/integration/general_test.go b/integration/general_test.go index 6de00fd2..a8421f47 100644 --- a/integration/general_test.go +++ b/integration/general_test.go @@ -954,3 +954,102 @@ func TestPingAllByIPManyUpDown(t *testing.T) { t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps)) } } + +func Test2118DeletingOnlineNodePanics(t *testing.T) { + IntegrationSkip(t) + t.Parallel() + + scenario, err := NewScenario(dockertestMaxWait()) + assertNoErr(t, err) + defer scenario.ShutdownAssertNoPanics(t) + + // TODO(kradalby): it does not look like the user thing works, only second + // get created? maybe only when many? + spec := map[string]int{ + "user1": 1, + "user2": 1, + } + + err = scenario.CreateHeadscaleEnv(spec, + []tsic.Option{}, + hsic.WithTestName("deletenocrash"), + hsic.WithEmbeddedDERPServerOnly(), + hsic.WithTLS(), + hsic.WithHostnameAsServerURL(), + ) + 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("%d successful pings out of %d", success, len(allClients)*len(allIps)) + + headscale, err := scenario.Headscale() + assertNoErr(t, err) + + // Test list all nodes after added otherUser + var nodeList []v1.Node + err = executeAndUnmarshal( + headscale, + []string{ + "headscale", + "nodes", + "list", + "--output", + "json", + }, + &nodeList, + ) + assert.Nil(t, err) + assert.Len(t, nodeList, 2) + assert.True(t, nodeList[0].Online) + assert.True(t, nodeList[1].Online) + + // Delete the first node, which is online + _, err = headscale.Execute( + []string{ + "headscale", + "nodes", + "delete", + "--identifier", + // Delete the last added machine + fmt.Sprintf("%d", nodeList[0].Id), + "--output", + "json", + "--force", + }, + ) + assert.Nil(t, err) + + time.Sleep(2 * time.Second) + + // Ensure that the node has been deleted, this did not occur due to a panic. + var nodeListAfter []v1.Node + err = executeAndUnmarshal( + headscale, + []string{ + "headscale", + "nodes", + "list", + "--output", + "json", + }, + &nodeListAfter, + ) + assert.Nil(t, err) + assert.Len(t, nodeListAfter, 1) + assert.True(t, nodeListAfter[0].Online) + assert.Equal(t, nodeList[1].Id, nodeListAfter[0].Id) + +} diff --git a/integration/hsic/hsic.go b/integration/hsic/hsic.go index b9026225..20a778b8 100644 --- a/integration/hsic/hsic.go +++ b/integration/hsic/hsic.go @@ -398,8 +398,8 @@ func (t *HeadscaleInContainer) hasTLS() bool { } // Shutdown stops and cleans up the Headscale container. -func (t *HeadscaleInContainer) Shutdown() error { - err := t.SaveLog("/tmp/control") +func (t *HeadscaleInContainer) Shutdown() (string, string, error) { + stdoutPath, stderrPath, err := t.SaveLog("/tmp/control") if err != nil { log.Printf( "Failed to save log from control: %s", @@ -458,12 +458,12 @@ func (t *HeadscaleInContainer) Shutdown() error { t.pool.Purge(t.pgContainer) } - return t.pool.Purge(t.container) + return stdoutPath, stderrPath, t.pool.Purge(t.container) } // SaveLog saves the current stdout log of the container to a path // on the host system. -func (t *HeadscaleInContainer) SaveLog(path string) error { +func (t *HeadscaleInContainer) SaveLog(path string) (string, string, error) { return dockertestutil.SaveLog(t.pool, t.container, path) } diff --git a/integration/scenario.go b/integration/scenario.go index 075d1fd5..df978f2a 100644 --- a/integration/scenario.go +++ b/integration/scenario.go @@ -8,6 +8,7 @@ import ( "os" "sort" "sync" + "testing" "time" v1 "github.com/juanfont/headscale/gen/go/headscale/v1" @@ -18,6 +19,7 @@ import ( "github.com/ory/dockertest/v3" "github.com/puzpuzpuz/xsync/v3" "github.com/samber/lo" + "github.com/stretchr/testify/assert" "golang.org/x/sync/errgroup" "tailscale.com/envknob" ) @@ -187,13 +189,9 @@ func NewScenario(maxWait time.Duration) (*Scenario, error) { }, nil } -// Shutdown shuts down and cleans up all the containers (ControlServer, TailscaleClient) -// and networks associated with it. -// In addition, it will save the logs of the ControlServer to `/tmp/control` in the -// environment running the tests. -func (s *Scenario) Shutdown() { +func (s *Scenario) ShutdownAssertNoPanics(t *testing.T) { s.controlServers.Range(func(_ string, control ControlServer) bool { - err := control.Shutdown() + stdoutPath, stderrPath, err := control.Shutdown() if err != nil { log.Printf( "Failed to shut down control: %s", @@ -201,6 +199,16 @@ func (s *Scenario) Shutdown() { ) } + if t != nil { + stdout, err := os.ReadFile(stdoutPath) + assert.NoError(t, err) + assert.NotContains(t, string(stdout), "panic") + + stderr, err := os.ReadFile(stderrPath) + assert.NoError(t, err) + assert.NotContains(t, string(stderr), "panic") + } + return true }) @@ -224,6 +232,14 @@ func (s *Scenario) Shutdown() { // } } +// Shutdown shuts down and cleans up all the containers (ControlServer, TailscaleClient) +// and networks associated with it. +// In addition, it will save the logs of the ControlServer to `/tmp/control` in the +// environment running the tests. +func (s *Scenario) Shutdown() { + s.ShutdownAssertNoPanics(nil) +} + // Users returns the name of all users associated with the Scenario. func (s *Scenario) Users() []string { users := make([]string, 0) diff --git a/integration/tsic/tsic.go b/integration/tsic/tsic.go index e1045ec3..a3fac17c 100644 --- a/integration/tsic/tsic.go +++ b/integration/tsic/tsic.go @@ -998,7 +998,9 @@ func (t *TailscaleInContainer) WriteFile(path string, data []byte) error { // 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) + // TODO(kradalby): Assert if tailscale logs contains panics. + _, _, err := dockertestutil.SaveLog(t.pool, t.container, path) + return err } // ReadFile reads a file from the Tailscale container. From 064c46f2a5889a328627673f153a01c26812c945 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 11 Sep 2024 18:27:49 +0200 Subject: [PATCH 143/145] move logic for validating node names (#2127) * move logic for validating node names this commits moves the generation of "given names" of nodes into the registration function, and adds validation of renames to RenameNode using the same logic. Fixes #2121 Signed-off-by: Kristoffer Dalby * fix double arg Signed-off-by: Kristoffer Dalby --------- Signed-off-by: Kristoffer Dalby --- hscontrol/auth.go | 26 +------ hscontrol/db/node.go | 72 +++++++++---------- hscontrol/db/node_test.go | 143 ++++++++++++++++++++++++++------------ hscontrol/grpcv1.go | 8 +-- 4 files changed, 134 insertions(+), 115 deletions(-) diff --git a/hscontrol/auth.go b/hscontrol/auth.go index aaab03ce..8b8557ba 100644 --- a/hscontrol/auth.go +++ b/hscontrol/auth.go @@ -66,7 +66,7 @@ func (h *Headscale) handleRegister( regReq tailcfg.RegisterRequest, machineKey key.MachinePublic, ) { - logInfo, logTrace, logErr := logAuthFunc(regReq, machineKey) + logInfo, logTrace, _ := logAuthFunc(regReq, machineKey) now := time.Now().UTC() logTrace("handleRegister called, looking up machine in DB") node, err := h.db.GetNodeByAnyKey(machineKey, regReq.NodeKey, regReq.OldNodeKey) @@ -105,16 +105,6 @@ func (h *Headscale) handleRegister( logInfo("Node not found in database, creating new") - givenName, err := h.db.GenerateGivenName( - machineKey, - regReq.Hostinfo.Hostname, - ) - if err != nil { - logErr(err, "Failed to generate given name for node") - - return - } - // The node did not have a key to authenticate, which means // that we rely on a method that calls back some how (OpenID or CLI) // We create the node and then keep it around until a callback @@ -122,7 +112,6 @@ func (h *Headscale) handleRegister( newNode := types.Node{ MachineKey: machineKey, Hostname: regReq.Hostinfo.Hostname, - GivenName: givenName, NodeKey: regReq.NodeKey, LastSeen: &now, Expiry: &time.Time{}, @@ -354,21 +343,8 @@ func (h *Headscale) handleAuthKey( } else { now := time.Now().UTC() - givenName, err := h.db.GenerateGivenName(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") - - return - } - nodeToRegister := types.Node{ Hostname: registerRequest.Hostinfo.Hostname, - GivenName: givenName, UserID: pak.User.ID, User: pak.User, MachineKey: machineKey, diff --git a/hscontrol/db/node.go b/hscontrol/db/node.go index a9e78a45..c0f42de1 100644 --- a/hscontrol/db/node.go +++ b/hscontrol/db/node.go @@ -90,20 +90,6 @@ func (hsdb *HSDatabase) ListEphemeralNodes() (types.Nodes, error) { }) } -func listNodesByGivenName(tx *gorm.DB, givenName string) (types.Nodes, error) { - nodes := types.Nodes{} - if err := tx. - Preload("AuthKey"). - Preload("AuthKey.User"). - Preload("User"). - Preload("Routes"). - Where("given_name = ?", givenName).Find(&nodes).Error; err != nil { - return nil, err - } - - return nodes, nil -} - func (hsdb *HSDatabase) getNode(user string, name string) (*types.Node, error) { return Read(hsdb.DB, func(rx *gorm.DB) (*types.Node, error) { return getNode(rx, user, name) @@ -242,9 +228,9 @@ func SetTags( } // RenameNode takes a Node struct and a new GivenName for the nodes -// and renames it. +// and renames it. If the name is not unique, it will return an error. func RenameNode(tx *gorm.DB, - nodeID uint64, newName string, + nodeID types.NodeID, newName string, ) error { err := util.CheckForFQDNRules( newName, @@ -253,6 +239,15 @@ func RenameNode(tx *gorm.DB, return fmt.Errorf("renaming node: %w", err) } + uniq, err := isUnqiueName(tx, newName) + if err != nil { + return fmt.Errorf("checking if name is unique: %w", err) + } + + if !uniq { + return fmt.Errorf("name is not unique: %s", newName) + } + if err := tx.Model(&types.Node{}).Where("id = ?", nodeID).Update("given_name", newName).Error; err != nil { return fmt.Errorf("failed to rename node in the database: %w", err) } @@ -415,6 +410,15 @@ func RegisterNode(tx *gorm.DB, node types.Node, ipv4 *netip.Addr, ipv6 *netip.Ad node.IPv4 = ipv4 node.IPv6 = ipv6 + if node.GivenName == "" { + givenName, err := ensureUniqueGivenName(tx, node.Hostname) + if err != nil { + return nil, fmt.Errorf("failed to ensure unique given name: %w", err) + } + + node.GivenName = givenName + } + if err := tx.Save(&node).Error; err != nil { return nil, fmt.Errorf("failed register(save) node in the database: %w", err) } @@ -642,40 +646,32 @@ func generateGivenName(suppliedName string, randomSuffix bool) (string, error) { return normalizedHostname, nil } -func (hsdb *HSDatabase) GenerateGivenName( - mkey key.MachinePublic, - suppliedName string, -) (string, error) { - return Read(hsdb.DB, func(rx *gorm.DB) (string, error) { - return GenerateGivenName(rx, mkey, suppliedName) - }) +func isUnqiueName(tx *gorm.DB, name string) (bool, error) { + nodes := types.Nodes{} + if err := tx. + Where("given_name = ?", name).Find(&nodes).Error; err != nil { + return false, err + } + + return len(nodes) == 0, nil } -func GenerateGivenName( +func ensureUniqueGivenName( tx *gorm.DB, - mkey key.MachinePublic, - suppliedName string, + name string, ) (string, error) { - givenName, err := generateGivenName(suppliedName, false) + givenName, err := generateGivenName(name, false) if err != nil { return "", err } - // Tailscale rules (may differ) https://tailscale.com/kb/1098/machine-names/ - nodes, err := listNodesByGivenName(tx, givenName) + unique, err := isUnqiueName(tx, givenName) if err != nil { return "", err } - 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 !unique { + postfixedName, err := generateGivenName(name, true) if err != nil { return "", err } diff --git a/hscontrol/db/node_test.go b/hscontrol/db/node_test.go index 94cce13b..bafb22ba 100644 --- a/hscontrol/db/node_test.go +++ b/hscontrol/db/node_test.go @@ -19,6 +19,7 @@ import ( "github.com/puzpuzpuz/xsync/v3" "github.com/stretchr/testify/assert" "gopkg.in/check.v1" + "gorm.io/gorm" "tailscale.com/tailcfg" "tailscale.com/types/key" "tailscale.com/types/ptr" @@ -313,51 +314,6 @@ func (s *Suite) TestExpireNode(c *check.C) { c.Assert(nodeFromDB.IsExpired(), check.Equals, true) } -func (s *Suite) TestGenerateGivenName(c *check.C) { - user1, err := db.CreateUser("user-1") - c.Assert(err, check.IsNil) - - pak, err := db.CreatePreAuthKey(user1.Name, false, false, nil, nil) - c.Assert(err, check.IsNil) - - _, 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: machineKey.Public(), - NodeKey: nodeKey.Public(), - Hostname: "hostname-1", - GivenName: "hostname-1", - UserID: user1.ID, - RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: ptr.To(pak.ID), - } - - trx := db.DB.Save(node) - c.Assert(trx.Error, check.IsNil) - - 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(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(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) -} - func (s *Suite) TestSetTags(c *check.C) { user, err := db.CreateUser("test") c.Assert(err, check.IsNil) @@ -778,3 +734,100 @@ func TestListEphemeralNodes(t *testing.T) { assert.Equal(t, nodeEph.UserID, ephemeralNodes[0].UserID) assert.Equal(t, nodeEph.Hostname, ephemeralNodes[0].Hostname) } + +func TestRenameNode(t *testing.T) { + db, err := newTestDB() + if err != nil { + t.Fatalf("creating db: %s", err) + } + + user, err := db.CreateUser("test") + assert.NoError(t, err) + + user2, err := db.CreateUser("test2") + assert.NoError(t, err) + + node := types.Node{ + ID: 0, + MachineKey: key.NewMachine().Public(), + NodeKey: key.NewNode().Public(), + Hostname: "test", + UserID: user.ID, + RegisterMethod: util.RegisterMethodAuthKey, + } + + node2 := types.Node{ + ID: 0, + MachineKey: key.NewMachine().Public(), + NodeKey: key.NewNode().Public(), + Hostname: "test", + UserID: user2.ID, + RegisterMethod: util.RegisterMethodAuthKey, + } + + err = db.DB.Save(&node).Error + assert.NoError(t, err) + + err = db.DB.Save(&node2).Error + assert.NoError(t, err) + + err = db.DB.Transaction(func(tx *gorm.DB) error { + _, err := RegisterNode(tx, node, nil, nil) + if err != nil { + return err + } + _, err = RegisterNode(tx, node2, nil, nil) + return err + }) + assert.NoError(t, err) + + nodes, err := db.ListNodes() + assert.NoError(t, err) + + assert.Len(t, nodes, 2) + + t.Logf("node1 %s %s", nodes[0].Hostname, nodes[0].GivenName) + t.Logf("node2 %s %s", nodes[1].Hostname, nodes[1].GivenName) + + assert.Equal(t, nodes[0].Hostname, nodes[0].GivenName) + assert.NotEqual(t, nodes[1].Hostname, nodes[1].GivenName) + assert.Equal(t, nodes[0].Hostname, nodes[1].Hostname) + assert.NotEqual(t, nodes[0].Hostname, nodes[1].GivenName) + assert.Contains(t, nodes[1].GivenName, nodes[0].Hostname) + assert.Equal(t, nodes[0].GivenName, nodes[1].Hostname) + assert.Len(t, nodes[0].Hostname, 4) + assert.Len(t, nodes[1].Hostname, 4) + assert.Len(t, nodes[0].GivenName, 4) + assert.Len(t, nodes[1].GivenName, 13) + + // Nodes can be renamed to a unique name + err = db.Write(func(tx *gorm.DB) error { + return RenameNode(tx, nodes[0].ID, "newname") + }) + assert.NoError(t, err) + + nodes, err = db.ListNodes() + assert.NoError(t, err) + assert.Len(t, nodes, 2) + assert.Equal(t, nodes[0].Hostname, "test") + assert.Equal(t, nodes[0].GivenName, "newname") + + // Nodes can reuse name that is no longer used + err = db.Write(func(tx *gorm.DB) error { + return RenameNode(tx, nodes[1].ID, "test") + }) + assert.NoError(t, err) + + nodes, err = db.ListNodes() + assert.NoError(t, err) + assert.Len(t, nodes, 2) + assert.Equal(t, nodes[0].Hostname, "test") + assert.Equal(t, nodes[0].GivenName, "newname") + assert.Equal(t, nodes[1].GivenName, "test") + + // Nodes cannot be renamed to used names + err = db.Write(func(tx *gorm.DB) error { + return RenameNode(tx, nodes[0].ID, "test") + }) + assert.ErrorContains(t, err, "name is not unique") +} diff --git a/hscontrol/grpcv1.go b/hscontrol/grpcv1.go index 3f985d98..596748f2 100644 --- a/hscontrol/grpcv1.go +++ b/hscontrol/grpcv1.go @@ -373,7 +373,7 @@ func (api headscaleV1APIServer) RenameNode( node, err := db.Write(api.h.db.DB, func(tx *gorm.DB) (*types.Node, error) { err := db.RenameNode( tx, - request.GetNodeId(), + types.NodeID(request.GetNodeId()), request.GetNewName(), ) if err != nil { @@ -802,18 +802,12 @@ func (api headscaleV1APIServer) DebugCreateNode( 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: mkey, NodeKey: nodeKey.Public(), Hostname: request.GetName(), - GivenName: givenName, User: *user, Expiry: &time.Time{}, From c3ef90a7f7b9e742ce55100db8a9af44f8540c7d Mon Sep 17 00:00:00 2001 From: nblock Date: Wed, 11 Sep 2024 18:43:59 +0200 Subject: [PATCH 144/145] Update documentation for Apple (#2117) * Rename docs/ios-client.md to docs/apple-client.md. Add instructions for macOS; those are copied from the /apple endpoint and slightly modified. Fix doc links in the README. * Move infoboxes for /apple and /windows under the "Goal" section to the top. Those should be seen by users first as they contain *their* specific headscale URL. * Swap order of macOS and iOS to move "Profiles" further down. * Remove apple configuration profiles * Remove Tailscale versions hints * Mention /apple and /windows in the README along with their docs See: #2096 --- README.md | 18 ++--- docs/apple-client.md | 51 +++++++++++++ docs/iOS-client.md | 30 -------- docs/windows-client.md | 10 +-- hscontrol/templates/apple.html | 129 ++++++++++++--------------------- integration/scenario_test.go | 2 +- mkdocs.yml | 2 +- 7 files changed, 112 insertions(+), 130 deletions(-) create mode 100644 docs/apple-client.md delete mode 100644 docs/iOS-client.md diff --git a/README.md b/README.md index 03802e27..ff44e8e4 100644 --- a/README.md +++ b/README.md @@ -62,15 +62,15 @@ buttons available in the repo. ## Client OS support -| OS | Supports headscale | -| ------- | --------------------------------------------------------- | -| Linux | Yes | -| OpenBSD | Yes | -| FreeBSD | Yes | -| macOS | Yes (see `/apple` on your headscale for more information) | -| Windows | Yes [docs](./docs/windows-client.md) | -| Android | Yes [docs](./docs/android-client.md) | -| iOS | Yes [docs](./docs/iOS-client.md) | +| OS | Supports headscale | +| ------- | -------------------------------------------------------------------------------------------------- | +| Linux | Yes | +| OpenBSD | Yes | +| FreeBSD | Yes | +| Windows | Yes (see [docs](./docs/windows-client.md) and `/windows` on your headscale for more information) | +| Android | Yes (see [docs](./docs/android-client.md)) | +| macOS | Yes (see [docs](./docs/apple-client.md#macos) and `/apple` on your headscale for more information) | +| iOS | Yes (see [docs](./docs/apple-client.md#ios) and `/apple` on your headscale for more information) | ## Running headscale diff --git a/docs/apple-client.md b/docs/apple-client.md new file mode 100644 index 00000000..29ad4b45 --- /dev/null +++ b/docs/apple-client.md @@ -0,0 +1,51 @@ +# Connecting an Apple client + +## Goal + +This documentation has the goal of showing how a user can use the official iOS and macOS [Tailscale](https://tailscale.com) clients with `headscale`. + +!!! info "Instructions on your headscale instance" + + An endpoint with information on how to connect your Apple device + is also available at `/apple` on your running instance. + +## iOS + +### Installation + +Install the official Tailscale iOS client from the [App Store](https://apps.apple.com/app/tailscale/id1470499037). + +### Configuring the headscale URL + +- Open Tailscale and make sure you are _not_ logged in to any account +- Open Settings on the iOS device +- Scroll down to the `third party apps` section, under `Game Center` or `TV Provider` +- Find Tailscale and select it + - If the iOS device was previously logged into Tailscale, switch the `Reset Keychain` toggle to `on` +- Enter the URL of your headscale instance (e.g `https://headscale.example.com`) under `Alternate Coordination Server URL` +- Restart the app by closing it from the iOS app switcher, open the app and select the regular sign in option + _(non-SSO)_. It should open up to the headscale authentication page. +- Enter your credentials and log in. Headscale should now be working on your iOS device. + +## macOS + +### Installation + +Choose one of the available [Tailscale clients for macOS](https://tailscale.com/kb/1065/macos-variants) and install it. + +### Configuring the headscale URL + +#### Command line + +Use Tailscale's login command to connect with your headscale instance (e.g `https://headscale.example.com`): + +``` +tailscale login --login-server +``` + +#### GUI + +- ALT + Click the Tailscale icon in the menu and hover over the Debug menu +- Under `Custom Login Server`, select `Add Account...` +- Enter the URL of your headscale instance (e.g `https://headscale.example.com`) and press `Add Account` +- Follow the login procedure in the browser diff --git a/docs/iOS-client.md b/docs/iOS-client.md deleted file mode 100644 index 761dfcf0..00000000 --- a/docs/iOS-client.md +++ /dev/null @@ -1,30 +0,0 @@ -# Connecting an iOS client - -## Goal - -This documentation has the goal of showing how a user can use the official iOS [Tailscale](https://tailscale.com) client with `headscale`. - -## Installation - -Install the official Tailscale iOS client from the [App Store](https://apps.apple.com/app/tailscale/id1470499037). - -Ensure that the installed version is at least 1.38.1, as that is the first release to support alternate control servers. - -## Configuring the headscale URL - -!!! info "Apple devices" - - An endpoint with information on how to connect your Apple devices - (currently macOS only) is available at `/apple` on your running instance. - -Ensure that the tailscale app is logged out before proceeding. - -Go to iOS settings, scroll down past game center and tv provider to the tailscale app and select it. The headscale URL can be entered into the _"ALTERNATE COORDINATION SERVER URL"_ box. - -> **Note** -> -> If the app was previously logged into tailscale, toggle on the _Reset Keychain_ switch. - -Restart the app by closing it from the iOS app switcher, open the app and select the regular _Sign in_ option (non-SSO), and it should open up to the headscale authentication page. - -Enter your credentials and log in. Headscale should now be working on your iOS device. diff --git a/docs/windows-client.md b/docs/windows-client.md index ff4834b4..66c47279 100644 --- a/docs/windows-client.md +++ b/docs/windows-client.md @@ -4,17 +4,17 @@ This documentation has the goal of showing how a user can use the official Windows [Tailscale](https://tailscale.com) client with `headscale`. +!!! info "Instructions on your headscale instance" + + An endpoint with information on how to connect your Windows device + is also available at `/windows` on your running instance. + ## Installation Download the [Official Windows Client](https://tailscale.com/download/windows) and install it. ## Configuring the headscale URL -!!! info "Instructions on your headscale instance" - - An endpoint with information on how to connect your Windows device - is also available at `/windows` on your running instance. - Open a Command Prompt or Powershell and use Tailscale's login command to connect with your headscale instance (e.g `https://headscale.example.com`): diff --git a/hscontrol/templates/apple.html b/hscontrol/templates/apple.html index 4064dced..9582594a 100644 --- a/hscontrol/templates/apple.html +++ b/hscontrol/templates/apple.html @@ -25,17 +25,48 @@ +

headscale: iOS configuration

+

GUI

+
    +
  1. + Install the official Tailscale iOS client from the + App store +
  2. +
  3. + Open Tailscale and make sure you are not logged in to any account +
  4. +
  5. Open Settings on the iOS device
  6. +
  7. + Scroll down to the "third party apps" section, under "Game Center" or + "TV Provider" +
  8. +
  9. + Find Tailscale and select it +
      +
    • + If the iOS device was previously logged into Tailscale, switch the + "Reset Keychain" toggle to "on" +
    • +
    +
  10. +
  11. Enter "{{.URL}}" under "Alternate Coordination Server URL"
  12. +
  13. + Restart the app by closing it from the iOS app switcher, open the app + and select the regular sign in option (non-SSO). It should open + up to the headscale authentication page. +
  14. +
  15. + Enter your credentials and log in. Headscale should now be working on + your iOS device +
  16. +

headscale: macOS configuration

-

Recent Tailscale versions (1.34.0 and higher)

-

- Tailscale added Fast User Switching in version 1.34 and you can now use - the new login command to connect to one or more headscale (and Tailscale) - servers. The previously used profiles does not have an effect anymore. -

-

Command line

+

Command line

Use Tailscale's login command to add your profile:

tailscale login --login-server {{.URL}}
-

GUI

+

GUI

  1. ALT + Click the Tailscale icon in the menu and hover over the Debug menu @@ -46,44 +77,7 @@
  2. Follow the login procedure in the browser
-

Apple configuration profiles (1.32.0 and lower)

-

- This page provides - configuration profiles - for the official Tailscale clients for -

- -

- The profiles will configure Tailscale.app to use {{.URL}} as - its control server. -

-

Caution

-

- You should always download and inspect the profile before installing it: -

-
    -
  • - for app store client: curl {{.URL}}/apple/macos-app-store -
  • -
  • - for standalone client: curl {{.URL}}/apple/macos-standalone -
  • -

Profiles

-

macOS

Headscale can be set to the default server by installing a Headscale configuration profile: @@ -121,50 +115,17 @@

Restart Tailscale.app and log in.

-

headscale: iOS configuration

-

Recent Tailscale versions (1.38.1 and higher)

+

Caution

- Tailscale 1.38.1 on - iOS - added a configuration option to allow user to set an "Alternate - Coordination server". This can be used to connect to your headscale - server. + You should always download and inspect the profile before installing it:

-

GUI

-
    +
    • - Install the official Tailscale iOS client from the - App store + for app store client: curl {{.URL}}/apple/macos-app-store
    • - Open Tailscale and make sure you are not logged in to any account + for standalone client: curl {{.URL}}/apple/macos-standalone
    • -
    • Open Settings on the iOS device
    • -
    • - Scroll down to the "third party apps" section, under "Game Center" or - "TV Provider" -
    • -
    • - Find Tailscale and select it -
        -
      • - If the iOS device was previously logged into Tailscale, switch the - "Reset Keychain" toggle to "on" -
      • -
      -
    • -
    • Enter "{{.URL}}" under "Alternate Coordination Server URL"
    • -
    • - Restart the app by closing it from the iOS app switcher, open the app - and select the regular sign in option (non-SSO). It should open - up to the headscale authentication page. -
    • -
    • - Enter your credentials and log in. Headscale should now be working on - your iOS device -
    • -
+ diff --git a/integration/scenario_test.go b/integration/scenario_test.go index ea941ed7..9db4c3a0 100644 --- a/integration/scenario_test.go +++ b/integration/scenario_test.go @@ -7,7 +7,7 @@ import ( ) // This file is intended to "test the test framework", by proxy it will also test -// some Headcsale/Tailscale stuff, but mostly in very simple ways. +// some Headscale/Tailscale stuff, but mostly in very simple ways. func IntegrationSkip(t *testing.T) { t.Helper() diff --git a/mkdocs.yml b/mkdocs.yml index b88cfcc4..fe5c0d64 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -139,5 +139,5 @@ nav: - Remote CLI: remote-cli.md - Usage: - Android: android-client.md + - Apple: apple-client.md - Windows: windows-client.md - - iOS: iOS-client.md From fe68f503289db6cb1c2a568b8ae02a45ac632dd6 Mon Sep 17 00:00:00 2001 From: nblock Date: Wed, 11 Sep 2024 18:46:06 +0200 Subject: [PATCH 145/145] Use headscale.example.com (#2122) --- docs/exit-node.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/exit-node.md b/docs/exit-node.md index 797f42f4..1acd20a3 100644 --- a/docs/exit-node.md +++ b/docs/exit-node.md @@ -5,7 +5,7 @@ Register the node and make it advertise itself as an exit node: ```console -$ sudo tailscale up --login-server https://my-server.com --advertise-exit-node +$ sudo tailscale up --login-server https://headscale.example.com --advertise-exit-node ``` If the node is already registered, it can advertise exit capabilities like this: