diff --git a/.github/workflows/golang-test-darwin.yml b/.github/workflows/golang-test-darwin.yml index 2dbeb106a..4571ce753 100644 --- a/.github/workflows/golang-test-darwin.yml +++ b/.github/workflows/golang-test-darwin.yml @@ -1,4 +1,4 @@ -name: Test Code Darwin +name: "Darwin" on: push: @@ -12,9 +12,7 @@ concurrency: jobs: test: - strategy: - matrix: - store: ['sqlite'] + name: "Client / Unit" runs-on: macos-latest steps: - name: Install Go @@ -44,4 +42,5 @@ jobs: run: git --no-pager diff --exit-code - name: Test - run: NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 5m -p 1 $(go list ./... | grep -v /management) + run: NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -tags=devcert -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 5m -p 1 $(go list ./... | grep -v /management) + diff --git a/.github/workflows/golang-test-freebsd.yml b/.github/workflows/golang-test-freebsd.yml index a2d743715..e1c688b1b 100644 --- a/.github/workflows/golang-test-freebsd.yml +++ b/.github/workflows/golang-test-freebsd.yml @@ -1,5 +1,4 @@ - -name: Test Code FreeBSD +name: "FreeBSD" on: push: @@ -13,6 +12,7 @@ concurrency: jobs: test: + name: "Client / Unit" runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 @@ -24,7 +24,7 @@ jobs: copyback: false release: "14.1" prepare: | - pkg install -y go + pkg install -y go pkgconf xorg # -x - to print all executed commands # -e - to faile on first error @@ -33,7 +33,7 @@ jobs: time go build -o netbird client/main.go # check all component except management, since we do not support management server on freebsd time go test -timeout 1m -failfast ./base62/... - # NOTE: without -p1 `client/internal/dns` will fail becasue of `listen udp4 :33100: bind: address already in use` + # NOTE: without -p1 `client/internal/dns` will fail because of `listen udp4 :33100: bind: address already in use` time go test -timeout 8m -failfast -p 1 ./client/... time go test -timeout 1m -failfast ./dns/... time go test -timeout 1m -failfast ./encryption/... diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index 774adf1c0..efe1a2654 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -1,4 +1,4 @@ -name: Test Code Linux +name: Linux on: push: @@ -12,11 +12,21 @@ concurrency: jobs: build-cache: + name: "Build Cache" runs-on: ubuntu-22.04 + outputs: + management: ${{ steps.filter.outputs.management }} steps: - name: Checkout code uses: actions/checkout@v4 + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + management: + - 'management/**' + - name: Install Go uses: actions/setup-go@v5 with: @@ -38,7 +48,6 @@ jobs: key: ${{ runner.os }}-gotest-cache-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-gotest-cache-${{ hashFiles('**/go.sum') }} - - name: Install dependencies if: steps.cache.outputs.cache-hit != 'true' @@ -89,6 +98,7 @@ jobs: run: CGO_ENABLED=1 GOARCH=386 go build -o relay-386 . test: + name: "Client / Unit" needs: [build-cache] strategy: fail-fast: false @@ -134,9 +144,116 @@ jobs: run: git --no-pager diff --exit-code - name: Test - run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} CI=true go test -exec 'sudo' -timeout 10m -p 1 $(go list ./... | grep -v /management) + run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} CI=true go test -tags devcert -exec 'sudo' -timeout 10m -p 1 $(go list ./... | grep -v -e /management -e /signal -e /relay) + + test_relay: + name: "Relay / Unit" + needs: [build-cache] + strategy: + fail-fast: false + matrix: + arch: [ '386','amd64' ] + runs-on: ubuntu-22.04 + steps: + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: "1.23.x" + cache: false + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Get Go environment + run: | + echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV + echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV + + - name: Cache Go modules + uses: actions/cache/restore@v4 + with: + path: | + ${{ env.cache }} + ${{ env.modcache }} + key: ${{ runner.os }}-gotest-cache-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-gotest-cache- + + - name: Install dependencies + run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev + + - name: Install 32-bit libpcap + if: matrix.arch == '386' + run: sudo dpkg --add-architecture i386 && sudo apt update && sudo apt-get install -y libpcap0.8-dev:i386 + + - name: Install modules + run: go mod tidy + + - name: check git status + run: git --no-pager diff --exit-code + + - name: Test + run: | + CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ + go test \ + -exec 'sudo' \ + -timeout 10m ./signal/... + + test_signal: + name: "Signal / Unit" + needs: [build-cache] + strategy: + fail-fast: false + matrix: + arch: [ '386','amd64' ] + runs-on: ubuntu-22.04 + steps: + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: "1.23.x" + cache: false + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Get Go environment + run: | + echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV + echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV + + - name: Cache Go modules + uses: actions/cache/restore@v4 + with: + path: | + ${{ env.cache }} + ${{ env.modcache }} + key: ${{ runner.os }}-gotest-cache-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-gotest-cache- + + - name: Install dependencies + run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev + + - name: Install 32-bit libpcap + if: matrix.arch == '386' + run: sudo dpkg --add-architecture i386 && sudo apt update && sudo apt-get install -y libpcap0.8-dev:i386 + + - name: Install modules + run: go mod tidy + + - name: check git status + run: git --no-pager diff --exit-code + + - name: Test + run: | + CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ + go test \ + -exec 'sudo' \ + -timeout 10m ./signal/... test_management: + name: "Management / Unit" needs: [ build-cache ] strategy: fail-fast: false @@ -194,10 +311,17 @@ jobs: run: docker pull mlsmaycon/warmed-mysql:8 - name: Test - run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -p 1 -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 10m $(go list ./... | grep /management) + run: | + CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ + NETBIRD_STORE_ENGINE=${{ matrix.store }} \ + go test -tags=devcert \ + -exec "sudo --preserve-env=CI,NETBIRD_STORE_ENGINE" \ + -timeout 10m ./management/... benchmark: + name: "Management / Benchmark" needs: [ build-cache ] + if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }} strategy: fail-fast: false matrix: @@ -254,10 +378,17 @@ jobs: run: docker pull mlsmaycon/warmed-mysql:8 - name: Test - run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -run=^$ -bench=. -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 20m ./... + run: | + CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ + NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true \ + go test -tags devcert -run=^$ -bench=. \ + -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' \ + -timeout 20m ./... api_benchmark: + name: "Management / Benchmark (API)" needs: [ build-cache ] + if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }} strategy: fail-fast: false matrix: @@ -312,12 +443,21 @@ jobs: - name: download mysql image if: matrix.store == 'mysql' run: docker pull mlsmaycon/warmed-mysql:8 - + - name: Test - run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -run=^$ -tags=benchmark -bench=. -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 30m $(go list -tags=benchmark ./... | grep /management) + run: | + CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ + NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true \ + go test -tags=benchmark \ + -run=^$ \ + -bench=. \ + -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' \ + -timeout 20m ./management/... api_integration_test: + name: "Management / Integration" needs: [ build-cache ] + if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }} strategy: fail-fast: false matrix: @@ -363,9 +503,15 @@ jobs: run: git --no-pager diff --exit-code - name: Test - run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -tags=integration -p 1 -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 30m $(go list -tags=integration ./... | grep /management) + run: | + CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ + NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true \ + go test -tags=integration \ + -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' \ + -timeout 10m ./management/... test_client_on_docker: + name: "Client (Docker) / Unit" needs: [ build-cache ] runs-on: ubuntu-20.04 steps: diff --git a/.github/workflows/golang-test-windows.yml b/.github/workflows/golang-test-windows.yml index 3a3c47052..d9ff0a84b 100644 --- a/.github/workflows/golang-test-windows.yml +++ b/.github/workflows/golang-test-windows.yml @@ -1,4 +1,4 @@ -name: Test Code Windows +name: "Windows" on: push: @@ -14,6 +14,7 @@ concurrency: jobs: test: + name: "Client / Unit" runs-on: windows-latest steps: - name: Checkout code @@ -65,7 +66,7 @@ jobs: - run: echo "files=$(go list ./... | ForEach-Object { $_ } | Where-Object { $_ -notmatch '/management' })" >> $env:GITHUB_ENV - name: test - run: PsExec64 -s -w ${{ github.workspace }} cmd.exe /c "C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe test -timeout 10m -p 1 ${{ env.files }} > test-out.txt 2>&1" + run: PsExec64 -s -w ${{ github.workspace }} cmd.exe /c "C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe test -tags=devcert -timeout 10m -p 1 ${{ env.files }} > test-out.txt 2>&1" - name: test output if: ${{ always() }} run: Get-Content test-out.txt diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 6705a34ec..ca075d30f 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -1,4 +1,4 @@ -name: golangci-lint +name: Lint on: [pull_request] permissions: @@ -27,7 +27,14 @@ jobs: fail-fast: false matrix: os: [macos-latest, windows-latest, ubuntu-latest] - name: lint + include: + - os: macos-latest + display_name: Darwin + - os: windows-latest + display_name: Windows + - os: ubuntu-latest + display_name: Linux + name: ${{ matrix.display_name }} runs-on: ${{ matrix.os }} timeout-minutes: 15 steps: diff --git a/.github/workflows/mobile-build-validation.yml b/.github/workflows/mobile-build-validation.yml index dcf461a34..569956a54 100644 --- a/.github/workflows/mobile-build-validation.yml +++ b/.github/workflows/mobile-build-validation.yml @@ -1,4 +1,4 @@ -name: Mobile build validation +name: Mobile on: push: @@ -12,6 +12,7 @@ concurrency: jobs: android_build: + name: "Android / Build" runs-on: ubuntu-latest steps: - name: Checkout repository @@ -47,6 +48,7 @@ jobs: CGO_ENABLED: 0 ANDROID_NDK_HOME: /usr/local/lib/android/sdk/ndk/23.1.7779620 ios_build: + name: "iOS / Build" runs-on: macos-latest steps: - name: Checkout repository diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 183cdb02c..04874bdf4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,10 +9,10 @@ on: pull_request: env: - SIGN_PIPE_VER: "v0.0.17" + SIGN_PIPE_VER: "v0.0.18" GORELEASER_VER: "v2.3.2" PRODUCT_NAME: "NetBird" - COPYRIGHT: "Wiretrustee UG (haftungsbeschreankt)" + COPYRIGHT: "NetBird GmbH" concurrency: group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }} diff --git a/.gitignore b/.gitignore index d0b4f82dd..abb728b19 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ infrastructure_files/setup.env infrastructure_files/setup-*.env .vscode .DS_Store +vendor/ diff --git a/.golangci.yaml b/.golangci.yaml index 44b03d0e1..461677c2e 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -103,7 +103,7 @@ linters: - predeclared # predeclared finds code that shadows one of Go's predeclared identifiers - revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint. - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed - - thelper # thelper detects Go test helpers without t.Helper() call and checks the consistency of test helpers. + # - thelper # thelper detects Go test helpers without t.Helper() call and checks the consistency of test helpers. - wastedassign # wastedassign finds wasted assignment statements issues: # Maximum count of issues with the same text. diff --git a/.goreleaser_ui.yaml b/.goreleaser_ui.yaml index 06577f4e3..1dd649d1b 100644 --- a/.goreleaser_ui.yaml +++ b/.goreleaser_ui.yaml @@ -50,10 +50,12 @@ nfpms: - netbird-ui formats: - deb + scripts: + postinstall: "release_files/ui-post-install.sh" contents: - src: client/ui/netbird.desktop dst: /usr/share/applications/netbird.desktop - - src: client/ui/netbird-systemtray-connected.png + - src: client/ui/netbird.png dst: /usr/share/pixmaps/netbird.png dependencies: - netbird @@ -67,10 +69,12 @@ nfpms: - netbird-ui formats: - rpm + scripts: + postinstall: "release_files/ui-post-install.sh" contents: - src: client/ui/netbird.desktop dst: /usr/share/applications/netbird.desktop - - src: client/ui/netbird-systemtray-connected.png + - src: client/ui/netbird.png dst: /usr/share/pixmaps/netbird.png dependencies: - netbird diff --git a/AUTHORS b/AUTHORS index f2b228766..f39620acc 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,3 +1,3 @@ Mikhail Bragin (https://github.com/braginini) Maycon Santos (https://github.com/mlsmaycon) -Wiretrustee UG (haftungsbeschränkt) +NetBird GmbH diff --git a/CONTRIBUTOR_LICENSE_AGREEMENT.md b/CONTRIBUTOR_LICENSE_AGREEMENT.md index c47eb813d..89e011ec1 100644 --- a/CONTRIBUTOR_LICENSE_AGREEMENT.md +++ b/CONTRIBUTOR_LICENSE_AGREEMENT.md @@ -3,10 +3,10 @@ We are incredibly thankful for the contributions we receive from the community. We require our external contributors to sign a Contributor License Agreement ("CLA") in order to ensure that our projects remain licensed under Free and Open Source licenses such -as BSD-3 while allowing Wiretrustee to build a sustainable business. +as BSD-3 while allowing NetBird to build a sustainable business. -Wiretrustee is committed to having a true Open Source Software ("OSS") license for -our software. A CLA enables Wiretrustee to safely commercialize our products +NetBird is committed to having a true Open Source Software ("OSS") license for +our software. A CLA enables NetBird to safely commercialize our products while keeping a standard OSS license with all the rights that license grants to users: the ability to use the project in their own projects or businesses, to republish modified source, or to completely fork the project. @@ -20,11 +20,11 @@ This is a human-readable summary of (and not a substitute for) the full agreemen This highlights only some of key terms of the CLA. It has no legal value and you should carefully review all the terms of the actual CLA before agreeing. -
  • Grant of copyright license. You give Wiretrustee permission to use your copyrighted work +
  • Grant of copyright license. You give NetBird permission to use your copyrighted work in commercial products.
  • -
  • Grant of patent license. If your contributed work uses a patent, you give Wiretrustee a +
  • Grant of patent license. If your contributed work uses a patent, you give NetBird a license to use that patent including within commercial products. You also agree that you have permission to grant this license.
  • @@ -45,7 +45,7 @@ more. # Why require a CLA? Agreeing to a CLA explicitly states that you are entitled to provide a contribution, that you cannot withdraw permission -to use your contribution at a later date, and that Wiretrustee has permission to use your contribution in our commercial +to use your contribution at a later date, and that NetBird has permission to use your contribution in our commercial products. This removes any ambiguities or uncertainties caused by not having a CLA and allows users and customers to confidently @@ -65,25 +65,25 @@ Follow the steps given by the bot to sign the CLA. This will require you to log information from your account) and to fill in a few additional details such as your name and email address. We will only use this information for CLA tracking; none of your submitted information will be used for marketing purposes. -You only have to sign the CLA once. Once you've signed the CLA, future contributions to any Wiretrustee project will not +You only have to sign the CLA once. Once you've signed the CLA, future contributions to any NetBird project will not require you to sign again. # Legal Terms and Agreement -In order to clarify the intellectual property license granted with Contributions from any person or entity, Wiretrustee -UG (haftungsbeschränkt) ("Wiretrustee") must have a Contributor License Agreement ("CLA") on file that has been signed +In order to clarify the intellectual property license granted with Contributions from any person or entity, NetBird +GmbH ("NetBird") must have a Contributor License Agreement ("CLA") on file that has been signed by each Contributor, indicating agreement to the license terms below. This license does not change your rights to use your own Contributions for any other purpose. You accept and agree to the following terms and conditions for Your present and future Contributions submitted to -Wiretrustee. Except for the license granted herein to Wiretrustee and recipients of software distributed by Wiretrustee, +NetBird. Except for the license granted herein to NetBird and recipients of software distributed by NetBird, You reserve all right, title, and interest in and to Your Contributions. 1. Definitions. ``` "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner - that is making this Agreement with Wiretrustee. For legal entities, the entity making a Contribution and all other + that is making this Agreement with NetBird. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty @@ -91,23 +91,23 @@ You reserve all right, title, and interest in and to Your Contributions. ``` ``` "Contribution" shall mean any original work of authorship, including any modifications or additions to - an existing work, that is or previously has been intentionally submitted by You to Wiretrustee for inclusion in, - or documentation of, any of the products owned or managed by Wiretrustee (the "Work"). + an existing work, that is or previously has been intentionally submitted by You to NetBird for inclusion in, + or documentation of, any of the products owned or managed by NetBird (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication - sent to Wiretrustee or its representatives, including but not limited to communication on electronic mailing lists, + sent to NetBird or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, - Wiretrustee for the purpose of discussing and improving the Work, but excluding communication that is conspicuously + NetBird for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." ``` -2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to Wiretrustee - and to recipients of software distributed by Wiretrustee a perpetual, worldwide, non-exclusive, no-charge, +2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to NetBird + and to recipients of software distributed by NetBird a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works. -3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to Wiretrustee and - to recipients of software distributed by Wiretrustee a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to NetBird and + to recipients of software distributed by NetBird a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which @@ -121,8 +121,8 @@ You reserve all right, title, and interest in and to Your Contributions. intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that you will have received permission from your current and future employers for all future Contributions, that your applicable employer has waived such rights for all of - your current and future Contributions to Wiretrustee, or that your employer has executed a separate Corporate CLA - with Wiretrustee. + your current and future Contributions to NetBird, or that your employer has executed a separate Corporate CLA + with NetBird. 5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of @@ -138,11 +138,11 @@ You reserve all right, title, and interest in and to Your Contributions. MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. -7. Should You wish to submit work that is not Your original creation, You may submit it to Wiretrustee separately from +7. Should You wish to submit work that is not Your original creation, You may submit it to NetBird separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]". -8. You agree to notify Wiretrustee of any facts or circumstances of which you become aware that would make these - representations inaccurate in any respect. \ No newline at end of file +8. You agree to notify NetBird of any facts or circumstances of which you become aware that would make these + representations inaccurate in any respect. diff --git a/LICENSE b/LICENSE index bddb455d5..7cba76dfd 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2022 Wiretrustee UG (haftungsbeschränkt) & AUTHORS +Copyright (c) 2022 NetBird GmbH & AUTHORS Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -10,4 +10,4 @@ Redistribution and use in source and binary forms, with or without modification, 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 0537710e9..7cee2f8dc 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,9 @@
    + + Webinar: How to Achieve Zero Trust Access to Kubernetes — Effortlessly + +
    +

    diff --git a/client/Dockerfile b/client/Dockerfile index 2f5ff14ae..35c1d04c2 100644 --- a/client/Dockerfile +++ b/client/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.21.0 +FROM alpine:3.21.3 RUN apk add --no-cache ca-certificates iptables ip6tables ENV NB_FOREGROUND_MODE=true ENTRYPOINT [ "/usr/local/bin/netbird","up"] diff --git a/client/Dockerfile-rootless b/client/Dockerfile-rootless index 62bcaf964..78314ba12 100644 --- a/client/Dockerfile-rootless +++ b/client/Dockerfile-rootless @@ -9,6 +9,7 @@ USER netbird:netbird ENV NB_FOREGROUND_MODE=true ENV NB_USE_NETSTACK_MODE=true +ENV NB_ENABLE_NETSTACK_LOCAL_FORWARDING=true ENV NB_CONFIG=config.json ENV NB_DAEMON_ADDR=unix://netbird.sock ENV NB_DISABLE_DNS=true diff --git a/client/android/login.go b/client/android/login.go index bb61edfa8..3d674c5be 100644 --- a/client/android/login.go +++ b/client/android/login.go @@ -162,7 +162,7 @@ func (a *Auth) login(urlOpener URLOpener) error { // check if we need to generate JWT token err := a.withBackOff(a.ctx, func() (err error) { - needsLogin, err = internal.IsLoginRequired(a.ctx, a.config.PrivateKey, a.config.ManagementURL, a.config.SSHKey) + needsLogin, err = internal.IsLoginRequired(a.ctx, a.config) return }) if err != nil { diff --git a/client/cmd/debug.go b/client/cmd/debug.go index c7ab87b47..c02f60aed 100644 --- a/client/cmd/debug.go +++ b/client/cmd/debug.go @@ -13,6 +13,7 @@ import ( "github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/server" + nbstatus "github.com/netbirdio/netbird/client/status" ) const errCloseConnection = "Failed to close connection: %v" @@ -85,7 +86,7 @@ func debugBundle(cmd *cobra.Command, _ []string) error { client := proto.NewDaemonServiceClient(conn) resp, err := client.DebugBundle(cmd.Context(), &proto.DebugBundleRequest{ Anonymize: anonymizeFlag, - Status: getStatusOutput(cmd), + Status: getStatusOutput(cmd, anonymizeFlag), SystemInfo: debugSystemInfoFlag, }) if err != nil { @@ -196,7 +197,7 @@ func runForDuration(cmd *cobra.Command, args []string) error { time.Sleep(3 * time.Second) headerPostUp := fmt.Sprintf("----- Netbird post-up - Timestamp: %s", time.Now().Format(time.RFC3339)) - statusOutput := fmt.Sprintf("%s\n%s", headerPostUp, getStatusOutput(cmd)) + statusOutput := fmt.Sprintf("%s\n%s", headerPostUp, getStatusOutput(cmd, anonymizeFlag)) if waitErr := waitForDurationOrCancel(cmd.Context(), duration, cmd); waitErr != nil { return waitErr @@ -206,7 +207,7 @@ func runForDuration(cmd *cobra.Command, args []string) error { cmd.Println("Creating debug bundle...") headerPreDown := fmt.Sprintf("----- Netbird pre-down - Timestamp: %s - Duration: %s", time.Now().Format(time.RFC3339), duration) - statusOutput = fmt.Sprintf("%s\n%s\n%s", statusOutput, headerPreDown, getStatusOutput(cmd)) + statusOutput = fmt.Sprintf("%s\n%s\n%s", statusOutput, headerPreDown, getStatusOutput(cmd, anonymizeFlag)) resp, err := client.DebugBundle(cmd.Context(), &proto.DebugBundleRequest{ Anonymize: anonymizeFlag, @@ -271,13 +272,15 @@ func setNetworkMapPersistence(cmd *cobra.Command, args []string) error { return nil } -func getStatusOutput(cmd *cobra.Command) string { +func getStatusOutput(cmd *cobra.Command, anon bool) string { var statusOutputString string statusResp, err := getStatus(cmd.Context()) if err != nil { cmd.PrintErrf("Failed to get status: %v\n", err) } else { - statusOutputString = parseToFullDetailSummary(convertToStatusOutputOverview(statusResp)) + statusOutputString = nbstatus.ParseToFullDetailSummary( + nbstatus.ConvertToStatusOutputOverview(statusResp, anon, "", nil, nil, nil), + ) } return statusOutputString } diff --git a/client/cmd/login.go b/client/cmd/login.go index c7dd0fda1..b91cedede 100644 --- a/client/cmd/login.go +++ b/client/cmd/login.go @@ -85,11 +85,17 @@ var loginCmd = &cobra.Command{ client := proto.NewDaemonServiceClient(conn) + var dnsLabelsReq []string + if dnsLabelsValidated != nil { + dnsLabelsReq = dnsLabelsValidated.ToSafeStringList() + } + loginRequest := proto.LoginRequest{ SetupKey: providedSetupKey, ManagementUrl: managementURL, IsLinuxDesktopClient: isLinuxRunningDesktop(), Hostname: hostName, + DnsLabels: dnsLabelsReq, } if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) { diff --git a/client/cmd/root.go b/client/cmd/root.go index 0305bacc8..b25c2750c 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -38,6 +38,7 @@ const ( extraIFaceBlackListFlag = "extra-iface-blacklist" dnsRouteIntervalFlag = "dns-router-interval" systemInfoFlag = "system-info" + blockLANAccessFlag = "block-lan-access" ) var ( @@ -73,6 +74,7 @@ var ( anonymizeFlag bool debugSystemInfoFlag bool dnsRouteInterval time.Duration + blockLANAccess bool rootCmd = &cobra.Command{ Use: "netbird", diff --git a/client/cmd/ssh.go b/client/cmd/ssh.go index 81e6c255a..f9dbc26fc 100644 --- a/client/cmd/ssh.go +++ b/client/cmd/ssh.go @@ -9,7 +9,6 @@ import ( "strings" "syscall" - log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/netbirdio/netbird/client/internal" @@ -73,7 +72,7 @@ var sshCmd = &cobra.Command{ go func() { // blocking if err := runSSH(sshctx, host, []byte(config.SSHKey), cmd); err != nil { - log.Debug(err) + cmd.Printf("Error: %v\n", err) os.Exit(1) } cancel() diff --git a/client/cmd/status.go b/client/cmd/status.go index fa4bff77b..0ddba8b2f 100644 --- a/client/cmd/status.go +++ b/client/cmd/status.go @@ -2,107 +2,20 @@ package cmd import ( "context" - "encoding/json" "fmt" "net" "net/netip" - "os" - "runtime" - "sort" "strings" - "time" "github.com/spf13/cobra" "google.golang.org/grpc/status" - "gopkg.in/yaml.v3" - "github.com/netbirdio/netbird/client/anonymize" "github.com/netbirdio/netbird/client/internal" - "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/proto" + nbstatus "github.com/netbirdio/netbird/client/status" "github.com/netbirdio/netbird/util" - "github.com/netbirdio/netbird/version" ) -type peerStateDetailOutput struct { - FQDN string `json:"fqdn" yaml:"fqdn"` - IP string `json:"netbirdIp" yaml:"netbirdIp"` - PubKey string `json:"publicKey" yaml:"publicKey"` - Status string `json:"status" yaml:"status"` - LastStatusUpdate time.Time `json:"lastStatusUpdate" yaml:"lastStatusUpdate"` - ConnType string `json:"connectionType" yaml:"connectionType"` - IceCandidateType iceCandidateType `json:"iceCandidateType" yaml:"iceCandidateType"` - IceCandidateEndpoint iceCandidateType `json:"iceCandidateEndpoint" yaml:"iceCandidateEndpoint"` - RelayAddress string `json:"relayAddress" yaml:"relayAddress"` - LastWireguardHandshake time.Time `json:"lastWireguardHandshake" yaml:"lastWireguardHandshake"` - TransferReceived int64 `json:"transferReceived" yaml:"transferReceived"` - TransferSent int64 `json:"transferSent" yaml:"transferSent"` - Latency time.Duration `json:"latency" yaml:"latency"` - RosenpassEnabled bool `json:"quantumResistance" yaml:"quantumResistance"` - Routes []string `json:"routes" yaml:"routes"` - Networks []string `json:"networks" yaml:"networks"` -} - -type peersStateOutput struct { - Total int `json:"total" yaml:"total"` - Connected int `json:"connected" yaml:"connected"` - Details []peerStateDetailOutput `json:"details" yaml:"details"` -} - -type signalStateOutput struct { - URL string `json:"url" yaml:"url"` - Connected bool `json:"connected" yaml:"connected"` - Error string `json:"error" yaml:"error"` -} - -type managementStateOutput struct { - URL string `json:"url" yaml:"url"` - Connected bool `json:"connected" yaml:"connected"` - Error string `json:"error" yaml:"error"` -} - -type relayStateOutputDetail struct { - URI string `json:"uri" yaml:"uri"` - Available bool `json:"available" yaml:"available"` - Error string `json:"error" yaml:"error"` -} - -type relayStateOutput struct { - Total int `json:"total" yaml:"total"` - Available int `json:"available" yaml:"available"` - Details []relayStateOutputDetail `json:"details" yaml:"details"` -} - -type iceCandidateType struct { - Local string `json:"local" yaml:"local"` - Remote string `json:"remote" yaml:"remote"` -} - -type nsServerGroupStateOutput struct { - Servers []string `json:"servers" yaml:"servers"` - Domains []string `json:"domains" yaml:"domains"` - Enabled bool `json:"enabled" yaml:"enabled"` - Error string `json:"error" yaml:"error"` -} - -type statusOutputOverview struct { - Peers peersStateOutput `json:"peers" yaml:"peers"` - CliVersion string `json:"cliVersion" yaml:"cliVersion"` - DaemonVersion string `json:"daemonVersion" yaml:"daemonVersion"` - ManagementState managementStateOutput `json:"management" yaml:"management"` - SignalState signalStateOutput `json:"signal" yaml:"signal"` - Relays relayStateOutput `json:"relays" yaml:"relays"` - IP string `json:"netbirdIp" yaml:"netbirdIp"` - PubKey string `json:"publicKey" yaml:"publicKey"` - KernelInterface bool `json:"usesKernelInterface" yaml:"usesKernelInterface"` - FQDN string `json:"fqdn" yaml:"fqdn"` - RosenpassEnabled bool `json:"quantumResistance" yaml:"quantumResistance"` - RosenpassPermissive bool `json:"quantumResistancePermissive" yaml:"quantumResistancePermissive"` - Routes []string `json:"routes" yaml:"routes"` - Networks []string `json:"networks" yaml:"networks"` - NSServerGroups []nsServerGroupStateOutput `json:"dnsServers" yaml:"dnsServers"` -} - var ( detailFlag bool ipv4Flag bool @@ -173,18 +86,17 @@ func statusFunc(cmd *cobra.Command, args []string) error { return nil } - outputInformationHolder := convertToStatusOutputOverview(resp) - + var outputInformationHolder = nbstatus.ConvertToStatusOutputOverview(resp, anonymizeFlag, statusFilter, prefixNamesFilter, prefixNamesFilterMap, ipsFilterMap) var statusOutputString string switch { case detailFlag: - statusOutputString = parseToFullDetailSummary(outputInformationHolder) + statusOutputString = nbstatus.ParseToFullDetailSummary(outputInformationHolder) case jsonFlag: - statusOutputString, err = parseToJSON(outputInformationHolder) + statusOutputString, err = nbstatus.ParseToJSON(outputInformationHolder) case yamlFlag: - statusOutputString, err = parseToYAML(outputInformationHolder) + statusOutputString, err = nbstatus.ParseToYAML(outputInformationHolder) default: - statusOutputString = parseGeneralSummary(outputInformationHolder, false, false, false) + statusOutputString = nbstatus.ParseGeneralSummary(outputInformationHolder, false, false, false) } if err != nil { @@ -214,7 +126,6 @@ func getStatus(ctx context.Context) (*proto.StatusResponse, error) { } func parseFilters() error { - switch strings.ToLower(statusFilter) { case "", "disconnected", "connected": if strings.ToLower(statusFilter) != "" { @@ -251,175 +162,6 @@ func enableDetailFlagWhenFilterFlag() { } } -func convertToStatusOutputOverview(resp *proto.StatusResponse) statusOutputOverview { - pbFullStatus := resp.GetFullStatus() - - managementState := pbFullStatus.GetManagementState() - managementOverview := managementStateOutput{ - URL: managementState.GetURL(), - Connected: managementState.GetConnected(), - Error: managementState.Error, - } - - signalState := pbFullStatus.GetSignalState() - signalOverview := signalStateOutput{ - URL: signalState.GetURL(), - Connected: signalState.GetConnected(), - Error: signalState.Error, - } - - relayOverview := mapRelays(pbFullStatus.GetRelays()) - peersOverview := mapPeers(resp.GetFullStatus().GetPeers()) - - overview := statusOutputOverview{ - Peers: peersOverview, - CliVersion: version.NetbirdVersion(), - DaemonVersion: resp.GetDaemonVersion(), - ManagementState: managementOverview, - SignalState: signalOverview, - Relays: relayOverview, - IP: pbFullStatus.GetLocalPeerState().GetIP(), - PubKey: pbFullStatus.GetLocalPeerState().GetPubKey(), - KernelInterface: pbFullStatus.GetLocalPeerState().GetKernelInterface(), - FQDN: pbFullStatus.GetLocalPeerState().GetFqdn(), - RosenpassEnabled: pbFullStatus.GetLocalPeerState().GetRosenpassEnabled(), - RosenpassPermissive: pbFullStatus.GetLocalPeerState().GetRosenpassPermissive(), - Routes: pbFullStatus.GetLocalPeerState().GetNetworks(), - Networks: pbFullStatus.GetLocalPeerState().GetNetworks(), - NSServerGroups: mapNSGroups(pbFullStatus.GetDnsServers()), - } - - if anonymizeFlag { - anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses()) - anonymizeOverview(anonymizer, &overview) - } - - return overview -} - -func mapRelays(relays []*proto.RelayState) relayStateOutput { - var relayStateDetail []relayStateOutputDetail - - var relaysAvailable int - for _, relay := range relays { - available := relay.GetAvailable() - relayStateDetail = append(relayStateDetail, - relayStateOutputDetail{ - URI: relay.URI, - Available: available, - Error: relay.GetError(), - }, - ) - - if available { - relaysAvailable++ - } - } - - return relayStateOutput{ - Total: len(relays), - Available: relaysAvailable, - Details: relayStateDetail, - } -} - -func mapNSGroups(servers []*proto.NSGroupState) []nsServerGroupStateOutput { - mappedNSGroups := make([]nsServerGroupStateOutput, 0, len(servers)) - for _, pbNsGroupServer := range servers { - mappedNSGroups = append(mappedNSGroups, nsServerGroupStateOutput{ - Servers: pbNsGroupServer.GetServers(), - Domains: pbNsGroupServer.GetDomains(), - Enabled: pbNsGroupServer.GetEnabled(), - Error: pbNsGroupServer.GetError(), - }) - } - return mappedNSGroups -} - -func mapPeers(peers []*proto.PeerState) peersStateOutput { - var peersStateDetail []peerStateDetailOutput - peersConnected := 0 - for _, pbPeerState := range peers { - localICE := "" - remoteICE := "" - localICEEndpoint := "" - remoteICEEndpoint := "" - relayServerAddress := "" - connType := "" - lastHandshake := time.Time{} - transferReceived := int64(0) - transferSent := int64(0) - - isPeerConnected := pbPeerState.ConnStatus == peer.StatusConnected.String() - if skipDetailByFilters(pbPeerState, isPeerConnected) { - continue - } - if isPeerConnected { - peersConnected++ - - localICE = pbPeerState.GetLocalIceCandidateType() - remoteICE = pbPeerState.GetRemoteIceCandidateType() - localICEEndpoint = pbPeerState.GetLocalIceCandidateEndpoint() - remoteICEEndpoint = pbPeerState.GetRemoteIceCandidateEndpoint() - connType = "P2P" - if pbPeerState.Relayed { - connType = "Relayed" - } - relayServerAddress = pbPeerState.GetRelayAddress() - lastHandshake = pbPeerState.GetLastWireguardHandshake().AsTime().Local() - transferReceived = pbPeerState.GetBytesRx() - transferSent = pbPeerState.GetBytesTx() - } - - timeLocal := pbPeerState.GetConnStatusUpdate().AsTime().Local() - peerState := peerStateDetailOutput{ - IP: pbPeerState.GetIP(), - PubKey: pbPeerState.GetPubKey(), - Status: pbPeerState.GetConnStatus(), - LastStatusUpdate: timeLocal, - ConnType: connType, - IceCandidateType: iceCandidateType{ - Local: localICE, - Remote: remoteICE, - }, - IceCandidateEndpoint: iceCandidateType{ - Local: localICEEndpoint, - Remote: remoteICEEndpoint, - }, - RelayAddress: relayServerAddress, - FQDN: pbPeerState.GetFqdn(), - LastWireguardHandshake: lastHandshake, - TransferReceived: transferReceived, - TransferSent: transferSent, - Latency: pbPeerState.GetLatency().AsDuration(), - RosenpassEnabled: pbPeerState.GetRosenpassEnabled(), - Routes: pbPeerState.GetNetworks(), - Networks: pbPeerState.GetNetworks(), - } - - peersStateDetail = append(peersStateDetail, peerState) - } - - sortPeersByIP(peersStateDetail) - - peersOverview := peersStateOutput{ - Total: len(peersStateDetail), - Connected: peersConnected, - Details: peersStateDetail, - } - return peersOverview -} - -func sortPeersByIP(peersStateDetail []peerStateDetailOutput) { - if len(peersStateDetail) > 0 { - sort.SliceStable(peersStateDetail, func(i, j int) bool { - iAddr, _ := netip.ParseAddr(peersStateDetail[i].IP) - jAddr, _ := netip.ParseAddr(peersStateDetail[j].IP) - return iAddr.Compare(jAddr) == -1 - }) - } -} - func parseInterfaceIP(interfaceIP string) string { ip, _, err := net.ParseCIDR(interfaceIP) if err != nil { @@ -427,452 +169,3 @@ func parseInterfaceIP(interfaceIP string) string { } return fmt.Sprintf("%s\n", ip) } - -func parseToJSON(overview statusOutputOverview) (string, error) { - jsonBytes, err := json.Marshal(overview) - if err != nil { - return "", fmt.Errorf("json marshal failed") - } - return string(jsonBytes), err -} - -func parseToYAML(overview statusOutputOverview) (string, error) { - yamlBytes, err := yaml.Marshal(overview) - if err != nil { - return "", fmt.Errorf("yaml marshal failed") - } - return string(yamlBytes), nil -} - -func parseGeneralSummary(overview statusOutputOverview, showURL bool, showRelays bool, showNameServers bool) string { - var managementConnString string - if overview.ManagementState.Connected { - managementConnString = "Connected" - if showURL { - managementConnString = fmt.Sprintf("%s to %s", managementConnString, overview.ManagementState.URL) - } - } else { - managementConnString = "Disconnected" - if overview.ManagementState.Error != "" { - managementConnString = fmt.Sprintf("%s, reason: %s", managementConnString, overview.ManagementState.Error) - } - } - - var signalConnString string - if overview.SignalState.Connected { - signalConnString = "Connected" - if showURL { - signalConnString = fmt.Sprintf("%s to %s", signalConnString, overview.SignalState.URL) - } - } else { - signalConnString = "Disconnected" - if overview.SignalState.Error != "" { - signalConnString = fmt.Sprintf("%s, reason: %s", signalConnString, overview.SignalState.Error) - } - } - - interfaceTypeString := "Userspace" - interfaceIP := overview.IP - if overview.KernelInterface { - interfaceTypeString = "Kernel" - } else if overview.IP == "" { - interfaceTypeString = "N/A" - interfaceIP = "N/A" - } - - var relaysString string - if showRelays { - for _, relay := range overview.Relays.Details { - available := "Available" - reason := "" - if !relay.Available { - available = "Unavailable" - reason = fmt.Sprintf(", reason: %s", relay.Error) - } - relaysString += fmt.Sprintf("\n [%s] is %s%s", relay.URI, available, reason) - } - } else { - relaysString = fmt.Sprintf("%d/%d Available", overview.Relays.Available, overview.Relays.Total) - } - - networks := "-" - if len(overview.Networks) > 0 { - sort.Strings(overview.Networks) - networks = strings.Join(overview.Networks, ", ") - } - - var dnsServersString string - if showNameServers { - for _, nsServerGroup := range overview.NSServerGroups { - enabled := "Available" - if !nsServerGroup.Enabled { - enabled = "Unavailable" - } - errorString := "" - if nsServerGroup.Error != "" { - errorString = fmt.Sprintf(", reason: %s", nsServerGroup.Error) - errorString = strings.TrimSpace(errorString) - } - - domainsString := strings.Join(nsServerGroup.Domains, ", ") - if domainsString == "" { - domainsString = "." // Show "." for the default zone - } - dnsServersString += fmt.Sprintf( - "\n [%s] for [%s] is %s%s", - strings.Join(nsServerGroup.Servers, ", "), - domainsString, - enabled, - errorString, - ) - } - } else { - dnsServersString = fmt.Sprintf("%d/%d Available", countEnabled(overview.NSServerGroups), len(overview.NSServerGroups)) - } - - rosenpassEnabledStatus := "false" - if overview.RosenpassEnabled { - rosenpassEnabledStatus = "true" - if overview.RosenpassPermissive { - rosenpassEnabledStatus = "true (permissive)" //nolint:gosec - } - } - - peersCountString := fmt.Sprintf("%d/%d Connected", overview.Peers.Connected, overview.Peers.Total) - - goos := runtime.GOOS - goarch := runtime.GOARCH - goarm := "" - if goarch == "arm" { - goarm = fmt.Sprintf(" (ARMv%s)", os.Getenv("GOARM")) - } - - summary := fmt.Sprintf( - "OS: %s\n"+ - "Daemon version: %s\n"+ - "CLI version: %s\n"+ - "Management: %s\n"+ - "Signal: %s\n"+ - "Relays: %s\n"+ - "Nameservers: %s\n"+ - "FQDN: %s\n"+ - "NetBird IP: %s\n"+ - "Interface type: %s\n"+ - "Quantum resistance: %s\n"+ - "Routes: %s\n"+ - "Networks: %s\n"+ - "Peers count: %s\n", - fmt.Sprintf("%s/%s%s", goos, goarch, goarm), - overview.DaemonVersion, - version.NetbirdVersion(), - managementConnString, - signalConnString, - relaysString, - dnsServersString, - overview.FQDN, - interfaceIP, - interfaceTypeString, - rosenpassEnabledStatus, - networks, - networks, - peersCountString, - ) - return summary -} - -func parseToFullDetailSummary(overview statusOutputOverview) string { - parsedPeersString := parsePeers(overview.Peers, overview.RosenpassEnabled, overview.RosenpassPermissive) - summary := parseGeneralSummary(overview, true, true, true) - - return fmt.Sprintf( - "Peers detail:"+ - "%s\n"+ - "%s", - parsedPeersString, - summary, - ) -} - -func parsePeers(peers peersStateOutput, rosenpassEnabled, rosenpassPermissive bool) string { - var ( - peersString = "" - ) - - for _, peerState := range peers.Details { - - localICE := "-" - if peerState.IceCandidateType.Local != "" { - localICE = peerState.IceCandidateType.Local - } - - remoteICE := "-" - if peerState.IceCandidateType.Remote != "" { - remoteICE = peerState.IceCandidateType.Remote - } - - localICEEndpoint := "-" - if peerState.IceCandidateEndpoint.Local != "" { - localICEEndpoint = peerState.IceCandidateEndpoint.Local - } - - remoteICEEndpoint := "-" - if peerState.IceCandidateEndpoint.Remote != "" { - remoteICEEndpoint = peerState.IceCandidateEndpoint.Remote - } - - rosenpassEnabledStatus := "false" - if rosenpassEnabled { - if peerState.RosenpassEnabled { - rosenpassEnabledStatus = "true" - } else { - if rosenpassPermissive { - rosenpassEnabledStatus = "false (remote didn't enable quantum resistance)" - } else { - rosenpassEnabledStatus = "false (connection won't work without a permissive mode)" - } - } - } else { - if peerState.RosenpassEnabled { - rosenpassEnabledStatus = "false (connection might not work without a remote permissive mode)" - } - } - - networks := "-" - if len(peerState.Networks) > 0 { - sort.Strings(peerState.Networks) - networks = strings.Join(peerState.Networks, ", ") - } - - peerString := fmt.Sprintf( - "\n %s:\n"+ - " NetBird IP: %s\n"+ - " Public key: %s\n"+ - " Status: %s\n"+ - " -- detail --\n"+ - " Connection type: %s\n"+ - " ICE candidate (Local/Remote): %s/%s\n"+ - " ICE candidate endpoints (Local/Remote): %s/%s\n"+ - " Relay server address: %s\n"+ - " Last connection update: %s\n"+ - " Last WireGuard handshake: %s\n"+ - " Transfer status (received/sent) %s/%s\n"+ - " Quantum resistance: %s\n"+ - " Routes: %s\n"+ - " Networks: %s\n"+ - " Latency: %s\n", - peerState.FQDN, - peerState.IP, - peerState.PubKey, - peerState.Status, - peerState.ConnType, - localICE, - remoteICE, - localICEEndpoint, - remoteICEEndpoint, - peerState.RelayAddress, - timeAgo(peerState.LastStatusUpdate), - timeAgo(peerState.LastWireguardHandshake), - toIEC(peerState.TransferReceived), - toIEC(peerState.TransferSent), - rosenpassEnabledStatus, - networks, - networks, - peerState.Latency.String(), - ) - - peersString += peerString - } - return peersString -} - -func skipDetailByFilters(peerState *proto.PeerState, isConnected bool) bool { - statusEval := false - ipEval := false - nameEval := true - - if statusFilter != "" { - lowerStatusFilter := strings.ToLower(statusFilter) - if lowerStatusFilter == "disconnected" && isConnected { - statusEval = true - } else if lowerStatusFilter == "connected" && !isConnected { - statusEval = true - } - } - - if len(ipsFilter) > 0 { - _, ok := ipsFilterMap[peerState.IP] - if !ok { - ipEval = true - } - } - - if len(prefixNamesFilter) > 0 { - for prefixNameFilter := range prefixNamesFilterMap { - if strings.HasPrefix(peerState.Fqdn, prefixNameFilter) { - nameEval = false - break - } - } - } else { - nameEval = false - } - - return statusEval || ipEval || nameEval -} - -func toIEC(b int64) string { - const unit = 1024 - if b < unit { - return fmt.Sprintf("%d B", b) - } - div, exp := int64(unit), 0 - for n := b / unit; n >= unit; n /= unit { - div *= unit - exp++ - } - return fmt.Sprintf("%.1f %ciB", - float64(b)/float64(div), "KMGTPE"[exp]) -} - -func countEnabled(dnsServers []nsServerGroupStateOutput) int { - count := 0 - for _, server := range dnsServers { - if server.Enabled { - count++ - } - } - return count -} - -// timeAgo returns a string representing the duration since the provided time in a human-readable format. -func timeAgo(t time.Time) string { - if t.IsZero() || t.Equal(time.Unix(0, 0)) { - return "-" - } - duration := time.Since(t) - switch { - case duration < time.Second: - return "Now" - case duration < time.Minute: - seconds := int(duration.Seconds()) - if seconds == 1 { - return "1 second ago" - } - return fmt.Sprintf("%d seconds ago", seconds) - case duration < time.Hour: - minutes := int(duration.Minutes()) - seconds := int(duration.Seconds()) % 60 - if minutes == 1 { - if seconds == 1 { - return "1 minute, 1 second ago" - } else if seconds > 0 { - return fmt.Sprintf("1 minute, %d seconds ago", seconds) - } - return "1 minute ago" - } - if seconds > 0 { - return fmt.Sprintf("%d minutes, %d seconds ago", minutes, seconds) - } - return fmt.Sprintf("%d minutes ago", minutes) - case duration < 24*time.Hour: - hours := int(duration.Hours()) - minutes := int(duration.Minutes()) % 60 - if hours == 1 { - if minutes == 1 { - return "1 hour, 1 minute ago" - } else if minutes > 0 { - return fmt.Sprintf("1 hour, %d minutes ago", minutes) - } - return "1 hour ago" - } - if minutes > 0 { - return fmt.Sprintf("%d hours, %d minutes ago", hours, minutes) - } - return fmt.Sprintf("%d hours ago", hours) - } - - days := int(duration.Hours()) / 24 - hours := int(duration.Hours()) % 24 - if days == 1 { - if hours == 1 { - return "1 day, 1 hour ago" - } else if hours > 0 { - return fmt.Sprintf("1 day, %d hours ago", hours) - } - return "1 day ago" - } - if hours > 0 { - return fmt.Sprintf("%d days, %d hours ago", days, hours) - } - return fmt.Sprintf("%d days ago", days) -} - -func anonymizePeerDetail(a *anonymize.Anonymizer, peer *peerStateDetailOutput) { - peer.FQDN = a.AnonymizeDomain(peer.FQDN) - if localIP, port, err := net.SplitHostPort(peer.IceCandidateEndpoint.Local); err == nil { - peer.IceCandidateEndpoint.Local = fmt.Sprintf("%s:%s", a.AnonymizeIPString(localIP), port) - } - if remoteIP, port, err := net.SplitHostPort(peer.IceCandidateEndpoint.Remote); err == nil { - peer.IceCandidateEndpoint.Remote = fmt.Sprintf("%s:%s", a.AnonymizeIPString(remoteIP), port) - } - - peer.RelayAddress = a.AnonymizeURI(peer.RelayAddress) - - for i, route := range peer.Networks { - peer.Networks[i] = a.AnonymizeIPString(route) - } - - for i, route := range peer.Networks { - peer.Networks[i] = a.AnonymizeRoute(route) - } - - for i, route := range peer.Routes { - peer.Routes[i] = a.AnonymizeIPString(route) - } - - for i, route := range peer.Routes { - peer.Routes[i] = a.AnonymizeRoute(route) - } -} - -func anonymizeOverview(a *anonymize.Anonymizer, overview *statusOutputOverview) { - for i, peer := range overview.Peers.Details { - peer := peer - anonymizePeerDetail(a, &peer) - overview.Peers.Details[i] = peer - } - - overview.ManagementState.URL = a.AnonymizeURI(overview.ManagementState.URL) - overview.ManagementState.Error = a.AnonymizeString(overview.ManagementState.Error) - overview.SignalState.URL = a.AnonymizeURI(overview.SignalState.URL) - overview.SignalState.Error = a.AnonymizeString(overview.SignalState.Error) - - overview.IP = a.AnonymizeIPString(overview.IP) - for i, detail := range overview.Relays.Details { - detail.URI = a.AnonymizeURI(detail.URI) - detail.Error = a.AnonymizeString(detail.Error) - overview.Relays.Details[i] = detail - } - - for i, nsGroup := range overview.NSServerGroups { - for j, domain := range nsGroup.Domains { - overview.NSServerGroups[i].Domains[j] = a.AnonymizeDomain(domain) - } - for j, ns := range nsGroup.Servers { - host, port, err := net.SplitHostPort(ns) - if err == nil { - overview.NSServerGroups[i].Servers[j] = fmt.Sprintf("%s:%s", a.AnonymizeIPString(host), port) - } - } - } - - for i, route := range overview.Networks { - overview.Networks[i] = a.AnonymizeRoute(route) - } - - for i, route := range overview.Routes { - overview.Routes[i] = a.AnonymizeRoute(route) - } - - overview.FQDN = a.AnonymizeDomain(overview.FQDN) -} diff --git a/client/cmd/status_test.go b/client/cmd/status_test.go index 1f1e95726..03608eab0 100644 --- a/client/cmd/status_test.go +++ b/client/cmd/status_test.go @@ -1,597 +1,11 @@ package cmd import ( - "bytes" - "encoding/json" - "fmt" - "runtime" "testing" - "time" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/types/known/durationpb" - "google.golang.org/protobuf/types/known/timestamppb" - - "github.com/netbirdio/netbird/client/proto" - "github.com/netbirdio/netbird/version" ) -func init() { - loc, err := time.LoadLocation("UTC") - if err != nil { - panic(err) - } - - time.Local = loc -} - -var resp = &proto.StatusResponse{ - Status: "Connected", - FullStatus: &proto.FullStatus{ - Peers: []*proto.PeerState{ - { - IP: "192.168.178.101", - PubKey: "Pubkey1", - Fqdn: "peer-1.awesome-domain.com", - ConnStatus: "Connected", - ConnStatusUpdate: timestamppb.New(time.Date(2001, time.Month(1), 1, 1, 1, 1, 0, time.UTC)), - Relayed: false, - LocalIceCandidateType: "", - RemoteIceCandidateType: "", - LocalIceCandidateEndpoint: "", - RemoteIceCandidateEndpoint: "", - LastWireguardHandshake: timestamppb.New(time.Date(2001, time.Month(1), 1, 1, 1, 2, 0, time.UTC)), - BytesRx: 200, - BytesTx: 100, - Networks: []string{ - "10.1.0.0/24", - }, - Latency: durationpb.New(time.Duration(10000000)), - }, - { - IP: "192.168.178.102", - PubKey: "Pubkey2", - Fqdn: "peer-2.awesome-domain.com", - ConnStatus: "Connected", - ConnStatusUpdate: timestamppb.New(time.Date(2002, time.Month(2), 2, 2, 2, 2, 0, time.UTC)), - Relayed: true, - LocalIceCandidateType: "relay", - RemoteIceCandidateType: "prflx", - LocalIceCandidateEndpoint: "10.0.0.1:10001", - RemoteIceCandidateEndpoint: "10.0.10.1:10002", - LastWireguardHandshake: timestamppb.New(time.Date(2002, time.Month(2), 2, 2, 2, 3, 0, time.UTC)), - BytesRx: 2000, - BytesTx: 1000, - Latency: durationpb.New(time.Duration(10000000)), - }, - }, - ManagementState: &proto.ManagementState{ - URL: "my-awesome-management.com:443", - Connected: true, - Error: "", - }, - SignalState: &proto.SignalState{ - URL: "my-awesome-signal.com:443", - Connected: true, - Error: "", - }, - Relays: []*proto.RelayState{ - { - URI: "stun:my-awesome-stun.com:3478", - Available: true, - Error: "", - }, - { - URI: "turns:my-awesome-turn.com:443?transport=tcp", - Available: false, - Error: "context: deadline exceeded", - }, - }, - LocalPeerState: &proto.LocalPeerState{ - IP: "192.168.178.100/16", - PubKey: "Some-Pub-Key", - KernelInterface: true, - Fqdn: "some-localhost.awesome-domain.com", - Networks: []string{ - "10.10.0.0/24", - }, - }, - DnsServers: []*proto.NSGroupState{ - { - Servers: []string{ - "8.8.8.8:53", - }, - Domains: nil, - Enabled: true, - Error: "", - }, - { - Servers: []string{ - "1.1.1.1:53", - "2.2.2.2:53", - }, - Domains: []string{ - "example.com", - "example.net", - }, - Enabled: false, - Error: "timeout", - }, - }, - }, - DaemonVersion: "0.14.1", -} - -var overview = statusOutputOverview{ - Peers: peersStateOutput{ - Total: 2, - Connected: 2, - Details: []peerStateDetailOutput{ - { - IP: "192.168.178.101", - PubKey: "Pubkey1", - FQDN: "peer-1.awesome-domain.com", - Status: "Connected", - LastStatusUpdate: time.Date(2001, 1, 1, 1, 1, 1, 0, time.UTC), - ConnType: "P2P", - IceCandidateType: iceCandidateType{ - Local: "", - Remote: "", - }, - IceCandidateEndpoint: iceCandidateType{ - Local: "", - Remote: "", - }, - LastWireguardHandshake: time.Date(2001, 1, 1, 1, 1, 2, 0, time.UTC), - TransferReceived: 200, - TransferSent: 100, - Routes: []string{ - "10.1.0.0/24", - }, - Networks: []string{ - "10.1.0.0/24", - }, - Latency: time.Duration(10000000), - }, - { - IP: "192.168.178.102", - PubKey: "Pubkey2", - FQDN: "peer-2.awesome-domain.com", - Status: "Connected", - LastStatusUpdate: time.Date(2002, 2, 2, 2, 2, 2, 0, time.UTC), - ConnType: "Relayed", - IceCandidateType: iceCandidateType{ - Local: "relay", - Remote: "prflx", - }, - IceCandidateEndpoint: iceCandidateType{ - Local: "10.0.0.1:10001", - Remote: "10.0.10.1:10002", - }, - LastWireguardHandshake: time.Date(2002, 2, 2, 2, 2, 3, 0, time.UTC), - TransferReceived: 2000, - TransferSent: 1000, - Latency: time.Duration(10000000), - }, - }, - }, - CliVersion: version.NetbirdVersion(), - DaemonVersion: "0.14.1", - ManagementState: managementStateOutput{ - URL: "my-awesome-management.com:443", - Connected: true, - Error: "", - }, - SignalState: signalStateOutput{ - URL: "my-awesome-signal.com:443", - Connected: true, - Error: "", - }, - Relays: relayStateOutput{ - Total: 2, - Available: 1, - Details: []relayStateOutputDetail{ - { - URI: "stun:my-awesome-stun.com:3478", - Available: true, - Error: "", - }, - { - URI: "turns:my-awesome-turn.com:443?transport=tcp", - Available: false, - Error: "context: deadline exceeded", - }, - }, - }, - IP: "192.168.178.100/16", - PubKey: "Some-Pub-Key", - KernelInterface: true, - FQDN: "some-localhost.awesome-domain.com", - NSServerGroups: []nsServerGroupStateOutput{ - { - Servers: []string{ - "8.8.8.8:53", - }, - Domains: nil, - Enabled: true, - Error: "", - }, - { - Servers: []string{ - "1.1.1.1:53", - "2.2.2.2:53", - }, - Domains: []string{ - "example.com", - "example.net", - }, - Enabled: false, - Error: "timeout", - }, - }, - Routes: []string{ - "10.10.0.0/24", - }, - Networks: []string{ - "10.10.0.0/24", - }, -} - -func TestConversionFromFullStatusToOutputOverview(t *testing.T) { - convertedResult := convertToStatusOutputOverview(resp) - - assert.Equal(t, overview, convertedResult) -} - -func TestSortingOfPeers(t *testing.T) { - peers := []peerStateDetailOutput{ - { - IP: "192.168.178.104", - }, - { - IP: "192.168.178.102", - }, - { - IP: "192.168.178.101", - }, - { - IP: "192.168.178.105", - }, - { - IP: "192.168.178.103", - }, - } - - sortPeersByIP(peers) - - assert.Equal(t, peers[3].IP, "192.168.178.104") -} - -func TestParsingToJSON(t *testing.T) { - jsonString, _ := parseToJSON(overview) - - //@formatter:off - expectedJSONString := ` - { - "peers": { - "total": 2, - "connected": 2, - "details": [ - { - "fqdn": "peer-1.awesome-domain.com", - "netbirdIp": "192.168.178.101", - "publicKey": "Pubkey1", - "status": "Connected", - "lastStatusUpdate": "2001-01-01T01:01:01Z", - "connectionType": "P2P", - "iceCandidateType": { - "local": "", - "remote": "" - }, - "iceCandidateEndpoint": { - "local": "", - "remote": "" - }, - "relayAddress": "", - "lastWireguardHandshake": "2001-01-01T01:01:02Z", - "transferReceived": 200, - "transferSent": 100, - "latency": 10000000, - "quantumResistance": false, - "routes": [ - "10.1.0.0/24" - ], - "networks": [ - "10.1.0.0/24" - ] - }, - { - "fqdn": "peer-2.awesome-domain.com", - "netbirdIp": "192.168.178.102", - "publicKey": "Pubkey2", - "status": "Connected", - "lastStatusUpdate": "2002-02-02T02:02:02Z", - "connectionType": "Relayed", - "iceCandidateType": { - "local": "relay", - "remote": "prflx" - }, - "iceCandidateEndpoint": { - "local": "10.0.0.1:10001", - "remote": "10.0.10.1:10002" - }, - "relayAddress": "", - "lastWireguardHandshake": "2002-02-02T02:02:03Z", - "transferReceived": 2000, - "transferSent": 1000, - "latency": 10000000, - "quantumResistance": false, - "routes": null, - "networks": null - } - ] - }, - "cliVersion": "development", - "daemonVersion": "0.14.1", - "management": { - "url": "my-awesome-management.com:443", - "connected": true, - "error": "" - }, - "signal": { - "url": "my-awesome-signal.com:443", - "connected": true, - "error": "" - }, - "relays": { - "total": 2, - "available": 1, - "details": [ - { - "uri": "stun:my-awesome-stun.com:3478", - "available": true, - "error": "" - }, - { - "uri": "turns:my-awesome-turn.com:443?transport=tcp", - "available": false, - "error": "context: deadline exceeded" - } - ] - }, - "netbirdIp": "192.168.178.100/16", - "publicKey": "Some-Pub-Key", - "usesKernelInterface": true, - "fqdn": "some-localhost.awesome-domain.com", - "quantumResistance": false, - "quantumResistancePermissive": false, - "routes": [ - "10.10.0.0/24" - ], - "networks": [ - "10.10.0.0/24" - ], - "dnsServers": [ - { - "servers": [ - "8.8.8.8:53" - ], - "domains": null, - "enabled": true, - "error": "" - }, - { - "servers": [ - "1.1.1.1:53", - "2.2.2.2:53" - ], - "domains": [ - "example.com", - "example.net" - ], - "enabled": false, - "error": "timeout" - } - ] - }` - // @formatter:on - - var expectedJSON bytes.Buffer - require.NoError(t, json.Compact(&expectedJSON, []byte(expectedJSONString))) - - assert.Equal(t, expectedJSON.String(), jsonString) -} - -func TestParsingToYAML(t *testing.T) { - yaml, _ := parseToYAML(overview) - - expectedYAML := - `peers: - total: 2 - connected: 2 - details: - - fqdn: peer-1.awesome-domain.com - netbirdIp: 192.168.178.101 - publicKey: Pubkey1 - status: Connected - lastStatusUpdate: 2001-01-01T01:01:01Z - connectionType: P2P - iceCandidateType: - local: "" - remote: "" - iceCandidateEndpoint: - local: "" - remote: "" - relayAddress: "" - lastWireguardHandshake: 2001-01-01T01:01:02Z - transferReceived: 200 - transferSent: 100 - latency: 10ms - quantumResistance: false - routes: - - 10.1.0.0/24 - networks: - - 10.1.0.0/24 - - fqdn: peer-2.awesome-domain.com - netbirdIp: 192.168.178.102 - publicKey: Pubkey2 - status: Connected - lastStatusUpdate: 2002-02-02T02:02:02Z - connectionType: Relayed - iceCandidateType: - local: relay - remote: prflx - iceCandidateEndpoint: - local: 10.0.0.1:10001 - remote: 10.0.10.1:10002 - relayAddress: "" - lastWireguardHandshake: 2002-02-02T02:02:03Z - transferReceived: 2000 - transferSent: 1000 - latency: 10ms - quantumResistance: false - routes: [] - networks: [] -cliVersion: development -daemonVersion: 0.14.1 -management: - url: my-awesome-management.com:443 - connected: true - error: "" -signal: - url: my-awesome-signal.com:443 - connected: true - error: "" -relays: - total: 2 - available: 1 - details: - - uri: stun:my-awesome-stun.com:3478 - available: true - error: "" - - uri: turns:my-awesome-turn.com:443?transport=tcp - available: false - error: 'context: deadline exceeded' -netbirdIp: 192.168.178.100/16 -publicKey: Some-Pub-Key -usesKernelInterface: true -fqdn: some-localhost.awesome-domain.com -quantumResistance: false -quantumResistancePermissive: false -routes: - - 10.10.0.0/24 -networks: - - 10.10.0.0/24 -dnsServers: - - servers: - - 8.8.8.8:53 - domains: [] - enabled: true - error: "" - - servers: - - 1.1.1.1:53 - - 2.2.2.2:53 - domains: - - example.com - - example.net - enabled: false - error: timeout -` - - assert.Equal(t, expectedYAML, yaml) -} - -func TestParsingToDetail(t *testing.T) { - // Calculate time ago based on the fixture dates - lastConnectionUpdate1 := timeAgo(overview.Peers.Details[0].LastStatusUpdate) - lastHandshake1 := timeAgo(overview.Peers.Details[0].LastWireguardHandshake) - lastConnectionUpdate2 := timeAgo(overview.Peers.Details[1].LastStatusUpdate) - lastHandshake2 := timeAgo(overview.Peers.Details[1].LastWireguardHandshake) - - detail := parseToFullDetailSummary(overview) - - expectedDetail := fmt.Sprintf( - `Peers detail: - peer-1.awesome-domain.com: - NetBird IP: 192.168.178.101 - Public key: Pubkey1 - Status: Connected - -- detail -- - Connection type: P2P - ICE candidate (Local/Remote): -/- - ICE candidate endpoints (Local/Remote): -/- - Relay server address: - Last connection update: %s - Last WireGuard handshake: %s - Transfer status (received/sent) 200 B/100 B - Quantum resistance: false - Routes: 10.1.0.0/24 - Networks: 10.1.0.0/24 - Latency: 10ms - - peer-2.awesome-domain.com: - NetBird IP: 192.168.178.102 - Public key: Pubkey2 - Status: Connected - -- detail -- - Connection type: Relayed - ICE candidate (Local/Remote): relay/prflx - ICE candidate endpoints (Local/Remote): 10.0.0.1:10001/10.0.10.1:10002 - Relay server address: - Last connection update: %s - Last WireGuard handshake: %s - Transfer status (received/sent) 2.0 KiB/1000 B - Quantum resistance: false - Routes: - - Networks: - - Latency: 10ms - -OS: %s/%s -Daemon version: 0.14.1 -CLI version: %s -Management: Connected to my-awesome-management.com:443 -Signal: Connected to my-awesome-signal.com:443 -Relays: - [stun:my-awesome-stun.com:3478] is Available - [turns:my-awesome-turn.com:443?transport=tcp] is Unavailable, reason: context: deadline exceeded -Nameservers: - [8.8.8.8:53] for [.] is Available - [1.1.1.1:53, 2.2.2.2:53] for [example.com, example.net] is Unavailable, reason: timeout -FQDN: some-localhost.awesome-domain.com -NetBird IP: 192.168.178.100/16 -Interface type: Kernel -Quantum resistance: false -Routes: 10.10.0.0/24 -Networks: 10.10.0.0/24 -Peers count: 2/2 Connected -`, lastConnectionUpdate1, lastHandshake1, lastConnectionUpdate2, lastHandshake2, runtime.GOOS, runtime.GOARCH, overview.CliVersion) - - assert.Equal(t, expectedDetail, detail) -} - -func TestParsingToShortVersion(t *testing.T) { - shortVersion := parseGeneralSummary(overview, false, false, false) - - expectedString := fmt.Sprintf("OS: %s/%s", runtime.GOOS, runtime.GOARCH) + ` -Daemon version: 0.14.1 -CLI version: development -Management: Connected -Signal: Connected -Relays: 1/2 Available -Nameservers: 1/2 Available -FQDN: some-localhost.awesome-domain.com -NetBird IP: 192.168.178.100/16 -Interface type: Kernel -Quantum resistance: false -Routes: 10.10.0.0/24 -Networks: 10.10.0.0/24 -Peers count: 2/2 Connected -` - - assert.Equal(t, expectedString, shortVersion) -} - func TestParsingOfIP(t *testing.T) { InterfaceIP := "192.168.178.123/16" @@ -599,31 +13,3 @@ func TestParsingOfIP(t *testing.T) { assert.Equal(t, "192.168.178.123\n", parsedIP) } - -func TestTimeAgo(t *testing.T) { - now := time.Now() - - cases := []struct { - name string - input time.Time - expected string - }{ - {"Now", now, "Now"}, - {"Seconds ago", now.Add(-10 * time.Second), "10 seconds ago"}, - {"One minute ago", now.Add(-1 * time.Minute), "1 minute ago"}, - {"Minutes and seconds ago", now.Add(-(1*time.Minute + 30*time.Second)), "1 minute, 30 seconds ago"}, - {"One hour ago", now.Add(-1 * time.Hour), "1 hour ago"}, - {"Hours and minutes ago", now.Add(-(2*time.Hour + 15*time.Minute)), "2 hours, 15 minutes ago"}, - {"One day ago", now.Add(-24 * time.Hour), "1 day ago"}, - {"Multiple days ago", now.Add(-(72*time.Hour + 20*time.Minute)), "3 days ago"}, - {"Zero time", time.Time{}, "-"}, - {"Unix zero time", time.Unix(0, 0), "-"}, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - result := timeAgo(tc.input) - assert.Equal(t, tc.expected, result, "Failed %s", tc.name) - }) - } -} diff --git a/client/cmd/testutil_test.go b/client/cmd/testutil_test.go index e3e644357..e0d784048 100644 --- a/client/cmd/testutil_test.go +++ b/client/cmd/testutil_test.go @@ -95,7 +95,7 @@ func startManagement(t *testing.T, config *mgmt.Config, testFile string) (*grpc. } secretsManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay) - mgmtServer, err := mgmt.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil) + mgmtServer, err := mgmt.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil, nil) if err != nil { t.Fatal(err) } diff --git a/client/cmd/trace.go b/client/cmd/trace.go new file mode 100644 index 000000000..b2ff1f1b5 --- /dev/null +++ b/client/cmd/trace.go @@ -0,0 +1,137 @@ +package cmd + +import ( + "fmt" + "math/rand" + "strings" + + "github.com/spf13/cobra" + "google.golang.org/grpc/status" + + "github.com/netbirdio/netbird/client/proto" +) + +var traceCmd = &cobra.Command{ + Use: "trace ", + Short: "Trace a packet through the firewall", + Example: ` + netbird debug trace in 192.168.1.10 10.10.0.2 -p tcp --sport 12345 --dport 443 --syn --ack + netbird debug trace out 10.10.0.1 8.8.8.8 -p udp --dport 53 + netbird debug trace in 10.10.0.2 10.10.0.1 -p icmp --type 8 --code 0 + netbird debug trace in 100.64.1.1 self -p tcp --dport 80`, + Args: cobra.ExactArgs(3), + RunE: tracePacket, +} + +func init() { + debugCmd.AddCommand(traceCmd) + + traceCmd.Flags().StringP("protocol", "p", "tcp", "Protocol (tcp/udp/icmp)") + traceCmd.Flags().Uint16("sport", 0, "Source port") + traceCmd.Flags().Uint16("dport", 0, "Destination port") + traceCmd.Flags().Uint8("icmp-type", 0, "ICMP type") + traceCmd.Flags().Uint8("icmp-code", 0, "ICMP code") + traceCmd.Flags().Bool("syn", false, "TCP SYN flag") + traceCmd.Flags().Bool("ack", false, "TCP ACK flag") + traceCmd.Flags().Bool("fin", false, "TCP FIN flag") + traceCmd.Flags().Bool("rst", false, "TCP RST flag") + traceCmd.Flags().Bool("psh", false, "TCP PSH flag") + traceCmd.Flags().Bool("urg", false, "TCP URG flag") +} + +func tracePacket(cmd *cobra.Command, args []string) error { + direction := strings.ToLower(args[0]) + if direction != "in" && direction != "out" { + return fmt.Errorf("invalid direction: use 'in' or 'out'") + } + + protocol := cmd.Flag("protocol").Value.String() + if protocol != "tcp" && protocol != "udp" && protocol != "icmp" { + return fmt.Errorf("invalid protocol: use tcp/udp/icmp") + } + + sport, err := cmd.Flags().GetUint16("sport") + if err != nil { + return fmt.Errorf("invalid source port: %v", err) + } + dport, err := cmd.Flags().GetUint16("dport") + if err != nil { + return fmt.Errorf("invalid destination port: %v", err) + } + + // For TCP/UDP, generate random ephemeral port (49152-65535) if not specified + if protocol != "icmp" { + if sport == 0 { + sport = uint16(rand.Intn(16383) + 49152) + } + if dport == 0 { + dport = uint16(rand.Intn(16383) + 49152) + } + } + + var tcpFlags *proto.TCPFlags + if protocol == "tcp" { + syn, _ := cmd.Flags().GetBool("syn") + ack, _ := cmd.Flags().GetBool("ack") + fin, _ := cmd.Flags().GetBool("fin") + rst, _ := cmd.Flags().GetBool("rst") + psh, _ := cmd.Flags().GetBool("psh") + urg, _ := cmd.Flags().GetBool("urg") + + tcpFlags = &proto.TCPFlags{ + Syn: syn, + Ack: ack, + Fin: fin, + Rst: rst, + Psh: psh, + Urg: urg, + } + } + + icmpType, _ := cmd.Flags().GetUint32("icmp-type") + icmpCode, _ := cmd.Flags().GetUint32("icmp-code") + + conn, err := getClient(cmd) + if err != nil { + return err + } + defer conn.Close() + + client := proto.NewDaemonServiceClient(conn) + resp, err := client.TracePacket(cmd.Context(), &proto.TracePacketRequest{ + SourceIp: args[1], + DestinationIp: args[2], + Protocol: protocol, + SourcePort: uint32(sport), + DestinationPort: uint32(dport), + Direction: direction, + TcpFlags: tcpFlags, + IcmpType: &icmpType, + IcmpCode: &icmpCode, + }) + if err != nil { + return fmt.Errorf("trace failed: %v", status.Convert(err).Message()) + } + + printTrace(cmd, args[1], args[2], protocol, sport, dport, resp) + return nil +} + +func printTrace(cmd *cobra.Command, src, dst, proto string, sport, dport uint16, resp *proto.TracePacketResponse) { + cmd.Printf("Packet trace %s:%d -> %s:%d (%s)\n\n", src, sport, dst, dport, strings.ToUpper(proto)) + + for _, stage := range resp.Stages { + if stage.ForwardingDetails != nil { + cmd.Printf("%s: %s [%s]\n", stage.Name, stage.Message, *stage.ForwardingDetails) + } else { + cmd.Printf("%s: %s\n", stage.Name, stage.Message) + } + } + + disposition := map[bool]string{ + true: "\033[32mALLOWED\033[0m", // Green + false: "\033[31mDENIED\033[0m", // Red + }[resp.FinalDisposition] + + cmd.Printf("\nFinal disposition: %s\n", disposition) +} diff --git a/client/cmd/up.go b/client/cmd/up.go index cd5521371..926317b8e 100644 --- a/client/cmd/up.go +++ b/client/cmd/up.go @@ -20,6 +20,7 @@ import ( "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/system" + "github.com/netbirdio/netbird/management/domain" "github.com/netbirdio/netbird/util" ) @@ -29,9 +30,16 @@ const ( interfaceInputType ) +const ( + dnsLabelsFlag = "extra-dns-labels" +) + var ( - foregroundMode bool - upCmd = &cobra.Command{ + foregroundMode bool + dnsLabels []string + dnsLabelsValidated domain.List + + upCmd = &cobra.Command{ Use: "up", Short: "install, login and start Netbird client", RunE: upFunc, @@ -48,6 +56,15 @@ func init() { ) upCmd.PersistentFlags().StringSliceVar(&extraIFaceBlackList, extraIFaceBlackListFlag, nil, "Extra list of default interfaces to ignore for listening") upCmd.PersistentFlags().DurationVar(&dnsRouteInterval, dnsRouteIntervalFlag, time.Minute, "DNS route update interval") + upCmd.PersistentFlags().BoolVar(&blockLANAccess, blockLANAccessFlag, false, "Block access to local networks (LAN) when using this peer as a router or exit node") + + upCmd.PersistentFlags().StringSliceVar(&dnsLabels, dnsLabelsFlag, nil, + `Sets DNS labels`+ + `You can specify a comma-separated list of up to 32 labels. `+ + `An empty string "" clears the previous configuration. `+ + `E.g. --extra-dns-labels vpc1 or --extra-dns-labels vpc1,mgmt1 `+ + `or --extra-dns-labels ""`, + ) } func upFunc(cmd *cobra.Command, args []string) error { @@ -66,6 +83,11 @@ func upFunc(cmd *cobra.Command, args []string) error { return err } + dnsLabelsValidated, err = validateDnsLabels(dnsLabels) + if err != nil { + return err + } + ctx := internal.CtxInitState(cmd.Context()) if hostName != "" { @@ -97,6 +119,7 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error { NATExternalIPs: natExternalIPs, CustomDNSAddress: customDNSAddressConverted, ExtraIFaceBlackList: extraIFaceBlackList, + DNSLabels: dnsLabelsValidated, } if cmd.Flag(enableRosenpassFlag).Changed { @@ -160,6 +183,10 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error { ic.DisableFirewall = &disableFirewall } + if cmd.Flag(blockLANAccessFlag).Changed { + ic.BlockLANAccess = &blockLANAccess + } + providedSetupKey, err := getSetupKey() if err != nil { return err @@ -185,7 +212,7 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error { r.GetFullStatus() connectClient := internal.NewConnectClient(ctx, config, r) - return connectClient.Run() + return connectClient.Run(nil) } func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error { @@ -235,6 +262,8 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error { IsLinuxDesktopClient: isLinuxRunningDesktop(), Hostname: hostName, ExtraIFaceBlacklist: extraIFaceBlackList, + DnsLabels: dnsLabels, + CleanDNSLabels: dnsLabels != nil && len(dnsLabels) == 0, } if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) { @@ -290,6 +319,10 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error { loginRequest.DisableFirewall = &disableFirewall } + if cmd.Flag(blockLANAccessFlag).Changed { + loginRequest.BlockLanAccess = &blockLANAccess + } + var loginErr error var loginResp *proto.LoginResponse @@ -421,6 +454,24 @@ func parseCustomDNSAddress(modified bool) ([]byte, error) { return parsed, nil } +func validateDnsLabels(labels []string) (domain.List, error) { + var ( + domains domain.List + err error + ) + + if len(labels) == 0 { + return domains, nil + } + + domains, err = domain.ValidateDomains(labels) + if err != nil { + return nil, fmt.Errorf("failed to validate dns labels: %v", err) + } + + return domains, nil +} + func isValidAddrPort(input string) bool { if input == "" { return true diff --git a/client/embed/doc.go b/client/embed/doc.go new file mode 100644 index 000000000..069d53ebf --- /dev/null +++ b/client/embed/doc.go @@ -0,0 +1,167 @@ +// Package embed provides a way to embed the NetBird client directly +// into Go programs without requiring a separate NetBird client installation. +package embed + +// Basic Usage: +// +// client, err := embed.New(embed.Options{ +// DeviceName: "my-service", +// SetupKey: os.Getenv("NB_SETUP_KEY"), +// ManagementURL: os.Getenv("NB_MANAGEMENT_URL"), +// }) +// if err != nil { +// log.Fatal(err) +// } +// +// ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) +// defer cancel() +// if err := client.Start(ctx); err != nil { +// log.Fatal(err) +// } +// +// Complete HTTP Server Example: +// +// package main +// +// import ( +// "context" +// "fmt" +// "log" +// "net/http" +// "os" +// "os/signal" +// "syscall" +// "time" +// +// netbird "github.com/netbirdio/netbird/client/embed" +// ) +// +// func main() { +// // Create client with setup key and device name +// client, err := netbird.New(netbird.Options{ +// DeviceName: "http-server", +// SetupKey: os.Getenv("NB_SETUP_KEY"), +// ManagementURL: os.Getenv("NB_MANAGEMENT_URL"), +// LogOutput: io.Discard, +// }) +// if err != nil { +// log.Fatal(err) +// } +// +// // Start with timeout +// ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) +// defer cancel() +// if err := client.Start(ctx); err != nil { +// log.Fatal(err) +// } +// +// // Create HTTP server +// mux := http.NewServeMux() +// mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { +// fmt.Printf("Request from %s: %s %s\n", r.RemoteAddr, r.Method, r.URL.Path) +// fmt.Fprintf(w, "Hello from netbird!") +// }) +// +// // Listen on netbird network +// l, err := client.ListenTCP(":8080") +// if err != nil { +// log.Fatal(err) +// } +// +// server := &http.Server{Handler: mux} +// go func() { +// if err := server.Serve(l); !errors.Is(err, http.ErrServerClosed) { +// log.Printf("HTTP server error: %v", err) +// } +// }() +// +// log.Printf("HTTP server listening on netbird network port 8080") +// +// // Handle shutdown +// stop := make(chan os.Signal, 1) +// signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) +// <-stop +// +// shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) +// defer cancel() +// +// if err := server.Shutdown(shutdownCtx); err != nil { +// log.Printf("HTTP shutdown error: %v", err) +// } +// if err := client.Stop(shutdownCtx); err != nil { +// log.Printf("Netbird shutdown error: %v", err) +// } +// } +// +// Complete HTTP Client Example: +// +// package main +// +// import ( +// "context" +// "fmt" +// "io" +// "log" +// "os" +// "time" +// +// netbird "github.com/netbirdio/netbird/client/embed" +// ) +// +// func main() { +// // Create client with setup key and device name +// client, err := netbird.New(netbird.Options{ +// DeviceName: "http-client", +// SetupKey: os.Getenv("NB_SETUP_KEY"), +// ManagementURL: os.Getenv("NB_MANAGEMENT_URL"), +// LogOutput: io.Discard, +// }) +// if err != nil { +// log.Fatal(err) +// } +// +// // Start with timeout +// ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) +// defer cancel() +// +// if err := client.Start(ctx); err != nil { +// log.Fatal(err) +// } +// +// // Create HTTP client that uses netbird network +// httpClient := client.NewHTTPClient() +// httpClient.Timeout = 10 * time.Second +// +// // Make request to server in netbird network +// target := os.Getenv("NB_TARGET") +// resp, err := httpClient.Get(target) +// if err != nil { +// log.Fatal(err) +// } +// defer resp.Body.Close() +// +// // Read and print response +// body, err := io.ReadAll(resp.Body) +// if err != nil { +// log.Fatal(err) +// } +// +// fmt.Printf("Response from server: %s\n", string(body)) +// +// // Clean shutdown +// shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) +// defer cancel() +// +// if err := client.Stop(shutdownCtx); err != nil { +// log.Printf("Netbird shutdown error: %v", err) +// } +// } +// +// The package provides several methods for network operations: +// - Dial: Creates outbound connections +// - ListenTCP: Creates TCP listeners +// - ListenUDP: Creates UDP listeners +// +// By default, the embed package uses userspace networking mode, which doesn't +// require root/admin privileges. For production deployments, consider setting +// appropriate config and state paths for persistence. diff --git a/client/embed/embed.go b/client/embed/embed.go new file mode 100644 index 000000000..9ded618c5 --- /dev/null +++ b/client/embed/embed.go @@ -0,0 +1,296 @@ +package embed + +import ( + "context" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/netip" + "os" + "sync" + + "github.com/sirupsen/logrus" + wgnetstack "golang.zx2c4.com/wireguard/tun/netstack" + + "github.com/netbirdio/netbird/client/iface/netstack" + "github.com/netbirdio/netbird/client/internal" + "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/client/system" +) + +var ErrClientAlreadyStarted = errors.New("client already started") +var ErrClientNotStarted = errors.New("client not started") + +// Client manages a netbird embedded client instance +type Client struct { + deviceName string + config *internal.Config + mu sync.Mutex + cancel context.CancelFunc + setupKey string + connect *internal.ConnectClient +} + +// Options configures a new Client +type Options struct { + // DeviceName is this peer's name in the network + DeviceName string + // SetupKey is used for authentication + SetupKey string + // ManagementURL overrides the default management server URL + ManagementURL string + // PreSharedKey is the pre-shared key for the WireGuard interface + PreSharedKey string + // LogOutput is the output destination for logs (defaults to os.Stderr if nil) + LogOutput io.Writer + // LogLevel sets the logging level (defaults to info if empty) + LogLevel string + // NoUserspace disables the userspace networking mode. Needs admin/root privileges + NoUserspace bool + // ConfigPath is the path to the netbird config file. If empty, the config will be stored in memory and not persisted. + ConfigPath string + // StatePath is the path to the netbird state file + StatePath string + // DisableClientRoutes disables the client routes + DisableClientRoutes bool +} + +// New creates a new netbird embedded client +func New(opts Options) (*Client, error) { + if opts.LogOutput != nil { + logrus.SetOutput(opts.LogOutput) + } + + if opts.LogLevel != "" { + level, err := logrus.ParseLevel(opts.LogLevel) + if err != nil { + return nil, fmt.Errorf("parse log level: %w", err) + } + logrus.SetLevel(level) + } + + if !opts.NoUserspace { + if err := os.Setenv(netstack.EnvUseNetstackMode, "true"); err != nil { + return nil, fmt.Errorf("setenv: %w", err) + } + if err := os.Setenv(netstack.EnvSkipProxy, "true"); err != nil { + return nil, fmt.Errorf("setenv: %w", err) + } + } + + if opts.StatePath != "" { + // TODO: Disable state if path not provided + if err := os.Setenv("NB_DNS_STATE_FILE", opts.StatePath); err != nil { + return nil, fmt.Errorf("setenv: %w", err) + } + } + + t := true + var config *internal.Config + var err error + input := internal.ConfigInput{ + ConfigPath: opts.ConfigPath, + ManagementURL: opts.ManagementURL, + PreSharedKey: &opts.PreSharedKey, + DisableServerRoutes: &t, + DisableClientRoutes: &opts.DisableClientRoutes, + } + if opts.ConfigPath != "" { + config, err = internal.UpdateOrCreateConfig(input) + } else { + config, err = internal.CreateInMemoryConfig(input) + } + if err != nil { + return nil, fmt.Errorf("create config: %w", err) + } + + return &Client{ + deviceName: opts.DeviceName, + setupKey: opts.SetupKey, + config: config, + }, nil +} + +// Start begins client operation and blocks until the engine has been started successfully or a startup error occurs. +// Pass a context with a deadline to limit the time spent waiting for the engine to start. +func (c *Client) Start(startCtx context.Context) error { + c.mu.Lock() + defer c.mu.Unlock() + if c.cancel != nil { + return ErrClientAlreadyStarted + } + + ctx := internal.CtxInitState(context.Background()) + // nolint:staticcheck + ctx = context.WithValue(ctx, system.DeviceNameCtxKey, c.deviceName) + if err := internal.Login(ctx, c.config, c.setupKey, ""); err != nil { + return fmt.Errorf("login: %w", err) + } + + recorder := peer.NewRecorder(c.config.ManagementURL.String()) + client := internal.NewConnectClient(ctx, c.config, recorder) + + // either startup error (permanent backoff err) or nil err (successful engine up) + // TODO: make after-startup backoff err available + run := make(chan error, 1) + go func() { + if err := client.Run(run); err != nil { + run <- err + } + }() + + select { + case <-startCtx.Done(): + if stopErr := client.Stop(); stopErr != nil { + return fmt.Errorf("stop error after context done. Stop error: %w. Context done: %w", stopErr, startCtx.Err()) + } + return startCtx.Err() + case err := <-run: + if err != nil { + if stopErr := client.Stop(); stopErr != nil { + return fmt.Errorf("stop error after failed to startup. Stop error: %w. Start error: %w", stopErr, err) + } + return fmt.Errorf("startup: %w", err) + } + } + + c.connect = client + + return nil +} + +// Stop gracefully stops the client. +// Pass a context with a deadline to limit the time spent waiting for the engine to stop. +func (c *Client) Stop(ctx context.Context) error { + c.mu.Lock() + defer c.mu.Unlock() + + if c.connect == nil { + return ErrClientNotStarted + } + + done := make(chan error, 1) + go func() { + done <- c.connect.Stop() + }() + + select { + case <-ctx.Done(): + c.cancel = nil + return ctx.Err() + case err := <-done: + c.cancel = nil + if err != nil { + return fmt.Errorf("stop: %w", err) + } + return nil + } +} + +// Dial dials a network address in the netbird network. +// Not applicable if the userspace networking mode is disabled. +func (c *Client) Dial(ctx context.Context, network, address string) (net.Conn, error) { + c.mu.Lock() + connect := c.connect + if connect == nil { + c.mu.Unlock() + return nil, ErrClientNotStarted + } + c.mu.Unlock() + + engine := connect.Engine() + if engine == nil { + return nil, errors.New("engine not started") + } + + nsnet, err := engine.GetNet() + if err != nil { + return nil, fmt.Errorf("get net: %w", err) + } + + return nsnet.DialContext(ctx, network, address) +} + +// ListenTCP listens on the given address in the netbird network +// Not applicable if the userspace networking mode is disabled. +func (c *Client) ListenTCP(address string) (net.Listener, error) { + nsnet, addr, err := c.getNet() + if err != nil { + return nil, err + } + + _, port, err := net.SplitHostPort(address) + if err != nil { + return nil, fmt.Errorf("split host port: %w", err) + } + listenAddr := fmt.Sprintf("%s:%s", addr, port) + + tcpAddr, err := net.ResolveTCPAddr("tcp", listenAddr) + if err != nil { + return nil, fmt.Errorf("resolve: %w", err) + } + return nsnet.ListenTCP(tcpAddr) +} + +// ListenUDP listens on the given address in the netbird network +// Not applicable if the userspace networking mode is disabled. +func (c *Client) ListenUDP(address string) (net.PacketConn, error) { + nsnet, addr, err := c.getNet() + if err != nil { + return nil, err + } + + _, port, err := net.SplitHostPort(address) + if err != nil { + return nil, fmt.Errorf("split host port: %w", err) + } + listenAddr := fmt.Sprintf("%s:%s", addr, port) + + udpAddr, err := net.ResolveUDPAddr("udp", listenAddr) + if err != nil { + return nil, fmt.Errorf("resolve: %w", err) + } + + return nsnet.ListenUDP(udpAddr) +} + +// NewHTTPClient returns a configured http.Client that uses the netbird network for requests. +// Not applicable if the userspace networking mode is disabled. +func (c *Client) NewHTTPClient() *http.Client { + transport := &http.Transport{ + DialContext: c.Dial, + } + + return &http.Client{ + Transport: transport, + } +} + +func (c *Client) getNet() (*wgnetstack.Net, netip.Addr, error) { + c.mu.Lock() + connect := c.connect + if connect == nil { + c.mu.Unlock() + return nil, netip.Addr{}, errors.New("client not started") + } + c.mu.Unlock() + + engine := connect.Engine() + if engine == nil { + return nil, netip.Addr{}, errors.New("engine not started") + } + + addr, err := engine.Address() + if err != nil { + return nil, netip.Addr{}, fmt.Errorf("engine address: %w", err) + } + + nsnet, err := engine.GetNet() + if err != nil { + return nil, netip.Addr{}, fmt.Errorf("get net: %w", err) + } + + return nsnet, addr, nil +} diff --git a/client/firewall/create.go b/client/firewall/create.go index 9466f4b4d..37ea5ceb3 100644 --- a/client/firewall/create.go +++ b/client/firewall/create.go @@ -14,13 +14,13 @@ import ( ) // NewFirewall creates a firewall manager instance -func NewFirewall(iface IFaceMapper, _ *statemanager.Manager) (firewall.Manager, error) { +func NewFirewall(iface IFaceMapper, _ *statemanager.Manager, disableServerRoutes bool) (firewall.Manager, error) { if !iface.IsUserspaceBind() { return nil, fmt.Errorf("not implemented for this OS: %s", runtime.GOOS) } // use userspace packet filtering firewall - fm, err := uspfilter.Create(iface) + fm, err := uspfilter.Create(iface, disableServerRoutes) if err != nil { return nil, err } diff --git a/client/firewall/create_linux.go b/client/firewall/create_linux.go index 076d08ec2..be1b37916 100644 --- a/client/firewall/create_linux.go +++ b/client/firewall/create_linux.go @@ -33,12 +33,12 @@ const SKIP_NFTABLES_ENV = "NB_SKIP_NFTABLES_CHECK" // FWType is the type for the firewall type type FWType int -func NewFirewall(iface IFaceMapper, stateManager *statemanager.Manager) (firewall.Manager, error) { +func NewFirewall(iface IFaceMapper, stateManager *statemanager.Manager, disableServerRoutes bool) (firewall.Manager, error) { // on the linux system we try to user nftables or iptables // in any case, because we need to allow netbird interface traffic // so we use AllowNetbird traffic from these firewall managers // for the userspace packet filtering firewall - fm, err := createNativeFirewall(iface, stateManager) + fm, err := createNativeFirewall(iface, stateManager, disableServerRoutes) if !iface.IsUserspaceBind() { return fm, err @@ -47,10 +47,10 @@ func NewFirewall(iface IFaceMapper, stateManager *statemanager.Manager) (firewal if err != nil { log.Warnf("failed to create native firewall: %v. Proceeding with userspace", err) } - return createUserspaceFirewall(iface, fm) + return createUserspaceFirewall(iface, fm, disableServerRoutes) } -func createNativeFirewall(iface IFaceMapper, stateManager *statemanager.Manager) (firewall.Manager, error) { +func createNativeFirewall(iface IFaceMapper, stateManager *statemanager.Manager, routes bool) (firewall.Manager, error) { fm, err := createFW(iface) if err != nil { return nil, fmt.Errorf("create firewall: %s", err) @@ -77,12 +77,12 @@ func createFW(iface IFaceMapper) (firewall.Manager, error) { } } -func createUserspaceFirewall(iface IFaceMapper, fm firewall.Manager) (firewall.Manager, error) { +func createUserspaceFirewall(iface IFaceMapper, fm firewall.Manager, disableServerRoutes bool) (firewall.Manager, error) { var errUsp error if fm != nil { - fm, errUsp = uspfilter.CreateWithNativeFirewall(iface, fm) + fm, errUsp = uspfilter.CreateWithNativeFirewall(iface, fm, disableServerRoutes) } else { - fm, errUsp = uspfilter.Create(iface) + fm, errUsp = uspfilter.Create(iface, disableServerRoutes) } if errUsp != nil { diff --git a/client/firewall/iface.go b/client/firewall/iface.go index f349f9210..d842abaa1 100644 --- a/client/firewall/iface.go +++ b/client/firewall/iface.go @@ -1,6 +1,8 @@ package firewall import ( + wgdevice "golang.zx2c4.com/wireguard/device" + "github.com/netbirdio/netbird/client/iface/device" ) @@ -10,4 +12,6 @@ type IFaceMapper interface { Address() device.WGAddress IsUserspaceBind() bool SetFilter(device.PacketFilter) error + GetDevice() *device.FilteredDevice + GetWGDevice() *wgdevice.Device } diff --git a/client/firewall/iptables/acl_linux.go b/client/firewall/iptables/acl_linux.go index d774f4538..6c4895e05 100644 --- a/client/firewall/iptables/acl_linux.go +++ b/client/firewall/iptables/acl_linux.go @@ -3,7 +3,7 @@ package iptables import ( "fmt" "net" - "strconv" + "slices" "github.com/coreos/go-iptables/iptables" "github.com/google/uuid" @@ -19,8 +19,7 @@ const ( tableName = "filter" // rules chains contains the effective ACL rules - chainNameInputRules = "NETBIRD-ACL-INPUT" - chainNameOutputRules = "NETBIRD-ACL-OUTPUT" + chainNameInputRules = "NETBIRD-ACL-INPUT" ) type aclEntries map[string][][]string @@ -84,28 +83,22 @@ func (m *aclManager) AddPeerFiltering( protocol firewall.Protocol, sPort *firewall.Port, dPort *firewall.Port, - direction firewall.RuleDirection, action firewall.Action, ipsetName string, ) ([]firewall.Rule, error) { - var dPortVal, sPortVal string - if dPort != nil && dPort.Values != nil { - // TODO: we support only one port per rule in current implementation of ACLs - dPortVal = strconv.Itoa(dPort.Values[0]) - } - if sPort != nil && sPort.Values != nil { - sPortVal = strconv.Itoa(sPort.Values[0]) - } + chain := chainNameInputRules - var chain string - if direction == firewall.RuleDirectionOUT { - chain = chainNameOutputRules - } else { - chain = chainNameInputRules - } + ipsetName = transformIPsetName(ipsetName, sPort, dPort) + specs := filterRuleSpecs(ip, string(protocol), sPort, dPort, action, ipsetName) - ipsetName = transformIPsetName(ipsetName, sPortVal, dPortVal) - specs := filterRuleSpecs(ip, string(protocol), sPortVal, dPortVal, direction, action, ipsetName) + mangleSpecs := slices.Clone(specs) + mangleSpecs = append(mangleSpecs, + "-i", m.wgIface.Name(), + "-m", "addrtype", "--dst-type", "LOCAL", + "-j", "MARK", "--set-xmark", fmt.Sprintf("%#x", nbnet.PreroutingFwmarkRedirected), + ) + + specs = append(specs, "-j", actionToStr(action)) if ipsetName != "" { if ipList, ipsetExists := m.ipsetStore.ipset(ipsetName); ipsetExists { if err := ipset.Add(ipsetName, ip.String()); err != nil { @@ -137,7 +130,7 @@ func (m *aclManager) AddPeerFiltering( m.ipsetStore.addIpList(ipsetName, ipList) } - ok, err := m.iptablesClient.Exists("filter", chain, specs...) + ok, err := m.iptablesClient.Exists(tableFilter, chain, specs...) if err != nil { return nil, fmt.Errorf("failed to check rule: %w", err) } @@ -145,16 +138,22 @@ func (m *aclManager) AddPeerFiltering( return nil, fmt.Errorf("rule already exists") } - if err := m.iptablesClient.Append("filter", chain, specs...); err != nil { + if err := m.iptablesClient.Append(tableFilter, chain, specs...); err != nil { return nil, err } + if err := m.iptablesClient.Append(tableMangle, chainRTPRE, mangleSpecs...); err != nil { + log.Errorf("failed to add mangle rule: %v", err) + mangleSpecs = nil + } + rule := &Rule{ - ruleID: uuid.New().String(), - specs: specs, - ipsetName: ipsetName, - ip: ip.String(), - chain: chain, + ruleID: uuid.New().String(), + specs: specs, + mangleSpecs: mangleSpecs, + ipsetName: ipsetName, + ip: ip.String(), + chain: chain, } m.updateState() @@ -197,6 +196,12 @@ func (m *aclManager) DeletePeerRule(rule firewall.Rule) error { return fmt.Errorf("failed to delete rule: %s, %v: %w", r.chain, r.specs, err) } + if r.mangleSpecs != nil { + if err := m.iptablesClient.Delete(tableMangle, chainRTPRE, r.mangleSpecs...); err != nil { + log.Errorf("failed to delete mangle rule: %v", err) + } + } + m.updateState() return nil @@ -214,28 +219,7 @@ func (m *aclManager) Reset() error { // todo write less destructive cleanup mechanism func (m *aclManager) cleanChains() error { - ok, err := m.iptablesClient.ChainExists(tableName, chainNameOutputRules) - if err != nil { - log.Debugf("failed to list chains: %s", err) - return err - } - if ok { - rules := m.entries["OUTPUT"] - for _, rule := range rules { - err := m.iptablesClient.DeleteIfExists(tableName, "OUTPUT", rule...) - if err != nil { - log.Errorf("failed to delete rule: %v, %s", rule, err) - } - } - - err = m.iptablesClient.ClearAndDeleteChain(tableName, chainNameOutputRules) - if err != nil { - log.Debugf("failed to clear and delete %s chain: %s", chainNameOutputRules, err) - return err - } - } - - ok, err = m.iptablesClient.ChainExists(tableName, chainNameInputRules) + ok, err := m.iptablesClient.ChainExists(tableName, chainNameInputRules) if err != nil { log.Debugf("failed to list chains: %s", err) return err @@ -295,12 +279,6 @@ func (m *aclManager) createDefaultChains() error { return err } - // chain netbird-acl-output-rules - if err := m.iptablesClient.NewChain(tableName, chainNameOutputRules); err != nil { - log.Debugf("failed to create '%s' chain: %s", chainNameOutputRules, err) - return err - } - for chainName, rules := range m.entries { for _, rule := range rules { if err := m.iptablesClient.InsertUnique(tableName, chainName, 1, rule...); err != nil { @@ -329,8 +307,6 @@ func (m *aclManager) createDefaultChains() error { // The existing FORWARD rules/policies decide outbound traffic towards our interface. // In case the FORWARD policy is set to "drop", we add an established/related rule to allow return traffic for the inbound rule. - -// The OUTPUT chain gets an extra rule to allow traffic to any set up routes, the return traffic is handled by the INPUT related/established rule. func (m *aclManager) seedInitialEntries() { established := getConntrackEstablished() @@ -346,17 +322,10 @@ func (m *aclManager) seedInitialEntries() { func (m *aclManager) seedInitialOptionalEntries() { m.optionalEntries["FORWARD"] = []entry{ { - spec: []string{"-m", "mark", "--mark", fmt.Sprintf("%#x", nbnet.PreroutingFwmarkRedirected), "-j", chainNameInputRules}, + spec: []string{"-m", "mark", "--mark", fmt.Sprintf("%#x", nbnet.PreroutingFwmarkRedirected), "-j", "ACCEPT"}, position: 2, }, } - - m.optionalEntries["PREROUTING"] = []entry{ - { - spec: []string{"-t", "mangle", "-i", m.wgIface.Name(), "-m", "addrtype", "--dst-type", "LOCAL", "-j", "MARK", "--set-mark", fmt.Sprintf("%#x", nbnet.PreroutingFwmarkRedirected)}, - position: 1, - }, - } } func (m *aclManager) appendToEntries(chainName string, spec []string) { @@ -390,42 +359,26 @@ func (m *aclManager) updateState() { } // filterRuleSpecs returns the specs of a filtering rule -func filterRuleSpecs( - ip net.IP, protocol string, sPort, dPort string, direction firewall.RuleDirection, action firewall.Action, ipsetName string, -) (specs []string) { +func filterRuleSpecs(ip net.IP, protocol string, sPort, dPort *firewall.Port, action firewall.Action, ipsetName string) (specs []string) { matchByIP := true // don't use IP matching if IP is ip 0.0.0.0 if ip.String() == "0.0.0.0" { matchByIP = false } - switch direction { - case firewall.RuleDirectionIN: - if matchByIP { - if ipsetName != "" { - specs = append(specs, "-m", "set", "--set", ipsetName, "src") - } else { - specs = append(specs, "-s", ip.String()) - } - } - case firewall.RuleDirectionOUT: - if matchByIP { - if ipsetName != "" { - specs = append(specs, "-m", "set", "--set", ipsetName, "dst") - } else { - specs = append(specs, "-d", ip.String()) - } + + if matchByIP { + if ipsetName != "" { + specs = append(specs, "-m", "set", "--set", ipsetName, "src") + } else { + specs = append(specs, "-s", ip.String()) } } if protocol != "all" { specs = append(specs, "-p", protocol) } - if sPort != "" { - specs = append(specs, "--sport", sPort) - } - if dPort != "" { - specs = append(specs, "--dport", dPort) - } - return append(specs, "-j", actionToStr(action)) + specs = append(specs, applyPort("--sport", sPort)...) + specs = append(specs, applyPort("--dport", dPort)...) + return specs } func actionToStr(action firewall.Action) string { @@ -435,15 +388,15 @@ func actionToStr(action firewall.Action) string { return "DROP" } -func transformIPsetName(ipsetName string, sPort, dPort string) string { +func transformIPsetName(ipsetName string, sPort, dPort *firewall.Port) string { switch { case ipsetName == "": return "" - case sPort != "" && dPort != "": + case sPort != nil && dPort != nil: return ipsetName + "-sport-dport" - case sPort != "": + case sPort != nil: return ipsetName + "-sport" - case dPort != "": + case dPort != nil: return ipsetName + "-dport" default: return ipsetName diff --git a/client/firewall/iptables/manager_linux.go b/client/firewall/iptables/manager_linux.go index da8e2c08f..929e8a656 100644 --- a/client/firewall/iptables/manager_linux.go +++ b/client/firewall/iptables/manager_linux.go @@ -100,15 +100,14 @@ func (m *Manager) AddPeerFiltering( protocol firewall.Protocol, sPort *firewall.Port, dPort *firewall.Port, - direction firewall.RuleDirection, action firewall.Action, ipsetName string, - comment string, + _ string, ) ([]firewall.Rule, error) { m.mutex.Lock() defer m.mutex.Unlock() - return m.aclMgr.AddPeerFiltering(ip, protocol, sPort, dPort, direction, action, ipsetName) + return m.aclMgr.AddPeerFiltering(ip, protocol, sPort, dPort, action, ipsetName) } func (m *Manager) AddRouteFiltering( @@ -201,7 +200,6 @@ func (m *Manager) AllowNetbird() error { "all", nil, nil, - firewall.RuleDirectionIN, firewall.ActionAccept, "", "", @@ -215,6 +213,19 @@ func (m *Manager) AllowNetbird() error { // Flush doesn't need to be implemented for this manager func (m *Manager) Flush() error { return nil } +// SetLogLevel sets the log level for the firewall manager +func (m *Manager) SetLogLevel(log.Level) { + // not supported +} + +func (m *Manager) EnableRouting() error { + return nil +} + +func (m *Manager) DisableRouting() error { + return nil +} + func getConntrackEstablished() []string { return []string{"-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT"} } diff --git a/client/firewall/iptables/manager_linux_test.go b/client/firewall/iptables/manager_linux_test.go index ebdb83137..ba578c033 100644 --- a/client/firewall/iptables/manager_linux_test.go +++ b/client/firewall/iptables/manager_linux_test.go @@ -68,27 +68,14 @@ func TestIptablesManager(t *testing.T) { time.Sleep(time.Second) }() - var rule1 []fw.Rule - t.Run("add first rule", func(t *testing.T) { - ip := net.ParseIP("10.20.0.2") - port := &fw.Port{Values: []int{8080}} - rule1, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.RuleDirectionOUT, fw.ActionAccept, "", "accept HTTP traffic") - require.NoError(t, err, "failed to add rule") - - for _, r := range rule1 { - checkRuleSpecs(t, ipv4Client, chainNameOutputRules, true, r.(*Rule).specs...) - } - - }) - var rule2 []fw.Rule t.Run("add second rule", func(t *testing.T) { ip := net.ParseIP("10.20.0.3") port := &fw.Port{ - Values: []int{8043: 8046}, + IsRange: true, + Values: []uint16{8043, 8046}, } - rule2, err = manager.AddPeerFiltering( - ip, "tcp", port, nil, fw.RuleDirectionIN, fw.ActionAccept, "", "accept HTTPS traffic from ports range") + rule2, err = manager.AddPeerFiltering(ip, "tcp", port, nil, fw.ActionAccept, "", "accept HTTPS traffic from ports range") require.NoError(t, err, "failed to add rule") for _, r := range rule2 { @@ -97,15 +84,6 @@ func TestIptablesManager(t *testing.T) { } }) - t.Run("delete first rule", func(t *testing.T) { - for _, r := range rule1 { - err := manager.DeletePeerRule(r) - require.NoError(t, err, "failed to delete rule") - - checkRuleSpecs(t, ipv4Client, chainNameOutputRules, false, r.(*Rule).specs...) - } - }) - t.Run("delete second rule", func(t *testing.T) { for _, r := range rule2 { err := manager.DeletePeerRule(r) @@ -118,8 +96,8 @@ func TestIptablesManager(t *testing.T) { t.Run("reset check", func(t *testing.T) { // add second rule ip := net.ParseIP("10.20.0.3") - port := &fw.Port{Values: []int{5353}} - _, err = manager.AddPeerFiltering(ip, "udp", nil, port, fw.RuleDirectionOUT, fw.ActionAccept, "", "accept Fake DNS traffic") + port := &fw.Port{Values: []uint16{5353}} + _, err = manager.AddPeerFiltering(ip, "udp", nil, port, fw.ActionAccept, "", "accept Fake DNS traffic") require.NoError(t, err, "failed to add rule") err = manager.Reset(nil) @@ -135,9 +113,6 @@ func TestIptablesManager(t *testing.T) { } func TestIptablesManagerIPSet(t *testing.T) { - ipv4Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv4) - require.NoError(t, err) - mock := &iFaceMock{ NameFunc: func() string { return "lo" @@ -167,33 +142,13 @@ func TestIptablesManagerIPSet(t *testing.T) { time.Sleep(time.Second) }() - var rule1 []fw.Rule - t.Run("add first rule with set", func(t *testing.T) { - ip := net.ParseIP("10.20.0.2") - port := &fw.Port{Values: []int{8080}} - rule1, err = manager.AddPeerFiltering( - ip, "tcp", nil, port, fw.RuleDirectionOUT, - fw.ActionAccept, "default", "accept HTTP traffic", - ) - require.NoError(t, err, "failed to add rule") - - for _, r := range rule1 { - checkRuleSpecs(t, ipv4Client, chainNameOutputRules, true, r.(*Rule).specs...) - require.Equal(t, r.(*Rule).ipsetName, "default-dport", "ipset name must be set") - require.Equal(t, r.(*Rule).ip, "10.20.0.2", "ipset IP must be set") - } - }) - var rule2 []fw.Rule t.Run("add second rule", func(t *testing.T) { ip := net.ParseIP("10.20.0.3") port := &fw.Port{ - Values: []int{443}, + Values: []uint16{443}, } - rule2, err = manager.AddPeerFiltering( - ip, "tcp", port, nil, fw.RuleDirectionIN, fw.ActionAccept, - "default", "accept HTTPS traffic from ports range", - ) + rule2, err = manager.AddPeerFiltering(ip, "tcp", port, nil, fw.ActionAccept, "default", "accept HTTPS traffic from ports range") for _, r := range rule2 { require.NoError(t, err, "failed to add rule") require.Equal(t, r.(*Rule).ipsetName, "default-sport", "ipset name must be set") @@ -201,15 +156,6 @@ func TestIptablesManagerIPSet(t *testing.T) { } }) - t.Run("delete first rule", func(t *testing.T) { - for _, r := range rule1 { - err := manager.DeletePeerRule(r) - require.NoError(t, err, "failed to delete rule") - - require.NotContains(t, manager.aclMgr.ipsetStore.ipsets, r.(*Rule).ruleID, "rule must be removed form the ruleset index") - } - }) - t.Run("delete second rule", func(t *testing.T) { for _, r := range rule2 { err := manager.DeletePeerRule(r) @@ -269,12 +215,8 @@ func TestIptablesCreatePerformance(t *testing.T) { ip := net.ParseIP("10.20.0.100") start := time.Now() for i := 0; i < testMax; i++ { - port := &fw.Port{Values: []int{1000 + i}} - if i%2 == 0 { - _, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.RuleDirectionOUT, fw.ActionAccept, "", "accept HTTP traffic") - } else { - _, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.RuleDirectionIN, fw.ActionAccept, "", "accept HTTP traffic") - } + port := &fw.Port{Values: []uint16{uint16(1000 + i)}} + _, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.ActionAccept, "", "accept HTTP traffic") require.NoError(t, err, "failed to add rule") } diff --git a/client/firewall/iptables/router_linux.go b/client/firewall/iptables/router_linux.go index d067a3e7b..6522daa3f 100644 --- a/client/firewall/iptables/router_linux.go +++ b/client/firewall/iptables/router_linux.go @@ -135,7 +135,16 @@ func (r *router) AddRouteFiltering( } rule := genRouteFilteringRuleSpec(params) - if err := r.iptablesClient.Append(tableFilter, chainRTFWD, rule...); err != nil { + // Insert DROP rules at the beginning, append ACCEPT rules at the end + var err error + if action == firewall.ActionDrop { + // after the established rule + err = r.iptablesClient.Insert(tableFilter, chainRTFWD, 2, rule...) + } else { + err = r.iptablesClient.Append(tableFilter, chainRTFWD, rule...) + } + + if err != nil { return nil, fmt.Errorf("add route rule: %v", err) } @@ -590,10 +599,10 @@ func applyPort(flag string, port *firewall.Port) []string { if len(port.Values) > 1 { portList := make([]string, len(port.Values)) for i, p := range port.Values { - portList[i] = strconv.Itoa(p) + portList[i] = strconv.Itoa(int(p)) } return []string{"-m", "multiport", flag, strings.Join(portList, ",")} } - return []string{flag, strconv.Itoa(port.Values[0])} + return []string{flag, strconv.Itoa(int(port.Values[0]))} } diff --git a/client/firewall/iptables/router_linux_test.go b/client/firewall/iptables/router_linux_test.go index 861bf8601..0eb207567 100644 --- a/client/firewall/iptables/router_linux_test.go +++ b/client/firewall/iptables/router_linux_test.go @@ -239,7 +239,7 @@ func TestRouter_AddRouteFiltering(t *testing.T) { destination: netip.MustParsePrefix("10.0.0.0/24"), proto: firewall.ProtocolTCP, sPort: nil, - dPort: &firewall.Port{Values: []int{80}}, + dPort: &firewall.Port{Values: []uint16{80}}, direction: firewall.RuleDirectionIN, action: firewall.ActionAccept, expectSet: false, @@ -252,7 +252,7 @@ func TestRouter_AddRouteFiltering(t *testing.T) { }, destination: netip.MustParsePrefix("10.0.0.0/8"), proto: firewall.ProtocolUDP, - sPort: &firewall.Port{Values: []int{1024, 2048}, IsRange: true}, + sPort: &firewall.Port{Values: []uint16{1024, 2048}, IsRange: true}, dPort: nil, direction: firewall.RuleDirectionOUT, action: firewall.ActionDrop, @@ -285,7 +285,7 @@ func TestRouter_AddRouteFiltering(t *testing.T) { sources: []netip.Prefix{netip.MustParsePrefix("172.16.0.0/12")}, destination: netip.MustParsePrefix("192.168.0.0/16"), proto: firewall.ProtocolTCP, - sPort: &firewall.Port{Values: []int{80, 443, 8080}}, + sPort: &firewall.Port{Values: []uint16{80, 443, 8080}}, dPort: nil, direction: firewall.RuleDirectionOUT, action: firewall.ActionAccept, @@ -297,7 +297,7 @@ func TestRouter_AddRouteFiltering(t *testing.T) { destination: netip.MustParsePrefix("10.0.0.0/24"), proto: firewall.ProtocolUDP, sPort: nil, - dPort: &firewall.Port{Values: []int{5000, 5100}, IsRange: true}, + dPort: &firewall.Port{Values: []uint16{5000, 5100}, IsRange: true}, direction: firewall.RuleDirectionIN, action: firewall.ActionDrop, expectSet: false, @@ -307,8 +307,8 @@ func TestRouter_AddRouteFiltering(t *testing.T) { sources: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/24")}, destination: netip.MustParsePrefix("172.16.0.0/16"), proto: firewall.ProtocolTCP, - sPort: &firewall.Port{Values: []int{1024, 65535}, IsRange: true}, - dPort: &firewall.Port{Values: []int{22}}, + sPort: &firewall.Port{Values: []uint16{1024, 65535}, IsRange: true}, + dPort: &firewall.Port{Values: []uint16{22}}, direction: firewall.RuleDirectionOUT, action: firewall.ActionAccept, expectSet: false, diff --git a/client/firewall/iptables/rule.go b/client/firewall/iptables/rule.go index 1047c5cf8..e90e32f8b 100644 --- a/client/firewall/iptables/rule.go +++ b/client/firewall/iptables/rule.go @@ -5,9 +5,10 @@ type Rule struct { ruleID string ipsetName string - specs []string - ip string - chain string + specs []string + mangleSpecs []string + ip string + chain string } // GetRuleID returns the rule id diff --git a/client/firewall/manager/firewall.go b/client/firewall/manager/firewall.go index 9391b47ec..d007e20a5 100644 --- a/client/firewall/manager/firewall.go +++ b/client/firewall/manager/firewall.go @@ -69,7 +69,6 @@ type Manager interface { proto Protocol, sPort *Port, dPort *Port, - direction RuleDirection, action Action, ipsetName string, comment string, @@ -100,6 +99,12 @@ type Manager interface { // Flush the changes to firewall controller Flush() error + + SetLogLevel(log.Level) + + EnableRouting() error + + DisableRouting() error } func GenKey(format string, pair RouterPair) string { diff --git a/client/firewall/manager/port.go b/client/firewall/manager/port.go index 9061c1e63..df02e3117 100644 --- a/client/firewall/manager/port.go +++ b/client/firewall/manager/port.go @@ -30,7 +30,7 @@ type Port struct { IsRange bool // Values contains one value for single port, multiple values for the list of ports, or two values for the range of ports - Values []int + Values []uint16 } // String interface implementation @@ -40,7 +40,11 @@ func (p *Port) String() string { if ports != "" { ports += "," } - ports += strconv.Itoa(port) + ports += strconv.Itoa(int(port)) } + if p.IsRange { + ports = "range:" + ports + } + return ports } diff --git a/client/firewall/nftables/acl_linux.go b/client/firewall/nftables/acl_linux.go index 852cfec8d..aff9e9188 100644 --- a/client/firewall/nftables/acl_linux.go +++ b/client/firewall/nftables/acl_linux.go @@ -2,9 +2,9 @@ package nftables import ( "bytes" - "encoding/binary" "fmt" "net" + "slices" "strconv" "strings" "time" @@ -22,8 +22,7 @@ import ( const ( // rules chains contains the effective ACL rules - chainNameInputRules = "netbird-acl-input-rules" - chainNameOutputRules = "netbird-acl-output-rules" + chainNameInputRules = "netbird-acl-input-rules" // filter chains contains the rules that jump to the rules chains chainNameInputFilter = "netbird-acl-input-filter" @@ -45,9 +44,9 @@ type AclManager struct { wgIface iFaceMapper routingFwChainName string - workTable *nftables.Table - chainInputRules *nftables.Chain - chainOutputRules *nftables.Chain + workTable *nftables.Table + chainInputRules *nftables.Chain + chainPrerouting *nftables.Chain ipsetStore *ipsetStore rules map[string]*Rule @@ -89,7 +88,6 @@ func (m *AclManager) AddPeerFiltering( proto firewall.Protocol, sPort *firewall.Port, dPort *firewall.Port, - direction firewall.RuleDirection, action firewall.Action, ipsetName string, comment string, @@ -104,7 +102,7 @@ func (m *AclManager) AddPeerFiltering( } newRules := make([]firewall.Rule, 0, 2) - ioRule, err := m.addIOFiltering(ip, proto, sPort, dPort, direction, action, ipset, comment) + ioRule, err := m.addIOFiltering(ip, proto, sPort, dPort, action, ipset, comment) if err != nil { return nil, err } @@ -121,23 +119,32 @@ func (m *AclManager) DeletePeerRule(rule firewall.Rule) error { } if r.nftSet == nil { - err := m.rConn.DelRule(r.nftRule) - if err != nil { + if err := m.rConn.DelRule(r.nftRule); err != nil { log.Errorf("failed to delete rule: %v", err) } + if r.mangleRule != nil { + if err := m.rConn.DelRule(r.mangleRule); err != nil { + log.Errorf("failed to delete mangle rule: %v", err) + } + } delete(m.rules, r.GetRuleID()) return m.rConn.Flush() } ips, ok := m.ipsetStore.ips(r.nftSet.Name) if !ok { - err := m.rConn.DelRule(r.nftRule) - if err != nil { + if err := m.rConn.DelRule(r.nftRule); err != nil { log.Errorf("failed to delete rule: %v", err) } + if r.mangleRule != nil { + if err := m.rConn.DelRule(r.mangleRule); err != nil { + log.Errorf("failed to delete mangle rule: %v", err) + } + } delete(m.rules, r.GetRuleID()) return m.rConn.Flush() } + if _, ok := ips[r.ip.String()]; ok { err := m.sConn.SetDeleteElements(r.nftSet, []nftables.SetElement{{Key: r.ip.To4()}}) if err != nil { @@ -156,12 +163,16 @@ func (m *AclManager) DeletePeerRule(rule firewall.Rule) error { return nil } - err := m.rConn.DelRule(r.nftRule) - if err != nil { + if err := m.rConn.DelRule(r.nftRule); err != nil { log.Errorf("failed to delete rule: %v", err) } - err = m.rConn.Flush() - if err != nil { + if r.mangleRule != nil { + if err := m.rConn.DelRule(r.mangleRule); err != nil { + log.Errorf("failed to delete mangle rule: %v", err) + } + } + + if err := m.rConn.Flush(); err != nil { return err } @@ -214,38 +225,6 @@ func (m *AclManager) createDefaultAllowRules() error { Exprs: expIn, }) - expOut := []expr.Any{ - &expr.Payload{ - DestRegister: 1, - Base: expr.PayloadBaseNetworkHeader, - Offset: 16, - Len: 4, - }, - // mask - &expr.Bitwise{ - SourceRegister: 1, - DestRegister: 1, - Len: 4, - Mask: []byte{0, 0, 0, 0}, - Xor: []byte{0, 0, 0, 0}, - }, - // net address - &expr.Cmp{ - Register: 1, - Data: []byte{0, 0, 0, 0}, - }, - &expr.Verdict{ - Kind: expr.VerdictAccept, - }, - } - - _ = m.rConn.InsertRule(&nftables.Rule{ - Table: m.workTable, - Chain: m.chainOutputRules, - Position: 0, - Exprs: expOut, - }) - if err := m.rConn.Flush(); err != nil { return fmt.Errorf(flushError, err) } @@ -260,25 +239,33 @@ func (m *AclManager) Flush() error { return err } - if err := m.refreshRuleHandles(m.chainInputRules); err != nil { + if err := m.refreshRuleHandles(m.chainInputRules, false); err != nil { log.Errorf("failed to refresh rule handles ipv4 input chain: %v", err) } - - if err := m.refreshRuleHandles(m.chainOutputRules); err != nil { - log.Errorf("failed to refresh rule handles IPv4 output chain: %v", err) + if err := m.refreshRuleHandles(m.chainPrerouting, true); err != nil { + log.Errorf("failed to refresh rule handles prerouting chain: %v", err) } return nil } -func (m *AclManager) addIOFiltering(ip net.IP, proto firewall.Protocol, sPort *firewall.Port, dPort *firewall.Port, direction firewall.RuleDirection, action firewall.Action, ipset *nftables.Set, comment string) (*Rule, error) { - ruleId := generatePeerRuleId(ip, sPort, dPort, direction, action, ipset) +func (m *AclManager) addIOFiltering( + ip net.IP, + proto firewall.Protocol, + sPort *firewall.Port, + dPort *firewall.Port, + action firewall.Action, + ipset *nftables.Set, + comment string, +) (*Rule, error) { + ruleId := generatePeerRuleId(ip, sPort, dPort, action, ipset) if r, ok := m.rules[ruleId]; ok { return &Rule{ - r.nftRule, - r.nftSet, - r.ruleID, - ip, + nftRule: r.nftRule, + mangleRule: r.mangleRule, + nftSet: r.nftSet, + ruleID: r.ruleID, + ip: ip, }, nil } @@ -310,9 +297,6 @@ func (m *AclManager) addIOFiltering(ip net.IP, proto firewall.Protocol, sPort *f if !bytes.HasPrefix(anyIP, rawIP) { // source address position addrOffset := uint32(12) - if direction == firewall.RuleDirectionOUT { - addrOffset += 4 // is ipv4 address length - } expressions = append(expressions, &expr.Payload{ @@ -342,73 +326,100 @@ func (m *AclManager) addIOFiltering(ip net.IP, proto firewall.Protocol, sPort *f } } - if sPort != nil && len(sPort.Values) != 0 { - expressions = append(expressions, - &expr.Payload{ - DestRegister: 1, - Base: expr.PayloadBaseTransportHeader, - Offset: 0, - Len: 2, - }, - &expr.Cmp{ - Op: expr.CmpOpEq, - Register: 1, - Data: encodePort(*sPort), - }, - ) - } + expressions = append(expressions, applyPort(sPort, true)...) + expressions = append(expressions, applyPort(dPort, false)...) - if dPort != nil && len(dPort.Values) != 0 { - expressions = append(expressions, - &expr.Payload{ - DestRegister: 1, - Base: expr.PayloadBaseTransportHeader, - Offset: 2, - Len: 2, - }, - &expr.Cmp{ - Op: expr.CmpOpEq, - Register: 1, - Data: encodePort(*dPort), - }, - ) - } + mainExpressions := slices.Clone(expressions) switch action { case firewall.ActionAccept: - expressions = append(expressions, &expr.Verdict{Kind: expr.VerdictAccept}) + mainExpressions = append(mainExpressions, &expr.Verdict{Kind: expr.VerdictAccept}) case firewall.ActionDrop: - expressions = append(expressions, &expr.Verdict{Kind: expr.VerdictDrop}) + mainExpressions = append(mainExpressions, &expr.Verdict{Kind: expr.VerdictDrop}) } userData := []byte(strings.Join([]string{ruleId, comment}, " ")) - var chain *nftables.Chain - if direction == firewall.RuleDirectionIN { - chain = m.chainInputRules - } else { - chain = m.chainOutputRules - } + chain := m.chainInputRules nftRule := m.rConn.AddRule(&nftables.Rule{ Table: m.workTable, Chain: chain, - Exprs: expressions, + Exprs: mainExpressions, UserData: userData, }) + if err := m.rConn.Flush(); err != nil { + return nil, fmt.Errorf(flushError, err) + } + rule := &Rule{ - nftRule: nftRule, - nftSet: ipset, - ruleID: ruleId, - ip: ip, + nftRule: nftRule, + mangleRule: m.createPreroutingRule(expressions, userData), + nftSet: ipset, + ruleID: ruleId, + ip: ip, } m.rules[ruleId] = rule if ipset != nil { m.ipsetStore.AddReferenceToIpset(ipset.Name) } + return rule, nil } +func (m *AclManager) createPreroutingRule(expressions []expr.Any, userData []byte) *nftables.Rule { + if m.chainPrerouting == nil { + log.Warn("prerouting chain is not created") + return nil + } + + preroutingExprs := slices.Clone(expressions) + + // interface + preroutingExprs = append([]expr.Any{ + &expr.Meta{ + Key: expr.MetaKeyIIFNAME, + Register: 1, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: ifname(m.wgIface.Name()), + }, + }, preroutingExprs...) + + // local destination and mark + preroutingExprs = append(preroutingExprs, + &expr.Fib{ + Register: 1, + ResultADDRTYPE: true, + FlagDADDR: true, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: binaryutil.NativeEndian.PutUint32(unix.RTN_LOCAL), + }, + + &expr.Immediate{ + Register: 1, + Data: binaryutil.NativeEndian.PutUint32(nbnet.PreroutingFwmarkRedirected), + }, + &expr.Meta{ + Key: expr.MetaKeyMARK, + Register: 1, + SourceRegister: true, + }, + ) + + return m.rConn.AddRule(&nftables.Rule{ + Table: m.workTable, + Chain: m.chainPrerouting, + Exprs: preroutingExprs, + UserData: userData, + }) +} + func (m *AclManager) createDefaultChains() (err error) { // chainNameInputRules chain := m.createChain(chainNameInputRules) @@ -419,15 +430,6 @@ func (m *AclManager) createDefaultChains() (err error) { } m.chainInputRules = chain - // chainNameOutputRules - chain = m.createChain(chainNameOutputRules) - err = m.rConn.Flush() - if err != nil { - log.Debugf("failed to create chain (%s): %s", chainNameOutputRules, err) - return err - } - m.chainOutputRules = chain - // netbird-acl-input-filter // type filter hook input priority filter; policy accept; chain = m.createFilterChainWithHook(chainNameInputFilter, nftables.ChainHookInput) @@ -461,7 +463,7 @@ func (m *AclManager) createDefaultChains() (err error) { // go through the input filter as well. This will enable e.g. Docker services to keep working by accessing the // netbird peer IP. func (m *AclManager) allowRedirectedTraffic(chainFwFilter *nftables.Chain) error { - preroutingChain := m.rConn.AddChain(&nftables.Chain{ + m.chainPrerouting = m.rConn.AddChain(&nftables.Chain{ Name: chainNamePrerouting, Table: m.workTable, Type: nftables.ChainTypeFilter, @@ -469,8 +471,6 @@ func (m *AclManager) allowRedirectedTraffic(chainFwFilter *nftables.Chain) error Priority: nftables.ChainPriorityMangle, }) - m.addPreroutingRule(preroutingChain) - m.addFwmarkToForward(chainFwFilter) if err := m.rConn.Flush(); err != nil { @@ -480,43 +480,6 @@ func (m *AclManager) allowRedirectedTraffic(chainFwFilter *nftables.Chain) error return nil } -func (m *AclManager) addPreroutingRule(preroutingChain *nftables.Chain) { - m.rConn.AddRule(&nftables.Rule{ - Table: m.workTable, - Chain: preroutingChain, - Exprs: []expr.Any{ - &expr.Meta{ - Key: expr.MetaKeyIIFNAME, - Register: 1, - }, - &expr.Cmp{ - Op: expr.CmpOpEq, - Register: 1, - Data: ifname(m.wgIface.Name()), - }, - &expr.Fib{ - Register: 1, - ResultADDRTYPE: true, - FlagDADDR: true, - }, - &expr.Cmp{ - Op: expr.CmpOpEq, - Register: 1, - Data: binaryutil.NativeEndian.PutUint32(unix.RTN_LOCAL), - }, - &expr.Immediate{ - Register: 1, - Data: binaryutil.NativeEndian.PutUint32(nbnet.PreroutingFwmarkRedirected), - }, - &expr.Meta{ - Key: expr.MetaKeyMARK, - Register: 1, - SourceRegister: true, - }, - }, - }) -} - func (m *AclManager) addFwmarkToForward(chainFwFilter *nftables.Chain) { m.rConn.InsertRule(&nftables.Rule{ Table: m.workTable, @@ -532,8 +495,7 @@ func (m *AclManager) addFwmarkToForward(chainFwFilter *nftables.Chain) { Data: binaryutil.NativeEndian.PutUint32(nbnet.PreroutingFwmarkRedirected), }, &expr.Verdict{ - Kind: expr.VerdictJump, - Chain: m.chainInputRules.Name, + Kind: expr.VerdictAccept, }, }, }) @@ -680,6 +642,7 @@ func (m *AclManager) flushWithBackoff() (err error) { for i := 0; ; i++ { err = m.rConn.Flush() if err != nil { + log.Debugf("failed to flush nftables: %v", err) if !strings.Contains(err.Error(), "busy") { return } @@ -696,7 +659,7 @@ func (m *AclManager) flushWithBackoff() (err error) { return } -func (m *AclManager) refreshRuleHandles(chain *nftables.Chain) error { +func (m *AclManager) refreshRuleHandles(chain *nftables.Chain, mangle bool) error { if m.workTable == nil || chain == nil { return nil } @@ -713,22 +676,19 @@ func (m *AclManager) refreshRuleHandles(chain *nftables.Chain) error { split := bytes.Split(rule.UserData, []byte(" ")) r, ok := m.rules[string(split[0])] if ok { - *r.nftRule = *rule + if mangle { + *r.mangleRule = *rule + } else { + *r.nftRule = *rule + } } } return nil } -func generatePeerRuleId( - ip net.IP, - sPort *firewall.Port, - dPort *firewall.Port, - direction firewall.RuleDirection, - action firewall.Action, - ipset *nftables.Set, -) string { - rulesetID := ":" + strconv.Itoa(int(direction)) + ":" +func generatePeerRuleId(ip net.IP, sPort *firewall.Port, dPort *firewall.Port, action firewall.Action, ipset *nftables.Set) string { + rulesetID := ":" if sPort != nil { rulesetID += sPort.String() } @@ -744,12 +704,6 @@ func generatePeerRuleId( return "set:" + ipset.Name + rulesetID } -func encodePort(port firewall.Port) []byte { - bs := make([]byte, 2) - binary.BigEndian.PutUint16(bs, uint16(port.Values[0])) - return bs -} - func ifname(n string) []byte { b := make([]byte, 16) copy(b, n+"\x00") diff --git a/client/firewall/nftables/manager_linux.go b/client/firewall/nftables/manager_linux.go index 8e1aa0d80..de68f3291 100644 --- a/client/firewall/nftables/manager_linux.go +++ b/client/firewall/nftables/manager_linux.go @@ -117,7 +117,6 @@ func (m *Manager) AddPeerFiltering( proto firewall.Protocol, sPort *firewall.Port, dPort *firewall.Port, - direction firewall.RuleDirection, action firewall.Action, ipsetName string, comment string, @@ -130,10 +129,17 @@ func (m *Manager) AddPeerFiltering( return nil, fmt.Errorf("unsupported IP version: %s", ip.String()) } - return m.aclManager.AddPeerFiltering(ip, proto, sPort, dPort, direction, action, ipsetName, comment) + return m.aclManager.AddPeerFiltering(ip, proto, sPort, dPort, action, ipsetName, comment) } -func (m *Manager) AddRouteFiltering(sources []netip.Prefix, destination netip.Prefix, proto firewall.Protocol, sPort *firewall.Port, dPort *firewall.Port, action firewall.Action) (firewall.Rule, error) { +func (m *Manager) AddRouteFiltering( + sources []netip.Prefix, + destination netip.Prefix, + proto firewall.Protocol, + sPort *firewall.Port, + dPort *firewall.Port, + action firewall.Action, +) (firewall.Rule, error) { m.mutex.Lock() defer m.mutex.Unlock() @@ -312,6 +318,19 @@ func (m *Manager) cleanupNetbirdTables() error { return nil } +// SetLogLevel sets the log level for the firewall manager +func (m *Manager) SetLogLevel(log.Level) { + // not supported +} + +func (m *Manager) EnableRouting() error { + return nil +} + +func (m *Manager) DisableRouting() error { + return nil +} + // Flush rule/chain/set operations from the buffer // // Method also get all rules after flush and refreshes handle values in the rulesets diff --git a/client/firewall/nftables/manager_linux_test.go b/client/firewall/nftables/manager_linux_test.go index 33fdc4b3d..eaa8ef1f5 100644 --- a/client/firewall/nftables/manager_linux_test.go +++ b/client/firewall/nftables/manager_linux_test.go @@ -74,16 +74,7 @@ func TestNftablesManager(t *testing.T) { testClient := &nftables.Conn{} - rule, err := manager.AddPeerFiltering( - ip, - fw.ProtocolTCP, - nil, - &fw.Port{Values: []int{53}}, - fw.RuleDirectionIN, - fw.ActionDrop, - "", - "", - ) + rule, err := manager.AddPeerFiltering(ip, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{53}}, fw.ActionDrop, "", "") require.NoError(t, err, "failed to add rule") err = manager.Flush() @@ -116,7 +107,7 @@ func TestNftablesManager(t *testing.T) { Kind: expr.VerdictAccept, }, } - require.ElementsMatch(t, rules[0].Exprs, expectedExprs1, "expected the same expressions") + compareExprsIgnoringCounters(t, rules[0].Exprs, expectedExprs1) ipToAdd, _ := netip.AddrFromSlice(ip) add := ipToAdd.Unmap() @@ -209,12 +200,8 @@ func TestNFtablesCreatePerformance(t *testing.T) { ip := net.ParseIP("10.20.0.100") start := time.Now() for i := 0; i < testMax; i++ { - port := &fw.Port{Values: []int{1000 + i}} - if i%2 == 0 { - _, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.RuleDirectionOUT, fw.ActionAccept, "", "accept HTTP traffic") - } else { - _, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.RuleDirectionIN, fw.ActionAccept, "", "accept HTTP traffic") - } + port := &fw.Port{Values: []uint16{uint16(1000 + i)}} + _, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.ActionAccept, "", "accept HTTP traffic") require.NoError(t, err, "failed to add rule") if i%100 == 0 { @@ -296,16 +283,7 @@ func TestNftablesManagerCompatibilityWithIptables(t *testing.T) { }) ip := net.ParseIP("100.96.0.1") - _, err = manager.AddPeerFiltering( - ip, - fw.ProtocolTCP, - nil, - &fw.Port{Values: []int{80}}, - fw.RuleDirectionIN, - fw.ActionAccept, - "", - "test rule", - ) + _, err = manager.AddPeerFiltering(ip, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionAccept, "", "test rule") require.NoError(t, err, "failed to add peer filtering rule") _, err = manager.AddRouteFiltering( @@ -313,7 +291,7 @@ func TestNftablesManagerCompatibilityWithIptables(t *testing.T) { netip.MustParsePrefix("10.1.0.0/24"), fw.ProtocolTCP, nil, - &fw.Port{Values: []int{443}}, + &fw.Port{Values: []uint16{443}}, fw.ActionAccept, ) require.NoError(t, err, "failed to add route filtering rule") @@ -329,3 +307,18 @@ func TestNftablesManagerCompatibilityWithIptables(t *testing.T) { stdout, stderr = runIptablesSave(t) verifyIptablesOutput(t, stdout, stderr) } + +func compareExprsIgnoringCounters(t *testing.T, got, want []expr.Any) { + t.Helper() + require.Equal(t, len(got), len(want), "expression count mismatch") + + for i := range got { + if _, isCounter := got[i].(*expr.Counter); isCounter { + _, wantIsCounter := want[i].(*expr.Counter) + require.True(t, wantIsCounter, "expected Counter at index %d", i) + continue + } + + require.Equal(t, got[i], want[i], "expression mismatch at index %d", i) + } +} diff --git a/client/firewall/nftables/router_linux.go b/client/firewall/nftables/router_linux.go index 34bc9a9bc..92f81f39c 100644 --- a/client/firewall/nftables/router_linux.go +++ b/client/firewall/nftables/router_linux.go @@ -233,7 +233,13 @@ func (r *router) AddRouteFiltering( UserData: []byte(ruleKey), } - rule = r.conn.AddRule(rule) + // Insert DROP rules at the beginning, append ACCEPT rules at the end + if action == firewall.ActionDrop { + // TODO: Insert after the established rule + rule = r.conn.InsertRule(rule) + } else { + rule = r.conn.AddRule(rule) + } log.Tracef("Adding route rule %s", spew.Sdump(rule)) if err := r.conn.Flush(); err != nil { @@ -956,12 +962,12 @@ func applyPort(port *firewall.Port, isSource bool) []expr.Any { &expr.Cmp{ Op: expr.CmpOpGte, Register: 1, - Data: binaryutil.BigEndian.PutUint16(uint16(port.Values[0])), + Data: binaryutil.BigEndian.PutUint16(port.Values[0]), }, &expr.Cmp{ Op: expr.CmpOpLte, Register: 1, - Data: binaryutil.BigEndian.PutUint16(uint16(port.Values[1])), + Data: binaryutil.BigEndian.PutUint16(port.Values[1]), }, ) } else { @@ -980,7 +986,7 @@ func applyPort(port *firewall.Port, isSource bool) []expr.Any { exprs = append(exprs, &expr.Cmp{ Op: expr.CmpOpEq, Register: 1, - Data: binaryutil.BigEndian.PutUint16(uint16(p)), + Data: binaryutil.BigEndian.PutUint16(p), }) } } diff --git a/client/firewall/nftables/router_linux_test.go b/client/firewall/nftables/router_linux_test.go index afc4d5c39..2a5d7168d 100644 --- a/client/firewall/nftables/router_linux_test.go +++ b/client/firewall/nftables/router_linux_test.go @@ -222,7 +222,7 @@ func TestRouter_AddRouteFiltering(t *testing.T) { destination: netip.MustParsePrefix("10.0.0.0/24"), proto: firewall.ProtocolTCP, sPort: nil, - dPort: &firewall.Port{Values: []int{80}}, + dPort: &firewall.Port{Values: []uint16{80}}, direction: firewall.RuleDirectionIN, action: firewall.ActionAccept, expectSet: false, @@ -235,7 +235,7 @@ func TestRouter_AddRouteFiltering(t *testing.T) { }, destination: netip.MustParsePrefix("10.0.0.0/8"), proto: firewall.ProtocolUDP, - sPort: &firewall.Port{Values: []int{1024, 2048}, IsRange: true}, + sPort: &firewall.Port{Values: []uint16{1024, 2048}, IsRange: true}, dPort: nil, direction: firewall.RuleDirectionOUT, action: firewall.ActionDrop, @@ -268,7 +268,7 @@ func TestRouter_AddRouteFiltering(t *testing.T) { sources: []netip.Prefix{netip.MustParsePrefix("172.16.0.0/12")}, destination: netip.MustParsePrefix("192.168.0.0/16"), proto: firewall.ProtocolTCP, - sPort: &firewall.Port{Values: []int{80, 443, 8080}}, + sPort: &firewall.Port{Values: []uint16{80, 443, 8080}}, dPort: nil, direction: firewall.RuleDirectionOUT, action: firewall.ActionAccept, @@ -280,7 +280,7 @@ func TestRouter_AddRouteFiltering(t *testing.T) { destination: netip.MustParsePrefix("10.0.0.0/24"), proto: firewall.ProtocolUDP, sPort: nil, - dPort: &firewall.Port{Values: []int{5000, 5100}, IsRange: true}, + dPort: &firewall.Port{Values: []uint16{5000, 5100}, IsRange: true}, direction: firewall.RuleDirectionIN, action: firewall.ActionDrop, expectSet: false, @@ -290,8 +290,8 @@ func TestRouter_AddRouteFiltering(t *testing.T) { sources: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/24")}, destination: netip.MustParsePrefix("172.16.0.0/16"), proto: firewall.ProtocolTCP, - sPort: &firewall.Port{Values: []int{1024, 65535}, IsRange: true}, - dPort: &firewall.Port{Values: []int{22}}, + sPort: &firewall.Port{Values: []uint16{1024, 65535}, IsRange: true}, + dPort: &firewall.Port{Values: []uint16{22}}, direction: firewall.RuleDirectionOUT, action: firewall.ActionAccept, expectSet: false, diff --git a/client/firewall/nftables/rule_linux.go b/client/firewall/nftables/rule_linux.go index 678c10b44..4d652346b 100644 --- a/client/firewall/nftables/rule_linux.go +++ b/client/firewall/nftables/rule_linux.go @@ -8,10 +8,11 @@ import ( // Rule to handle management of rules type Rule struct { - nftRule *nftables.Rule - nftSet *nftables.Set - ruleID string - ip net.IP + nftRule *nftables.Rule + mangleRule *nftables.Rule + nftSet *nftables.Set + ruleID string + ip net.IP } // GetRuleID returns the rule id diff --git a/client/firewall/uspfilter/allow_netbird.go b/client/firewall/uspfilter/allow_netbird.go index cc0792255..03f23f5e6 100644 --- a/client/firewall/uspfilter/allow_netbird.go +++ b/client/firewall/uspfilter/allow_netbird.go @@ -3,6 +3,11 @@ package uspfilter import ( + "context" + "time" + + log "github.com/sirupsen/logrus" + "github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack" "github.com/netbirdio/netbird/client/internal/statemanager" ) @@ -17,17 +22,29 @@ func (m *Manager) Reset(stateManager *statemanager.Manager) error { if m.udpTracker != nil { m.udpTracker.Close() - m.udpTracker = conntrack.NewUDPTracker(conntrack.DefaultUDPTimeout) + m.udpTracker = conntrack.NewUDPTracker(conntrack.DefaultUDPTimeout, m.logger) } if m.icmpTracker != nil { m.icmpTracker.Close() - m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout) + m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout, m.logger) } if m.tcpTracker != nil { m.tcpTracker.Close() - m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout) + m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout, m.logger) + } + + if m.forwarder != nil { + m.forwarder.Stop() + } + + if m.logger != nil { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + if err := m.logger.Stop(ctx); err != nil { + log.Errorf("failed to shutdown logger: %v", err) + } } if m.nativeFirewall != nil { diff --git a/client/firewall/uspfilter/allow_netbird_windows.go b/client/firewall/uspfilter/allow_netbird_windows.go index 0d55d6268..379585978 100644 --- a/client/firewall/uspfilter/allow_netbird_windows.go +++ b/client/firewall/uspfilter/allow_netbird_windows.go @@ -1,9 +1,11 @@ package uspfilter import ( + "context" "fmt" "os/exec" "syscall" + "time" log "github.com/sirupsen/logrus" @@ -29,17 +31,29 @@ func (m *Manager) Reset(*statemanager.Manager) error { if m.udpTracker != nil { m.udpTracker.Close() - m.udpTracker = conntrack.NewUDPTracker(conntrack.DefaultUDPTimeout) + m.udpTracker = conntrack.NewUDPTracker(conntrack.DefaultUDPTimeout, m.logger) } if m.icmpTracker != nil { m.icmpTracker.Close() - m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout) + m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout, m.logger) } if m.tcpTracker != nil { m.tcpTracker.Close() - m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout) + m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout, m.logger) + } + + if m.forwarder != nil { + m.forwarder.Stop() + } + + if m.logger != nil { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + if err := m.logger.Stop(ctx); err != nil { + log.Errorf("failed to shutdown logger: %v", err) + } } if !isWindowsFirewallReachable() { diff --git a/client/firewall/uspfilter/common/iface.go b/client/firewall/uspfilter/common/iface.go new file mode 100644 index 000000000..d44e79509 --- /dev/null +++ b/client/firewall/uspfilter/common/iface.go @@ -0,0 +1,16 @@ +package common + +import ( + wgdevice "golang.zx2c4.com/wireguard/device" + + "github.com/netbirdio/netbird/client/iface" + "github.com/netbirdio/netbird/client/iface/device" +) + +// IFaceMapper defines subset methods of interface required for manager +type IFaceMapper interface { + SetFilter(device.PacketFilter) error + Address() iface.WGAddress + GetWGDevice() *wgdevice.Device + GetDevice() *device.FilteredDevice +} diff --git a/client/firewall/uspfilter/conntrack/common.go b/client/firewall/uspfilter/conntrack/common.go index e459bc75a..f5f502540 100644 --- a/client/firewall/uspfilter/conntrack/common.go +++ b/client/firewall/uspfilter/conntrack/common.go @@ -10,12 +10,11 @@ import ( // BaseConnTrack provides common fields and locking for all connection types type BaseConnTrack struct { - SourceIP net.IP - DestIP net.IP - SourcePort uint16 - DestPort uint16 - lastSeen atomic.Int64 // Unix nano for atomic access - established atomic.Bool + SourceIP net.IP + DestIP net.IP + SourcePort uint16 + DestPort uint16 + lastSeen atomic.Int64 // Unix nano for atomic access } // these small methods will be inlined by the compiler @@ -25,16 +24,6 @@ func (b *BaseConnTrack) UpdateLastSeen() { b.lastSeen.Store(time.Now().UnixNano()) } -// IsEstablished safely checks if connection is established -func (b *BaseConnTrack) IsEstablished() bool { - return b.established.Load() -} - -// SetEstablished safely sets the established state -func (b *BaseConnTrack) SetEstablished(state bool) { - b.established.Store(state) -} - // GetLastSeen safely gets the last seen timestamp func (b *BaseConnTrack) GetLastSeen() time.Time { return time.Unix(0, b.lastSeen.Load()) diff --git a/client/firewall/uspfilter/conntrack/common_test.go b/client/firewall/uspfilter/conntrack/common_test.go index 72d006def..81fa64b19 100644 --- a/client/firewall/uspfilter/conntrack/common_test.go +++ b/client/firewall/uspfilter/conntrack/common_test.go @@ -3,8 +3,14 @@ package conntrack import ( "net" "testing" + + "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/firewall/uspfilter/log" ) +var logger = log.NewFromLogrus(logrus.StandardLogger()) + func BenchmarkIPOperations(b *testing.B) { b.Run("MakeIPAddr", func(b *testing.B) { ip := net.ParseIP("192.168.1.1") @@ -34,37 +40,11 @@ func BenchmarkIPOperations(b *testing.B) { }) } -func BenchmarkAtomicOperations(b *testing.B) { - conn := &BaseConnTrack{} - b.Run("UpdateLastSeen", func(b *testing.B) { - for i := 0; i < b.N; i++ { - conn.UpdateLastSeen() - } - }) - - b.Run("IsEstablished", func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = conn.IsEstablished() - } - }) - - b.Run("SetEstablished", func(b *testing.B) { - for i := 0; i < b.N; i++ { - conn.SetEstablished(i%2 == 0) - } - }) - - b.Run("GetLastSeen", func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = conn.GetLastSeen() - } - }) -} // Memory pressure tests func BenchmarkMemoryPressure(b *testing.B) { b.Run("TCPHighLoad", func(b *testing.B) { - tracker := NewTCPTracker(DefaultTCPTimeout) + tracker := NewTCPTracker(DefaultTCPTimeout, logger) defer tracker.Close() // Generate different IPs @@ -89,7 +69,7 @@ func BenchmarkMemoryPressure(b *testing.B) { }) b.Run("UDPHighLoad", func(b *testing.B) { - tracker := NewUDPTracker(DefaultUDPTimeout) + tracker := NewUDPTracker(DefaultUDPTimeout, logger) defer tracker.Close() // Generate different IPs diff --git a/client/firewall/uspfilter/conntrack/icmp.go b/client/firewall/uspfilter/conntrack/icmp.go index e0a971678..25cd9e87d 100644 --- a/client/firewall/uspfilter/conntrack/icmp.go +++ b/client/firewall/uspfilter/conntrack/icmp.go @@ -6,6 +6,8 @@ import ( "time" "github.com/google/gopacket/layers" + + nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log" ) const ( @@ -33,6 +35,7 @@ type ICMPConnTrack struct { // ICMPTracker manages ICMP connection states type ICMPTracker struct { + logger *nblog.Logger connections map[ICMPConnKey]*ICMPConnTrack timeout time.Duration cleanupTicker *time.Ticker @@ -42,12 +45,13 @@ type ICMPTracker struct { } // NewICMPTracker creates a new ICMP connection tracker -func NewICMPTracker(timeout time.Duration) *ICMPTracker { +func NewICMPTracker(timeout time.Duration, logger *nblog.Logger) *ICMPTracker { if timeout == 0 { timeout = DefaultICMPTimeout } tracker := &ICMPTracker{ + logger: logger, connections: make(map[ICMPConnKey]*ICMPConnTrack), timeout: timeout, cleanupTicker: time.NewTicker(ICMPCleanupInterval), @@ -62,7 +66,6 @@ func NewICMPTracker(timeout time.Duration) *ICMPTracker { // TrackOutbound records an outbound ICMP Echo Request func (t *ICMPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, id uint16, seq uint16) { key := makeICMPKey(srcIP, dstIP, id, seq) - now := time.Now().UnixNano() t.mutex.Lock() conn, exists := t.connections[key] @@ -80,24 +83,19 @@ func (t *ICMPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, id uint16, seq u ID: id, Sequence: seq, } - conn.lastSeen.Store(now) - conn.established.Store(true) + conn.UpdateLastSeen() t.connections[key] = conn + + t.logger.Trace("New ICMP connection %v", key) } t.mutex.Unlock() - conn.lastSeen.Store(now) + conn.UpdateLastSeen() } // IsValidInbound checks if an inbound ICMP Echo Reply matches a tracked request func (t *ICMPTracker) IsValidInbound(srcIP net.IP, dstIP net.IP, id uint16, seq uint16, icmpType uint8) bool { - switch icmpType { - case uint8(layers.ICMPv4TypeDestinationUnreachable), - uint8(layers.ICMPv4TypeTimeExceeded): - return true - case uint8(layers.ICMPv4TypeEchoReply): - // continue processing - default: + if icmpType != uint8(layers.ICMPv4TypeEchoReply) { return false } @@ -115,8 +113,7 @@ func (t *ICMPTracker) IsValidInbound(srcIP net.IP, dstIP net.IP, id uint16, seq return false } - return conn.IsEstablished() && - ValidateIPs(MakeIPAddr(srcIP), conn.DestIP) && + return ValidateIPs(MakeIPAddr(srcIP), conn.DestIP) && ValidateIPs(MakeIPAddr(dstIP), conn.SourceIP) && conn.ID == id && conn.Sequence == seq @@ -141,6 +138,8 @@ func (t *ICMPTracker) cleanup() { t.ipPool.Put(conn.SourceIP) t.ipPool.Put(conn.DestIP) delete(t.connections, key) + + t.logger.Debug("Removed ICMP connection %v (timeout)", key) } } } diff --git a/client/firewall/uspfilter/conntrack/icmp_test.go b/client/firewall/uspfilter/conntrack/icmp_test.go index 21176e719..32553c836 100644 --- a/client/firewall/uspfilter/conntrack/icmp_test.go +++ b/client/firewall/uspfilter/conntrack/icmp_test.go @@ -7,7 +7,7 @@ import ( func BenchmarkICMPTracker(b *testing.B) { b.Run("TrackOutbound", func(b *testing.B) { - tracker := NewICMPTracker(DefaultICMPTimeout) + tracker := NewICMPTracker(DefaultICMPTimeout, logger) defer tracker.Close() srcIP := net.ParseIP("192.168.1.1") @@ -20,7 +20,7 @@ func BenchmarkICMPTracker(b *testing.B) { }) b.Run("IsValidInbound", func(b *testing.B) { - tracker := NewICMPTracker(DefaultICMPTimeout) + tracker := NewICMPTracker(DefaultICMPTimeout, logger) defer tracker.Close() srcIP := net.ParseIP("192.168.1.1") diff --git a/client/firewall/uspfilter/conntrack/tcp.go b/client/firewall/uspfilter/conntrack/tcp.go index a7968dc73..7c12e8ad0 100644 --- a/client/firewall/uspfilter/conntrack/tcp.go +++ b/client/firewall/uspfilter/conntrack/tcp.go @@ -5,7 +5,10 @@ package conntrack import ( "net" "sync" + "sync/atomic" "time" + + nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log" ) const ( @@ -61,12 +64,24 @@ type TCPConnKey struct { // TCPConnTrack represents a TCP connection state type TCPConnTrack struct { BaseConnTrack - State TCPState + State TCPState + established atomic.Bool sync.RWMutex } +// IsEstablished safely checks if connection is established +func (t *TCPConnTrack) IsEstablished() bool { + return t.established.Load() +} + +// SetEstablished safely sets the established state +func (t *TCPConnTrack) SetEstablished(state bool) { + t.established.Store(state) +} + // TCPTracker manages TCP connection states type TCPTracker struct { + logger *nblog.Logger connections map[ConnKey]*TCPConnTrack mutex sync.RWMutex cleanupTicker *time.Ticker @@ -76,8 +91,9 @@ type TCPTracker struct { } // NewTCPTracker creates a new TCP connection tracker -func NewTCPTracker(timeout time.Duration) *TCPTracker { +func NewTCPTracker(timeout time.Duration, logger *nblog.Logger) *TCPTracker { tracker := &TCPTracker{ + logger: logger, connections: make(map[ConnKey]*TCPConnTrack), cleanupTicker: time.NewTicker(TCPCleanupInterval), done: make(chan struct{}), @@ -93,7 +109,6 @@ func NewTCPTracker(timeout time.Duration) *TCPTracker { func (t *TCPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, srcPort uint16, dstPort uint16, flags uint8) { // Create key before lock key := makeConnKey(srcIP, dstIP, srcPort, dstPort) - now := time.Now().UnixNano() t.mutex.Lock() conn, exists := t.connections[key] @@ -113,9 +128,11 @@ func (t *TCPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, srcPort uint16, d }, State: TCPStateNew, } - conn.lastSeen.Store(now) + conn.UpdateLastSeen() conn.established.Store(false) t.connections[key] = conn + + t.logger.Trace("New TCP connection: %s:%d -> %s:%d", srcIP, srcPort, dstIP, dstPort) } t.mutex.Unlock() @@ -123,7 +140,7 @@ func (t *TCPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, srcPort uint16, d conn.Lock() t.updateState(conn, flags, true) conn.Unlock() - conn.lastSeen.Store(now) + conn.UpdateLastSeen() } // IsValidInbound checks if an inbound TCP packet matches a tracked connection @@ -171,6 +188,9 @@ func (t *TCPTracker) updateState(conn *TCPConnTrack, flags uint8, isOutbound boo if flags&TCPRst != 0 { conn.State = TCPStateClosed conn.SetEstablished(false) + + t.logger.Trace("TCP connection reset: %s:%d -> %s:%d", + conn.SourceIP, conn.SourcePort, conn.DestIP, conn.DestPort) return } @@ -227,6 +247,9 @@ func (t *TCPTracker) updateState(conn *TCPConnTrack, flags uint8, isOutbound boo if flags&TCPAck != 0 { conn.State = TCPStateTimeWait // Keep established = false from previous state + + t.logger.Trace("TCP connection closed (simultaneous) - %s:%d -> %s:%d", + conn.SourceIP, conn.SourcePort, conn.DestIP, conn.DestPort) } case TCPStateCloseWait: @@ -237,11 +260,17 @@ func (t *TCPTracker) updateState(conn *TCPConnTrack, flags uint8, isOutbound boo case TCPStateLastAck: if flags&TCPAck != 0 { conn.State = TCPStateClosed + + t.logger.Trace("TCP connection gracefully closed: %s:%d -> %s:%d", + conn.SourceIP, conn.SourcePort, conn.DestIP, conn.DestPort) } case TCPStateTimeWait: // Stay in TIME-WAIT for 2MSL before transitioning to closed // This is handled by the cleanup routine + + t.logger.Trace("TCP connection completed - %s:%d -> %s:%d", + conn.SourceIP, conn.SourcePort, conn.DestIP, conn.DestPort) } } @@ -318,6 +347,8 @@ func (t *TCPTracker) cleanup() { t.ipPool.Put(conn.SourceIP) t.ipPool.Put(conn.DestIP) delete(t.connections, key) + + t.logger.Trace("Cleaned up TCP connection: %s:%d -> %s:%d", conn.SourceIP, conn.SourcePort, conn.DestIP, conn.DestPort) } } } diff --git a/client/firewall/uspfilter/conntrack/tcp_test.go b/client/firewall/uspfilter/conntrack/tcp_test.go index 6c8f82423..5f4c43915 100644 --- a/client/firewall/uspfilter/conntrack/tcp_test.go +++ b/client/firewall/uspfilter/conntrack/tcp_test.go @@ -9,7 +9,7 @@ import ( ) func TestTCPStateMachine(t *testing.T) { - tracker := NewTCPTracker(DefaultTCPTimeout) + tracker := NewTCPTracker(DefaultTCPTimeout, logger) defer tracker.Close() srcIP := net.ParseIP("100.64.0.1") @@ -154,7 +154,7 @@ func TestTCPStateMachine(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Helper() - tracker = NewTCPTracker(DefaultTCPTimeout) + tracker = NewTCPTracker(DefaultTCPTimeout, logger) tt.test(t) }) } @@ -162,7 +162,7 @@ func TestTCPStateMachine(t *testing.T) { } func TestRSTHandling(t *testing.T) { - tracker := NewTCPTracker(DefaultTCPTimeout) + tracker := NewTCPTracker(DefaultTCPTimeout, logger) defer tracker.Close() srcIP := net.ParseIP("100.64.0.1") @@ -233,7 +233,7 @@ func establishConnection(t *testing.T, tracker *TCPTracker, srcIP, dstIP net.IP, func BenchmarkTCPTracker(b *testing.B) { b.Run("TrackOutbound", func(b *testing.B) { - tracker := NewTCPTracker(DefaultTCPTimeout) + tracker := NewTCPTracker(DefaultTCPTimeout, logger) defer tracker.Close() srcIP := net.ParseIP("192.168.1.1") @@ -246,7 +246,7 @@ func BenchmarkTCPTracker(b *testing.B) { }) b.Run("IsValidInbound", func(b *testing.B) { - tracker := NewTCPTracker(DefaultTCPTimeout) + tracker := NewTCPTracker(DefaultTCPTimeout, logger) defer tracker.Close() srcIP := net.ParseIP("192.168.1.1") @@ -264,7 +264,7 @@ func BenchmarkTCPTracker(b *testing.B) { }) b.Run("ConcurrentAccess", func(b *testing.B) { - tracker := NewTCPTracker(DefaultTCPTimeout) + tracker := NewTCPTracker(DefaultTCPTimeout, logger) defer tracker.Close() srcIP := net.ParseIP("192.168.1.1") @@ -287,7 +287,7 @@ func BenchmarkTCPTracker(b *testing.B) { // Benchmark connection cleanup func BenchmarkCleanup(b *testing.B) { b.Run("TCPCleanup", func(b *testing.B) { - tracker := NewTCPTracker(100 * time.Millisecond) // Short timeout for testing + tracker := NewTCPTracker(100*time.Millisecond, logger) // Short timeout for testing defer tracker.Close() // Pre-populate with expired connections diff --git a/client/firewall/uspfilter/conntrack/udp.go b/client/firewall/uspfilter/conntrack/udp.go index a969a4e84..e73465e31 100644 --- a/client/firewall/uspfilter/conntrack/udp.go +++ b/client/firewall/uspfilter/conntrack/udp.go @@ -4,6 +4,8 @@ import ( "net" "sync" "time" + + nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log" ) const ( @@ -20,6 +22,7 @@ type UDPConnTrack struct { // UDPTracker manages UDP connection states type UDPTracker struct { + logger *nblog.Logger connections map[ConnKey]*UDPConnTrack timeout time.Duration cleanupTicker *time.Ticker @@ -29,12 +32,13 @@ type UDPTracker struct { } // NewUDPTracker creates a new UDP connection tracker -func NewUDPTracker(timeout time.Duration) *UDPTracker { +func NewUDPTracker(timeout time.Duration, logger *nblog.Logger) *UDPTracker { if timeout == 0 { timeout = DefaultUDPTimeout } tracker := &UDPTracker{ + logger: logger, connections: make(map[ConnKey]*UDPConnTrack), timeout: timeout, cleanupTicker: time.NewTicker(UDPCleanupInterval), @@ -49,7 +53,6 @@ func NewUDPTracker(timeout time.Duration) *UDPTracker { // TrackOutbound records an outbound UDP connection func (t *UDPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, srcPort uint16, dstPort uint16) { key := makeConnKey(srcIP, dstIP, srcPort, dstPort) - now := time.Now().UnixNano() t.mutex.Lock() conn, exists := t.connections[key] @@ -67,13 +70,14 @@ func (t *UDPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, srcPort uint16, d DestPort: dstPort, }, } - conn.lastSeen.Store(now) - conn.established.Store(true) + conn.UpdateLastSeen() t.connections[key] = conn + + t.logger.Trace("New UDP connection: %v", conn) } t.mutex.Unlock() - conn.lastSeen.Store(now) + conn.UpdateLastSeen() } // IsValidInbound checks if an inbound packet matches a tracked connection @@ -92,8 +96,7 @@ func (t *UDPTracker) IsValidInbound(srcIP net.IP, dstIP net.IP, srcPort uint16, return false } - return conn.IsEstablished() && - ValidateIPs(MakeIPAddr(srcIP), conn.DestIP) && + return ValidateIPs(MakeIPAddr(srcIP), conn.DestIP) && ValidateIPs(MakeIPAddr(dstIP), conn.SourceIP) && conn.DestPort == srcPort && conn.SourcePort == dstPort @@ -120,6 +123,8 @@ func (t *UDPTracker) cleanup() { t.ipPool.Put(conn.SourceIP) t.ipPool.Put(conn.DestIP) delete(t.connections, key) + + t.logger.Trace("Removed UDP connection %v (timeout)", conn) } } } diff --git a/client/firewall/uspfilter/conntrack/udp_test.go b/client/firewall/uspfilter/conntrack/udp_test.go index 671721890..fa83ee356 100644 --- a/client/firewall/uspfilter/conntrack/udp_test.go +++ b/client/firewall/uspfilter/conntrack/udp_test.go @@ -29,7 +29,7 @@ func TestNewUDPTracker(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tracker := NewUDPTracker(tt.timeout) + tracker := NewUDPTracker(tt.timeout, logger) assert.NotNil(t, tracker) assert.Equal(t, tt.wantTimeout, tracker.timeout) assert.NotNil(t, tracker.connections) @@ -40,7 +40,7 @@ func TestNewUDPTracker(t *testing.T) { } func TestUDPTracker_TrackOutbound(t *testing.T) { - tracker := NewUDPTracker(DefaultUDPTimeout) + tracker := NewUDPTracker(DefaultUDPTimeout, logger) defer tracker.Close() srcIP := net.ParseIP("192.168.1.2") @@ -58,12 +58,11 @@ func TestUDPTracker_TrackOutbound(t *testing.T) { assert.True(t, conn.DestIP.Equal(dstIP)) assert.Equal(t, srcPort, conn.SourcePort) assert.Equal(t, dstPort, conn.DestPort) - assert.True(t, conn.IsEstablished()) assert.WithinDuration(t, time.Now(), conn.GetLastSeen(), 1*time.Second) } func TestUDPTracker_IsValidInbound(t *testing.T) { - tracker := NewUDPTracker(1 * time.Second) + tracker := NewUDPTracker(1*time.Second, logger) defer tracker.Close() srcIP := net.ParseIP("192.168.1.2") @@ -162,6 +161,7 @@ func TestUDPTracker_Cleanup(t *testing.T) { cleanupTicker: time.NewTicker(cleanupInterval), done: make(chan struct{}), ipPool: NewPreallocatedIPs(), + logger: logger, } // Start cleanup routine @@ -211,7 +211,7 @@ func TestUDPTracker_Cleanup(t *testing.T) { func BenchmarkUDPTracker(b *testing.B) { b.Run("TrackOutbound", func(b *testing.B) { - tracker := NewUDPTracker(DefaultUDPTimeout) + tracker := NewUDPTracker(DefaultUDPTimeout, logger) defer tracker.Close() srcIP := net.ParseIP("192.168.1.1") @@ -224,7 +224,7 @@ func BenchmarkUDPTracker(b *testing.B) { }) b.Run("IsValidInbound", func(b *testing.B) { - tracker := NewUDPTracker(DefaultUDPTimeout) + tracker := NewUDPTracker(DefaultUDPTimeout, logger) defer tracker.Close() srcIP := net.ParseIP("192.168.1.1") diff --git a/client/firewall/uspfilter/forwarder/endpoint.go b/client/firewall/uspfilter/forwarder/endpoint.go new file mode 100644 index 000000000..e8a265c94 --- /dev/null +++ b/client/firewall/uspfilter/forwarder/endpoint.go @@ -0,0 +1,81 @@ +package forwarder + +import ( + wgdevice "golang.zx2c4.com/wireguard/device" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/stack" + + nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log" +) + +// endpoint implements stack.LinkEndpoint and handles integration with the wireguard device +type endpoint struct { + logger *nblog.Logger + dispatcher stack.NetworkDispatcher + device *wgdevice.Device + mtu uint32 +} + +func (e *endpoint) Attach(dispatcher stack.NetworkDispatcher) { + e.dispatcher = dispatcher +} + +func (e *endpoint) IsAttached() bool { + return e.dispatcher != nil +} + +func (e *endpoint) MTU() uint32 { + return e.mtu +} + +func (e *endpoint) Capabilities() stack.LinkEndpointCapabilities { + return stack.CapabilityNone +} + +func (e *endpoint) MaxHeaderLength() uint16 { + return 0 +} + +func (e *endpoint) LinkAddress() tcpip.LinkAddress { + return "" +} + +func (e *endpoint) WritePackets(pkts stack.PacketBufferList) (int, tcpip.Error) { + var written int + for _, pkt := range pkts.AsSlice() { + netHeader := header.IPv4(pkt.NetworkHeader().View().AsSlice()) + + data := stack.PayloadSince(pkt.NetworkHeader()) + if data == nil { + continue + } + + // Send the packet through WireGuard + address := netHeader.DestinationAddress() + err := e.device.CreateOutboundPacket(data.AsSlice(), address.AsSlice()) + if err != nil { + e.logger.Error("CreateOutboundPacket: %v", err) + continue + } + written++ + } + + return written, nil +} + +func (e *endpoint) Wait() { + // not required +} + +func (e *endpoint) ARPHardwareType() header.ARPHardwareType { + return header.ARPHardwareNone +} + +func (e *endpoint) AddHeader(*stack.PacketBuffer) { + // not required +} + +func (e *endpoint) ParseHeader(*stack.PacketBuffer) bool { + return true +} diff --git a/client/firewall/uspfilter/forwarder/forwarder.go b/client/firewall/uspfilter/forwarder/forwarder.go new file mode 100644 index 000000000..4ed152b79 --- /dev/null +++ b/client/firewall/uspfilter/forwarder/forwarder.go @@ -0,0 +1,166 @@ +package forwarder + +import ( + "context" + "fmt" + "net" + "runtime" + + log "github.com/sirupsen/logrus" + "gvisor.dev/gvisor/pkg/buffer" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/network/ipv4" + "gvisor.dev/gvisor/pkg/tcpip/stack" + "gvisor.dev/gvisor/pkg/tcpip/transport/icmp" + "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" + "gvisor.dev/gvisor/pkg/tcpip/transport/udp" + + "github.com/netbirdio/netbird/client/firewall/uspfilter/common" + nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log" +) + +const ( + defaultReceiveWindow = 32768 + defaultMaxInFlight = 1024 + iosReceiveWindow = 16384 + iosMaxInFlight = 256 +) + +type Forwarder struct { + logger *nblog.Logger + stack *stack.Stack + endpoint *endpoint + udpForwarder *udpForwarder + ctx context.Context + cancel context.CancelFunc + ip net.IP + netstack bool +} + +func New(iface common.IFaceMapper, logger *nblog.Logger, netstack bool) (*Forwarder, error) { + s := stack.New(stack.Options{ + NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol}, + TransportProtocols: []stack.TransportProtocolFactory{ + tcp.NewProtocol, + udp.NewProtocol, + icmp.NewProtocol4, + }, + HandleLocal: false, + }) + + mtu, err := iface.GetDevice().MTU() + if err != nil { + return nil, fmt.Errorf("get MTU: %w", err) + } + nicID := tcpip.NICID(1) + endpoint := &endpoint{ + logger: logger, + device: iface.GetWGDevice(), + mtu: uint32(mtu), + } + + if err := s.CreateNIC(nicID, endpoint); err != nil { + return nil, fmt.Errorf("failed to create NIC: %v", err) + } + + ones, _ := iface.Address().Network.Mask.Size() + protoAddr := tcpip.ProtocolAddress{ + Protocol: ipv4.ProtocolNumber, + AddressWithPrefix: tcpip.AddressWithPrefix{ + Address: tcpip.AddrFromSlice(iface.Address().IP.To4()), + PrefixLen: ones, + }, + } + + if err := s.AddProtocolAddress(nicID, protoAddr, stack.AddressProperties{}); err != nil { + return nil, fmt.Errorf("failed to add protocol address: %s", err) + } + + defaultSubnet, err := tcpip.NewSubnet( + tcpip.AddrFrom4([4]byte{0, 0, 0, 0}), + tcpip.MaskFromBytes([]byte{0, 0, 0, 0}), + ) + if err != nil { + return nil, fmt.Errorf("creating default subnet: %w", err) + } + + if err := s.SetPromiscuousMode(nicID, true); err != nil { + return nil, fmt.Errorf("set promiscuous mode: %s", err) + } + if err := s.SetSpoofing(nicID, true); err != nil { + return nil, fmt.Errorf("set spoofing: %s", err) + } + + s.SetRouteTable([]tcpip.Route{ + { + Destination: defaultSubnet, + NIC: nicID, + }, + }) + + ctx, cancel := context.WithCancel(context.Background()) + f := &Forwarder{ + logger: logger, + stack: s, + endpoint: endpoint, + udpForwarder: newUDPForwarder(mtu, logger), + ctx: ctx, + cancel: cancel, + netstack: netstack, + ip: iface.Address().IP, + } + + receiveWindow := defaultReceiveWindow + maxInFlight := defaultMaxInFlight + if runtime.GOOS == "ios" { + receiveWindow = iosReceiveWindow + maxInFlight = iosMaxInFlight + } + + tcpForwarder := tcp.NewForwarder(s, receiveWindow, maxInFlight, f.handleTCP) + s.SetTransportProtocolHandler(tcp.ProtocolNumber, tcpForwarder.HandlePacket) + + udpForwarder := udp.NewForwarder(s, f.handleUDP) + s.SetTransportProtocolHandler(udp.ProtocolNumber, udpForwarder.HandlePacket) + + s.SetTransportProtocolHandler(icmp.ProtocolNumber4, f.handleICMP) + + log.Debugf("forwarder: Initialization complete with NIC %d", nicID) + return f, nil +} + +func (f *Forwarder) InjectIncomingPacket(payload []byte) error { + if len(payload) < header.IPv4MinimumSize { + return fmt.Errorf("packet too small: %d bytes", len(payload)) + } + + pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ + Payload: buffer.MakeWithData(payload), + }) + defer pkt.DecRef() + + if f.endpoint.dispatcher != nil { + f.endpoint.dispatcher.DeliverNetworkPacket(ipv4.ProtocolNumber, pkt) + } + return nil +} + +// Stop gracefully shuts down the forwarder +func (f *Forwarder) Stop() { + f.cancel() + + if f.udpForwarder != nil { + f.udpForwarder.Stop() + } + + f.stack.Close() + f.stack.Wait() +} + +func (f *Forwarder) determineDialAddr(addr tcpip.Address) net.IP { + if f.netstack && f.ip.Equal(addr.AsSlice()) { + return net.IPv4(127, 0, 0, 1) + } + return addr.AsSlice() +} diff --git a/client/firewall/uspfilter/forwarder/icmp.go b/client/firewall/uspfilter/forwarder/icmp.go new file mode 100644 index 000000000..14cdc37be --- /dev/null +++ b/client/firewall/uspfilter/forwarder/icmp.go @@ -0,0 +1,109 @@ +package forwarder + +import ( + "context" + "net" + "time" + + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/stack" +) + +// handleICMP handles ICMP packets from the network stack +func (f *Forwarder) handleICMP(id stack.TransportEndpointID, pkt stack.PacketBufferPtr) bool { + ctx, cancel := context.WithTimeout(f.ctx, 5*time.Second) + defer cancel() + + lc := net.ListenConfig{} + // TODO: support non-root + conn, err := lc.ListenPacket(ctx, "ip4:icmp", "0.0.0.0") + if err != nil { + f.logger.Error("Failed to create ICMP socket for %v: %v", id, err) + + // This will make netstack reply on behalf of the original destination, that's ok for now + return false + } + defer func() { + if err := conn.Close(); err != nil { + f.logger.Debug("Failed to close ICMP socket: %v", err) + } + }() + + dstIP := f.determineDialAddr(id.LocalAddress) + dst := &net.IPAddr{IP: dstIP} + + // Get the complete ICMP message (header + data) + fullPacket := stack.PayloadSince(pkt.TransportHeader()) + payload := fullPacket.AsSlice() + + icmpHdr := header.ICMPv4(pkt.TransportHeader().View().AsSlice()) + + // For Echo Requests, send and handle response + switch icmpHdr.Type() { + case header.ICMPv4Echo: + return f.handleEchoResponse(icmpHdr, payload, dst, conn, id) + case header.ICMPv4EchoReply: + // dont process our own replies + return true + default: + } + + // For other ICMP types (Time Exceeded, Destination Unreachable, etc) + _, err = conn.WriteTo(payload, dst) + if err != nil { + f.logger.Error("Failed to write ICMP packet for %v: %v", id, err) + return true + } + + f.logger.Trace("Forwarded ICMP packet %v type=%v code=%v", + id, icmpHdr.Type(), icmpHdr.Code()) + + return true +} + +func (f *Forwarder) handleEchoResponse(icmpHdr header.ICMPv4, payload []byte, dst *net.IPAddr, conn net.PacketConn, id stack.TransportEndpointID) bool { + if _, err := conn.WriteTo(payload, dst); err != nil { + f.logger.Error("Failed to write ICMP packet for %v: %v", id, err) + return true + } + + f.logger.Trace("Forwarded ICMP packet %v type=%v code=%v", + id, icmpHdr.Type(), icmpHdr.Code()) + + if err := conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil { + f.logger.Error("Failed to set read deadline for ICMP response: %v", err) + return true + } + + response := make([]byte, f.endpoint.mtu) + n, _, err := conn.ReadFrom(response) + if err != nil { + if !isTimeout(err) { + f.logger.Error("Failed to read ICMP response: %v", err) + } + return true + } + + ipHdr := make([]byte, header.IPv4MinimumSize) + ip := header.IPv4(ipHdr) + ip.Encode(&header.IPv4Fields{ + TotalLength: uint16(header.IPv4MinimumSize + n), + TTL: 64, + Protocol: uint8(header.ICMPv4ProtocolNumber), + SrcAddr: id.LocalAddress, + DstAddr: id.RemoteAddress, + }) + ip.SetChecksum(^ip.CalculateChecksum()) + + fullPacket := make([]byte, 0, len(ipHdr)+n) + fullPacket = append(fullPacket, ipHdr...) + fullPacket = append(fullPacket, response[:n]...) + + if err := f.InjectIncomingPacket(fullPacket); err != nil { + f.logger.Error("Failed to inject ICMP response: %v", err) + return true + } + + f.logger.Trace("Forwarded ICMP echo reply for %v", id) + return true +} diff --git a/client/firewall/uspfilter/forwarder/tcp.go b/client/firewall/uspfilter/forwarder/tcp.go new file mode 100644 index 000000000..6d7cf3b6a --- /dev/null +++ b/client/firewall/uspfilter/forwarder/tcp.go @@ -0,0 +1,90 @@ +package forwarder + +import ( + "context" + "fmt" + "io" + "net" + + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" + "gvisor.dev/gvisor/pkg/tcpip/stack" + "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" + "gvisor.dev/gvisor/pkg/waiter" +) + +// handleTCP is called by the TCP forwarder for new connections. +func (f *Forwarder) handleTCP(r *tcp.ForwarderRequest) { + id := r.ID() + + dialAddr := fmt.Sprintf("%s:%d", f.determineDialAddr(id.LocalAddress), id.LocalPort) + + outConn, err := (&net.Dialer{}).DialContext(f.ctx, "tcp", dialAddr) + if err != nil { + r.Complete(true) + f.logger.Trace("forwarder: dial error for %v: %v", id, err) + return + } + + // Create wait queue for blocking syscalls + wq := waiter.Queue{} + + ep, epErr := r.CreateEndpoint(&wq) + if epErr != nil { + f.logger.Error("forwarder: failed to create TCP endpoint: %v", epErr) + if err := outConn.Close(); err != nil { + f.logger.Debug("forwarder: outConn close error: %v", err) + } + r.Complete(true) + return + } + + // Complete the handshake + r.Complete(false) + + inConn := gonet.NewTCPConn(&wq, ep) + + f.logger.Trace("forwarder: established TCP connection %v", id) + + go f.proxyTCP(id, inConn, outConn, ep) +} + +func (f *Forwarder) proxyTCP(id stack.TransportEndpointID, inConn *gonet.TCPConn, outConn net.Conn, ep tcpip.Endpoint) { + defer func() { + if err := inConn.Close(); err != nil { + f.logger.Debug("forwarder: inConn close error: %v", err) + } + if err := outConn.Close(); err != nil { + f.logger.Debug("forwarder: outConn close error: %v", err) + } + ep.Close() + }() + + // Create context for managing the proxy goroutines + ctx, cancel := context.WithCancel(f.ctx) + defer cancel() + + errChan := make(chan error, 2) + + go func() { + _, err := io.Copy(outConn, inConn) + errChan <- err + }() + + go func() { + _, err := io.Copy(inConn, outConn) + errChan <- err + }() + + select { + case <-ctx.Done(): + f.logger.Trace("forwarder: tearing down TCP connection %v due to context done", id) + return + case err := <-errChan: + if err != nil && !isClosedError(err) { + f.logger.Error("proxyTCP: copy error: %v", err) + } + f.logger.Trace("forwarder: tearing down TCP connection %v", id) + return + } +} diff --git a/client/firewall/uspfilter/forwarder/udp.go b/client/firewall/uspfilter/forwarder/udp.go new file mode 100644 index 000000000..c37740587 --- /dev/null +++ b/client/firewall/uspfilter/forwarder/udp.go @@ -0,0 +1,284 @@ +package forwarder + +import ( + "context" + "errors" + "fmt" + "net" + "sync" + "sync/atomic" + "time" + + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" + "gvisor.dev/gvisor/pkg/tcpip/stack" + "gvisor.dev/gvisor/pkg/tcpip/transport/udp" + "gvisor.dev/gvisor/pkg/waiter" + + nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log" +) + +const ( + udpTimeout = 30 * time.Second +) + +type udpPacketConn struct { + conn *gonet.UDPConn + outConn net.Conn + lastSeen atomic.Int64 + cancel context.CancelFunc + ep tcpip.Endpoint +} + +type udpForwarder struct { + sync.RWMutex + logger *nblog.Logger + conns map[stack.TransportEndpointID]*udpPacketConn + bufPool sync.Pool + ctx context.Context + cancel context.CancelFunc +} + +type idleConn struct { + id stack.TransportEndpointID + conn *udpPacketConn +} + +func newUDPForwarder(mtu int, logger *nblog.Logger) *udpForwarder { + ctx, cancel := context.WithCancel(context.Background()) + f := &udpForwarder{ + logger: logger, + conns: make(map[stack.TransportEndpointID]*udpPacketConn), + ctx: ctx, + cancel: cancel, + bufPool: sync.Pool{ + New: func() any { + b := make([]byte, mtu) + return &b + }, + }, + } + go f.cleanup() + return f +} + +// Stop stops the UDP forwarder and all active connections +func (f *udpForwarder) Stop() { + f.cancel() + + f.Lock() + defer f.Unlock() + + for id, conn := range f.conns { + conn.cancel() + if err := conn.conn.Close(); err != nil { + f.logger.Debug("forwarder: UDP conn close error for %v: %v", id, err) + } + if err := conn.outConn.Close(); err != nil { + f.logger.Debug("forwarder: UDP outConn close error for %v: %v", id, err) + } + + conn.ep.Close() + delete(f.conns, id) + } +} + +// cleanup periodically removes idle UDP connections +func (f *udpForwarder) cleanup() { + ticker := time.NewTicker(time.Minute) + defer ticker.Stop() + + for { + select { + case <-f.ctx.Done(): + return + case <-ticker.C: + var idleConns []idleConn + + f.RLock() + for id, conn := range f.conns { + if conn.getIdleDuration() > udpTimeout { + idleConns = append(idleConns, idleConn{id, conn}) + } + } + f.RUnlock() + + for _, idle := range idleConns { + idle.conn.cancel() + if err := idle.conn.conn.Close(); err != nil { + f.logger.Debug("forwarder: UDP conn close error for %v: %v", idle.id, err) + } + if err := idle.conn.outConn.Close(); err != nil { + f.logger.Debug("forwarder: UDP outConn close error for %v: %v", idle.id, err) + } + + idle.conn.ep.Close() + + f.Lock() + delete(f.conns, idle.id) + f.Unlock() + + f.logger.Trace("forwarder: cleaned up idle UDP connection %v", idle.id) + } + } + } +} + +// handleUDP is called by the UDP forwarder for new packets +func (f *Forwarder) handleUDP(r *udp.ForwarderRequest) { + if f.ctx.Err() != nil { + f.logger.Trace("forwarder: context done, dropping UDP packet") + return + } + + id := r.ID() + + f.udpForwarder.RLock() + _, exists := f.udpForwarder.conns[id] + f.udpForwarder.RUnlock() + if exists { + f.logger.Trace("forwarder: existing UDP connection for %v", id) + return + } + + dstAddr := fmt.Sprintf("%s:%d", f.determineDialAddr(id.LocalAddress), id.LocalPort) + outConn, err := (&net.Dialer{}).DialContext(f.ctx, "udp", dstAddr) + if err != nil { + f.logger.Debug("forwarder: UDP dial error for %v: %v", id, err) + // TODO: Send ICMP error message + return + } + + // Create wait queue for blocking syscalls + wq := waiter.Queue{} + ep, epErr := r.CreateEndpoint(&wq) + if epErr != nil { + f.logger.Debug("forwarder: failed to create UDP endpoint: %v", epErr) + if err := outConn.Close(); err != nil { + f.logger.Debug("forwarder: UDP outConn close error for %v: %v", id, err) + } + return + } + + inConn := gonet.NewUDPConn(f.stack, &wq, ep) + connCtx, connCancel := context.WithCancel(f.ctx) + + pConn := &udpPacketConn{ + conn: inConn, + outConn: outConn, + cancel: connCancel, + ep: ep, + } + pConn.updateLastSeen() + + f.udpForwarder.Lock() + // Double-check no connection was created while we were setting up + if _, exists := f.udpForwarder.conns[id]; exists { + f.udpForwarder.Unlock() + pConn.cancel() + if err := inConn.Close(); err != nil { + f.logger.Debug("forwarder: UDP inConn close error for %v: %v", id, err) + } + if err := outConn.Close(); err != nil { + f.logger.Debug("forwarder: UDP outConn close error for %v: %v", id, err) + } + return + } + f.udpForwarder.conns[id] = pConn + f.udpForwarder.Unlock() + + f.logger.Trace("forwarder: established UDP connection to %v", id) + go f.proxyUDP(connCtx, pConn, id, ep) +} + +func (f *Forwarder) proxyUDP(ctx context.Context, pConn *udpPacketConn, id stack.TransportEndpointID, ep tcpip.Endpoint) { + defer func() { + pConn.cancel() + if err := pConn.conn.Close(); err != nil { + f.logger.Debug("forwarder: UDP inConn close error for %v: %v", id, err) + } + if err := pConn.outConn.Close(); err != nil { + f.logger.Debug("forwarder: UDP outConn close error for %v: %v", id, err) + } + + ep.Close() + + f.udpForwarder.Lock() + delete(f.udpForwarder.conns, id) + f.udpForwarder.Unlock() + }() + + errChan := make(chan error, 2) + + go func() { + errChan <- pConn.copy(ctx, pConn.conn, pConn.outConn, &f.udpForwarder.bufPool, "outbound->inbound") + }() + + go func() { + errChan <- pConn.copy(ctx, pConn.outConn, pConn.conn, &f.udpForwarder.bufPool, "inbound->outbound") + }() + + select { + case <-ctx.Done(): + f.logger.Trace("forwarder: tearing down UDP connection %v due to context done", id) + return + case err := <-errChan: + if err != nil && !isClosedError(err) { + f.logger.Error("proxyUDP: copy error: %v", err) + } + f.logger.Trace("forwarder: tearing down UDP connection %v", id) + return + } +} + +func (c *udpPacketConn) updateLastSeen() { + c.lastSeen.Store(time.Now().UnixNano()) +} + +func (c *udpPacketConn) getIdleDuration() time.Duration { + lastSeen := time.Unix(0, c.lastSeen.Load()) + return time.Since(lastSeen) +} + +func (c *udpPacketConn) copy(ctx context.Context, dst net.Conn, src net.Conn, bufPool *sync.Pool, direction string) error { + bufp := bufPool.Get().(*[]byte) + defer bufPool.Put(bufp) + buffer := *bufp + + for { + if ctx.Err() != nil { + return ctx.Err() + } + + if err := src.SetDeadline(time.Now().Add(udpTimeout)); err != nil { + return fmt.Errorf("set read deadline: %w", err) + } + + n, err := src.Read(buffer) + if err != nil { + if isTimeout(err) { + continue + } + return fmt.Errorf("read from %s: %w", direction, err) + } + + _, err = dst.Write(buffer[:n]) + if err != nil { + return fmt.Errorf("write to %s: %w", direction, err) + } + + c.updateLastSeen() + } +} + +func isClosedError(err error) bool { + return errors.Is(err, net.ErrClosed) || errors.Is(err, context.Canceled) +} + +func isTimeout(err error) bool { + var netErr net.Error + if errors.As(err, &netErr) { + return netErr.Timeout() + } + return false +} diff --git a/client/firewall/uspfilter/localip.go b/client/firewall/uspfilter/localip.go new file mode 100644 index 000000000..7664b65d5 --- /dev/null +++ b/client/firewall/uspfilter/localip.go @@ -0,0 +1,134 @@ +package uspfilter + +import ( + "fmt" + "net" + "sync" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/firewall/uspfilter/common" +) + +type localIPManager struct { + mu sync.RWMutex + + // Use bitmap for IPv4 (32 bits * 2^16 = 256KB memory) + ipv4Bitmap [1 << 16]uint32 +} + +func newLocalIPManager() *localIPManager { + return &localIPManager{} +} + +func (m *localIPManager) setBitmapBit(ip net.IP) { + ipv4 := ip.To4() + if ipv4 == nil { + return + } + high := (uint16(ipv4[0]) << 8) | uint16(ipv4[1]) + low := (uint16(ipv4[2]) << 8) | uint16(ipv4[3]) + m.ipv4Bitmap[high] |= 1 << (low % 32) +} + +func (m *localIPManager) checkBitmapBit(ip net.IP) bool { + ipv4 := ip.To4() + if ipv4 == nil { + return false + } + high := (uint16(ipv4[0]) << 8) | uint16(ipv4[1]) + low := (uint16(ipv4[2]) << 8) | uint16(ipv4[3]) + return (m.ipv4Bitmap[high] & (1 << (low % 32))) != 0 +} + +func (m *localIPManager) processIP(ip net.IP, newIPv4Bitmap *[1 << 16]uint32, ipv4Set map[string]struct{}, ipv4Addresses *[]string) error { + if ipv4 := ip.To4(); ipv4 != nil { + high := (uint16(ipv4[0]) << 8) | uint16(ipv4[1]) + low := (uint16(ipv4[2]) << 8) | uint16(ipv4[3]) + if int(high) >= len(*newIPv4Bitmap) { + return fmt.Errorf("invalid IPv4 address: %s", ip) + } + ipStr := ip.String() + if _, exists := ipv4Set[ipStr]; !exists { + ipv4Set[ipStr] = struct{}{} + *ipv4Addresses = append(*ipv4Addresses, ipStr) + newIPv4Bitmap[high] |= 1 << (low % 32) + } + } + return nil +} + +func (m *localIPManager) processInterface(iface net.Interface, newIPv4Bitmap *[1 << 16]uint32, ipv4Set map[string]struct{}, ipv4Addresses *[]string) { + addrs, err := iface.Addrs() + if err != nil { + log.Debugf("get addresses for interface %s failed: %v", iface.Name, err) + return + } + + for _, addr := range addrs { + var ip net.IP + switch v := addr.(type) { + case *net.IPNet: + ip = v.IP + case *net.IPAddr: + ip = v.IP + default: + continue + } + + if err := m.processIP(ip, newIPv4Bitmap, ipv4Set, ipv4Addresses); err != nil { + log.Debugf("process IP failed: %v", err) + } + } +} + +func (m *localIPManager) UpdateLocalIPs(iface common.IFaceMapper) (err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("panic: %v", r) + } + }() + + var newIPv4Bitmap [1 << 16]uint32 + ipv4Set := make(map[string]struct{}) + var ipv4Addresses []string + + // 127.0.0.0/8 + high := uint16(127) << 8 + for i := uint16(0); i < 256; i++ { + newIPv4Bitmap[high|i] = 0xffffffff + } + + if iface != nil { + if err := m.processIP(iface.Address().IP, &newIPv4Bitmap, ipv4Set, &ipv4Addresses); err != nil { + return err + } + } + + interfaces, err := net.Interfaces() + if err != nil { + log.Warnf("failed to get interfaces: %v", err) + } else { + for _, intf := range interfaces { + m.processInterface(intf, &newIPv4Bitmap, ipv4Set, &ipv4Addresses) + } + } + + m.mu.Lock() + m.ipv4Bitmap = newIPv4Bitmap + m.mu.Unlock() + + log.Debugf("Local IPv4 addresses: %v", ipv4Addresses) + return nil +} + +func (m *localIPManager) IsLocalIP(ip net.IP) bool { + m.mu.RLock() + defer m.mu.RUnlock() + + if ipv4 := ip.To4(); ipv4 != nil { + return m.checkBitmapBit(ipv4) + } + + return false +} diff --git a/client/firewall/uspfilter/localip_test.go b/client/firewall/uspfilter/localip_test.go new file mode 100644 index 000000000..02f41bf4f --- /dev/null +++ b/client/firewall/uspfilter/localip_test.go @@ -0,0 +1,270 @@ +package uspfilter + +import ( + "net" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/client/iface" +) + +func TestLocalIPManager(t *testing.T) { + tests := []struct { + name string + setupAddr iface.WGAddress + testIP net.IP + expected bool + }{ + { + name: "Localhost range", + setupAddr: iface.WGAddress{ + IP: net.ParseIP("192.168.1.1"), + Network: &net.IPNet{ + IP: net.ParseIP("192.168.1.0"), + Mask: net.CIDRMask(24, 32), + }, + }, + testIP: net.ParseIP("127.0.0.2"), + expected: true, + }, + { + name: "Localhost standard address", + setupAddr: iface.WGAddress{ + IP: net.ParseIP("192.168.1.1"), + Network: &net.IPNet{ + IP: net.ParseIP("192.168.1.0"), + Mask: net.CIDRMask(24, 32), + }, + }, + testIP: net.ParseIP("127.0.0.1"), + expected: true, + }, + { + name: "Localhost range edge", + setupAddr: iface.WGAddress{ + IP: net.ParseIP("192.168.1.1"), + Network: &net.IPNet{ + IP: net.ParseIP("192.168.1.0"), + Mask: net.CIDRMask(24, 32), + }, + }, + testIP: net.ParseIP("127.255.255.255"), + expected: true, + }, + { + name: "Local IP matches", + setupAddr: iface.WGAddress{ + IP: net.ParseIP("192.168.1.1"), + Network: &net.IPNet{ + IP: net.ParseIP("192.168.1.0"), + Mask: net.CIDRMask(24, 32), + }, + }, + testIP: net.ParseIP("192.168.1.1"), + expected: true, + }, + { + name: "Local IP doesn't match", + setupAddr: iface.WGAddress{ + IP: net.ParseIP("192.168.1.1"), + Network: &net.IPNet{ + IP: net.ParseIP("192.168.1.0"), + Mask: net.CIDRMask(24, 32), + }, + }, + testIP: net.ParseIP("192.168.1.2"), + expected: false, + }, + { + name: "IPv6 address", + setupAddr: iface.WGAddress{ + IP: net.ParseIP("fe80::1"), + Network: &net.IPNet{ + IP: net.ParseIP("fe80::"), + Mask: net.CIDRMask(64, 128), + }, + }, + testIP: net.ParseIP("fe80::1"), + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + manager := newLocalIPManager() + + mock := &IFaceMock{ + AddressFunc: func() iface.WGAddress { + return tt.setupAddr + }, + } + + err := manager.UpdateLocalIPs(mock) + require.NoError(t, err) + + result := manager.IsLocalIP(tt.testIP) + require.Equal(t, tt.expected, result) + }) + } +} + +func TestLocalIPManager_AllInterfaces(t *testing.T) { + manager := newLocalIPManager() + mock := &IFaceMock{} + + // Get actual local interfaces + interfaces, err := net.Interfaces() + require.NoError(t, err) + + var tests []struct { + ip string + expected bool + } + + // Add all local interface IPs to test cases + for _, iface := range interfaces { + addrs, err := iface.Addrs() + require.NoError(t, err) + + for _, addr := range addrs { + var ip net.IP + switch v := addr.(type) { + case *net.IPNet: + ip = v.IP + case *net.IPAddr: + ip = v.IP + default: + continue + } + + if ip4 := ip.To4(); ip4 != nil { + tests = append(tests, struct { + ip string + expected bool + }{ + ip: ip4.String(), + expected: true, + }) + } + } + } + + // Add some external IPs as negative test cases + externalIPs := []string{ + "8.8.8.8", + "1.1.1.1", + "208.67.222.222", + } + for _, ip := range externalIPs { + tests = append(tests, struct { + ip string + expected bool + }{ + ip: ip, + expected: false, + }) + } + + require.NotEmpty(t, tests, "No test cases generated") + + err = manager.UpdateLocalIPs(mock) + require.NoError(t, err) + + t.Logf("Testing %d IPs", len(tests)) + for _, tt := range tests { + t.Run(tt.ip, func(t *testing.T) { + result := manager.IsLocalIP(net.ParseIP(tt.ip)) + require.Equal(t, tt.expected, result, "IP: %s", tt.ip) + }) + } +} + +// MapImplementation is a version using map[string]struct{} +type MapImplementation struct { + localIPs map[string]struct{} +} + +func BenchmarkIPChecks(b *testing.B) { + interfaces := make([]net.IP, 16) + for i := range interfaces { + interfaces[i] = net.IPv4(10, 0, byte(i>>8), byte(i)) + } + + // Setup bitmap version + bitmapManager := &localIPManager{ + ipv4Bitmap: [1 << 16]uint32{}, + } + for _, ip := range interfaces[:8] { // Add half of IPs + bitmapManager.setBitmapBit(ip) + } + + // Setup map version + mapManager := &MapImplementation{ + localIPs: make(map[string]struct{}), + } + for _, ip := range interfaces[:8] { + mapManager.localIPs[ip.String()] = struct{}{} + } + + b.Run("Bitmap_Hit", func(b *testing.B) { + ip := interfaces[4] + b.ResetTimer() + for i := 0; i < b.N; i++ { + bitmapManager.checkBitmapBit(ip) + } + }) + + b.Run("Bitmap_Miss", func(b *testing.B) { + ip := interfaces[12] + b.ResetTimer() + for i := 0; i < b.N; i++ { + bitmapManager.checkBitmapBit(ip) + } + }) + + b.Run("Map_Hit", func(b *testing.B) { + ip := interfaces[4] + b.ResetTimer() + for i := 0; i < b.N; i++ { + // nolint:gosimple + _, _ = mapManager.localIPs[ip.String()] + } + }) + + b.Run("Map_Miss", func(b *testing.B) { + ip := interfaces[12] + b.ResetTimer() + for i := 0; i < b.N; i++ { + // nolint:gosimple + _, _ = mapManager.localIPs[ip.String()] + } + }) +} + +func BenchmarkWGPosition(b *testing.B) { + wgIP := net.ParseIP("10.10.0.1") + + // Create two managers - one checks WG IP first, other checks it last + b.Run("WG_First", func(b *testing.B) { + bm := &localIPManager{ipv4Bitmap: [1 << 16]uint32{}} + bm.setBitmapBit(wgIP) + b.ResetTimer() + for i := 0; i < b.N; i++ { + bm.checkBitmapBit(wgIP) + } + }) + + b.Run("WG_Last", func(b *testing.B) { + bm := &localIPManager{ipv4Bitmap: [1 << 16]uint32{}} + // Fill with other IPs first + for i := 0; i < 15; i++ { + bm.setBitmapBit(net.IPv4(10, 0, byte(i>>8), byte(i))) + } + bm.setBitmapBit(wgIP) // Add WG IP last + b.ResetTimer() + for i := 0; i < b.N; i++ { + bm.checkBitmapBit(wgIP) + } + }) +} diff --git a/client/firewall/uspfilter/log/log.go b/client/firewall/uspfilter/log/log.go new file mode 100644 index 000000000..984b6ad08 --- /dev/null +++ b/client/firewall/uspfilter/log/log.go @@ -0,0 +1,196 @@ +// Package logger provides a high-performance, non-blocking logger for userspace networking +package log + +import ( + "context" + "fmt" + "io" + "sync" + "sync/atomic" + "time" + + log "github.com/sirupsen/logrus" +) + +const ( + maxBatchSize = 1024 * 16 // 16KB max batch size + maxMessageSize = 1024 * 2 // 2KB per message + bufferSize = 1024 * 256 // 256KB ring buffer + defaultFlushInterval = 2 * time.Second +) + +// Level represents log severity +type Level uint32 + +const ( + LevelPanic Level = iota + LevelFatal + LevelError + LevelWarn + LevelInfo + LevelDebug + LevelTrace +) + +var levelStrings = map[Level]string{ + LevelPanic: "PANC", + LevelFatal: "FATL", + LevelError: "ERRO", + LevelWarn: "WARN", + LevelInfo: "INFO", + LevelDebug: "DEBG", + LevelTrace: "TRAC", +} + +// Logger is a high-performance, non-blocking logger +type Logger struct { + output io.Writer + level atomic.Uint32 + buffer *ringBuffer + shutdown chan struct{} + closeOnce sync.Once + wg sync.WaitGroup + + // Reusable buffer pool for formatting messages + bufPool sync.Pool +} + +func NewFromLogrus(logrusLogger *log.Logger) *Logger { + l := &Logger{ + output: logrusLogger.Out, + buffer: newRingBuffer(bufferSize), + shutdown: make(chan struct{}), + bufPool: sync.Pool{ + New: func() interface{} { + // Pre-allocate buffer for message formatting + b := make([]byte, 0, maxMessageSize) + return &b + }, + }, + } + logrusLevel := logrusLogger.GetLevel() + l.level.Store(uint32(logrusLevel)) + level := levelStrings[Level(logrusLevel)] + log.Debugf("New uspfilter logger created with loglevel %v", level) + + l.wg.Add(1) + go l.worker() + + return l +} + +func (l *Logger) SetLevel(level Level) { + l.level.Store(uint32(level)) + + log.Debugf("Set uspfilter logger loglevel to %v", levelStrings[level]) +} + +func (l *Logger) formatMessage(buf *[]byte, level Level, format string, args ...interface{}) { + *buf = (*buf)[:0] + + // Timestamp + *buf = time.Now().AppendFormat(*buf, "2006-01-02T15:04:05-07:00") + *buf = append(*buf, ' ') + + // Level + *buf = append(*buf, levelStrings[level]...) + *buf = append(*buf, ' ') + + // Message + if len(args) > 0 { + *buf = append(*buf, fmt.Sprintf(format, args...)...) + } else { + *buf = append(*buf, format...) + } + + *buf = append(*buf, '\n') +} + +func (l *Logger) log(level Level, format string, args ...interface{}) { + bufp := l.bufPool.Get().(*[]byte) + l.formatMessage(bufp, level, format, args...) + + if len(*bufp) > maxMessageSize { + *bufp = (*bufp)[:maxMessageSize] + } + _, _ = l.buffer.Write(*bufp) + + l.bufPool.Put(bufp) +} + +func (l *Logger) Error(format string, args ...interface{}) { + if l.level.Load() >= uint32(LevelError) { + l.log(LevelError, format, args...) + } +} + +func (l *Logger) Warn(format string, args ...interface{}) { + if l.level.Load() >= uint32(LevelWarn) { + l.log(LevelWarn, format, args...) + } +} + +func (l *Logger) Info(format string, args ...interface{}) { + if l.level.Load() >= uint32(LevelInfo) { + l.log(LevelInfo, format, args...) + } +} + +func (l *Logger) Debug(format string, args ...interface{}) { + if l.level.Load() >= uint32(LevelDebug) { + l.log(LevelDebug, format, args...) + } +} + +func (l *Logger) Trace(format string, args ...interface{}) { + if l.level.Load() >= uint32(LevelTrace) { + l.log(LevelTrace, format, args...) + } +} + +// worker periodically flushes the buffer +func (l *Logger) worker() { + defer l.wg.Done() + + ticker := time.NewTicker(defaultFlushInterval) + defer ticker.Stop() + + buf := make([]byte, 0, maxBatchSize) + + for { + select { + case <-l.shutdown: + return + case <-ticker.C: + // Read accumulated messages + n, _ := l.buffer.Read(buf[:cap(buf)]) + if n == 0 { + continue + } + + // Write batch + _, _ = l.output.Write(buf[:n]) + } + } +} + +// Stop gracefully shuts down the logger +func (l *Logger) Stop(ctx context.Context) error { + done := make(chan struct{}) + + l.closeOnce.Do(func() { + close(l.shutdown) + }) + + go func() { + l.wg.Wait() + close(done) + }() + + select { + case <-ctx.Done(): + return ctx.Err() + case <-done: + return nil + } +} diff --git a/client/firewall/uspfilter/log/ringbuffer.go b/client/firewall/uspfilter/log/ringbuffer.go new file mode 100644 index 000000000..dbc8f1289 --- /dev/null +++ b/client/firewall/uspfilter/log/ringbuffer.go @@ -0,0 +1,85 @@ +package log + +import "sync" + +// ringBuffer is a simple ring buffer implementation +type ringBuffer struct { + buf []byte + size int + r, w int64 // Read and write positions + mu sync.Mutex +} + +func newRingBuffer(size int) *ringBuffer { + return &ringBuffer{ + buf: make([]byte, size), + size: size, + } +} + +func (r *ringBuffer) Write(p []byte) (n int, err error) { + if len(p) == 0 { + return 0, nil + } + + r.mu.Lock() + defer r.mu.Unlock() + + if len(p) > r.size { + p = p[:r.size] + } + + n = len(p) + + // Write data, handling wrap-around + pos := int(r.w % int64(r.size)) + writeLen := min(len(p), r.size-pos) + copy(r.buf[pos:], p[:writeLen]) + + // If we have more data and need to wrap around + if writeLen < len(p) { + copy(r.buf, p[writeLen:]) + } + + // Update write position + r.w += int64(n) + + return n, nil +} + +func (r *ringBuffer) Read(p []byte) (n int, err error) { + r.mu.Lock() + defer r.mu.Unlock() + + if r.w == r.r { + return 0, nil + } + + // Calculate available data accounting for wraparound + available := int(r.w - r.r) + if available < 0 { + available += r.size + } + available = min(available, r.size) + + // Limit read to buffer size + toRead := min(available, len(p)) + if toRead == 0 { + return 0, nil + } + + // Read data, handling wrap-around + pos := int(r.r % int64(r.size)) + readLen := min(toRead, r.size-pos) + n = copy(p, r.buf[pos:pos+readLen]) + + // If we need more data and need to wrap around + if readLen < toRead { + n += copy(p[readLen:toRead], r.buf[:toRead-readLen]) + } + + // Update read position + r.r += int64(n) + + return n, nil +} diff --git a/client/firewall/uspfilter/rule.go b/client/firewall/uspfilter/rule.go index 5c1daccaf..6a4415f73 100644 --- a/client/firewall/uspfilter/rule.go +++ b/client/firewall/uspfilter/rule.go @@ -2,22 +2,22 @@ package uspfilter import ( "net" + "net/netip" "github.com/google/gopacket" firewall "github.com/netbirdio/netbird/client/firewall/manager" ) -// Rule to handle management of rules -type Rule struct { +// PeerRule to handle management of rules +type PeerRule struct { id string ip net.IP ipLayer gopacket.LayerType matchByIP bool protoLayer gopacket.LayerType - direction firewall.RuleDirection - sPort uint16 - dPort uint16 + sPort *firewall.Port + dPort *firewall.Port drop bool comment string @@ -25,6 +25,21 @@ type Rule struct { } // GetRuleID returns the rule id -func (r *Rule) GetRuleID() string { +func (r *PeerRule) GetRuleID() string { + return r.id +} + +type RouteRule struct { + id string + sources []netip.Prefix + destination netip.Prefix + proto firewall.Protocol + srcPort *firewall.Port + dstPort *firewall.Port + action firewall.Action +} + +// GetRuleID returns the rule id +func (r *RouteRule) GetRuleID() string { return r.id } diff --git a/client/firewall/uspfilter/tracer.go b/client/firewall/uspfilter/tracer.go new file mode 100644 index 000000000..a4c653b3b --- /dev/null +++ b/client/firewall/uspfilter/tracer.go @@ -0,0 +1,390 @@ +package uspfilter + +import ( + "fmt" + "net" + "time" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + + fw "github.com/netbirdio/netbird/client/firewall/manager" + "github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack" +) + +type PacketStage int + +const ( + StageReceived PacketStage = iota + StageConntrack + StagePeerACL + StageRouting + StageRouteACL + StageForwarding + StageCompleted +) + +const msgProcessingCompleted = "Processing completed" + +func (s PacketStage) String() string { + return map[PacketStage]string{ + StageReceived: "Received", + StageConntrack: "Connection Tracking", + StagePeerACL: "Peer ACL", + StageRouting: "Routing", + StageRouteACL: "Route ACL", + StageForwarding: "Forwarding", + StageCompleted: "Completed", + }[s] +} + +type ForwarderAction struct { + Action string + RemoteAddr string + Error error +} + +type TraceResult struct { + Timestamp time.Time + Stage PacketStage + Message string + Allowed bool + ForwarderAction *ForwarderAction +} + +type PacketTrace struct { + SourceIP net.IP + DestinationIP net.IP + Protocol string + SourcePort uint16 + DestinationPort uint16 + Direction fw.RuleDirection + Results []TraceResult +} + +type TCPState struct { + SYN bool + ACK bool + FIN bool + RST bool + PSH bool + URG bool +} + +type PacketBuilder struct { + SrcIP net.IP + DstIP net.IP + Protocol fw.Protocol + SrcPort uint16 + DstPort uint16 + ICMPType uint8 + ICMPCode uint8 + Direction fw.RuleDirection + PayloadSize int + TCPState *TCPState +} + +func (t *PacketTrace) AddResult(stage PacketStage, message string, allowed bool) { + t.Results = append(t.Results, TraceResult{ + Timestamp: time.Now(), + Stage: stage, + Message: message, + Allowed: allowed, + }) +} + +func (t *PacketTrace) AddResultWithForwarder(stage PacketStage, message string, allowed bool, action *ForwarderAction) { + t.Results = append(t.Results, TraceResult{ + Timestamp: time.Now(), + Stage: stage, + Message: message, + Allowed: allowed, + ForwarderAction: action, + }) +} + +func (p *PacketBuilder) Build() ([]byte, error) { + ip := p.buildIPLayer() + pktLayers := []gopacket.SerializableLayer{ip} + + transportLayer, err := p.buildTransportLayer(ip) + if err != nil { + return nil, err + } + pktLayers = append(pktLayers, transportLayer...) + + if p.PayloadSize > 0 { + payload := make([]byte, p.PayloadSize) + pktLayers = append(pktLayers, gopacket.Payload(payload)) + } + + return serializePacket(pktLayers) +} + +func (p *PacketBuilder) buildIPLayer() *layers.IPv4 { + return &layers.IPv4{ + Version: 4, + TTL: 64, + Protocol: layers.IPProtocol(getIPProtocolNumber(p.Protocol)), + SrcIP: p.SrcIP, + DstIP: p.DstIP, + } +} + +func (p *PacketBuilder) buildTransportLayer(ip *layers.IPv4) ([]gopacket.SerializableLayer, error) { + switch p.Protocol { + case "tcp": + return p.buildTCPLayer(ip) + case "udp": + return p.buildUDPLayer(ip) + case "icmp": + return p.buildICMPLayer() + default: + return nil, fmt.Errorf("unsupported protocol: %s", p.Protocol) + } +} + +func (p *PacketBuilder) buildTCPLayer(ip *layers.IPv4) ([]gopacket.SerializableLayer, error) { + tcp := &layers.TCP{ + SrcPort: layers.TCPPort(p.SrcPort), + DstPort: layers.TCPPort(p.DstPort), + Window: 65535, + SYN: p.TCPState != nil && p.TCPState.SYN, + ACK: p.TCPState != nil && p.TCPState.ACK, + FIN: p.TCPState != nil && p.TCPState.FIN, + RST: p.TCPState != nil && p.TCPState.RST, + PSH: p.TCPState != nil && p.TCPState.PSH, + URG: p.TCPState != nil && p.TCPState.URG, + } + if err := tcp.SetNetworkLayerForChecksum(ip); err != nil { + return nil, fmt.Errorf("set network layer for TCP checksum: %w", err) + } + return []gopacket.SerializableLayer{tcp}, nil +} + +func (p *PacketBuilder) buildUDPLayer(ip *layers.IPv4) ([]gopacket.SerializableLayer, error) { + udp := &layers.UDP{ + SrcPort: layers.UDPPort(p.SrcPort), + DstPort: layers.UDPPort(p.DstPort), + } + if err := udp.SetNetworkLayerForChecksum(ip); err != nil { + return nil, fmt.Errorf("set network layer for UDP checksum: %w", err) + } + return []gopacket.SerializableLayer{udp}, nil +} + +func (p *PacketBuilder) buildICMPLayer() ([]gopacket.SerializableLayer, error) { + icmp := &layers.ICMPv4{ + TypeCode: layers.CreateICMPv4TypeCode(p.ICMPType, p.ICMPCode), + } + if p.ICMPType == layers.ICMPv4TypeEchoRequest || p.ICMPType == layers.ICMPv4TypeEchoReply { + icmp.Id = uint16(1) + icmp.Seq = uint16(1) + } + return []gopacket.SerializableLayer{icmp}, nil +} + +func serializePacket(layers []gopacket.SerializableLayer) ([]byte, error) { + buf := gopacket.NewSerializeBuffer() + opts := gopacket.SerializeOptions{ + ComputeChecksums: true, + FixLengths: true, + } + if err := gopacket.SerializeLayers(buf, opts, layers...); err != nil { + return nil, fmt.Errorf("serialize packet: %w", err) + } + return buf.Bytes(), nil +} + +func getIPProtocolNumber(protocol fw.Protocol) int { + switch protocol { + case fw.ProtocolTCP: + return int(layers.IPProtocolTCP) + case fw.ProtocolUDP: + return int(layers.IPProtocolUDP) + case fw.ProtocolICMP: + return int(layers.IPProtocolICMPv4) + default: + return 0 + } +} + +func (m *Manager) TracePacketFromBuilder(builder *PacketBuilder) (*PacketTrace, error) { + packetData, err := builder.Build() + if err != nil { + return nil, fmt.Errorf("build packet: %w", err) + } + + return m.TracePacket(packetData, builder.Direction), nil +} + +func (m *Manager) TracePacket(packetData []byte, direction fw.RuleDirection) *PacketTrace { + + d := m.decoders.Get().(*decoder) + defer m.decoders.Put(d) + + trace := &PacketTrace{Direction: direction} + + // Initial packet decoding + if err := d.parser.DecodeLayers(packetData, &d.decoded); err != nil { + trace.AddResult(StageReceived, fmt.Sprintf("Failed to decode packet: %v", err), false) + return trace + } + + // Extract base packet info + srcIP, dstIP := m.extractIPs(d) + trace.SourceIP = srcIP + trace.DestinationIP = dstIP + + // Determine protocol and ports + switch d.decoded[1] { + case layers.LayerTypeTCP: + trace.Protocol = "TCP" + trace.SourcePort = uint16(d.tcp.SrcPort) + trace.DestinationPort = uint16(d.tcp.DstPort) + case layers.LayerTypeUDP: + trace.Protocol = "UDP" + trace.SourcePort = uint16(d.udp.SrcPort) + trace.DestinationPort = uint16(d.udp.DstPort) + case layers.LayerTypeICMPv4: + trace.Protocol = "ICMP" + } + + trace.AddResult(StageReceived, fmt.Sprintf("Received %s packet: %s:%d -> %s:%d", + trace.Protocol, srcIP, trace.SourcePort, dstIP, trace.DestinationPort), true) + + if direction == fw.RuleDirectionOUT { + return m.traceOutbound(packetData, trace) + } + + return m.traceInbound(packetData, trace, d, srcIP, dstIP) +} + +func (m *Manager) traceInbound(packetData []byte, trace *PacketTrace, d *decoder, srcIP net.IP, dstIP net.IP) *PacketTrace { + if m.stateful && m.handleConntrackState(trace, d, srcIP, dstIP) { + return trace + } + + if m.handleLocalDelivery(trace, packetData, d, srcIP, dstIP) { + return trace + } + + if !m.handleRouting(trace) { + return trace + } + + if m.nativeRouter { + return m.handleNativeRouter(trace) + } + + return m.handleRouteACLs(trace, d, srcIP, dstIP) +} + +func (m *Manager) handleConntrackState(trace *PacketTrace, d *decoder, srcIP, dstIP net.IP) bool { + allowed := m.isValidTrackedConnection(d, srcIP, dstIP) + msg := "No existing connection found" + if allowed { + msg = m.buildConntrackStateMessage(d) + trace.AddResult(StageConntrack, msg, true) + trace.AddResult(StageCompleted, "Packet allowed by connection tracking", true) + return true + } + trace.AddResult(StageConntrack, msg, false) + return false +} + +func (m *Manager) buildConntrackStateMessage(d *decoder) string { + msg := "Matched existing connection state" + switch d.decoded[1] { + case layers.LayerTypeTCP: + flags := getTCPFlags(&d.tcp) + msg += fmt.Sprintf(" (TCP Flags: SYN=%v ACK=%v RST=%v FIN=%v)", + flags&conntrack.TCPSyn != 0, + flags&conntrack.TCPAck != 0, + flags&conntrack.TCPRst != 0, + flags&conntrack.TCPFin != 0) + case layers.LayerTypeICMPv4: + msg += fmt.Sprintf(" (ICMP ID=%d, Seq=%d)", d.icmp4.Id, d.icmp4.Seq) + } + return msg +} + +func (m *Manager) handleLocalDelivery(trace *PacketTrace, packetData []byte, d *decoder, srcIP, dstIP net.IP) bool { + if !m.localForwarding { + trace.AddResult(StageRouting, "Local forwarding disabled", false) + trace.AddResult(StageCompleted, "Packet dropped - local forwarding disabled", false) + return true + } + + trace.AddResult(StageRouting, "Packet destined for local delivery", true) + blocked := m.peerACLsBlock(srcIP, packetData, m.incomingRules, d) + + msg := "Allowed by peer ACL rules" + if blocked { + msg = "Blocked by peer ACL rules" + } + trace.AddResult(StagePeerACL, msg, !blocked) + + if m.netstack { + m.addForwardingResult(trace, "proxy-local", "127.0.0.1", !blocked) + } + + trace.AddResult(StageCompleted, msgProcessingCompleted, !blocked) + return true +} + +func (m *Manager) handleRouting(trace *PacketTrace) bool { + if !m.routingEnabled { + trace.AddResult(StageRouting, "Routing disabled", false) + trace.AddResult(StageCompleted, "Packet dropped - routing disabled", false) + return false + } + trace.AddResult(StageRouting, "Routing enabled, checking ACLs", true) + return true +} + +func (m *Manager) handleNativeRouter(trace *PacketTrace) *PacketTrace { + trace.AddResult(StageRouteACL, "Using native router, skipping ACL checks", true) + trace.AddResult(StageForwarding, "Forwarding via native router", true) + trace.AddResult(StageCompleted, msgProcessingCompleted, true) + return trace +} + +func (m *Manager) handleRouteACLs(trace *PacketTrace, d *decoder, srcIP, dstIP net.IP) *PacketTrace { + proto := getProtocolFromPacket(d) + srcPort, dstPort := getPortsFromPacket(d) + allowed := m.routeACLsPass(srcIP, dstIP, proto, srcPort, dstPort) + + msg := "Allowed by route ACLs" + if !allowed { + msg = "Blocked by route ACLs" + } + trace.AddResult(StageRouteACL, msg, allowed) + + if allowed && m.forwarder != nil { + m.addForwardingResult(trace, "proxy-remote", fmt.Sprintf("%s:%d", dstIP, dstPort), true) + } + + trace.AddResult(StageCompleted, msgProcessingCompleted, allowed) + return trace +} + +func (m *Manager) addForwardingResult(trace *PacketTrace, action, remoteAddr string, allowed bool) { + fwdAction := &ForwarderAction{ + Action: action, + RemoteAddr: remoteAddr, + } + trace.AddResultWithForwarder(StageForwarding, + fmt.Sprintf("Forwarding to %s", fwdAction.Action), allowed, fwdAction) +} + +func (m *Manager) traceOutbound(packetData []byte, trace *PacketTrace) *PacketTrace { + // will create or update the connection state + dropped := m.processOutgoingHooks(packetData) + if dropped { + trace.AddResult(StageCompleted, "Packet dropped by outgoing hook", false) + } else { + trace.AddResult(StageCompleted, "Packet allowed (outgoing)", true) + } + return trace +} diff --git a/client/firewall/uspfilter/uspfilter.go b/client/firewall/uspfilter/uspfilter.go index ebe04caee..50f48a5c4 100644 --- a/client/firewall/uspfilter/uspfilter.go +++ b/client/firewall/uspfilter/uspfilter.go @@ -1,11 +1,14 @@ package uspfilter import ( + "errors" "fmt" "net" "net/netip" "os" + "slices" "strconv" + "strings" "sync" "github.com/google/gopacket" @@ -14,44 +17,83 @@ import ( log "github.com/sirupsen/logrus" firewall "github.com/netbirdio/netbird/client/firewall/manager" + "github.com/netbirdio/netbird/client/firewall/uspfilter/common" "github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack" - "github.com/netbirdio/netbird/client/iface" - "github.com/netbirdio/netbird/client/iface/device" + "github.com/netbirdio/netbird/client/firewall/uspfilter/forwarder" + nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log" + "github.com/netbirdio/netbird/client/iface/netstack" "github.com/netbirdio/netbird/client/internal/statemanager" ) const layerTypeAll = 0 -const EnvDisableConntrack = "NB_DISABLE_CONNTRACK" +const ( + // EnvDisableConntrack disables the stateful filter, replies to outbound traffic won't be allowed. + EnvDisableConntrack = "NB_DISABLE_CONNTRACK" -var ( - errRouteNotSupported = fmt.Errorf("route not supported with userspace firewall") + // EnvDisableUserspaceRouting disables userspace routing, to-be-routed packets will be dropped. + EnvDisableUserspaceRouting = "NB_DISABLE_USERSPACE_ROUTING" + + // EnvForceUserspaceRouter forces userspace routing even if native routing is available. + EnvForceUserspaceRouter = "NB_FORCE_USERSPACE_ROUTER" + + // EnvEnableNetstackLocalForwarding enables forwarding of local traffic to the native stack when running netstack + // Leaving this on by default introduces a security risk as sockets on listening on localhost only will be accessible + EnvEnableNetstackLocalForwarding = "NB_ENABLE_NETSTACK_LOCAL_FORWARDING" ) -// IFaceMapper defines subset methods of interface required for manager -type IFaceMapper interface { - SetFilter(device.PacketFilter) error - Address() iface.WGAddress -} - // RuleSet is a set of rules grouped by a string key -type RuleSet map[string]Rule +type RuleSet map[string]PeerRule + +type RouteRules []RouteRule + +func (r RouteRules) Sort() { + slices.SortStableFunc(r, func(a, b RouteRule) int { + // Deny rules come first + if a.action == firewall.ActionDrop && b.action != firewall.ActionDrop { + return -1 + } + if a.action != firewall.ActionDrop && b.action == firewall.ActionDrop { + return 1 + } + return strings.Compare(a.id, b.id) + }) +} // Manager userspace firewall manager type Manager struct { - outgoingRules map[string]RuleSet + // outgoingRules is used for hooks only + outgoingRules map[string]RuleSet + // incomingRules is used for filtering and hooks incomingRules map[string]RuleSet + routeRules RouteRules wgNetwork *net.IPNet decoders sync.Pool - wgIface IFaceMapper + wgIface common.IFaceMapper nativeFirewall firewall.Manager mutex sync.RWMutex - stateful bool + // indicates whether server routes are disabled + disableServerRoutes bool + // indicates whether we forward packets not destined for ourselves + routingEnabled bool + // indicates whether we leave forwarding and filtering to the native firewall + nativeRouter bool + // indicates whether we track outbound connections + stateful bool + // indicates whether wireguards runs in netstack mode + netstack bool + // indicates whether we forward local traffic to the native stack + localForwarding bool + + localipmanager *localIPManager + udpTracker *conntrack.UDPTracker icmpTracker *conntrack.ICMPTracker tcpTracker *conntrack.TCPTracker + forwarder *forwarder.Forwarder + logger *nblog.Logger } // decoder for packages @@ -68,22 +110,44 @@ type decoder struct { } // Create userspace firewall manager constructor -func Create(iface IFaceMapper) (*Manager, error) { - return create(iface) +func Create(iface common.IFaceMapper, disableServerRoutes bool) (*Manager, error) { + return create(iface, nil, disableServerRoutes) } -func CreateWithNativeFirewall(iface IFaceMapper, nativeFirewall firewall.Manager) (*Manager, error) { - mgr, err := create(iface) +func CreateWithNativeFirewall(iface common.IFaceMapper, nativeFirewall firewall.Manager, disableServerRoutes bool) (*Manager, error) { + if nativeFirewall == nil { + return nil, errors.New("native firewall is nil") + } + + mgr, err := create(iface, nativeFirewall, disableServerRoutes) if err != nil { return nil, err } - mgr.nativeFirewall = nativeFirewall return mgr, nil } -func create(iface IFaceMapper) (*Manager, error) { - disableConntrack, _ := strconv.ParseBool(os.Getenv(EnvDisableConntrack)) +func parseCreateEnv() (bool, bool) { + var disableConntrack, enableLocalForwarding bool + var err error + if val := os.Getenv(EnvDisableConntrack); val != "" { + disableConntrack, err = strconv.ParseBool(val) + if err != nil { + log.Warnf("failed to parse %s: %v", EnvDisableConntrack, err) + } + } + if val := os.Getenv(EnvEnableNetstackLocalForwarding); val != "" { + enableLocalForwarding, err = strconv.ParseBool(val) + if err != nil { + log.Warnf("failed to parse %s: %v", EnvEnableNetstackLocalForwarding, err) + } + } + + return disableConntrack, enableLocalForwarding +} + +func create(iface common.IFaceMapper, nativeFirewall firewall.Manager, disableServerRoutes bool) (*Manager, error) { + disableConntrack, enableLocalForwarding := parseCreateEnv() m := &Manager{ decoders: sync.Pool{ @@ -99,52 +163,182 @@ func create(iface IFaceMapper) (*Manager, error) { return d }, }, - outgoingRules: make(map[string]RuleSet), - incomingRules: make(map[string]RuleSet), - wgIface: iface, - stateful: !disableConntrack, + nativeFirewall: nativeFirewall, + outgoingRules: make(map[string]RuleSet), + incomingRules: make(map[string]RuleSet), + wgIface: iface, + localipmanager: newLocalIPManager(), + disableServerRoutes: disableServerRoutes, + routingEnabled: false, + stateful: !disableConntrack, + logger: nblog.NewFromLogrus(log.StandardLogger()), + netstack: netstack.IsEnabled(), + localForwarding: enableLocalForwarding, + } + + if err := m.localipmanager.UpdateLocalIPs(iface); err != nil { + return nil, fmt.Errorf("update local IPs: %w", err) } - // Only initialize trackers if stateful mode is enabled if disableConntrack { log.Info("conntrack is disabled") } else { - m.udpTracker = conntrack.NewUDPTracker(conntrack.DefaultUDPTimeout) - m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout) - m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout) + m.udpTracker = conntrack.NewUDPTracker(conntrack.DefaultUDPTimeout, m.logger) + m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout, m.logger) + m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout, m.logger) + } + + // netstack needs the forwarder for local traffic + if m.netstack && m.localForwarding { + if err := m.initForwarder(); err != nil { + log.Errorf("failed to initialize forwarder: %v", err) + } + } + + if err := m.blockInvalidRouted(iface); err != nil { + log.Errorf("failed to block invalid routed traffic: %v", err) } if err := iface.SetFilter(m); err != nil { - return nil, err + return nil, fmt.Errorf("set filter: %w", err) } return m, nil } +func (m *Manager) blockInvalidRouted(iface common.IFaceMapper) error { + if m.forwarder == nil { + return nil + } + wgPrefix, err := netip.ParsePrefix(iface.Address().Network.String()) + if err != nil { + return fmt.Errorf("parse wireguard network: %w", err) + } + log.Debugf("blocking invalid routed traffic for %s", wgPrefix) + + if _, err := m.AddRouteFiltering( + []netip.Prefix{netip.PrefixFrom(netip.IPv4Unspecified(), 0)}, + wgPrefix, + firewall.ProtocolALL, + nil, + nil, + firewall.ActionDrop, + ); err != nil { + return fmt.Errorf("block wg nte : %w", err) + } + + // TODO: Block networks that we're a client of + + return nil +} + +func (m *Manager) determineRouting() error { + var disableUspRouting, forceUserspaceRouter bool + var err error + if val := os.Getenv(EnvDisableUserspaceRouting); val != "" { + disableUspRouting, err = strconv.ParseBool(val) + if err != nil { + log.Warnf("failed to parse %s: %v", EnvDisableUserspaceRouting, err) + } + } + if val := os.Getenv(EnvForceUserspaceRouter); val != "" { + forceUserspaceRouter, err = strconv.ParseBool(val) + if err != nil { + log.Warnf("failed to parse %s: %v", EnvForceUserspaceRouter, err) + } + } + + switch { + case disableUspRouting: + m.routingEnabled = false + m.nativeRouter = false + log.Info("userspace routing is disabled") + + case m.disableServerRoutes: + // if server routes are disabled we will let packets pass to the native stack + m.routingEnabled = true + m.nativeRouter = true + + log.Info("server routes are disabled") + + case forceUserspaceRouter: + m.routingEnabled = true + m.nativeRouter = false + + log.Info("userspace routing is forced") + + case !m.netstack && m.nativeFirewall != nil && m.nativeFirewall.IsServerRouteSupported(): + // if the OS supports routing natively, then we don't need to filter/route ourselves + // netstack mode won't support native routing as there is no interface + + m.routingEnabled = true + m.nativeRouter = true + + log.Info("native routing is enabled") + + default: + m.routingEnabled = true + m.nativeRouter = false + + log.Info("userspace routing enabled by default") + } + + if m.routingEnabled && !m.nativeRouter { + return m.initForwarder() + } + + return nil +} + +// initForwarder initializes the forwarder, it disables routing on errors +func (m *Manager) initForwarder() error { + if m.forwarder != nil { + return nil + } + + // Only supported in userspace mode as we need to inject packets back into wireguard directly + intf := m.wgIface.GetWGDevice() + if intf == nil { + m.routingEnabled = false + return errors.New("forwarding not supported") + } + + forwarder, err := forwarder.New(m.wgIface, m.logger, m.netstack) + if err != nil { + m.routingEnabled = false + return fmt.Errorf("create forwarder: %w", err) + } + + m.forwarder = forwarder + + log.Debug("forwarder initialized") + + return nil +} + func (m *Manager) Init(*statemanager.Manager) error { return nil } func (m *Manager) IsServerRouteSupported() bool { - if m.nativeFirewall == nil { - return false - } else { - return true - } + return true } func (m *Manager) AddNatRule(pair firewall.RouterPair) error { - if m.nativeFirewall == nil { - return errRouteNotSupported + if m.nativeRouter && m.nativeFirewall != nil { + return m.nativeFirewall.AddNatRule(pair) } - return m.nativeFirewall.AddNatRule(pair) + + // userspace routed packets are always SNATed to the inbound direction + // TODO: implement outbound SNAT + return nil } // RemoveNatRule removes a routing firewall rule func (m *Manager) RemoveNatRule(pair firewall.RouterPair) error { - if m.nativeFirewall == nil { - return errRouteNotSupported + if m.nativeRouter && m.nativeFirewall != nil { + return m.nativeFirewall.RemoveNatRule(pair) } - return m.nativeFirewall.RemoveNatRule(pair) + return nil } // AddPeerFiltering rule to the firewall @@ -156,17 +350,15 @@ func (m *Manager) AddPeerFiltering( proto firewall.Protocol, sPort *firewall.Port, dPort *firewall.Port, - direction firewall.RuleDirection, action firewall.Action, - ipsetName string, + _ string, comment string, ) ([]firewall.Rule, error) { - r := Rule{ + r := PeerRule{ id: uuid.New().String(), ip: ip, ipLayer: layers.LayerTypeIPv6, matchByIP: true, - direction: direction, drop: action == firewall.ActionDrop, comment: comment, } @@ -179,13 +371,8 @@ func (m *Manager) AddPeerFiltering( r.matchByIP = false } - if sPort != nil && len(sPort.Values) == 1 { - r.sPort = uint16(sPort.Values[0]) - } - - if dPort != nil && len(dPort.Values) == 1 { - r.dPort = uint16(dPort.Values[0]) - } + r.sPort = sPort + r.dPort = dPort switch proto { case firewall.ProtocolTCP: @@ -202,33 +389,64 @@ func (m *Manager) AddPeerFiltering( } m.mutex.Lock() - if direction == firewall.RuleDirectionIN { - if _, ok := m.incomingRules[r.ip.String()]; !ok { - m.incomingRules[r.ip.String()] = make(RuleSet) - } - m.incomingRules[r.ip.String()][r.id] = r - } else { - if _, ok := m.outgoingRules[r.ip.String()]; !ok { - m.outgoingRules[r.ip.String()] = make(RuleSet) - } - m.outgoingRules[r.ip.String()][r.id] = r + if _, ok := m.incomingRules[r.ip.String()]; !ok { + m.incomingRules[r.ip.String()] = make(RuleSet) } + m.incomingRules[r.ip.String()][r.id] = r m.mutex.Unlock() return []firewall.Rule{&r}, nil } -func (m *Manager) AddRouteFiltering(sources []netip.Prefix, destination netip.Prefix, proto firewall.Protocol, sPort *firewall.Port, dPort *firewall.Port, action firewall.Action) (firewall.Rule, error) { - if m.nativeFirewall == nil { - return nil, errRouteNotSupported +func (m *Manager) AddRouteFiltering( + sources []netip.Prefix, + destination netip.Prefix, + proto firewall.Protocol, + sPort *firewall.Port, + dPort *firewall.Port, + action firewall.Action, +) (firewall.Rule, error) { + if m.nativeRouter && m.nativeFirewall != nil { + return m.nativeFirewall.AddRouteFiltering(sources, destination, proto, sPort, dPort, action) } - return m.nativeFirewall.AddRouteFiltering(sources, destination, proto, sPort, dPort, action) + + m.mutex.Lock() + defer m.mutex.Unlock() + + ruleID := uuid.New().String() + rule := RouteRule{ + id: ruleID, + sources: sources, + destination: destination, + proto: proto, + srcPort: sPort, + dstPort: dPort, + action: action, + } + + m.routeRules = append(m.routeRules, rule) + m.routeRules.Sort() + + return &rule, nil } func (m *Manager) DeleteRouteRule(rule firewall.Rule) error { - if m.nativeFirewall == nil { - return errRouteNotSupported + if m.nativeRouter && m.nativeFirewall != nil { + return m.nativeFirewall.DeleteRouteRule(rule) } - return m.nativeFirewall.DeleteRouteRule(rule) + + m.mutex.Lock() + defer m.mutex.Unlock() + + ruleID := rule.GetRuleID() + idx := slices.IndexFunc(m.routeRules, func(r RouteRule) bool { + return r.id == ruleID + }) + if idx < 0 { + return fmt.Errorf("route rule not found: %s", ruleID) + } + + m.routeRules = slices.Delete(m.routeRules, idx, idx+1) + return nil } // DeletePeerRule from the firewall by rule definition @@ -236,24 +454,15 @@ func (m *Manager) DeletePeerRule(rule firewall.Rule) error { m.mutex.Lock() defer m.mutex.Unlock() - r, ok := rule.(*Rule) + r, ok := rule.(*PeerRule) if !ok { return fmt.Errorf("delete rule: invalid rule type: %T", rule) } - if r.direction == firewall.RuleDirectionIN { - _, ok := m.incomingRules[r.ip.String()][r.id] - if !ok { - return fmt.Errorf("delete rule: no rule with such id: %v", r.id) - } - delete(m.incomingRules[r.ip.String()], r.id) - } else { - _, ok := m.outgoingRules[r.ip.String()][r.id] - if !ok { - return fmt.Errorf("delete rule: no rule with such id: %v", r.id) - } - delete(m.outgoingRules[r.ip.String()], r.id) + if _, ok := m.incomingRules[r.ip.String()][r.id]; !ok { + return fmt.Errorf("delete rule: no rule with such id: %v", r.id) } + delete(m.incomingRules[r.ip.String()], r.id) return nil } @@ -276,10 +485,14 @@ func (m *Manager) DropOutgoing(packetData []byte) bool { // DropIncoming filter incoming packets func (m *Manager) DropIncoming(packetData []byte) bool { - return m.dropFilter(packetData, m.incomingRules) + return m.dropFilter(packetData) +} + +// UpdateLocalIPs updates the list of local IPs +func (m *Manager) UpdateLocalIPs() error { + return m.localipmanager.UpdateLocalIPs(m.wgIface) } -// processOutgoingHooks processes UDP hooks for outgoing packets and tracks TCP/UDP/ICMP func (m *Manager) processOutgoingHooks(packetData []byte) bool { m.mutex.RLock() defer m.mutex.RUnlock() @@ -300,18 +513,11 @@ func (m *Manager) processOutgoingHooks(packetData []byte) bool { return false } - // Always process UDP hooks - if d.decoded[1] == layers.LayerTypeUDP { - // Track UDP state only if enabled - if m.stateful { - m.trackUDPOutbound(d, srcIP, dstIP) - } - return m.checkUDPHooks(d, dstIP, packetData) - } - - // Track other protocols only if stateful mode is enabled + // Track all protocols if stateful mode is enabled if m.stateful { switch d.decoded[1] { + case layers.LayerTypeUDP: + m.trackUDPOutbound(d, srcIP, dstIP) case layers.LayerTypeTCP: m.trackTCPOutbound(d, srcIP, dstIP) case layers.LayerTypeICMPv4: @@ -319,6 +525,11 @@ func (m *Manager) processOutgoingHooks(packetData []byte) bool { } } + // Process UDP hooks even if stateful mode is disabled + if d.decoded[1] == layers.LayerTypeUDP { + return m.checkUDPHooks(d, dstIP, packetData) + } + return false } @@ -380,7 +591,7 @@ func (m *Manager) checkUDPHooks(d *decoder, dstIP net.IP, packetData []byte) boo for _, ipKey := range []string{dstIP.String(), "0.0.0.0", "::"} { if rules, exists := m.outgoingRules[ipKey]; exists { for _, rule := range rules { - if rule.udpHook != nil && (rule.dPort == 0 || rule.dPort == uint16(d.udp.DstPort)) { + if rule.udpHook != nil && portsMatch(rule.dPort, uint16(d.udp.DstPort)) { return rule.udpHook(packetData) } } @@ -400,10 +611,9 @@ func (m *Manager) trackICMPOutbound(d *decoder, srcIP, dstIP net.IP) { } } -// dropFilter implements filtering logic for incoming packets -func (m *Manager) dropFilter(packetData []byte, rules map[string]RuleSet) bool { - // TODO: Disable router if --disable-server-router is set - +// dropFilter implements filtering logic for incoming packets. +// If it returns true, the packet should be dropped. +func (m *Manager) dropFilter(packetData []byte) bool { m.mutex.RLock() defer m.mutex.RUnlock() @@ -416,39 +626,129 @@ func (m *Manager) dropFilter(packetData []byte, rules map[string]RuleSet) bool { srcIP, dstIP := m.extractIPs(d) if srcIP == nil { - log.Errorf("unknown layer: %v", d.decoded[0]) + m.logger.Error("Unknown network layer: %v", d.decoded[0]) return true } - if !m.isWireguardTraffic(srcIP, dstIP) { - return false - } - - // Check connection state only if enabled + // For all inbound traffic, first check if it matches a tracked connection. + // This must happen before any other filtering because the packets are statefully tracked. if m.stateful && m.isValidTrackedConnection(d, srcIP, dstIP) { return false } - return m.applyRules(srcIP, packetData, rules, d) + if m.localipmanager.IsLocalIP(dstIP) { + return m.handleLocalTraffic(d, srcIP, dstIP, packetData) + } + + return m.handleRoutedTraffic(d, srcIP, dstIP, packetData) +} + +// handleLocalTraffic handles local traffic. +// If it returns true, the packet should be dropped. +func (m *Manager) handleLocalTraffic(d *decoder, srcIP, dstIP net.IP, packetData []byte) bool { + if m.peerACLsBlock(srcIP, packetData, m.incomingRules, d) { + m.logger.Trace("Dropping local packet (ACL denied): src=%s dst=%s", + srcIP, dstIP) + return true + } + + // if running in netstack mode we need to pass this to the forwarder + if m.netstack { + return m.handleNetstackLocalTraffic(packetData) + } + + return false +} + +func (m *Manager) handleNetstackLocalTraffic(packetData []byte) bool { + if !m.localForwarding { + // pass to virtual tcp/ip stack to be picked up by listeners + return false + } + + if m.forwarder == nil { + m.logger.Trace("Dropping local packet (forwarder not initialized)") + return true + } + + if err := m.forwarder.InjectIncomingPacket(packetData); err != nil { + m.logger.Error("Failed to inject local packet: %v", err) + } + + // don't process this packet further + return true +} + +// handleRoutedTraffic handles routed traffic. +// If it returns true, the packet should be dropped. +func (m *Manager) handleRoutedTraffic(d *decoder, srcIP, dstIP net.IP, packetData []byte) bool { + // Drop if routing is disabled + if !m.routingEnabled { + m.logger.Trace("Dropping routed packet (routing disabled): src=%s dst=%s", + srcIP, dstIP) + return true + } + + // Pass to native stack if native router is enabled or forced + if m.nativeRouter { + return false + } + + proto := getProtocolFromPacket(d) + srcPort, dstPort := getPortsFromPacket(d) + + if !m.routeACLsPass(srcIP, dstIP, proto, srcPort, dstPort) { + m.logger.Trace("Dropping routed packet (ACL denied): src=%s:%d dst=%s:%d proto=%v", + srcIP, srcPort, dstIP, dstPort, proto) + return true + } + + // Let forwarder handle the packet if it passed route ACLs + if err := m.forwarder.InjectIncomingPacket(packetData); err != nil { + m.logger.Error("Failed to inject incoming packet: %v", err) + } + + // Forwarded packets shouldn't reach the native stack, hence they won't be visible in a packet capture + return true +} + +func getProtocolFromPacket(d *decoder) firewall.Protocol { + switch d.decoded[1] { + case layers.LayerTypeTCP: + return firewall.ProtocolTCP + case layers.LayerTypeUDP: + return firewall.ProtocolUDP + case layers.LayerTypeICMPv4, layers.LayerTypeICMPv6: + return firewall.ProtocolICMP + default: + return firewall.ProtocolALL + } +} + +func getPortsFromPacket(d *decoder) (srcPort, dstPort uint16) { + switch d.decoded[1] { + case layers.LayerTypeTCP: + return uint16(d.tcp.SrcPort), uint16(d.tcp.DstPort) + case layers.LayerTypeUDP: + return uint16(d.udp.SrcPort), uint16(d.udp.DstPort) + default: + return 0, 0 + } } func (m *Manager) isValidPacket(d *decoder, packetData []byte) bool { if err := d.parser.DecodeLayers(packetData, &d.decoded); err != nil { - log.Tracef("couldn't decode layer, err: %s", err) + m.logger.Trace("couldn't decode packet, err: %s", err) return false } if len(d.decoded) < 2 { - log.Tracef("not enough levels in network packet") + m.logger.Trace("packet doesn't have network and transport layers") return false } return true } -func (m *Manager) isWireguardTraffic(srcIP, dstIP net.IP) bool { - return m.wgNetwork.Contains(srcIP) && m.wgNetwork.Contains(dstIP) -} - func (m *Manager) isValidTrackedConnection(d *decoder, srcIP, dstIP net.IP) bool { switch d.decoded[1] { case layers.LayerTypeTCP: @@ -483,7 +783,22 @@ func (m *Manager) isValidTrackedConnection(d *decoder, srcIP, dstIP net.IP) bool return false } -func (m *Manager) applyRules(srcIP net.IP, packetData []byte, rules map[string]RuleSet, d *decoder) bool { +// isSpecialICMP returns true if the packet is a special ICMP packet that should be allowed +func (m *Manager) isSpecialICMP(d *decoder) bool { + if d.decoded[1] != layers.LayerTypeICMPv4 { + return false + } + + icmpType := d.icmp4.TypeCode.Type() + return icmpType == layers.ICMPv4TypeDestinationUnreachable || + icmpType == layers.ICMPv4TypeTimeExceeded +} + +func (m *Manager) peerACLsBlock(srcIP net.IP, packetData []byte, rules map[string]RuleSet, d *decoder) bool { + if m.isSpecialICMP(d) { + return false + } + if filter, ok := validateRule(srcIP, packetData, rules[srcIP.String()], d); ok { return filter } @@ -500,7 +815,24 @@ func (m *Manager) applyRules(srcIP net.IP, packetData []byte, rules map[string]R return true } -func validateRule(ip net.IP, packetData []byte, rules map[string]Rule, d *decoder) (bool, bool) { +func portsMatch(rulePort *firewall.Port, packetPort uint16) bool { + if rulePort == nil { + return true + } + + if rulePort.IsRange { + return packetPort >= rulePort.Values[0] && packetPort <= rulePort.Values[1] + } + + for _, p := range rulePort.Values { + if p == packetPort { + return true + } + } + return false +} + +func validateRule(ip net.IP, packetData []byte, rules map[string]PeerRule, d *decoder) (bool, bool) { payloadLayer := d.decoded[1] for _, rule := range rules { if rule.matchByIP && !ip.Equal(rule.ip) { @@ -517,13 +849,7 @@ func validateRule(ip net.IP, packetData []byte, rules map[string]Rule, d *decode switch payloadLayer { case layers.LayerTypeTCP: - if rule.sPort == 0 && rule.dPort == 0 { - return rule.drop, true - } - if rule.sPort != 0 && rule.sPort == uint16(d.tcp.SrcPort) { - return rule.drop, true - } - if rule.dPort != 0 && rule.dPort == uint16(d.tcp.DstPort) { + if portsMatch(rule.sPort, uint16(d.tcp.SrcPort)) && portsMatch(rule.dPort, uint16(d.tcp.DstPort)) { return rule.drop, true } case layers.LayerTypeUDP: @@ -533,13 +859,7 @@ func validateRule(ip net.IP, packetData []byte, rules map[string]Rule, d *decode return rule.udpHook(packetData), true } - if rule.sPort == 0 && rule.dPort == 0 { - return rule.drop, true - } - if rule.sPort != 0 && rule.sPort == uint16(d.udp.SrcPort) { - return rule.drop, true - } - if rule.dPort != 0 && rule.dPort == uint16(d.udp.DstPort) { + if portsMatch(rule.sPort, uint16(d.udp.SrcPort)) && portsMatch(rule.dPort, uint16(d.udp.DstPort)) { return rule.drop, true } case layers.LayerTypeICMPv4, layers.LayerTypeICMPv6: @@ -549,6 +869,51 @@ func validateRule(ip net.IP, packetData []byte, rules map[string]Rule, d *decode return false, false } +// routeACLsPass returns treu if the packet is allowed by the route ACLs +func (m *Manager) routeACLsPass(srcIP, dstIP net.IP, proto firewall.Protocol, srcPort, dstPort uint16) bool { + m.mutex.RLock() + defer m.mutex.RUnlock() + + srcAddr := netip.AddrFrom4([4]byte(srcIP.To4())) + dstAddr := netip.AddrFrom4([4]byte(dstIP.To4())) + + for _, rule := range m.routeRules { + if m.ruleMatches(rule, srcAddr, dstAddr, proto, srcPort, dstPort) { + return rule.action == firewall.ActionAccept + } + } + return false +} + +func (m *Manager) ruleMatches(rule RouteRule, srcAddr, dstAddr netip.Addr, proto firewall.Protocol, srcPort, dstPort uint16) bool { + if !rule.destination.Contains(dstAddr) { + return false + } + + sourceMatched := false + for _, src := range rule.sources { + if src.Contains(srcAddr) { + sourceMatched = true + break + } + } + if !sourceMatched { + return false + } + + if rule.proto != firewall.ProtocolALL && rule.proto != proto { + return false + } + + if proto == firewall.ProtocolTCP || proto == firewall.ProtocolUDP { + if !portsMatch(rule.srcPort, srcPort) || !portsMatch(rule.dstPort, dstPort) { + return false + } + } + + return true +} + // SetNetwork of the wireguard interface to which filtering applied func (m *Manager) SetNetwork(network *net.IPNet) { m.wgNetwork = network @@ -560,13 +925,12 @@ func (m *Manager) SetNetwork(network *net.IPNet) { func (m *Manager) AddUDPPacketHook( in bool, ip net.IP, dPort uint16, hook func([]byte) bool, ) string { - r := Rule{ + r := PeerRule{ id: uuid.New().String(), ip: ip, protoLayer: layers.LayerTypeUDP, - dPort: dPort, + dPort: &firewall.Port{Values: []uint16{dPort}}, ipLayer: layers.LayerTypeIPv6, - direction: firewall.RuleDirectionOUT, comment: fmt.Sprintf("UDP Hook direction: %v, ip:%v, dport:%d", in, ip, dPort), udpHook: hook, } @@ -577,14 +941,13 @@ func (m *Manager) AddUDPPacketHook( m.mutex.Lock() if in { - r.direction = firewall.RuleDirectionIN if _, ok := m.incomingRules[r.ip.String()]; !ok { - m.incomingRules[r.ip.String()] = make(map[string]Rule) + m.incomingRules[r.ip.String()] = make(map[string]PeerRule) } m.incomingRules[r.ip.String()][r.id] = r } else { if _, ok := m.outgoingRules[r.ip.String()]; !ok { - m.outgoingRules[r.ip.String()] = make(map[string]Rule) + m.outgoingRules[r.ip.String()] = make(map[string]PeerRule) } m.outgoingRules[r.ip.String()][r.id] = r } @@ -596,21 +959,62 @@ func (m *Manager) AddUDPPacketHook( // RemovePacketHook removes packet hook by given ID func (m *Manager) RemovePacketHook(hookID string) error { + m.mutex.Lock() + defer m.mutex.Unlock() + for _, arr := range m.incomingRules { for _, r := range arr { if r.id == hookID { - rule := r - return m.DeletePeerRule(&rule) + delete(arr, r.id) + return nil } } } for _, arr := range m.outgoingRules { for _, r := range arr { if r.id == hookID { - rule := r - return m.DeletePeerRule(&rule) + delete(arr, r.id) + return nil } } } return fmt.Errorf("hook with given id not found") } + +// SetLogLevel sets the log level for the firewall manager +func (m *Manager) SetLogLevel(level log.Level) { + if m.logger != nil { + m.logger.SetLevel(nblog.Level(level)) + } +} + +func (m *Manager) EnableRouting() error { + m.mutex.Lock() + defer m.mutex.Unlock() + + return m.determineRouting() +} + +func (m *Manager) DisableRouting() error { + m.mutex.Lock() + defer m.mutex.Unlock() + + if m.forwarder == nil { + return nil + } + + m.routingEnabled = false + m.nativeRouter = false + + // don't stop forwarder if in use by netstack + if m.netstack && m.localForwarding { + return nil + } + + m.forwarder.Stop() + m.forwarder = nil + + log.Debug("forwarder stopped") + + return nil +} diff --git a/client/firewall/uspfilter/uspfilter_bench_test.go b/client/firewall/uspfilter/uspfilter_bench_test.go index 3c661e71c..875bb2425 100644 --- a/client/firewall/uspfilter/uspfilter_bench_test.go +++ b/client/firewall/uspfilter/uspfilter_bench_test.go @@ -1,9 +1,12 @@ +//go:build uspbench + package uspfilter import ( "fmt" "math/rand" "net" + "net/netip" "os" "strings" "testing" @@ -91,7 +94,7 @@ func BenchmarkCoreFiltering(b *testing.B) { setupFunc: func(m *Manager) { // Single rule allowing all traffic _, err := m.AddPeerFiltering(net.ParseIP("0.0.0.0"), fw.ProtocolALL, nil, nil, - fw.RuleDirectionIN, fw.ActionAccept, "", "allow all") + fw.ActionAccept, "", "allow all") require.NoError(b, err) }, desc: "Baseline: Single 'allow all' rule without connection tracking", @@ -112,9 +115,9 @@ func BenchmarkCoreFiltering(b *testing.B) { for i := 0; i < 1000; i++ { // Simulate realistic ruleset size ip := generateRandomIPs(1)[0] _, err := m.AddPeerFiltering(ip, fw.ProtocolTCP, - &fw.Port{Values: []int{1024 + i}}, - &fw.Port{Values: []int{80}}, - fw.RuleDirectionIN, fw.ActionAccept, "", "explicit return") + &fw.Port{Values: []uint16{uint16(1024 + i)}}, + &fw.Port{Values: []uint16{80}}, + fw.ActionAccept, "", "explicit return") require.NoError(b, err) } }, @@ -126,7 +129,7 @@ func BenchmarkCoreFiltering(b *testing.B) { setupFunc: func(m *Manager) { // Add some basic rules but rely on state for established connections _, err := m.AddPeerFiltering(net.ParseIP("0.0.0.0"), fw.ProtocolTCP, nil, nil, - fw.RuleDirectionIN, fw.ActionDrop, "", "default drop") + fw.ActionDrop, "", "default drop") require.NoError(b, err) }, desc: "Connection tracking with established connections", @@ -155,7 +158,7 @@ func BenchmarkCoreFiltering(b *testing.B) { // Create manager and basic setup manager, _ := Create(&IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, - }) + }, false) defer b.Cleanup(func() { require.NoError(b, manager.Reset(nil)) }) @@ -185,7 +188,7 @@ func BenchmarkCoreFiltering(b *testing.B) { // Measure inbound packet processing b.ResetTimer() for i := 0; i < b.N; i++ { - manager.dropFilter(inbound, manager.incomingRules) + manager.dropFilter(inbound) } }) } @@ -200,7 +203,7 @@ func BenchmarkStateScaling(b *testing.B) { b.Run(fmt.Sprintf("conns_%d", count), func(b *testing.B) { manager, _ := Create(&IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, - }) + }, false) b.Cleanup(func() { require.NoError(b, manager.Reset(nil)) }) @@ -228,7 +231,7 @@ func BenchmarkStateScaling(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - manager.dropFilter(testIn, manager.incomingRules) + manager.dropFilter(testIn) } }) } @@ -248,7 +251,7 @@ func BenchmarkEstablishmentOverhead(b *testing.B) { b.Run(sc.name, func(b *testing.B) { manager, _ := Create(&IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, - }) + }, false) b.Cleanup(func() { require.NoError(b, manager.Reset(nil)) }) @@ -269,7 +272,7 @@ func BenchmarkEstablishmentOverhead(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - manager.dropFilter(inbound, manager.incomingRules) + manager.dropFilter(inbound) } }) } @@ -447,7 +450,7 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) { b.Run(sc.name, func(b *testing.B) { manager, _ := Create(&IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, - }) + }, false) b.Cleanup(func() { require.NoError(b, manager.Reset(nil)) }) @@ -472,7 +475,7 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) { manager.processOutgoingHooks(syn) // SYN-ACK synack := generateTCPPacketWithFlags(b, dstIP, srcIP, 80, 1024, uint16(conntrack.TCPSyn|conntrack.TCPAck)) - manager.dropFilter(synack, manager.incomingRules) + manager.dropFilter(synack) // ACK ack := generateTCPPacketWithFlags(b, srcIP, dstIP, 1024, 80, uint16(conntrack.TCPAck)) manager.processOutgoingHooks(ack) @@ -481,7 +484,7 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - manager.dropFilter(inbound, manager.incomingRules) + manager.dropFilter(inbound) } }) } @@ -574,7 +577,7 @@ func BenchmarkLongLivedConnections(b *testing.B) { manager, _ := Create(&IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, - }) + }, false) defer b.Cleanup(func() { require.NoError(b, manager.Reset(nil)) }) @@ -588,9 +591,9 @@ func BenchmarkLongLivedConnections(b *testing.B) { if sc.rules { // Single rule to allow all return traffic from port 80 _, err := manager.AddPeerFiltering(net.ParseIP("0.0.0.0"), fw.ProtocolTCP, - &fw.Port{Values: []int{80}}, + &fw.Port{Values: []uint16{80}}, nil, - fw.RuleDirectionIN, fw.ActionAccept, "", "return traffic") + fw.ActionAccept, "", "return traffic") require.NoError(b, err) } @@ -618,7 +621,7 @@ func BenchmarkLongLivedConnections(b *testing.B) { // SYN-ACK synack := generateTCPPacketWithFlags(b, dstIPs[i], srcIPs[i], 80, uint16(1024+i), uint16(conntrack.TCPSyn|conntrack.TCPAck)) - manager.dropFilter(synack, manager.incomingRules) + manager.dropFilter(synack) // ACK ack := generateTCPPacketWithFlags(b, srcIPs[i], dstIPs[i], @@ -646,7 +649,7 @@ func BenchmarkLongLivedConnections(b *testing.B) { // First outbound data manager.processOutgoingHooks(outPackets[connIdx]) // Then inbound response - this is what we're actually measuring - manager.dropFilter(inPackets[connIdx], manager.incomingRules) + manager.dropFilter(inPackets[connIdx]) } }) } @@ -665,7 +668,7 @@ func BenchmarkShortLivedConnections(b *testing.B) { manager, _ := Create(&IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, - }) + }, false) defer b.Cleanup(func() { require.NoError(b, manager.Reset(nil)) }) @@ -679,9 +682,9 @@ func BenchmarkShortLivedConnections(b *testing.B) { if sc.rules { // Single rule to allow all return traffic from port 80 _, err := manager.AddPeerFiltering(net.ParseIP("0.0.0.0"), fw.ProtocolTCP, - &fw.Port{Values: []int{80}}, + &fw.Port{Values: []uint16{80}}, nil, - fw.RuleDirectionIN, fw.ActionAccept, "", "return traffic") + fw.ActionAccept, "", "return traffic") require.NoError(b, err) } @@ -754,17 +757,17 @@ func BenchmarkShortLivedConnections(b *testing.B) { // Connection establishment manager.processOutgoingHooks(p.syn) - manager.dropFilter(p.synAck, manager.incomingRules) + manager.dropFilter(p.synAck) manager.processOutgoingHooks(p.ack) // Data transfer manager.processOutgoingHooks(p.request) - manager.dropFilter(p.response, manager.incomingRules) + manager.dropFilter(p.response) // Connection teardown manager.processOutgoingHooks(p.finClient) - manager.dropFilter(p.ackServer, manager.incomingRules) - manager.dropFilter(p.finServer, manager.incomingRules) + manager.dropFilter(p.ackServer) + manager.dropFilter(p.finServer) manager.processOutgoingHooks(p.ackClient) } }) @@ -784,7 +787,7 @@ func BenchmarkParallelLongLivedConnections(b *testing.B) { manager, _ := Create(&IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, - }) + }, false) defer b.Cleanup(func() { require.NoError(b, manager.Reset(nil)) }) @@ -797,9 +800,9 @@ func BenchmarkParallelLongLivedConnections(b *testing.B) { // Setup initial state based on scenario if sc.rules { _, err := manager.AddPeerFiltering(net.ParseIP("0.0.0.0"), fw.ProtocolTCP, - &fw.Port{Values: []int{80}}, + &fw.Port{Values: []uint16{80}}, nil, - fw.RuleDirectionIN, fw.ActionAccept, "", "return traffic") + fw.ActionAccept, "", "return traffic") require.NoError(b, err) } @@ -825,7 +828,7 @@ func BenchmarkParallelLongLivedConnections(b *testing.B) { synack := generateTCPPacketWithFlags(b, dstIPs[i], srcIPs[i], 80, uint16(1024+i), uint16(conntrack.TCPSyn|conntrack.TCPAck)) - manager.dropFilter(synack, manager.incomingRules) + manager.dropFilter(synack) ack := generateTCPPacketWithFlags(b, srcIPs[i], dstIPs[i], uint16(1024+i), 80, uint16(conntrack.TCPAck)) @@ -852,7 +855,7 @@ func BenchmarkParallelLongLivedConnections(b *testing.B) { // Simulate bidirectional traffic manager.processOutgoingHooks(outPackets[connIdx]) - manager.dropFilter(inPackets[connIdx], manager.incomingRules) + manager.dropFilter(inPackets[connIdx]) } }) }) @@ -872,7 +875,7 @@ func BenchmarkParallelShortLivedConnections(b *testing.B) { manager, _ := Create(&IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, - }) + }, false) defer b.Cleanup(func() { require.NoError(b, manager.Reset(nil)) }) @@ -884,9 +887,9 @@ func BenchmarkParallelShortLivedConnections(b *testing.B) { if sc.rules { _, err := manager.AddPeerFiltering(net.ParseIP("0.0.0.0"), fw.ProtocolTCP, - &fw.Port{Values: []int{80}}, + &fw.Port{Values: []uint16{80}}, nil, - fw.RuleDirectionIN, fw.ActionAccept, "", "return traffic") + fw.ActionAccept, "", "return traffic") require.NoError(b, err) } @@ -949,15 +952,15 @@ func BenchmarkParallelShortLivedConnections(b *testing.B) { // Full connection lifecycle manager.processOutgoingHooks(p.syn) - manager.dropFilter(p.synAck, manager.incomingRules) + manager.dropFilter(p.synAck) manager.processOutgoingHooks(p.ack) manager.processOutgoingHooks(p.request) - manager.dropFilter(p.response, manager.incomingRules) + manager.dropFilter(p.response) manager.processOutgoingHooks(p.finClient) - manager.dropFilter(p.ackServer, manager.incomingRules) - manager.dropFilter(p.finServer, manager.incomingRules) + manager.dropFilter(p.ackServer) + manager.dropFilter(p.finServer) manager.processOutgoingHooks(p.ackClient) } }) @@ -996,3 +999,72 @@ func generateTCPPacketWithFlags(b *testing.B, srcIP, dstIP net.IP, srcPort, dstP require.NoError(b, gopacket.SerializeLayers(buf, opts, ipv4, tcp, gopacket.Payload("test"))) return buf.Bytes() } + +func BenchmarkRouteACLs(b *testing.B) { + manager := setupRoutedManager(b, "10.10.0.100/16") + + // Add several route rules to simulate real-world scenario + rules := []struct { + sources []netip.Prefix + dest netip.Prefix + proto fw.Protocol + port *fw.Port + }{ + { + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + port: &fw.Port{Values: []uint16{80, 443}}, + }, + { + sources: []netip.Prefix{ + netip.MustParsePrefix("172.16.0.0/12"), + netip.MustParsePrefix("10.0.0.0/8"), + }, + dest: netip.MustParsePrefix("0.0.0.0/0"), + proto: fw.ProtocolICMP, + }, + { + sources: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")}, + dest: netip.MustParsePrefix("192.168.0.0/16"), + proto: fw.ProtocolUDP, + port: &fw.Port{Values: []uint16{53}}, + }, + } + + for _, r := range rules { + _, err := manager.AddRouteFiltering( + r.sources, + r.dest, + r.proto, + nil, + r.port, + fw.ActionAccept, + ) + if err != nil { + b.Fatal(err) + } + } + + // Test cases that exercise different matching scenarios + cases := []struct { + srcIP string + dstIP string + proto fw.Protocol + dstPort uint16 + }{ + {"100.10.0.1", "192.168.1.100", fw.ProtocolTCP, 443}, // Match first rule + {"172.16.0.1", "8.8.8.8", fw.ProtocolICMP, 0}, // Match second rule + {"1.1.1.1", "192.168.1.53", fw.ProtocolUDP, 53}, // Match third rule + {"192.168.1.1", "10.0.0.1", fw.ProtocolTCP, 8080}, // No match + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, tc := range cases { + srcIP := net.ParseIP(tc.srcIP) + dstIP := net.ParseIP(tc.dstIP) + manager.routeACLsPass(srcIP, dstIP, tc.proto, 0, tc.dstPort) + } + } +} diff --git a/client/firewall/uspfilter/uspfilter_filter_test.go b/client/firewall/uspfilter/uspfilter_filter_test.go new file mode 100644 index 000000000..9a1456d00 --- /dev/null +++ b/client/firewall/uspfilter/uspfilter_filter_test.go @@ -0,0 +1,1015 @@ +package uspfilter + +import ( + "net" + "net/netip" + "testing" + + "github.com/golang/mock/gomock" + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/stretchr/testify/require" + wgdevice "golang.zx2c4.com/wireguard/device" + + fw "github.com/netbirdio/netbird/client/firewall/manager" + "github.com/netbirdio/netbird/client/iface" + "github.com/netbirdio/netbird/client/iface/device" + "github.com/netbirdio/netbird/client/iface/mocks" +) + +func TestPeerACLFiltering(t *testing.T) { + localIP := net.ParseIP("100.10.0.100") + wgNet := &net.IPNet{ + IP: net.ParseIP("100.10.0.0"), + Mask: net.CIDRMask(16, 32), + } + + ifaceMock := &IFaceMock{ + SetFilterFunc: func(device.PacketFilter) error { return nil }, + AddressFunc: func() iface.WGAddress { + return iface.WGAddress{ + IP: localIP, + Network: wgNet, + } + }, + } + + manager, err := Create(ifaceMock, false) + require.NoError(t, err) + require.NotNil(t, manager) + + t.Cleanup(func() { + require.NoError(t, manager.Reset(nil)) + }) + + manager.wgNetwork = wgNet + + err = manager.UpdateLocalIPs() + require.NoError(t, err) + + testCases := []struct { + name string + srcIP string + dstIP string + proto fw.Protocol + srcPort uint16 + dstPort uint16 + ruleIP string + ruleProto fw.Protocol + ruleSrcPort *fw.Port + ruleDstPort *fw.Port + ruleAction fw.Action + shouldBeBlocked bool + }{ + { + name: "Allow TCP traffic from WG peer", + srcIP: "100.10.0.1", + dstIP: "100.10.0.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 443, + ruleIP: "100.10.0.1", + ruleProto: fw.ProtocolTCP, + ruleDstPort: &fw.Port{Values: []uint16{443}}, + ruleAction: fw.ActionAccept, + shouldBeBlocked: false, + }, + { + name: "Allow UDP traffic from WG peer", + srcIP: "100.10.0.1", + dstIP: "100.10.0.100", + proto: fw.ProtocolUDP, + srcPort: 12345, + dstPort: 53, + ruleIP: "100.10.0.1", + ruleProto: fw.ProtocolUDP, + ruleDstPort: &fw.Port{Values: []uint16{53}}, + ruleAction: fw.ActionAccept, + shouldBeBlocked: false, + }, + { + name: "Allow ICMP traffic from WG peer", + srcIP: "100.10.0.1", + dstIP: "100.10.0.100", + proto: fw.ProtocolICMP, + ruleIP: "100.10.0.1", + ruleProto: fw.ProtocolICMP, + ruleAction: fw.ActionAccept, + shouldBeBlocked: false, + }, + { + name: "Allow all traffic from WG peer", + srcIP: "100.10.0.1", + dstIP: "100.10.0.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 443, + ruleIP: "100.10.0.1", + ruleProto: fw.ProtocolALL, + ruleAction: fw.ActionAccept, + shouldBeBlocked: false, + }, + { + name: "Allow traffic from non-WG source", + srcIP: "192.168.1.1", + dstIP: "100.10.0.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 443, + ruleIP: "192.168.1.1", + ruleProto: fw.ProtocolTCP, + ruleDstPort: &fw.Port{Values: []uint16{443}}, + ruleAction: fw.ActionAccept, + shouldBeBlocked: false, + }, + { + name: "Allow all traffic with 0.0.0.0 rule", + srcIP: "100.10.0.1", + dstIP: "100.10.0.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 443, + ruleIP: "0.0.0.0", + ruleProto: fw.ProtocolALL, + ruleAction: fw.ActionAccept, + shouldBeBlocked: false, + }, + { + name: "Allow TCP traffic within port range", + srcIP: "100.10.0.1", + dstIP: "100.10.0.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 8080, + ruleIP: "100.10.0.1", + ruleProto: fw.ProtocolTCP, + ruleDstPort: &fw.Port{IsRange: true, Values: []uint16{8000, 8100}}, + ruleAction: fw.ActionAccept, + shouldBeBlocked: false, + }, + { + name: "Block TCP traffic outside port range", + srcIP: "100.10.0.1", + dstIP: "100.10.0.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 7999, + ruleIP: "100.10.0.1", + ruleProto: fw.ProtocolTCP, + ruleDstPort: &fw.Port{IsRange: true, Values: []uint16{8000, 8100}}, + ruleAction: fw.ActionAccept, + shouldBeBlocked: true, + }, + { + name: "Allow TCP traffic with source port range", + srcIP: "100.10.0.1", + dstIP: "100.10.0.100", + proto: fw.ProtocolTCP, + srcPort: 32100, + dstPort: 443, + ruleIP: "100.10.0.1", + ruleProto: fw.ProtocolTCP, + ruleSrcPort: &fw.Port{IsRange: true, Values: []uint16{32000, 33000}}, + ruleDstPort: &fw.Port{Values: []uint16{443}}, + ruleAction: fw.ActionAccept, + shouldBeBlocked: false, + }, + { + name: "Block TCP traffic outside source port range", + srcIP: "100.10.0.1", + dstIP: "100.10.0.100", + proto: fw.ProtocolTCP, + srcPort: 31999, + dstPort: 443, + ruleIP: "100.10.0.1", + ruleProto: fw.ProtocolTCP, + ruleSrcPort: &fw.Port{IsRange: true, Values: []uint16{32000, 33000}}, + ruleDstPort: &fw.Port{Values: []uint16{443}}, + ruleAction: fw.ActionAccept, + shouldBeBlocked: true, + }, + } + + t.Run("Implicit DROP (no rules)", func(t *testing.T) { + packet := createTestPacket(t, "100.10.0.1", "100.10.0.100", fw.ProtocolTCP, 12345, 443) + isDropped := manager.DropIncoming(packet) + require.True(t, isDropped, "Packet should be dropped when no rules exist") + }) + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + rules, err := manager.AddPeerFiltering( + net.ParseIP(tc.ruleIP), + tc.ruleProto, + tc.ruleSrcPort, + tc.ruleDstPort, + tc.ruleAction, + "", + tc.name, + ) + require.NoError(t, err) + require.NotEmpty(t, rules) + + t.Cleanup(func() { + for _, rule := range rules { + require.NoError(t, manager.DeletePeerRule(rule)) + } + }) + + packet := createTestPacket(t, tc.srcIP, tc.dstIP, tc.proto, tc.srcPort, tc.dstPort) + isDropped := manager.DropIncoming(packet) + require.Equal(t, tc.shouldBeBlocked, isDropped) + }) + } +} + +func createTestPacket(t *testing.T, srcIP, dstIP string, proto fw.Protocol, srcPort, dstPort uint16) []byte { + t.Helper() + + buf := gopacket.NewSerializeBuffer() + opts := gopacket.SerializeOptions{ + ComputeChecksums: true, + FixLengths: true, + } + + ipLayer := &layers.IPv4{ + Version: 4, + TTL: 64, + SrcIP: net.ParseIP(srcIP), + DstIP: net.ParseIP(dstIP), + } + + var err error + switch proto { + case fw.ProtocolTCP: + ipLayer.Protocol = layers.IPProtocolTCP + tcp := &layers.TCP{ + SrcPort: layers.TCPPort(srcPort), + DstPort: layers.TCPPort(dstPort), + } + err = tcp.SetNetworkLayerForChecksum(ipLayer) + require.NoError(t, err) + err = gopacket.SerializeLayers(buf, opts, ipLayer, tcp) + + case fw.ProtocolUDP: + ipLayer.Protocol = layers.IPProtocolUDP + udp := &layers.UDP{ + SrcPort: layers.UDPPort(srcPort), + DstPort: layers.UDPPort(dstPort), + } + err = udp.SetNetworkLayerForChecksum(ipLayer) + require.NoError(t, err) + err = gopacket.SerializeLayers(buf, opts, ipLayer, udp) + + case fw.ProtocolICMP: + ipLayer.Protocol = layers.IPProtocolICMPv4 + icmp := &layers.ICMPv4{ + TypeCode: layers.CreateICMPv4TypeCode(layers.ICMPv4TypeEchoRequest, 0), + } + err = gopacket.SerializeLayers(buf, opts, ipLayer, icmp) + + default: + err = gopacket.SerializeLayers(buf, opts, ipLayer) + } + + require.NoError(t, err) + return buf.Bytes() +} + +func setupRoutedManager(tb testing.TB, network string) *Manager { + tb.Helper() + + ctrl := gomock.NewController(tb) + dev := mocks.NewMockDevice(ctrl) + dev.EXPECT().MTU().Return(1500, nil).AnyTimes() + + localIP, wgNet, err := net.ParseCIDR(network) + require.NoError(tb, err) + + ifaceMock := &IFaceMock{ + SetFilterFunc: func(device.PacketFilter) error { return nil }, + AddressFunc: func() iface.WGAddress { + return iface.WGAddress{ + IP: localIP, + Network: wgNet, + } + }, + GetDeviceFunc: func() *device.FilteredDevice { + return &device.FilteredDevice{Device: dev} + }, + GetWGDeviceFunc: func() *wgdevice.Device { + return &wgdevice.Device{} + }, + } + + manager, err := Create(ifaceMock, false) + require.NoError(tb, manager.EnableRouting()) + require.NoError(tb, err) + require.NotNil(tb, manager) + require.True(tb, manager.routingEnabled) + require.False(tb, manager.nativeRouter) + + tb.Cleanup(func() { + require.NoError(tb, manager.Reset(nil)) + }) + + return manager +} + +func TestRouteACLFiltering(t *testing.T) { + manager := setupRoutedManager(t, "10.10.0.100/16") + + type rule struct { + sources []netip.Prefix + dest netip.Prefix + proto fw.Protocol + srcPort *fw.Port + dstPort *fw.Port + action fw.Action + } + + testCases := []struct { + name string + srcIP string + dstIP string + proto fw.Protocol + srcPort uint16 + dstPort uint16 + rule rule + shouldPass bool + }{ + { + name: "Allow TCP with specific source and destination", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 443, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{Values: []uint16{443}}, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Allow any source to specific destination", + srcIP: "172.16.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 443, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{Values: []uint16{443}}, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Allow any source to any destination", + srcIP: "172.16.0.1", + dstIP: "203.0.113.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 443, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")}, + dest: netip.MustParsePrefix("0.0.0.0/0"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{Values: []uint16{443}}, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Allow UDP DNS traffic", + srcIP: "100.10.0.1", + dstIP: "192.168.1.53", + proto: fw.ProtocolUDP, + srcPort: 54321, + dstPort: 53, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolUDP, + dstPort: &fw.Port{Values: []uint16{53}}, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Allow ICMP to any destination", + srcIP: "100.10.0.1", + dstIP: "8.8.8.8", + proto: fw.ProtocolICMP, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("0.0.0.0/0"), + proto: fw.ProtocolICMP, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Allow all protocols but specific port", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 80, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolALL, + dstPort: &fw.Port{Values: []uint16{80}}, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Implicit deny - wrong destination port", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 8080, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{Values: []uint16{80}}, + action: fw.ActionAccept, + }, + shouldPass: false, + }, + { + name: "Implicit deny - wrong protocol", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolUDP, + srcPort: 12345, + dstPort: 80, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{Values: []uint16{80}}, + action: fw.ActionAccept, + }, + shouldPass: false, + }, + { + name: "Implicit deny - wrong source network", + srcIP: "172.16.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 80, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{Values: []uint16{80}}, + action: fw.ActionAccept, + }, + shouldPass: false, + }, + { + name: "Source port match", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 80, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + srcPort: &fw.Port{Values: []uint16{12345}}, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Multiple source networks", + srcIP: "172.16.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 80, + rule: rule{ + sources: []netip.Prefix{ + netip.MustParsePrefix("100.10.0.0/16"), + netip.MustParsePrefix("172.16.0.0/16"), + }, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{Values: []uint16{80}}, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Allow ALL protocol without ports", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolICMP, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolALL, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Allow ALL protocol with specific ports", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 80, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolALL, + dstPort: &fw.Port{Values: []uint16{80}}, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Multiple source networks with mismatched protocol", + srcIP: "172.16.0.1", + dstIP: "192.168.1.100", + // Should not match TCP rule + proto: fw.ProtocolUDP, + srcPort: 12345, + dstPort: 80, + rule: rule{ + sources: []netip.Prefix{ + netip.MustParsePrefix("100.10.0.0/16"), + netip.MustParsePrefix("172.16.0.0/16"), + }, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{Values: []uint16{80}}, + action: fw.ActionAccept, + }, + shouldPass: false, + }, + { + name: "Allow multiple destination ports", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 8080, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{Values: []uint16{80, 8080, 443}}, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Allow multiple source ports", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 80, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + srcPort: &fw.Port{Values: []uint16{12345, 12346, 12347}}, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Allow ALL protocol with both src and dst ports", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 80, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolALL, + srcPort: &fw.Port{Values: []uint16{12345}}, + dstPort: &fw.Port{Values: []uint16{80}}, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Port Range - Within Range", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 8080, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{ + IsRange: true, + Values: []uint16{8000, 8100}, + }, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Port Range - Outside Range", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 7999, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{ + IsRange: true, + Values: []uint16{8000, 8100}, + }, + action: fw.ActionAccept, + }, + shouldPass: false, + }, + { + name: "Source Port Range - Within Range", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 32100, + dstPort: 80, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + srcPort: &fw.Port{ + IsRange: true, + Values: []uint16{32000, 33000}, + }, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Mixed Port Specification - Range and Single", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 32100, + dstPort: 443, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + srcPort: &fw.Port{ + IsRange: true, + Values: []uint16{32000, 33000}, + }, + dstPort: &fw.Port{ + Values: []uint16{443}, + }, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Edge Case - Port at Range Boundary", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 8100, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{ + IsRange: true, + Values: []uint16{8000, 8100}, + }, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "UDP Port Range", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolUDP, + srcPort: 12345, + dstPort: 5060, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolUDP, + dstPort: &fw.Port{ + IsRange: true, + Values: []uint16{5060, 5070}, + }, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "ALL Protocol with Port Range", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 8080, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolALL, + dstPort: &fw.Port{ + IsRange: true, + Values: []uint16{8000, 8100}, + }, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Drop TCP traffic to specific destination", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 443, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{Values: []uint16{443}}, + action: fw.ActionDrop, + }, + shouldPass: false, + }, + { + name: "Drop all traffic to specific destination", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 80, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolALL, + action: fw.ActionDrop, + }, + shouldPass: false, + }, + { + name: "Drop traffic from multiple source networks", + srcIP: "172.16.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 80, + rule: rule{ + sources: []netip.Prefix{ + netip.MustParsePrefix("100.10.0.0/16"), + netip.MustParsePrefix("172.16.0.0/16"), + }, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{Values: []uint16{80}}, + action: fw.ActionDrop, + }, + shouldPass: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + rule, err := manager.AddRouteFiltering( + tc.rule.sources, + tc.rule.dest, + tc.rule.proto, + tc.rule.srcPort, + tc.rule.dstPort, + tc.rule.action, + ) + require.NoError(t, err) + require.NotNil(t, rule) + + t.Cleanup(func() { + require.NoError(t, manager.DeleteRouteRule(rule)) + }) + + srcIP := net.ParseIP(tc.srcIP) + dstIP := net.ParseIP(tc.dstIP) + + // testing routeACLsPass only and not DropIncoming, as routed packets are dropped after being passed + // to the forwarder + isAllowed := manager.routeACLsPass(srcIP, dstIP, tc.proto, tc.srcPort, tc.dstPort) + require.Equal(t, tc.shouldPass, isAllowed) + }) + } +} + +func TestRouteACLOrder(t *testing.T) { + manager := setupRoutedManager(t, "10.10.0.100/16") + + type testCase struct { + name string + rules []struct { + sources []netip.Prefix + dest netip.Prefix + proto fw.Protocol + srcPort *fw.Port + dstPort *fw.Port + action fw.Action + } + packets []struct { + srcIP string + dstIP string + proto fw.Protocol + srcPort uint16 + dstPort uint16 + shouldPass bool + } + } + + testCases := []testCase{ + { + name: "Drop rules take precedence over accept", + rules: []struct { + sources []netip.Prefix + dest netip.Prefix + proto fw.Protocol + srcPort *fw.Port + dstPort *fw.Port + action fw.Action + }{ + { + // Accept rule added first + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{Values: []uint16{80, 443}}, + action: fw.ActionAccept, + }, + { + // Drop rule added second but should be evaluated first + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{Values: []uint16{443}}, + action: fw.ActionDrop, + }, + }, + packets: []struct { + srcIP string + dstIP string + proto fw.Protocol + srcPort uint16 + dstPort uint16 + shouldPass bool + }{ + { + // Should be dropped by the drop rule + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 443, + shouldPass: false, + }, + { + // Should be allowed by the accept rule (port 80 not in drop rule) + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 80, + shouldPass: true, + }, + }, + }, + { + name: "Multiple drop rules take precedence", + rules: []struct { + sources []netip.Prefix + dest netip.Prefix + proto fw.Protocol + srcPort *fw.Port + dstPort *fw.Port + action fw.Action + }{ + { + // Accept all + sources: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")}, + dest: netip.MustParsePrefix("0.0.0.0/0"), + proto: fw.ProtocolALL, + action: fw.ActionAccept, + }, + { + // Drop specific port + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{Values: []uint16{443}}, + action: fw.ActionDrop, + }, + { + // Drop different port + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{Values: []uint16{80}}, + action: fw.ActionDrop, + }, + }, + packets: []struct { + srcIP string + dstIP string + proto fw.Protocol + srcPort uint16 + dstPort uint16 + shouldPass bool + }{ + { + // Should be dropped by first drop rule + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 443, + shouldPass: false, + }, + { + // Should be dropped by second drop rule + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 80, + shouldPass: false, + }, + { + // Should be allowed by the accept rule (different port) + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 8080, + shouldPass: true, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var rules []fw.Rule + for _, r := range tc.rules { + rule, err := manager.AddRouteFiltering( + r.sources, + r.dest, + r.proto, + r.srcPort, + r.dstPort, + r.action, + ) + require.NoError(t, err) + require.NotNil(t, rule) + rules = append(rules, rule) + } + + t.Cleanup(func() { + for _, rule := range rules { + require.NoError(t, manager.DeleteRouteRule(rule)) + } + }) + + for i, p := range tc.packets { + srcIP := net.ParseIP(p.srcIP) + dstIP := net.ParseIP(p.dstIP) + + isAllowed := manager.routeACLsPass(srcIP, dstIP, p.proto, p.srcPort, p.dstPort) + require.Equal(t, p.shouldPass, isAllowed, "packet %d failed", i) + } + }) + } +} diff --git a/client/firewall/uspfilter/uspfilter_test.go b/client/firewall/uspfilter/uspfilter_test.go index d3563e6f2..089bf8f55 100644 --- a/client/firewall/uspfilter/uspfilter_test.go +++ b/client/firewall/uspfilter/uspfilter_test.go @@ -9,17 +9,38 @@ import ( "github.com/google/gopacket" "github.com/google/gopacket/layers" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" + wgdevice "golang.zx2c4.com/wireguard/device" fw "github.com/netbirdio/netbird/client/firewall/manager" "github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack" + "github.com/netbirdio/netbird/client/firewall/uspfilter/log" "github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/iface/device" ) +var logger = log.NewFromLogrus(logrus.StandardLogger()) + type IFaceMock struct { - SetFilterFunc func(device.PacketFilter) error - AddressFunc func() iface.WGAddress + SetFilterFunc func(device.PacketFilter) error + AddressFunc func() iface.WGAddress + GetWGDeviceFunc func() *wgdevice.Device + GetDeviceFunc func() *device.FilteredDevice +} + +func (i *IFaceMock) GetWGDevice() *wgdevice.Device { + if i.GetWGDeviceFunc == nil { + return nil + } + return i.GetWGDeviceFunc() +} + +func (i *IFaceMock) GetDevice() *device.FilteredDevice { + if i.GetDeviceFunc == nil { + return nil + } + return i.GetDeviceFunc() } func (i *IFaceMock) SetFilter(iface device.PacketFilter) error { @@ -41,7 +62,7 @@ func TestManagerCreate(t *testing.T) { SetFilterFunc: func(device.PacketFilter) error { return nil }, } - m, err := Create(ifaceMock) + m, err := Create(ifaceMock, false) if err != nil { t.Errorf("failed to create Manager: %v", err) return @@ -61,7 +82,7 @@ func TestManagerAddPeerFiltering(t *testing.T) { }, } - m, err := Create(ifaceMock) + m, err := Create(ifaceMock, false) if err != nil { t.Errorf("failed to create Manager: %v", err) return @@ -69,12 +90,11 @@ func TestManagerAddPeerFiltering(t *testing.T) { ip := net.ParseIP("192.168.1.1") proto := fw.ProtocolTCP - port := &fw.Port{Values: []int{80}} - direction := fw.RuleDirectionOUT + port := &fw.Port{Values: []uint16{80}} action := fw.ActionDrop comment := "Test rule" - rule, err := m.AddPeerFiltering(ip, proto, nil, port, direction, action, "", comment) + rule, err := m.AddPeerFiltering(ip, proto, nil, port, action, "", comment) if err != nil { t.Errorf("failed to add filtering: %v", err) return @@ -96,7 +116,7 @@ func TestManagerDeleteRule(t *testing.T) { SetFilterFunc: func(device.PacketFilter) error { return nil }, } - m, err := Create(ifaceMock) + m, err := Create(ifaceMock, false) if err != nil { t.Errorf("failed to create Manager: %v", err) return @@ -104,38 +124,16 @@ func TestManagerDeleteRule(t *testing.T) { ip := net.ParseIP("192.168.1.1") proto := fw.ProtocolTCP - port := &fw.Port{Values: []int{80}} - direction := fw.RuleDirectionOUT + port := &fw.Port{Values: []uint16{80}} action := fw.ActionDrop - comment := "Test rule" + comment := "Test rule 2" - rule, err := m.AddPeerFiltering(ip, proto, nil, port, direction, action, "", comment) + rule2, err := m.AddPeerFiltering(ip, proto, nil, port, action, "", comment) if err != nil { t.Errorf("failed to add filtering: %v", err) return } - ip = net.ParseIP("192.168.1.1") - proto = fw.ProtocolTCP - port = &fw.Port{Values: []int{80}} - direction = fw.RuleDirectionIN - action = fw.ActionDrop - comment = "Test rule 2" - - rule2, err := m.AddPeerFiltering(ip, proto, nil, port, direction, action, "", comment) - if err != nil { - t.Errorf("failed to add filtering: %v", err) - return - } - - for _, r := range rule { - err = m.DeletePeerRule(r) - if err != nil { - t.Errorf("failed to delete rule: %v", err) - return - } - } - for _, r := range rule2 { if _, ok := m.incomingRules[ip.String()][r.GetRuleID()]; !ok { t.Errorf("rule2 is not in the incomingRules") @@ -189,12 +187,12 @@ func TestAddUDPPacketHook(t *testing.T) { t.Run(tt.name, func(t *testing.T) { manager, err := Create(&IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, - }) + }, false) require.NoError(t, err) manager.AddUDPPacketHook(tt.in, tt.ip, tt.dPort, tt.hook) - var addedRule Rule + var addedRule PeerRule if tt.in { if len(manager.incomingRules[tt.ip.String()]) != 1 { t.Errorf("expected 1 incoming rule, got %d", len(manager.incomingRules)) @@ -217,18 +215,14 @@ func TestAddUDPPacketHook(t *testing.T) { t.Errorf("expected ip %s, got %s", tt.ip, addedRule.ip) return } - if tt.dPort != addedRule.dPort { - t.Errorf("expected dPort %d, got %d", tt.dPort, addedRule.dPort) + if tt.dPort != addedRule.dPort.Values[0] { + t.Errorf("expected dPort %d, got %d", tt.dPort, addedRule.dPort.Values[0]) return } if layers.LayerTypeUDP != addedRule.protoLayer { t.Errorf("expected protoLayer %s, got %s", layers.LayerTypeUDP, addedRule.protoLayer) return } - if tt.expDir != addedRule.direction { - t.Errorf("expected direction %d, got %d", tt.expDir, addedRule.direction) - return - } if addedRule.udpHook == nil { t.Errorf("expected udpHook to be set") return @@ -242,7 +236,7 @@ func TestManagerReset(t *testing.T) { SetFilterFunc: func(device.PacketFilter) error { return nil }, } - m, err := Create(ifaceMock) + m, err := Create(ifaceMock, false) if err != nil { t.Errorf("failed to create Manager: %v", err) return @@ -250,12 +244,11 @@ func TestManagerReset(t *testing.T) { ip := net.ParseIP("192.168.1.1") proto := fw.ProtocolTCP - port := &fw.Port{Values: []int{80}} - direction := fw.RuleDirectionOUT + port := &fw.Port{Values: []uint16{80}} action := fw.ActionDrop comment := "Test rule" - _, err = m.AddPeerFiltering(ip, proto, nil, port, direction, action, "", comment) + _, err = m.AddPeerFiltering(ip, proto, nil, port, action, "", comment) if err != nil { t.Errorf("failed to add filtering: %v", err) return @@ -275,9 +268,18 @@ func TestManagerReset(t *testing.T) { func TestNotMatchByIP(t *testing.T) { ifaceMock := &IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, + AddressFunc: func() iface.WGAddress { + return iface.WGAddress{ + IP: net.ParseIP("100.10.0.100"), + Network: &net.IPNet{ + IP: net.ParseIP("100.10.0.0"), + Mask: net.CIDRMask(16, 32), + }, + } + }, } - m, err := Create(ifaceMock) + m, err := Create(ifaceMock, false) if err != nil { t.Errorf("failed to create Manager: %v", err) return @@ -289,11 +291,10 @@ func TestNotMatchByIP(t *testing.T) { ip := net.ParseIP("0.0.0.0") proto := fw.ProtocolUDP - direction := fw.RuleDirectionOUT action := fw.ActionAccept comment := "Test rule" - _, err = m.AddPeerFiltering(ip, proto, nil, nil, direction, action, "", comment) + _, err = m.AddPeerFiltering(ip, proto, nil, nil, action, "", comment) if err != nil { t.Errorf("failed to add filtering: %v", err) return @@ -327,7 +328,7 @@ func TestNotMatchByIP(t *testing.T) { return } - if m.dropFilter(buf.Bytes(), m.outgoingRules) { + if m.dropFilter(buf.Bytes()) { t.Errorf("expected packet to be accepted") return } @@ -346,7 +347,7 @@ func TestRemovePacketHook(t *testing.T) { } // creating manager instance - manager, err := Create(iface) + manager, err := Create(iface, false) if err != nil { t.Fatalf("Failed to create Manager: %s", err) } @@ -392,7 +393,7 @@ func TestRemovePacketHook(t *testing.T) { func TestProcessOutgoingHooks(t *testing.T) { manager, err := Create(&IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, - }) + }, false) require.NoError(t, err) manager.wgNetwork = &net.IPNet{ @@ -400,7 +401,7 @@ func TestProcessOutgoingHooks(t *testing.T) { Mask: net.CIDRMask(16, 32), } manager.udpTracker.Close() - manager.udpTracker = conntrack.NewUDPTracker(100 * time.Millisecond) + manager.udpTracker = conntrack.NewUDPTracker(100*time.Millisecond, logger) defer func() { require.NoError(t, manager.Reset(nil)) }() @@ -478,7 +479,7 @@ func TestUSPFilterCreatePerformance(t *testing.T) { ifaceMock := &IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, } - manager, err := Create(ifaceMock) + manager, err := Create(ifaceMock, false) require.NoError(t, err) time.Sleep(time.Second) @@ -492,12 +493,8 @@ func TestUSPFilterCreatePerformance(t *testing.T) { ip := net.ParseIP("10.20.0.100") start := time.Now() for i := 0; i < testMax; i++ { - port := &fw.Port{Values: []int{1000 + i}} - if i%2 == 0 { - _, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.RuleDirectionOUT, fw.ActionAccept, "", "accept HTTP traffic") - } else { - _, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.RuleDirectionIN, fw.ActionAccept, "", "accept HTTP traffic") - } + port := &fw.Port{Values: []uint16{uint16(1000 + i)}} + _, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.ActionAccept, "", "accept HTTP traffic") require.NoError(t, err, "failed to add rule") } @@ -509,7 +506,7 @@ func TestUSPFilterCreatePerformance(t *testing.T) { func TestStatefulFirewall_UDPTracking(t *testing.T) { manager, err := Create(&IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, - }) + }, false) require.NoError(t, err) manager.wgNetwork = &net.IPNet{ @@ -518,7 +515,7 @@ func TestStatefulFirewall_UDPTracking(t *testing.T) { } manager.udpTracker.Close() // Close the existing tracker - manager.udpTracker = conntrack.NewUDPTracker(200 * time.Millisecond) + manager.udpTracker = conntrack.NewUDPTracker(200*time.Millisecond, logger) manager.decoders = sync.Pool{ New: func() any { d := &decoder{ @@ -639,7 +636,7 @@ func TestStatefulFirewall_UDPTracking(t *testing.T) { for _, cp := range checkPoints { time.Sleep(cp.sleep) - drop = manager.dropFilter(inboundBuf.Bytes(), manager.incomingRules) + drop = manager.dropFilter(inboundBuf.Bytes()) require.Equal(t, cp.shouldAllow, !drop, cp.description) // If the connection should still be valid, verify it exists @@ -710,7 +707,7 @@ func TestStatefulFirewall_UDPTracking(t *testing.T) { require.NoError(t, err) // Verify the invalid packet is dropped - drop = manager.dropFilter(testBuf.Bytes(), manager.incomingRules) + drop = manager.dropFilter(testBuf.Bytes()) require.True(t, drop, tc.description) }) } diff --git a/client/iface/bind/udp_mux.go b/client/iface/bind/udp_mux.go index 00a91f0ec..4c827de95 100644 --- a/client/iface/bind/udp_mux.go +++ b/client/iface/bind/udp_mux.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "net" + "slices" "strings" "sync" @@ -152,46 +153,7 @@ func NewUDPMuxDefault(params UDPMuxParams) *UDPMuxDefault { params.Logger = logging.NewDefaultLoggerFactory().NewLogger("ice") } - var localAddrsForUnspecified []net.Addr - if addr, ok := params.UDPConn.LocalAddr().(*net.UDPAddr); !ok { - params.Logger.Errorf("LocalAddr is not a net.UDPAddr, got %T", params.UDPConn.LocalAddr()) - } else if ok && addr.IP.IsUnspecified() { - // For unspecified addresses, the correct behavior is to return errListenUnspecified, but - // it will break the applications that are already using unspecified UDP connection - // with UDPMuxDefault, so print a warn log and create a local address list for mux. - params.Logger.Warn("UDPMuxDefault should not listening on unspecified address, use NewMultiUDPMuxFromPort instead") - var networks []ice.NetworkType - switch { - - case addr.IP.To16() != nil: - networks = []ice.NetworkType{ice.NetworkTypeUDP4, ice.NetworkTypeUDP6} - - case addr.IP.To4() != nil: - networks = []ice.NetworkType{ice.NetworkTypeUDP4} - - default: - params.Logger.Errorf("LocalAddr expected IPV4 or IPV6, got %T", params.UDPConn.LocalAddr()) - } - if len(networks) > 0 { - if params.Net == nil { - var err error - if params.Net, err = stdnet.NewNet(); err != nil { - params.Logger.Errorf("failed to get create network: %v", err) - } - } - - ips, err := localInterfaces(params.Net, params.InterfaceFilter, nil, networks, true) - if err == nil { - for _, ip := range ips { - localAddrsForUnspecified = append(localAddrsForUnspecified, &net.UDPAddr{IP: ip, Port: addr.Port}) - } - } else { - params.Logger.Errorf("failed to get local interfaces for unspecified addr: %v", err) - } - } - } - - return &UDPMuxDefault{ + mux := &UDPMuxDefault{ addressMap: map[string][]*udpMuxedConn{}, params: params, connsIPv4: make(map[string]*udpMuxedConn), @@ -203,8 +165,55 @@ func NewUDPMuxDefault(params UDPMuxParams) *UDPMuxDefault { return newBufferHolder(receiveMTU + maxAddrSize) }, }, - localAddrsForUnspecified: localAddrsForUnspecified, } + + mux.updateLocalAddresses() + return mux +} + +func (m *UDPMuxDefault) updateLocalAddresses() { + var localAddrsForUnspecified []net.Addr + if addr, ok := m.params.UDPConn.LocalAddr().(*net.UDPAddr); !ok { + m.params.Logger.Errorf("LocalAddr is not a net.UDPAddr, got %T", m.params.UDPConn.LocalAddr()) + } else if ok && addr.IP.IsUnspecified() { + // For unspecified addresses, the correct behavior is to return errListenUnspecified, but + // it will break the applications that are already using unspecified UDP connection + // with UDPMuxDefault, so print a warn log and create a local address list for mux. + m.params.Logger.Warn("UDPMuxDefault should not listening on unspecified address, use NewMultiUDPMuxFromPort instead") + var networks []ice.NetworkType + switch { + + case addr.IP.To16() != nil: + networks = []ice.NetworkType{ice.NetworkTypeUDP4, ice.NetworkTypeUDP6} + + case addr.IP.To4() != nil: + networks = []ice.NetworkType{ice.NetworkTypeUDP4} + + default: + m.params.Logger.Errorf("LocalAddr expected IPV4 or IPV6, got %T", m.params.UDPConn.LocalAddr()) + } + if len(networks) > 0 { + if m.params.Net == nil { + var err error + if m.params.Net, err = stdnet.NewNet(); err != nil { + m.params.Logger.Errorf("failed to get create network: %v", err) + } + } + + ips, err := localInterfaces(m.params.Net, m.params.InterfaceFilter, nil, networks, true) + if err == nil { + for _, ip := range ips { + localAddrsForUnspecified = append(localAddrsForUnspecified, &net.UDPAddr{IP: ip, Port: addr.Port}) + } + } else { + m.params.Logger.Errorf("failed to get local interfaces for unspecified addr: %v", err) + } + } + } + + m.mu.Lock() + m.localAddrsForUnspecified = localAddrsForUnspecified + m.mu.Unlock() } // LocalAddr returns the listening address of this UDPMuxDefault @@ -214,8 +223,12 @@ func (m *UDPMuxDefault) LocalAddr() net.Addr { // GetListenAddresses returns the list of addresses that this mux is listening on func (m *UDPMuxDefault) GetListenAddresses() []net.Addr { + m.updateLocalAddresses() + + m.mu.Lock() + defer m.mu.Unlock() if len(m.localAddrsForUnspecified) > 0 { - return m.localAddrsForUnspecified + return slices.Clone(m.localAddrsForUnspecified) } return []net.Addr{m.LocalAddr()} @@ -225,7 +238,10 @@ func (m *UDPMuxDefault) GetListenAddresses() []net.Addr { // creates the connection if an existing one can't be found func (m *UDPMuxDefault) GetConn(ufrag string, addr net.Addr) (net.PacketConn, error) { // don't check addr for mux using unspecified address - if len(m.localAddrsForUnspecified) == 0 && m.params.UDPConn.LocalAddr().String() != addr.String() { + m.mu.Lock() + lenLocalAddrs := len(m.localAddrsForUnspecified) + m.mu.Unlock() + if lenLocalAddrs == 0 && m.params.UDPConn.LocalAddr().String() != addr.String() { return nil, fmt.Errorf("invalid address %s", addr.String()) } diff --git a/client/iface/configurer/name.go b/client/iface/configurer/name.go index e2133d0ea..3b9abc0e8 100644 --- a/client/iface/configurer/name.go +++ b/client/iface/configurer/name.go @@ -2,5 +2,5 @@ package configurer -// WgInterfaceDefault is a default interface name of Wiretrustee +// WgInterfaceDefault is a default interface name of Netbird const WgInterfaceDefault = "wt0" diff --git a/client/iface/configurer/name_darwin.go b/client/iface/configurer/name_darwin.go index 034ce388d..eaf04fc2d 100644 --- a/client/iface/configurer/name_darwin.go +++ b/client/iface/configurer/name_darwin.go @@ -2,5 +2,5 @@ package configurer -// WgInterfaceDefault is a default interface name of Wiretrustee +// WgInterfaceDefault is a default interface name of Netbird const WgInterfaceDefault = "utun100" diff --git a/client/iface/configurer/usp.go b/client/iface/configurer/usp.go index 21d65ab2a..391269dd0 100644 --- a/client/iface/configurer/usp.go +++ b/client/iface/configurer/usp.go @@ -362,7 +362,7 @@ func toWgUserspaceString(wgCfg wgtypes.Config) string { } func getFwmark() int { - if runtime.GOOS == "linux" && !nbnet.CustomRoutingDisabled() { + if nbnet.AdvancedRouting() { return nbnet.NetbirdFwmark } return 0 diff --git a/client/iface/device.go b/client/iface/device.go index 0d4e69145..86e9dab4b 100644 --- a/client/iface/device.go +++ b/client/iface/device.go @@ -3,6 +3,10 @@ package iface import ( + "golang.zx2c4.com/wireguard/tun/netstack" + + wgdevice "golang.zx2c4.com/wireguard/device" + "github.com/netbirdio/netbird/client/iface/bind" "github.com/netbirdio/netbird/client/iface/device" ) @@ -15,4 +19,6 @@ type WGTunDevice interface { DeviceName() string Close() error FilteredDevice() *device.FilteredDevice + Device() *wgdevice.Device + GetNet() *netstack.Net } diff --git a/client/iface/device/device_android.go b/client/iface/device/device_android.go index fac2ba63d..55081e181 100644 --- a/client/iface/device/device_android.go +++ b/client/iface/device/device_android.go @@ -9,6 +9,7 @@ import ( "golang.org/x/sys/unix" "golang.zx2c4.com/wireguard/device" "golang.zx2c4.com/wireguard/tun" + "golang.zx2c4.com/wireguard/tun/netstack" "github.com/netbirdio/netbird/client/iface/bind" "github.com/netbirdio/netbird/client/iface/configurer" @@ -63,7 +64,7 @@ func (t *WGTunDevice) Create(routes []string, dns string, searchDomains []string t.filteredDevice = newDeviceFilter(tunDevice) log.Debugf("attaching to interface %v", name) - t.device = device.NewDevice(t.filteredDevice, t.iceBind, device.NewLogger(wgLogLevel(), "[wiretrustee] ")) + t.device = device.NewDevice(t.filteredDevice, t.iceBind, device.NewLogger(wgLogLevel(), "[netbird] ")) // without this property mobile devices can discover remote endpoints if the configured one was wrong. // this helps with support for the older NetBird clients that had a hardcoded direct mode // t.device.DisableSomeRoamingForBrokenMobileSemantics() @@ -130,6 +131,10 @@ func (t *WGTunDevice) FilteredDevice() *FilteredDevice { return t.filteredDevice } +func (t *WGTunDevice) GetNet() *netstack.Net { + return nil +} + func routesToString(routes []string) string { return strings.Join(routes, ";") } diff --git a/client/iface/device/device_darwin.go b/client/iface/device/device_darwin.go index b5a128bc1..1a5635ff2 100644 --- a/client/iface/device/device_darwin.go +++ b/client/iface/device/device_darwin.go @@ -9,6 +9,7 @@ import ( log "github.com/sirupsen/logrus" "golang.zx2c4.com/wireguard/device" "golang.zx2c4.com/wireguard/tun" + "golang.zx2c4.com/wireguard/tun/netstack" "github.com/netbirdio/netbird/client/iface/bind" "github.com/netbirdio/netbird/client/iface/configurer" @@ -117,6 +118,11 @@ func (t *TunDevice) FilteredDevice() *FilteredDevice { return t.filteredDevice } +// Device returns the wireguard device +func (t *TunDevice) Device() *device.Device { + return t.device +} + // assignAddr Adds IP address to the tunnel interface and network route based on the range provided func (t *TunDevice) assignAddr() error { cmd := exec.Command("ifconfig", t.name, "inet", t.address.IP.String(), t.address.IP.String()) @@ -138,3 +144,7 @@ func (t *TunDevice) assignAddr() error { } return nil } + +func (t *TunDevice) GetNet() *netstack.Net { + return nil +} diff --git a/client/iface/device/device_ios.go b/client/iface/device/device_ios.go index b9591e0b8..b106d475c 100644 --- a/client/iface/device/device_ios.go +++ b/client/iface/device/device_ios.go @@ -10,6 +10,7 @@ import ( "golang.org/x/sys/unix" "golang.zx2c4.com/wireguard/device" "golang.zx2c4.com/wireguard/tun" + "golang.zx2c4.com/wireguard/tun/netstack" "github.com/netbirdio/netbird/client/iface/bind" "github.com/netbirdio/netbird/client/iface/configurer" @@ -64,7 +65,7 @@ func (t *TunDevice) Create() (WGConfigurer, error) { t.filteredDevice = newDeviceFilter(tunDevice) log.Debug("Attaching to interface") - t.device = device.NewDevice(t.filteredDevice, t.iceBind, device.NewLogger(wgLogLevel(), "[wiretrustee] ")) + t.device = device.NewDevice(t.filteredDevice, t.iceBind, device.NewLogger(wgLogLevel(), "[netbird] ")) // without this property mobile devices can discover remote endpoints if the configured one was wrong. // this helps with support for the older NetBird clients that had a hardcoded direct mode // t.device.DisableSomeRoamingForBrokenMobileSemantics() @@ -131,3 +132,7 @@ func (t *TunDevice) UpdateAddr(addr WGAddress) error { func (t *TunDevice) FilteredDevice() *FilteredDevice { return t.filteredDevice } + +func (t *TunDevice) GetNet() *netstack.Net { + return nil +} diff --git a/client/iface/device/device_kernel_unix.go b/client/iface/device/device_kernel_unix.go index f355d2cf7..fe1d1147f 100644 --- a/client/iface/device/device_kernel_unix.go +++ b/client/iface/device/device_kernel_unix.go @@ -9,6 +9,8 @@ import ( "github.com/pion/transport/v3" log "github.com/sirupsen/logrus" + "golang.zx2c4.com/wireguard/device" + "golang.zx2c4.com/wireguard/tun/netstack" "github.com/netbirdio/netbird/client/iface/bind" "github.com/netbirdio/netbird/client/iface/configurer" @@ -33,8 +35,6 @@ type TunKernelDevice struct { } func NewKernelDevice(name string, address WGAddress, wgPort int, key string, mtu int, transportNet transport.Net) *TunKernelDevice { - checkUser() - ctx, cancel := context.WithCancel(context.Background()) return &TunKernelDevice{ ctx: ctx, @@ -153,6 +153,11 @@ func (t *TunKernelDevice) DeviceName() string { return t.name } +// Device returns the wireguard device, not applicable for kernel devices +func (t *TunKernelDevice) Device() *device.Device { + return nil +} + func (t *TunKernelDevice) FilteredDevice() *FilteredDevice { return nil } @@ -161,3 +166,7 @@ func (t *TunKernelDevice) FilteredDevice() *FilteredDevice { func (t *TunKernelDevice) assignAddr() error { return t.link.assignAddr(t.address) } + +func (t *TunKernelDevice) GetNet() *netstack.Net { + return nil +} diff --git a/client/iface/device/device_netstack.go b/client/iface/device/device_netstack.go index f5d39e9e0..0cb02fd19 100644 --- a/client/iface/device/device_netstack.go +++ b/client/iface/device/device_netstack.go @@ -8,10 +8,12 @@ import ( log "github.com/sirupsen/logrus" "golang.zx2c4.com/wireguard/device" + "golang.zx2c4.com/wireguard/tun/netstack" "github.com/netbirdio/netbird/client/iface/bind" "github.com/netbirdio/netbird/client/iface/configurer" - "github.com/netbirdio/netbird/client/iface/netstack" + nbnetstack "github.com/netbirdio/netbird/client/iface/netstack" + nbnet "github.com/netbirdio/netbird/util/net" ) type TunNetstackDevice struct { @@ -25,9 +27,11 @@ type TunNetstackDevice struct { device *device.Device filteredDevice *FilteredDevice - nsTun *netstack.NetStackTun + nsTun *nbnetstack.NetStackTun udpMux *bind.UniversalUDPMuxDefault configurer WGConfigurer + + net *netstack.Net } func NewNetstackDevice(name string, address WGAddress, wgPort int, key string, mtu int, iceBind *bind.ICEBind, listenAddress string) *TunNetstackDevice { @@ -43,13 +47,19 @@ func NewNetstackDevice(name string, address WGAddress, wgPort int, key string, m } func (t *TunNetstackDevice) Create() (WGConfigurer, error) { - log.Info("create netstack tun interface") - t.nsTun = netstack.NewNetStackTun(t.listenAddress, t.address.IP.String(), t.mtu) - tunIface, err := t.nsTun.Create() + log.Info("create nbnetstack tun interface") + + // TODO: get from service listener runtime IP + dnsAddr := nbnet.GetLastIPFromNetwork(t.address.Network, 1) + log.Debugf("netstack using address: %s", t.address.IP) + t.nsTun = nbnetstack.NewNetStackTun(t.listenAddress, t.address.IP, dnsAddr, t.mtu) + log.Debugf("netstack using dns address: %s", dnsAddr) + tunIface, net, err := t.nsTun.Create() if err != nil { return nil, fmt.Errorf("error creating tun device: %s", err) } t.filteredDevice = newDeviceFilter(tunIface) + t.net = net t.device = device.NewDevice( t.filteredDevice, @@ -117,3 +127,12 @@ func (t *TunNetstackDevice) DeviceName() string { func (t *TunNetstackDevice) FilteredDevice() *FilteredDevice { return t.filteredDevice } + +// Device returns the wireguard device +func (t *TunNetstackDevice) Device() *device.Device { + return t.device +} + +func (t *TunNetstackDevice) GetNet() *netstack.Net { + return t.net +} diff --git a/client/iface/device/device_usp_unix.go b/client/iface/device/device_usp_unix.go index 643d77565..07570617a 100644 --- a/client/iface/device/device_usp_unix.go +++ b/client/iface/device/device_usp_unix.go @@ -4,12 +4,11 @@ package device import ( "fmt" - "os" - "runtime" log "github.com/sirupsen/logrus" "golang.zx2c4.com/wireguard/device" "golang.zx2c4.com/wireguard/tun" + "golang.zx2c4.com/wireguard/tun/netstack" "github.com/netbirdio/netbird/client/iface/bind" "github.com/netbirdio/netbird/client/iface/configurer" @@ -32,8 +31,6 @@ type USPDevice struct { func NewUSPDevice(name string, address WGAddress, port int, key string, mtu int, iceBind *bind.ICEBind) *USPDevice { log.Infof("using userspace bind mode") - checkUser() - return &USPDevice{ name: name, address: address, @@ -128,6 +125,11 @@ func (t *USPDevice) FilteredDevice() *FilteredDevice { return t.filteredDevice } +// Device returns the wireguard device +func (t *USPDevice) Device() *device.Device { + return t.device +} + // assignAddr Adds IP address to the tunnel interface func (t *USPDevice) assignAddr() error { link := newWGLink(t.name) @@ -135,11 +137,6 @@ func (t *USPDevice) assignAddr() error { return link.assignAddr(t.address) } -func checkUser() { - if runtime.GOOS == "freebsd" { - euid := os.Geteuid() - if euid != 0 { - log.Warn("newTunUSPDevice: on netbird must run as root to be able to assign address to the tun interface with ifconfig") - } - } +func (t *USPDevice) GetNet() *netstack.Net { + return nil } diff --git a/client/iface/device/device_windows.go b/client/iface/device/device_windows.go index 86968d06d..0fd1b3326 100644 --- a/client/iface/device/device_windows.go +++ b/client/iface/device/device_windows.go @@ -8,6 +8,7 @@ import ( "golang.org/x/sys/windows" "golang.zx2c4.com/wireguard/device" "golang.zx2c4.com/wireguard/tun" + "golang.zx2c4.com/wireguard/tun/netstack" "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" "github.com/netbirdio/netbird/client/iface/bind" @@ -150,6 +151,11 @@ func (t *TunDevice) FilteredDevice() *FilteredDevice { return t.filteredDevice } +// Device returns the wireguard device +func (t *TunDevice) Device() *device.Device { + return t.device +} + func (t *TunDevice) GetInterfaceGUIDString() (string, error) { if t.nativeTunDevice == nil { return "", fmt.Errorf("interface has not been initialized yet") @@ -169,3 +175,7 @@ func (t *TunDevice) assignAddr() error { log.Debugf("adding address %s to interface: %s", t.address.IP, t.name) return luid.SetIPAddresses([]netip.Prefix{netip.MustParsePrefix(t.address.String())}) } + +func (t *TunDevice) GetNet() *netstack.Net { + return nil +} diff --git a/client/iface/device_android.go b/client/iface/device_android.go index 3d15080ff..5cbeb70f8 100644 --- a/client/iface/device_android.go +++ b/client/iface/device_android.go @@ -1,6 +1,10 @@ package iface import ( + wgdevice "golang.zx2c4.com/wireguard/device" + + "golang.zx2c4.com/wireguard/tun/netstack" + "github.com/netbirdio/netbird/client/iface/bind" "github.com/netbirdio/netbird/client/iface/device" ) @@ -13,4 +17,6 @@ type WGTunDevice interface { DeviceName() string Close() error FilteredDevice() *device.FilteredDevice + Device() *wgdevice.Device + GetNet() *netstack.Net } diff --git a/client/iface/freebsd/link.go b/client/iface/freebsd/link.go index b7924f04b..e28970fa1 100644 --- a/client/iface/freebsd/link.go +++ b/client/iface/freebsd/link.go @@ -203,6 +203,11 @@ func (l *Link) setAddr(ip, netmask string) error { return fmt.Errorf("set interface addr: %w", err) } + cmd = exec.Command("ifconfig", l.name, "inet6", "fe80::/64") + if out, err := cmd.CombinedOutput(); err != nil { + log.Debugf("adding address command '%v' failed with output: %s", cmd.String(), out) + } + return nil } diff --git a/client/iface/iface.go b/client/iface/iface.go index 1fb9c2691..8056dd9a6 100644 --- a/client/iface/iface.go +++ b/client/iface/iface.go @@ -9,8 +9,11 @@ import ( "github.com/hashicorp/go-multierror" "github.com/pion/transport/v3" log "github.com/sirupsen/logrus" + "golang.zx2c4.com/wireguard/tun/netstack" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" + wgdevice "golang.zx2c4.com/wireguard/device" + "github.com/netbirdio/netbird/client/errors" "github.com/netbirdio/netbird/client/iface/bind" "github.com/netbirdio/netbird/client/iface/configurer" @@ -203,6 +206,11 @@ func (w *WGIface) GetDevice() *device.FilteredDevice { return w.tun.FilteredDevice() } +// GetWGDevice returns the WireGuard device +func (w *WGIface) GetWGDevice() *wgdevice.Device { + return w.tun.Device() +} + // GetStats returns the last handshake time, rx and tx bytes for the given peer func (w *WGIface) GetStats(peerKey string) (configurer.WGStats, error) { return w.configurer.GetStats(peerKey) @@ -234,3 +242,11 @@ func (w *WGIface) waitUntilRemoved() error { } } } + +// GetNet returns the netstack.Net for the netstack device +func (w *WGIface) GetNet() *netstack.Net { + w.mu.Lock() + defer w.mu.Unlock() + + return w.tun.GetNet() +} diff --git a/client/iface/iface_moc.go b/client/iface/iface_moc.go deleted file mode 100644 index d91a7224f..000000000 --- a/client/iface/iface_moc.go +++ /dev/null @@ -1,112 +0,0 @@ -package iface - -import ( - "net" - "time" - - "golang.zx2c4.com/wireguard/wgctrl/wgtypes" - - "github.com/netbirdio/netbird/client/iface/bind" - "github.com/netbirdio/netbird/client/iface/configurer" - "github.com/netbirdio/netbird/client/iface/device" - "github.com/netbirdio/netbird/client/iface/wgproxy" -) - -type MockWGIface struct { - CreateFunc func() error - CreateOnAndroidFunc func(routeRange []string, ip string, domains []string) error - IsUserspaceBindFunc func() bool - NameFunc func() string - AddressFunc func() device.WGAddress - ToInterfaceFunc func() *net.Interface - UpFunc func() (*bind.UniversalUDPMuxDefault, error) - UpdateAddrFunc func(newAddr string) error - UpdatePeerFunc func(peerKey string, allowedIps string, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error - RemovePeerFunc func(peerKey string) error - AddAllowedIPFunc func(peerKey string, allowedIP string) error - RemoveAllowedIPFunc func(peerKey string, allowedIP string) error - CloseFunc func() error - SetFilterFunc func(filter device.PacketFilter) error - GetFilterFunc func() device.PacketFilter - GetDeviceFunc func() *device.FilteredDevice - GetStatsFunc func(peerKey string) (configurer.WGStats, error) - GetInterfaceGUIDStringFunc func() (string, error) - GetProxyFunc func() wgproxy.Proxy -} - -func (m *MockWGIface) GetInterfaceGUIDString() (string, error) { - return m.GetInterfaceGUIDStringFunc() -} - -func (m *MockWGIface) Create() error { - return m.CreateFunc() -} - -func (m *MockWGIface) CreateOnAndroid(routeRange []string, ip string, domains []string) error { - return m.CreateOnAndroidFunc(routeRange, ip, domains) -} - -func (m *MockWGIface) IsUserspaceBind() bool { - return m.IsUserspaceBindFunc() -} - -func (m *MockWGIface) Name() string { - return m.NameFunc() -} - -func (m *MockWGIface) Address() device.WGAddress { - return m.AddressFunc() -} - -func (m *MockWGIface) ToInterface() *net.Interface { - return m.ToInterfaceFunc() -} - -func (m *MockWGIface) Up() (*bind.UniversalUDPMuxDefault, error) { - return m.UpFunc() -} - -func (m *MockWGIface) UpdateAddr(newAddr string) error { - return m.UpdateAddrFunc(newAddr) -} - -func (m *MockWGIface) UpdatePeer(peerKey string, allowedIps string, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error { - return m.UpdatePeerFunc(peerKey, allowedIps, keepAlive, endpoint, preSharedKey) -} - -func (m *MockWGIface) RemovePeer(peerKey string) error { - return m.RemovePeerFunc(peerKey) -} - -func (m *MockWGIface) AddAllowedIP(peerKey string, allowedIP string) error { - return m.AddAllowedIPFunc(peerKey, allowedIP) -} - -func (m *MockWGIface) RemoveAllowedIP(peerKey string, allowedIP string) error { - return m.RemoveAllowedIPFunc(peerKey, allowedIP) -} - -func (m *MockWGIface) Close() error { - return m.CloseFunc() -} - -func (m *MockWGIface) SetFilter(filter device.PacketFilter) error { - return m.SetFilterFunc(filter) -} - -func (m *MockWGIface) GetFilter() device.PacketFilter { - return m.GetFilterFunc() -} - -func (m *MockWGIface) GetDevice() *device.FilteredDevice { - return m.GetDeviceFunc() -} - -func (m *MockWGIface) GetStats(peerKey string) (configurer.WGStats, error) { - return m.GetStatsFunc(peerKey) -} - -func (m *MockWGIface) GetProxy() wgproxy.Proxy { - //TODO implement me - panic("implement me") -} diff --git a/client/iface/iwginterface_windows.go b/client/iface/iwginterface_windows.go deleted file mode 100644 index 96eec52a5..000000000 --- a/client/iface/iwginterface_windows.go +++ /dev/null @@ -1,35 +0,0 @@ -package iface - -import ( - "net" - "time" - - "golang.zx2c4.com/wireguard/wgctrl/wgtypes" - - "github.com/netbirdio/netbird/client/iface/bind" - "github.com/netbirdio/netbird/client/iface/configurer" - "github.com/netbirdio/netbird/client/iface/device" - "github.com/netbirdio/netbird/client/iface/wgproxy" -) - -type IWGIface interface { - Create() error - CreateOnAndroid(routeRange []string, ip string, domains []string) error - IsUserspaceBind() bool - Name() string - Address() device.WGAddress - ToInterface() *net.Interface - Up() (*bind.UniversalUDPMuxDefault, error) - UpdateAddr(newAddr string) error - GetProxy() wgproxy.Proxy - UpdatePeer(peerKey string, allowedIps string, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error - RemovePeer(peerKey string) error - AddAllowedIP(peerKey string, allowedIP string) error - RemoveAllowedIP(peerKey string, allowedIP string) error - Close() error - SetFilter(filter device.PacketFilter) error - GetFilter() device.PacketFilter - GetDevice() *device.FilteredDevice - GetStats(peerKey string) (configurer.WGStats, error) - GetInterfaceGUIDString() (string, error) -} diff --git a/client/iface/netstack/env.go b/client/iface/netstack/env.go index 09889a57e..cdbf975b1 100644 --- a/client/iface/netstack/env.go +++ b/client/iface/netstack/env.go @@ -8,9 +8,11 @@ import ( log "github.com/sirupsen/logrus" ) +const EnvUseNetstackMode = "NB_USE_NETSTACK_MODE" + // IsEnabled todo: move these function to cmd layer func IsEnabled() bool { - return os.Getenv("NB_USE_NETSTACK_MODE") == "true" + return os.Getenv(EnvUseNetstackMode) == "true" } func ListenAddr() string { diff --git a/client/iface/netstack/tun.go b/client/iface/netstack/tun.go index c180e4ef5..01f19875e 100644 --- a/client/iface/netstack/tun.go +++ b/client/iface/netstack/tun.go @@ -1,15 +1,22 @@ package netstack import ( + "fmt" + "net" "net/netip" + "os" + "strconv" log "github.com/sirupsen/logrus" "golang.zx2c4.com/wireguard/tun" "golang.zx2c4.com/wireguard/tun/netstack" ) +const EnvSkipProxy = "NB_NETSTACK_SKIP_PROXY" + type NetStackTun struct { //nolint:revive - address string + address net.IP + dnsAddress net.IP mtu int listenAddress string @@ -17,29 +24,48 @@ type NetStackTun struct { //nolint:revive tundev tun.Device } -func NewNetStackTun(listenAddress string, address string, mtu int) *NetStackTun { +func NewNetStackTun(listenAddress string, address net.IP, dnsAddress net.IP, mtu int) *NetStackTun { return &NetStackTun{ address: address, + dnsAddress: dnsAddress, mtu: mtu, listenAddress: listenAddress, } } -func (t *NetStackTun) Create() (tun.Device, error) { +func (t *NetStackTun) Create() (tun.Device, *netstack.Net, error) { + addr, ok := netip.AddrFromSlice(t.address) + if !ok { + return nil, nil, fmt.Errorf("convert address to netip.Addr: %v", t.address) + } + + dnsAddr, ok := netip.AddrFromSlice(t.dnsAddress) + if !ok { + return nil, nil, fmt.Errorf("convert dns address to netip.Addr: %v", t.dnsAddress) + } + nsTunDev, tunNet, err := netstack.CreateNetTUN( - []netip.Addr{netip.MustParseAddr(t.address)}, - []netip.Addr{}, + []netip.Addr{addr.Unmap()}, + []netip.Addr{dnsAddr.Unmap()}, t.mtu) if err != nil { - return nil, err + return nil, nil, err } t.tundev = nsTunDev + skipProxy, err := strconv.ParseBool(os.Getenv(EnvSkipProxy)) + if err != nil { + log.Errorf("failed to parse NB_ETSTACK_SKIP_PROXY: %s", err) + } + if skipProxy { + return nsTunDev, tunNet, nil + } + dialer := NewNSDialer(tunNet) t.proxy, err = NewSocks5(dialer) if err != nil { _ = t.tundev.Close() - return nil, err + return nil, nil, err } go func() { @@ -49,7 +75,7 @@ func (t *NetStackTun) Create() (tun.Device, error) { } }() - return nsTunDev, nil + return nsTunDev, tunNet, nil } func (t *NetStackTun) Close() error { diff --git a/client/internal/acl/manager.go b/client/internal/acl/manager.go index 5bb0905d2..31173a5f7 100644 --- a/client/internal/acl/manager.go +++ b/client/internal/acl/manager.go @@ -151,7 +151,7 @@ func (d *DefaultManager) applyPeerACLs(networkMap *mgmProto.NetworkMap) { d.rollBack(newRulePairs) break } - if len(rules) > 0 { + if len(rulePair) > 0 { d.peerRulesPairs[pairID] = rulePair newRulePairs[pairID] = rulePair } @@ -268,13 +268,16 @@ func (d *DefaultManager) protoRuleToFirewallRule( } var port *firewall.Port - if r.Port != "" { + if !portInfoEmpty(r.PortInfo) { + port = convertPortInfo(r.PortInfo) + } else if r.Port != "" { + // old version of management, single port value, err := strconv.Atoi(r.Port) if err != nil { - return "", nil, fmt.Errorf("invalid port, skipping firewall rule") + return "", nil, fmt.Errorf("invalid port: %w", err) } port = &firewall.Port{ - Values: []int{value}, + Values: []uint16{uint16(value)}, } } @@ -288,6 +291,8 @@ func (d *DefaultManager) protoRuleToFirewallRule( case mgmProto.RuleDirection_IN: rules, err = d.addInRules(ip, protocol, port, action, ipsetName, "") case mgmProto.RuleDirection_OUT: + // TODO: Remove this soon. Outbound rules are obsolete. + // We only maintain this for return traffic (inbound dir) which is now handled by the stateful firewall already rules, err = d.addOutRules(ip, protocol, port, action, ipsetName, "") default: return "", nil, fmt.Errorf("invalid direction, skipping firewall rule") @@ -300,6 +305,22 @@ func (d *DefaultManager) protoRuleToFirewallRule( return ruleID, rules, nil } +func portInfoEmpty(portInfo *mgmProto.PortInfo) bool { + if portInfo == nil { + return true + } + + switch portInfo.GetPortSelection().(type) { + case *mgmProto.PortInfo_Port: + return portInfo.GetPort() == 0 + case *mgmProto.PortInfo_Range_: + r := portInfo.GetRange() + return r == nil || r.Start == 0 || r.End == 0 + default: + return true + } +} + func (d *DefaultManager) addInRules( ip net.IP, protocol firewall.Protocol, @@ -308,25 +329,12 @@ func (d *DefaultManager) addInRules( ipsetName string, comment string, ) ([]firewall.Rule, error) { - var rules []firewall.Rule - rule, err := d.firewall.AddPeerFiltering( - ip, protocol, nil, port, firewall.RuleDirectionIN, action, ipsetName, comment) + rule, err := d.firewall.AddPeerFiltering(ip, protocol, nil, port, action, ipsetName, comment) if err != nil { - return nil, fmt.Errorf("failed to add firewall rule: %v", err) - } - rules = append(rules, rule...) - - if shouldSkipInvertedRule(protocol, port) { - return rules, nil + return nil, fmt.Errorf("add firewall rule: %w", err) } - rule, err = d.firewall.AddPeerFiltering( - ip, protocol, port, nil, firewall.RuleDirectionOUT, action, ipsetName, comment) - if err != nil { - return nil, fmt.Errorf("failed to add firewall rule: %v", err) - } - - return append(rules, rule...), nil + return rule, nil } func (d *DefaultManager) addOutRules( @@ -337,25 +345,16 @@ func (d *DefaultManager) addOutRules( ipsetName string, comment string, ) ([]firewall.Rule, error) { - var rules []firewall.Rule - rule, err := d.firewall.AddPeerFiltering( - ip, protocol, nil, port, firewall.RuleDirectionOUT, action, ipsetName, comment) - if err != nil { - return nil, fmt.Errorf("failed to add firewall rule: %v", err) - } - rules = append(rules, rule...) - if shouldSkipInvertedRule(protocol, port) { - return rules, nil + return nil, nil } - rule, err = d.firewall.AddPeerFiltering( - ip, protocol, port, nil, firewall.RuleDirectionIN, action, ipsetName, comment) + rule, err := d.firewall.AddPeerFiltering(ip, protocol, port, nil, action, ipsetName, comment) if err != nil { - return nil, fmt.Errorf("failed to add firewall rule: %v", err) + return nil, fmt.Errorf("add firewall rule: %w", err) } - return append(rules, rule...), nil + return rule, nil } // getPeerRuleID() returns unique ID for the rule based on its parameters. @@ -508,7 +507,7 @@ func (d *DefaultManager) squashAcceptRules( // getRuleGroupingSelector takes all rule properties except IP address to build selector func (d *DefaultManager) getRuleGroupingSelector(rule *mgmProto.FirewallRule) string { - return fmt.Sprintf("%v:%v:%v:%s", strconv.Itoa(int(rule.Direction)), rule.Action, rule.Protocol, rule.Port) + return fmt.Sprintf("%v:%v:%v:%s:%v", strconv.Itoa(int(rule.Direction)), rule.Action, rule.Protocol, rule.Port, rule.PortInfo) } func (d *DefaultManager) rollBack(newRulePairs map[id.RuleID][]firewall.Rule) { @@ -559,14 +558,14 @@ func convertPortInfo(portInfo *mgmProto.PortInfo) *firewall.Port { if portInfo.GetPort() != 0 { return &firewall.Port{ - Values: []int{int(portInfo.GetPort())}, + Values: []uint16{uint16(int(portInfo.GetPort()))}, } } if portInfo.GetRange() != nil { return &firewall.Port{ IsRange: true, - Values: []int{int(portInfo.GetRange().Start), int(portInfo.GetRange().End)}, + Values: []uint16{uint16(portInfo.GetRange().Start), uint16(portInfo.GetRange().End)}, } } diff --git a/client/internal/acl/manager_test.go b/client/internal/acl/manager_test.go index 9a766021a..217dbce9f 100644 --- a/client/internal/acl/manager_test.go +++ b/client/internal/acl/manager_test.go @@ -49,9 +49,10 @@ func TestDefaultManager(t *testing.T) { IP: ip, Network: network, }).AnyTimes() + ifaceMock.EXPECT().GetWGDevice().Return(nil).AnyTimes() // we receive one rule from the management so for testing purposes ignore it - fw, err := firewall.NewFirewall(ifaceMock, nil) + fw, err := firewall.NewFirewall(ifaceMock, nil, false) if err != nil { t.Errorf("create firewall: %v", err) return @@ -119,8 +120,8 @@ func TestDefaultManager(t *testing.T) { networkMap.FirewallRulesIsEmpty = false acl.ApplyFiltering(networkMap) - if len(acl.peerRulesPairs) != 2 { - t.Errorf("rules should contain 2 rules if FirewallRulesIsEmpty is not set, got: %v", len(acl.peerRulesPairs)) + if len(acl.peerRulesPairs) != 1 { + t.Errorf("rules should contain 1 rules if FirewallRulesIsEmpty is not set, got: %v", len(acl.peerRulesPairs)) return } }) @@ -342,9 +343,10 @@ func TestDefaultManagerEnableSSHRules(t *testing.T) { IP: ip, Network: network, }).AnyTimes() + ifaceMock.EXPECT().GetWGDevice().Return(nil).AnyTimes() // we receive one rule from the management so for testing purposes ignore it - fw, err := firewall.NewFirewall(ifaceMock, nil) + fw, err := firewall.NewFirewall(ifaceMock, nil, false) if err != nil { t.Errorf("create firewall: %v", err) return @@ -356,8 +358,8 @@ func TestDefaultManagerEnableSSHRules(t *testing.T) { acl.ApplyFiltering(networkMap) - if len(acl.peerRulesPairs) != 4 { - t.Errorf("expect 4 rules (last must be SSH), got: %d", len(acl.peerRulesPairs)) + if len(acl.peerRulesPairs) != 3 { + t.Errorf("expect 3 rules (last must be SSH), got: %d", len(acl.peerRulesPairs)) return } } diff --git a/client/internal/acl/mocks/iface_mapper.go b/client/internal/acl/mocks/iface_mapper.go index 3ed12b6dd..08aa4fd5a 100644 --- a/client/internal/acl/mocks/iface_mapper.go +++ b/client/internal/acl/mocks/iface_mapper.go @@ -8,6 +8,8 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" + wgdevice "golang.zx2c4.com/wireguard/device" + iface "github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/iface/device" ) @@ -90,3 +92,31 @@ func (mr *MockIFaceMapperMockRecorder) SetFilter(arg0 interface{}) *gomock.Call mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetFilter", reflect.TypeOf((*MockIFaceMapper)(nil).SetFilter), arg0) } + +// GetDevice mocks base method. +func (m *MockIFaceMapper) GetDevice() *device.FilteredDevice { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDevice") + ret0, _ := ret[0].(*device.FilteredDevice) + return ret0 +} + +// GetDevice indicates an expected call of GetDevice. +func (mr *MockIFaceMapperMockRecorder) GetDevice() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDevice", reflect.TypeOf((*MockIFaceMapper)(nil).GetDevice)) +} + +// GetWGDevice mocks base method. +func (m *MockIFaceMapper) GetWGDevice() *wgdevice.Device { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetWGDevice") + ret0, _ := ret[0].(*wgdevice.Device) + return ret0 +} + +// GetWGDevice indicates an expected call of GetWGDevice. +func (mr *MockIFaceMapperMockRecorder) GetWGDevice() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWGDevice", reflect.TypeOf((*MockIFaceMapper)(nil).GetWGDevice)) +} diff --git a/client/internal/auth/device_flow.go b/client/internal/auth/device_flow.go index 87d00de5e..da4f16c8d 100644 --- a/client/internal/auth/device_flow.go +++ b/client/internal/auth/device_flow.go @@ -2,6 +2,8 @@ package auth import ( "context" + "crypto/tls" + "crypto/x509" "encoding/json" "errors" "fmt" @@ -11,7 +13,10 @@ import ( "strings" "time" + log "github.com/sirupsen/logrus" + "github.com/netbirdio/netbird/client/internal" + "github.com/netbirdio/netbird/util/embeddedroots" ) // HostedGrantType grant type for device flow on Hosted @@ -56,6 +61,18 @@ func NewDeviceAuthorizationFlow(config internal.DeviceAuthProviderConfig) (*Devi httpTransport := http.DefaultTransport.(*http.Transport).Clone() httpTransport.MaxIdleConns = 5 + certPool, err := x509.SystemCertPool() + if err != nil || certPool == nil { + log.Debugf("System cert pool not available; falling back to embedded cert, error: %v", err) + certPool = embeddedroots.Get() + } else { + log.Debug("Using system certificate pool.") + } + + httpTransport.TLSClientConfig = &tls.Config{ + RootCAs: certPool, + } + httpClient := &http.Client{ Timeout: 10 * time.Second, Transport: httpTransport, diff --git a/client/internal/config.go b/client/internal/config.go index 594bdc570..b2f96cbdc 100644 --- a/client/internal/config.go +++ b/client/internal/config.go @@ -8,6 +8,7 @@ import ( "os" "reflect" "runtime" + "slices" "strings" "time" @@ -20,6 +21,7 @@ import ( "github.com/netbirdio/netbird/client/internal/routemanager/dynamic" "github.com/netbirdio/netbird/client/ssh" mgm "github.com/netbirdio/netbird/management/client" + "github.com/netbirdio/netbird/management/domain" "github.com/netbirdio/netbird/util" ) @@ -66,6 +68,12 @@ type ConfigInput struct { DisableServerRoutes *bool DisableDNS *bool DisableFirewall *bool + + BlockLANAccess *bool + + DisableNotifications *bool + + DNSLabels domain.List } // Config Configuration type @@ -89,6 +97,12 @@ type Config struct { DisableDNS bool DisableFirewall bool + BlockLANAccess bool + + DisableNotifications *bool + + DNSLabels domain.List + // SSHKey is a private SSH key in a PEM format SSHKey string @@ -455,6 +469,33 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) { updated = true } + if input.BlockLANAccess != nil && *input.BlockLANAccess != config.BlockLANAccess { + if *input.BlockLANAccess { + log.Infof("blocking LAN access") + } else { + log.Infof("allowing LAN access") + } + config.BlockLANAccess = *input.BlockLANAccess + updated = true + } + + if input.DisableNotifications != nil && input.DisableNotifications != config.DisableNotifications { + if *input.DisableNotifications { + log.Infof("disabling notifications") + } else { + log.Infof("enabling notifications") + } + config.DisableNotifications = input.DisableNotifications + updated = true + } + + if config.DisableNotifications == nil { + disabled := true + config.DisableNotifications = &disabled + log.Infof("setting notifications to disabled by default") + updated = true + } + if input.ClientCertKeyPath != "" { config.ClientCertKeyPath = input.ClientCertKeyPath updated = true @@ -475,6 +516,14 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) { } } + if input.DNSLabels != nil && !slices.Equal(config.DNSLabels, input.DNSLabels) { + log.Infof("updating DNS labels [ %s ] (old value: [ %s ])", + input.DNSLabels.SafeString(), + config.DNSLabels.SafeString()) + config.DNSLabels = input.DNSLabels + updated = true + } + return updated, nil } diff --git a/client/internal/connect.go b/client/internal/connect.go index afd1f4454..bf513ed39 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -23,6 +23,7 @@ import ( "github.com/netbirdio/netbird/client/internal/listener" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/stdnet" + cProto "github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/ssh" "github.com/netbirdio/netbird/client/system" mgm "github.com/netbirdio/netbird/management/client" @@ -31,6 +32,7 @@ import ( relayClient "github.com/netbirdio/netbird/relay/client" signal "github.com/netbirdio/netbird/signal/client" "github.com/netbirdio/netbird/util" + nbnet "github.com/netbirdio/netbird/util/net" "github.com/netbirdio/netbird/version" ) @@ -59,13 +61,8 @@ func NewConnectClient( } // Run with main logic. -func (c *ConnectClient) Run() error { - return c.run(MobileDependency{}, nil, nil) -} - -// RunWithProbes runs the client's main logic with probes attached -func (c *ConnectClient) RunWithProbes(probes *ProbeHolder, runningChan chan error) error { - return c.run(MobileDependency{}, probes, runningChan) +func (c *ConnectClient) Run(runningChan chan error) error { + return c.run(MobileDependency{}, runningChan) } // RunOnAndroid with main logic on mobile system @@ -84,7 +81,7 @@ func (c *ConnectClient) RunOnAndroid( HostDNSAddresses: dnsAddresses, DnsReadyListener: dnsReadyListener, } - return c.run(mobileDependency, nil, nil) + return c.run(mobileDependency, nil) } func (c *ConnectClient) RunOniOS( @@ -102,18 +99,30 @@ func (c *ConnectClient) RunOniOS( DnsManager: dnsManager, StateFilePath: stateFilePath, } - return c.run(mobileDependency, nil, nil) + return c.run(mobileDependency, nil) } -func (c *ConnectClient) run(mobileDependency MobileDependency, probes *ProbeHolder, runningChan chan error) error { +func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan error) error { defer func() { if r := recover(); r != nil { + rec := c.statusRecorder + if rec != nil { + rec.PublishEvent( + cProto.SystemEvent_CRITICAL, cProto.SystemEvent_SYSTEM, + "panic occurred", + "The Netbird service panicked. Please restart the service and submit a bug report with the client logs.", + nil, + ) + } + log.Panicf("Panic occurred: %v, stack trace: %s", r, string(debug.Stack())) } }() log.Infof("starting NetBird client version %s on %s/%s", version.NetbirdVersion(), runtime.GOOS, runtime.GOARCH) + nbnet.Init() + backOff := &backoff.ExponentialBackOff{ InitialInterval: time.Second, RandomizationFactor: 1, @@ -182,8 +191,8 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, probes *ProbeHold } }() - // connect (just a connection, no stream yet) and login to Management Service to get an initial global Wiretrustee config - loginResp, err := loginToManagement(engineCtx, mgmClient, publicSSHKey) + // connect (just a connection, no stream yet) and login to Management Service to get an initial global Netbird config + loginResp, err := loginToManagement(engineCtx, mgmClient, publicSSHKey, c.config) if err != nil { log.Debug(err) if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.PermissionDenied) { @@ -204,8 +213,8 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, probes *ProbeHold c.statusRecorder.UpdateLocalPeerState(localPeerState) signalURL := fmt.Sprintf("%s://%s", - strings.ToLower(loginResp.GetWiretrusteeConfig().GetSignal().GetProtocol().String()), - loginResp.GetWiretrusteeConfig().GetSignal().GetUri(), + strings.ToLower(loginResp.GetNetbirdConfig().GetSignal().GetProtocol().String()), + loginResp.GetNetbirdConfig().GetSignal().GetUri(), ) c.statusRecorder.UpdateSignalAddress(signalURL) @@ -216,8 +225,8 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, probes *ProbeHold c.statusRecorder.MarkSignalDisconnected(err) }() - // with the global Wiretrustee config in hand connect (just a connection, no stream yet) Signal - signalClient, err := connectToSignal(engineCtx, loginResp.GetWiretrusteeConfig(), myPrivateKey) + // with the global Netbird config in hand connect (just a connection, no stream yet) Signal + signalClient, err := connectToSignal(engineCtx, loginResp.GetNetbirdConfig(), myPrivateKey) if err != nil { log.Error(err) return wrapErr(err) @@ -261,7 +270,7 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, probes *ProbeHold checks := loginResp.GetChecks() c.engineMutex.Lock() - c.engine = NewEngineWithProbes(engineCtx, cancel, signalClient, mgmClient, relayManager, engineConfig, mobileDependency, c.statusRecorder, probes, checks) + c.engine = NewEngine(engineCtx, cancel, signalClient, mgmClient, relayManager, engineConfig, mobileDependency, c.statusRecorder, checks) c.engine.SetNetworkMapPersistence(c.persistNetworkMap) c.engineMutex.Unlock() @@ -316,7 +325,7 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, probes *ProbeHold } func parseRelayInfo(loginResp *mgmProto.LoginResponse) ([]string, *hmac.Token) { - relayCfg := loginResp.GetWiretrusteeConfig().GetRelay() + relayCfg := loginResp.GetNetbirdConfig().GetRelay() if relayCfg == nil { return nil, nil } @@ -420,6 +429,8 @@ func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.Pe DisableServerRoutes: config.DisableServerRoutes, DisableDNS: config.DisableDNS, DisableFirewall: config.DisableFirewall, + + BlockLANAccess: config.BlockLANAccess, } if config.PreSharedKey != "" { @@ -443,7 +454,7 @@ func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.Pe } // connectToSignal creates Signal Service client and established a connection -func connectToSignal(ctx context.Context, wtConfig *mgmProto.WiretrusteeConfig, ourPrivateKey wgtypes.Key) (*signal.GrpcClient, error) { +func connectToSignal(ctx context.Context, wtConfig *mgmProto.NetbirdConfig, ourPrivateKey wgtypes.Key) (*signal.GrpcClient, error) { var sigTLSEnabled bool if wtConfig.Signal.Protocol == mgmProto.HostConfig_HTTPS { sigTLSEnabled = true @@ -460,8 +471,8 @@ func connectToSignal(ctx context.Context, wtConfig *mgmProto.WiretrusteeConfig, return signalClient, nil } -// loginToManagement creates Management Services client, establishes a connection, logs-in and gets a global Wiretrustee config (signal, turn, stun hosts, etc) -func loginToManagement(ctx context.Context, client mgm.Client, pubSSHKey []byte) (*mgmProto.LoginResponse, error) { +// loginToManagement creates Management Services client, establishes a connection, logs-in and gets a global Netbird config (signal, turn, stun hosts, etc) +func loginToManagement(ctx context.Context, client mgm.Client, pubSSHKey []byte, config *Config) (*mgmProto.LoginResponse, error) { serverPublicKey, err := client.GetServerPublicKey() if err != nil { @@ -469,7 +480,16 @@ func loginToManagement(ctx context.Context, client mgm.Client, pubSSHKey []byte) } sysInfo := system.GetInfo(ctx) - loginResp, err := client.Login(*serverPublicKey, sysInfo, pubSSHKey) + sysInfo.SetFlags( + config.RosenpassEnabled, + config.RosenpassPermissive, + config.ServerSSHAllowed, + config.DisableClientRoutes, + config.DisableServerRoutes, + config.DisableDNS, + config.DisableFirewall, + ) + loginResp, err := client.Login(*serverPublicKey, sysInfo, pubSSHKey, config.DNSLabels) if err != nil { return nil, err } diff --git a/client/internal/dns.go b/client/internal/dns.go new file mode 100644 index 000000000..8a73f50f2 --- /dev/null +++ b/client/internal/dns.go @@ -0,0 +1,111 @@ +package internal + +import ( + "fmt" + "net" + "slices" + "strings" + + "github.com/miekg/dns" + log "github.com/sirupsen/logrus" + + nbdns "github.com/netbirdio/netbird/dns" +) + +func createPTRRecord(aRecord nbdns.SimpleRecord, ipNet *net.IPNet) (nbdns.SimpleRecord, bool) { + ip := net.ParseIP(aRecord.RData) + if ip == nil || ip.To4() == nil { + return nbdns.SimpleRecord{}, false + } + + if !ipNet.Contains(ip) { + return nbdns.SimpleRecord{}, false + } + + ipOctets := strings.Split(ip.String(), ".") + slices.Reverse(ipOctets) + rdnsName := dns.Fqdn(strings.Join(ipOctets, ".") + ".in-addr.arpa") + + return nbdns.SimpleRecord{ + Name: rdnsName, + Type: int(dns.TypePTR), + Class: aRecord.Class, + TTL: aRecord.TTL, + RData: dns.Fqdn(aRecord.Name), + }, true +} + +// generateReverseZoneName creates the reverse DNS zone name for a given network +func generateReverseZoneName(ipNet *net.IPNet) (string, error) { + networkIP := ipNet.IP.Mask(ipNet.Mask) + maskOnes, _ := ipNet.Mask.Size() + + // round up to nearest byte + octetsToUse := (maskOnes + 7) / 8 + + octets := strings.Split(networkIP.String(), ".") + if octetsToUse > len(octets) { + return "", fmt.Errorf("invalid network mask size for reverse DNS: %d", maskOnes) + } + + reverseOctets := make([]string, octetsToUse) + for i := 0; i < octetsToUse; i++ { + reverseOctets[octetsToUse-1-i] = octets[i] + } + + return dns.Fqdn(strings.Join(reverseOctets, ".") + ".in-addr.arpa"), nil +} + +// zoneExists checks if a zone with the given name already exists in the configuration +func zoneExists(config *nbdns.Config, zoneName string) bool { + for _, zone := range config.CustomZones { + if zone.Domain == zoneName { + log.Debugf("reverse DNS zone %s already exists", zoneName) + return true + } + } + return false +} + +// collectPTRRecords gathers all PTR records for the given network from A records +func collectPTRRecords(config *nbdns.Config, ipNet *net.IPNet) []nbdns.SimpleRecord { + var records []nbdns.SimpleRecord + + for _, zone := range config.CustomZones { + for _, record := range zone.Records { + if record.Type != int(dns.TypeA) { + continue + } + + if ptrRecord, ok := createPTRRecord(record, ipNet); ok { + records = append(records, ptrRecord) + } + } + } + + return records +} + +// addReverseZone adds a reverse DNS zone to the configuration for the given network +func addReverseZone(config *nbdns.Config, ipNet *net.IPNet) { + zoneName, err := generateReverseZoneName(ipNet) + if err != nil { + log.Warn(err) + return + } + + if zoneExists(config, zoneName) { + log.Debugf("reverse DNS zone %s already exists", zoneName) + return + } + + records := collectPTRRecords(config, ipNet) + + reverseZone := nbdns.CustomZone{ + Domain: zoneName, + Records: records, + } + + config.CustomZones = append(config.CustomZones, reverseZone) + log.Debugf("added reverse DNS zone: %s with %d records", zoneName, len(records)) +} diff --git a/client/internal/dns/file_unix.go b/client/internal/dns/file_unix.go index 02ae26e10..1f4ddb67c 100644 --- a/client/internal/dns/file_unix.go +++ b/client/internal/dns/file_unix.go @@ -58,7 +58,7 @@ func (f *fileConfigurator) applyDNSConfig(config HostDNSConfig, stateManager *st return fmt.Errorf("restoring the original resolv.conf file return err: %w", err) } } - return fmt.Errorf("unable to configure DNS for this peer using file manager without a nameserver group with all domains configured") + return ErrRouteAllWithoutNameserverGroup } if !backupFileExist { @@ -121,6 +121,10 @@ func (f *fileConfigurator) restoreHostDNS() error { return f.restore() } +func (f *fileConfigurator) string() string { + return "file" +} + func (f *fileConfigurator) backup() error { stats, err := os.Stat(defaultResolvConfPath) if err != nil { diff --git a/client/internal/dns/handler_chain.go b/client/internal/dns/handler_chain.go index 5f63d1ab3..3286daabf 100644 --- a/client/internal/dns/handler_chain.go +++ b/client/internal/dns/handler_chain.go @@ -12,7 +12,7 @@ import ( const ( PriorityDNSRoute = 100 PriorityMatchDomain = 50 - PriorityDefault = 0 + PriorityDefault = 1 ) type SubdomainMatcher interface { @@ -26,7 +26,6 @@ type HandlerEntry struct { Pattern string OrigPattern string IsWildcard bool - StopHandler handlerWithStop MatchSubdomains bool } @@ -64,7 +63,7 @@ func (w *ResponseWriterChain) GetOrigPattern() string { } // AddHandler adds a new handler to the chain, replacing any existing handler with the same pattern and priority -func (c *HandlerChain) AddHandler(pattern string, handler dns.Handler, priority int, stopHandler handlerWithStop) { +func (c *HandlerChain) AddHandler(pattern string, handler dns.Handler, priority int) { c.mu.Lock() defer c.mu.Unlock() @@ -78,9 +77,6 @@ func (c *HandlerChain) AddHandler(pattern string, handler dns.Handler, priority // First remove any existing handler with same pattern (case-insensitive) and priority for i := len(c.handlers) - 1; i >= 0; i-- { if strings.EqualFold(c.handlers[i].OrigPattern, origPattern) && c.handlers[i].Priority == priority { - if c.handlers[i].StopHandler != nil { - c.handlers[i].StopHandler.stop() - } c.handlers = append(c.handlers[:i], c.handlers[i+1:]...) break } @@ -101,21 +97,33 @@ func (c *HandlerChain) AddHandler(pattern string, handler dns.Handler, priority Pattern: pattern, OrigPattern: origPattern, IsWildcard: isWildcard, - StopHandler: stopHandler, MatchSubdomains: matchSubdomains, } - // Insert handler in priority order - pos := 0 + pos := c.findHandlerPosition(entry) + c.handlers = append(c.handlers[:pos], append([]HandlerEntry{entry}, c.handlers[pos:]...)...) +} + +// findHandlerPosition determines where to insert a new handler based on priority and specificity +func (c *HandlerChain) findHandlerPosition(newEntry HandlerEntry) int { for i, h := range c.handlers { - if h.Priority < priority { - pos = i - break + // prio first + if h.Priority < newEntry.Priority { + return i + } + + // domain specificity next + if h.Priority == newEntry.Priority { + newDots := strings.Count(newEntry.Pattern, ".") + existingDots := strings.Count(h.Pattern, ".") + if newDots > existingDots { + return i + } } - pos = i + 1 } - c.handlers = append(c.handlers[:pos], append([]HandlerEntry{entry}, c.handlers[pos:]...)...) + // add at end + return len(c.handlers) } // RemoveHandler removes a handler for the given pattern and priority @@ -129,9 +137,6 @@ func (c *HandlerChain) RemoveHandler(pattern string, priority int) { for i := len(c.handlers) - 1; i >= 0; i-- { entry := c.handlers[i] if strings.EqualFold(entry.OrigPattern, pattern) && entry.Priority == priority { - if entry.StopHandler != nil { - entry.StopHandler.stop() - } c.handlers = append(c.handlers[:i], c.handlers[i+1:]...) return } @@ -167,8 +172,8 @@ func (c *HandlerChain) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { if log.IsLevelEnabled(log.TraceLevel) { log.Tracef("current handlers (%d):", len(handlers)) for _, h := range handlers { - log.Tracef(" - pattern: domain=%s original: domain=%s wildcard=%v priority=%d", - h.Pattern, h.OrigPattern, h.IsWildcard, h.Priority) + log.Tracef(" - pattern: domain=%s original: domain=%s wildcard=%v match_subdomain=%v priority=%d", + h.Pattern, h.OrigPattern, h.IsWildcard, h.MatchSubdomains, h.Priority) } } @@ -193,13 +198,13 @@ func (c *HandlerChain) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { } if !matched { - log.Tracef("trying domain match: request: domain=%s pattern: domain=%s wildcard=%v match_subdomain=%v matched=false", - qname, entry.OrigPattern, entry.MatchSubdomains, entry.IsWildcard) + log.Tracef("trying domain match: request: domain=%s pattern: domain=%s wildcard=%v match_subdomain=%v priority=%d matched=false", + qname, entry.OrigPattern, entry.MatchSubdomains, entry.IsWildcard, entry.Priority) continue } - log.Tracef("handler matched: request: domain=%s pattern: domain=%s wildcard=%v match_subdomain=%v", - qname, entry.OrigPattern, entry.IsWildcard, entry.MatchSubdomains) + log.Tracef("handler matched: request: domain=%s pattern: domain=%s wildcard=%v match_subdomain=%v priority=%d", + qname, entry.OrigPattern, entry.IsWildcard, entry.MatchSubdomains, entry.Priority) chainWriter := &ResponseWriterChain{ ResponseWriter: w, diff --git a/client/internal/dns/handler_chain_test.go b/client/internal/dns/handler_chain_test.go index eb40c907f..94aa987af 100644 --- a/client/internal/dns/handler_chain_test.go +++ b/client/internal/dns/handler_chain_test.go @@ -21,9 +21,9 @@ func TestHandlerChain_ServeDNS_Priorities(t *testing.T) { dnsRouteHandler := &nbdns.MockHandler{} // Setup handlers with different priorities - chain.AddHandler("example.com.", defaultHandler, nbdns.PriorityDefault, nil) - chain.AddHandler("example.com.", matchDomainHandler, nbdns.PriorityMatchDomain, nil) - chain.AddHandler("example.com.", dnsRouteHandler, nbdns.PriorityDNSRoute, nil) + chain.AddHandler("example.com.", defaultHandler, nbdns.PriorityDefault) + chain.AddHandler("example.com.", matchDomainHandler, nbdns.PriorityMatchDomain) + chain.AddHandler("example.com.", dnsRouteHandler, nbdns.PriorityDNSRoute) // Create test request r := new(dns.Msg) @@ -138,7 +138,7 @@ func TestHandlerChain_ServeDNS_DomainMatching(t *testing.T) { pattern = "*." + tt.handlerDomain[2:] } - chain.AddHandler(pattern, handler, nbdns.PriorityDefault, nil) + chain.AddHandler(pattern, handler, nbdns.PriorityDefault) r := new(dns.Msg) r.SetQuestion(tt.queryDomain, dns.TypeA) @@ -253,7 +253,7 @@ func TestHandlerChain_ServeDNS_OverlappingDomains(t *testing.T) { handler.On("ServeDNS", mock.Anything, mock.Anything).Maybe() } - chain.AddHandler(tt.handlers[i].pattern, handler, tt.handlers[i].priority, nil) + chain.AddHandler(tt.handlers[i].pattern, handler, tt.handlers[i].priority) } // Create and execute request @@ -280,9 +280,9 @@ func TestHandlerChain_ServeDNS_ChainContinuation(t *testing.T) { handler3 := &nbdns.MockHandler{} // Add handlers in priority order - chain.AddHandler("example.com.", handler1, nbdns.PriorityDNSRoute, nil) - chain.AddHandler("example.com.", handler2, nbdns.PriorityMatchDomain, nil) - chain.AddHandler("example.com.", handler3, nbdns.PriorityDefault, nil) + chain.AddHandler("example.com.", handler1, nbdns.PriorityDNSRoute) + chain.AddHandler("example.com.", handler2, nbdns.PriorityMatchDomain) + chain.AddHandler("example.com.", handler3, nbdns.PriorityDefault) // Create test request r := new(dns.Msg) @@ -416,7 +416,7 @@ func TestHandlerChain_PriorityDeregistration(t *testing.T) { if op.action == "add" { handler := &nbdns.MockHandler{} handlers[op.priority] = handler - chain.AddHandler(op.pattern, handler, op.priority, nil) + chain.AddHandler(op.pattern, handler, op.priority) } else { chain.RemoveHandler(op.pattern, op.priority) } @@ -471,9 +471,9 @@ func TestHandlerChain_MultiPriorityHandling(t *testing.T) { r.SetQuestion(testQuery, dns.TypeA) // Add handlers in mixed order - chain.AddHandler(testDomain, defaultHandler, nbdns.PriorityDefault, nil) - chain.AddHandler(testDomain, routeHandler, nbdns.PriorityDNSRoute, nil) - chain.AddHandler(testDomain, matchHandler, nbdns.PriorityMatchDomain, nil) + chain.AddHandler(testDomain, defaultHandler, nbdns.PriorityDefault) + chain.AddHandler(testDomain, routeHandler, nbdns.PriorityDNSRoute) + chain.AddHandler(testDomain, matchHandler, nbdns.PriorityMatchDomain) // Test 1: Initial state with all three handlers w := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}} @@ -653,7 +653,7 @@ func TestHandlerChain_CaseSensitivity(t *testing.T) { handler = mockHandler } - chain.AddHandler(pattern, handler, h.priority, nil) + chain.AddHandler(pattern, handler, h.priority) } // Execute request @@ -677,3 +677,156 @@ func TestHandlerChain_CaseSensitivity(t *testing.T) { }) } } + +func TestHandlerChain_DomainSpecificityOrdering(t *testing.T) { + tests := []struct { + name string + scenario string + ops []struct { + action string + pattern string + priority int + subdomain bool + } + query string + expectedMatch string + }{ + { + name: "more specific domain matches first", + scenario: "sub.example.com should match before example.com", + ops: []struct { + action string + pattern string + priority int + subdomain bool + }{ + {"add", "example.com.", nbdns.PriorityMatchDomain, true}, + {"add", "sub.example.com.", nbdns.PriorityMatchDomain, false}, + }, + query: "sub.example.com.", + expectedMatch: "sub.example.com.", + }, + { + name: "more specific domain matches first, both match subdomains", + scenario: "sub.example.com should match before example.com", + ops: []struct { + action string + pattern string + priority int + subdomain bool + }{ + {"add", "example.com.", nbdns.PriorityMatchDomain, true}, + {"add", "sub.example.com.", nbdns.PriorityMatchDomain, true}, + }, + query: "sub.example.com.", + expectedMatch: "sub.example.com.", + }, + { + name: "maintain specificity order after removal", + scenario: "after removing most specific, should fall back to less specific", + ops: []struct { + action string + pattern string + priority int + subdomain bool + }{ + {"add", "example.com.", nbdns.PriorityMatchDomain, true}, + {"add", "sub.example.com.", nbdns.PriorityMatchDomain, true}, + {"add", "test.sub.example.com.", nbdns.PriorityMatchDomain, false}, + {"remove", "test.sub.example.com.", nbdns.PriorityMatchDomain, false}, + }, + query: "test.sub.example.com.", + expectedMatch: "sub.example.com.", + }, + { + name: "priority overrides specificity", + scenario: "less specific domain with higher priority should match first", + ops: []struct { + action string + pattern string + priority int + subdomain bool + }{ + {"add", "sub.example.com.", nbdns.PriorityMatchDomain, false}, + {"add", "example.com.", nbdns.PriorityDNSRoute, true}, + }, + query: "sub.example.com.", + expectedMatch: "example.com.", + }, + { + name: "equal priority respects specificity", + scenario: "with equal priority, more specific domain should match", + ops: []struct { + action string + pattern string + priority int + subdomain bool + }{ + {"add", "example.com.", nbdns.PriorityMatchDomain, true}, + {"add", "other.example.com.", nbdns.PriorityMatchDomain, true}, + {"add", "sub.example.com.", nbdns.PriorityMatchDomain, false}, + }, + query: "sub.example.com.", + expectedMatch: "sub.example.com.", + }, + { + name: "specific matches before wildcard", + scenario: "specific domain should match before wildcard at same priority", + ops: []struct { + action string + pattern string + priority int + subdomain bool + }{ + {"add", "*.example.com.", nbdns.PriorityDNSRoute, false}, + {"add", "sub.example.com.", nbdns.PriorityDNSRoute, false}, + }, + query: "sub.example.com.", + expectedMatch: "sub.example.com.", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + chain := nbdns.NewHandlerChain() + handlers := make(map[string]*nbdns.MockSubdomainHandler) + + for _, op := range tt.ops { + if op.action == "add" { + handler := &nbdns.MockSubdomainHandler{Subdomains: op.subdomain} + handlers[op.pattern] = handler + chain.AddHandler(op.pattern, handler, op.priority) + } else { + chain.RemoveHandler(op.pattern, op.priority) + } + } + + r := new(dns.Msg) + r.SetQuestion(tt.query, dns.TypeA) + w := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}} + + // Setup handler expectations + for pattern, handler := range handlers { + if pattern == tt.expectedMatch { + handler.On("ServeDNS", mock.Anything, r).Run(func(args mock.Arguments) { + w := args.Get(0).(dns.ResponseWriter) + r := args.Get(1).(*dns.Msg) + resp := new(dns.Msg) + resp.SetReply(r) + assert.NoError(t, w.WriteMsg(resp)) + }).Once() + } + } + + chain.ServeDNS(w, r) + + for pattern, handler := range handlers { + if pattern == tt.expectedMatch { + handler.AssertNumberOfCalls(t, "ServeDNS", 1) + } else { + handler.AssertNumberOfCalls(t, "ServeDNS", 0) + } + } + }) + } +} diff --git a/client/internal/dns/host.go b/client/internal/dns/host.go index fbe8c4dbb..25e9ff7e5 100644 --- a/client/internal/dns/host.go +++ b/client/internal/dns/host.go @@ -9,10 +9,18 @@ import ( nbdns "github.com/netbirdio/netbird/dns" ) +var ErrRouteAllWithoutNameserverGroup = fmt.Errorf("unable to configure DNS for this peer using file manager without a nameserver group with all domains configured") + +const ( + ipv4ReverseZone = ".in-addr.arpa" + ipv6ReverseZone = ".ip6.arpa" +) + type hostManager interface { applyDNSConfig(config HostDNSConfig, stateManager *statemanager.Manager) error restoreHostDNS() error supportCustomPort() bool + string() string } type SystemDNSSettings struct { @@ -39,6 +47,7 @@ type mockHostConfigurator struct { restoreHostDNSFunc func() error supportCustomPortFunc func() bool restoreUncleanShutdownDNSFunc func(*netip.Addr) error + stringFunc func() string } func (m *mockHostConfigurator) applyDNSConfig(config HostDNSConfig, stateManager *statemanager.Manager) error { @@ -62,6 +71,13 @@ func (m *mockHostConfigurator) supportCustomPort() bool { return false } +func (m *mockHostConfigurator) string() string { + if m.stringFunc != nil { + return m.stringFunc() + } + return "mock" +} + func newNoopHostMocker() hostManager { return &mockHostConfigurator{ applyDNSConfigFunc: func(config HostDNSConfig, stateManager *statemanager.Manager) error { return nil }, @@ -94,9 +110,10 @@ func dnsConfigToHostDNSConfig(dnsConfig nbdns.Config, ip string, port int) HostD } for _, customZone := range dnsConfig.CustomZones { + matchOnly := strings.HasSuffix(customZone.Domain, ipv4ReverseZone) || strings.HasSuffix(customZone.Domain, ipv6ReverseZone) config.Domains = append(config.Domains, DomainConfig{ Domain: strings.TrimSuffix(customZone.Domain, "."), - MatchOnly: false, + MatchOnly: matchOnly, }) } @@ -116,3 +133,7 @@ func (n noopHostConfigurator) restoreHostDNS() error { func (n noopHostConfigurator) supportCustomPort() bool { return true } + +func (n noopHostConfigurator) string() string { + return "noop" +} diff --git a/client/internal/dns/host_android.go b/client/internal/dns/host_android.go index 5653710d7..dfa3e5712 100644 --- a/client/internal/dns/host_android.go +++ b/client/internal/dns/host_android.go @@ -22,3 +22,7 @@ func (a androidHostManager) restoreHostDNS() error { func (a androidHostManager) supportCustomPort() bool { return false } + +func (a androidHostManager) string() string { + return "none" +} diff --git a/client/internal/dns/host_darwin.go b/client/internal/dns/host_darwin.go index b8ba33e34..f727f68b5 100644 --- a/client/internal/dns/host_darwin.go +++ b/client/internal/dns/host_darwin.go @@ -28,6 +28,7 @@ const ( arraySymbol = "* " digitSymbol = "# " scutilPath = "/usr/sbin/scutil" + dscacheutilPath = "/usr/bin/dscacheutil" searchSuffix = "Search" matchSuffix = "Match" localSuffix = "Local" @@ -106,9 +107,17 @@ func (s *systemConfigurator) applyDNSConfig(config HostDNSConfig, stateManager * return fmt.Errorf("add search domains: %w", err) } + if err := s.flushDNSCache(); err != nil { + log.Errorf("failed to flush DNS cache: %v", err) + } + return nil } +func (s *systemConfigurator) string() string { + return "scutil" +} + func (s *systemConfigurator) restoreHostDNS() error { keys := s.getRemovableKeysWithDefaults() for _, key := range keys { @@ -123,6 +132,10 @@ func (s *systemConfigurator) restoreHostDNS() error { } } + if err := s.flushDNSCache(); err != nil { + log.Errorf("failed to flush DNS cache: %v", err) + } + return nil } @@ -316,6 +329,21 @@ func (s *systemConfigurator) getPrimaryService() (string, string, error) { return primaryService, router, nil } +func (s *systemConfigurator) flushDNSCache() error { + cmd := exec.Command(dscacheutilPath, "-flushcache") + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("flush DNS cache: %w, output: %s", err, out) + } + + cmd = exec.Command("killall", "-HUP", "mDNSResponder") + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("restart mDNSResponder: %w, output: %s", err, out) + } + + log.Info("flushed DNS cache") + return nil +} + func (s *systemConfigurator) restoreUncleanShutdownDNS() error { if err := s.restoreHostDNS(); err != nil { return fmt.Errorf("restoring dns via scutil: %w", err) diff --git a/client/internal/dns/host_ios.go b/client/internal/dns/host_ios.go index 4a0acf572..1c0ac63e9 100644 --- a/client/internal/dns/host_ios.go +++ b/client/internal/dns/host_ios.go @@ -38,3 +38,7 @@ func (a iosHostManager) restoreHostDNS() error { func (a iosHostManager) supportCustomPort() bool { return false } + +func (a iosHostManager) string() string { + return "none" +} diff --git a/client/internal/dns/host_unix.go b/client/internal/dns/host_unix.go index 7bd4aec64..297d50822 100644 --- a/client/internal/dns/host_unix.go +++ b/client/internal/dns/host_unix.go @@ -48,11 +48,17 @@ type restoreHostManager interface { func newHostManager(wgInterface string) (hostManager, error) { osManager, err := getOSDNSManagerType() if err != nil { - return nil, err + return nil, fmt.Errorf("get os dns manager type: %w", err) } log.Infof("System DNS manager discovered: %s", osManager) - return newHostManagerFromType(wgInterface, osManager) + mgr, err := newHostManagerFromType(wgInterface, osManager) + // need to explicitly return nil mgr on error to avoid returning a non-nil interface containing a nil value + if err != nil { + return nil, fmt.Errorf("create host manager: %w", err) + } + + return mgr, nil } func newHostManagerFromType(wgInterface string, osManager osManagerType) (restoreHostManager, error) { diff --git a/client/internal/dns/host_windows.go b/client/internal/dns/host_windows.go index 7ecca8a41..dceb24420 100644 --- a/client/internal/dns/host_windows.go +++ b/client/internal/dns/host_windows.go @@ -1,35 +1,51 @@ package dns import ( + "errors" "fmt" "io" "strings" + "syscall" + "github.com/hashicorp/go-multierror" log "github.com/sirupsen/logrus" "golang.org/x/sys/windows/registry" + nberrors "github.com/netbirdio/netbird/client/errors" "github.com/netbirdio/netbird/client/internal/statemanager" ) +var ( + userenv = syscall.NewLazyDLL("userenv.dll") + + // https://learn.microsoft.com/en-us/windows/win32/api/userenv/nf-userenv-refreshpolicyex + refreshPolicyExFn = userenv.NewProc("RefreshPolicyEx") +) + const ( - dnsPolicyConfigMatchPath = `SYSTEM\CurrentControlSet\Services\Dnscache\Parameters\DnsPolicyConfig\NetBird-Match` + dnsPolicyConfigMatchPath = `SYSTEM\CurrentControlSet\Services\Dnscache\Parameters\DnsPolicyConfig\NetBird-Match` + gpoDnsPolicyRoot = `SOFTWARE\Policies\Microsoft\Windows NT\DNSClient` + gpoDnsPolicyConfigMatchPath = gpoDnsPolicyRoot + `\DnsPolicyConfig\NetBird-Match` + dnsPolicyConfigVersionKey = "Version" dnsPolicyConfigVersionValue = 2 dnsPolicyConfigNameKey = "Name" dnsPolicyConfigGenericDNSServersKey = "GenericDNSServers" dnsPolicyConfigConfigOptionsKey = "ConfigOptions" dnsPolicyConfigConfigOptionsValue = 0x8 -) -const ( interfaceConfigPath = `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces` interfaceConfigNameServerKey = "NameServer" interfaceConfigSearchListKey = "SearchList" + + // RP_FORCE: Reapply all policies even if no policy change was detected + rpForce = 0x1 ) type registryConfigurator struct { guid string routingAll bool + gpo bool } func newHostManager(wgInterface WGIface) (*registryConfigurator, error) { @@ -37,12 +53,20 @@ func newHostManager(wgInterface WGIface) (*registryConfigurator, error) { if err != nil { return nil, err } - return newHostManagerWithGuid(guid) -} -func newHostManagerWithGuid(guid string) (*registryConfigurator, error) { + var useGPO bool + k, err := registry.OpenKey(registry.LOCAL_MACHINE, gpoDnsPolicyRoot, registry.QUERY_VALUE) + if err != nil { + log.Debugf("failed to open GPO DNS policy root: %v", err) + } else { + closer(k) + useGPO = true + log.Infof("detected GPO DNS policy configuration, using policy store") + } + return ®istryConfigurator{ guid: guid, + gpo: useGPO, }, nil } @@ -51,30 +75,23 @@ func (r *registryConfigurator) supportCustomPort() bool { } func (r *registryConfigurator) applyDNSConfig(config HostDNSConfig, stateManager *statemanager.Manager) error { - var err error if config.RouteAll { - err = r.addDNSSetupForAll(config.ServerIP) - if err != nil { + if err := r.addDNSSetupForAll(config.ServerIP); err != nil { return fmt.Errorf("add dns setup: %w", err) } } else if r.routingAll { - err = r.deleteInterfaceRegistryKeyProperty(interfaceConfigNameServerKey) - if err != nil { + if err := r.deleteInterfaceRegistryKeyProperty(interfaceConfigNameServerKey); err != nil { return fmt.Errorf("delete interface registry key property: %w", err) } r.routingAll = false log.Infof("removed %s as main DNS forwarder for this peer", config.ServerIP) } - if err := stateManager.UpdateState(&ShutdownState{Guid: r.guid}); err != nil { + if err := stateManager.UpdateState(&ShutdownState{Guid: r.guid, GPO: r.gpo}); err != nil { log.Errorf("failed to update shutdown state: %s", err) } - var ( - searchDomains []string - matchDomains []string - ) - + var searchDomains, matchDomains []string for _, dConf := range config.Domains { if dConf.Disabled { continue @@ -86,16 +103,16 @@ func (r *registryConfigurator) applyDNSConfig(config HostDNSConfig, stateManager } if len(matchDomains) != 0 { - err = r.addDNSMatchPolicy(matchDomains, config.ServerIP) + if err := r.addDNSMatchPolicy(matchDomains, config.ServerIP); err != nil { + return fmt.Errorf("add dns match policy: %w", err) + } } else { - err = removeRegistryKeyFromDNSPolicyConfig(dnsPolicyConfigMatchPath) - } - if err != nil { - return fmt.Errorf("add dns match policy: %w", err) + if err := r.removeDNSMatchPolicies(); err != nil { + return fmt.Errorf("remove dns match policies: %w", err) + } } - err = r.updateSearchDomains(searchDomains) - if err != nil { + if err := r.updateSearchDomains(searchDomains); err != nil { return fmt.Errorf("update search domains: %w", err) } @@ -103,9 +120,8 @@ func (r *registryConfigurator) applyDNSConfig(config HostDNSConfig, stateManager } func (r *registryConfigurator) addDNSSetupForAll(ip string) error { - err := r.setInterfaceRegistryKeyStringValue(interfaceConfigNameServerKey, ip) - if err != nil { - return fmt.Errorf("adding dns setup for all failed with error: %w", err) + if err := r.setInterfaceRegistryKeyStringValue(interfaceConfigNameServerKey, ip); err != nil { + return fmt.Errorf("adding dns setup for all failed: %w", err) } r.routingAll = true log.Infof("configured %s:53 as main DNS forwarder for this peer", ip) @@ -113,64 +129,70 @@ func (r *registryConfigurator) addDNSSetupForAll(ip string) error { } func (r *registryConfigurator) addDNSMatchPolicy(domains []string, ip string) error { - _, err := registry.OpenKey(registry.LOCAL_MACHINE, dnsPolicyConfigMatchPath, registry.QUERY_VALUE) - if err == nil { - err = registry.DeleteKey(registry.LOCAL_MACHINE, dnsPolicyConfigMatchPath) - if err != nil { - return fmt.Errorf("unable to remove existing key from registry, key: HKEY_LOCAL_MACHINE\\%s, error: %w", dnsPolicyConfigMatchPath, err) + // if the gpo key is present, we need to put our DNS settings there, otherwise our config might be ignored + // see https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gpnrpt/8cc31cb9-20cb-4140-9e85-3e08703b4745 + if r.gpo { + if err := r.configureDNSPolicy(gpoDnsPolicyConfigMatchPath, domains, ip); err != nil { + return fmt.Errorf("configure GPO DNS policy: %w", err) + } + + if err := r.configureDNSPolicy(dnsPolicyConfigMatchPath, domains, ip); err != nil { + return fmt.Errorf("configure local DNS policy: %w", err) + } + + if err := refreshGroupPolicy(); err != nil { + log.Warnf("failed to refresh group policy: %v", err) + } + } else { + if err := r.configureDNSPolicy(dnsPolicyConfigMatchPath, domains, ip); err != nil { + return fmt.Errorf("configure local DNS policy: %w", err) } } - regKey, _, err := registry.CreateKey(registry.LOCAL_MACHINE, dnsPolicyConfigMatchPath, registry.SET_VALUE) - if err != nil { - return fmt.Errorf("unable to create registry key, key: HKEY_LOCAL_MACHINE\\%s, error: %w", dnsPolicyConfigMatchPath, err) + log.Infof("added %d match domains. Domain list: %s", len(domains), domains) + return nil +} + +// configureDNSPolicy handles the actual configuration of a DNS policy at the specified path +func (r *registryConfigurator) configureDNSPolicy(policyPath string, domains []string, ip string) error { + if err := removeRegistryKeyFromDNSPolicyConfig(policyPath); err != nil { + return fmt.Errorf("remove existing dns policy: %w", err) } - err = regKey.SetDWordValue(dnsPolicyConfigVersionKey, dnsPolicyConfigVersionValue) + regKey, _, err := registry.CreateKey(registry.LOCAL_MACHINE, policyPath, registry.SET_VALUE) if err != nil { - return fmt.Errorf("unable to set registry value for %s, error: %w", dnsPolicyConfigVersionKey, err) + return fmt.Errorf("create registry key HKEY_LOCAL_MACHINE\\%s: %w", policyPath, err) + } + defer closer(regKey) + + if err := regKey.SetDWordValue(dnsPolicyConfigVersionKey, dnsPolicyConfigVersionValue); err != nil { + return fmt.Errorf("set %s: %w", dnsPolicyConfigVersionKey, err) } - err = regKey.SetStringsValue(dnsPolicyConfigNameKey, domains) - if err != nil { - return fmt.Errorf("unable to set registry value for %s, error: %w", dnsPolicyConfigNameKey, err) + if err := regKey.SetStringsValue(dnsPolicyConfigNameKey, domains); err != nil { + return fmt.Errorf("set %s: %w", dnsPolicyConfigNameKey, err) } - err = regKey.SetStringValue(dnsPolicyConfigGenericDNSServersKey, ip) - if err != nil { - return fmt.Errorf("unable to set registry value for %s, error: %w", dnsPolicyConfigGenericDNSServersKey, err) + if err := regKey.SetStringValue(dnsPolicyConfigGenericDNSServersKey, ip); err != nil { + return fmt.Errorf("set %s: %w", dnsPolicyConfigGenericDNSServersKey, err) } - err = regKey.SetDWordValue(dnsPolicyConfigConfigOptionsKey, dnsPolicyConfigConfigOptionsValue) - if err != nil { - return fmt.Errorf("unable to set registry value for %s, error: %w", dnsPolicyConfigConfigOptionsKey, err) + if err := regKey.SetDWordValue(dnsPolicyConfigConfigOptionsKey, dnsPolicyConfigConfigOptionsValue); err != nil { + return fmt.Errorf("set %s: %w", dnsPolicyConfigConfigOptionsKey, err) } - log.Infof("added %d match domains to the state. Domain list: %s", len(domains), domains) - return nil } -func (r *registryConfigurator) restoreHostDNS() error { - if err := removeRegistryKeyFromDNSPolicyConfig(dnsPolicyConfigMatchPath); err != nil { - log.Errorf("remove registry key from dns policy config: %s", err) - } - - if err := r.deleteInterfaceRegistryKeyProperty(interfaceConfigSearchListKey); err != nil { - return fmt.Errorf("remove interface registry key: %w", err) - } - - return nil +func (r *registryConfigurator) string() string { + return "registry" } func (r *registryConfigurator) updateSearchDomains(domains []string) error { - err := r.setInterfaceRegistryKeyStringValue(interfaceConfigSearchListKey, strings.Join(domains, ",")) - if err != nil { - return fmt.Errorf("adding search domain failed with error: %w", err) + if err := r.setInterfaceRegistryKeyStringValue(interfaceConfigSearchListKey, strings.Join(domains, ",")); err != nil { + return fmt.Errorf("update search domains: %w", err) } - - log.Infof("updated the search domains in the registry with %d domains. Domain list: %s", len(domains), domains) - + log.Infof("updated search domains: %s", domains) return nil } @@ -181,11 +203,9 @@ func (r *registryConfigurator) setInterfaceRegistryKeyStringValue(key, value str } defer closer(regKey) - err = regKey.SetStringValue(key, value) - if err != nil { - return fmt.Errorf("applying key %s with value \"%s\" for interface failed with error: %w", key, value, err) + if err := regKey.SetStringValue(key, value); err != nil { + return fmt.Errorf("set key %s=%s: %w", key, value, err) } - return nil } @@ -196,43 +216,91 @@ func (r *registryConfigurator) deleteInterfaceRegistryKeyProperty(propertyKey st } defer closer(regKey) - err = regKey.DeleteValue(propertyKey) - if err != nil { - return fmt.Errorf("deleting registry key %s for interface failed with error: %w", propertyKey, err) + if err := regKey.DeleteValue(propertyKey); err != nil { + return fmt.Errorf("delete registry key %s: %w", propertyKey, err) } - return nil } func (r *registryConfigurator) getInterfaceRegistryKey() (registry.Key, error) { - var regKey registry.Key - regKeyPath := interfaceConfigPath + "\\" + r.guid - regKey, err := registry.OpenKey(registry.LOCAL_MACHINE, regKeyPath, registry.SET_VALUE) if err != nil { - return regKey, fmt.Errorf("unable to open the interface registry key, key: HKEY_LOCAL_MACHINE\\%s, error: %w", regKeyPath, err) + return regKey, fmt.Errorf("open HKEY_LOCAL_MACHINE\\%s: %w", regKeyPath, err) } - return regKey, nil } -func (r *registryConfigurator) restoreUncleanShutdownDNS() error { - if err := r.restoreHostDNS(); err != nil { - return fmt.Errorf("restoring dns via registry: %w", err) +func (r *registryConfigurator) restoreHostDNS() error { + if err := r.removeDNSMatchPolicies(); err != nil { + log.Errorf("remove dns match policies: %s", err) } + + if err := r.deleteInterfaceRegistryKeyProperty(interfaceConfigSearchListKey); err != nil { + return fmt.Errorf("remove interface registry key: %w", err) + } + return nil } +func (r *registryConfigurator) removeDNSMatchPolicies() error { + var merr *multierror.Error + if err := removeRegistryKeyFromDNSPolicyConfig(dnsPolicyConfigMatchPath); err != nil { + merr = multierror.Append(merr, fmt.Errorf("remove local registry key: %w", err)) + } + + if err := removeRegistryKeyFromDNSPolicyConfig(gpoDnsPolicyConfigMatchPath); err != nil { + merr = multierror.Append(merr, fmt.Errorf("remove GPO registry key: %w", err)) + } + + if err := refreshGroupPolicy(); err != nil { + merr = multierror.Append(merr, fmt.Errorf("refresh group policy: %w", err)) + } + + return nberrors.FormatErrorOrNil(merr) +} + +func (r *registryConfigurator) restoreUncleanShutdownDNS() error { + return r.restoreHostDNS() +} + func removeRegistryKeyFromDNSPolicyConfig(regKeyPath string) error { k, err := registry.OpenKey(registry.LOCAL_MACHINE, regKeyPath, registry.QUERY_VALUE) - if err == nil { - defer closer(k) - err = registry.DeleteKey(registry.LOCAL_MACHINE, regKeyPath) - if err != nil { - return fmt.Errorf("unable to remove existing key from registry, key: HKEY_LOCAL_MACHINE\\%s, error: %w", regKeyPath, err) - } + if err != nil { + log.Debugf("failed to open HKEY_LOCAL_MACHINE\\%s: %v", regKeyPath, err) + return nil } + + closer(k) + if err := registry.DeleteKey(registry.LOCAL_MACHINE, regKeyPath); err != nil { + return fmt.Errorf("delete HKEY_LOCAL_MACHINE\\%s: %w", regKeyPath, err) + } + + return nil +} + +func refreshGroupPolicy() error { + // refreshPolicyExFn.Call() panics if the func is not found + defer func() { + if r := recover(); r != nil { + log.Errorf("Recovered from panic: %v", r) + } + }() + + ret, _, err := refreshPolicyExFn.Call( + // bMachine = TRUE (computer policy) + uintptr(1), + // dwOptions = RP_FORCE + uintptr(rpForce), + ) + + if ret == 0 { + if err != nil && !errors.Is(err, syscall.Errno(0)) { + return fmt.Errorf("RefreshPolicyEx failed: %w", err) + } + return fmt.Errorf("RefreshPolicyEx failed") + } + return nil } diff --git a/client/internal/dns/local.go b/client/internal/dns/local.go index 9a78d4d50..3a25a23b6 100644 --- a/client/internal/dns/local.go +++ b/client/internal/dns/local.go @@ -2,6 +2,7 @@ package dns import ( "fmt" + "strings" "sync" "github.com/miekg/dns" @@ -14,7 +15,7 @@ type registrationMap map[string]struct{} type localResolver struct { registeredMap registrationMap - records sync.Map + records sync.Map // key: string (domain_class_type), value: []dns.RR } func (d *localResolver) MatchSubdomains() bool { @@ -29,20 +30,26 @@ func (d *localResolver) String() string { return fmt.Sprintf("local resolver [%d records]", len(d.registeredMap)) } +// ID returns the unique handler ID +func (d *localResolver) id() handlerID { + return "local-resolver" +} + // ServeDNS handles a DNS request func (d *localResolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { if len(r.Question) > 0 { - log.Tracef("received question: domain=%s type=%v class=%v", r.Question[0].Name, r.Question[0].Qtype, r.Question[0].Qclass) + log.Tracef("received local question: domain=%s type=%v class=%v", r.Question[0].Name, r.Question[0].Qtype, r.Question[0].Qclass) } replyMessage := &dns.Msg{} replyMessage.SetReply(r) replyMessage.RecursionAvailable = true - replyMessage.Rcode = dns.RcodeSuccess - response := d.lookupRecord(r) - if response != nil { - replyMessage.Answer = append(replyMessage.Answer, response) + // lookup all records matching the question + records := d.lookupRecords(r) + if len(records) > 0 { + replyMessage.Rcode = dns.RcodeSuccess + replyMessage.Answer = append(replyMessage.Answer, records...) } else { replyMessage.Rcode = dns.RcodeNameError } @@ -53,37 +60,65 @@ func (d *localResolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { } } -func (d *localResolver) lookupRecord(r *dns.Msg) dns.RR { +// lookupRecords fetches *all* DNS records matching the first question in r. +func (d *localResolver) lookupRecords(r *dns.Msg) []dns.RR { + if len(r.Question) == 0 { + return nil + } question := r.Question[0] - record, found := d.records.Load(buildRecordKey(question.Name, question.Qclass, question.Qtype)) + question.Name = strings.ToLower(question.Name) + key := buildRecordKey(question.Name, question.Qclass, question.Qtype) + + value, found := d.records.Load(key) if !found { return nil } - return record.(dns.RR) -} - -func (d *localResolver) registerRecord(record nbdns.SimpleRecord) error { - fullRecord, err := dns.NewRR(record.String()) - if err != nil { - return fmt.Errorf("register record: %w", err) + records, ok := value.([]dns.RR) + if !ok { + log.Errorf("failed to cast records to []dns.RR, records: %v", value) + return nil } - fullRecord.Header().Rdlength = record.Len() + // if there's more than one record, rotate them (round-robin) + if len(records) > 1 { + first := records[0] + records = append(records[1:], first) + d.records.Store(key, records) + } - header := fullRecord.Header() - d.records.Store(buildRecordKey(header.Name, header.Class, header.Rrtype), fullRecord) - - return nil + return records } +// registerRecord stores a new record by appending it to any existing list +func (d *localResolver) registerRecord(record nbdns.SimpleRecord) (string, error) { + rr, err := dns.NewRR(record.String()) + if err != nil { + return "", fmt.Errorf("register record: %w", err) + } + + rr.Header().Rdlength = record.Len() + header := rr.Header() + key := buildRecordKey(header.Name, header.Class, header.Rrtype) + + // load any existing slice of records, then append + existing, _ := d.records.LoadOrStore(key, []dns.RR{}) + records := existing.([]dns.RR) + records = append(records, rr) + + // store updated slice + d.records.Store(key, records) + return key, nil +} + +// deleteRecord removes *all* records under the recordKey. func (d *localResolver) deleteRecord(recordKey string) { d.records.Delete(dns.Fqdn(recordKey)) } +// buildRecordKey consistently generates a key: name_class_type func buildRecordKey(name string, class, qType uint16) string { - key := fmt.Sprintf("%s_%d_%d", name, class, qType) - return key + return fmt.Sprintf("%s_%d_%d", dns.Fqdn(name), class, qType) } func (d *localResolver) probeAvailability() {} diff --git a/client/internal/dns/local_test.go b/client/internal/dns/local_test.go index b62cd66a9..0a42b321a 100644 --- a/client/internal/dns/local_test.go +++ b/client/internal/dns/local_test.go @@ -55,7 +55,7 @@ func TestLocalResolver_ServeDNS(t *testing.T) { resolver := &localResolver{ registeredMap: make(registrationMap), } - _ = resolver.registerRecord(testCase.inputRecord) + _, _ = resolver.registerRecord(testCase.inputRecord) var responseMSG *dns.Msg responseWriter := &mockResponseWriter{ WriteMsgFunc: func(m *dns.Msg) error { diff --git a/client/internal/dns/network_manager_unix.go b/client/internal/dns/network_manager_unix.go index 63bbead77..10b4e6a6e 100644 --- a/client/internal/dns/network_manager_unix.go +++ b/client/internal/dns/network_manager_unix.go @@ -179,6 +179,10 @@ func (n *networkManagerDbusConfigurator) restoreHostDNS() error { return nil } +func (n *networkManagerDbusConfigurator) string() string { + return "network-manager" +} + func (n *networkManagerDbusConfigurator) getAppliedConnectionSettings() (networkManagerConnSettings, networkManagerConfigVersion, error) { obj, closeConn, err := getDbusObject(networkManagerDest, n.dbusLinkObject) if err != nil { diff --git a/client/internal/dns/resolvconf_unix.go b/client/internal/dns/resolvconf_unix.go index a5d1cc8a2..54c4c75bf 100644 --- a/client/internal/dns/resolvconf_unix.go +++ b/client/internal/dns/resolvconf_unix.go @@ -7,6 +7,7 @@ import ( "fmt" "net/netip" "os/exec" + "strings" log "github.com/sirupsen/logrus" @@ -15,23 +16,64 @@ import ( const resolvconfCommand = "resolvconf" +// resolvconfType represents the type of resolvconf implementation +type resolvconfType int + +func (r resolvconfType) String() string { + switch r { + case typeOpenresolv: + return "openresolv" + case typeResolvconf: + return "resolvconf" + default: + return "unknown" + } +} + +const ( + typeOpenresolv resolvconfType = iota + typeResolvconf +) + type resolvconf struct { ifaceName string + implType resolvconfType originalSearchDomains []string originalNameServers []string othersConfigs []string } -// supported "openresolv" only +func detectResolvconfType() (resolvconfType, error) { + cmd := exec.Command(resolvconfCommand, "--version") + out, err := cmd.Output() + if err != nil { + return typeOpenresolv, fmt.Errorf("failed to determine resolvconf type: %w", err) + } + + if strings.Contains(string(out), "openresolv") { + return typeOpenresolv, nil + } + return typeResolvconf, nil +} + func newResolvConfConfigurator(wgInterface string) (*resolvconf, error) { resolvConfEntries, err := parseDefaultResolvConf() if err != nil { log.Errorf("could not read original search domains from %s: %s", defaultResolvConfPath, err) } + implType, err := detectResolvconfType() + if err != nil { + log.Warnf("failed to detect resolvconf type, defaulting to openresolv: %v", err) + implType = typeOpenresolv + } else { + log.Infof("detected resolvconf type: %v", implType) + } + return &resolvconf{ ifaceName: wgInterface, + implType: implType, originalSearchDomains: resolvConfEntries.searchDomains, originalNameServers: resolvConfEntries.nameServers, othersConfigs: resolvConfEntries.others, @@ -49,7 +91,7 @@ func (r *resolvconf) applyDNSConfig(config HostDNSConfig, stateManager *stateman if err != nil { log.Errorf("restore host dns: %s", err) } - return fmt.Errorf("unable to configure DNS for this peer using resolvconf manager without a nameserver group with all domains configured") + return ErrRouteAllWithoutNameserverGroup } searchDomainList := searchDomains(config) @@ -80,8 +122,15 @@ func (r *resolvconf) applyDNSConfig(config HostDNSConfig, stateManager *stateman } func (r *resolvconf) restoreHostDNS() error { - // openresolv only, debian resolvconf doesn't support "-f" - cmd := exec.Command(resolvconfCommand, "-f", "-d", r.ifaceName) + var cmd *exec.Cmd + + switch r.implType { + case typeOpenresolv: + cmd = exec.Command(resolvconfCommand, "-f", "-d", r.ifaceName) + case typeResolvconf: + cmd = exec.Command(resolvconfCommand, "-d", r.ifaceName) + } + _, err := cmd.Output() if err != nil { return fmt.Errorf("removing resolvconf configuration for %s interface: %w", r.ifaceName, err) @@ -90,11 +139,26 @@ func (r *resolvconf) restoreHostDNS() error { return nil } +func (r *resolvconf) string() string { + return fmt.Sprintf("resolvconf (%s)", r.implType) +} + func (r *resolvconf) applyConfig(content bytes.Buffer) error { - // openresolv only, debian resolvconf doesn't support "-x" - cmd := exec.Command(resolvconfCommand, "-x", "-a", r.ifaceName) + var cmd *exec.Cmd + + switch r.implType { + case typeOpenresolv: + // OpenResolv supports exclusive mode with -x + cmd = exec.Command(resolvconfCommand, "-x", "-a", r.ifaceName) + case typeResolvconf: + cmd = exec.Command(resolvconfCommand, "-a", r.ifaceName) + default: + return fmt.Errorf("unsupported resolvconf type: %v", r.implType) + } + cmd.Stdin = &content - _, err := cmd.Output() + out, err := cmd.Output() + log.Tracef("resolvconf output: %s", out) if err != nil { return fmt.Errorf("applying resolvconf configuration for %s interface: %w", r.ifaceName, err) } diff --git a/client/internal/dns/server.go b/client/internal/dns/server.go index bb097c4cb..bc87012f2 100644 --- a/client/internal/dns/server.go +++ b/client/internal/dns/server.go @@ -2,19 +2,21 @@ package dns import ( "context" + "errors" "fmt" "net/netip" "runtime" - "strings" "sync" "github.com/miekg/dns" "github.com/mitchellh/hashstructure/v2" log "github.com/sirupsen/logrus" + "github.com/netbirdio/netbird/client/iface/netstack" "github.com/netbirdio/netbird/client/internal/listener" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/statemanager" + cProto "github.com/netbirdio/netbird/client/proto" nbdns "github.com/netbirdio/netbird/dns" ) @@ -41,7 +43,12 @@ type Server interface { ProbeAvailability() } -type registeredHandlerMap map[string]handlerWithStop +type handlerID string + +type nsGroupsByDomain struct { + domain string + groups []*nbdns.NameServerGroup +} // DefaultServer dns server object type DefaultServer struct { @@ -51,7 +58,6 @@ type DefaultServer struct { mux sync.Mutex service service dnsMuxMap registeredHandlerMap - handlerPriorities map[string]int localResolver *localResolver wgInterface WGIface hostManager hostManager @@ -76,14 +82,17 @@ type handlerWithStop interface { dns.Handler stop() probeAvailability() + id() handlerID } -type muxUpdate struct { +type handlerWrapper struct { domain string handler handlerWithStop priority int } +type registeredHandlerMap map[handlerID]handlerWrapper + // NewDefaultServer returns a new dns server func NewDefaultServer( ctx context.Context, @@ -157,13 +166,12 @@ func newDefaultServer( ) *DefaultServer { ctx, stop := context.WithCancel(ctx) defaultServer := &DefaultServer{ - ctx: ctx, - ctxCancel: stop, - disableSys: disableSys, - service: dnsService, - handlerChain: NewHandlerChain(), - dnsMuxMap: make(registeredHandlerMap), - handlerPriorities: make(map[string]int), + ctx: ctx, + ctxCancel: stop, + disableSys: disableSys, + service: dnsService, + handlerChain: NewHandlerChain(), + dnsMuxMap: make(registeredHandlerMap), localResolver: &localResolver{ registeredMap: make(registrationMap), }, @@ -191,8 +199,7 @@ func (s *DefaultServer) registerHandler(domains []string, handler dns.Handler, p log.Warn("skipping empty domain") continue } - s.handlerChain.AddHandler(domain, handler, priority, nil) - s.handlerPriorities[domain] = priority + s.handlerChain.AddHandler(domain, handler, priority) s.service.RegisterMux(nbdns.NormalizeZone(domain), s.handlerChain) } } @@ -208,14 +215,15 @@ func (s *DefaultServer) deregisterHandler(domains []string, priority int) { log.Debugf("deregistering handler %v with priority %d", domains, priority) for _, domain := range domains { + if domain == "" { + log.Warn("skipping empty domain") + continue + } + s.handlerChain.RemoveHandler(domain, priority) // Only deregister from service if no handlers remain if !s.handlerChain.HasHandlers(domain) { - if domain == "" { - log.Warn("skipping empty domain") - continue - } s.service.DeregisterMux(nbdns.NormalizeZone(domain)) } } @@ -239,7 +247,10 @@ func (s *DefaultServer) Initialize() (err error) { s.stateManager.RegisterState(&ShutdownState{}) - if s.disableSys { + // use noop host manager if requested or running in netstack mode. + // Netstack mode currently doesn't have a way to receive DNS requests. + // TODO: Use listener on localhost in netstack mode when running as root. + if s.disableSys || netstack.IsEnabled() { log.Info("system DNS is disabled, not setting up host manager") s.hostManager = &noopHostConfigurator{} return nil @@ -279,14 +290,24 @@ func (s *DefaultServer) Stop() { // OnUpdatedHostDNSServer update the DNS servers addresses for root zones // It will be applied if the mgm server do not enforce DNS settings for root zone + func (s *DefaultServer) OnUpdatedHostDNSServer(hostsDnsList []string) { s.hostsDNSHolder.set(hostsDnsList) - _, ok := s.dnsMuxMap[nbdns.RootZone] - if ok { + // Check if there's any root handler + var hasRootHandler bool + for _, handler := range s.dnsMuxMap { + if handler.domain == nbdns.RootZone { + hasRootHandler = true + break + } + } + + if hasRootHandler { log.Debugf("on new host DNS config but skip to apply it") return } + log.Debugf("update host DNS settings: %+v", hostsDnsList) s.addHostRootZone() } @@ -360,7 +381,7 @@ func (s *DefaultServer) ProbeAvailability() { go func(mux handlerWithStop) { defer wg.Done() mux.probeAvailability() - }(mux) + }(mux.handler) } wg.Wait() } @@ -374,18 +395,22 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error { s.service.Stop() } - localMuxUpdates, localRecords, err := s.buildLocalHandlerUpdate(update.CustomZones) + localMuxUpdates, localRecordsByDomain, err := s.buildLocalHandlerUpdate(update.CustomZones) if err != nil { - return fmt.Errorf("not applying dns update, error: %v", err) + return fmt.Errorf("local handler updater: %w", err) } + upstreamMuxUpdates, err := s.buildUpstreamHandlerUpdate(update.NameServerGroups) if err != nil { - return fmt.Errorf("not applying dns update, error: %v", err) + return fmt.Errorf("upstream handler updater: %w", err) } muxUpdates := append(localMuxUpdates, upstreamMuxUpdates...) //nolint:gocritic s.updateMux(muxUpdates) - s.updateLocalResolver(localRecords) + + // register local records + s.updateLocalResolver(localRecordsByDomain) + s.currentConfig = dnsConfigToHostDNSConfig(update, s.service.RuntimeIP(), s.service.RuntimePort()) hostUpdate := s.currentConfig @@ -397,6 +422,7 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error { if err = s.hostManager.applyDNSConfig(hostUpdate, s.stateManager); err != nil { log.Error(err) + s.handleErrNoGroupaAll(err) } go func() { @@ -415,42 +441,111 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error { return nil } -func (s *DefaultServer) buildLocalHandlerUpdate(customZones []nbdns.CustomZone) ([]muxUpdate, map[string]nbdns.SimpleRecord, error) { - var muxUpdates []muxUpdate - localRecords := make(map[string]nbdns.SimpleRecord, 0) +func (s *DefaultServer) handleErrNoGroupaAll(err error) { + if !errors.Is(ErrRouteAllWithoutNameserverGroup, err) { + return + } + + if s.statusRecorder == nil { + return + } + + s.statusRecorder.PublishEvent( + cProto.SystemEvent_WARNING, cProto.SystemEvent_DNS, + "The host dns manager does not support match domains", + "The host dns manager does not support match domains without a catch-all nameserver group.", + map[string]string{"manager": s.hostManager.string()}, + ) +} + +func (s *DefaultServer) buildLocalHandlerUpdate( + customZones []nbdns.CustomZone, +) ([]handlerWrapper, map[string][]nbdns.SimpleRecord, error) { + var muxUpdates []handlerWrapper + localRecords := make(map[string][]nbdns.SimpleRecord) for _, customZone := range customZones { if len(customZone.Records) == 0 { - return nil, nil, fmt.Errorf("received an empty list of records") + log.Warnf("received a custom zone with empty records, skipping domain: %s", customZone.Domain) + continue } - muxUpdates = append(muxUpdates, muxUpdate{ + muxUpdates = append(muxUpdates, handlerWrapper{ domain: customZone.Domain, handler: s.localResolver, priority: PriorityMatchDomain, }) + // group all records under this domain for _, record := range customZone.Records { var class uint16 = dns.ClassINET if record.Class != nbdns.DefaultClass { - return nil, nil, fmt.Errorf("received an invalid class type: %s", record.Class) + log.Warnf("received an invalid class type: %s", record.Class) + continue } + key := buildRecordKey(record.Name, class, uint16(record.Type)) - localRecords[key] = record + + localRecords[key] = append(localRecords[key], record) } } + return muxUpdates, localRecords, nil } -func (s *DefaultServer) buildUpstreamHandlerUpdate(nameServerGroups []*nbdns.NameServerGroup) ([]muxUpdate, error) { +func (s *DefaultServer) buildUpstreamHandlerUpdate(nameServerGroups []*nbdns.NameServerGroup) ([]handlerWrapper, error) { + var muxUpdates []handlerWrapper - var muxUpdates []muxUpdate for _, nsGroup := range nameServerGroups { if len(nsGroup.NameServers) == 0 { log.Warn("received a nameserver group with empty nameserver list") continue } + if !nsGroup.Primary && len(nsGroup.Domains) == 0 { + return nil, fmt.Errorf("received a non primary nameserver group with an empty domain list") + } + + for _, domain := range nsGroup.Domains { + if domain == "" { + return nil, fmt.Errorf("received a nameserver group with an empty domain element") + } + } + } + + groupedNS := groupNSGroupsByDomain(nameServerGroups) + + for _, domainGroup := range groupedNS { + basePriority := PriorityMatchDomain + if domainGroup.domain == nbdns.RootZone { + basePriority = PriorityDefault + } + + updates, err := s.createHandlersForDomainGroup(domainGroup, basePriority) + if err != nil { + return nil, err + } + muxUpdates = append(muxUpdates, updates...) + } + + return muxUpdates, nil +} + +func (s *DefaultServer) createHandlersForDomainGroup(domainGroup nsGroupsByDomain, basePriority int) ([]handlerWrapper, error) { + var muxUpdates []handlerWrapper + + for i, nsGroup := range domainGroup.groups { + // Decrement priority by handler index (0, 1, 2, ...) to avoid conflicts + priority := basePriority - i + + // Check if we're about to overlap with the next priority tier + if basePriority == PriorityMatchDomain && priority <= PriorityDefault { + log.Warnf("too many handlers for domain=%s, would overlap with default priority tier (diff=%d). Skipping remaining handlers", + domainGroup.domain, PriorityMatchDomain-PriorityDefault) + break + } + + log.Debugf("creating handler for domain=%s with priority=%d", domainGroup.domain, priority) handler, err := newUpstreamResolver( s.ctx, s.wgInterface.Name(), @@ -458,10 +553,12 @@ func (s *DefaultServer) buildUpstreamHandlerUpdate(nameServerGroups []*nbdns.Nam s.wgInterface.Address().Network, s.statusRecorder, s.hostsDNSHolder, + domainGroup.domain, ) if err != nil { - return nil, fmt.Errorf("unable to create a new upstream resolver, error: %v", err) + return nil, fmt.Errorf("create upstream resolver: %v", err) } + for _, ns := range nsGroup.NameServers { if ns.NSType != nbdns.UDPNameServerType { log.Warnf("skipping nameserver %s with type %s, this peer supports only %s", @@ -485,81 +582,51 @@ func (s *DefaultServer) buildUpstreamHandlerUpdate(nameServerGroups []*nbdns.Nam // after some period defined by upstream it tries to reactivate self by calling this hook // everything we need here is just to re-apply current configuration because it already // contains this upstream settings (temporal deactivation not removed it) - handler.deactivate, handler.reactivate = s.upstreamCallbacks(nsGroup, handler) + handler.deactivate, handler.reactivate = s.upstreamCallbacks(nsGroup, handler, priority) - if nsGroup.Primary { - muxUpdates = append(muxUpdates, muxUpdate{ - domain: nbdns.RootZone, - handler: handler, - priority: PriorityDefault, - }) - continue - } - - if len(nsGroup.Domains) == 0 { - handler.stop() - return nil, fmt.Errorf("received a non primary nameserver group with an empty domain list") - } - - for _, domain := range nsGroup.Domains { - if domain == "" { - handler.stop() - return nil, fmt.Errorf("received a nameserver group with an empty domain element") - } - muxUpdates = append(muxUpdates, muxUpdate{ - domain: domain, - handler: handler, - priority: PriorityMatchDomain, - }) - } + muxUpdates = append(muxUpdates, handlerWrapper{ + domain: domainGroup.domain, + handler: handler, + priority: priority, + }) } return muxUpdates, nil } -func (s *DefaultServer) updateMux(muxUpdates []muxUpdate) { - muxUpdateMap := make(registeredHandlerMap) - handlersByPriority := make(map[string]int) - - var isContainRootUpdate bool - - // First register new handlers - for _, update := range muxUpdates { - s.registerHandler([]string{update.domain}, update.handler, update.priority) - muxUpdateMap[update.domain] = update.handler - handlersByPriority[update.domain] = update.priority - - if existingHandler, ok := s.dnsMuxMap[update.domain]; ok { - existingHandler.stop() - } - - if update.domain == nbdns.RootZone { - isContainRootUpdate = true - } +func (s *DefaultServer) updateMux(muxUpdates []handlerWrapper) { + // this will introduce a short period of time when the server is not able to handle DNS requests + for _, existing := range s.dnsMuxMap { + s.deregisterHandler([]string{existing.domain}, existing.priority) + existing.handler.stop() } - // Then deregister old handlers not in the update - for key, existingHandler := range s.dnsMuxMap { - _, found := muxUpdateMap[key] - if !found { - if !isContainRootUpdate && key == nbdns.RootZone { + muxUpdateMap := make(registeredHandlerMap) + var containsRootUpdate bool + + for _, update := range muxUpdates { + if update.domain == nbdns.RootZone { + containsRootUpdate = true + } + s.registerHandler([]string{update.domain}, update.handler, update.priority) + muxUpdateMap[update.handler.id()] = update + } + + // If there's no root update and we had a root handler, restore it + if !containsRootUpdate { + for _, existing := range s.dnsMuxMap { + if existing.domain == nbdns.RootZone { s.addHostRootZone() - existingHandler.stop() - } else { - existingHandler.stop() - // Deregister with the priority that was used to register - if oldPriority, ok := s.handlerPriorities[key]; ok { - s.deregisterHandler([]string{key}, oldPriority) - } + break } } } s.dnsMuxMap = muxUpdateMap - s.handlerPriorities = handlersByPriority } -func (s *DefaultServer) updateLocalResolver(update map[string]nbdns.SimpleRecord) { +func (s *DefaultServer) updateLocalResolver(update map[string][]nbdns.SimpleRecord) { + // remove old records that are no longer present for key := range s.localResolver.registeredMap { _, found := update[key] if !found { @@ -568,12 +635,18 @@ func (s *DefaultServer) updateLocalResolver(update map[string]nbdns.SimpleRecord } updatedMap := make(registrationMap) - for key, record := range update { - err := s.localResolver.registerRecord(record) - if err != nil { - log.Warnf("got an error while registering the record (%s), error: %v", record.String(), err) + for _, recs := range update { + for _, rec := range recs { + // convert the record to a dns.RR and register + key, err := s.localResolver.registerRecord(rec) + if err != nil { + log.Warnf("got an error while registering the record (%s), error: %v", + rec.String(), err) + continue + } + + updatedMap[key] = struct{}{} } - updatedMap[key] = struct{}{} } s.localResolver.registeredMap = updatedMap @@ -589,6 +662,7 @@ func getNSHostPort(ns nbdns.NameServer) string { func (s *DefaultServer) upstreamCallbacks( nsGroup *nbdns.NameServerGroup, handler dns.Handler, + priority int, ) (deactivate func(error), reactivate func()) { var removeIndex map[string]int deactivate = func(err error) { @@ -605,18 +679,19 @@ func (s *DefaultServer) upstreamCallbacks( if nsGroup.Primary { removeIndex[nbdns.RootZone] = -1 s.currentConfig.RouteAll = false - s.deregisterHandler([]string{nbdns.RootZone}, PriorityDefault) + s.deregisterHandler([]string{nbdns.RootZone}, priority) } for i, item := range s.currentConfig.Domains { if _, found := removeIndex[item.Domain]; found { s.currentConfig.Domains[i].Disabled = true - s.deregisterHandler([]string{item.Domain}, PriorityMatchDomain) + s.deregisterHandler([]string{item.Domain}, priority) removeIndex[item.Domain] = i } } if err := s.hostManager.applyDNSConfig(s.currentConfig, s.stateManager); err != nil { + s.handleErrNoGroupaAll(err) l.Errorf("Failed to apply nameserver deactivation on the host: %v", err) } @@ -631,8 +706,8 @@ func (s *DefaultServer) upstreamCallbacks( } s.updateNSState(nsGroup, err, false) - } + reactivate = func() { s.mux.Lock() defer s.mux.Unlock() @@ -642,7 +717,7 @@ func (s *DefaultServer) upstreamCallbacks( continue } s.currentConfig.Domains[i].Disabled = false - s.registerHandler([]string{domain}, handler, PriorityMatchDomain) + s.registerHandler([]string{domain}, handler, priority) } l := log.WithField("nameservers", nsGroup.NameServers) @@ -650,11 +725,12 @@ func (s *DefaultServer) upstreamCallbacks( if nsGroup.Primary { s.currentConfig.RouteAll = true - s.registerHandler([]string{nbdns.RootZone}, handler, PriorityDefault) + s.registerHandler([]string{nbdns.RootZone}, handler, priority) } if s.hostManager != nil { if err := s.hostManager.applyDNSConfig(s.currentConfig, s.stateManager); err != nil { + s.handleErrNoGroupaAll(err) l.WithError(err).Error("reactivate temporary disabled nameserver group, DNS update apply") } } @@ -672,6 +748,7 @@ func (s *DefaultServer) addHostRootZone() { s.wgInterface.Address().Network, s.statusRecorder, s.hostsDNSHolder, + nbdns.RootZone, ) if err != nil { log.Errorf("unable to create a new upstream resolver, error: %v", err) @@ -728,5 +805,34 @@ func generateGroupKey(nsGroup *nbdns.NameServerGroup) string { for _, ns := range nsGroup.NameServers { servers = append(servers, fmt.Sprintf("%s:%d", ns.IP, ns.Port)) } - return fmt.Sprintf("%s_%s_%s", nsGroup.ID, nsGroup.Name, strings.Join(servers, ",")) + return fmt.Sprintf("%v_%v", servers, nsGroup.Domains) +} + +// groupNSGroupsByDomain groups nameserver groups by their match domains +func groupNSGroupsByDomain(nsGroups []*nbdns.NameServerGroup) []nsGroupsByDomain { + domainMap := make(map[string][]*nbdns.NameServerGroup) + + for _, group := range nsGroups { + if group.Primary { + domainMap[nbdns.RootZone] = append(domainMap[nbdns.RootZone], group) + continue + } + + for _, domain := range group.Domains { + if domain == "" { + continue + } + domainMap[domain] = append(domainMap[domain], group) + } + } + + var result []nsGroupsByDomain + for domain, groups := range domainMap { + result = append(result, nsGroupsByDomain{ + domain: domain, + groups: groups, + }) + } + + return result } diff --git a/client/internal/dns/server_test.go b/client/internal/dns/server_test.go index c166820c4..94b87124b 100644 --- a/client/internal/dns/server_test.go +++ b/client/internal/dns/server_test.go @@ -13,6 +13,7 @@ import ( "github.com/golang/mock/gomock" "github.com/miekg/dns" log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" @@ -88,6 +89,18 @@ func init() { formatter.SetTextFormatter(log.StandardLogger()) } +func generateDummyHandler(domain string, servers []nbdns.NameServer) *upstreamResolverBase { + var srvs []string + for _, srv := range servers { + srvs = append(srvs, getNSHostPort(srv)) + } + return &upstreamResolverBase{ + domain: domain, + upstreamServers: srvs, + cancel: func() {}, + } +} + func TestUpdateDNSServer(t *testing.T) { nameServers := []nbdns.NameServer{ { @@ -140,15 +153,37 @@ func TestUpdateDNSServer(t *testing.T) { }, }, }, - expectedUpstreamMap: registeredHandlerMap{"netbird.io": dummyHandler, "netbird.cloud": dummyHandler, nbdns.RootZone: dummyHandler}, - expectedLocalMap: registrationMap{buildRecordKey(zoneRecords[0].Name, 1, 1): struct{}{}}, + expectedUpstreamMap: registeredHandlerMap{ + generateDummyHandler("netbird.io", nameServers).id(): handlerWrapper{ + domain: "netbird.io", + handler: dummyHandler, + priority: PriorityMatchDomain, + }, + dummyHandler.id(): handlerWrapper{ + domain: "netbird.cloud", + handler: dummyHandler, + priority: PriorityMatchDomain, + }, + generateDummyHandler(".", nameServers).id(): handlerWrapper{ + domain: nbdns.RootZone, + handler: dummyHandler, + priority: PriorityDefault, + }, + }, + expectedLocalMap: registrationMap{buildRecordKey(zoneRecords[0].Name, 1, 1): struct{}{}}, }, { - name: "New Config Should Succeed", - initLocalMap: registrationMap{"netbird.cloud": struct{}{}}, - initUpstreamMap: registeredHandlerMap{buildRecordKey(zoneRecords[0].Name, 1, 1): dummyHandler}, - initSerial: 0, - inputSerial: 1, + name: "New Config Should Succeed", + initLocalMap: registrationMap{"netbird.cloud": struct{}{}}, + initUpstreamMap: registeredHandlerMap{ + generateDummyHandler(zoneRecords[0].Name, nameServers).id(): handlerWrapper{ + domain: buildRecordKey(zoneRecords[0].Name, 1, 1), + handler: dummyHandler, + priority: PriorityMatchDomain, + }, + }, + initSerial: 0, + inputSerial: 1, inputUpdate: nbdns.Config{ ServiceEnable: true, CustomZones: []nbdns.CustomZone{ @@ -164,8 +199,19 @@ func TestUpdateDNSServer(t *testing.T) { }, }, }, - expectedUpstreamMap: registeredHandlerMap{"netbird.io": dummyHandler, "netbird.cloud": dummyHandler}, - expectedLocalMap: registrationMap{buildRecordKey(zoneRecords[0].Name, 1, 1): struct{}{}}, + expectedUpstreamMap: registeredHandlerMap{ + generateDummyHandler("netbird.io", nameServers).id(): handlerWrapper{ + domain: "netbird.io", + handler: dummyHandler, + priority: PriorityMatchDomain, + }, + "local-resolver": handlerWrapper{ + domain: "netbird.cloud", + handler: dummyHandler, + priority: PriorityMatchDomain, + }, + }, + expectedLocalMap: registrationMap{buildRecordKey(zoneRecords[0].Name, 1, 1): struct{}{}}, }, { name: "Smaller Config Serial Should Be Skipped", @@ -220,7 +266,7 @@ func TestUpdateDNSServer(t *testing.T) { shouldFail: true, }, { - name: "Invalid Custom Zone Records list Should Fail", + name: "Invalid Custom Zone Records list Should Skip", initLocalMap: make(registrationMap), initUpstreamMap: make(registeredHandlerMap), initSerial: 0, @@ -239,12 +285,22 @@ func TestUpdateDNSServer(t *testing.T) { }, }, }, - shouldFail: true, + expectedUpstreamMap: registeredHandlerMap{generateDummyHandler(".", nameServers).id(): handlerWrapper{ + domain: ".", + handler: dummyHandler, + priority: PriorityDefault, + }}, }, { - name: "Empty Config Should Succeed and Clean Maps", - initLocalMap: registrationMap{"netbird.cloud": struct{}{}}, - initUpstreamMap: registeredHandlerMap{zoneRecords[0].Name: dummyHandler}, + name: "Empty Config Should Succeed and Clean Maps", + initLocalMap: registrationMap{"netbird.cloud": struct{}{}}, + initUpstreamMap: registeredHandlerMap{ + generateDummyHandler(zoneRecords[0].Name, nameServers).id(): handlerWrapper{ + domain: zoneRecords[0].Name, + handler: dummyHandler, + priority: PriorityMatchDomain, + }, + }, initSerial: 0, inputSerial: 1, inputUpdate: nbdns.Config{ServiceEnable: true}, @@ -252,9 +308,15 @@ func TestUpdateDNSServer(t *testing.T) { expectedLocalMap: make(registrationMap), }, { - name: "Disabled Service Should clean map", - initLocalMap: registrationMap{"netbird.cloud": struct{}{}}, - initUpstreamMap: registeredHandlerMap{zoneRecords[0].Name: dummyHandler}, + name: "Disabled Service Should clean map", + initLocalMap: registrationMap{"netbird.cloud": struct{}{}}, + initUpstreamMap: registeredHandlerMap{ + generateDummyHandler(zoneRecords[0].Name, nameServers).id(): handlerWrapper{ + domain: zoneRecords[0].Name, + handler: dummyHandler, + priority: PriorityMatchDomain, + }, + }, initSerial: 0, inputSerial: 1, inputUpdate: nbdns.Config{ServiceEnable: false}, @@ -294,7 +356,7 @@ func TestUpdateDNSServer(t *testing.T) { t.Log(err) } }() - dnsServer, err := NewDefaultServer(context.Background(), wgIface, "", &peer.Status{}, nil, false) + dnsServer, err := NewDefaultServer(context.Background(), wgIface, "", peer.NewRecorder("mgm"), nil, false) if err != nil { t.Fatal(err) } @@ -351,7 +413,7 @@ func TestDNSFakeResolverHandleUpdates(t *testing.T) { defer t.Setenv("NB_WG_KERNEL_DISABLED", ov) t.Setenv("NB_WG_KERNEL_DISABLED", "true") - newNet, err := stdnet.NewNet(nil) + newNet, err := stdnet.NewNet([]string{"utun2301"}) if err != nil { t.Errorf("create stdnet: %v", err) return @@ -403,7 +465,7 @@ func TestDNSFakeResolverHandleUpdates(t *testing.T) { return } - dnsServer, err := NewDefaultServer(context.Background(), wgIface, "", &peer.Status{}, nil, false) + dnsServer, err := NewDefaultServer(context.Background(), wgIface, "", peer.NewRecorder("mgm"), nil, false) if err != nil { t.Errorf("create DNS server: %v", err) return @@ -421,7 +483,13 @@ func TestDNSFakeResolverHandleUpdates(t *testing.T) { } }() - dnsServer.dnsMuxMap = registeredHandlerMap{zoneRecords[0].Name: &localResolver{}} + dnsServer.dnsMuxMap = registeredHandlerMap{ + "id1": handlerWrapper{ + domain: zoneRecords[0].Name, + handler: &localResolver{}, + priority: PriorityMatchDomain, + }, + } dnsServer.localResolver.registeredMap = registrationMap{"netbird.cloud": struct{}{}} dnsServer.updateSerial = 0 @@ -498,7 +566,7 @@ func TestDNSServerStartStop(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - dnsServer, err := NewDefaultServer(context.Background(), &mocWGIface{}, testCase.addrPort, &peer.Status{}, nil, false) + dnsServer, err := NewDefaultServer(context.Background(), &mocWGIface{}, testCase.addrPort, peer.NewRecorder("mgm"), nil, false) if err != nil { t.Fatalf("%v", err) } @@ -509,7 +577,7 @@ func TestDNSServerStartStop(t *testing.T) { } time.Sleep(100 * time.Millisecond) defer dnsServer.Stop() - err = dnsServer.localResolver.registerRecord(zoneRecords[0]) + _, err = dnsServer.localResolver.registerRecord(zoneRecords[0]) if err != nil { t.Error(err) } @@ -562,9 +630,8 @@ func TestDNSServerUpstreamDeactivateCallback(t *testing.T) { localResolver: &localResolver{ registeredMap: make(registrationMap), }, - handlerChain: NewHandlerChain(), - handlerPriorities: make(map[string]int), - hostManager: hostManager, + handlerChain: NewHandlerChain(), + hostManager: hostManager, currentConfig: HostDNSConfig{ Domains: []DomainConfig{ {false, "domain0", false}, @@ -572,7 +639,7 @@ func TestDNSServerUpstreamDeactivateCallback(t *testing.T) { {false, "domain2", false}, }, }, - statusRecorder: &peer.Status{}, + statusRecorder: peer.NewRecorder("mgm"), } var domainsUpdate string @@ -593,7 +660,7 @@ func TestDNSServerUpstreamDeactivateCallback(t *testing.T) { NameServers: []nbdns.NameServer{ {IP: netip.MustParseAddr("8.8.0.0"), NSType: nbdns.UDPNameServerType, Port: 53}, }, - }, nil) + }, nil, 0) deactivate(nil) expected := "domain0,domain2" @@ -633,7 +700,7 @@ func TestDNSPermanent_updateHostDNS_emptyUpstream(t *testing.T) { var dnsList []string dnsConfig := nbdns.Config{} - dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, dnsList, dnsConfig, nil, &peer.Status{}, false) + dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, dnsList, dnsConfig, nil, peer.NewRecorder("mgm"), false) err = dnsServer.Initialize() if err != nil { t.Errorf("failed to initialize DNS server: %v", err) @@ -657,7 +724,7 @@ func TestDNSPermanent_updateUpstream(t *testing.T) { } defer wgIFace.Close() dnsConfig := nbdns.Config{} - dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}, dnsConfig, nil, &peer.Status{}, false) + dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}, dnsConfig, nil, peer.NewRecorder("mgm"), false) err = dnsServer.Initialize() if err != nil { t.Errorf("failed to initialize DNS server: %v", err) @@ -749,7 +816,7 @@ func TestDNSPermanent_matchOnly(t *testing.T) { } defer wgIFace.Close() dnsConfig := nbdns.Config{} - dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}, dnsConfig, nil, &peer.Status{}, false) + dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}, dnsConfig, nil, peer.NewRecorder("mgm"), false) err = dnsServer.Initialize() if err != nil { t.Errorf("failed to initialize DNS server: %v", err) @@ -820,7 +887,7 @@ func createWgInterfaceWithBind(t *testing.T) (*iface.WGIface, error) { defer t.Setenv("NB_WG_KERNEL_DISABLED", ov) t.Setenv("NB_WG_KERNEL_DISABLED", "true") - newNet, err := stdnet.NewNet(nil) + newNet, err := stdnet.NewNet([]string{"utun2301"}) if err != nil { t.Fatalf("create stdnet: %v", err) return nil, err @@ -849,7 +916,7 @@ func createWgInterfaceWithBind(t *testing.T) (*iface.WGIface, error) { return nil, err } - pf, err := uspfilter.Create(wgIface) + pf, err := uspfilter.Create(wgIface, false) if err != nil { t.Fatalf("failed to create uspfilter: %v", err) return nil, err @@ -903,8 +970,8 @@ func TestHandlerChain_DomainPriorities(t *testing.T) { Subdomains: true, } - chain.AddHandler("example.com.", dnsRouteHandler, PriorityDNSRoute, nil) - chain.AddHandler("example.com.", upstreamHandler, PriorityMatchDomain, nil) + chain.AddHandler("example.com.", dnsRouteHandler, PriorityDNSRoute) + chain.AddHandler("example.com.", upstreamHandler, PriorityMatchDomain) testCases := []struct { name string @@ -959,3 +1026,421 @@ func TestHandlerChain_DomainPriorities(t *testing.T) { }) } } + +type mockHandler struct { + Id string +} + +func (m *mockHandler) ServeDNS(dns.ResponseWriter, *dns.Msg) {} +func (m *mockHandler) stop() {} +func (m *mockHandler) probeAvailability() {} +func (m *mockHandler) id() handlerID { return handlerID(m.Id) } + +type mockService struct{} + +func (m *mockService) Listen() error { return nil } +func (m *mockService) Stop() {} +func (m *mockService) RuntimeIP() string { return "127.0.0.1" } +func (m *mockService) RuntimePort() int { return 53 } +func (m *mockService) RegisterMux(string, dns.Handler) {} +func (m *mockService) DeregisterMux(string) {} + +func TestDefaultServer_UpdateMux(t *testing.T) { + baseMatchHandlers := registeredHandlerMap{ + "upstream-group1": { + domain: "example.com", + handler: &mockHandler{ + Id: "upstream-group1", + }, + priority: PriorityMatchDomain, + }, + "upstream-group2": { + domain: "example.com", + handler: &mockHandler{ + Id: "upstream-group2", + }, + priority: PriorityMatchDomain - 1, + }, + } + + baseRootHandlers := registeredHandlerMap{ + "upstream-root1": { + domain: ".", + handler: &mockHandler{ + Id: "upstream-root1", + }, + priority: PriorityDefault, + }, + "upstream-root2": { + domain: ".", + handler: &mockHandler{ + Id: "upstream-root2", + }, + priority: PriorityDefault - 1, + }, + } + + baseMixedHandlers := registeredHandlerMap{ + "upstream-group1": { + domain: "example.com", + handler: &mockHandler{ + Id: "upstream-group1", + }, + priority: PriorityMatchDomain, + }, + "upstream-group2": { + domain: "example.com", + handler: &mockHandler{ + Id: "upstream-group2", + }, + priority: PriorityMatchDomain - 1, + }, + "upstream-other": { + domain: "other.com", + handler: &mockHandler{ + Id: "upstream-other", + }, + priority: PriorityMatchDomain, + }, + } + + tests := []struct { + name string + initialHandlers registeredHandlerMap + updates []handlerWrapper + expectedHandlers map[string]string // map[handlerID]domain + description string + }{ + { + name: "Remove group1 from update", + initialHandlers: baseMatchHandlers, + updates: []handlerWrapper{ + // Only group2 remains + { + domain: "example.com", + handler: &mockHandler{ + Id: "upstream-group2", + }, + priority: PriorityMatchDomain - 1, + }, + }, + expectedHandlers: map[string]string{ + "upstream-group2": "example.com", + }, + description: "When group1 is not included in the update, it should be removed while group2 remains", + }, + { + name: "Remove group2 from update", + initialHandlers: baseMatchHandlers, + updates: []handlerWrapper{ + // Only group1 remains + { + domain: "example.com", + handler: &mockHandler{ + Id: "upstream-group1", + }, + priority: PriorityMatchDomain, + }, + }, + expectedHandlers: map[string]string{ + "upstream-group1": "example.com", + }, + description: "When group2 is not included in the update, it should be removed while group1 remains", + }, + { + name: "Add group3 in first position", + initialHandlers: baseMatchHandlers, + updates: []handlerWrapper{ + // Add group3 with highest priority + { + domain: "example.com", + handler: &mockHandler{ + Id: "upstream-group3", + }, + priority: PriorityMatchDomain + 1, + }, + // Keep existing groups with their original priorities + { + domain: "example.com", + handler: &mockHandler{ + Id: "upstream-group1", + }, + priority: PriorityMatchDomain, + }, + { + domain: "example.com", + handler: &mockHandler{ + Id: "upstream-group2", + }, + priority: PriorityMatchDomain - 1, + }, + }, + expectedHandlers: map[string]string{ + "upstream-group1": "example.com", + "upstream-group2": "example.com", + "upstream-group3": "example.com", + }, + description: "When adding group3 with highest priority, it should be first in chain while maintaining existing groups", + }, + { + name: "Add group3 in last position", + initialHandlers: baseMatchHandlers, + updates: []handlerWrapper{ + // Keep existing groups with their original priorities + { + domain: "example.com", + handler: &mockHandler{ + Id: "upstream-group1", + }, + priority: PriorityMatchDomain, + }, + { + domain: "example.com", + handler: &mockHandler{ + Id: "upstream-group2", + }, + priority: PriorityMatchDomain - 1, + }, + // Add group3 with lowest priority + { + domain: "example.com", + handler: &mockHandler{ + Id: "upstream-group3", + }, + priority: PriorityMatchDomain - 2, + }, + }, + expectedHandlers: map[string]string{ + "upstream-group1": "example.com", + "upstream-group2": "example.com", + "upstream-group3": "example.com", + }, + description: "When adding group3 with lowest priority, it should be last in chain while maintaining existing groups", + }, + // Root zone tests + { + name: "Remove root1 from update", + initialHandlers: baseRootHandlers, + updates: []handlerWrapper{ + { + domain: ".", + handler: &mockHandler{ + Id: "upstream-root2", + }, + priority: PriorityDefault - 1, + }, + }, + expectedHandlers: map[string]string{ + "upstream-root2": ".", + }, + description: "When root1 is not included in the update, it should be removed while root2 remains", + }, + { + name: "Remove root2 from update", + initialHandlers: baseRootHandlers, + updates: []handlerWrapper{ + { + domain: ".", + handler: &mockHandler{ + Id: "upstream-root1", + }, + priority: PriorityDefault, + }, + }, + expectedHandlers: map[string]string{ + "upstream-root1": ".", + }, + description: "When root2 is not included in the update, it should be removed while root1 remains", + }, + { + name: "Add root3 in first position", + initialHandlers: baseRootHandlers, + updates: []handlerWrapper{ + { + domain: ".", + handler: &mockHandler{ + Id: "upstream-root3", + }, + priority: PriorityDefault + 1, + }, + { + domain: ".", + handler: &mockHandler{ + Id: "upstream-root1", + }, + priority: PriorityDefault, + }, + { + domain: ".", + handler: &mockHandler{ + Id: "upstream-root2", + }, + priority: PriorityDefault - 1, + }, + }, + expectedHandlers: map[string]string{ + "upstream-root1": ".", + "upstream-root2": ".", + "upstream-root3": ".", + }, + description: "When adding root3 with highest priority, it should be first in chain while maintaining existing root handlers", + }, + { + name: "Add root3 in last position", + initialHandlers: baseRootHandlers, + updates: []handlerWrapper{ + { + domain: ".", + handler: &mockHandler{ + Id: "upstream-root1", + }, + priority: PriorityDefault, + }, + { + domain: ".", + handler: &mockHandler{ + Id: "upstream-root2", + }, + priority: PriorityDefault - 1, + }, + { + domain: ".", + handler: &mockHandler{ + Id: "upstream-root3", + }, + priority: PriorityDefault - 2, + }, + }, + expectedHandlers: map[string]string{ + "upstream-root1": ".", + "upstream-root2": ".", + "upstream-root3": ".", + }, + description: "When adding root3 with lowest priority, it should be last in chain while maintaining existing root handlers", + }, + // Mixed domain tests + { + name: "Update with mixed domains - remove one of duplicate domain", + initialHandlers: baseMixedHandlers, + updates: []handlerWrapper{ + { + domain: "example.com", + handler: &mockHandler{ + Id: "upstream-group1", + }, + priority: PriorityMatchDomain, + }, + { + domain: "other.com", + handler: &mockHandler{ + Id: "upstream-other", + }, + priority: PriorityMatchDomain, + }, + }, + expectedHandlers: map[string]string{ + "upstream-group1": "example.com", + "upstream-other": "other.com", + }, + description: "When updating mixed domains, should correctly handle removal of one duplicate while maintaining other domains", + }, + { + name: "Update with mixed domains - add new domain", + initialHandlers: baseMixedHandlers, + updates: []handlerWrapper{ + { + domain: "example.com", + handler: &mockHandler{ + Id: "upstream-group1", + }, + priority: PriorityMatchDomain, + }, + { + domain: "example.com", + handler: &mockHandler{ + Id: "upstream-group2", + }, + priority: PriorityMatchDomain - 1, + }, + { + domain: "other.com", + handler: &mockHandler{ + Id: "upstream-other", + }, + priority: PriorityMatchDomain, + }, + { + domain: "new.com", + handler: &mockHandler{ + Id: "upstream-new", + }, + priority: PriorityMatchDomain, + }, + }, + expectedHandlers: map[string]string{ + "upstream-group1": "example.com", + "upstream-group2": "example.com", + "upstream-other": "other.com", + "upstream-new": "new.com", + }, + description: "When updating mixed domains, should maintain existing duplicates and add new domain", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := &DefaultServer{ + dnsMuxMap: tt.initialHandlers, + handlerChain: NewHandlerChain(), + service: &mockService{}, + } + + // Perform the update + server.updateMux(tt.updates) + + // Verify the results + assert.Equal(t, len(tt.expectedHandlers), len(server.dnsMuxMap), + "Number of handlers after update doesn't match expected") + + // Check each expected handler + for id, expectedDomain := range tt.expectedHandlers { + handler, exists := server.dnsMuxMap[handlerID(id)] + assert.True(t, exists, "Expected handler %s not found", id) + if exists { + assert.Equal(t, expectedDomain, handler.domain, + "Domain mismatch for handler %s", id) + } + } + + // Verify no unexpected handlers exist + for handlerID := range server.dnsMuxMap { + _, expected := tt.expectedHandlers[string(handlerID)] + assert.True(t, expected, "Unexpected handler found: %s", handlerID) + } + + // Verify the handlerChain state and order + previousPriority := 0 + for _, chainEntry := range server.handlerChain.handlers { + // Verify priority order + if previousPriority > 0 { + assert.True(t, chainEntry.Priority <= previousPriority, + "Handlers in chain not properly ordered by priority") + } + previousPriority = chainEntry.Priority + + // Verify handler exists in mux + foundInMux := false + for _, muxEntry := range server.dnsMuxMap { + if chainEntry.Handler == muxEntry.handler && + chainEntry.Priority == muxEntry.priority && + chainEntry.Pattern == dns.Fqdn(muxEntry.domain) { + foundInMux = true + break + } + } + assert.True(t, foundInMux, + "Handler in chain not found in dnsMuxMap") + } + }) + } +} diff --git a/client/internal/dns/service_memory.go b/client/internal/dns/service_memory.go index 729b90cc0..250f3ab2e 100644 --- a/client/internal/dns/service_memory.go +++ b/client/internal/dns/service_memory.go @@ -2,7 +2,6 @@ package dns import ( "fmt" - "math/big" "net" "sync" @@ -10,6 +9,8 @@ import ( "github.com/google/gopacket/layers" "github.com/miekg/dns" log "github.com/sirupsen/logrus" + + nbnet "github.com/netbirdio/netbird/util/net" ) type ServiceViaMemory struct { @@ -27,7 +28,7 @@ func NewServiceViaMemory(wgIface WGIface) *ServiceViaMemory { wgInterface: wgIface, dnsMux: dns.NewServeMux(), - runtimeIP: getLastIPFromNetwork(wgIface.Address().Network, 1), + runtimeIP: nbnet.GetLastIPFromNetwork(wgIface.Address().Network, 1).String(), runtimePort: defaultPort, } return s @@ -118,22 +119,3 @@ func (s *ServiceViaMemory) filterDNSTraffic() (string, error) { return filter.AddUDPPacketHook(false, net.ParseIP(s.runtimeIP), uint16(s.runtimePort), hook), nil } - -func getLastIPFromNetwork(network *net.IPNet, fromEnd int) string { - // Calculate the last IP in the CIDR range - var endIP net.IP - for i := 0; i < len(network.IP); i++ { - endIP = append(endIP, network.IP[i]|^network.Mask[i]) - } - - // convert to big.Int - endInt := big.NewInt(0) - endInt.SetBytes(endIP) - - // subtract fromEnd from the last ip - fromEndBig := big.NewInt(int64(fromEnd)) - resultInt := big.NewInt(0) - resultInt.Sub(endInt, fromEndBig) - - return net.IP(resultInt.Bytes()).String() -} diff --git a/client/internal/dns/service_memory_test.go b/client/internal/dns/service_memory_test.go index bea4f4ce8..244adfaef 100644 --- a/client/internal/dns/service_memory_test.go +++ b/client/internal/dns/service_memory_test.go @@ -3,6 +3,8 @@ package dns import ( "net" "testing" + + nbnet "github.com/netbirdio/netbird/util/net" ) func TestGetLastIPFromNetwork(t *testing.T) { @@ -23,7 +25,7 @@ func TestGetLastIPFromNetwork(t *testing.T) { return } - lastIP := getLastIPFromNetwork(ipnet, 1) + lastIP := nbnet.GetLastIPFromNetwork(ipnet, 1).String() if lastIP != tt.ip { t.Errorf("wrong IP address, expected %s: got %s", tt.ip, lastIP) } diff --git a/client/internal/dns/systemd_linux.go b/client/internal/dns/systemd_linux.go index a031be582..a87cc73e5 100644 --- a/client/internal/dns/systemd_linux.go +++ b/client/internal/dns/systemd_linux.go @@ -154,6 +154,10 @@ func (s *systemdDbusConfigurator) applyDNSConfig(config HostDNSConfig, stateMana return nil } +func (s *systemdDbusConfigurator) string() string { + return "dbus" +} + func (s *systemdDbusConfigurator) setDomainsForInterface(domainsInput []systemdDbusLinkDomainsInput) error { err := s.callLinkMethod(systemdDbusSetDomainsMethodSuffix, domainsInput) if err != nil { diff --git a/client/internal/dns/unclean_shutdown_windows.go b/client/internal/dns/unclean_shutdown_windows.go index 74e40cc11..ab0b2cc63 100644 --- a/client/internal/dns/unclean_shutdown_windows.go +++ b/client/internal/dns/unclean_shutdown_windows.go @@ -6,6 +6,7 @@ import ( type ShutdownState struct { Guid string + GPO bool } func (s *ShutdownState) Name() string { @@ -13,9 +14,9 @@ func (s *ShutdownState) Name() string { } func (s *ShutdownState) Cleanup() error { - manager, err := newHostManagerWithGuid(s.Guid) - if err != nil { - return fmt.Errorf("create host manager: %w", err) + manager := ®istryConfigurator{ + guid: s.Guid, + gpo: s.GPO, } if err := manager.restoreUncleanShutdownDNS(); err != nil { diff --git a/client/internal/dns/upstream.go b/client/internal/dns/upstream.go index f0aa12b65..a22689cf9 100644 --- a/client/internal/dns/upstream.go +++ b/client/internal/dns/upstream.go @@ -2,9 +2,13 @@ package dns import ( "context" + "crypto/sha256" + "encoding/hex" "errors" "fmt" "net" + "slices" + "strings" "sync" "sync/atomic" "time" @@ -15,6 +19,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/client/proto" ) const ( @@ -40,6 +45,7 @@ type upstreamResolverBase struct { cancel context.CancelFunc upstreamClient upstreamClient upstreamServers []string + domain string disabled bool failsCount atomic.Int32 successCount atomic.Int32 @@ -53,12 +59,13 @@ type upstreamResolverBase struct { statusRecorder *peer.Status } -func newUpstreamResolverBase(ctx context.Context, statusRecorder *peer.Status) *upstreamResolverBase { +func newUpstreamResolverBase(ctx context.Context, statusRecorder *peer.Status, domain string) *upstreamResolverBase { ctx, cancel := context.WithCancel(ctx) return &upstreamResolverBase{ ctx: ctx, cancel: cancel, + domain: domain, upstreamTimeout: upstreamTimeout, reactivatePeriod: reactivatePeriod, failsTillDeact: failsTillDeact, @@ -71,6 +78,17 @@ func (u *upstreamResolverBase) String() string { return fmt.Sprintf("upstream %v", u.upstreamServers) } +// ID returns the unique handler ID +func (u *upstreamResolverBase) id() handlerID { + servers := slices.Clone(u.upstreamServers) + slices.Sort(servers) + + hash := sha256.New() + hash.Write([]byte(u.domain + ":")) + hash.Write([]byte(strings.Join(servers, ","))) + return handlerID("upstream-" + hex.EncodeToString(hash.Sum(nil)[:8])) +} + func (u *upstreamResolverBase) MatchSubdomains() bool { return true } @@ -87,7 +105,7 @@ func (u *upstreamResolverBase) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { u.checkUpstreamFails(err) }() - log.WithField("question", r.Question[0]).Trace("received an upstream question") + log.Tracef("received upstream question: domain=%s type=%v class=%v", r.Question[0].Name, r.Question[0].Qtype, r.Question[0].Qclass) // set the AuthenticatedData flag and the EDNS0 buffer size to 4096 bytes to support larger dns records if r.Extra == nil { r.SetEdns0(4096, false) @@ -96,6 +114,7 @@ func (u *upstreamResolverBase) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { select { case <-u.ctx.Done(): + log.Tracef("%s has been stopped", u) return default: } @@ -112,41 +131,36 @@ func (u *upstreamResolverBase) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { if err != nil { if errors.Is(err, context.DeadlineExceeded) || isTimeout(err) { - log.WithError(err).WithField("upstream", upstream). - Warn("got an error while connecting to upstream") + log.Warnf("upstream %s timed out for question domain=%s", upstream, r.Question[0].Name) continue } - u.failsCount.Add(1) - log.WithError(err).WithField("upstream", upstream). - Error("got other error while querying the upstream") - return + log.Warnf("failed to query upstream %s for question domain=%s: %s", upstream, r.Question[0].Name, err) + continue } - if rm == nil { - log.WithError(err).WithField("upstream", upstream). - Warn("no response from upstream") - return - } - // those checks need to be independent of each other due to memory address issues - if !rm.Response { - log.WithError(err).WithField("upstream", upstream). - Warn("no response from upstream") - return + if rm == nil || !rm.Response { + log.Warnf("no response from upstream %s for question domain=%s", upstream, r.Question[0].Name) + continue } u.successCount.Add(1) - log.Tracef("took %s to query the upstream %s", t, upstream) + log.Tracef("took %s to query the upstream %s for question domain=%s", t, upstream, r.Question[0].Name) - err = w.WriteMsg(rm) - if err != nil { - log.WithError(err).Error("got an error while writing the upstream resolver response") + if err = w.WriteMsg(rm); err != nil { + log.Errorf("failed to write DNS response for question domain=%s: %s", r.Question[0].Name, err) } // count the fails only if they happen sequentially u.failsCount.Store(0) return } u.failsCount.Add(1) - log.Error("all queries to the upstream nameservers failed with timeout") + log.Errorf("all queries to the %s failed for question domain=%s", u, r.Question[0].Name) + + m := new(dns.Msg) + m.SetRcode(r, dns.RcodeServerFailure) + if err := w.WriteMsg(m); err != nil { + log.Errorf("failed to write error response for %s for question domain=%s: %s", u, r.Question[0].Name, err) + } } // checkUpstreamFails counts fails and disables or enables upstream resolving @@ -169,6 +183,19 @@ func (u *upstreamResolverBase) checkUpstreamFails(err error) { } u.disable(err) + + if u.statusRecorder == nil { + return + } + + u.statusRecorder.PublishEvent( + proto.SystemEvent_WARNING, + proto.SystemEvent_DNS, + "All upstream servers failed (fail count exceeded)", + "Unable to reach one or more DNS servers. This might affect your ability to connect to some services.", + map[string]string{"upstreams": strings.Join(u.upstreamServers, ", ")}, + // TODO add domain meta + ) } // probeAvailability tests all upstream servers simultaneously and @@ -217,6 +244,18 @@ func (u *upstreamResolverBase) probeAvailability() { // didn't find a working upstream server, let's disable and try later if !success { u.disable(errors.ErrorOrNil()) + + if u.statusRecorder == nil { + return + } + + u.statusRecorder.PublishEvent( + proto.SystemEvent_WARNING, + proto.SystemEvent_DNS, + "All upstream servers failed (probe failed)", + "Unable to reach one or more DNS servers. This might affect your ability to connect to some services.", + map[string]string{"upstreams": strings.Join(u.upstreamServers, ", ")}, + ) } } diff --git a/client/internal/dns/upstream_android.go b/client/internal/dns/upstream_android.go index 36ea05e44..a9e46ca02 100644 --- a/client/internal/dns/upstream_android.go +++ b/client/internal/dns/upstream_android.go @@ -27,8 +27,9 @@ func newUpstreamResolver( _ *net.IPNet, statusRecorder *peer.Status, hostsDNSHolder *hostsDNSHolder, + domain string, ) (*upstreamResolver, error) { - upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder) + upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder, domain) c := &upstreamResolver{ upstreamResolverBase: upstreamResolverBase, hostsDNSHolder: hostsDNSHolder, diff --git a/client/internal/dns/upstream_general.go b/client/internal/dns/upstream_general.go index a29350f8c..51acbf7a6 100644 --- a/client/internal/dns/upstream_general.go +++ b/client/internal/dns/upstream_general.go @@ -23,8 +23,9 @@ func newUpstreamResolver( _ *net.IPNet, statusRecorder *peer.Status, _ *hostsDNSHolder, + domain string, ) (*upstreamResolver, error) { - upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder) + upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder, domain) nonIOS := &upstreamResolver{ upstreamResolverBase: upstreamResolverBase, } diff --git a/client/internal/dns/upstream_ios.go b/client/internal/dns/upstream_ios.go index 60ed79d87..7d3301e14 100644 --- a/client/internal/dns/upstream_ios.go +++ b/client/internal/dns/upstream_ios.go @@ -30,8 +30,9 @@ func newUpstreamResolver( net *net.IPNet, statusRecorder *peer.Status, _ *hostsDNSHolder, + domain string, ) (*upstreamResolverIOS, error) { - upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder) + upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder, domain) ios := &upstreamResolverIOS{ upstreamResolverBase: upstreamResolverBase, diff --git a/client/internal/dns/upstream_test.go b/client/internal/dns/upstream_test.go index c1251dcc1..c5adc0858 100644 --- a/client/internal/dns/upstream_test.go +++ b/client/internal/dns/upstream_test.go @@ -20,6 +20,7 @@ func TestUpstreamResolver_ServeDNS(t *testing.T) { timeout time.Duration cancelCTX bool expectedAnswer string + acceptNXDomain bool }{ { name: "Should Resolve A Record", @@ -36,11 +37,11 @@ func TestUpstreamResolver_ServeDNS(t *testing.T) { expectedAnswer: "1.1.1.1", }, { - name: "Should Not Resolve If Can't Connect To Both Servers", - inputMSG: new(dns.Msg).SetQuestion("one.one.one.one.", dns.TypeA), - InputServers: []string{"8.0.0.0:53", "8.0.0.1:53"}, - timeout: 200 * time.Millisecond, - responseShouldBeNil: true, + name: "Should Not Resolve If Can't Connect To Both Servers", + inputMSG: new(dns.Msg).SetQuestion("one.one.one.one.", dns.TypeA), + InputServers: []string{"8.0.0.0:53", "8.0.0.1:53"}, + timeout: 200 * time.Millisecond, + acceptNXDomain: true, }, { name: "Should Not Resolve If Parent Context Is Canceled", @@ -51,14 +52,11 @@ func TestUpstreamResolver_ServeDNS(t *testing.T) { responseShouldBeNil: true, }, } - // should resolve if first upstream times out - // should not write when both fails - // should not resolve if parent context is canceled for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) - resolver, _ := newUpstreamResolver(ctx, "", net.IP{}, &net.IPNet{}, nil, nil) + resolver, _ := newUpstreamResolver(ctx, "", net.IP{}, &net.IPNet{}, nil, nil, ".") resolver.upstreamServers = testCase.InputServers resolver.upstreamTimeout = testCase.timeout if testCase.cancelCTX { @@ -84,16 +82,22 @@ func TestUpstreamResolver_ServeDNS(t *testing.T) { t.Fatalf("should write a response message") } - foundAnswer := false - for _, answer := range responseMSG.Answer { - if strings.Contains(answer.String(), testCase.expectedAnswer) { - foundAnswer = true - break - } + if testCase.acceptNXDomain && responseMSG.Rcode == dns.RcodeNameError { + return } - if !foundAnswer { - t.Errorf("couldn't find the required answer, %s, in the dns response", testCase.expectedAnswer) + if testCase.expectedAnswer != "" { + foundAnswer := false + for _, answer := range responseMSG.Answer { + if strings.Contains(answer.String(), testCase.expectedAnswer) { + foundAnswer = true + break + } + } + + if !foundAnswer { + t.Errorf("couldn't find the required answer, %s, in the dns response", testCase.expectedAnswer) + } } }) } diff --git a/client/internal/dnsfwd/manager.go b/client/internal/dnsfwd/manager.go index e6dfd278e..5d3036dde 100644 --- a/client/internal/dnsfwd/manager.go +++ b/client/internal/dnsfwd/manager.go @@ -81,14 +81,14 @@ func (m *Manager) Stop(ctx context.Context) error { func (h *Manager) allowDNSFirewall() error { dport := &firewall.Port{ IsRange: false, - Values: []int{ListenPort}, + Values: []uint16{ListenPort}, } if h.firewall == nil { return nil } - dnsRules, err := h.firewall.AddPeerFiltering(net.IP{0, 0, 0, 0}, firewall.ProtocolUDP, nil, dport, firewall.RuleDirectionIN, firewall.ActionAccept, "", "") + dnsRules, err := h.firewall.AddPeerFiltering(net.IP{0, 0, 0, 0}, firewall.ProtocolUDP, nil, dport, firewall.ActionAccept, "", "") if err != nil { log.Errorf("failed to add allow DNS router rules, err: %v", err) return err diff --git a/client/internal/engine.go b/client/internal/engine.go index b50532b7d..c939240d9 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -13,20 +13,23 @@ import ( "sort" "strings" "sync" - "sync/atomic" "time" + "github.com/hashicorp/go-multierror" "github.com/pion/ice/v3" "github.com/pion/stun/v2" log "github.com/sirupsen/logrus" + "golang.zx2c4.com/wireguard/tun/netstack" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "google.golang.org/protobuf/proto" + nberrors "github.com/netbirdio/netbird/client/errors" "github.com/netbirdio/netbird/client/firewall" "github.com/netbirdio/netbird/client/firewall/manager" "github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/iface/bind" "github.com/netbirdio/netbird/client/iface/device" + nbnetstack "github.com/netbirdio/netbird/client/iface/netstack" "github.com/netbirdio/netbird/client/internal/acl" "github.com/netbirdio/netbird/client/internal/dns" "github.com/netbirdio/netbird/client/internal/dnsfwd" @@ -40,13 +43,14 @@ import ( "github.com/netbirdio/netbird/client/internal/routemanager" "github.com/netbirdio/netbird/client/internal/routemanager/systemops" "github.com/netbirdio/netbird/client/internal/statemanager" + cProto "github.com/netbirdio/netbird/client/proto" + "github.com/netbirdio/netbird/management/domain" semaphoregroup "github.com/netbirdio/netbird/util/semaphore-group" nbssh "github.com/netbirdio/netbird/client/ssh" "github.com/netbirdio/netbird/client/system" nbdns "github.com/netbirdio/netbird/dns" mgm "github.com/netbirdio/netbird/management/client" - "github.com/netbirdio/netbird/management/domain" mgmProto "github.com/netbirdio/netbird/management/proto" auth "github.com/netbirdio/netbird/relay/auth/hmac" relayClient "github.com/netbirdio/netbird/relay/client" @@ -113,6 +117,8 @@ type EngineConfig struct { DisableServerRoutes bool DisableDNS bool DisableFirewall bool + + BlockLANAccess bool } // Engine is a mechanism responsible for reacting on Signal and Management stream events and managing connections to the remote peers. @@ -141,7 +147,7 @@ type Engine struct { STUNs []*stun.URI // TURNs is a list of STUN servers used by ICE TURNs []*stun.URI - stunTurn atomic.Value + stunTurn icemaker.StunTurn clientCtx context.Context clientCancel context.CancelFunc @@ -149,7 +155,7 @@ type Engine struct { ctx context.Context cancel context.CancelFunc - wgInterface iface.IWGIface + wgInterface WGIface udpMux *bind.UniversalUDPMuxDefault @@ -170,8 +176,6 @@ type Engine struct { dnsServer dns.Server - probes *ProbeHolder - // checks are the client-applied posture checks that need to be evaluated on the client checks []*mgmProto.Checks @@ -191,7 +195,11 @@ type Peer struct { WgAllowedIps string } -// NewEngine creates a new Connection Engine +type localIpUpdater interface { + UpdateLocalIPs() error +} + +// NewEngine creates a new Connection Engine with probes attached func NewEngine( clientCtx context.Context, clientCancel context.CancelFunc, @@ -202,33 +210,6 @@ func NewEngine( mobileDep MobileDependency, statusRecorder *peer.Status, checks []*mgmProto.Checks, -) *Engine { - return NewEngineWithProbes( - clientCtx, - clientCancel, - signalClient, - mgmClient, - relayManager, - config, - mobileDep, - statusRecorder, - nil, - checks, - ) -} - -// NewEngineWithProbes creates a new Connection Engine with probes attached -func NewEngineWithProbes( - clientCtx context.Context, - clientCancel context.CancelFunc, - signalClient signal.Client, - mgmClient mgm.Client, - relayManager *relayClient.Manager, - config *EngineConfig, - mobileDep MobileDependency, - statusRecorder *peer.Status, - probes *ProbeHolder, - checks []*mgmProto.Checks, ) *Engine { engine := &Engine{ clientCtx: clientCtx, @@ -246,7 +227,6 @@ func NewEngineWithProbes( networkSerial: 0, sshServerFunc: nbssh.DefaultSSHServer, statusRecorder: statusRecorder, - probes: probes, checks: checks, connSemaphore: semaphoregroup.NewSemaphoreGroup(connInitLimit), } @@ -445,7 +425,6 @@ func (e *Engine) Start() error { e.receiveSignalEvents() e.receiveManagementEvents() - e.receiveProbeEvents() // starting network monitor at the very last to avoid disruptions e.startNetworkMonitor() @@ -460,7 +439,7 @@ func (e *Engine) createFirewall() error { } var err error - e.firewall, err = firewall.NewFirewall(e.wgInterface, e.stateManager) + e.firewall, err = firewall.NewFirewall(e.wgInterface, e.stateManager, e.config.DisableServerRoutes) if err != nil || e.firewall == nil { log.Errorf("failed creating firewall manager: %s", err) return nil @@ -481,12 +460,16 @@ func (e *Engine) initFirewall() error { } } + if e.config.BlockLANAccess { + e.blockLanAccess() + } + if e.rpManager == nil || !e.config.RosenpassEnabled { return nil } rosenpassPort := e.rpManager.GetAddress().Port - port := manager.Port{Values: []int{rosenpassPort}} + port := manager.Port{Values: []uint16{uint16(rosenpassPort)}} // this rule is static and will be torn down on engine down by the firewall manager if _, err := e.firewall.AddPeerFiltering( @@ -494,7 +477,6 @@ func (e *Engine) initFirewall() error { manager.ProtocolUDP, nil, &port, - manager.RuleDirectionIN, manager.ActionAccept, "", "", @@ -508,6 +490,35 @@ func (e *Engine) initFirewall() error { return nil } +func (e *Engine) blockLanAccess() { + var merr *multierror.Error + + // TODO: keep this updated + toBlock, err := getInterfacePrefixes() + if err != nil { + merr = multierror.Append(merr, fmt.Errorf("get local addresses: %w", err)) + } + + log.Infof("blocking route LAN access for networks: %v", toBlock) + v4 := netip.PrefixFrom(netip.IPv4Unspecified(), 0) + for _, network := range toBlock { + if _, err := e.firewall.AddRouteFiltering( + []netip.Prefix{v4}, + network, + manager.ProtocolALL, + nil, + nil, + manager.ActionDrop, + ); err != nil { + merr = multierror.Append(merr, fmt.Errorf("add fw rule for network %s: %w", network, err)) + } + } + + if merr != nil { + log.Warnf("encountered errors blocking IPs to block LAN access: %v", nberrors.FormatErrorOrNil(merr)) + } +} + // modifyPeers updates peers that have been modified (e.g. IP address has been changed). // It closes the existing connection, removes it from the peerConns map, and creates a new one. func (e *Engine) modifyPeers(peersUpdate []*mgmProto.RemotePeerConfig) error { @@ -602,8 +613,8 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error { e.syncMsgMux.Lock() defer e.syncMsgMux.Unlock() - if update.GetWiretrusteeConfig() != nil { - wCfg := update.GetWiretrusteeConfig() + if update.GetNetbirdConfig() != nil { + wCfg := update.GetNetbirdConfig() err := e.updateTURNs(wCfg.GetTurns()) if err != nil { return fmt.Errorf("update TURNs: %w", err) @@ -663,6 +674,8 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error { return err } + e.statusRecorder.PublishEvent(cProto.SystemEvent_INFO, cProto.SystemEvent_SYSTEM, "Network map updated", "", nil) + return nil } @@ -679,6 +692,15 @@ func (e *Engine) updateChecksIfNew(checks []*mgmProto.Checks) error { log.Warnf("failed to get system info with checks: %v", err) info = system.GetInfo(e.ctx) } + info.SetFlags( + e.config.RosenpassEnabled, + e.config.RosenpassPermissive, + &e.config.ServerSSHAllowed, + e.config.DisableClientRoutes, + e.config.DisableServerRoutes, + e.config.DisableDNS, + e.config.DisableFirewall, + ) if err := e.mgmClient.SyncMeta(info); err != nil { log.Errorf("could not sync meta: error %s", err) @@ -699,18 +721,22 @@ func (e *Engine) updateSSH(sshConf *mgmProto.SSHConfig) error { } else { if sshConf.GetSshEnabled() { - if runtime.GOOS == "windows" || runtime.GOOS == "freebsd" { + if runtime.GOOS == "windows" { log.Warnf("running SSH server on %s is not supported", runtime.GOOS) return nil } // start SSH server if it wasn't running if isNil(e.sshServer) { + listenAddr := fmt.Sprintf("%s:%d", e.wgInterface.Address().IP.String(), nbssh.DefaultSSHPort) + if nbnetstack.IsEnabled() { + listenAddr = fmt.Sprintf("127.0.0.1:%d", nbssh.DefaultSSHPort) + } // nil sshServer means it has not yet been started var err error - e.sshServer, err = e.sshServerFunc(e.config.SSHKey, - fmt.Sprintf("%s:%d", e.wgInterface.Address().IP.String(), nbssh.DefaultSSHPort)) + e.sshServer, err = e.sshServerFunc(e.config.SSHKey, listenAddr) + if err != nil { - return err + return fmt.Errorf("create ssh server: %w", err) } go func() { // blocking @@ -759,7 +785,7 @@ func (e *Engine) updateConfig(conf *mgmProto.PeerConfig) error { if conf.GetSshConfig() != nil { err := e.updateSSH(conf.GetSshConfig()) if err != nil { - log.Warnf("failed handling SSH server setup %v", err) + log.Warnf("failed handling SSH server setup: %v", err) } } @@ -783,6 +809,15 @@ func (e *Engine) receiveManagementEvents() { log.Warnf("failed to get system info with checks: %v", err) info = system.GetInfo(e.ctx) } + info.SetFlags( + e.config.RosenpassEnabled, + e.config.RosenpassPermissive, + &e.config.ServerSSHAllowed, + e.config.DisableClientRoutes, + e.config.DisableServerRoutes, + e.config.DisableDNS, + e.config.DisableFirewall, + ) // err = e.mgmClient.Sync(info, e.handleSync) err = e.mgmClient.Sync(e.ctx, info, e.handleSync) @@ -856,6 +891,14 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error { e.acl.ApplyFiltering(networkMap) } + if e.firewall != nil { + if localipfw, ok := e.firewall.(localIpUpdater); ok { + if err := localipfw.UpdateLocalIPs(); err != nil { + log.Errorf("failed to update local IPs: %v", err) + } + } + } + // DNS forwarder dnsRouteFeatureFlag := toDNSFeatureFlag(networkMap) dnsRouteDomains := toRouteDomains(e.config.WgPrivateKey.PublicKey().String(), networkMap.GetRoutes()) @@ -913,7 +956,7 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error { protoDNSConfig = &mgmProto.DNSConfig{} } - if err := e.dnsServer.UpdateDNSServer(serial, toDNSConfig(protoDNSConfig)); err != nil { + if err := e.dnsServer.UpdateDNSServer(serial, toDNSConfig(protoDNSConfig, e.wgInterface.Address().Network)); err != nil { log.Errorf("failed to update dns server, err: %v", err) } @@ -982,7 +1025,7 @@ func toRouteDomains(myPubKey string, protoRoutes []*mgmProto.Route) []string { return dnsRoutes } -func toDNSConfig(protoDNSConfig *mgmProto.DNSConfig) nbdns.Config { +func toDNSConfig(protoDNSConfig *mgmProto.DNSConfig, network *net.IPNet) nbdns.Config { dnsUpdate := nbdns.Config{ ServiceEnable: protoDNSConfig.GetServiceEnable(), CustomZones: make([]nbdns.CustomZone, 0), @@ -1022,6 +1065,11 @@ func toDNSConfig(protoDNSConfig *mgmProto.DNSConfig) nbdns.Config { } dnsUpdate.NameServerGroups = append(dnsUpdate.NameServerGroups, dnsNSGroup) } + + if len(dnsUpdate.CustomZones) > 0 { + addReverseZone(&dnsUpdate, network) + } + return dnsUpdate } @@ -1313,12 +1361,22 @@ func (e *Engine) close() { func (e *Engine) readInitialSettings() ([]*route.Route, *nbdns.Config, error) { info := system.GetInfo(e.ctx) + info.SetFlags( + e.config.RosenpassEnabled, + e.config.RosenpassPermissive, + &e.config.ServerSSHAllowed, + e.config.DisableClientRoutes, + e.config.DisableServerRoutes, + e.config.DisableDNS, + e.config.DisableFirewall, + ) + netMap, err := e.mgmClient.GetNetworkMap(info) if err != nil { return nil, nil, err } routes := toRoutes(netMap.GetRoutes()) - dnsCfg := toDNSConfig(netMap.GetDNSConfig()) + dnsCfg := toDNSConfig(netMap.GetDNSConfig(), e.wgInterface.Address().Network) return routes, &dnsCfg, nil } @@ -1409,6 +1467,11 @@ func (e *Engine) GetRouteManager() routemanager.Manager { return e.routeManager } +// GetFirewallManager returns the firewall manager +func (e *Engine) GetFirewallManager() manager.Manager { + return e.firewall +} + func findIPFromInterfaceName(ifaceName string) (net.IP, error) { iface, err := net.InterfaceByName(ifaceName) if err != nil { @@ -1444,72 +1507,58 @@ func (e *Engine) getRosenpassAddr() string { return "" } -func (e *Engine) receiveProbeEvents() { - if e.probes == nil { - return +// RunHealthProbes executes health checks for Signal, Management, Relay and WireGuard services +// and updates the status recorder with the latest states. +func (e *Engine) RunHealthProbes() bool { + signalHealthy := e.signal.IsHealthy() + log.Debugf("signal health check: healthy=%t", signalHealthy) + + managementHealthy := e.mgmClient.IsHealthy() + log.Debugf("management health check: healthy=%t", managementHealthy) + + results := append(e.probeSTUNs(), e.probeTURNs()...) + e.statusRecorder.UpdateRelayStates(results) + + relayHealthy := true + for _, res := range results { + if res.Err != nil { + relayHealthy = false + break + } } - if e.probes.SignalProbe != nil { - go e.probes.SignalProbe.Receive(e.ctx, func() bool { - healthy := e.signal.IsHealthy() - log.Debugf("received signal probe request, healthy: %t", healthy) - return healthy - }) + log.Debugf("relay health check: healthy=%t", relayHealthy) + + for _, key := range e.peerStore.PeersPubKey() { + wgStats, err := e.wgInterface.GetStats(key) + if err != nil { + log.Debugf("failed to get wg stats for peer %s: %s", key, err) + continue + } + // wgStats could be zero value, in which case we just reset the stats + if err := e.statusRecorder.UpdateWireGuardPeerState(key, wgStats); err != nil { + log.Debugf("failed to update wg stats for peer %s: %s", key, err) + } } - if e.probes.MgmProbe != nil { - go e.probes.MgmProbe.Receive(e.ctx, func() bool { - healthy := e.mgmClient.IsHealthy() - log.Debugf("received management probe request, healthy: %t", healthy) - return healthy - }) - } - - if e.probes.RelayProbe != nil { - go e.probes.RelayProbe.Receive(e.ctx, func() bool { - healthy := true - - results := append(e.probeSTUNs(), e.probeTURNs()...) - e.statusRecorder.UpdateRelayStates(results) - - // A single failed server will result in a "failed" probe - for _, res := range results { - if res.Err != nil { - healthy = false - break - } - } - - log.Debugf("received relay probe request, healthy: %t", healthy) - return healthy - }) - } - - if e.probes.WgProbe != nil { - go e.probes.WgProbe.Receive(e.ctx, func() bool { - log.Debug("received wg probe request") - - for _, key := range e.peerStore.PeersPubKey() { - wgStats, err := e.wgInterface.GetStats(key) - if err != nil { - log.Debugf("failed to get wg stats for peer %s: %s", key, err) - } - // wgStats could be zero value, in which case we just reset the stats - if err := e.statusRecorder.UpdateWireGuardPeerState(key, wgStats); err != nil { - log.Debugf("failed to update wg stats for peer %s: %s", key, err) - } - } - - return true - }) - } + allHealthy := signalHealthy && managementHealthy && relayHealthy + log.Debugf("all health checks completed: healthy=%t", allHealthy) + return allHealthy } func (e *Engine) probeSTUNs() []relay.ProbeResult { - return relay.ProbeAll(e.ctx, relay.ProbeSTUN, e.STUNs) + e.syncMsgMux.Lock() + stuns := slices.Clone(e.STUNs) + e.syncMsgMux.Unlock() + + return relay.ProbeAll(e.ctx, relay.ProbeSTUN, stuns) } func (e *Engine) probeTURNs() []relay.ProbeResult { - return relay.ProbeAll(e.ctx, relay.ProbeTURN, e.TURNs) + e.syncMsgMux.Lock() + turns := slices.Clone(e.TURNs) + e.syncMsgMux.Unlock() + + return relay.ProbeAll(e.ctx, relay.ProbeTURN, turns) } func (e *Engine) restartEngine() { @@ -1634,6 +1683,14 @@ func (e *Engine) GetLatestNetworkMap() (*mgmProto.NetworkMap, error) { return nm, nil } +// GetWgAddr returns the wireguard address +func (e *Engine) GetWgAddr() net.IP { + if e.wgInterface == nil { + return nil + } + return e.wgInterface.Address().IP +} + // updateDNSForwarder start or stop the DNS forwarder based on the domains and the feature flag func (e *Engine) updateDNSForwarder(enabled bool, domains []string) { if !enabled { @@ -1668,6 +1725,37 @@ func (e *Engine) updateDNSForwarder(enabled bool, domains []string) { } } +func (e *Engine) GetNet() (*netstack.Net, error) { + e.syncMsgMux.Lock() + intf := e.wgInterface + e.syncMsgMux.Unlock() + if intf == nil { + return nil, errors.New("wireguard interface not initialized") + } + + nsnet := intf.GetNet() + if nsnet == nil { + return nil, errors.New("failed to get netstack") + } + return nsnet, nil +} + +func (e *Engine) Address() (netip.Addr, error) { + e.syncMsgMux.Lock() + intf := e.wgInterface + e.syncMsgMux.Unlock() + if intf == nil { + return netip.Addr{}, errors.New("wireguard interface not initialized") + } + + addr := e.wgInterface.Address() + ip, ok := netip.AddrFromSlice(addr.IP) + if !ok { + return netip.Addr{}, errors.New("failed to convert address to netip.Addr") + } + return ip.Unmap(), nil +} + // isChecksEqual checks if two slices of checks are equal. func isChecksEqual(checks []*mgmProto.Checks, oChecks []*mgmProto.Checks) bool { for _, check := range checks { @@ -1685,3 +1773,45 @@ func isChecksEqual(checks []*mgmProto.Checks, oChecks []*mgmProto.Checks) bool { return slices.Equal(checks.Files, oChecks.Files) }) } + +func getInterfacePrefixes() ([]netip.Prefix, error) { + ifaces, err := net.Interfaces() + if err != nil { + return nil, fmt.Errorf("get interfaces: %w", err) + } + + var prefixes []netip.Prefix + var merr *multierror.Error + + for _, iface := range ifaces { + addrs, err := iface.Addrs() + if err != nil { + merr = multierror.Append(merr, fmt.Errorf("get addresses for interface %s: %w", iface.Name, err)) + continue + } + for _, addr := range addrs { + ipNet, ok := addr.(*net.IPNet) + if !ok { + merr = multierror.Append(merr, fmt.Errorf("cast address to IPNet: %v", addr)) + continue + } + addr, ok := netip.AddrFromSlice(ipNet.IP) + if !ok { + merr = multierror.Append(merr, fmt.Errorf("cast IPNet to netip.Addr: %v", ipNet.IP)) + continue + } + ones, _ := ipNet.Mask.Size() + prefix := netip.PrefixFrom(addr.Unmap(), ones).Masked() + ip := prefix.Addr() + + // TODO: add IPv6 + if !ip.Is4() || ip.IsLoopback() || ip.IsMulticast() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() { + continue + } + + prefixes = append(prefixes, prefix) + } + } + + return prefixes, nberrors.FormatErrorOrNil(merr) +} diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index 1deea1cb8..02c8edea7 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -22,11 +22,15 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/keepalive" - "github.com/netbirdio/management-integrations/integrations" + wgdevice "golang.zx2c4.com/wireguard/device" + "golang.zx2c4.com/wireguard/tun/netstack" + "github.com/netbirdio/management-integrations/integrations" "github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/iface/bind" + "github.com/netbirdio/netbird/client/iface/configurer" "github.com/netbirdio/netbird/client/iface/device" + "github.com/netbirdio/netbird/client/iface/wgproxy" "github.com/netbirdio/netbird/client/internal/dns" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/peer/guard" @@ -64,6 +68,114 @@ var ( } ) +type MockWGIface struct { + CreateFunc func() error + CreateOnAndroidFunc func(routeRange []string, ip string, domains []string) error + IsUserspaceBindFunc func() bool + NameFunc func() string + AddressFunc func() device.WGAddress + ToInterfaceFunc func() *net.Interface + UpFunc func() (*bind.UniversalUDPMuxDefault, error) + UpdateAddrFunc func(newAddr string) error + UpdatePeerFunc func(peerKey string, allowedIps string, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error + RemovePeerFunc func(peerKey string) error + AddAllowedIPFunc func(peerKey string, allowedIP string) error + RemoveAllowedIPFunc func(peerKey string, allowedIP string) error + CloseFunc func() error + SetFilterFunc func(filter device.PacketFilter) error + GetFilterFunc func() device.PacketFilter + GetDeviceFunc func() *device.FilteredDevice + GetWGDeviceFunc func() *wgdevice.Device + GetStatsFunc func(peerKey string) (configurer.WGStats, error) + GetInterfaceGUIDStringFunc func() (string, error) + GetProxyFunc func() wgproxy.Proxy + GetNetFunc func() *netstack.Net +} + +func (m *MockWGIface) GetInterfaceGUIDString() (string, error) { + return m.GetInterfaceGUIDStringFunc() +} + +func (m *MockWGIface) Create() error { + return m.CreateFunc() +} + +func (m *MockWGIface) CreateOnAndroid(routeRange []string, ip string, domains []string) error { + return m.CreateOnAndroidFunc(routeRange, ip, domains) +} + +func (m *MockWGIface) IsUserspaceBind() bool { + return m.IsUserspaceBindFunc() +} + +func (m *MockWGIface) Name() string { + return m.NameFunc() +} + +func (m *MockWGIface) Address() device.WGAddress { + return m.AddressFunc() +} + +func (m *MockWGIface) ToInterface() *net.Interface { + return m.ToInterfaceFunc() +} + +func (m *MockWGIface) Up() (*bind.UniversalUDPMuxDefault, error) { + return m.UpFunc() +} + +func (m *MockWGIface) UpdateAddr(newAddr string) error { + return m.UpdateAddrFunc(newAddr) +} + +func (m *MockWGIface) UpdatePeer(peerKey string, allowedIps string, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error { + return m.UpdatePeerFunc(peerKey, allowedIps, keepAlive, endpoint, preSharedKey) +} + +func (m *MockWGIface) RemovePeer(peerKey string) error { + return m.RemovePeerFunc(peerKey) +} + +func (m *MockWGIface) AddAllowedIP(peerKey string, allowedIP string) error { + return m.AddAllowedIPFunc(peerKey, allowedIP) +} + +func (m *MockWGIface) RemoveAllowedIP(peerKey string, allowedIP string) error { + return m.RemoveAllowedIPFunc(peerKey, allowedIP) +} + +func (m *MockWGIface) Close() error { + return m.CloseFunc() +} + +func (m *MockWGIface) SetFilter(filter device.PacketFilter) error { + return m.SetFilterFunc(filter) +} + +func (m *MockWGIface) GetFilter() device.PacketFilter { + return m.GetFilterFunc() +} + +func (m *MockWGIface) GetDevice() *device.FilteredDevice { + return m.GetDeviceFunc() +} + +func (m *MockWGIface) GetWGDevice() *wgdevice.Device { + return m.GetWGDeviceFunc() +} + +func (m *MockWGIface) GetStats(peerKey string) (configurer.WGStats, error) { + return m.GetStatsFunc(peerKey) +} + +func (m *MockWGIface) GetProxy() wgproxy.Proxy { + return m.GetProxyFunc() +} + +func (m *MockWGIface) GetNet() *netstack.Net { + return m.GetNetFunc() +} + func TestMain(m *testing.M) { _ = util.InitLog("debug", "console") code := m.Run() @@ -71,8 +183,7 @@ func TestMain(m *testing.M) { } func TestEngine_SSH(t *testing.T) { - // todo resolve test execution on freebsd - if runtime.GOOS == "windows" || runtime.GOOS == "freebsd" { + if runtime.GOOS == "windows" { t.Skip("skipping TestEngine_SSH") } @@ -246,11 +357,20 @@ func TestEngine_UpdateNetworkMap(t *testing.T) { peer.NewRecorder("https://mgm"), nil) - wgIface := &iface.MockWGIface{ + wgIface := &MockWGIface{ NameFunc: func() string { return "utun102" }, RemovePeerFunc: func(peerKey string) error { return nil }, + AddressFunc: func() iface.WGAddress { + return iface.WGAddress{ + IP: net.ParseIP("10.20.0.1"), + Network: &net.IPNet{ + IP: net.ParseIP("10.20.0.0"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, + } + }, } engine.wgInterface = wgIface engine.routeManager = routemanager.NewManager(routemanager.ManagerConfig{ @@ -693,6 +813,9 @@ func TestEngine_UpdateNetworkMapWithDNSUpdate(t *testing.T) { }, }, }, + { + Domain: "0.66.100.in-addr.arpa.", + }, }, NameServerGroups: []*mgmtProto.NameServerGroup{ { @@ -722,6 +845,9 @@ func TestEngine_UpdateNetworkMapWithDNSUpdate(t *testing.T) { }, }, }, + { + Domain: "0.66.100.in-addr.arpa.", + }, }, expectedNSGroupsLen: 1, expectedNSGroups: []*nbdns.NameServerGroup{ @@ -1131,7 +1257,7 @@ func createEngine(ctx context.Context, cancel context.CancelFunc, setupKey strin } info := system.GetInfo(ctx) - resp, err := mgmtClient.Register(*publicKey, setupKey, "", info, nil) + resp, err := mgmtClient.Register(*publicKey, setupKey, "", info, nil, nil) if err != nil { return nil, err } @@ -1227,7 +1353,7 @@ func startManagement(t *testing.T, dataDir, testFile string) (*grpc.Server, stri } secretsManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay) - mgmtServer, err := server.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil) + mgmtServer, err := server.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil, nil) if err != nil { return nil, "", err } diff --git a/client/internal/iface.go b/client/internal/iface.go new file mode 100644 index 000000000..bd0069c19 --- /dev/null +++ b/client/internal/iface.go @@ -0,0 +1,8 @@ +//go:build !windows +// +build !windows + +package internal + +type WGIface interface { + wgIfaceBase +} diff --git a/client/iface/iwginterface.go b/client/internal/iface_common.go similarity index 84% rename from client/iface/iwginterface.go rename to client/internal/iface_common.go index f5ab29539..a66342707 100644 --- a/client/iface/iwginterface.go +++ b/client/internal/iface_common.go @@ -1,11 +1,11 @@ -//go:build !windows - -package iface +package internal import ( "net" "time" + wgdevice "golang.zx2c4.com/wireguard/device" + "golang.zx2c4.com/wireguard/tun/netstack" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "github.com/netbirdio/netbird/client/iface/bind" @@ -14,7 +14,7 @@ import ( "github.com/netbirdio/netbird/client/iface/wgproxy" ) -type IWGIface interface { +type wgIfaceBase interface { Create() error CreateOnAndroid(routeRange []string, ip string, domains []string) error IsUserspaceBind() bool @@ -32,5 +32,7 @@ type IWGIface interface { SetFilter(filter device.PacketFilter) error GetFilter() device.PacketFilter GetDevice() *device.FilteredDevice + GetWGDevice() *wgdevice.Device GetStats(peerKey string) (configurer.WGStats, error) + GetNet() *netstack.Net } diff --git a/client/internal/iface_windows.go b/client/internal/iface_windows.go new file mode 100644 index 000000000..113217815 --- /dev/null +++ b/client/internal/iface_windows.go @@ -0,0 +1,6 @@ +package internal + +type WGIface interface { + wgIfaceBase + GetInterfaceGUIDString() (string, error) +} diff --git a/client/internal/login.go b/client/internal/login.go index 9b6cf7848..395a17199 100644 --- a/client/internal/login.go +++ b/client/internal/login.go @@ -17,8 +17,9 @@ import ( ) // IsLoginRequired check that the server is support SSO or not -func IsLoginRequired(ctx context.Context, privateKey string, mgmURL *url.URL, sshKey string) (bool, error) { - mgmClient, err := getMgmClient(ctx, privateKey, mgmURL) +func IsLoginRequired(ctx context.Context, config *Config) (bool, error) { + mgmURL := config.ManagementURL + mgmClient, err := getMgmClient(ctx, config.PrivateKey, mgmURL) if err != nil { return false, err } @@ -33,12 +34,12 @@ func IsLoginRequired(ctx context.Context, privateKey string, mgmURL *url.URL, ss }() log.Debugf("connected to the Management service %s", mgmURL.String()) - pubSSHKey, err := ssh.GeneratePublicKey([]byte(sshKey)) + pubSSHKey, err := ssh.GeneratePublicKey([]byte(config.SSHKey)) if err != nil { return false, err } - _, err = doMgmLogin(ctx, mgmClient, pubSSHKey) + _, err = doMgmLogin(ctx, mgmClient, pubSSHKey, config) if isLoginNeeded(err) { return true, nil } @@ -67,10 +68,10 @@ func Login(ctx context.Context, config *Config, setupKey string, jwtToken string return err } - serverKey, err := doMgmLogin(ctx, mgmClient, pubSSHKey) + serverKey, err := doMgmLogin(ctx, mgmClient, pubSSHKey, config) if serverKey != nil && isRegistrationNeeded(err) { log.Debugf("peer registration required") - _, err = registerPeer(ctx, *serverKey, mgmClient, setupKey, jwtToken, pubSSHKey) + _, err = registerPeer(ctx, *serverKey, mgmClient, setupKey, jwtToken, pubSSHKey, config) return err } @@ -99,7 +100,7 @@ func getMgmClient(ctx context.Context, privateKey string, mgmURL *url.URL) (*mgm return mgmClient, err } -func doMgmLogin(ctx context.Context, mgmClient *mgm.GrpcClient, pubSSHKey []byte) (*wgtypes.Key, error) { +func doMgmLogin(ctx context.Context, mgmClient *mgm.GrpcClient, pubSSHKey []byte, config *Config) (*wgtypes.Key, error) { serverKey, err := mgmClient.GetServerPublicKey() if err != nil { log.Errorf("failed while getting Management Service public key: %v", err) @@ -107,13 +108,22 @@ func doMgmLogin(ctx context.Context, mgmClient *mgm.GrpcClient, pubSSHKey []byte } sysInfo := system.GetInfo(ctx) - _, err = mgmClient.Login(*serverKey, sysInfo, pubSSHKey) + sysInfo.SetFlags( + config.RosenpassEnabled, + config.RosenpassPermissive, + config.ServerSSHAllowed, + config.DisableClientRoutes, + config.DisableServerRoutes, + config.DisableDNS, + config.DisableFirewall, + ) + _, err = mgmClient.Login(*serverKey, sysInfo, pubSSHKey, config.DNSLabels) return serverKey, err } // registerPeer checks whether setupKey was provided via cmd line and if not then it prompts user to enter a key. // Otherwise tries to register with the provided setupKey via command line. -func registerPeer(ctx context.Context, serverPublicKey wgtypes.Key, client *mgm.GrpcClient, setupKey string, jwtToken string, pubSSHKey []byte) (*mgmProto.LoginResponse, error) { +func registerPeer(ctx context.Context, serverPublicKey wgtypes.Key, client *mgm.GrpcClient, setupKey string, jwtToken string, pubSSHKey []byte, config *Config) (*mgmProto.LoginResponse, error) { validSetupKey, err := uuid.Parse(setupKey) if err != nil && jwtToken == "" { return nil, status.Errorf(codes.InvalidArgument, "invalid setup-key or no sso information provided, err: %v", err) @@ -121,7 +131,16 @@ func registerPeer(ctx context.Context, serverPublicKey wgtypes.Key, client *mgm. log.Debugf("sending peer registration request to Management Service") info := system.GetInfo(ctx) - loginResp, err := client.Register(serverPublicKey, validSetupKey.String(), jwtToken, info, pubSSHKey) + info.SetFlags( + config.RosenpassEnabled, + config.RosenpassPermissive, + config.ServerSSHAllowed, + config.DisableClientRoutes, + config.DisableServerRoutes, + config.DisableDNS, + config.DisableFirewall, + ) + loginResp, err := client.Register(serverPublicKey, validSetupKey.String(), jwtToken, info, pubSSHKey, config.DNSLabels) if err != nil { log.Errorf("failed registering peer %v,%s", err, validSetupKey.String()) return nil, err diff --git a/client/internal/peer/conn.go b/client/internal/peer/conn.go index b8cb2582f..0337960bb 100644 --- a/client/internal/peer/conn.go +++ b/client/internal/peer/conn.go @@ -2,6 +2,7 @@ package peer import ( "context" + "fmt" "math/rand" "net" "os" @@ -14,7 +15,6 @@ import ( log "github.com/sirupsen/logrus" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" - "github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/iface/configurer" "github.com/netbirdio/netbird/client/iface/wgproxy" "github.com/netbirdio/netbird/client/internal/peer/guard" @@ -28,18 +28,34 @@ import ( type ConnPriority int +func (cp ConnPriority) String() string { + switch cp { + case connPriorityNone: + return "None" + case connPriorityRelay: + return "PriorityRelay" + case connPriorityICETurn: + return "PriorityICETurn" + case connPriorityICEP2P: + return "PriorityICEP2P" + default: + return fmt.Sprintf("ConnPriority(%d)", cp) + } +} + const ( defaultWgKeepAlive = 25 * time.Second + connPriorityNone ConnPriority = 0 connPriorityRelay ConnPriority = 1 - connPriorityICETurn ConnPriority = 1 - connPriorityICEP2P ConnPriority = 2 + connPriorityICETurn ConnPriority = 2 + connPriorityICEP2P ConnPriority = 3 ) type WgConfig struct { WgListenPort int RemoteKey string - WgInterface iface.IWGIface + WgInterface WGIface AllowedIps string PreSharedKey *wgtypes.Key } @@ -66,14 +82,6 @@ type ConnConfig struct { ICEConfig icemaker.Config } -type WorkerCallbacks struct { - OnRelayReadyCallback func(info RelayConnInfo) - OnRelayStatusChanged func(ConnStatus) - - OnICEConnReadyCallback func(ConnPriority, ICEConnInfo) - OnICEStatusChanged func(ConnStatus) -} - type Conn struct { log *log.Entry mu sync.Mutex @@ -135,21 +143,11 @@ func NewConn(engineCtx context.Context, config ConnConfig, statusRecorder *Statu semaphore: semaphore, } - rFns := WorkerRelayCallbacks{ - OnConnReady: conn.relayConnectionIsReady, - OnDisconnected: conn.onWorkerRelayStateDisconnected, - } - - wFns := WorkerICECallbacks{ - OnConnReady: conn.iCEConnectionIsReady, - OnStatusChanged: conn.onWorkerICEStateDisconnected, - } - ctrl := isController(config) - conn.workerRelay = NewWorkerRelay(connLog, ctrl, config, relayManager, rFns) + conn.workerRelay = NewWorkerRelay(connLog, ctrl, config, conn, relayManager) relayIsSupportedLocally := conn.workerRelay.RelayIsSupportedLocally() - conn.workerICE, err = NewWorkerICE(ctx, connLog, config, signaler, iFaceDiscover, statusRecorder, relayIsSupportedLocally, wFns) + conn.workerICE, err = NewWorkerICE(ctx, connLog, config, conn, signaler, iFaceDiscover, statusRecorder, relayIsSupportedLocally) if err != nil { return nil, err } @@ -304,7 +302,7 @@ func (conn *Conn) GetKey() string { } // configureConnection starts proxying traffic from/to local Wireguard and sets connection status to StatusConnected -func (conn *Conn) iCEConnectionIsReady(priority ConnPriority, iceConnInfo ICEConnInfo) { +func (conn *Conn) onICEConnectionIsReady(priority ConnPriority, iceConnInfo ICEConnInfo) { conn.mu.Lock() defer conn.mu.Unlock() @@ -317,9 +315,10 @@ func (conn *Conn) iCEConnectionIsReady(priority ConnPriority, iceConnInfo ICECon return } - conn.log.Debugf("ICE connection is ready") - + // this never should happen, because Relay is the lower priority and ICE always close the deprecated connection before upgrade + // todo consider to remove this check if conn.currentConnPriority > priority { + conn.log.Infof("current connection priority (%s) is higher than the new one (%s), do not upgrade connection", conn.currentConnPriority, priority) conn.statusICE.Set(StatusConnected) conn.updateIceState(iceConnInfo) return @@ -375,8 +374,7 @@ func (conn *Conn) iCEConnectionIsReady(priority ConnPriority, iceConnInfo ICECon conn.doOnConnected(iceConnInfo.RosenpassPubKey, iceConnInfo.RosenpassAddr) } -// todo review to make sense to handle connecting and disconnected status also? -func (conn *Conn) onWorkerICEStateDisconnected(newState ConnStatus) { +func (conn *Conn) onICEStateDisconnected() { conn.mu.Lock() defer conn.mu.Unlock() @@ -384,7 +382,7 @@ func (conn *Conn) onWorkerICEStateDisconnected(newState ConnStatus) { return } - conn.log.Tracef("ICE connection state changed to %s", newState) + conn.log.Tracef("ICE connection state changed to disconnected") if conn.wgProxyICE != nil { if err := conn.wgProxyICE.CloseConn(); err != nil { @@ -394,7 +392,7 @@ func (conn *Conn) onWorkerICEStateDisconnected(newState ConnStatus) { // switch back to relay connection if conn.isReadyToUpgrade() { - conn.log.Debugf("ICE disconnected, set Relay to active connection") + conn.log.Infof("ICE disconnected, set Relay to active connection") conn.wgProxyRelay.Work() if err := conn.configureWGEndpoint(conn.wgProxyRelay.EndpointAddr()); err != nil { @@ -402,12 +400,16 @@ func (conn *Conn) onWorkerICEStateDisconnected(newState ConnStatus) { } conn.workerRelay.EnableWgWatcher(conn.ctx) conn.currentConnPriority = connPriorityRelay + } else { + conn.log.Infof("ICE disconnected, do not switch to Relay. Reset priority to: %s", connPriorityNone.String()) + conn.currentConnPriority = connPriorityNone } - changed := conn.statusICE.Get() != newState && newState != StatusConnecting - conn.statusICE.Set(newState) - - conn.guard.SetICEConnDisconnected(changed) + changed := conn.statusICE.Get() != StatusDisconnected + if changed { + conn.guard.SetICEConnDisconnected() + } + conn.statusICE.Set(StatusDisconnected) peerState := State{ PubKey: conn.config.Key, @@ -422,7 +424,7 @@ func (conn *Conn) onWorkerICEStateDisconnected(newState ConnStatus) { } } -func (conn *Conn) relayConnectionIsReady(rci RelayConnInfo) { +func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) { conn.mu.Lock() defer conn.mu.Unlock() @@ -444,7 +446,7 @@ func (conn *Conn) relayConnectionIsReady(rci RelayConnInfo) { conn.log.Infof("created new wgProxy for relay connection: %s", wgProxy.EndpointAddr().String()) if conn.iceP2PIsActive() { - conn.log.Debugf("do not switch to relay because current priority is: %v", conn.currentConnPriority) + conn.log.Debugf("do not switch to relay because current priority is: %s", conn.currentConnPriority.String()) conn.setRelayedProxy(wgProxy) conn.statusRelay.Set(StatusConnected) conn.updateRelayStatus(rci.relayedConn.RemoteAddr().String(), rci.rosenpassPubKey) @@ -474,7 +476,7 @@ func (conn *Conn) relayConnectionIsReady(rci RelayConnInfo) { conn.doOnConnected(rci.rosenpassPubKey, rci.rosenpassAddr) } -func (conn *Conn) onWorkerRelayStateDisconnected() { +func (conn *Conn) onRelayDisconnected() { conn.mu.Lock() defer conn.mu.Unlock() @@ -497,8 +499,10 @@ func (conn *Conn) onWorkerRelayStateDisconnected() { } changed := conn.statusRelay.Get() != StatusDisconnected + if changed { + conn.guard.SetRelayedConnDisconnected() + } conn.statusRelay.Set(StatusDisconnected) - conn.guard.SetRelayedConnDisconnected(changed) peerState := State{ PubKey: conn.config.Key, diff --git a/client/internal/peer/conn_status_test.go b/client/internal/peer/conn_status_test.go index 564fd41c1..6088df55d 100644 --- a/client/internal/peer/conn_status_test.go +++ b/client/internal/peer/conn_status_test.go @@ -1,8 +1,9 @@ package peer import ( - "github.com/magiconair/properties/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestConnStatus_String(t *testing.T) { diff --git a/client/internal/peer/conn_test.go b/client/internal/peer/conn_test.go index b3e9d5b60..505bedb7f 100644 --- a/client/internal/peer/conn_test.go +++ b/client/internal/peer/conn_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "github.com/magiconair/properties/assert" + "github.com/stretchr/testify/assert" "github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/internal/peer/guard" diff --git a/client/internal/peer/guard/guard.go b/client/internal/peer/guard/guard.go index bf3527a62..1fc2b4a4a 100644 --- a/client/internal/peer/guard/guard.go +++ b/client/internal/peer/guard/guard.go @@ -29,8 +29,8 @@ type Guard struct { isConnectedOnAllWay isConnectedFunc timeout time.Duration srWatcher *SRWatcher - relayedConnDisconnected chan bool - iCEConnDisconnected chan bool + relayedConnDisconnected chan struct{} + iCEConnDisconnected chan struct{} } func NewGuard(log *log.Entry, isController bool, isConnectedFn isConnectedFunc, timeout time.Duration, srWatcher *SRWatcher) *Guard { @@ -41,8 +41,8 @@ func NewGuard(log *log.Entry, isController bool, isConnectedFn isConnectedFunc, isConnectedOnAllWay: isConnectedFn, timeout: timeout, srWatcher: srWatcher, - relayedConnDisconnected: make(chan bool, 1), - iCEConnDisconnected: make(chan bool, 1), + relayedConnDisconnected: make(chan struct{}, 1), + iCEConnDisconnected: make(chan struct{}, 1), } } @@ -54,16 +54,16 @@ func (g *Guard) Start(ctx context.Context) { } } -func (g *Guard) SetRelayedConnDisconnected(changed bool) { +func (g *Guard) SetRelayedConnDisconnected() { select { - case g.relayedConnDisconnected <- changed: + case g.relayedConnDisconnected <- struct{}{}: default: } } -func (g *Guard) SetICEConnDisconnected(changed bool) { +func (g *Guard) SetICEConnDisconnected() { select { - case g.iCEConnDisconnected <- changed: + case g.iCEConnDisconnected <- struct{}{}: default: } } @@ -96,19 +96,13 @@ func (g *Guard) reconnectLoopWithRetry(ctx context.Context) { g.triggerOfferSending() } - case changed := <-g.relayedConnDisconnected: - if !changed { - continue - } + case <-g.relayedConnDisconnected: g.log.Debugf("Relay connection changed, reset reconnection ticker") ticker.Stop() ticker = g.prepareExponentTicker(ctx) tickerChannel = ticker.C - case changed := <-g.iCEConnDisconnected: - if !changed { - continue - } + case <-g.iCEConnDisconnected: g.log.Debugf("ICE connection changed, reset reconnection ticker") ticker.Stop() ticker = g.prepareExponentTicker(ctx) @@ -138,16 +132,10 @@ func (g *Guard) listenForDisconnectEvents(ctx context.Context) { g.log.Infof("start listen for reconnect events...") for { select { - case changed := <-g.relayedConnDisconnected: - if !changed { - continue - } + case <-g.relayedConnDisconnected: g.log.Debugf("Relay connection changed, triggering reconnect") g.triggerOfferSending() - case changed := <-g.iCEConnDisconnected: - if !changed { - continue - } + case <-g.iCEConnDisconnected: g.log.Debugf("ICE state changed, try to send new offer") g.triggerOfferSending() case <-srReconnectedChan: diff --git a/client/internal/peer/ice/StunTurn.go b/client/internal/peer/ice/StunTurn.go new file mode 100644 index 000000000..63ee8c713 --- /dev/null +++ b/client/internal/peer/ice/StunTurn.go @@ -0,0 +1,22 @@ +package ice + +import ( + "sync/atomic" + + "github.com/pion/stun/v2" +) + +type StunTurn atomic.Value + +func (s *StunTurn) Load() []*stun.URI { + v := (*atomic.Value)(s).Load() + if v == nil { + return nil + } + + return v.([]*stun.URI) +} + +func (s *StunTurn) Store(value []*stun.URI) { + (*atomic.Value)(s).Store(value) +} diff --git a/client/internal/peer/ice/StunTurn_test.go b/client/internal/peer/ice/StunTurn_test.go new file mode 100644 index 000000000..7233afa6c --- /dev/null +++ b/client/internal/peer/ice/StunTurn_test.go @@ -0,0 +1,13 @@ +package ice + +import ( + "testing" +) + +func TestStunTurn_LoadEmpty(t *testing.T) { + var stStunTurn StunTurn + got := stStunTurn.Load() + if len(got) != 0 { + t.Errorf("StunTurn.Load() = %v, want %v", got, nil) + } +} diff --git a/client/internal/peer/ice/agent.go b/client/internal/peer/ice/agent.go index dc4750f24..af9e60f2d 100644 --- a/client/internal/peer/ice/agent.go +++ b/client/internal/peer/ice/agent.go @@ -5,7 +5,6 @@ import ( "github.com/pion/ice/v3" "github.com/pion/randutil" - "github.com/pion/stun/v2" log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/client/internal/stdnet" @@ -39,7 +38,7 @@ func NewAgent(iFaceDiscover stdnet.ExternalIFaceDiscover, config Config, candida agentConfig := &ice.AgentConfig{ MulticastDNSMode: ice.MulticastDNSModeDisabled, NetworkTypes: []ice.NetworkType{ice.NetworkTypeUDP4, ice.NetworkTypeUDP6}, - Urls: config.StunTurn.Load().([]*stun.URI), + Urls: config.StunTurn.Load(), CandidateTypes: candidateTypes, InterfaceFilter: stdnet.InterfaceFilter(config.InterfaceBlackList), UDPMux: config.UDPMux, diff --git a/client/internal/peer/ice/config.go b/client/internal/peer/ice/config.go index 8abc842f0..dd854a605 100644 --- a/client/internal/peer/ice/config.go +++ b/client/internal/peer/ice/config.go @@ -1,14 +1,12 @@ package ice import ( - "sync/atomic" - "github.com/pion/ice/v3" ) type Config struct { // StunTurn is a list of STUN and TURN URLs - StunTurn *atomic.Value // []*stun.URI + StunTurn *StunTurn // []*stun.URI // InterfaceBlackList is a list of machine interfaces that should be filtered out by ICE Candidate gathering // (e.g. if eth0 is in the list, host candidate of this interface won't be used) diff --git a/client/internal/peer/iface.go b/client/internal/peer/iface.go new file mode 100644 index 000000000..ae6b3bd0a --- /dev/null +++ b/client/internal/peer/iface.go @@ -0,0 +1,17 @@ +package peer + +import ( + "net" + "time" + + "github.com/netbirdio/netbird/client/iface/configurer" + "github.com/netbirdio/netbird/client/iface/wgproxy" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" +) + +type WGIface interface { + UpdatePeer(peerKey string, allowedIps string, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error + RemovePeer(peerKey string) error + GetStats(peerKey string) (configurer.WGStats, error) + GetProxy() wgproxy.Proxy +} diff --git a/client/internal/peer/status.go b/client/internal/peer/status.go index 0df2a2e81..e9976270c 100644 --- a/client/internal/peer/status.go +++ b/client/internal/peer/status.go @@ -7,21 +7,31 @@ import ( "sync" "time" + "github.com/google/uuid" + log "github.com/sirupsen/logrus" "golang.org/x/exp/maps" "google.golang.org/grpc/codes" gstatus "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" "github.com/netbirdio/netbird/client/iface/configurer" "github.com/netbirdio/netbird/client/internal/relay" + "github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/management/domain" relayClient "github.com/netbirdio/netbird/relay/client" ) +const eventQueueSize = 10 + type ResolvedDomainInfo struct { Prefixes []netip.Prefix ParentDomain domain.Domain } +type EventListener interface { + OnEvent(event *proto.SystemEvent) +} + // State contains the latest state of a peer type State struct { Mux *sync.RWMutex @@ -157,6 +167,10 @@ type Status struct { peerListChangedForNotification bool relayMgr *relayClient.Manager + + eventMux sync.RWMutex + eventStreams map[string]chan *proto.SystemEvent + eventQueue *EventQueue } // NewRecorder returns a new Status instance @@ -164,6 +178,8 @@ func NewRecorder(mgmAddress string) *Status { return &Status{ peers: make(map[string]State), changeNotify: make(map[string]chan struct{}), + eventStreams: make(map[string]chan *proto.SystemEvent), + eventQueue: NewEventQueue(eventQueueSize), offlinePeers: make([]State, 0), notifier: newNotifier(), mgmAddress: mgmAddress, @@ -721,7 +737,9 @@ func (d *Status) GetRelayStates() []relay.ProbeResult { func (d *Status) GetDNSStates() []NSGroupState { d.mux.Lock() defer d.mux.Unlock() - return d.nsGroupStates + + // shallow copy is good enough, as slices fields are currently not updated + return slices.Clone(d.nsGroupStates) } func (d *Status) GetResolvedDomainsStates() map[domain.Domain]ResolvedDomainInfo { @@ -804,3 +822,112 @@ func (d *Status) notifyAddressChanged() { func (d *Status) numOfPeers() int { return len(d.peers) + len(d.offlinePeers) } + +// PublishEvent adds an event to the queue and distributes it to all subscribers +func (d *Status) PublishEvent( + severity proto.SystemEvent_Severity, + category proto.SystemEvent_Category, + msg string, + userMsg string, + metadata map[string]string, +) { + event := &proto.SystemEvent{ + Id: uuid.New().String(), + Severity: severity, + Category: category, + Message: msg, + UserMessage: userMsg, + Metadata: metadata, + Timestamp: timestamppb.Now(), + } + + d.eventMux.Lock() + defer d.eventMux.Unlock() + + d.eventQueue.Add(event) + + for _, stream := range d.eventStreams { + select { + case stream <- event: + default: + log.Debugf("event stream buffer full, skipping event: %v", event) + } + } + + log.Debugf("event published: %v", event) +} + +// SubscribeToEvents returns a new event subscription +func (d *Status) SubscribeToEvents() *EventSubscription { + d.eventMux.Lock() + defer d.eventMux.Unlock() + + id := uuid.New().String() + stream := make(chan *proto.SystemEvent, 10) + d.eventStreams[id] = stream + + return &EventSubscription{ + id: id, + events: stream, + } +} + +// UnsubscribeFromEvents removes an event subscription +func (d *Status) UnsubscribeFromEvents(sub *EventSubscription) { + if sub == nil { + return + } + + d.eventMux.Lock() + defer d.eventMux.Unlock() + + if stream, exists := d.eventStreams[sub.id]; exists { + close(stream) + delete(d.eventStreams, sub.id) + } +} + +// GetEventHistory returns all events in the queue +func (d *Status) GetEventHistory() []*proto.SystemEvent { + return d.eventQueue.GetAll() +} + +type EventQueue struct { + maxSize int + events []*proto.SystemEvent + mutex sync.RWMutex +} + +func NewEventQueue(size int) *EventQueue { + return &EventQueue{ + maxSize: size, + events: make([]*proto.SystemEvent, 0, size), + } +} + +func (q *EventQueue) Add(event *proto.SystemEvent) { + q.mutex.Lock() + defer q.mutex.Unlock() + + q.events = append(q.events, event) + + if len(q.events) > q.maxSize { + q.events = q.events[len(q.events)-q.maxSize:] + } +} + +func (q *EventQueue) GetAll() []*proto.SystemEvent { + q.mutex.RLock() + defer q.mutex.RUnlock() + + return slices.Clone(q.events) +} + +type EventSubscription struct { + id string + events chan *proto.SystemEvent +} + +func (s *EventSubscription) Events() <-chan *proto.SystemEvent { + return s.events +} diff --git a/client/internal/peer/wg_watcher.go b/client/internal/peer/wg_watcher.go new file mode 100644 index 000000000..6670c6517 --- /dev/null +++ b/client/internal/peer/wg_watcher.go @@ -0,0 +1,154 @@ +package peer + +import ( + "context" + "sync" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/iface/configurer" +) + +const ( + wgHandshakePeriod = 3 * time.Minute +) + +var ( + wgHandshakeOvertime = 30 * time.Second // allowed delay in network + checkPeriod = wgHandshakePeriod + wgHandshakeOvertime +) + +type WGInterfaceStater interface { + GetStats(key string) (configurer.WGStats, error) +} + +type WGWatcher struct { + log *log.Entry + wgIfaceStater WGInterfaceStater + peerKey string + + ctx context.Context + ctxCancel context.CancelFunc + ctxLock sync.Mutex + waitGroup sync.WaitGroup +} + +func NewWGWatcher(log *log.Entry, wgIfaceStater WGInterfaceStater, peerKey string) *WGWatcher { + return &WGWatcher{ + log: log, + wgIfaceStater: wgIfaceStater, + peerKey: peerKey, + } +} + +// EnableWgWatcher starts the WireGuard watcher. If it is already enabled, it will return immediately and do nothing. +func (w *WGWatcher) EnableWgWatcher(parentCtx context.Context, onDisconnectedFn func()) { + w.log.Debugf("enable WireGuard watcher") + w.ctxLock.Lock() + defer w.ctxLock.Unlock() + + if w.ctx != nil && w.ctx.Err() == nil { + w.log.Errorf("WireGuard watcher already enabled") + return + } + + ctx, ctxCancel := context.WithCancel(parentCtx) + w.ctx = ctx + w.ctxCancel = ctxCancel + + initialHandshake, err := w.wgState() + if err != nil { + w.log.Warnf("failed to read initial wg stats: %v", err) + } + + w.waitGroup.Add(1) + go w.periodicHandshakeCheck(ctx, ctxCancel, onDisconnectedFn, initialHandshake) +} + +// DisableWgWatcher stops the WireGuard watcher and wait for the watcher to exit +func (w *WGWatcher) DisableWgWatcher() { + w.ctxLock.Lock() + defer w.ctxLock.Unlock() + + if w.ctxCancel == nil { + return + } + + w.log.Debugf("disable WireGuard watcher") + + w.ctxCancel() + w.ctxCancel = nil + w.waitGroup.Wait() +} + +// wgStateCheck help to check the state of the WireGuard handshake and relay connection +func (w *WGWatcher) periodicHandshakeCheck(ctx context.Context, ctxCancel context.CancelFunc, onDisconnectedFn func(), initialHandshake time.Time) { + w.log.Infof("WireGuard watcher started") + defer w.waitGroup.Done() + + timer := time.NewTimer(wgHandshakeOvertime) + defer timer.Stop() + defer ctxCancel() + + lastHandshake := initialHandshake + + for { + select { + case <-timer.C: + handshake, ok := w.handshakeCheck(lastHandshake) + if !ok { + onDisconnectedFn() + return + } + lastHandshake = *handshake + + resetTime := time.Until(handshake.Add(checkPeriod)) + timer.Reset(resetTime) + + w.log.Debugf("WireGuard watcher reset timer: %v", resetTime) + case <-ctx.Done(): + w.log.Infof("WireGuard watcher stopped") + return + } + } +} + +// handshakeCheck checks the WireGuard handshake and return the new handshake time if it is different from the previous one +func (w *WGWatcher) handshakeCheck(lastHandshake time.Time) (*time.Time, bool) { + handshake, err := w.wgState() + if err != nil { + w.log.Errorf("failed to read wg stats: %v", err) + return nil, false + } + + w.log.Tracef("previous handshake, handshake: %v, %v", lastHandshake, handshake) + + // the current know handshake did not change + if handshake.Equal(lastHandshake) { + w.log.Warnf("WireGuard handshake timed out, closing relay connection: %v", handshake) + return nil, false + } + + // in case if the machine is suspended, the handshake time will be in the past + if handshake.Add(checkPeriod).Before(time.Now()) { + w.log.Warnf("WireGuard handshake timed out, closing relay connection: %v", handshake) + return nil, false + } + + // error handling for handshake time in the future + if handshake.After(time.Now()) { + w.log.Warnf("WireGuard handshake is in the future, closing relay connection: %v", handshake) + return nil, false + } + + return &handshake, true +} + +func (w *WGWatcher) wgState() (time.Time, error) { + wgState, err := w.wgIfaceStater.GetStats(w.peerKey) + if err != nil { + return time.Time{}, err + } + return wgState.LastHandshake, nil +} diff --git a/client/internal/peer/wg_watcher_test.go b/client/internal/peer/wg_watcher_test.go new file mode 100644 index 000000000..a5b9026ad --- /dev/null +++ b/client/internal/peer/wg_watcher_test.go @@ -0,0 +1,98 @@ +package peer + +import ( + "context" + "testing" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/iface/configurer" +) + +type MocWgIface struct { + initial bool + lastHandshake time.Time + stop bool +} + +func (m *MocWgIface) GetStats(key string) (configurer.WGStats, error) { + if !m.initial { + m.initial = true + return configurer.WGStats{}, nil + } + + if !m.stop { + m.lastHandshake = time.Now() + } + + stats := configurer.WGStats{ + LastHandshake: m.lastHandshake, + } + + return stats, nil +} + +func (m *MocWgIface) disconnect() { + m.stop = true +} + +func TestWGWatcher_EnableWgWatcher(t *testing.T) { + checkPeriod = 5 * time.Second + wgHandshakeOvertime = 1 * time.Second + + mlog := log.WithField("peer", "tet") + mocWgIface := &MocWgIface{} + watcher := NewWGWatcher(mlog, mocWgIface, "") + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + onDisconnected := make(chan struct{}, 1) + watcher.EnableWgWatcher(ctx, func() { + mlog.Infof("onDisconnectedFn") + onDisconnected <- struct{}{} + }) + + // wait for initial reading + time.Sleep(2 * time.Second) + mocWgIface.disconnect() + + select { + case <-onDisconnected: + case <-time.After(10 * time.Second): + t.Errorf("timeout") + } + watcher.DisableWgWatcher() +} + +func TestWGWatcher_ReEnable(t *testing.T) { + checkPeriod = 5 * time.Second + wgHandshakeOvertime = 1 * time.Second + + mlog := log.WithField("peer", "tet") + mocWgIface := &MocWgIface{} + watcher := NewWGWatcher(mlog, mocWgIface, "") + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + onDisconnected := make(chan struct{}, 1) + + watcher.EnableWgWatcher(ctx, func() {}) + watcher.DisableWgWatcher() + + watcher.EnableWgWatcher(ctx, func() { + onDisconnected <- struct{}{} + }) + + time.Sleep(2 * time.Second) + mocWgIface.disconnect() + + select { + case <-onDisconnected: + case <-time.After(10 * time.Second): + t.Errorf("timeout") + } + watcher.DisableWgWatcher() +} diff --git a/client/internal/peer/worker_ice.go b/client/internal/peer/worker_ice.go index 4cdd18ff1..7dd84a98e 100644 --- a/client/internal/peer/worker_ice.go +++ b/client/internal/peer/worker_ice.go @@ -31,20 +31,15 @@ type ICEConnInfo struct { RelayedOnLocal bool } -type WorkerICECallbacks struct { - OnConnReady func(ConnPriority, ICEConnInfo) - OnStatusChanged func(ConnStatus) -} - type WorkerICE struct { ctx context.Context log *log.Entry config ConnConfig + conn *Conn signaler *Signaler iFaceDiscover stdnet.ExternalIFaceDiscover statusRecorder *Status hasRelayOnLocally bool - conn WorkerICECallbacks agent *ice.Agent muxAgent sync.Mutex @@ -60,16 +55,16 @@ type WorkerICE struct { lastKnownState ice.ConnectionState } -func NewWorkerICE(ctx context.Context, log *log.Entry, config ConnConfig, signaler *Signaler, ifaceDiscover stdnet.ExternalIFaceDiscover, statusRecorder *Status, hasRelayOnLocally bool, callBacks WorkerICECallbacks) (*WorkerICE, error) { +func NewWorkerICE(ctx context.Context, log *log.Entry, config ConnConfig, conn *Conn, signaler *Signaler, ifaceDiscover stdnet.ExternalIFaceDiscover, statusRecorder *Status, hasRelayOnLocally bool) (*WorkerICE, error) { w := &WorkerICE{ ctx: ctx, log: log, config: config, + conn: conn, signaler: signaler, iFaceDiscover: ifaceDiscover, statusRecorder: statusRecorder, hasRelayOnLocally: hasRelayOnLocally, - conn: callBacks, } localUfrag, localPwd, err := icemaker.GenerateICECredentials() @@ -154,8 +149,8 @@ func (w *WorkerICE) OnNewOffer(remoteOfferAnswer *OfferAnswer) { Relayed: isRelayed(pair), RelayedOnLocal: isRelayCandidate(pair.Local), } - w.log.Debugf("on ICE conn read to use ready") - go w.conn.OnConnReady(selectedPriority(pair), ci) + w.log.Debugf("on ICE conn is ready to use") + go w.conn.onICEConnectionIsReady(selectedPriority(pair), ci) } // OnRemoteCandidate Handles ICE connection Candidate provided by the remote peer. @@ -220,7 +215,7 @@ func (w *WorkerICE) reCreateAgent(agentCancel context.CancelFunc, candidates []i case ice.ConnectionStateFailed, ice.ConnectionStateDisconnected: if w.lastKnownState != ice.ConnectionStateDisconnected { w.lastKnownState = ice.ConnectionStateDisconnected - w.conn.OnStatusChanged(StatusDisconnected) + w.conn.onICEStateDisconnected() } w.closeAgent(agentCancel) default: @@ -255,6 +250,10 @@ func (w *WorkerICE) closeAgent(cancel context.CancelFunc) { defer w.muxAgent.Unlock() cancel() + if w.agent == nil { + return + } + if err := w.agent.Close(); err != nil { w.log.Warnf("failed to close ICE agent: %s", err) } diff --git a/client/internal/peer/worker_relay.go b/client/internal/peer/worker_relay.go index c22dcdeda..56c19cd1e 100644 --- a/client/internal/peer/worker_relay.go +++ b/client/internal/peer/worker_relay.go @@ -6,52 +6,41 @@ import ( "net" "sync" "sync/atomic" - "time" log "github.com/sirupsen/logrus" relayClient "github.com/netbirdio/netbird/relay/client" ) -var ( - wgHandshakePeriod = 3 * time.Minute - wgHandshakeOvertime = 30 * time.Second -) - type RelayConnInfo struct { relayedConn net.Conn rosenpassPubKey []byte rosenpassAddr string } -type WorkerRelayCallbacks struct { - OnConnReady func(RelayConnInfo) - OnDisconnected func() -} - type WorkerRelay struct { log *log.Entry isController bool config ConnConfig + conn *Conn relayManager relayClient.ManagerService - callBacks WorkerRelayCallbacks - relayedConn net.Conn - relayLock sync.Mutex - ctxWgWatch context.Context - ctxCancelWgWatch context.CancelFunc - ctxLock sync.Mutex + relayedConn net.Conn + relayLock sync.Mutex relaySupportedOnRemotePeer atomic.Bool + + wgWatcher *WGWatcher } -func NewWorkerRelay(log *log.Entry, ctrl bool, config ConnConfig, relayManager relayClient.ManagerService, callbacks WorkerRelayCallbacks) *WorkerRelay { +func NewWorkerRelay(log *log.Entry, ctrl bool, config ConnConfig, conn *Conn, relayManager relayClient.ManagerService) *WorkerRelay { r := &WorkerRelay{ log: log, isController: ctrl, config: config, + conn: conn, relayManager: relayManager, - callBacks: callbacks, + wgWatcher: NewWGWatcher(log, config.WgConfig.WgInterface, config.Key), } return r } @@ -87,7 +76,7 @@ func (w *WorkerRelay) OnNewOffer(remoteOfferAnswer *OfferAnswer) { w.relayedConn = relayedConn w.relayLock.Unlock() - err = w.relayManager.AddCloseListener(srv, w.onRelayMGDisconnected) + err = w.relayManager.AddCloseListener(srv, w.onRelayClientDisconnected) if err != nil { log.Errorf("failed to add close listener: %s", err) _ = relayedConn.Close() @@ -95,7 +84,7 @@ func (w *WorkerRelay) OnNewOffer(remoteOfferAnswer *OfferAnswer) { } w.log.Debugf("peer conn opened via Relay: %s", srv) - go w.callBacks.OnConnReady(RelayConnInfo{ + go w.conn.onRelayConnectionIsReady(RelayConnInfo{ relayedConn: relayedConn, rosenpassPubKey: remoteOfferAnswer.RosenpassPubKey, rosenpassAddr: remoteOfferAnswer.RosenpassAddr, @@ -103,32 +92,11 @@ func (w *WorkerRelay) OnNewOffer(remoteOfferAnswer *OfferAnswer) { } func (w *WorkerRelay) EnableWgWatcher(ctx context.Context) { - w.log.Debugf("enable WireGuard watcher") - w.ctxLock.Lock() - defer w.ctxLock.Unlock() - - if w.ctxWgWatch != nil && w.ctxWgWatch.Err() == nil { - return - } - - ctx, ctxCancel := context.WithCancel(ctx) - w.ctxWgWatch = ctx - w.ctxCancelWgWatch = ctxCancel - - w.wgStateCheck(ctx, ctxCancel) + w.wgWatcher.EnableWgWatcher(ctx, w.onWGDisconnected) } func (w *WorkerRelay) DisableWgWatcher() { - w.ctxLock.Lock() - defer w.ctxLock.Unlock() - - if w.ctxCancelWgWatch == nil { - return - } - - w.log.Debugf("disable WireGuard watcher") - - w.ctxCancelWgWatch() + w.wgWatcher.DisableWgWatcher() } func (w *WorkerRelay) RelayInstanceAddress() (string, error) { @@ -150,57 +118,17 @@ func (w *WorkerRelay) CloseConn() { return } - err := w.relayedConn.Close() - if err != nil { + if err := w.relayedConn.Close(); err != nil { w.log.Warnf("failed to close relay connection: %v", err) } } -// wgStateCheck help to check the state of the WireGuard handshake and relay connection -func (w *WorkerRelay) wgStateCheck(ctx context.Context, ctxCancel context.CancelFunc) { - w.log.Debugf("WireGuard watcher started") - lastHandshake, err := w.wgState() - if err != nil { - w.log.Warnf("failed to read wg stats: %v", err) - lastHandshake = time.Time{} - } - - go func(lastHandshake time.Time) { - timer := time.NewTimer(wgHandshakeOvertime) - defer timer.Stop() - defer ctxCancel() - - for { - select { - case <-timer.C: - handshake, err := w.wgState() - if err != nil { - w.log.Errorf("failed to read wg stats: %v", err) - timer.Reset(wgHandshakeOvertime) - continue - } - - w.log.Tracef("previous handshake, handshake: %v, %v", lastHandshake, handshake) - - if handshake.Equal(lastHandshake) { - w.log.Infof("WireGuard handshake timed out, closing relay connection: %v", handshake) - w.relayLock.Lock() - _ = w.relayedConn.Close() - w.relayLock.Unlock() - w.callBacks.OnDisconnected() - return - } - - resetTime := time.Until(handshake.Add(wgHandshakePeriod + wgHandshakeOvertime)) - lastHandshake = handshake - timer.Reset(resetTime) - case <-ctx.Done(): - w.log.Debugf("WireGuard watcher stopped") - return - } - } - }(lastHandshake) +func (w *WorkerRelay) onWGDisconnected() { + w.relayLock.Lock() + _ = w.relayedConn.Close() + w.relayLock.Unlock() + w.conn.onRelayDisconnected() } func (w *WorkerRelay) isRelaySupported(answer *OfferAnswer) bool { @@ -217,20 +145,7 @@ func (w *WorkerRelay) preferredRelayServer(myRelayAddress, remoteRelayAddress st return remoteRelayAddress } -func (w *WorkerRelay) wgState() (time.Time, error) { - wgState, err := w.config.WgConfig.WgInterface.GetStats(w.config.Key) - if err != nil { - return time.Time{}, err - } - return wgState.LastHandshake, nil -} - -func (w *WorkerRelay) onRelayMGDisconnected() { - w.ctxLock.Lock() - defer w.ctxLock.Unlock() - - if w.ctxCancelWgWatch != nil { - w.ctxCancelWgWatch() - } - go w.callBacks.OnDisconnected() +func (w *WorkerRelay) onRelayClientDisconnected() { + w.wgWatcher.DisableWgWatcher() + go w.conn.onRelayDisconnected() } diff --git a/client/internal/probe.go b/client/internal/probe.go deleted file mode 100644 index 23290cf74..000000000 --- a/client/internal/probe.go +++ /dev/null @@ -1,58 +0,0 @@ -package internal - -import "context" - -type ProbeHolder struct { - MgmProbe *Probe - SignalProbe *Probe - RelayProbe *Probe - WgProbe *Probe -} - -// Probe allows to run on-demand callbacks from different code locations. -// Pass the probe to a receiving and a sending end. The receiving end starts listening -// to requests with Receive and executes a callback when the sending end requests it -// by calling Probe. -type Probe struct { - request chan struct{} - result chan bool - ready bool -} - -// NewProbe returns a new initialized probe. -func NewProbe() *Probe { - return &Probe{ - request: make(chan struct{}), - result: make(chan bool), - } -} - -// Probe requests the callback to be run and returns a bool indicating success. -// It always returns true as long as the receiver is not ready. -func (p *Probe) Probe() bool { - if !p.ready { - return true - } - - p.request <- struct{}{} - return <-p.result -} - -// Receive starts listening for probe requests. On such a request it runs the supplied -// callback func which must return a bool indicating success. -// Blocks until the passed context is cancelled. -func (p *Probe) Receive(ctx context.Context, callback func() bool) { - p.ready = true - defer func() { - p.ready = false - }() - - for { - select { - case <-ctx.Done(): - return - case <-p.request: - p.result <- callback() - } - } -} diff --git a/client/internal/routemanager/client.go b/client/internal/routemanager/client.go index 73f552aab..2f0b78e7b 100644 --- a/client/internal/routemanager/client.go +++ b/client/internal/routemanager/client.go @@ -4,20 +4,22 @@ import ( "context" "fmt" "reflect" + "runtime" "time" "github.com/hashicorp/go-multierror" log "github.com/sirupsen/logrus" nberrors "github.com/netbirdio/netbird/client/errors" - "github.com/netbirdio/netbird/client/iface" nbdns "github.com/netbirdio/netbird/client/internal/dns" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/peerstore" "github.com/netbirdio/netbird/client/internal/routemanager/dnsinterceptor" "github.com/netbirdio/netbird/client/internal/routemanager/dynamic" + "github.com/netbirdio/netbird/client/internal/routemanager/iface" "github.com/netbirdio/netbird/client/internal/routemanager/refcounter" "github.com/netbirdio/netbird/client/internal/routemanager/static" + "github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/route" ) @@ -27,6 +29,15 @@ const ( handlerTypeStatic ) +type reason int + +const ( + reasonUnknown reason = iota + reasonRouteUpdate + reasonPeerUpdate + reasonShutdown +) + type routerPeerStatus struct { connected bool relayed bool @@ -51,7 +62,7 @@ type clientNetwork struct { ctx context.Context cancel context.CancelFunc statusRecorder *peer.Status - wgInterface iface.IWGIface + wgInterface iface.WGIface routes map[route.ID]*route.Route routeUpdate chan routesUpdate peerStateUpdate chan struct{} @@ -64,7 +75,7 @@ type clientNetwork struct { func newClientNetworkWatcher( ctx context.Context, dnsRouteInterval time.Duration, - wgInterface iface.IWGIface, + wgInterface iface.WGIface, statusRecorder *peer.Status, rt *route.Route, routeRefCounter *refcounter.RouteRefCounter, @@ -254,7 +265,7 @@ func (c *clientNetwork) removeRouteFromWireGuardPeer() error { return nil } -func (c *clientNetwork) removeRouteFromPeerAndSystem() error { +func (c *clientNetwork) removeRouteFromPeerAndSystem(rsn reason) error { if c.currentChosen == nil { return nil } @@ -268,17 +279,19 @@ func (c *clientNetwork) removeRouteFromPeerAndSystem() error { merr = multierror.Append(merr, fmt.Errorf("remove route: %w", err)) } + c.disconnectEvent(rsn) + return nberrors.FormatErrorOrNil(merr) } -func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem() error { +func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem(rsn reason) error { routerPeerStatuses := c.getRouterPeerStatuses() newChosenID := c.getBestRouteFromStatuses(routerPeerStatuses) // If no route is chosen, remove the route from the peer and system if newChosenID == "" { - if err := c.removeRouteFromPeerAndSystem(); err != nil { + if err := c.removeRouteFromPeerAndSystem(rsn); err != nil { return fmt.Errorf("remove route for peer %s: %w", c.currentChosen.Peer, err) } @@ -293,11 +306,13 @@ func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem() error { return nil } + var isNew bool if c.currentChosen == nil { // If they were not previously assigned to another peer, add routes to the system first if err := c.handler.AddRoute(c.ctx); err != nil { return fmt.Errorf("add route: %w", err) } + isNew = true } else { // Otherwise, remove the allowed IPs from the previous peer first if err := c.removeRouteFromWireGuardPeer(); err != nil { @@ -311,6 +326,10 @@ func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem() error { return fmt.Errorf("add allowed IPs for peer %s: %w", c.currentChosen.Peer, err) } + if isNew { + c.connectEvent() + } + err := c.statusRecorder.AddPeerStateRoute(c.currentChosen.Peer, c.handler.String()) if err != nil { return fmt.Errorf("add peer state route: %w", err) @@ -318,6 +337,85 @@ func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem() error { return nil } +func (c *clientNetwork) connectEvent() { + var defaultRoute bool + for _, r := range c.routes { + if r.Network.Bits() == 0 { + defaultRoute = true + break + } + } + + if !defaultRoute { + return + } + + meta := map[string]string{ + "network": c.handler.String(), + } + if c.currentChosen != nil { + meta["id"] = string(c.currentChosen.NetID) + meta["peer"] = c.currentChosen.Peer + } + c.statusRecorder.PublishEvent( + proto.SystemEvent_INFO, + proto.SystemEvent_NETWORK, + "Default route added", + "Exit node connected.", + meta, + ) +} + +func (c *clientNetwork) disconnectEvent(rsn reason) { + var defaultRoute bool + for _, r := range c.routes { + if r.Network.Bits() == 0 { + defaultRoute = true + break + } + } + + if !defaultRoute { + return + } + + var severity proto.SystemEvent_Severity + var message string + var userMessage string + meta := make(map[string]string) + + if c.currentChosen != nil { + meta["id"] = string(c.currentChosen.NetID) + meta["peer"] = c.currentChosen.Peer + } + meta["network"] = c.handler.String() + switch rsn { + case reasonShutdown: + severity = proto.SystemEvent_INFO + message = "Default route removed" + userMessage = "Exit node disconnected." + case reasonRouteUpdate: + severity = proto.SystemEvent_INFO + message = "Default route updated due to configuration change" + case reasonPeerUpdate: + severity = proto.SystemEvent_WARNING + message = "Default route disconnected due to peer unreachability" + userMessage = "Exit node connection lost. Your internet access might be affected." + default: + severity = proto.SystemEvent_ERROR + message = "Default route disconnected for unknown reasons" + userMessage = "Exit node disconnected for unknown reasons." + } + + c.statusRecorder.PublishEvent( + severity, + proto.SystemEvent_NETWORK, + message, + userMessage, + meta, + ) +} + func (c *clientNetwork) sendUpdateToClientNetworkWatcher(update routesUpdate) { go func() { c.routeUpdate <- update @@ -360,12 +458,12 @@ func (c *clientNetwork) peersStateAndUpdateWatcher() { select { case <-c.ctx.Done(): log.Debugf("Stopping watcher for network [%v]", c.handler) - if err := c.removeRouteFromPeerAndSystem(); err != nil { + if err := c.removeRouteFromPeerAndSystem(reasonShutdown); err != nil { log.Errorf("Failed to remove routes for [%v]: %v", c.handler, err) } return case <-c.peerStateUpdate: - err := c.recalculateRouteAndUpdatePeerAndSystem() + err := c.recalculateRouteAndUpdatePeerAndSystem(reasonPeerUpdate) if err != nil { log.Errorf("Failed to recalculate routes for network [%v]: %v", c.handler, err) } @@ -384,7 +482,7 @@ func (c *clientNetwork) peersStateAndUpdateWatcher() { if isTrueRouteUpdate { log.Debug("Client network update contains different routes, recalculating routes") - err := c.recalculateRouteAndUpdatePeerAndSystem() + err := c.recalculateRouteAndUpdatePeerAndSystem(reasonRouteUpdate) if err != nil { log.Errorf("Failed to recalculate routes for network [%v]: %v", c.handler, err) } @@ -403,7 +501,7 @@ func handlerFromRoute( allowedIPsRefCounter *refcounter.AllowedIPsRefCounter, dnsRouterInteval time.Duration, statusRecorder *peer.Status, - wgInterface iface.IWGIface, + wgInterface iface.WGIface, dnsServer nbdns.Server, peerStore *peerstore.Store, useNewDNSRoute bool, @@ -439,7 +537,7 @@ func handlerType(rt *route.Route, useNewDNSRoute bool) int { return handlerTypeStatic } - if useNewDNSRoute { + if useNewDNSRoute && runtime.GOOS != "ios" { return handlerTypeDomain } return handlerTypeDynamic diff --git a/client/internal/routemanager/dynamic/route.go b/client/internal/routemanager/dynamic/route.go index a0fff7713..5ef18a47e 100644 --- a/client/internal/routemanager/dynamic/route.go +++ b/client/internal/routemanager/dynamic/route.go @@ -13,8 +13,8 @@ import ( log "github.com/sirupsen/logrus" nberrors "github.com/netbirdio/netbird/client/errors" - "github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/client/internal/routemanager/iface" "github.com/netbirdio/netbird/client/internal/routemanager/refcounter" "github.com/netbirdio/netbird/client/internal/routemanager/util" "github.com/netbirdio/netbird/management/domain" @@ -48,7 +48,7 @@ type Route struct { currentPeerKey string cancel context.CancelFunc statusRecorder *peer.Status - wgInterface iface.IWGIface + wgInterface iface.WGIface resolverAddr string } @@ -58,7 +58,7 @@ func NewRoute( allowedIPsRefCounter *refcounter.AllowedIPsRefCounter, interval time.Duration, statusRecorder *peer.Status, - wgInterface iface.IWGIface, + wgInterface iface.WGIface, resolverAddr string, ) *Route { return &Route{ diff --git a/client/internal/routemanager/iface/iface.go b/client/internal/routemanager/iface/iface.go new file mode 100644 index 000000000..57dbec03d --- /dev/null +++ b/client/internal/routemanager/iface/iface.go @@ -0,0 +1,9 @@ +//go:build !windows +// +build !windows + +package iface + +// WGIface defines subset methods of interface required for router +type WGIface interface { + wgIfaceBase +} diff --git a/client/internal/routemanager/iface/iface_common.go b/client/internal/routemanager/iface/iface_common.go new file mode 100644 index 000000000..8b2dc9714 --- /dev/null +++ b/client/internal/routemanager/iface/iface_common.go @@ -0,0 +1,22 @@ +package iface + +import ( + "net" + + "github.com/netbirdio/netbird/client/iface" + "github.com/netbirdio/netbird/client/iface/configurer" + "github.com/netbirdio/netbird/client/iface/device" +) + +type wgIfaceBase interface { + AddAllowedIP(peerKey string, allowedIP string) error + RemoveAllowedIP(peerKey string, allowedIP string) error + + Name() string + Address() iface.WGAddress + ToInterface() *net.Interface + IsUserspaceBind() bool + GetFilter() device.PacketFilter + GetDevice() *device.FilteredDevice + GetStats(peerKey string) (configurer.WGStats, error) +} diff --git a/client/internal/routemanager/iface/iface_windows.go b/client/internal/routemanager/iface/iface_windows.go new file mode 100644 index 000000000..7ab7e239c --- /dev/null +++ b/client/internal/routemanager/iface/iface_windows.go @@ -0,0 +1,7 @@ +package iface + +// WGIface defines subset methods of interface required for router +type WGIface interface { + wgIfaceBase + GetInterfaceGUIDString() (string, error) +} diff --git a/client/internal/routemanager/manager.go b/client/internal/routemanager/manager.go index 6f73fb166..ae0d1d220 100644 --- a/client/internal/routemanager/manager.go +++ b/client/internal/routemanager/manager.go @@ -15,13 +15,13 @@ import ( "golang.org/x/exp/maps" firewall "github.com/netbirdio/netbird/client/firewall/manager" - "github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/iface/configurer" "github.com/netbirdio/netbird/client/iface/netstack" "github.com/netbirdio/netbird/client/internal/dns" "github.com/netbirdio/netbird/client/internal/listener" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/peerstore" + "github.com/netbirdio/netbird/client/internal/routemanager/iface" "github.com/netbirdio/netbird/client/internal/routemanager/notifier" "github.com/netbirdio/netbird/client/internal/routemanager/refcounter" "github.com/netbirdio/netbird/client/internal/routemanager/systemops" @@ -52,7 +52,7 @@ type ManagerConfig struct { Context context.Context PublicKey string DNSRouteInterval time.Duration - WGInterface iface.IWGIface + WGInterface iface.WGIface StatusRecorder *peer.Status RelayManager *relayClient.Manager InitialRoutes []*route.Route @@ -74,7 +74,7 @@ type DefaultManager struct { sysOps *systemops.SysOps statusRecorder *peer.Status relayMgr *relayClient.Manager - wgInterface iface.IWGIface + wgInterface iface.WGIface pubKey string notifier *notifier.Notifier routeRefCounter *refcounter.RouteRefCounter @@ -113,13 +113,14 @@ func NewManager(config ManagerConfig) *DefaultManager { disableServerRoutes: config.DisableServerRoutes, } + useNoop := netstack.IsEnabled() || config.DisableClientRoutes + dm.setupRefCounters(useNoop) + // don't proceed with client routes if it is disabled if config.DisableClientRoutes { return dm } - dm.setupRefCounters() - if runtime.GOOS == "android" { cr := dm.initialClientRoutes(config.InitialRoutes) dm.notifier.SetInitialClientRoutes(cr) @@ -127,7 +128,7 @@ func NewManager(config ManagerConfig) *DefaultManager { return dm } -func (m *DefaultManager) setupRefCounters() { +func (m *DefaultManager) setupRefCounters(useNoop bool) { m.routeRefCounter = refcounter.New( func(prefix netip.Prefix, _ struct{}) (struct{}, error) { return struct{}{}, m.sysOps.AddVPNRoute(prefix, m.wgInterface.ToInterface()) @@ -137,7 +138,7 @@ func (m *DefaultManager) setupRefCounters() { }, ) - if netstack.IsEnabled() { + if useNoop { m.routeRefCounter = refcounter.New( func(netip.Prefix, struct{}) (struct{}, error) { return struct{}{}, refcounter.ErrIgnore @@ -285,15 +286,15 @@ func (m *DefaultManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Ro m.updateClientNetworks(updateSerial, filteredClientRoutes) m.notifier.OnNewRoutes(filteredClientRoutes) } + m.clientRoutes = newClientRoutesIDMap - if m.serverRouter != nil { - err := m.serverRouter.updateRoutes(newServerRoutesMap) - if err != nil { - return err - } + if m.serverRouter == nil { + return nil } - m.clientRoutes = newClientRoutesIDMap + if err := m.serverRouter.updateRoutes(newServerRoutesMap); err != nil { + return fmt.Errorf("update routes: %w", err) + } return nil } @@ -422,11 +423,6 @@ func (m *DefaultManager) classifyRoutes(newRoutes []*route.Route) (map[route.ID] haID := newRoute.GetHAUniqueID() if newRoute.Peer == m.pubKey { ownNetworkIDs[haID] = true - // only linux is supported for now - if runtime.GOOS != "linux" { - log.Warnf("received a route to manage, but agent doesn't support router mode on %s OS", runtime.GOOS) - continue - } newServerRoutesMap[newRoute.ID] = newRoute } } @@ -454,7 +450,7 @@ func (m *DefaultManager) initialClientRoutes(initialRoutes []*route.Route) []*ro } func isRouteSupported(route *route.Route) bool { - if !nbnet.CustomRoutingDisabled() || route.IsDynamic() { + if netstack.IsEnabled() || !nbnet.CustomRoutingDisabled() || route.IsDynamic() { return true } diff --git a/client/internal/routemanager/server_android.go b/client/internal/routemanager/server_android.go index e9cfa0826..48bb0380d 100644 --- a/client/internal/routemanager/server_android.go +++ b/client/internal/routemanager/server_android.go @@ -7,8 +7,8 @@ import ( "fmt" firewall "github.com/netbirdio/netbird/client/firewall/manager" - "github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/client/internal/routemanager/iface" "github.com/netbirdio/netbird/route" ) @@ -22,6 +22,6 @@ func (r serverRouter) updateRoutes(map[route.ID]*route.Route) error { return nil } -func newServerRouter(context.Context, iface.IWGIface, firewall.Manager, *peer.Status) (*serverRouter, error) { +func newServerRouter(context.Context, iface.WGIface, firewall.Manager, *peer.Status) (*serverRouter, error) { return nil, fmt.Errorf("server route not supported on this os") } diff --git a/client/internal/routemanager/server_nonandroid.go b/client/internal/routemanager/server_nonandroid.go index b60cb318e..c9bbe10a6 100644 --- a/client/internal/routemanager/server_nonandroid.go +++ b/client/internal/routemanager/server_nonandroid.go @@ -11,8 +11,8 @@ import ( log "github.com/sirupsen/logrus" firewall "github.com/netbirdio/netbird/client/firewall/manager" - "github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/client/internal/routemanager/iface" "github.com/netbirdio/netbird/client/internal/routemanager/systemops" "github.com/netbirdio/netbird/route" ) @@ -22,11 +22,11 @@ type serverRouter struct { ctx context.Context routes map[route.ID]*route.Route firewall firewall.Manager - wgInterface iface.IWGIface + wgInterface iface.WGIface statusRecorder *peer.Status } -func newServerRouter(ctx context.Context, wgInterface iface.IWGIface, firewall firewall.Manager, statusRecorder *peer.Status) (*serverRouter, error) { +func newServerRouter(ctx context.Context, wgInterface iface.WGIface, firewall firewall.Manager, statusRecorder *peer.Status) (*serverRouter, error) { return &serverRouter{ ctx: ctx, routes: make(map[route.ID]*route.Route), @@ -71,9 +71,15 @@ func (m *serverRouter) updateRoutes(routesMap map[route.ID]*route.Route) error { } if len(m.routes) > 0 { - err := systemops.EnableIPForwarding() - if err != nil { - return err + if err := systemops.EnableIPForwarding(); err != nil { + return fmt.Errorf("enable ip forwarding: %w", err) + } + if err := m.firewall.EnableRouting(); err != nil { + return fmt.Errorf("enable routing: %w", err) + } + } else { + if err := m.firewall.DisableRouting(); err != nil { + return fmt.Errorf("disable routing: %w", err) } } diff --git a/client/internal/routemanager/sysctl/sysctl_linux.go b/client/internal/routemanager/sysctl/sysctl_linux.go index bb620ee68..ea63f02fc 100644 --- a/client/internal/routemanager/sysctl/sysctl_linux.go +++ b/client/internal/routemanager/sysctl/sysctl_linux.go @@ -13,7 +13,7 @@ import ( log "github.com/sirupsen/logrus" nberrors "github.com/netbirdio/netbird/client/errors" - "github.com/netbirdio/netbird/client/iface" + "github.com/netbirdio/netbird/client/internal/routemanager/iface" ) const ( @@ -23,7 +23,7 @@ const ( ) // Setup configures sysctl settings for RP filtering and source validation. -func Setup(wgIface iface.IWGIface) (map[string]int, error) { +func Setup(wgIface iface.WGIface) (map[string]int, error) { keys := map[string]int{} var result *multierror.Error diff --git a/client/internal/routemanager/systemops/systemops.go b/client/internal/routemanager/systemops/systemops.go index d1cb83bfb..5c117b94d 100644 --- a/client/internal/routemanager/systemops/systemops.go +++ b/client/internal/routemanager/systemops/systemops.go @@ -5,7 +5,7 @@ import ( "net/netip" "sync" - "github.com/netbirdio/netbird/client/iface" + "github.com/netbirdio/netbird/client/internal/routemanager/iface" "github.com/netbirdio/netbird/client/internal/routemanager/notifier" "github.com/netbirdio/netbird/client/internal/routemanager/refcounter" ) @@ -19,7 +19,7 @@ type ExclusionCounter = refcounter.Counter[netip.Prefix, struct{}, Nexthop] type SysOps struct { refCounter *ExclusionCounter - wgInterface iface.IWGIface + wgInterface iface.WGIface // prefixes is tracking all the current added prefixes im memory // (this is used in iOS as all route updates require a full table update) //nolint @@ -30,7 +30,7 @@ type SysOps struct { notifier *notifier.Notifier } -func NewSysOps(wgInterface iface.IWGIface, notifier *notifier.Notifier) *SysOps { +func NewSysOps(wgInterface iface.WGIface, notifier *notifier.Notifier) *SysOps { return &SysOps{ wgInterface: wgInterface, notifier: notifier, diff --git a/client/internal/routemanager/systemops/systemops_generic.go b/client/internal/routemanager/systemops/systemops_generic.go index 31b7f3ac2..eaef01815 100644 --- a/client/internal/routemanager/systemops/systemops_generic.go +++ b/client/internal/routemanager/systemops/systemops_generic.go @@ -16,8 +16,8 @@ import ( log "github.com/sirupsen/logrus" nberrors "github.com/netbirdio/netbird/client/errors" - "github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/iface/netstack" + "github.com/netbirdio/netbird/client/internal/routemanager/iface" "github.com/netbirdio/netbird/client/internal/routemanager/refcounter" "github.com/netbirdio/netbird/client/internal/routemanager/util" "github.com/netbirdio/netbird/client/internal/routemanager/vars" @@ -149,7 +149,7 @@ func (r *SysOps) addRouteForCurrentDefaultGateway(prefix netip.Prefix) error { // addRouteToNonVPNIntf adds a new route to the routing table for the given prefix and returns the next hop and interface. // If the next hop or interface is pointing to the VPN interface, it will return the initial values. -func (r *SysOps) addRouteToNonVPNIntf(prefix netip.Prefix, vpnIntf iface.IWGIface, initialNextHop Nexthop) (Nexthop, error) { +func (r *SysOps) addRouteToNonVPNIntf(prefix netip.Prefix, vpnIntf iface.WGIface, initialNextHop Nexthop) (Nexthop, error) { addr := prefix.Addr() switch { case addr.IsLoopback(), diff --git a/client/internal/routemanager/systemops/systemops_linux.go b/client/internal/routemanager/systemops/systemops_linux.go index 1da92cc80..d724cb1a7 100644 --- a/client/internal/routemanager/systemops/systemops_linux.go +++ b/client/internal/routemanager/systemops/systemops_linux.go @@ -53,20 +53,6 @@ type ruleParams struct { description string } -// isLegacy determines whether to use the legacy routing setup -func isLegacy() bool { - return os.Getenv("NB_USE_LEGACY_ROUTING") == "true" || nbnet.CustomRoutingDisabled() || nbnet.SkipSocketMark() -} - -// setIsLegacy sets the legacy routing setup -func setIsLegacy(b bool) { - if b { - os.Setenv("NB_USE_LEGACY_ROUTING", "true") - } else { - os.Unsetenv("NB_USE_LEGACY_ROUTING") - } -} - func getSetupRules() []ruleParams { return []ruleParams{ {100, -1, syscall.RT_TABLE_MAIN, netlink.FAMILY_V4, false, 0, "rule with suppress prefixlen v4"}, @@ -87,7 +73,7 @@ func getSetupRules() []ruleParams { // This table is where a default route or other specific routes received from the management server are configured, // enabling VPN connectivity. func (r *SysOps) SetupRouting(initAddresses []net.IP, stateManager *statemanager.Manager) (_ nbnet.AddHookFunc, _ nbnet.RemoveHookFunc, err error) { - if isLegacy() { + if !nbnet.AdvancedRouting() { log.Infof("Using legacy routing setup") return r.setupRefCounter(initAddresses, stateManager) } @@ -103,11 +89,6 @@ func (r *SysOps) SetupRouting(initAddresses []net.IP, stateManager *statemanager rules := getSetupRules() for _, rule := range rules { if err := addRule(rule); err != nil { - if errors.Is(err, syscall.EOPNOTSUPP) { - log.Warnf("Rule operations are not supported, falling back to the legacy routing setup") - setIsLegacy(true) - return r.setupRefCounter(initAddresses, stateManager) - } return nil, nil, fmt.Errorf("%s: %w", rule.description, err) } } @@ -130,7 +111,7 @@ func (r *SysOps) SetupRouting(initAddresses []net.IP, stateManager *statemanager // It systematically removes the three rules and any associated routing table entries to ensure a clean state. // The function uses error aggregation to report any errors encountered during the cleanup process. func (r *SysOps) CleanupRouting(stateManager *statemanager.Manager) error { - if isLegacy() { + if !nbnet.AdvancedRouting() { return r.cleanupRefCounter(stateManager) } @@ -168,7 +149,7 @@ func (r *SysOps) removeFromRouteTable(prefix netip.Prefix, nexthop Nexthop) erro } func (r *SysOps) AddVPNRoute(prefix netip.Prefix, intf *net.Interface) error { - if isLegacy() { + if !nbnet.AdvancedRouting() { return r.genericAddVPNRoute(prefix, intf) } @@ -191,7 +172,7 @@ func (r *SysOps) AddVPNRoute(prefix netip.Prefix, intf *net.Interface) error { } func (r *SysOps) RemoveVPNRoute(prefix netip.Prefix, intf *net.Interface) error { - if isLegacy() { + if !nbnet.AdvancedRouting() { return r.genericRemoveVPNRoute(prefix, intf) } @@ -504,7 +485,7 @@ func getAddressFamily(prefix netip.Prefix) int { } func hasSeparateRouting() ([]netip.Prefix, error) { - if isLegacy() { + if !nbnet.AdvancedRouting() { return GetRoutesFromTable() } return nil, ErrRoutingIsSeparate diff --git a/client/internal/routemanager/systemops/systemops_unix_test.go b/client/internal/routemanager/systemops/systemops_unix_test.go index a6000d963..d88c1ab6b 100644 --- a/client/internal/routemanager/systemops/systemops_unix_test.go +++ b/client/internal/routemanager/systemops/systemops_unix_test.go @@ -85,6 +85,7 @@ var testCases = []testCase{ } func TestRouting(t *testing.T) { + nbnet.Init() for _, tc := range testCases { // todo resolve test execution on freebsd if runtime.GOOS == "freebsd" { diff --git a/client/internal/statemanager/manager.go b/client/internal/statemanager/manager.go index 9a99c76f1..29f962ad2 100644 --- a/client/internal/statemanager/manager.go +++ b/client/internal/statemanager/manager.go @@ -303,20 +303,29 @@ func (m *Manager) loadStateFile(deleteCorrupt bool) (map[string]json.RawMessage, var rawStates map[string]json.RawMessage if err := json.Unmarshal(data, &rawStates); err != nil { - if deleteCorrupt { - log.Warn("State file appears to be corrupted, attempting to delete it", err) - if err := os.Remove(m.filePath); err != nil { - log.Errorf("Failed to delete corrupted state file: %v", err) - } else { - log.Info("State file deleted") - } - } + m.handleCorruptedState(deleteCorrupt) return nil, fmt.Errorf("unmarshal states: %w", err) } return rawStates, nil } +// handleCorruptedState creates a backup of a corrupted state file by moving it +func (m *Manager) handleCorruptedState(deleteCorrupt bool) { + if !deleteCorrupt { + return + } + log.Warn("State file appears to be corrupted, attempting to back it up") + + backupPath := fmt.Sprintf("%s.corrupted.%d", m.filePath, time.Now().UnixNano()) + if err := os.Rename(m.filePath, backupPath); err != nil { + log.Errorf("Failed to backup corrupted state file: %v", err) + return + } + + log.Infof("Created backup of corrupted state file at: %s", backupPath) +} + // loadSingleRawState unmarshals a raw state into a concrete state object func (m *Manager) loadSingleRawState(name string, rawState json.RawMessage) (State, error) { stateType, ok := m.stateTypes[name] diff --git a/client/internal/stdnet/filter.go b/client/internal/stdnet/filter.go index c04250b2d..e45714001 100644 --- a/client/internal/stdnet/filter.go +++ b/client/internal/stdnet/filter.go @@ -21,7 +21,6 @@ func InterfaceFilter(disallowList []string) func(string) bool { for _, s := range disallowList { if strings.HasPrefix(iFace, s) && runtime.GOOS != "ios" { - log.Tracef("ignoring interface %s - it is not allowed", iFace) return false } } diff --git a/client/internal/stdnet/stdnet.go b/client/internal/stdnet/stdnet.go index 2e87475a5..aa9fdd045 100644 --- a/client/internal/stdnet/stdnet.go +++ b/client/internal/stdnet/stdnet.go @@ -5,11 +5,16 @@ package stdnet import ( "fmt" + "slices" + "sync" + "time" "github.com/pion/transport/v3" "github.com/pion/transport/v3/stdnet" ) +const updateInterval = 30 * time.Second + // Net is an implementation of the net.Net interface // based on functions of the standard net package. type Net struct { @@ -18,6 +23,10 @@ type Net struct { iFaceDiscover iFaceDiscover // interfaceFilter should return true if the given interfaceName is allowed interfaceFilter func(interfaceName string) bool + lastUpdate time.Time + + // mu is shared between interfaces and lastUpdate + mu sync.Mutex } // NewNetWithDiscover creates a new StdNet instance. @@ -43,18 +52,40 @@ func NewNet(disallowList []string) (*Net, error) { // The interfaces are discovered by an external iFaceDiscover function or by a default discoverer if the external one // wasn't specified. func (n *Net) UpdateInterfaces() (err error) { + n.mu.Lock() + defer n.mu.Unlock() + + return n.updateInterfaces() +} + +func (n *Net) updateInterfaces() (err error) { allIfaces, err := n.iFaceDiscover.iFaces() if err != nil { return err } + n.interfaces = n.filterInterfaces(allIfaces) + + n.lastUpdate = time.Now() + return nil } // Interfaces returns a slice of interfaces which are available on the // system func (n *Net) Interfaces() ([]*transport.Interface, error) { - return n.interfaces, nil + n.mu.Lock() + defer n.mu.Unlock() + + if time.Since(n.lastUpdate) < updateInterval { + return slices.Clone(n.interfaces), nil + } + + if err := n.updateInterfaces(); err != nil { + return nil, fmt.Errorf("update interfaces: %w", err) + } + + return slices.Clone(n.interfaces), nil } // InterfaceByIndex returns the interface specified by index. @@ -63,6 +94,8 @@ func (n *Net) Interfaces() ([]*transport.Interface, error) { // sharing the logical data link; for more precision use // InterfaceByName. func (n *Net) InterfaceByIndex(index int) (*transport.Interface, error) { + n.mu.Lock() + defer n.mu.Unlock() for _, ifc := range n.interfaces { if ifc.Index == index { return ifc, nil @@ -74,6 +107,8 @@ func (n *Net) InterfaceByIndex(index int) (*transport.Interface, error) { // InterfaceByName returns the interface specified by name. func (n *Net) InterfaceByName(name string) (*transport.Interface, error) { + n.mu.Lock() + defer n.mu.Unlock() for _, ifc := range n.interfaces { if ifc.Name == name { return ifc, nil @@ -87,7 +122,7 @@ func (n *Net) filterInterfaces(interfaces []*transport.Interface) []*transport.I if n.interfaceFilter == nil { return interfaces } - result := []*transport.Interface{} + var result []*transport.Interface for _, iface := range interfaces { if n.interfaceFilter(iface.Name) { result = append(result, iface) diff --git a/client/ios/NetBirdSDK/client.go b/client/ios/NetBirdSDK/client.go index befce56a2..622f8e840 100644 --- a/client/ios/NetBirdSDK/client.go +++ b/client/ios/NetBirdSDK/client.go @@ -207,7 +207,7 @@ func (c *Client) IsLoginRequired() bool { ConfigPath: c.cfgFile, }) - needsLogin, _ := internal.IsLoginRequired(ctx, cfg.PrivateKey, cfg.ManagementURL, cfg.SSHKey) + needsLogin, _ := internal.IsLoginRequired(ctx, cfg) return needsLogin } diff --git a/client/ios/NetBirdSDK/login.go b/client/ios/NetBirdSDK/login.go index ff637edd4..986874758 100644 --- a/client/ios/NetBirdSDK/login.go +++ b/client/ios/NetBirdSDK/login.go @@ -123,7 +123,7 @@ func (a *Auth) Login() error { // check if we need to generate JWT token err := a.withBackOff(a.ctx, func() (err error) { - needsLogin, err = internal.IsLoginRequired(a.ctx, a.config.PrivateKey, a.config.ManagementURL, a.config.SSHKey) + needsLogin, err = internal.IsLoginRequired(a.ctx, a.config) return }) if err != nil { diff --git a/client/netbird.wxs b/client/netbird.wxs index 0e2be7b3c..ee9ab667f 100644 --- a/client/netbird.wxs +++ b/client/netbird.wxs @@ -1,6 +1,6 @@ - @@ -75,4 +75,4 @@ - \ No newline at end of file + diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index 659277570..55b7aa7e9 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.26.0 -// protoc v4.23.4 +// protoc v4.24.3 // source: daemon.proto package proto @@ -87,12 +87,119 @@ func (LogLevel) EnumDescriptor() ([]byte, []int) { return file_daemon_proto_rawDescGZIP(), []int{0} } +type SystemEvent_Severity int32 + +const ( + SystemEvent_INFO SystemEvent_Severity = 0 + SystemEvent_WARNING SystemEvent_Severity = 1 + SystemEvent_ERROR SystemEvent_Severity = 2 + SystemEvent_CRITICAL SystemEvent_Severity = 3 +) + +// Enum value maps for SystemEvent_Severity. +var ( + SystemEvent_Severity_name = map[int32]string{ + 0: "INFO", + 1: "WARNING", + 2: "ERROR", + 3: "CRITICAL", + } + SystemEvent_Severity_value = map[string]int32{ + "INFO": 0, + "WARNING": 1, + "ERROR": 2, + "CRITICAL": 3, + } +) + +func (x SystemEvent_Severity) Enum() *SystemEvent_Severity { + p := new(SystemEvent_Severity) + *p = x + return p +} + +func (x SystemEvent_Severity) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SystemEvent_Severity) Descriptor() protoreflect.EnumDescriptor { + return file_daemon_proto_enumTypes[1].Descriptor() +} + +func (SystemEvent_Severity) Type() protoreflect.EnumType { + return &file_daemon_proto_enumTypes[1] +} + +func (x SystemEvent_Severity) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SystemEvent_Severity.Descriptor instead. +func (SystemEvent_Severity) EnumDescriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{45, 0} +} + +type SystemEvent_Category int32 + +const ( + SystemEvent_NETWORK SystemEvent_Category = 0 + SystemEvent_DNS SystemEvent_Category = 1 + SystemEvent_AUTHENTICATION SystemEvent_Category = 2 + SystemEvent_CONNECTIVITY SystemEvent_Category = 3 + SystemEvent_SYSTEM SystemEvent_Category = 4 +) + +// Enum value maps for SystemEvent_Category. +var ( + SystemEvent_Category_name = map[int32]string{ + 0: "NETWORK", + 1: "DNS", + 2: "AUTHENTICATION", + 3: "CONNECTIVITY", + 4: "SYSTEM", + } + SystemEvent_Category_value = map[string]int32{ + "NETWORK": 0, + "DNS": 1, + "AUTHENTICATION": 2, + "CONNECTIVITY": 3, + "SYSTEM": 4, + } +) + +func (x SystemEvent_Category) Enum() *SystemEvent_Category { + p := new(SystemEvent_Category) + *p = x + return p +} + +func (x SystemEvent_Category) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SystemEvent_Category) Descriptor() protoreflect.EnumDescriptor { + return file_daemon_proto_enumTypes[2].Descriptor() +} + +func (SystemEvent_Category) Type() protoreflect.EnumType { + return &file_daemon_proto_enumTypes[2] +} + +func (x SystemEvent_Category) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SystemEvent_Category.Descriptor instead. +func (SystemEvent_Category) EnumDescriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{45, 1} +} + type LoginRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // setupKey wiretrustee setup key. + // setupKey netbird setup key. SetupKey string `protobuf:"bytes,1,opt,name=setupKey,proto3" json:"setupKey,omitempty"` // This is the old PreSharedKey field which will be deprecated in favor of optionalPreSharedKey field that is defined as optional // to allow clearing of preshared key while being able to persist in the config file. @@ -126,6 +233,13 @@ type LoginRequest struct { DisableServerRoutes *bool `protobuf:"varint,21,opt,name=disable_server_routes,json=disableServerRoutes,proto3,oneof" json:"disable_server_routes,omitempty"` DisableDns *bool `protobuf:"varint,22,opt,name=disable_dns,json=disableDns,proto3,oneof" json:"disable_dns,omitempty"` DisableFirewall *bool `protobuf:"varint,23,opt,name=disable_firewall,json=disableFirewall,proto3,oneof" json:"disable_firewall,omitempty"` + BlockLanAccess *bool `protobuf:"varint,24,opt,name=block_lan_access,json=blockLanAccess,proto3,oneof" json:"block_lan_access,omitempty"` + DisableNotifications *bool `protobuf:"varint,25,opt,name=disable_notifications,json=disableNotifications,proto3,oneof" json:"disable_notifications,omitempty"` + DnsLabels []string `protobuf:"bytes,26,rep,name=dns_labels,json=dnsLabels,proto3" json:"dns_labels,omitempty"` + // cleanDNSLabels clean map list of DNS labels. + // This is needed because the generated code + // omits initialized empty slices due to omitempty tags + CleanDNSLabels bool `protobuf:"varint,27,opt,name=cleanDNSLabels,proto3" json:"cleanDNSLabels,omitempty"` } func (x *LoginRequest) Reset() { @@ -322,6 +436,34 @@ func (x *LoginRequest) GetDisableFirewall() bool { return false } +func (x *LoginRequest) GetBlockLanAccess() bool { + if x != nil && x.BlockLanAccess != nil { + return *x.BlockLanAccess + } + return false +} + +func (x *LoginRequest) GetDisableNotifications() bool { + if x != nil && x.DisableNotifications != nil { + return *x.DisableNotifications + } + return false +} + +func (x *LoginRequest) GetDnsLabels() []string { + if x != nil { + return x.DnsLabels + } + return nil +} + +func (x *LoginRequest) GetCleanDNSLabels() bool { + if x != nil { + return x.CleanDNSLabels + } + return false +} + type LoginResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -802,13 +944,14 @@ type GetConfigResponse struct { // preSharedKey settings value. PreSharedKey string `protobuf:"bytes,4,opt,name=preSharedKey,proto3" json:"preSharedKey,omitempty"` // adminURL settings value. - AdminURL string `protobuf:"bytes,5,opt,name=adminURL,proto3" json:"adminURL,omitempty"` - InterfaceName string `protobuf:"bytes,6,opt,name=interfaceName,proto3" json:"interfaceName,omitempty"` - WireguardPort int64 `protobuf:"varint,7,opt,name=wireguardPort,proto3" json:"wireguardPort,omitempty"` - DisableAutoConnect bool `protobuf:"varint,9,opt,name=disableAutoConnect,proto3" json:"disableAutoConnect,omitempty"` - ServerSSHAllowed bool `protobuf:"varint,10,opt,name=serverSSHAllowed,proto3" json:"serverSSHAllowed,omitempty"` - RosenpassEnabled bool `protobuf:"varint,11,opt,name=rosenpassEnabled,proto3" json:"rosenpassEnabled,omitempty"` - RosenpassPermissive bool `protobuf:"varint,12,opt,name=rosenpassPermissive,proto3" json:"rosenpassPermissive,omitempty"` + AdminURL string `protobuf:"bytes,5,opt,name=adminURL,proto3" json:"adminURL,omitempty"` + InterfaceName string `protobuf:"bytes,6,opt,name=interfaceName,proto3" json:"interfaceName,omitempty"` + WireguardPort int64 `protobuf:"varint,7,opt,name=wireguardPort,proto3" json:"wireguardPort,omitempty"` + DisableAutoConnect bool `protobuf:"varint,9,opt,name=disableAutoConnect,proto3" json:"disableAutoConnect,omitempty"` + ServerSSHAllowed bool `protobuf:"varint,10,opt,name=serverSSHAllowed,proto3" json:"serverSSHAllowed,omitempty"` + RosenpassEnabled bool `protobuf:"varint,11,opt,name=rosenpassEnabled,proto3" json:"rosenpassEnabled,omitempty"` + RosenpassPermissive bool `protobuf:"varint,12,opt,name=rosenpassPermissive,proto3" json:"rosenpassPermissive,omitempty"` + DisableNotifications bool `protobuf:"varint,13,opt,name=disable_notifications,json=disableNotifications,proto3" json:"disable_notifications,omitempty"` } func (x *GetConfigResponse) Reset() { @@ -920,6 +1063,13 @@ func (x *GetConfigResponse) GetRosenpassPermissive() bool { return false } +func (x *GetConfigResponse) GetDisableNotifications() bool { + if x != nil { + return x.DisableNotifications + } + return false +} + // PeerState contains the latest state of a peer type PeerState struct { state protoimpl.MessageState @@ -1467,6 +1617,7 @@ type FullStatus struct { Peers []*PeerState `protobuf:"bytes,4,rep,name=peers,proto3" json:"peers,omitempty"` Relays []*RelayState `protobuf:"bytes,5,rep,name=relays,proto3" json:"relays,omitempty"` DnsServers []*NSGroupState `protobuf:"bytes,6,rep,name=dns_servers,json=dnsServers,proto3" json:"dns_servers,omitempty"` + Events []*SystemEvent `protobuf:"bytes,7,rep,name=events,proto3" json:"events,omitempty"` } func (x *FullStatus) Reset() { @@ -1543,6 +1694,13 @@ func (x *FullStatus) GetDnsServers() []*NSGroupState { return nil } +func (x *FullStatus) GetEvents() []*SystemEvent { + if x != nil { + return x.Events + } + return nil +} + type ListNetworksRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2563,6 +2721,548 @@ func (*SetNetworkMapPersistenceResponse) Descriptor() ([]byte, []int) { return file_daemon_proto_rawDescGZIP(), []int{39} } +type TCPFlags struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Syn bool `protobuf:"varint,1,opt,name=syn,proto3" json:"syn,omitempty"` + Ack bool `protobuf:"varint,2,opt,name=ack,proto3" json:"ack,omitempty"` + Fin bool `protobuf:"varint,3,opt,name=fin,proto3" json:"fin,omitempty"` + Rst bool `protobuf:"varint,4,opt,name=rst,proto3" json:"rst,omitempty"` + Psh bool `protobuf:"varint,5,opt,name=psh,proto3" json:"psh,omitempty"` + Urg bool `protobuf:"varint,6,opt,name=urg,proto3" json:"urg,omitempty"` +} + +func (x *TCPFlags) Reset() { + *x = TCPFlags{} + if protoimpl.UnsafeEnabled { + mi := &file_daemon_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TCPFlags) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TCPFlags) ProtoMessage() {} + +func (x *TCPFlags) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[40] + 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 TCPFlags.ProtoReflect.Descriptor instead. +func (*TCPFlags) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{40} +} + +func (x *TCPFlags) GetSyn() bool { + if x != nil { + return x.Syn + } + return false +} + +func (x *TCPFlags) GetAck() bool { + if x != nil { + return x.Ack + } + return false +} + +func (x *TCPFlags) GetFin() bool { + if x != nil { + return x.Fin + } + return false +} + +func (x *TCPFlags) GetRst() bool { + if x != nil { + return x.Rst + } + return false +} + +func (x *TCPFlags) GetPsh() bool { + if x != nil { + return x.Psh + } + return false +} + +func (x *TCPFlags) GetUrg() bool { + if x != nil { + return x.Urg + } + return false +} + +type TracePacketRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + SourceIp string `protobuf:"bytes,1,opt,name=source_ip,json=sourceIp,proto3" json:"source_ip,omitempty"` + DestinationIp string `protobuf:"bytes,2,opt,name=destination_ip,json=destinationIp,proto3" json:"destination_ip,omitempty"` + Protocol string `protobuf:"bytes,3,opt,name=protocol,proto3" json:"protocol,omitempty"` + SourcePort uint32 `protobuf:"varint,4,opt,name=source_port,json=sourcePort,proto3" json:"source_port,omitempty"` + DestinationPort uint32 `protobuf:"varint,5,opt,name=destination_port,json=destinationPort,proto3" json:"destination_port,omitempty"` + Direction string `protobuf:"bytes,6,opt,name=direction,proto3" json:"direction,omitempty"` + TcpFlags *TCPFlags `protobuf:"bytes,7,opt,name=tcp_flags,json=tcpFlags,proto3,oneof" json:"tcp_flags,omitempty"` + IcmpType *uint32 `protobuf:"varint,8,opt,name=icmp_type,json=icmpType,proto3,oneof" json:"icmp_type,omitempty"` + IcmpCode *uint32 `protobuf:"varint,9,opt,name=icmp_code,json=icmpCode,proto3,oneof" json:"icmp_code,omitempty"` +} + +func (x *TracePacketRequest) Reset() { + *x = TracePacketRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_daemon_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TracePacketRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TracePacketRequest) ProtoMessage() {} + +func (x *TracePacketRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[41] + 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 TracePacketRequest.ProtoReflect.Descriptor instead. +func (*TracePacketRequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{41} +} + +func (x *TracePacketRequest) GetSourceIp() string { + if x != nil { + return x.SourceIp + } + return "" +} + +func (x *TracePacketRequest) GetDestinationIp() string { + if x != nil { + return x.DestinationIp + } + return "" +} + +func (x *TracePacketRequest) GetProtocol() string { + if x != nil { + return x.Protocol + } + return "" +} + +func (x *TracePacketRequest) GetSourcePort() uint32 { + if x != nil { + return x.SourcePort + } + return 0 +} + +func (x *TracePacketRequest) GetDestinationPort() uint32 { + if x != nil { + return x.DestinationPort + } + return 0 +} + +func (x *TracePacketRequest) GetDirection() string { + if x != nil { + return x.Direction + } + return "" +} + +func (x *TracePacketRequest) GetTcpFlags() *TCPFlags { + if x != nil { + return x.TcpFlags + } + return nil +} + +func (x *TracePacketRequest) GetIcmpType() uint32 { + if x != nil && x.IcmpType != nil { + return *x.IcmpType + } + return 0 +} + +func (x *TracePacketRequest) GetIcmpCode() uint32 { + if x != nil && x.IcmpCode != nil { + return *x.IcmpCode + } + return 0 +} + +type TraceStage struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + Allowed bool `protobuf:"varint,3,opt,name=allowed,proto3" json:"allowed,omitempty"` + ForwardingDetails *string `protobuf:"bytes,4,opt,name=forwarding_details,json=forwardingDetails,proto3,oneof" json:"forwarding_details,omitempty"` +} + +func (x *TraceStage) Reset() { + *x = TraceStage{} + if protoimpl.UnsafeEnabled { + mi := &file_daemon_proto_msgTypes[42] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TraceStage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TraceStage) ProtoMessage() {} + +func (x *TraceStage) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[42] + 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 TraceStage.ProtoReflect.Descriptor instead. +func (*TraceStage) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{42} +} + +func (x *TraceStage) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *TraceStage) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *TraceStage) GetAllowed() bool { + if x != nil { + return x.Allowed + } + return false +} + +func (x *TraceStage) GetForwardingDetails() string { + if x != nil && x.ForwardingDetails != nil { + return *x.ForwardingDetails + } + return "" +} + +type TracePacketResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Stages []*TraceStage `protobuf:"bytes,1,rep,name=stages,proto3" json:"stages,omitempty"` + FinalDisposition bool `protobuf:"varint,2,opt,name=final_disposition,json=finalDisposition,proto3" json:"final_disposition,omitempty"` +} + +func (x *TracePacketResponse) Reset() { + *x = TracePacketResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_daemon_proto_msgTypes[43] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TracePacketResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TracePacketResponse) ProtoMessage() {} + +func (x *TracePacketResponse) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[43] + 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 TracePacketResponse.ProtoReflect.Descriptor instead. +func (*TracePacketResponse) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{43} +} + +func (x *TracePacketResponse) GetStages() []*TraceStage { + if x != nil { + return x.Stages + } + return nil +} + +func (x *TracePacketResponse) GetFinalDisposition() bool { + if x != nil { + return x.FinalDisposition + } + return false +} + +type SubscribeRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *SubscribeRequest) Reset() { + *x = SubscribeRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_daemon_proto_msgTypes[44] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SubscribeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubscribeRequest) ProtoMessage() {} + +func (x *SubscribeRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[44] + 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 SubscribeRequest.ProtoReflect.Descriptor instead. +func (*SubscribeRequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{44} +} + +type SystemEvent struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Severity SystemEvent_Severity `protobuf:"varint,2,opt,name=severity,proto3,enum=daemon.SystemEvent_Severity" json:"severity,omitempty"` + Category SystemEvent_Category `protobuf:"varint,3,opt,name=category,proto3,enum=daemon.SystemEvent_Category" json:"category,omitempty"` + Message string `protobuf:"bytes,4,opt,name=message,proto3" json:"message,omitempty"` + UserMessage string `protobuf:"bytes,5,opt,name=userMessage,proto3" json:"userMessage,omitempty"` + Timestamp *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + Metadata map[string]string `protobuf:"bytes,7,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *SystemEvent) Reset() { + *x = SystemEvent{} + if protoimpl.UnsafeEnabled { + mi := &file_daemon_proto_msgTypes[45] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SystemEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SystemEvent) ProtoMessage() {} + +func (x *SystemEvent) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[45] + 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 SystemEvent.ProtoReflect.Descriptor instead. +func (*SystemEvent) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{45} +} + +func (x *SystemEvent) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *SystemEvent) GetSeverity() SystemEvent_Severity { + if x != nil { + return x.Severity + } + return SystemEvent_INFO +} + +func (x *SystemEvent) GetCategory() SystemEvent_Category { + if x != nil { + return x.Category + } + return SystemEvent_NETWORK +} + +func (x *SystemEvent) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *SystemEvent) GetUserMessage() string { + if x != nil { + return x.UserMessage + } + return "" +} + +func (x *SystemEvent) GetTimestamp() *timestamppb.Timestamp { + if x != nil { + return x.Timestamp + } + return nil +} + +func (x *SystemEvent) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +type GetEventsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *GetEventsRequest) Reset() { + *x = GetEventsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_daemon_proto_msgTypes[46] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetEventsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetEventsRequest) ProtoMessage() {} + +func (x *GetEventsRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[46] + 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 GetEventsRequest.ProtoReflect.Descriptor instead. +func (*GetEventsRequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{46} +} + +type GetEventsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Events []*SystemEvent `protobuf:"bytes,1,rep,name=events,proto3" json:"events,omitempty"` +} + +func (x *GetEventsResponse) Reset() { + *x = GetEventsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_daemon_proto_msgTypes[47] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetEventsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetEventsResponse) ProtoMessage() {} + +func (x *GetEventsResponse) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[47] + 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 GetEventsResponse.ProtoReflect.Descriptor instead. +func (*GetEventsResponse) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{47} +} + +func (x *GetEventsResponse) GetEvents() []*SystemEvent { + if x != nil { + return x.Events + } + return nil +} + var File_daemon_proto protoreflect.FileDescriptor var file_daemon_proto_rawDesc = []byte{ @@ -2573,7 +3273,7 @@ var file_daemon_proto_rawDesc = []byte{ 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, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xd1, 0x0a, 0x0a, 0x0c, 0x4c, 0x6f, + 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb0, 0x0c, 0x0a, 0x0c, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65, 0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x12, 0x26, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, @@ -2641,7 +3341,18 @@ var file_daemon_proto_rawDesc = []byte{ 0x44, 0x6e, 0x73, 0x88, 0x01, 0x01, 0x12, 0x2e, 0x0a, 0x10, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x18, 0x17, 0x20, 0x01, 0x28, 0x08, 0x48, 0x0c, 0x52, 0x0f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, - 0x61, 0x6c, 0x6c, 0x88, 0x01, 0x01, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e, + 0x61, 0x6c, 0x6c, 0x88, 0x01, 0x01, 0x12, 0x2d, 0x0a, 0x10, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, + 0x6c, 0x61, 0x6e, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x18, 0x20, 0x01, 0x28, 0x08, + 0x48, 0x0d, 0x52, 0x0e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x4c, 0x61, 0x6e, 0x41, 0x63, 0x63, 0x65, + 0x73, 0x73, 0x88, 0x01, 0x01, 0x12, 0x38, 0x0a, 0x15, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, + 0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x19, + 0x20, 0x01, 0x28, 0x08, 0x48, 0x0e, 0x52, 0x14, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x4e, + 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x88, 0x01, 0x01, 0x12, + 0x1d, 0x0a, 0x0a, 0x64, 0x6e, 0x73, 0x5f, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x1a, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x09, 0x64, 0x6e, 0x73, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x26, + 0x0a, 0x0e, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x44, 0x4e, 0x53, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, + 0x18, 0x1b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x44, 0x4e, 0x53, + 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x42, @@ -2658,337 +3369,448 @@ var file_daemon_proto_rawDesc = []byte{ 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x6e, 0x73, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x64, 0x69, 0x73, - 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x22, 0xb5, 0x01, - 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x24, 0x0a, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, - 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, - 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x55, 0x52, 0x49, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x76, 0x65, 0x72, 0x69, - 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x12, 0x38, 0x0a, 0x17, 0x76, - 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, - 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x76, 0x65, + 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x42, 0x13, 0x0a, + 0x11, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x6c, 0x61, 0x6e, 0x5f, 0x61, 0x63, 0x63, 0x65, + 0x73, 0x73, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e, + 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xb5, 0x01, 0x0a, + 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, + 0x0a, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, + 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, + 0x12, 0x28, 0x0a, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x55, 0x52, 0x49, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x12, 0x38, 0x0a, 0x17, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, - 0x70, 0x6c, 0x65, 0x74, 0x65, 0x22, 0x4d, 0x0a, 0x13, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, - 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, - 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, - 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x16, 0x0a, 0x14, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, - 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x0b, 0x0a, 0x09, - 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0c, 0x0a, 0x0a, 0x55, 0x70, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3d, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x67, 0x65, 0x74, 0x46, - 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, - 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x82, 0x01, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x12, 0x32, 0x0a, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, - 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x56, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x0d, 0x0a, 0x0b, 0x44, - 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0e, 0x0a, 0x0c, 0x44, 0x6f, - 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, - 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xb9, - 0x03, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, - 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6c, 0x6f, - 0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, - 0x46, 0x69, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, - 0x64, 0x4b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x65, 0x53, - 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x64, 0x6d, 0x69, - 0x6e, 0x55, 0x52, 0x4c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, - 0x6e, 0x55, 0x52, 0x4c, 0x12, 0x24, 0x0a, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, - 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x69, 0x6e, 0x74, - 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x77, 0x69, - 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, - 0x12, 0x2e, 0x0a, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, - 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x64, 0x69, - 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, - 0x12, 0x2a, 0x0a, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, - 0x6f, 0x77, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x73, 0x65, 0x72, 0x76, - 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x2a, 0x0a, 0x10, - 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, - 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, - 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, - 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, - 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, - 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x22, 0xde, 0x05, 0x0a, 0x09, 0x50, - 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, - 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, - 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x12, 0x46, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 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, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x6c, 0x61, - 0x79, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, - 0x65, 0x64, 0x12, 0x34, 0x0a, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, - 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, - 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x36, 0x0a, 0x16, 0x72, 0x65, 0x6d, 0x6f, - 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, - 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, - 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, - 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x66, 0x71, 0x64, 0x6e, 0x12, 0x3c, 0x0a, 0x19, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, - 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, - 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x19, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, - 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, - 0x6e, 0x74, 0x12, 0x3e, 0x0a, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, - 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, - 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, - 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, - 0x6e, 0x74, 0x12, 0x52, 0x0a, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, - 0x61, 0x72, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x18, 0x0c, 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, 0x16, - 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x48, 0x61, 0x6e, - 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x52, - 0x78, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x52, 0x78, - 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x18, 0x0e, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, - 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0f, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, - 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x73, 0x12, 0x33, 0x0a, 0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x11, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, - 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x79, - 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, - 0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0xf0, 0x01, 0x0a, 0x0e, - 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, - 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, - 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, - 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, - 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x66, 0x71, 0x64, 0x6e, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, - 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, - 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, - 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, - 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72, - 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, - 0x76, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x07, - 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x22, 0x53, - 0x0a, 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, - 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, - 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, - 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, - 0x72, 0x6f, 0x72, 0x22, 0x57, 0x0a, 0x0f, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, - 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, - 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x52, 0x0a, 0x0a, - 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, - 0x49, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x49, 0x12, 0x1c, 0x0a, 0x09, - 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, + 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x76, 0x65, 0x72, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70, + 0x6c, 0x65, 0x74, 0x65, 0x22, 0x4d, 0x0a, 0x13, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, + 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, + 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, + 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, + 0x61, 0x6d, 0x65, 0x22, 0x16, 0x0a, 0x14, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, + 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x0b, 0x0a, 0x09, 0x55, + 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0c, 0x0a, 0x0a, 0x55, 0x70, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3d, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, + 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x82, 0x01, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x12, 0x32, 0x0a, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x75, + 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x56, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x0d, 0x0a, 0x0b, 0x44, 0x6f, + 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0e, 0x0a, 0x0c, 0x44, 0x6f, 0x77, + 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xee, 0x03, + 0x0a, 0x11, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x55, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6c, 0x6f, 0x67, + 0x46, 0x69, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x46, + 0x69, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, + 0x4b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, + 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, + 0x55, 0x52, 0x4c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, + 0x55, 0x52, 0x4c, 0x12, 0x24, 0x0a, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, + 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x77, 0x69, 0x72, + 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x12, + 0x2e, 0x0a, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x64, 0x69, 0x73, + 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x12, + 0x2a, 0x0a, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, + 0x77, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x2a, 0x0a, 0x10, 0x72, + 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, + 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, + 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, + 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x0c, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, + 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x12, 0x33, 0x0a, 0x15, 0x64, 0x69, 0x73, + 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, + 0x65, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xde, + 0x05, 0x0a, 0x09, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, + 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, + 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, + 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x12, 0x46, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 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, 0x10, 0x63, 0x6f, 0x6e, 0x6e, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, + 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72, + 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, + 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, + 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x36, 0x0a, 0x16, + 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, + 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x72, 0x65, + 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x09, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x3c, 0x0a, 0x19, 0x6c, 0x6f, 0x63, 0x61, + 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, + 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x19, 0x6c, 0x6f, 0x63, + 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, + 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x3e, 0x0a, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, + 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, + 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1a, 0x72, 0x65, 0x6d, 0x6f, + 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, + 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x52, 0x0a, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69, + 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, + 0x18, 0x0c, 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, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, + 0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79, + 0x74, 0x65, 0x73, 0x52, 0x78, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74, + 0x65, 0x73, 0x52, 0x78, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x18, + 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x12, 0x2a, + 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, + 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65, + 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, + 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x33, 0x0a, 0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, + 0x79, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x72, + 0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x12, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, + 0xf0, 0x01, 0x0a, 0x0e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, + 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x6b, 0x65, + 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, + 0x66, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, + 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, + 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, + 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, + 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x73, 0x22, 0x53, 0x0a, 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, + 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x57, 0x0a, 0x0f, 0x4d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, + 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, + 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, - 0x22, 0x72, 0x0a, 0x0c, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, - 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, - 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, - 0x61, 0x69, 0x6e, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x14, - 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, - 0x72, 0x72, 0x6f, 0x72, 0x22, 0xd2, 0x02, 0x0a, 0x0a, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x12, 0x41, 0x0a, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, - 0x52, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x3e, 0x0a, - 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, - 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0e, 0x6c, - 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x27, 0x0a, - 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, - 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2a, 0x0a, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, - 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, 0x72, 0x65, 0x6c, 0x61, - 0x79, 0x73, 0x12, 0x35, 0x0a, 0x0b, 0x64, 0x6e, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0a, 0x64, - 0x6e, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x22, 0x15, 0x0a, 0x13, 0x4c, 0x69, 0x73, - 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x22, 0x3f, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74, - 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, - 0x73, 0x22, 0x61, 0x0a, 0x15, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x6e, 0x65, - 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x44, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, - 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x44, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x70, - 0x70, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x61, 0x70, 0x70, 0x65, - 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x18, 0x0a, 0x16, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, - 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1a, - 0x0a, 0x06, 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x70, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x69, 0x70, 0x73, 0x22, 0xf9, 0x01, 0x0a, 0x07, 0x4e, - 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x1a, 0x0a, 0x08, - 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, - 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, - 0x69, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, - 0x6e, 0x73, 0x12, 0x42, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, - 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, - 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x6c, - 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x1a, 0x4e, 0x0a, 0x10, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, - 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x6a, 0x0a, 0x12, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, - 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, - 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x69, 0x7a, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, - 0x66, 0x6f, 0x22, 0x29, 0x0a, 0x13, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, - 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x22, 0x14, 0x0a, - 0x12, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x22, 0x3d, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, - 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, - 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, - 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, - 0x65, 0x6c, 0x22, 0x3c, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, - 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, - 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, - 0x22, 0x15, 0x0a, 0x13, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1b, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, - 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x13, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3b, 0x0a, 0x12, 0x4c, 0x69, 0x73, - 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x25, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x0d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, - 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x44, 0x0a, 0x11, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, - 0x74, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x09, 0x73, 0x74, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, - 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3b, 0x0a, 0x12, - 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x5f, 0x73, 0x74, - 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x63, 0x6c, 0x65, 0x61, - 0x6e, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x45, 0x0a, 0x12, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, - 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, - 0x22, 0x3c, 0x0a, 0x13, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, - 0x0d, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x3b, - 0x0a, 0x1f, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, - 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x22, 0x0a, 0x20, 0x53, - 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, - 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2a, - 0x62, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, - 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x41, 0x4e, 0x49, - 0x43, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x09, - 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, - 0x4e, 0x10, 0x04, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x05, 0x12, 0x09, 0x0a, - 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x06, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, - 0x45, 0x10, 0x07, 0x32, 0x93, 0x09, 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x14, - 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, - 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, - 0x0c, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1b, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, - 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02, 0x55, 0x70, - 0x12, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, - 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1b, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x0e, 0x53, 0x65, - 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, - 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x53, 0x0a, - 0x10, 0x44, 0x65, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, - 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, - 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, - 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, - 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, - 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, - 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, - 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, + 0x22, 0x52, 0x0a, 0x0a, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, + 0x0a, 0x03, 0x55, 0x52, 0x49, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x49, + 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x14, + 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x22, 0x72, 0x0a, 0x0c, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, + 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, + 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0xff, 0x02, 0x0a, 0x0a, 0x46, 0x75, 0x6c, + 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x41, 0x0a, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x17, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, 0x0b, 0x73, 0x69, + 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x12, 0x3e, 0x0a, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x12, 0x27, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2a, 0x0a, 0x06, 0x72, 0x65, + 0x6c, 0x61, 0x79, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, + 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x12, 0x35, 0x0a, 0x0b, 0x64, 0x6e, 0x73, 0x5f, 0x73, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x52, 0x0a, 0x64, 0x6e, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x2b, 0x0a, + 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, + 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, + 0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x15, 0x0a, 0x13, 0x4c, 0x69, + 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x22, 0x3f, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x06, 0x72, 0x6f, 0x75, + 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, + 0x65, 0x73, 0x22, 0x61, 0x0a, 0x15, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x6e, + 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x44, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x44, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x61, + 0x70, 0x70, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x61, 0x70, 0x70, + 0x65, 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x18, 0x0a, 0x16, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, + 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x1a, 0x0a, 0x06, 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x70, 0x73, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x69, 0x70, 0x73, 0x22, 0xf9, 0x01, 0x0a, 0x07, + 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x1a, 0x0a, + 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, + 0x61, 0x69, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, + 0x69, 0x6e, 0x73, 0x12, 0x42, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, + 0x50, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, + 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x72, 0x65, 0x73, 0x6f, + 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x1a, 0x4e, 0x0a, 0x10, 0x52, 0x65, 0x73, 0x6f, 0x6c, + 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, + 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x24, 0x0a, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x6a, 0x0a, 0x12, 0x44, 0x65, 0x62, 0x75, 0x67, + 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, + 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x69, 0x7a, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, + 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, + 0x6e, 0x66, 0x6f, 0x22, 0x29, 0x0a, 0x13, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, + 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, + 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x22, 0x14, + 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x22, 0x3d, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, + 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x05, 0x6c, + 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, + 0x76, 0x65, 0x6c, 0x22, 0x3c, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, + 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76, + 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, + 0x6c, 0x22, 0x15, 0x0a, 0x13, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1b, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x13, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3b, 0x0a, 0x12, 0x4c, 0x69, + 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x25, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x0d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, + 0x06, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x44, 0x0a, 0x11, 0x43, 0x6c, 0x65, 0x61, 0x6e, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, + 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x73, 0x74, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, + 0x6c, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3b, 0x0a, + 0x12, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x5f, 0x73, + 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x63, 0x6c, 0x65, + 0x61, 0x6e, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x45, 0x0a, 0x12, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, + 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, + 0x6c, 0x22, 0x3c, 0x0a, 0x13, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x0d, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, + 0x3b, 0x0a, 0x1f, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, + 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x22, 0x0a, 0x20, + 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, + 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x76, 0x0a, 0x08, 0x54, 0x43, 0x50, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x10, 0x0a, 0x03, + 0x73, 0x79, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x73, 0x79, 0x6e, 0x12, 0x10, + 0x0a, 0x03, 0x61, 0x63, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x63, 0x6b, + 0x12, 0x10, 0x0a, 0x03, 0x66, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x66, + 0x69, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x73, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x03, 0x72, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x70, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x03, 0x70, 0x73, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x67, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x03, 0x75, 0x72, 0x67, 0x22, 0x80, 0x03, 0x0a, 0x12, 0x54, 0x72, 0x61, + 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x1b, 0x0a, 0x09, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x70, 0x12, 0x25, 0x0a, 0x0e, + 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x70, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x49, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, + 0x1f, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, 0x6f, 0x72, 0x74, + 0x12, 0x29, 0x0a, 0x10, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, + 0x70, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, + 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, + 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, + 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x32, 0x0a, 0x09, 0x74, 0x63, 0x70, + 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x43, 0x50, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x48, 0x00, + 0x52, 0x08, 0x74, 0x63, 0x70, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, + 0x09, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, + 0x48, 0x01, 0x52, 0x08, 0x69, 0x63, 0x6d, 0x70, 0x54, 0x79, 0x70, 0x65, 0x88, 0x01, 0x01, 0x12, + 0x20, 0x0a, 0x09, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x09, 0x20, 0x01, + 0x28, 0x0d, 0x48, 0x02, 0x52, 0x08, 0x69, 0x63, 0x6d, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x88, 0x01, + 0x01, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x74, 0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x42, + 0x0c, 0x0a, 0x0a, 0x5f, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x42, 0x0c, 0x0a, + 0x0a, 0x5f, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x22, 0x9f, 0x01, 0x0a, 0x0a, + 0x54, 0x72, 0x61, 0x63, 0x65, 0x53, 0x74, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, + 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x6c, 0x6c, 0x6f, + 0x77, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77, + 0x65, 0x64, 0x12, 0x32, 0x0a, 0x12, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, + 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, + 0x52, 0x11, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x44, 0x65, 0x74, 0x61, + 0x69, 0x6c, 0x73, 0x88, 0x01, 0x01, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x66, 0x6f, 0x72, 0x77, 0x61, + 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x22, 0x6e, 0x0a, + 0x13, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, + 0x61, 0x63, 0x65, 0x53, 0x74, 0x61, 0x67, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61, 0x67, 0x65, 0x73, + 0x12, 0x2b, 0x0a, 0x11, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x66, 0x69, 0x6e, + 0x61, 0x6c, 0x44, 0x69, 0x73, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x12, 0x0a, + 0x10, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x22, 0x93, 0x04, 0x0a, 0x0b, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, + 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, + 0x64, 0x12, 0x38, 0x0a, 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, + 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, + 0x79, 0x52, 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x38, 0x0a, 0x08, 0x63, + 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, + 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, + 0x6e, 0x74, 0x2e, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x52, 0x08, 0x63, 0x61, 0x74, + 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, + 0x20, 0x0a, 0x0b, 0x75, 0x73, 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x75, 0x73, 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x06, + 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, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x3d, 0x0a, 0x08, 0x6d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, + 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, + 0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, + 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x3a, 0x0a, 0x08, 0x53, 0x65, 0x76, 0x65, 0x72, + 0x69, 0x74, 0x79, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x00, 0x12, 0x0b, 0x0a, + 0x07, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, + 0x52, 0x4f, 0x52, 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x52, 0x49, 0x54, 0x49, 0x43, 0x41, + 0x4c, 0x10, 0x03, 0x22, 0x52, 0x0a, 0x08, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, + 0x0b, 0x0a, 0x07, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, + 0x44, 0x4e, 0x53, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, + 0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x43, 0x4f, 0x4e, + 0x4e, 0x45, 0x43, 0x54, 0x49, 0x56, 0x49, 0x54, 0x59, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x53, + 0x59, 0x53, 0x54, 0x45, 0x4d, 0x10, 0x04, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x45, 0x76, + 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x40, 0x0a, 0x11, 0x47, + 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x2b, 0x0a, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, + 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2a, 0x62, 0x0a, + 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, + 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x41, 0x4e, 0x49, 0x43, 0x10, + 0x01, 0x12, 0x09, 0x0a, 0x05, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, + 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, + 0x04, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x05, 0x12, 0x09, 0x0a, 0x05, 0x44, + 0x45, 0x42, 0x55, 0x47, 0x10, 0x06, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, + 0x07, 0x32, 0xe7, 0x0a, 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x14, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, + 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x57, + 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1b, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, + 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02, 0x55, 0x70, 0x12, 0x11, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x12, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x12, 0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13, 0x2e, 0x64, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, + 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x4c, + 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1b, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x0e, 0x53, 0x65, 0x6c, 0x65, + 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x53, 0x0a, 0x10, 0x44, + 0x65, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, + 0x1d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, + 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, + 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x12, + 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, + 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x47, 0x65, + 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, + 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, + 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, + 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, + 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, + 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x12, 0x19, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, + 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, - 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, - 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, - 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, - 0x12, 0x45, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x12, 0x19, - 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, - 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x43, 0x6c, 0x65, 0x61, 0x6e, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, - 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, - 0x0a, 0x0b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1a, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, - 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, - 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6f, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x4e, + 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6f, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, + 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, + 0x63, 0x65, 0x12, 0x27, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, - 0x65, 0x6e, 0x63, 0x65, 0x12, 0x27, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, - 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, - 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, + 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x54, 0x72, 0x61, 0x63, 0x65, + 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, + 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, + 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x00, 0x12, 0x44, 0x0a, 0x0f, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x45, 0x76, + 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x75, + 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, + 0x65, 0x6e, 0x74, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x45, 0x76, + 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, + 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -3003,109 +3825,134 @@ func file_daemon_proto_rawDescGZIP() []byte { return file_daemon_proto_rawDescData } -var file_daemon_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 41) +var file_daemon_proto_enumTypes = make([]protoimpl.EnumInfo, 3) +var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 50) var file_daemon_proto_goTypes = []interface{}{ (LogLevel)(0), // 0: daemon.LogLevel - (*LoginRequest)(nil), // 1: daemon.LoginRequest - (*LoginResponse)(nil), // 2: daemon.LoginResponse - (*WaitSSOLoginRequest)(nil), // 3: daemon.WaitSSOLoginRequest - (*WaitSSOLoginResponse)(nil), // 4: daemon.WaitSSOLoginResponse - (*UpRequest)(nil), // 5: daemon.UpRequest - (*UpResponse)(nil), // 6: daemon.UpResponse - (*StatusRequest)(nil), // 7: daemon.StatusRequest - (*StatusResponse)(nil), // 8: daemon.StatusResponse - (*DownRequest)(nil), // 9: daemon.DownRequest - (*DownResponse)(nil), // 10: daemon.DownResponse - (*GetConfigRequest)(nil), // 11: daemon.GetConfigRequest - (*GetConfigResponse)(nil), // 12: daemon.GetConfigResponse - (*PeerState)(nil), // 13: daemon.PeerState - (*LocalPeerState)(nil), // 14: daemon.LocalPeerState - (*SignalState)(nil), // 15: daemon.SignalState - (*ManagementState)(nil), // 16: daemon.ManagementState - (*RelayState)(nil), // 17: daemon.RelayState - (*NSGroupState)(nil), // 18: daemon.NSGroupState - (*FullStatus)(nil), // 19: daemon.FullStatus - (*ListNetworksRequest)(nil), // 20: daemon.ListNetworksRequest - (*ListNetworksResponse)(nil), // 21: daemon.ListNetworksResponse - (*SelectNetworksRequest)(nil), // 22: daemon.SelectNetworksRequest - (*SelectNetworksResponse)(nil), // 23: daemon.SelectNetworksResponse - (*IPList)(nil), // 24: daemon.IPList - (*Network)(nil), // 25: daemon.Network - (*DebugBundleRequest)(nil), // 26: daemon.DebugBundleRequest - (*DebugBundleResponse)(nil), // 27: daemon.DebugBundleResponse - (*GetLogLevelRequest)(nil), // 28: daemon.GetLogLevelRequest - (*GetLogLevelResponse)(nil), // 29: daemon.GetLogLevelResponse - (*SetLogLevelRequest)(nil), // 30: daemon.SetLogLevelRequest - (*SetLogLevelResponse)(nil), // 31: daemon.SetLogLevelResponse - (*State)(nil), // 32: daemon.State - (*ListStatesRequest)(nil), // 33: daemon.ListStatesRequest - (*ListStatesResponse)(nil), // 34: daemon.ListStatesResponse - (*CleanStateRequest)(nil), // 35: daemon.CleanStateRequest - (*CleanStateResponse)(nil), // 36: daemon.CleanStateResponse - (*DeleteStateRequest)(nil), // 37: daemon.DeleteStateRequest - (*DeleteStateResponse)(nil), // 38: daemon.DeleteStateResponse - (*SetNetworkMapPersistenceRequest)(nil), // 39: daemon.SetNetworkMapPersistenceRequest - (*SetNetworkMapPersistenceResponse)(nil), // 40: daemon.SetNetworkMapPersistenceResponse - nil, // 41: daemon.Network.ResolvedIPsEntry - (*durationpb.Duration)(nil), // 42: google.protobuf.Duration - (*timestamppb.Timestamp)(nil), // 43: google.protobuf.Timestamp + (SystemEvent_Severity)(0), // 1: daemon.SystemEvent.Severity + (SystemEvent_Category)(0), // 2: daemon.SystemEvent.Category + (*LoginRequest)(nil), // 3: daemon.LoginRequest + (*LoginResponse)(nil), // 4: daemon.LoginResponse + (*WaitSSOLoginRequest)(nil), // 5: daemon.WaitSSOLoginRequest + (*WaitSSOLoginResponse)(nil), // 6: daemon.WaitSSOLoginResponse + (*UpRequest)(nil), // 7: daemon.UpRequest + (*UpResponse)(nil), // 8: daemon.UpResponse + (*StatusRequest)(nil), // 9: daemon.StatusRequest + (*StatusResponse)(nil), // 10: daemon.StatusResponse + (*DownRequest)(nil), // 11: daemon.DownRequest + (*DownResponse)(nil), // 12: daemon.DownResponse + (*GetConfigRequest)(nil), // 13: daemon.GetConfigRequest + (*GetConfigResponse)(nil), // 14: daemon.GetConfigResponse + (*PeerState)(nil), // 15: daemon.PeerState + (*LocalPeerState)(nil), // 16: daemon.LocalPeerState + (*SignalState)(nil), // 17: daemon.SignalState + (*ManagementState)(nil), // 18: daemon.ManagementState + (*RelayState)(nil), // 19: daemon.RelayState + (*NSGroupState)(nil), // 20: daemon.NSGroupState + (*FullStatus)(nil), // 21: daemon.FullStatus + (*ListNetworksRequest)(nil), // 22: daemon.ListNetworksRequest + (*ListNetworksResponse)(nil), // 23: daemon.ListNetworksResponse + (*SelectNetworksRequest)(nil), // 24: daemon.SelectNetworksRequest + (*SelectNetworksResponse)(nil), // 25: daemon.SelectNetworksResponse + (*IPList)(nil), // 26: daemon.IPList + (*Network)(nil), // 27: daemon.Network + (*DebugBundleRequest)(nil), // 28: daemon.DebugBundleRequest + (*DebugBundleResponse)(nil), // 29: daemon.DebugBundleResponse + (*GetLogLevelRequest)(nil), // 30: daemon.GetLogLevelRequest + (*GetLogLevelResponse)(nil), // 31: daemon.GetLogLevelResponse + (*SetLogLevelRequest)(nil), // 32: daemon.SetLogLevelRequest + (*SetLogLevelResponse)(nil), // 33: daemon.SetLogLevelResponse + (*State)(nil), // 34: daemon.State + (*ListStatesRequest)(nil), // 35: daemon.ListStatesRequest + (*ListStatesResponse)(nil), // 36: daemon.ListStatesResponse + (*CleanStateRequest)(nil), // 37: daemon.CleanStateRequest + (*CleanStateResponse)(nil), // 38: daemon.CleanStateResponse + (*DeleteStateRequest)(nil), // 39: daemon.DeleteStateRequest + (*DeleteStateResponse)(nil), // 40: daemon.DeleteStateResponse + (*SetNetworkMapPersistenceRequest)(nil), // 41: daemon.SetNetworkMapPersistenceRequest + (*SetNetworkMapPersistenceResponse)(nil), // 42: daemon.SetNetworkMapPersistenceResponse + (*TCPFlags)(nil), // 43: daemon.TCPFlags + (*TracePacketRequest)(nil), // 44: daemon.TracePacketRequest + (*TraceStage)(nil), // 45: daemon.TraceStage + (*TracePacketResponse)(nil), // 46: daemon.TracePacketResponse + (*SubscribeRequest)(nil), // 47: daemon.SubscribeRequest + (*SystemEvent)(nil), // 48: daemon.SystemEvent + (*GetEventsRequest)(nil), // 49: daemon.GetEventsRequest + (*GetEventsResponse)(nil), // 50: daemon.GetEventsResponse + nil, // 51: daemon.Network.ResolvedIPsEntry + nil, // 52: daemon.SystemEvent.MetadataEntry + (*durationpb.Duration)(nil), // 53: google.protobuf.Duration + (*timestamppb.Timestamp)(nil), // 54: google.protobuf.Timestamp } var file_daemon_proto_depIdxs = []int32{ - 42, // 0: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration - 19, // 1: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus - 43, // 2: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp - 43, // 3: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp - 42, // 4: daemon.PeerState.latency:type_name -> google.protobuf.Duration - 16, // 5: daemon.FullStatus.managementState:type_name -> daemon.ManagementState - 15, // 6: daemon.FullStatus.signalState:type_name -> daemon.SignalState - 14, // 7: daemon.FullStatus.localPeerState:type_name -> daemon.LocalPeerState - 13, // 8: daemon.FullStatus.peers:type_name -> daemon.PeerState - 17, // 9: daemon.FullStatus.relays:type_name -> daemon.RelayState - 18, // 10: daemon.FullStatus.dns_servers:type_name -> daemon.NSGroupState - 25, // 11: daemon.ListNetworksResponse.routes:type_name -> daemon.Network - 41, // 12: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry - 0, // 13: daemon.GetLogLevelResponse.level:type_name -> daemon.LogLevel - 0, // 14: daemon.SetLogLevelRequest.level:type_name -> daemon.LogLevel - 32, // 15: daemon.ListStatesResponse.states:type_name -> daemon.State - 24, // 16: daemon.Network.ResolvedIPsEntry.value:type_name -> daemon.IPList - 1, // 17: daemon.DaemonService.Login:input_type -> daemon.LoginRequest - 3, // 18: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest - 5, // 19: daemon.DaemonService.Up:input_type -> daemon.UpRequest - 7, // 20: daemon.DaemonService.Status:input_type -> daemon.StatusRequest - 9, // 21: daemon.DaemonService.Down:input_type -> daemon.DownRequest - 11, // 22: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest - 20, // 23: daemon.DaemonService.ListNetworks:input_type -> daemon.ListNetworksRequest - 22, // 24: daemon.DaemonService.SelectNetworks:input_type -> daemon.SelectNetworksRequest - 22, // 25: daemon.DaemonService.DeselectNetworks:input_type -> daemon.SelectNetworksRequest - 26, // 26: daemon.DaemonService.DebugBundle:input_type -> daemon.DebugBundleRequest - 28, // 27: daemon.DaemonService.GetLogLevel:input_type -> daemon.GetLogLevelRequest - 30, // 28: daemon.DaemonService.SetLogLevel:input_type -> daemon.SetLogLevelRequest - 33, // 29: daemon.DaemonService.ListStates:input_type -> daemon.ListStatesRequest - 35, // 30: daemon.DaemonService.CleanState:input_type -> daemon.CleanStateRequest - 37, // 31: daemon.DaemonService.DeleteState:input_type -> daemon.DeleteStateRequest - 39, // 32: daemon.DaemonService.SetNetworkMapPersistence:input_type -> daemon.SetNetworkMapPersistenceRequest - 2, // 33: daemon.DaemonService.Login:output_type -> daemon.LoginResponse - 4, // 34: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse - 6, // 35: daemon.DaemonService.Up:output_type -> daemon.UpResponse - 8, // 36: daemon.DaemonService.Status:output_type -> daemon.StatusResponse - 10, // 37: daemon.DaemonService.Down:output_type -> daemon.DownResponse - 12, // 38: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse - 21, // 39: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse - 23, // 40: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse - 23, // 41: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse - 27, // 42: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse - 29, // 43: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse - 31, // 44: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse - 34, // 45: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse - 36, // 46: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse - 38, // 47: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse - 40, // 48: daemon.DaemonService.SetNetworkMapPersistence:output_type -> daemon.SetNetworkMapPersistenceResponse - 33, // [33:49] is the sub-list for method output_type - 17, // [17:33] is the sub-list for method input_type - 17, // [17:17] is the sub-list for extension type_name - 17, // [17:17] is the sub-list for extension extendee - 0, // [0:17] is the sub-list for field type_name + 53, // 0: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration + 21, // 1: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus + 54, // 2: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp + 54, // 3: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp + 53, // 4: daemon.PeerState.latency:type_name -> google.protobuf.Duration + 18, // 5: daemon.FullStatus.managementState:type_name -> daemon.ManagementState + 17, // 6: daemon.FullStatus.signalState:type_name -> daemon.SignalState + 16, // 7: daemon.FullStatus.localPeerState:type_name -> daemon.LocalPeerState + 15, // 8: daemon.FullStatus.peers:type_name -> daemon.PeerState + 19, // 9: daemon.FullStatus.relays:type_name -> daemon.RelayState + 20, // 10: daemon.FullStatus.dns_servers:type_name -> daemon.NSGroupState + 48, // 11: daemon.FullStatus.events:type_name -> daemon.SystemEvent + 27, // 12: daemon.ListNetworksResponse.routes:type_name -> daemon.Network + 51, // 13: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry + 0, // 14: daemon.GetLogLevelResponse.level:type_name -> daemon.LogLevel + 0, // 15: daemon.SetLogLevelRequest.level:type_name -> daemon.LogLevel + 34, // 16: daemon.ListStatesResponse.states:type_name -> daemon.State + 43, // 17: daemon.TracePacketRequest.tcp_flags:type_name -> daemon.TCPFlags + 45, // 18: daemon.TracePacketResponse.stages:type_name -> daemon.TraceStage + 1, // 19: daemon.SystemEvent.severity:type_name -> daemon.SystemEvent.Severity + 2, // 20: daemon.SystemEvent.category:type_name -> daemon.SystemEvent.Category + 54, // 21: daemon.SystemEvent.timestamp:type_name -> google.protobuf.Timestamp + 52, // 22: daemon.SystemEvent.metadata:type_name -> daemon.SystemEvent.MetadataEntry + 48, // 23: daemon.GetEventsResponse.events:type_name -> daemon.SystemEvent + 26, // 24: daemon.Network.ResolvedIPsEntry.value:type_name -> daemon.IPList + 3, // 25: daemon.DaemonService.Login:input_type -> daemon.LoginRequest + 5, // 26: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest + 7, // 27: daemon.DaemonService.Up:input_type -> daemon.UpRequest + 9, // 28: daemon.DaemonService.Status:input_type -> daemon.StatusRequest + 11, // 29: daemon.DaemonService.Down:input_type -> daemon.DownRequest + 13, // 30: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest + 22, // 31: daemon.DaemonService.ListNetworks:input_type -> daemon.ListNetworksRequest + 24, // 32: daemon.DaemonService.SelectNetworks:input_type -> daemon.SelectNetworksRequest + 24, // 33: daemon.DaemonService.DeselectNetworks:input_type -> daemon.SelectNetworksRequest + 28, // 34: daemon.DaemonService.DebugBundle:input_type -> daemon.DebugBundleRequest + 30, // 35: daemon.DaemonService.GetLogLevel:input_type -> daemon.GetLogLevelRequest + 32, // 36: daemon.DaemonService.SetLogLevel:input_type -> daemon.SetLogLevelRequest + 35, // 37: daemon.DaemonService.ListStates:input_type -> daemon.ListStatesRequest + 37, // 38: daemon.DaemonService.CleanState:input_type -> daemon.CleanStateRequest + 39, // 39: daemon.DaemonService.DeleteState:input_type -> daemon.DeleteStateRequest + 41, // 40: daemon.DaemonService.SetNetworkMapPersistence:input_type -> daemon.SetNetworkMapPersistenceRequest + 44, // 41: daemon.DaemonService.TracePacket:input_type -> daemon.TracePacketRequest + 47, // 42: daemon.DaemonService.SubscribeEvents:input_type -> daemon.SubscribeRequest + 49, // 43: daemon.DaemonService.GetEvents:input_type -> daemon.GetEventsRequest + 4, // 44: daemon.DaemonService.Login:output_type -> daemon.LoginResponse + 6, // 45: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse + 8, // 46: daemon.DaemonService.Up:output_type -> daemon.UpResponse + 10, // 47: daemon.DaemonService.Status:output_type -> daemon.StatusResponse + 12, // 48: daemon.DaemonService.Down:output_type -> daemon.DownResponse + 14, // 49: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse + 23, // 50: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse + 25, // 51: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse + 25, // 52: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse + 29, // 53: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse + 31, // 54: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse + 33, // 55: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse + 36, // 56: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse + 38, // 57: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse + 40, // 58: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse + 42, // 59: daemon.DaemonService.SetNetworkMapPersistence:output_type -> daemon.SetNetworkMapPersistenceResponse + 46, // 60: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse + 48, // 61: daemon.DaemonService.SubscribeEvents:output_type -> daemon.SystemEvent + 50, // 62: daemon.DaemonService.GetEvents:output_type -> daemon.GetEventsResponse + 44, // [44:63] is the sub-list for method output_type + 25, // [25:44] is the sub-list for method input_type + 25, // [25:25] is the sub-list for extension type_name + 25, // [25:25] is the sub-list for extension extendee + 0, // [0:25] is the sub-list for field type_name } func init() { file_daemon_proto_init() } @@ -3594,15 +4441,113 @@ func file_daemon_proto_init() { return nil } } + file_daemon_proto_msgTypes[40].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TCPFlags); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_daemon_proto_msgTypes[41].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TracePacketRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_daemon_proto_msgTypes[42].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TraceStage); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_daemon_proto_msgTypes[43].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TracePacketResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_daemon_proto_msgTypes[44].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SubscribeRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_daemon_proto_msgTypes[45].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SystemEvent); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_daemon_proto_msgTypes[46].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetEventsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_daemon_proto_msgTypes[47].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetEventsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_daemon_proto_msgTypes[0].OneofWrappers = []interface{}{} + file_daemon_proto_msgTypes[41].OneofWrappers = []interface{}{} + file_daemon_proto_msgTypes[42].OneofWrappers = []interface{}{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_daemon_proto_rawDesc, - NumEnums: 1, - NumMessages: 41, + NumEnums: 3, + NumMessages: 50, NumExtensions: 0, NumServices: 1, }, diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index ad3a4bc1a..b1a6a6614 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -57,11 +57,17 @@ service DaemonService { // SetNetworkMapPersistence enables or disables network map persistence rpc SetNetworkMapPersistence(SetNetworkMapPersistenceRequest) returns (SetNetworkMapPersistenceResponse) {} + + rpc TracePacket(TracePacketRequest) returns (TracePacketResponse) {} + + rpc SubscribeEvents(SubscribeRequest) returns (stream SystemEvent) {} + + rpc GetEvents(GetEventsRequest) returns (GetEventsResponse) {} } message LoginRequest { - // setupKey wiretrustee setup key. + // setupKey netbird setup key. string setupKey = 1; // This is the old PreSharedKey field which will be deprecated in favor of optionalPreSharedKey field that is defined as optional @@ -112,6 +118,18 @@ message LoginRequest { optional bool disable_server_routes = 21; optional bool disable_dns = 22; optional bool disable_firewall = 23; + + optional bool block_lan_access = 24; + + optional bool disable_notifications = 25; + + repeated string dns_labels = 26; + + // cleanDNSLabels clean map list of DNS labels. + // This is needed because the generated code + // omits initialized empty slices due to omitempty tags + bool cleanDNSLabels = 27; + } message LoginResponse { @@ -177,6 +195,8 @@ message GetConfigResponse { bool rosenpassEnabled = 11; bool rosenpassPermissive = 12; + + bool disable_notifications = 13; } // PeerState contains the latest state of a peer @@ -247,6 +267,8 @@ message FullStatus { repeated PeerState peers = 4; repeated RelayState relays = 5; repeated NSGroupState dns_servers = 6; + + repeated SystemEvent events = 7; } message ListNetworksRequest { @@ -354,3 +376,69 @@ message SetNetworkMapPersistenceRequest { } message SetNetworkMapPersistenceResponse {} + +message TCPFlags { + bool syn = 1; + bool ack = 2; + bool fin = 3; + bool rst = 4; + bool psh = 5; + bool urg = 6; +} + +message TracePacketRequest { + string source_ip = 1; + string destination_ip = 2; + string protocol = 3; + uint32 source_port = 4; + uint32 destination_port = 5; + string direction = 6; + optional TCPFlags tcp_flags = 7; + optional uint32 icmp_type = 8; + optional uint32 icmp_code = 9; +} + +message TraceStage { + string name = 1; + string message = 2; + bool allowed = 3; + optional string forwarding_details = 4; +} + +message TracePacketResponse { + repeated TraceStage stages = 1; + bool final_disposition = 2; +} + +message SubscribeRequest{} + +message SystemEvent { + enum Severity { + INFO = 0; + WARNING = 1; + ERROR = 2; + CRITICAL = 3; + } + + enum Category { + NETWORK = 0; + DNS = 1; + AUTHENTICATION = 2; + CONNECTIVITY = 3; + SYSTEM = 4; + } + + string id = 1; + Severity severity = 2; + Category category = 3; + string message = 4; + string userMessage = 5; + google.protobuf.Timestamp timestamp = 6; + map metadata = 7; +} + +message GetEventsRequest {} + +message GetEventsResponse { + repeated SystemEvent events = 1; +} diff --git a/client/proto/daemon_grpc.pb.go b/client/proto/daemon_grpc.pb.go index 39424aee9..0cb2a7c59 100644 --- a/client/proto/daemon_grpc.pb.go +++ b/client/proto/daemon_grpc.pb.go @@ -51,6 +51,9 @@ type DaemonServiceClient interface { DeleteState(ctx context.Context, in *DeleteStateRequest, opts ...grpc.CallOption) (*DeleteStateResponse, error) // SetNetworkMapPersistence enables or disables network map persistence SetNetworkMapPersistence(ctx context.Context, in *SetNetworkMapPersistenceRequest, opts ...grpc.CallOption) (*SetNetworkMapPersistenceResponse, error) + TracePacket(ctx context.Context, in *TracePacketRequest, opts ...grpc.CallOption) (*TracePacketResponse, error) + SubscribeEvents(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (DaemonService_SubscribeEventsClient, error) + GetEvents(ctx context.Context, in *GetEventsRequest, opts ...grpc.CallOption) (*GetEventsResponse, error) } type daemonServiceClient struct { @@ -205,6 +208,56 @@ func (c *daemonServiceClient) SetNetworkMapPersistence(ctx context.Context, in * return out, nil } +func (c *daemonServiceClient) TracePacket(ctx context.Context, in *TracePacketRequest, opts ...grpc.CallOption) (*TracePacketResponse, error) { + out := new(TracePacketResponse) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/TracePacket", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *daemonServiceClient) SubscribeEvents(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (DaemonService_SubscribeEventsClient, error) { + stream, err := c.cc.NewStream(ctx, &DaemonService_ServiceDesc.Streams[0], "/daemon.DaemonService/SubscribeEvents", opts...) + if err != nil { + return nil, err + } + x := &daemonServiceSubscribeEventsClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type DaemonService_SubscribeEventsClient interface { + Recv() (*SystemEvent, error) + grpc.ClientStream +} + +type daemonServiceSubscribeEventsClient struct { + grpc.ClientStream +} + +func (x *daemonServiceSubscribeEventsClient) Recv() (*SystemEvent, error) { + m := new(SystemEvent) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *daemonServiceClient) GetEvents(ctx context.Context, in *GetEventsRequest, opts ...grpc.CallOption) (*GetEventsResponse, error) { + out := new(GetEventsResponse) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/GetEvents", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // DaemonServiceServer is the server API for DaemonService service. // All implementations must embed UnimplementedDaemonServiceServer // for forward compatibility @@ -242,6 +295,9 @@ type DaemonServiceServer interface { DeleteState(context.Context, *DeleteStateRequest) (*DeleteStateResponse, error) // SetNetworkMapPersistence enables or disables network map persistence SetNetworkMapPersistence(context.Context, *SetNetworkMapPersistenceRequest) (*SetNetworkMapPersistenceResponse, error) + TracePacket(context.Context, *TracePacketRequest) (*TracePacketResponse, error) + SubscribeEvents(*SubscribeRequest, DaemonService_SubscribeEventsServer) error + GetEvents(context.Context, *GetEventsRequest) (*GetEventsResponse, error) mustEmbedUnimplementedDaemonServiceServer() } @@ -297,6 +353,15 @@ func (UnimplementedDaemonServiceServer) DeleteState(context.Context, *DeleteStat func (UnimplementedDaemonServiceServer) SetNetworkMapPersistence(context.Context, *SetNetworkMapPersistenceRequest) (*SetNetworkMapPersistenceResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method SetNetworkMapPersistence not implemented") } +func (UnimplementedDaemonServiceServer) TracePacket(context.Context, *TracePacketRequest) (*TracePacketResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method TracePacket not implemented") +} +func (UnimplementedDaemonServiceServer) SubscribeEvents(*SubscribeRequest, DaemonService_SubscribeEventsServer) error { + return status.Errorf(codes.Unimplemented, "method SubscribeEvents not implemented") +} +func (UnimplementedDaemonServiceServer) GetEvents(context.Context, *GetEventsRequest) (*GetEventsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetEvents not implemented") +} func (UnimplementedDaemonServiceServer) mustEmbedUnimplementedDaemonServiceServer() {} // UnsafeDaemonServiceServer may be embedded to opt out of forward compatibility for this service. @@ -598,6 +663,63 @@ func _DaemonService_SetNetworkMapPersistence_Handler(srv interface{}, ctx contex return interceptor(ctx, in, info, handler) } +func _DaemonService_TracePacket_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TracePacketRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServiceServer).TracePacket(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/daemon.DaemonService/TracePacket", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServiceServer).TracePacket(ctx, req.(*TracePacketRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DaemonService_SubscribeEvents_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(SubscribeRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(DaemonServiceServer).SubscribeEvents(m, &daemonServiceSubscribeEventsServer{stream}) +} + +type DaemonService_SubscribeEventsServer interface { + Send(*SystemEvent) error + grpc.ServerStream +} + +type daemonServiceSubscribeEventsServer struct { + grpc.ServerStream +} + +func (x *daemonServiceSubscribeEventsServer) Send(m *SystemEvent) error { + return x.ServerStream.SendMsg(m) +} + +func _DaemonService_GetEvents_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetEventsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServiceServer).GetEvents(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/daemon.DaemonService/GetEvents", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServiceServer).GetEvents(ctx, req.(*GetEventsRequest)) + } + return interceptor(ctx, in, info, handler) +} + // DaemonService_ServiceDesc is the grpc.ServiceDesc for DaemonService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -669,7 +791,21 @@ var DaemonService_ServiceDesc = grpc.ServiceDesc{ MethodName: "SetNetworkMapPersistence", Handler: _DaemonService_SetNetworkMapPersistence_Handler, }, + { + MethodName: "TracePacket", + Handler: _DaemonService_TracePacket_Handler, + }, + { + MethodName: "GetEvents", + Handler: _DaemonService_GetEvents_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "SubscribeEvents", + Handler: _DaemonService_SubscribeEvents_Handler, + ServerStreams: true, + }, }, - Streams: []grpc.StreamDesc{}, Metadata: "daemon.proto", } diff --git a/client/server/debug.go b/client/server/debug.go index 3c4967b4e..749220d62 100644 --- a/client/server/debug.go +++ b/client/server/debug.go @@ -16,6 +16,7 @@ import ( "net/netip" "os" "path/filepath" + "runtime" "sort" "strings" "time" @@ -132,6 +133,9 @@ const ( clientLogFile = "client.log" errorLogFile = "netbird.err" stdoutLogFile = "netbird.out" + + darwinErrorLogPath = "/var/log/netbird.out.log" + darwinStdoutLogPath = "/var/log/netbird.err.log" ) // DebugBundle creates a debug bundle and returns the location. @@ -192,6 +196,10 @@ func (s *Server) createArchive(bundlePath *os.File, req *proto.DebugBundleReques log.Errorf("Failed to add state file to debug bundle: %v", err) } + if err := s.addCorruptedStateFiles(archive); err != nil { + log.Errorf("Failed to add corrupted state files to debug bundle: %v", err) + } + if s.logFile != "console" { if err := s.addLogfile(req, anonymizer, archive); err != nil { return fmt.Errorf("add log file: %w", err) @@ -293,6 +301,13 @@ func (s *Server) addCommonConfigFields(configContent *strings.Builder) { } configContent.WriteString(fmt.Sprintf("DisableAutoConnect: %v\n", s.config.DisableAutoConnect)) configContent.WriteString(fmt.Sprintf("DNSRouteInterval: %s\n", s.config.DNSRouteInterval)) + + configContent.WriteString(fmt.Sprintf("DisableClientRoutes: %v\n", s.config.DisableClientRoutes)) + configContent.WriteString(fmt.Sprintf("DisableServerRoutes: %v\n", s.config.DisableServerRoutes)) + configContent.WriteString(fmt.Sprintf("DisableDNS: %v\n", s.config.DisableDNS)) + configContent.WriteString(fmt.Sprintf("DisableFirewall: %v\n", s.config.DisableFirewall)) + + configContent.WriteString(fmt.Sprintf("BlockLANAccess: %v\n", s.config.BlockLANAccess)) } func (s *Server) addRoutes(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) error { @@ -396,6 +411,36 @@ func (s *Server) addStateFile(req *proto.DebugBundleRequest, anonymizer *anonymi return nil } +func (s *Server) addCorruptedStateFiles(archive *zip.Writer) error { + pattern := statemanager.GetDefaultStatePath() + if pattern == "" { + return nil + } + pattern += "*.corrupted.*" + matches, err := filepath.Glob(pattern) + if err != nil { + return fmt.Errorf("find corrupted state files: %w", err) + } + + for _, match := range matches { + data, err := os.ReadFile(match) + if err != nil { + log.Warnf("Failed to read corrupted state file %s: %v", match, err) + continue + } + + fileName := filepath.Base(match) + if err := addFileToZip(archive, bytes.NewReader(data), "corrupted_states/"+fileName); err != nil { + log.Warnf("Failed to add corrupted state file %s to zip: %v", fileName, err) + continue + } + + log.Debugf("Added corrupted state file to debug bundle: %s", fileName) + } + + return nil +} + func (s *Server) addLogfile(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) error { logDir := filepath.Dir(s.logFile) @@ -403,12 +448,17 @@ func (s *Server) addLogfile(req *proto.DebugBundleRequest, anonymizer *anonymize return fmt.Errorf("add client log file to zip: %w", err) } - errLogPath := filepath.Join(logDir, errorLogFile) - if err := s.addSingleLogfile(errLogPath, errorLogFile, req, anonymizer, archive); err != nil { + stdErrLogPath := filepath.Join(logDir, errorLogFile) + stdoutLogPath := filepath.Join(logDir, stdoutLogFile) + if runtime.GOOS == "darwin" { + stdErrLogPath = darwinErrorLogPath + stdoutLogPath = darwinStdoutLogPath + } + + if err := s.addSingleLogfile(stdErrLogPath, errorLogFile, req, anonymizer, archive); err != nil { log.Warnf("Failed to add %s to zip: %v", errorLogFile, err) } - stdoutLogPath := filepath.Join(logDir, stdoutLogFile) if err := s.addSingleLogfile(stdoutLogPath, stdoutLogFile, req, anonymizer, archive); err != nil { log.Warnf("Failed to add %s to zip: %v", stdoutLogFile, err) } @@ -488,7 +538,24 @@ func (s *Server) SetLogLevel(_ context.Context, req *proto.SetLogLevelRequest) ( } log.SetLevel(level) + + if s.connectClient == nil { + return nil, fmt.Errorf("connect client not initialized") + } + engine := s.connectClient.Engine() + if engine == nil { + return nil, fmt.Errorf("engine not initialized") + } + + fwManager := engine.GetFirewallManager() + if fwManager == nil { + return nil, fmt.Errorf("firewall manager not initialized") + } + + fwManager.SetLogLevel(level) + log.Infof("Log level set to %s", level.String()) + return &proto.SetLogLevelResponse{}, nil } diff --git a/client/server/event.go b/client/server/event.go new file mode 100644 index 000000000..9a4e0fbf5 --- /dev/null +++ b/client/server/event.go @@ -0,0 +1,36 @@ +package server + +import ( + "context" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/proto" +) + +func (s *Server) SubscribeEvents(req *proto.SubscribeRequest, stream proto.DaemonService_SubscribeEventsServer) error { + subscription := s.statusRecorder.SubscribeToEvents() + defer func() { + s.statusRecorder.UnsubscribeFromEvents(subscription) + log.Debug("client unsubscribed from events") + }() + + log.Debug("client subscribed to events") + + for { + select { + case event := <-subscription.Events(): + if err := stream.Send(event); err != nil { + log.Warnf("error sending event to %v: %v", req, err) + return err + } + case <-stream.Context().Done(): + return nil + } + } +} + +func (s *Server) GetEvents(context.Context, *proto.GetEventsRequest) (*proto.GetEventsResponse, error) { + events := s.statusRecorder.GetEventHistory() + return &proto.GetEventsResponse{Events: events}, nil +} diff --git a/client/server/network.go b/client/server/network.go index aaf361524..d310f4da1 100644 --- a/client/server/network.go +++ b/client/server/network.go @@ -6,6 +6,7 @@ import ( "net/netip" "slices" "sort" + "strings" "golang.org/x/exp/maps" @@ -134,6 +135,18 @@ func (s *Server) SelectNetworks(_ context.Context, req *proto.SelectNetworksRequ } routeManager.TriggerSelection(routeManager.GetClientRoutes()) + s.statusRecorder.PublishEvent( + proto.SystemEvent_INFO, + proto.SystemEvent_SYSTEM, + "Network selection changed", + "", + map[string]string{ + "networks": strings.Join(req.GetNetworkIDs(), ", "), + "append": fmt.Sprint(req.GetAppend()), + "all": fmt.Sprint(req.GetAll()), + }, + ) + return &proto.SelectNetworksResponse{}, nil } @@ -164,6 +177,18 @@ func (s *Server) DeselectNetworks(_ context.Context, req *proto.SelectNetworksRe } routeManager.TriggerSelection(routeManager.GetClientRoutes()) + s.statusRecorder.PublishEvent( + proto.SystemEvent_INFO, + proto.SystemEvent_SYSTEM, + "Network deselection changed", + "", + map[string]string{ + "networks": strings.Join(req.GetNetworkIDs(), ", "), + "append": fmt.Sprint(req.GetAppend()), + "all": fmt.Sprint(req.GetAll()), + }, + ) + return &proto.SelectNetworksResponse{}, nil } diff --git a/client/server/server.go b/client/server/server.go index 70d19bfab..348fb9872 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -22,6 +22,7 @@ import ( "github.com/netbirdio/netbird/client/internal/auth" "github.com/netbirdio/netbird/client/system" + "github.com/netbirdio/netbird/management/domain" "github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/internal/peer" @@ -63,12 +64,7 @@ type Server struct { statusRecorder *peer.Status sessionWatcher *internal.SessionWatcher - mgmProbe *internal.Probe - signalProbe *internal.Probe - relayProbe *internal.Probe - wgProbe *internal.Probe - lastProbe time.Time - + lastProbe time.Time persistNetworkMap bool } @@ -86,12 +82,7 @@ func New(ctx context.Context, configPath, logFile string) *Server { latestConfigInput: internal.ConfigInput{ ConfigPath: configPath, }, - logFile: logFile, - mgmProbe: internal.NewProbe(), - signalProbe: internal.NewProbe(), - relayProbe: internal.NewProbe(), - wgProbe: internal.NewProbe(), - + logFile: logFile, persistNetworkMap: true, } } @@ -202,14 +193,7 @@ func (s *Server) connectWithRetryRuns(ctx context.Context, config *internal.Conf s.connectClient = internal.NewConnectClient(ctx, config, statusRecorder) s.connectClient.SetNetworkMapPersistence(s.persistNetworkMap) - probes := internal.ProbeHolder{ - MgmProbe: s.mgmProbe, - SignalProbe: s.signalProbe, - RelayProbe: s.relayProbe, - WgProbe: s.wgProbe, - } - - err := s.connectClient.RunWithProbes(&probes, runningChan) + err := s.connectClient.Run(runningChan) if err != nil { log.Debugf("run client connection exited with error: %v. Will retry in the background", err) } @@ -416,6 +400,25 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro s.latestConfigInput.DisableFirewall = msg.DisableFirewall } + if msg.BlockLanAccess != nil { + inputConfig.BlockLANAccess = msg.BlockLanAccess + s.latestConfigInput.BlockLANAccess = msg.BlockLanAccess + } + + if msg.CleanDNSLabels { + inputConfig.DNSLabels = domain.List{} + s.latestConfigInput.DNSLabels = nil + } else if msg.DnsLabels != nil { + dnsLabels := domain.FromPunycodeList(msg.DnsLabels) + inputConfig.DNSLabels = dnsLabels + s.latestConfigInput.DNSLabels = dnsLabels + } + + if msg.DisableNotifications != nil { + inputConfig.DisableNotifications = msg.DisableNotifications + s.latestConfigInput.DisableNotifications = msg.DisableNotifications + } + s.mutex.Unlock() if msg.OptionalPreSharedKey != nil { @@ -671,9 +674,13 @@ func (s *Server) Down(ctx context.Context, _ *proto.DownRequest) (*proto.DownRes // Status returns the daemon status func (s *Server) Status( - _ context.Context, + ctx context.Context, msg *proto.StatusRequest, ) (*proto.StatusResponse, error) { + if ctx.Err() != nil { + return nil, ctx.Err() + } + s.mutex.Lock() defer s.mutex.Unlock() @@ -695,6 +702,7 @@ func (s *Server) Status( fullStatus := s.statusRecorder.GetFullStatus() pbFullStatus := toProtoFullStatus(fullStatus) + pbFullStatus.Events = s.statusRecorder.GetEventHistory() statusResponse.FullStatus = pbFullStatus } @@ -702,14 +710,17 @@ func (s *Server) Status( } func (s *Server) runProbes() { - if time.Since(s.lastProbe) > probeThreshold { - managementHealthy := s.mgmProbe.Probe() - signalHealthy := s.signalProbe.Probe() - relayHealthy := s.relayProbe.Probe() - wgProbe := s.wgProbe.Probe() + if s.connectClient == nil { + return + } - // Update last time only if all probes were successful - if managementHealthy && signalHealthy && relayHealthy && wgProbe { + engine := s.connectClient.Engine() + if engine == nil { + return + } + + if time.Since(s.lastProbe) > probeThreshold { + if engine.RunHealthProbes() { s.lastProbe = time.Now() } } @@ -740,24 +751,31 @@ func (s *Server) GetConfig(_ context.Context, _ *proto.GetConfigRequest) (*proto } + disableNotifications := true + if s.config.DisableNotifications != nil { + disableNotifications = *s.config.DisableNotifications + } + return &proto.GetConfigResponse{ - ManagementUrl: managementURL, - ConfigFile: s.latestConfigInput.ConfigPath, - LogFile: s.logFile, - PreSharedKey: preSharedKey, - AdminURL: adminURL, - InterfaceName: s.config.WgIface, - WireguardPort: int64(s.config.WgPort), - DisableAutoConnect: s.config.DisableAutoConnect, - ServerSSHAllowed: *s.config.ServerSSHAllowed, - RosenpassEnabled: s.config.RosenpassEnabled, - RosenpassPermissive: s.config.RosenpassPermissive, + ManagementUrl: managementURL, + ConfigFile: s.latestConfigInput.ConfigPath, + LogFile: s.logFile, + PreSharedKey: preSharedKey, + AdminURL: adminURL, + InterfaceName: s.config.WgIface, + WireguardPort: int64(s.config.WgPort), + DisableAutoConnect: s.config.DisableAutoConnect, + ServerSSHAllowed: *s.config.ServerSSHAllowed, + RosenpassEnabled: s.config.RosenpassEnabled, + RosenpassPermissive: s.config.RosenpassPermissive, + DisableNotifications: disableNotifications, }, nil } + func (s *Server) onSessionExpire() { if runtime.GOOS != "windows" { isUIActive := internal.CheckUIApp() - if !isUIActive { + if !isUIActive && s.config.DisableNotifications != nil && !*s.config.DisableNotifications { if err := sendTerminalNotification(); err != nil { log.Errorf("send session expire terminal notification: %v", err) } diff --git a/client/server/server_test.go b/client/server/server_test.go index 128de8e02..d6b651a79 100644 --- a/client/server/server_test.go +++ b/client/server/server_test.go @@ -134,7 +134,7 @@ func startManagement(t *testing.T, signalAddr string, counter *int) (*grpc.Serve } secretsManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay) - mgmtServer, err := server.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil) + mgmtServer, err := server.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil, nil) if err != nil { return nil, "", err } diff --git a/client/server/trace.go b/client/server/trace.go new file mode 100644 index 000000000..66b83d8cf --- /dev/null +++ b/client/server/trace.go @@ -0,0 +1,123 @@ +package server + +import ( + "context" + "fmt" + "net" + + fw "github.com/netbirdio/netbird/client/firewall/manager" + "github.com/netbirdio/netbird/client/firewall/uspfilter" + "github.com/netbirdio/netbird/client/proto" +) + +type packetTracer interface { + TracePacketFromBuilder(builder *uspfilter.PacketBuilder) (*uspfilter.PacketTrace, error) +} + +func (s *Server) TracePacket(_ context.Context, req *proto.TracePacketRequest) (*proto.TracePacketResponse, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + if s.connectClient == nil { + return nil, fmt.Errorf("connect client not initialized") + } + engine := s.connectClient.Engine() + if engine == nil { + return nil, fmt.Errorf("engine not initialized") + } + + fwManager := engine.GetFirewallManager() + if fwManager == nil { + return nil, fmt.Errorf("firewall manager not initialized") + } + + tracer, ok := fwManager.(packetTracer) + if !ok { + return nil, fmt.Errorf("firewall manager does not support packet tracing") + } + + srcIP := net.ParseIP(req.GetSourceIp()) + if req.GetSourceIp() == "self" { + srcIP = engine.GetWgAddr() + } + + dstIP := net.ParseIP(req.GetDestinationIp()) + if req.GetDestinationIp() == "self" { + dstIP = engine.GetWgAddr() + } + + if srcIP == nil || dstIP == nil { + return nil, fmt.Errorf("invalid IP address") + } + + var tcpState *uspfilter.TCPState + if flags := req.GetTcpFlags(); flags != nil { + tcpState = &uspfilter.TCPState{ + SYN: flags.GetSyn(), + ACK: flags.GetAck(), + FIN: flags.GetFin(), + RST: flags.GetRst(), + PSH: flags.GetPsh(), + URG: flags.GetUrg(), + } + } + + var dir fw.RuleDirection + switch req.GetDirection() { + case "in": + dir = fw.RuleDirectionIN + case "out": + dir = fw.RuleDirectionOUT + default: + return nil, fmt.Errorf("invalid direction") + } + + var protocol fw.Protocol + switch req.GetProtocol() { + case "tcp": + protocol = fw.ProtocolTCP + case "udp": + protocol = fw.ProtocolUDP + case "icmp": + protocol = fw.ProtocolICMP + default: + return nil, fmt.Errorf("invalid protocolcol") + } + + builder := &uspfilter.PacketBuilder{ + SrcIP: srcIP, + DstIP: dstIP, + Protocol: protocol, + SrcPort: uint16(req.GetSourcePort()), + DstPort: uint16(req.GetDestinationPort()), + Direction: dir, + TCPState: tcpState, + ICMPType: uint8(req.GetIcmpType()), + ICMPCode: uint8(req.GetIcmpCode()), + } + trace, err := tracer.TracePacketFromBuilder(builder) + if err != nil { + return nil, fmt.Errorf("trace packet: %w", err) + } + + resp := &proto.TracePacketResponse{} + + for _, result := range trace.Results { + stage := &proto.TraceStage{ + Name: result.Stage.String(), + Message: result.Message, + Allowed: result.Allowed, + } + if result.ForwarderAction != nil { + details := fmt.Sprintf("%s to %s", result.ForwarderAction.Action, result.ForwarderAction.RemoteAddr) + stage.ForwardingDetails = &details + } + resp.Stages = append(resp.Stages, stage) + } + + if len(trace.Results) > 0 { + resp.FinalDisposition = trace.Results[len(trace.Results)-1].Allowed + } + + return resp, nil +} diff --git a/client/ssh/login.go b/client/ssh/login.go index e6019578d..d1d56ceb0 100644 --- a/client/ssh/login.go +++ b/client/ssh/login.go @@ -2,14 +2,29 @@ package ssh import ( "fmt" - "github.com/netbirdio/netbird/util" "net" "net/netip" + "os" "os/exec" "runtime" + + "github.com/netbirdio/netbird/util" ) +func isRoot() bool { + return os.Geteuid() == 0 +} + func getLoginCmd(user string, remoteAddr net.Addr) (loginPath string, args []string, err error) { + if !isRoot() { + shell := getUserShell(user) + if shell == "" { + shell = "/bin/sh" + } + + return shell, []string{"-l"}, nil + } + loginPath, err = exec.LookPath("login") if err != nil { return "", nil, err @@ -20,17 +35,17 @@ func getLoginCmd(user string, remoteAddr net.Addr) (loginPath string, args []str return "", nil, err } - if runtime.GOOS == "linux" { - + switch runtime.GOOS { + case "linux": if util.FileExists("/etc/arch-release") && !util.FileExists("/etc/pam.d/remote") { - // detect if Arch Linux return loginPath, []string{"-f", user, "-p"}, nil } - return loginPath, []string{"-f", user, "-h", addrPort.Addr().String(), "-p"}, nil - } else if runtime.GOOS == "darwin" { + case "darwin": return loginPath, []string{"-fp", "-h", addrPort.Addr().String(), user}, nil + case "freebsd": + return loginPath, []string{"-f", user, "-h", addrPort.Addr().String(), "-p"}, nil + default: + return "", nil, fmt.Errorf("unsupported platform: %s", runtime.GOOS) } - - return "", nil, fmt.Errorf("unsupported platform") } diff --git a/client/ssh/lookup.go b/client/ssh/lookup.go index 7acef8f0b..9a7f6ff2e 100644 --- a/client/ssh/lookup.go +++ b/client/ssh/lookup.go @@ -6,5 +6,9 @@ package ssh import "os/user" func userNameLookup(username string) (*user.User, error) { + if username == "" || (username == "root" && !isRoot()) { + return user.Current() + } + return user.Lookup(username) } diff --git a/client/ssh/lookup_darwin.go b/client/ssh/lookup_darwin.go index e6f3c3b93..913d049dc 100644 --- a/client/ssh/lookup_darwin.go +++ b/client/ssh/lookup_darwin.go @@ -12,6 +12,10 @@ import ( ) func userNameLookup(username string) (*user.User, error) { + if username == "" || (username == "root" && !isRoot()) { + return user.Current() + } + var userObject *user.User userObject, err := user.Lookup(username) if err != nil && err.Error() == user.UnknownUserError(username).Error() { diff --git a/client/ssh/server.go b/client/ssh/server.go index a390302b7..1f2001d0f 100644 --- a/client/ssh/server.go +++ b/client/ssh/server.go @@ -168,8 +168,12 @@ func (srv *DefaultServer) sessionHandler(session ssh.Session) { cmd := exec.Command(loginCmd, loginArgs...) go func() { <-session.Context().Done() + if cmd.Process == nil { + return + } err := cmd.Process.Kill() if err != nil { + log.Debugf("failed killing SSH process %v", err) return } }() @@ -185,7 +189,7 @@ func (srv *DefaultServer) sessionHandler(session ssh.Session) { log.Debugf("Login command: %s", cmd.String()) file, err := pty.Start(cmd) if err != nil { - log.Errorf("failed starting SSH server %v", err) + log.Errorf("failed starting SSH server: %v", err) } go func() { diff --git a/client/status/event.go b/client/status/event.go new file mode 100644 index 000000000..2b65c9fa3 --- /dev/null +++ b/client/status/event.go @@ -0,0 +1,69 @@ +package status + +import ( + "fmt" + "sort" + "strings" + "time" + + "github.com/netbirdio/netbird/client/proto" +) + +type SystemEventOutput struct { + ID string `json:"id" yaml:"id"` + Severity string `json:"severity" yaml:"severity"` + Category string `json:"category" yaml:"category"` + Message string `json:"message" yaml:"message"` + UserMessage string `json:"userMessage" yaml:"userMessage"` + Timestamp time.Time `json:"timestamp" yaml:"timestamp"` + Metadata map[string]string `json:"metadata" yaml:"metadata"` +} + +func mapEvents(protoEvents []*proto.SystemEvent) []SystemEventOutput { + events := make([]SystemEventOutput, len(protoEvents)) + for i, event := range protoEvents { + events[i] = SystemEventOutput{ + ID: event.GetId(), + Severity: event.GetSeverity().String(), + Category: event.GetCategory().String(), + Message: event.GetMessage(), + UserMessage: event.GetUserMessage(), + Timestamp: event.GetTimestamp().AsTime(), + Metadata: event.GetMetadata(), + } + } + return events +} + +func parseEvents(events []SystemEventOutput) string { + if len(events) == 0 { + return " No events recorded" + } + + var eventsString strings.Builder + for _, event := range events { + timeStr := timeAgo(event.Timestamp) + + metadataStr := "" + if len(event.Metadata) > 0 { + pairs := make([]string, 0, len(event.Metadata)) + for k, v := range event.Metadata { + pairs = append(pairs, fmt.Sprintf("%s: %s", k, v)) + } + sort.Strings(pairs) + metadataStr = fmt.Sprintf("\n Metadata: %s", strings.Join(pairs, ", ")) + } + + eventsString.WriteString(fmt.Sprintf("\n [%s] %s (%s)"+ + "\n Message: %s"+ + "\n Time: %s%s", + event.Severity, + event.Category, + event.ID, + event.Message, + timeStr, + metadataStr, + )) + } + return eventsString.String() +} diff --git a/client/status/status.go b/client/status/status.go new file mode 100644 index 000000000..2d11ee3ba --- /dev/null +++ b/client/status/status.go @@ -0,0 +1,725 @@ +package status + +import ( + "encoding/json" + "fmt" + "net" + "net/netip" + "os" + "runtime" + "sort" + "strings" + "time" + + "gopkg.in/yaml.v3" + + "github.com/netbirdio/netbird/client/anonymize" + "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/client/proto" + "github.com/netbirdio/netbird/version" +) + +type PeerStateDetailOutput struct { + FQDN string `json:"fqdn" yaml:"fqdn"` + IP string `json:"netbirdIp" yaml:"netbirdIp"` + PubKey string `json:"publicKey" yaml:"publicKey"` + Status string `json:"status" yaml:"status"` + LastStatusUpdate time.Time `json:"lastStatusUpdate" yaml:"lastStatusUpdate"` + ConnType string `json:"connectionType" yaml:"connectionType"` + IceCandidateType IceCandidateType `json:"iceCandidateType" yaml:"iceCandidateType"` + IceCandidateEndpoint IceCandidateType `json:"iceCandidateEndpoint" yaml:"iceCandidateEndpoint"` + RelayAddress string `json:"relayAddress" yaml:"relayAddress"` + LastWireguardHandshake time.Time `json:"lastWireguardHandshake" yaml:"lastWireguardHandshake"` + TransferReceived int64 `json:"transferReceived" yaml:"transferReceived"` + TransferSent int64 `json:"transferSent" yaml:"transferSent"` + Latency time.Duration `json:"latency" yaml:"latency"` + RosenpassEnabled bool `json:"quantumResistance" yaml:"quantumResistance"` + Networks []string `json:"networks" yaml:"networks"` +} + +type PeersStateOutput struct { + Total int `json:"total" yaml:"total"` + Connected int `json:"connected" yaml:"connected"` + Details []PeerStateDetailOutput `json:"details" yaml:"details"` +} + +type SignalStateOutput struct { + URL string `json:"url" yaml:"url"` + Connected bool `json:"connected" yaml:"connected"` + Error string `json:"error" yaml:"error"` +} + +type ManagementStateOutput struct { + URL string `json:"url" yaml:"url"` + Connected bool `json:"connected" yaml:"connected"` + Error string `json:"error" yaml:"error"` +} + +type RelayStateOutputDetail struct { + URI string `json:"uri" yaml:"uri"` + Available bool `json:"available" yaml:"available"` + Error string `json:"error" yaml:"error"` +} + +type RelayStateOutput struct { + Total int `json:"total" yaml:"total"` + Available int `json:"available" yaml:"available"` + Details []RelayStateOutputDetail `json:"details" yaml:"details"` +} + +type IceCandidateType struct { + Local string `json:"local" yaml:"local"` + Remote string `json:"remote" yaml:"remote"` +} + +type NsServerGroupStateOutput struct { + Servers []string `json:"servers" yaml:"servers"` + Domains []string `json:"domains" yaml:"domains"` + Enabled bool `json:"enabled" yaml:"enabled"` + Error string `json:"error" yaml:"error"` +} + +type OutputOverview struct { + Peers PeersStateOutput `json:"peers" yaml:"peers"` + CliVersion string `json:"cliVersion" yaml:"cliVersion"` + DaemonVersion string `json:"daemonVersion" yaml:"daemonVersion"` + ManagementState ManagementStateOutput `json:"management" yaml:"management"` + SignalState SignalStateOutput `json:"signal" yaml:"signal"` + Relays RelayStateOutput `json:"relays" yaml:"relays"` + IP string `json:"netbirdIp" yaml:"netbirdIp"` + PubKey string `json:"publicKey" yaml:"publicKey"` + KernelInterface bool `json:"usesKernelInterface" yaml:"usesKernelInterface"` + FQDN string `json:"fqdn" yaml:"fqdn"` + RosenpassEnabled bool `json:"quantumResistance" yaml:"quantumResistance"` + RosenpassPermissive bool `json:"quantumResistancePermissive" yaml:"quantumResistancePermissive"` + Networks []string `json:"networks" yaml:"networks"` + NSServerGroups []NsServerGroupStateOutput `json:"dnsServers" yaml:"dnsServers"` + Events []SystemEventOutput `json:"events" yaml:"events"` +} + +func ConvertToStatusOutputOverview(resp *proto.StatusResponse, anon bool, statusFilter string, prefixNamesFilter []string, prefixNamesFilterMap map[string]struct{}, ipsFilter map[string]struct{}) OutputOverview { + pbFullStatus := resp.GetFullStatus() + + managementState := pbFullStatus.GetManagementState() + managementOverview := ManagementStateOutput{ + URL: managementState.GetURL(), + Connected: managementState.GetConnected(), + Error: managementState.Error, + } + + signalState := pbFullStatus.GetSignalState() + signalOverview := SignalStateOutput{ + URL: signalState.GetURL(), + Connected: signalState.GetConnected(), + Error: signalState.Error, + } + + relayOverview := mapRelays(pbFullStatus.GetRelays()) + peersOverview := mapPeers(resp.GetFullStatus().GetPeers(), statusFilter, prefixNamesFilter, prefixNamesFilterMap, ipsFilter) + + overview := OutputOverview{ + Peers: peersOverview, + CliVersion: version.NetbirdVersion(), + DaemonVersion: resp.GetDaemonVersion(), + ManagementState: managementOverview, + SignalState: signalOverview, + Relays: relayOverview, + IP: pbFullStatus.GetLocalPeerState().GetIP(), + PubKey: pbFullStatus.GetLocalPeerState().GetPubKey(), + KernelInterface: pbFullStatus.GetLocalPeerState().GetKernelInterface(), + FQDN: pbFullStatus.GetLocalPeerState().GetFqdn(), + RosenpassEnabled: pbFullStatus.GetLocalPeerState().GetRosenpassEnabled(), + RosenpassPermissive: pbFullStatus.GetLocalPeerState().GetRosenpassPermissive(), + Networks: pbFullStatus.GetLocalPeerState().GetNetworks(), + NSServerGroups: mapNSGroups(pbFullStatus.GetDnsServers()), + Events: mapEvents(pbFullStatus.GetEvents()), + } + + if anon { + anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses()) + anonymizeOverview(anonymizer, &overview) + } + + return overview +} + +func mapRelays(relays []*proto.RelayState) RelayStateOutput { + var relayStateDetail []RelayStateOutputDetail + + var relaysAvailable int + for _, relay := range relays { + available := relay.GetAvailable() + relayStateDetail = append(relayStateDetail, + RelayStateOutputDetail{ + URI: relay.URI, + Available: available, + Error: relay.GetError(), + }, + ) + + if available { + relaysAvailable++ + } + } + + return RelayStateOutput{ + Total: len(relays), + Available: relaysAvailable, + Details: relayStateDetail, + } +} + +func mapNSGroups(servers []*proto.NSGroupState) []NsServerGroupStateOutput { + mappedNSGroups := make([]NsServerGroupStateOutput, 0, len(servers)) + for _, pbNsGroupServer := range servers { + mappedNSGroups = append(mappedNSGroups, NsServerGroupStateOutput{ + Servers: pbNsGroupServer.GetServers(), + Domains: pbNsGroupServer.GetDomains(), + Enabled: pbNsGroupServer.GetEnabled(), + Error: pbNsGroupServer.GetError(), + }) + } + return mappedNSGroups +} + +func mapPeers( + peers []*proto.PeerState, + statusFilter string, + prefixNamesFilter []string, + prefixNamesFilterMap map[string]struct{}, + ipsFilter map[string]struct{}, +) PeersStateOutput { + var peersStateDetail []PeerStateDetailOutput + peersConnected := 0 + for _, pbPeerState := range peers { + localICE := "" + remoteICE := "" + localICEEndpoint := "" + remoteICEEndpoint := "" + relayServerAddress := "" + connType := "" + lastHandshake := time.Time{} + transferReceived := int64(0) + transferSent := int64(0) + + isPeerConnected := pbPeerState.ConnStatus == peer.StatusConnected.String() + if skipDetailByFilters(pbPeerState, isPeerConnected, statusFilter, prefixNamesFilter, prefixNamesFilterMap, ipsFilter) { + continue + } + if isPeerConnected { + peersConnected++ + + localICE = pbPeerState.GetLocalIceCandidateType() + remoteICE = pbPeerState.GetRemoteIceCandidateType() + localICEEndpoint = pbPeerState.GetLocalIceCandidateEndpoint() + remoteICEEndpoint = pbPeerState.GetRemoteIceCandidateEndpoint() + connType = "P2P" + if pbPeerState.Relayed { + connType = "Relayed" + } + relayServerAddress = pbPeerState.GetRelayAddress() + lastHandshake = pbPeerState.GetLastWireguardHandshake().AsTime().Local() + transferReceived = pbPeerState.GetBytesRx() + transferSent = pbPeerState.GetBytesTx() + } + + timeLocal := pbPeerState.GetConnStatusUpdate().AsTime().Local() + peerState := PeerStateDetailOutput{ + IP: pbPeerState.GetIP(), + PubKey: pbPeerState.GetPubKey(), + Status: pbPeerState.GetConnStatus(), + LastStatusUpdate: timeLocal, + ConnType: connType, + IceCandidateType: IceCandidateType{ + Local: localICE, + Remote: remoteICE, + }, + IceCandidateEndpoint: IceCandidateType{ + Local: localICEEndpoint, + Remote: remoteICEEndpoint, + }, + RelayAddress: relayServerAddress, + FQDN: pbPeerState.GetFqdn(), + LastWireguardHandshake: lastHandshake, + TransferReceived: transferReceived, + TransferSent: transferSent, + Latency: pbPeerState.GetLatency().AsDuration(), + RosenpassEnabled: pbPeerState.GetRosenpassEnabled(), + Networks: pbPeerState.GetNetworks(), + } + + peersStateDetail = append(peersStateDetail, peerState) + } + + sortPeersByIP(peersStateDetail) + + peersOverview := PeersStateOutput{ + Total: len(peersStateDetail), + Connected: peersConnected, + Details: peersStateDetail, + } + return peersOverview +} + +func sortPeersByIP(peersStateDetail []PeerStateDetailOutput) { + if len(peersStateDetail) > 0 { + sort.SliceStable(peersStateDetail, func(i, j int) bool { + iAddr, _ := netip.ParseAddr(peersStateDetail[i].IP) + jAddr, _ := netip.ParseAddr(peersStateDetail[j].IP) + return iAddr.Compare(jAddr) == -1 + }) + } +} + +func ParseToJSON(overview OutputOverview) (string, error) { + jsonBytes, err := json.Marshal(overview) + if err != nil { + return "", fmt.Errorf("json marshal failed") + } + return string(jsonBytes), err +} + +func ParseToYAML(overview OutputOverview) (string, error) { + yamlBytes, err := yaml.Marshal(overview) + if err != nil { + return "", fmt.Errorf("yaml marshal failed") + } + return string(yamlBytes), nil +} + +func ParseGeneralSummary(overview OutputOverview, showURL bool, showRelays bool, showNameServers bool) string { + var managementConnString string + if overview.ManagementState.Connected { + managementConnString = "Connected" + if showURL { + managementConnString = fmt.Sprintf("%s to %s", managementConnString, overview.ManagementState.URL) + } + } else { + managementConnString = "Disconnected" + if overview.ManagementState.Error != "" { + managementConnString = fmt.Sprintf("%s, reason: %s", managementConnString, overview.ManagementState.Error) + } + } + + var signalConnString string + if overview.SignalState.Connected { + signalConnString = "Connected" + if showURL { + signalConnString = fmt.Sprintf("%s to %s", signalConnString, overview.SignalState.URL) + } + } else { + signalConnString = "Disconnected" + if overview.SignalState.Error != "" { + signalConnString = fmt.Sprintf("%s, reason: %s", signalConnString, overview.SignalState.Error) + } + } + + interfaceTypeString := "Userspace" + interfaceIP := overview.IP + if overview.KernelInterface { + interfaceTypeString = "Kernel" + } else if overview.IP == "" { + interfaceTypeString = "N/A" + interfaceIP = "N/A" + } + + var relaysString string + if showRelays { + for _, relay := range overview.Relays.Details { + available := "Available" + reason := "" + if !relay.Available { + available = "Unavailable" + reason = fmt.Sprintf(", reason: %s", relay.Error) + } + relaysString += fmt.Sprintf("\n [%s] is %s%s", relay.URI, available, reason) + } + } else { + relaysString = fmt.Sprintf("%d/%d Available", overview.Relays.Available, overview.Relays.Total) + } + + networks := "-" + if len(overview.Networks) > 0 { + sort.Strings(overview.Networks) + networks = strings.Join(overview.Networks, ", ") + } + + var dnsServersString string + if showNameServers { + for _, nsServerGroup := range overview.NSServerGroups { + enabled := "Available" + if !nsServerGroup.Enabled { + enabled = "Unavailable" + } + errorString := "" + if nsServerGroup.Error != "" { + errorString = fmt.Sprintf(", reason: %s", nsServerGroup.Error) + errorString = strings.TrimSpace(errorString) + } + + domainsString := strings.Join(nsServerGroup.Domains, ", ") + if domainsString == "" { + domainsString = "." // Show "." for the default zone + } + dnsServersString += fmt.Sprintf( + "\n [%s] for [%s] is %s%s", + strings.Join(nsServerGroup.Servers, ", "), + domainsString, + enabled, + errorString, + ) + } + } else { + dnsServersString = fmt.Sprintf("%d/%d Available", countEnabled(overview.NSServerGroups), len(overview.NSServerGroups)) + } + + rosenpassEnabledStatus := "false" + if overview.RosenpassEnabled { + rosenpassEnabledStatus = "true" + if overview.RosenpassPermissive { + rosenpassEnabledStatus = "true (permissive)" //nolint:gosec + } + } + + peersCountString := fmt.Sprintf("%d/%d Connected", overview.Peers.Connected, overview.Peers.Total) + + goos := runtime.GOOS + goarch := runtime.GOARCH + goarm := "" + if goarch == "arm" { + goarm = fmt.Sprintf(" (ARMv%s)", os.Getenv("GOARM")) + } + + summary := fmt.Sprintf( + "OS: %s\n"+ + "Daemon version: %s\n"+ + "CLI version: %s\n"+ + "Management: %s\n"+ + "Signal: %s\n"+ + "Relays: %s\n"+ + "Nameservers: %s\n"+ + "FQDN: %s\n"+ + "NetBird IP: %s\n"+ + "Interface type: %s\n"+ + "Quantum resistance: %s\n"+ + "Networks: %s\n"+ + "Peers count: %s\n", + fmt.Sprintf("%s/%s%s", goos, goarch, goarm), + overview.DaemonVersion, + version.NetbirdVersion(), + managementConnString, + signalConnString, + relaysString, + dnsServersString, + overview.FQDN, + interfaceIP, + interfaceTypeString, + rosenpassEnabledStatus, + networks, + peersCountString, + ) + return summary +} + +func ParseToFullDetailSummary(overview OutputOverview) string { + parsedPeersString := parsePeers(overview.Peers, overview.RosenpassEnabled, overview.RosenpassPermissive) + parsedEventsString := parseEvents(overview.Events) + summary := ParseGeneralSummary(overview, true, true, true) + + return fmt.Sprintf( + "Peers detail:"+ + "%s\n"+ + "Events:"+ + "%s\n"+ + "%s", + parsedPeersString, + parsedEventsString, + summary, + ) +} + +func parsePeers(peers PeersStateOutput, rosenpassEnabled, rosenpassPermissive bool) string { + var ( + peersString = "" + ) + + for _, peerState := range peers.Details { + + localICE := "-" + if peerState.IceCandidateType.Local != "" { + localICE = peerState.IceCandidateType.Local + } + + remoteICE := "-" + if peerState.IceCandidateType.Remote != "" { + remoteICE = peerState.IceCandidateType.Remote + } + + localICEEndpoint := "-" + if peerState.IceCandidateEndpoint.Local != "" { + localICEEndpoint = peerState.IceCandidateEndpoint.Local + } + + remoteICEEndpoint := "-" + if peerState.IceCandidateEndpoint.Remote != "" { + remoteICEEndpoint = peerState.IceCandidateEndpoint.Remote + } + + rosenpassEnabledStatus := "false" + if rosenpassEnabled { + if peerState.RosenpassEnabled { + rosenpassEnabledStatus = "true" + } else { + if rosenpassPermissive { + rosenpassEnabledStatus = "false (remote didn't enable quantum resistance)" + } else { + rosenpassEnabledStatus = "false (connection won't work without a permissive mode)" + } + } + } else { + if peerState.RosenpassEnabled { + rosenpassEnabledStatus = "false (connection might not work without a remote permissive mode)" + } + } + + networks := "-" + if len(peerState.Networks) > 0 { + sort.Strings(peerState.Networks) + networks = strings.Join(peerState.Networks, ", ") + } + + peerString := fmt.Sprintf( + "\n %s:\n"+ + " NetBird IP: %s\n"+ + " Public key: %s\n"+ + " Status: %s\n"+ + " -- detail --\n"+ + " Connection type: %s\n"+ + " ICE candidate (Local/Remote): %s/%s\n"+ + " ICE candidate endpoints (Local/Remote): %s/%s\n"+ + " Relay server address: %s\n"+ + " Last connection update: %s\n"+ + " Last WireGuard handshake: %s\n"+ + " Transfer status (received/sent) %s/%s\n"+ + " Quantum resistance: %s\n"+ + " Networks: %s\n"+ + " Latency: %s\n", + peerState.FQDN, + peerState.IP, + peerState.PubKey, + peerState.Status, + peerState.ConnType, + localICE, + remoteICE, + localICEEndpoint, + remoteICEEndpoint, + peerState.RelayAddress, + timeAgo(peerState.LastStatusUpdate), + timeAgo(peerState.LastWireguardHandshake), + toIEC(peerState.TransferReceived), + toIEC(peerState.TransferSent), + rosenpassEnabledStatus, + networks, + peerState.Latency.String(), + ) + + peersString += peerString + } + return peersString +} + +func skipDetailByFilters( + peerState *proto.PeerState, + isConnected bool, + statusFilter string, + prefixNamesFilter []string, + prefixNamesFilterMap map[string]struct{}, + ipsFilter map[string]struct{}, +) bool { + statusEval := false + ipEval := false + nameEval := true + + if statusFilter != "" { + lowerStatusFilter := strings.ToLower(statusFilter) + if lowerStatusFilter == "disconnected" && isConnected { + statusEval = true + } else if lowerStatusFilter == "connected" && !isConnected { + statusEval = true + } + } + + if len(ipsFilter) > 0 { + _, ok := ipsFilter[peerState.IP] + if !ok { + ipEval = true + } + } + + if len(prefixNamesFilter) > 0 { + for prefixNameFilter := range prefixNamesFilterMap { + if strings.HasPrefix(peerState.Fqdn, prefixNameFilter) { + nameEval = false + break + } + } + } else { + nameEval = false + } + + return statusEval || ipEval || nameEval +} + +func toIEC(b int64) string { + const unit = 1024 + if b < unit { + return fmt.Sprintf("%d B", b) + } + div, exp := int64(unit), 0 + for n := b / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.1f %ciB", + float64(b)/float64(div), "KMGTPE"[exp]) +} + +func countEnabled(dnsServers []NsServerGroupStateOutput) int { + count := 0 + for _, server := range dnsServers { + if server.Enabled { + count++ + } + } + return count +} + +// timeAgo returns a string representing the duration since the provided time in a human-readable format. +func timeAgo(t time.Time) string { + if t.IsZero() || t.Equal(time.Unix(0, 0)) { + return "-" + } + duration := time.Since(t) + switch { + case duration < time.Second: + return "Now" + case duration < time.Minute: + seconds := int(duration.Seconds()) + if seconds == 1 { + return "1 second ago" + } + return fmt.Sprintf("%d seconds ago", seconds) + case duration < time.Hour: + minutes := int(duration.Minutes()) + seconds := int(duration.Seconds()) % 60 + if minutes == 1 { + if seconds == 1 { + return "1 minute, 1 second ago" + } else if seconds > 0 { + return fmt.Sprintf("1 minute, %d seconds ago", seconds) + } + return "1 minute ago" + } + if seconds > 0 { + return fmt.Sprintf("%d minutes, %d seconds ago", minutes, seconds) + } + return fmt.Sprintf("%d minutes ago", minutes) + case duration < 24*time.Hour: + hours := int(duration.Hours()) + minutes := int(duration.Minutes()) % 60 + if hours == 1 { + if minutes == 1 { + return "1 hour, 1 minute ago" + } else if minutes > 0 { + return fmt.Sprintf("1 hour, %d minutes ago", minutes) + } + return "1 hour ago" + } + if minutes > 0 { + return fmt.Sprintf("%d hours, %d minutes ago", hours, minutes) + } + return fmt.Sprintf("%d hours ago", hours) + } + + days := int(duration.Hours()) / 24 + hours := int(duration.Hours()) % 24 + if days == 1 { + if hours == 1 { + return "1 day, 1 hour ago" + } else if hours > 0 { + return fmt.Sprintf("1 day, %d hours ago", hours) + } + return "1 day ago" + } + if hours > 0 { + return fmt.Sprintf("%d days, %d hours ago", days, hours) + } + return fmt.Sprintf("%d days ago", days) +} + +func anonymizePeerDetail(a *anonymize.Anonymizer, peer *PeerStateDetailOutput) { + peer.FQDN = a.AnonymizeDomain(peer.FQDN) + if localIP, port, err := net.SplitHostPort(peer.IceCandidateEndpoint.Local); err == nil { + peer.IceCandidateEndpoint.Local = fmt.Sprintf("%s:%s", a.AnonymizeIPString(localIP), port) + } + if remoteIP, port, err := net.SplitHostPort(peer.IceCandidateEndpoint.Remote); err == nil { + peer.IceCandidateEndpoint.Remote = fmt.Sprintf("%s:%s", a.AnonymizeIPString(remoteIP), port) + } + + peer.RelayAddress = a.AnonymizeURI(peer.RelayAddress) + + for i, route := range peer.Networks { + peer.Networks[i] = a.AnonymizeIPString(route) + } + + for i, route := range peer.Networks { + peer.Networks[i] = a.AnonymizeRoute(route) + } +} + +func anonymizeOverview(a *anonymize.Anonymizer, overview *OutputOverview) { + for i, peer := range overview.Peers.Details { + peer := peer + anonymizePeerDetail(a, &peer) + overview.Peers.Details[i] = peer + } + + overview.ManagementState.URL = a.AnonymizeURI(overview.ManagementState.URL) + overview.ManagementState.Error = a.AnonymizeString(overview.ManagementState.Error) + overview.SignalState.URL = a.AnonymizeURI(overview.SignalState.URL) + overview.SignalState.Error = a.AnonymizeString(overview.SignalState.Error) + + overview.IP = a.AnonymizeIPString(overview.IP) + for i, detail := range overview.Relays.Details { + detail.URI = a.AnonymizeURI(detail.URI) + detail.Error = a.AnonymizeString(detail.Error) + overview.Relays.Details[i] = detail + } + + for i, nsGroup := range overview.NSServerGroups { + for j, domain := range nsGroup.Domains { + overview.NSServerGroups[i].Domains[j] = a.AnonymizeDomain(domain) + } + for j, ns := range nsGroup.Servers { + host, port, err := net.SplitHostPort(ns) + if err == nil { + overview.NSServerGroups[i].Servers[j] = fmt.Sprintf("%s:%s", a.AnonymizeIPString(host), port) + } + } + } + + for i, route := range overview.Networks { + overview.Networks[i] = a.AnonymizeRoute(route) + } + + overview.FQDN = a.AnonymizeDomain(overview.FQDN) + + for i, event := range overview.Events { + overview.Events[i].Message = a.AnonymizeString(event.Message) + overview.Events[i].UserMessage = a.AnonymizeString(event.UserMessage) + + for k, v := range event.Metadata { + event.Metadata[k] = a.AnonymizeString(v) + } + } +} diff --git a/client/status/status_test.go b/client/status/status_test.go new file mode 100644 index 000000000..24c4827d3 --- /dev/null +++ b/client/status/status_test.go @@ -0,0 +1,603 @@ +package status + +import ( + "bytes" + "encoding/json" + "fmt" + "runtime" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/netbirdio/netbird/client/proto" + "github.com/netbirdio/netbird/version" +) + +func init() { + loc, err := time.LoadLocation("UTC") + if err != nil { + panic(err) + } + + time.Local = loc +} + +var resp = &proto.StatusResponse{ + Status: "Connected", + FullStatus: &proto.FullStatus{ + Peers: []*proto.PeerState{ + { + IP: "192.168.178.101", + PubKey: "Pubkey1", + Fqdn: "peer-1.awesome-domain.com", + ConnStatus: "Connected", + ConnStatusUpdate: timestamppb.New(time.Date(2001, time.Month(1), 1, 1, 1, 1, 0, time.UTC)), + Relayed: false, + LocalIceCandidateType: "", + RemoteIceCandidateType: "", + LocalIceCandidateEndpoint: "", + RemoteIceCandidateEndpoint: "", + LastWireguardHandshake: timestamppb.New(time.Date(2001, time.Month(1), 1, 1, 1, 2, 0, time.UTC)), + BytesRx: 200, + BytesTx: 100, + Networks: []string{ + "10.1.0.0/24", + }, + Latency: durationpb.New(time.Duration(10000000)), + }, + { + IP: "192.168.178.102", + PubKey: "Pubkey2", + Fqdn: "peer-2.awesome-domain.com", + ConnStatus: "Connected", + ConnStatusUpdate: timestamppb.New(time.Date(2002, time.Month(2), 2, 2, 2, 2, 0, time.UTC)), + Relayed: true, + LocalIceCandidateType: "relay", + RemoteIceCandidateType: "prflx", + LocalIceCandidateEndpoint: "10.0.0.1:10001", + RemoteIceCandidateEndpoint: "10.0.10.1:10002", + LastWireguardHandshake: timestamppb.New(time.Date(2002, time.Month(2), 2, 2, 2, 3, 0, time.UTC)), + BytesRx: 2000, + BytesTx: 1000, + Latency: durationpb.New(time.Duration(10000000)), + }, + }, + ManagementState: &proto.ManagementState{ + URL: "my-awesome-management.com:443", + Connected: true, + Error: "", + }, + SignalState: &proto.SignalState{ + URL: "my-awesome-signal.com:443", + Connected: true, + Error: "", + }, + Relays: []*proto.RelayState{ + { + URI: "stun:my-awesome-stun.com:3478", + Available: true, + Error: "", + }, + { + URI: "turns:my-awesome-turn.com:443?transport=tcp", + Available: false, + Error: "context: deadline exceeded", + }, + }, + LocalPeerState: &proto.LocalPeerState{ + IP: "192.168.178.100/16", + PubKey: "Some-Pub-Key", + KernelInterface: true, + Fqdn: "some-localhost.awesome-domain.com", + Networks: []string{ + "10.10.0.0/24", + }, + }, + DnsServers: []*proto.NSGroupState{ + { + Servers: []string{ + "8.8.8.8:53", + }, + Domains: nil, + Enabled: true, + Error: "", + }, + { + Servers: []string{ + "1.1.1.1:53", + "2.2.2.2:53", + }, + Domains: []string{ + "example.com", + "example.net", + }, + Enabled: false, + Error: "timeout", + }, + }, + }, + DaemonVersion: "0.14.1", +} + +var overview = OutputOverview{ + Peers: PeersStateOutput{ + Total: 2, + Connected: 2, + Details: []PeerStateDetailOutput{ + { + IP: "192.168.178.101", + PubKey: "Pubkey1", + FQDN: "peer-1.awesome-domain.com", + Status: "Connected", + LastStatusUpdate: time.Date(2001, 1, 1, 1, 1, 1, 0, time.UTC), + ConnType: "P2P", + IceCandidateType: IceCandidateType{ + Local: "", + Remote: "", + }, + IceCandidateEndpoint: IceCandidateType{ + Local: "", + Remote: "", + }, + LastWireguardHandshake: time.Date(2001, 1, 1, 1, 1, 2, 0, time.UTC), + TransferReceived: 200, + TransferSent: 100, + Networks: []string{ + "10.1.0.0/24", + }, + Latency: time.Duration(10000000), + }, + { + IP: "192.168.178.102", + PubKey: "Pubkey2", + FQDN: "peer-2.awesome-domain.com", + Status: "Connected", + LastStatusUpdate: time.Date(2002, 2, 2, 2, 2, 2, 0, time.UTC), + ConnType: "Relayed", + IceCandidateType: IceCandidateType{ + Local: "relay", + Remote: "prflx", + }, + IceCandidateEndpoint: IceCandidateType{ + Local: "10.0.0.1:10001", + Remote: "10.0.10.1:10002", + }, + LastWireguardHandshake: time.Date(2002, 2, 2, 2, 2, 3, 0, time.UTC), + TransferReceived: 2000, + TransferSent: 1000, + Latency: time.Duration(10000000), + }, + }, + }, + Events: []SystemEventOutput{}, + CliVersion: version.NetbirdVersion(), + DaemonVersion: "0.14.1", + ManagementState: ManagementStateOutput{ + URL: "my-awesome-management.com:443", + Connected: true, + Error: "", + }, + SignalState: SignalStateOutput{ + URL: "my-awesome-signal.com:443", + Connected: true, + Error: "", + }, + Relays: RelayStateOutput{ + Total: 2, + Available: 1, + Details: []RelayStateOutputDetail{ + { + URI: "stun:my-awesome-stun.com:3478", + Available: true, + Error: "", + }, + { + URI: "turns:my-awesome-turn.com:443?transport=tcp", + Available: false, + Error: "context: deadline exceeded", + }, + }, + }, + IP: "192.168.178.100/16", + PubKey: "Some-Pub-Key", + KernelInterface: true, + FQDN: "some-localhost.awesome-domain.com", + NSServerGroups: []NsServerGroupStateOutput{ + { + Servers: []string{ + "8.8.8.8:53", + }, + Domains: nil, + Enabled: true, + Error: "", + }, + { + Servers: []string{ + "1.1.1.1:53", + "2.2.2.2:53", + }, + Domains: []string{ + "example.com", + "example.net", + }, + Enabled: false, + Error: "timeout", + }, + }, + Networks: []string{ + "10.10.0.0/24", + }, +} + +func TestConversionFromFullStatusToOutputOverview(t *testing.T) { + convertedResult := ConvertToStatusOutputOverview(resp, false, "", nil, nil, nil) + + assert.Equal(t, overview, convertedResult) +} + +func TestSortingOfPeers(t *testing.T) { + peers := []PeerStateDetailOutput{ + { + IP: "192.168.178.104", + }, + { + IP: "192.168.178.102", + }, + { + IP: "192.168.178.101", + }, + { + IP: "192.168.178.105", + }, + { + IP: "192.168.178.103", + }, + } + + sortPeersByIP(peers) + + assert.Equal(t, peers[3].IP, "192.168.178.104") +} + +func TestParsingToJSON(t *testing.T) { + jsonString, _ := ParseToJSON(overview) + + //@formatter:off + expectedJSONString := ` + { + "peers": { + "total": 2, + "connected": 2, + "details": [ + { + "fqdn": "peer-1.awesome-domain.com", + "netbirdIp": "192.168.178.101", + "publicKey": "Pubkey1", + "status": "Connected", + "lastStatusUpdate": "2001-01-01T01:01:01Z", + "connectionType": "P2P", + "iceCandidateType": { + "local": "", + "remote": "" + }, + "iceCandidateEndpoint": { + "local": "", + "remote": "" + }, + "relayAddress": "", + "lastWireguardHandshake": "2001-01-01T01:01:02Z", + "transferReceived": 200, + "transferSent": 100, + "latency": 10000000, + "quantumResistance": false, + "networks": [ + "10.1.0.0/24" + ] + }, + { + "fqdn": "peer-2.awesome-domain.com", + "netbirdIp": "192.168.178.102", + "publicKey": "Pubkey2", + "status": "Connected", + "lastStatusUpdate": "2002-02-02T02:02:02Z", + "connectionType": "Relayed", + "iceCandidateType": { + "local": "relay", + "remote": "prflx" + }, + "iceCandidateEndpoint": { + "local": "10.0.0.1:10001", + "remote": "10.0.10.1:10002" + }, + "relayAddress": "", + "lastWireguardHandshake": "2002-02-02T02:02:03Z", + "transferReceived": 2000, + "transferSent": 1000, + "latency": 10000000, + "quantumResistance": false, + "networks": null + } + ] + }, + "cliVersion": "development", + "daemonVersion": "0.14.1", + "management": { + "url": "my-awesome-management.com:443", + "connected": true, + "error": "" + }, + "signal": { + "url": "my-awesome-signal.com:443", + "connected": true, + "error": "" + }, + "relays": { + "total": 2, + "available": 1, + "details": [ + { + "uri": "stun:my-awesome-stun.com:3478", + "available": true, + "error": "" + }, + { + "uri": "turns:my-awesome-turn.com:443?transport=tcp", + "available": false, + "error": "context: deadline exceeded" + } + ] + }, + "netbirdIp": "192.168.178.100/16", + "publicKey": "Some-Pub-Key", + "usesKernelInterface": true, + "fqdn": "some-localhost.awesome-domain.com", + "quantumResistance": false, + "quantumResistancePermissive": false, + "networks": [ + "10.10.0.0/24" + ], + "dnsServers": [ + { + "servers": [ + "8.8.8.8:53" + ], + "domains": null, + "enabled": true, + "error": "" + }, + { + "servers": [ + "1.1.1.1:53", + "2.2.2.2:53" + ], + "domains": [ + "example.com", + "example.net" + ], + "enabled": false, + "error": "timeout" + } + ], + "events": [] + }` + // @formatter:on + + var expectedJSON bytes.Buffer + require.NoError(t, json.Compact(&expectedJSON, []byte(expectedJSONString))) + + assert.Equal(t, expectedJSON.String(), jsonString) +} + +func TestParsingToYAML(t *testing.T) { + yaml, _ := ParseToYAML(overview) + + expectedYAML := + `peers: + total: 2 + connected: 2 + details: + - fqdn: peer-1.awesome-domain.com + netbirdIp: 192.168.178.101 + publicKey: Pubkey1 + status: Connected + lastStatusUpdate: 2001-01-01T01:01:01Z + connectionType: P2P + iceCandidateType: + local: "" + remote: "" + iceCandidateEndpoint: + local: "" + remote: "" + relayAddress: "" + lastWireguardHandshake: 2001-01-01T01:01:02Z + transferReceived: 200 + transferSent: 100 + latency: 10ms + quantumResistance: false + networks: + - 10.1.0.0/24 + - fqdn: peer-2.awesome-domain.com + netbirdIp: 192.168.178.102 + publicKey: Pubkey2 + status: Connected + lastStatusUpdate: 2002-02-02T02:02:02Z + connectionType: Relayed + iceCandidateType: + local: relay + remote: prflx + iceCandidateEndpoint: + local: 10.0.0.1:10001 + remote: 10.0.10.1:10002 + relayAddress: "" + lastWireguardHandshake: 2002-02-02T02:02:03Z + transferReceived: 2000 + transferSent: 1000 + latency: 10ms + quantumResistance: false + networks: [] +cliVersion: development +daemonVersion: 0.14.1 +management: + url: my-awesome-management.com:443 + connected: true + error: "" +signal: + url: my-awesome-signal.com:443 + connected: true + error: "" +relays: + total: 2 + available: 1 + details: + - uri: stun:my-awesome-stun.com:3478 + available: true + error: "" + - uri: turns:my-awesome-turn.com:443?transport=tcp + available: false + error: 'context: deadline exceeded' +netbirdIp: 192.168.178.100/16 +publicKey: Some-Pub-Key +usesKernelInterface: true +fqdn: some-localhost.awesome-domain.com +quantumResistance: false +quantumResistancePermissive: false +networks: + - 10.10.0.0/24 +dnsServers: + - servers: + - 8.8.8.8:53 + domains: [] + enabled: true + error: "" + - servers: + - 1.1.1.1:53 + - 2.2.2.2:53 + domains: + - example.com + - example.net + enabled: false + error: timeout +events: [] +` + + assert.Equal(t, expectedYAML, yaml) +} + +func TestParsingToDetail(t *testing.T) { + // Calculate time ago based on the fixture dates + lastConnectionUpdate1 := timeAgo(overview.Peers.Details[0].LastStatusUpdate) + lastHandshake1 := timeAgo(overview.Peers.Details[0].LastWireguardHandshake) + lastConnectionUpdate2 := timeAgo(overview.Peers.Details[1].LastStatusUpdate) + lastHandshake2 := timeAgo(overview.Peers.Details[1].LastWireguardHandshake) + + detail := ParseToFullDetailSummary(overview) + + expectedDetail := fmt.Sprintf( + `Peers detail: + peer-1.awesome-domain.com: + NetBird IP: 192.168.178.101 + Public key: Pubkey1 + Status: Connected + -- detail -- + Connection type: P2P + ICE candidate (Local/Remote): -/- + ICE candidate endpoints (Local/Remote): -/- + Relay server address: + Last connection update: %s + Last WireGuard handshake: %s + Transfer status (received/sent) 200 B/100 B + Quantum resistance: false + Networks: 10.1.0.0/24 + Latency: 10ms + + peer-2.awesome-domain.com: + NetBird IP: 192.168.178.102 + Public key: Pubkey2 + Status: Connected + -- detail -- + Connection type: Relayed + ICE candidate (Local/Remote): relay/prflx + ICE candidate endpoints (Local/Remote): 10.0.0.1:10001/10.0.10.1:10002 + Relay server address: + Last connection update: %s + Last WireGuard handshake: %s + Transfer status (received/sent) 2.0 KiB/1000 B + Quantum resistance: false + Networks: - + Latency: 10ms + +Events: No events recorded +OS: %s/%s +Daemon version: 0.14.1 +CLI version: %s +Management: Connected to my-awesome-management.com:443 +Signal: Connected to my-awesome-signal.com:443 +Relays: + [stun:my-awesome-stun.com:3478] is Available + [turns:my-awesome-turn.com:443?transport=tcp] is Unavailable, reason: context: deadline exceeded +Nameservers: + [8.8.8.8:53] for [.] is Available + [1.1.1.1:53, 2.2.2.2:53] for [example.com, example.net] is Unavailable, reason: timeout +FQDN: some-localhost.awesome-domain.com +NetBird IP: 192.168.178.100/16 +Interface type: Kernel +Quantum resistance: false +Networks: 10.10.0.0/24 +Peers count: 2/2 Connected +`, lastConnectionUpdate1, lastHandshake1, lastConnectionUpdate2, lastHandshake2, runtime.GOOS, runtime.GOARCH, overview.CliVersion) + + assert.Equal(t, expectedDetail, detail) +} + +func TestParsingToShortVersion(t *testing.T) { + shortVersion := ParseGeneralSummary(overview, false, false, false) + + expectedString := fmt.Sprintf("OS: %s/%s", runtime.GOOS, runtime.GOARCH) + ` +Daemon version: 0.14.1 +CLI version: development +Management: Connected +Signal: Connected +Relays: 1/2 Available +Nameservers: 1/2 Available +FQDN: some-localhost.awesome-domain.com +NetBird IP: 192.168.178.100/16 +Interface type: Kernel +Quantum resistance: false +Networks: 10.10.0.0/24 +Peers count: 2/2 Connected +` + + assert.Equal(t, expectedString, shortVersion) +} + +func TestTimeAgo(t *testing.T) { + now := time.Now() + + cases := []struct { + name string + input time.Time + expected string + }{ + {"Now", now, "Now"}, + {"Seconds ago", now.Add(-10 * time.Second), "10 seconds ago"}, + {"One minute ago", now.Add(-1 * time.Minute), "1 minute ago"}, + {"Minutes and seconds ago", now.Add(-(1*time.Minute + 30*time.Second)), "1 minute, 30 seconds ago"}, + {"One hour ago", now.Add(-1 * time.Hour), "1 hour ago"}, + {"Hours and minutes ago", now.Add(-(2*time.Hour + 15*time.Minute)), "2 hours, 15 minutes ago"}, + {"One day ago", now.Add(-24 * time.Hour), "1 day ago"}, + {"Multiple days ago", now.Add(-(72*time.Hour + 20*time.Minute)), "3 days ago"}, + {"Zero time", time.Time{}, "-"}, + {"Unix zero time", time.Unix(0, 0), "-"}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + result := timeAgo(tc.input) + assert.Equal(t, tc.expected, result, "Failed %s", tc.name) + }) + } +} diff --git a/client/system/info.go b/client/system/info.go index 200d835df..2a0343ca6 100644 --- a/client/system/info.go +++ b/client/system/info.go @@ -9,7 +9,6 @@ import ( "google.golang.org/grpc/metadata" "github.com/netbirdio/netbird/management/proto" - "github.com/netbirdio/netbird/version" ) // DeviceNameCtxKey context key for device name @@ -50,7 +49,7 @@ type Info struct { OSVersion string Hostname string CPUs int - WiretrusteeVersion string + NetbirdVersion string UIVersion string KernelVersion string NetworkAddresses []NetworkAddress @@ -59,6 +58,31 @@ type Info struct { SystemManufacturer string Environment Environment Files []File // for posture checks + + RosenpassEnabled bool + RosenpassPermissive bool + ServerSSHAllowed bool + DisableClientRoutes bool + DisableServerRoutes bool + DisableDNS bool + DisableFirewall bool +} + +func (i *Info) SetFlags( + rosenpassEnabled, rosenpassPermissive bool, + serverSSHAllowed *bool, + disableClientRoutes, disableServerRoutes, + disableDNS, disableFirewall bool, +) { + i.RosenpassEnabled = rosenpassEnabled + i.RosenpassPermissive = rosenpassPermissive + if serverSSHAllowed != nil { + i.ServerSSHAllowed = *serverSSHAllowed + } + i.DisableClientRoutes = disableClientRoutes + i.DisableServerRoutes = disableServerRoutes + i.DisableDNS = disableDNS + i.DisableFirewall = disableFirewall } // StaticInfo is an object that contains machine information that does not change @@ -94,11 +118,6 @@ func extractDeviceName(ctx context.Context, defaultName string) string { return v } -// GetDesktopUIUserAgent returns the Desktop ui user agent -func GetDesktopUIUserAgent() string { - return "netbird-desktop-ui/" + version.NetbirdVersion() -} - func networkAddresses() ([]NetworkAddress, error) { interfaces, err := net.Interfaces() if err != nil { diff --git a/client/system/info_android.go b/client/system/info_android.go index 7718da913..56fe0741d 100644 --- a/client/system/info_android.go +++ b/client/system/info_android.go @@ -36,9 +36,12 @@ func GetInfo(ctx context.Context) *Info { OSVersion: osVersion(), Hostname: extractDeviceName(ctx, "android"), CPUs: runtime.NumCPU(), - WiretrusteeVersion: version.NetbirdVersion(), + NetbirdVersion: version.NetbirdVersion(), UIVersion: extractUIVersion(ctx), KernelVersion: kernelVersion, + SystemSerialNumber: serial(), + SystemProductName: productModel(), + SystemManufacturer: productManufacturer(), } return gio @@ -49,13 +52,42 @@ func checkFileAndProcess(paths []string) ([]File, error) { return []File{}, nil } +func serial() string { + // try to fetch serial ID using different properties + properties := []string{"ril.serialnumber", "ro.serialno", "ro.boot.serialno", "sys.serialnumber"} + var value string + + for _, property := range properties { + value = getprop(property) + if len(value) > 0 { + return value + } + } + + // unable to get serial ID, fallback to ANDROID_ID + return androidId() +} + +func androidId() string { + // this is a uniq id defined on first initialization, id will be a new one if user wipes his device + return run("/system/bin/settings", "get", "secure", "android_id") +} + +func productModel() string { + return getprop("ro.product.model") +} + +func productManufacturer() string { + return getprop("ro.product.manufacturer") +} + func uname() []string { res := run("/system/bin/uname", "-a") return strings.Split(res, " ") } func osVersion() string { - return run("/system/bin/getprop", "ro.build.version.release") + return getprop("ro.build.version.release") } func extractUIVersion(ctx context.Context) string { @@ -66,6 +98,10 @@ func extractUIVersion(ctx context.Context) string { return v } +func getprop(arg ...string) string { + return run("/system/bin/getprop", arg...) +} + func run(name string, arg ...string) string { cmd := exec.Command(name, arg...) cmd.Stdin = strings.NewReader("some") diff --git a/client/system/info_darwin.go b/client/system/info_darwin.go index 13b0a446b..f105ada60 100644 --- a/client/system/info_darwin.go +++ b/client/system/info_darwin.go @@ -63,7 +63,7 @@ func GetInfo(ctx context.Context) *Info { systemHostname, _ := os.Hostname() gio.Hostname = extractDeviceName(ctx, systemHostname) - gio.WiretrusteeVersion = version.NetbirdVersion() + gio.NetbirdVersion = version.NetbirdVersion() gio.UIVersion = extractUserAgent(ctx) return gio diff --git a/client/system/info_freebsd.go b/client/system/info_freebsd.go index 454e58a0b..bed6711de 100644 --- a/client/system/info_freebsd.go +++ b/client/system/info_freebsd.go @@ -39,17 +39,17 @@ func GetInfo(ctx context.Context) *Info { systemHostname, _ := os.Hostname() return &Info{ - GoOS: runtime.GOOS, - Kernel: osInfo[0], - Platform: runtime.GOARCH, - OS: osName, - OSVersion: osVersion, - Hostname: extractDeviceName(ctx, systemHostname), - CPUs: runtime.NumCPU(), - WiretrusteeVersion: version.NetbirdVersion(), - UIVersion: extractUserAgent(ctx), - KernelVersion: osInfo[1], - Environment: env, + GoOS: runtime.GOOS, + Kernel: osInfo[0], + Platform: runtime.GOARCH, + OS: osName, + OSVersion: osVersion, + Hostname: extractDeviceName(ctx, systemHostname), + CPUs: runtime.NumCPU(), + NetbirdVersion: version.NetbirdVersion(), + UIVersion: extractUserAgent(ctx), + KernelVersion: osInfo[1], + Environment: env, } } diff --git a/client/system/info_ios.go b/client/system/info_ios.go index 3dbf50e1e..897ec0a35 100644 --- a/client/system/info_ios.go +++ b/client/system/info_ios.go @@ -19,7 +19,7 @@ func GetInfo(ctx context.Context) *Info { gio := &Info{Kernel: sysName, OSVersion: swVersion, Platform: "unknown", OS: sysName, GoOS: runtime.GOOS, CPUs: runtime.NumCPU(), KernelVersion: swVersion} gio.Hostname = extractDeviceName(ctx, "hostname") - gio.WiretrusteeVersion = version.NetbirdVersion() + gio.NetbirdVersion = version.NetbirdVersion() gio.UIVersion = extractUserAgent(ctx) return gio diff --git a/client/system/info_linux.go b/client/system/info_linux.go index bfc77be19..9bfc82009 100644 --- a/client/system/info_linux.go +++ b/client/system/info_linux.go @@ -61,7 +61,7 @@ func GetInfo(ctx context.Context) *Info { Hostname: extractDeviceName(ctx, systemHostname), GoOS: runtime.GOOS, CPUs: runtime.NumCPU(), - WiretrusteeVersion: version.NetbirdVersion(), + NetbirdVersion: version.NetbirdVersion(), UIVersion: extractUserAgent(ctx), KernelVersion: osInfo[1], NetworkAddresses: addrs, diff --git a/client/system/info_test.go b/client/system/info_test.go index f44219d9e..27821f3c5 100644 --- a/client/system/info_test.go +++ b/client/system/info_test.go @@ -11,7 +11,7 @@ import ( func Test_LocalWTVersion(t *testing.T) { got := GetInfo(context.TODO()) want := "development" - assert.Equal(t, want, got.WiretrusteeVersion) + assert.Equal(t, want, got.NetbirdVersion) } func Test_UIVersion(t *testing.T) { diff --git a/client/system/info_windows.go b/client/system/info_windows.go index 28bd3d300..6f05ded20 100644 --- a/client/system/info_windows.go +++ b/client/system/info_windows.go @@ -64,7 +64,7 @@ func GetInfo(ctx context.Context) *Info { systemHostname, _ := os.Hostname() gio.Hostname = extractDeviceName(ctx, systemHostname) - gio.WiretrusteeVersion = version.NetbirdVersion() + gio.NetbirdVersion = version.NetbirdVersion() gio.UIVersion = extractUserAgent(ctx) return gio @@ -105,7 +105,7 @@ func getOSNameAndVersion() (string, string) { split := strings.Split(dst[0].Caption, " ") - if len(split) < 3 { + if len(split) <= 3 { return "Windows", getBuildVersion() } diff --git a/client/ui/bundled.go b/client/ui/bundled.go deleted file mode 100644 index e2c138b14..000000000 --- a/client/ui/bundled.go +++ /dev/null @@ -1,12 +0,0 @@ -// auto-generated -// Code generated by '$ fyne bundle'. DO NOT EDIT. - -package main - -import "fyne.io/fyne/v2" - -var resourceNetbirdSystemtrayConnectedPng = &fyne.StaticResource{ - StaticName: "netbird-systemtray-connected.png", - StaticContent: []byte( - "\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x01\x00\x00\x00\x01\x00\b\x06\x00\x00\x00\\r\xa8f\x00\x00\x00\xc3zTXtRaw profile type exif\x00\x00x\xdamP\xdb\r\xc3 \f\xfc\xf7\x14\x1d\xc1\xaf\x80\x19\x874\xa9\xd4\r:~\r8Q\x88r\x92χ\x9d\x1cư\xff\xbe\x1fx50)\xe8\x92-\x95\x94СE\vW\x17\x86\x03\xb53\xa1v>@\xc1S\x1dNɞų\x8c\x86\xa5\xf8\xeb\xa8\xd3d\x83T]-\x17#{Gc\x9d\x1bEGf\xbb\x19\xc5E\xd2&b\x17[\x18\x950\x12\x1e\r\n\x83:\x9e\x85\xa9X\xbe>a\xddq\x86\x8d\x80F\x92\xbb\xf7ir?k\xf6\xedm\x8b\x17\x85y\x17\x12t\x16\xd11\x80\xb4P\x90\xdaE\xf5\xf0\xa1\xfc#u-\x92:[L\xe2\vy\xda\xd3\x01\xf8\x03\xda\xd4Y\x17ݮ\xb7\xee\x00\x00\x01\x84iCCPICC profile\x00\x00x\x9c}\x91=H\xc3@\x1c\xc5_S\xa5\"-\x0e\x16\x14\x11\xccP\x9d\xec\xa2\"\xe2T\xabP\x84\n\xa5Vh\xd5\xc1\xe4\xd2/hҐ\xa4\xb88\n\xae\x05\a?\x16\xab\x0e.κ:\xb8\n\x82\xe0\a\x88\xb3\x83\x93\xa2\x8b\x94\xf8\xbf\xa4\xd0\"ƃ\xe3~\xbc\xbb\xf7\xb8{\a\b\x8d\nSͮ\x18\xa0j\x96\x91N\xc4\xc5lnU\f\xbcB\xc0\x00B\x18\xc1\xac\xc4L}.\x95J\xc2s|\xdd\xc3\xc7\u05fb(\xcf\xf2>\xf7\xe7\b)y\x93\x01>\x918\xc6t\xc3\"\xde \x9e\u07b4t\xce\xfb\xc4aV\x92\x14\xe2s\xe2q\x83.H\xfc\xc8u\xd9\xe57\xceE\x87\x05\x9e\x1962\xe9y\xe20\xb1X\xec`\xb9\x83Y\xc9P\x89\xa7\x88#\x8a\xaaQ\xbe\x90uY\xe1\xbc\xc5Y\xad\xd4X\xeb\x9e\xfc\x85\xc1\xbc\xb6\xb2\xccu\x9a\xc3H`\x11KHA\x84\x8c\x1aʨ\xc0B\x94V\x8d\x14\x13iڏ{\xf8\x87\x1c\x7f\x8a\\2\xb9\xca`\xe4X@\x15*$\xc7\x0f\xfe\a\xbf\xbb5\v\x93\x13nR0\x0et\xbf\xd8\xf6\xc7(\x10\xd8\x05\x9au\xdb\xfe>\xb6\xed\xe6\t\xe0\x7f\x06\xae\xb4\xb6\xbf\xda\x00f>I\xaf\xb7\xb5\xc8\x11з\r\\\\\xb75y\x0f\xb8\xdc\x01\x06\x9ftɐ\x1c\xc9OS(\x14\x80\xf73\xfa\xa6\x1c\xd0\x7f\v\xf4\xae\xb9\xbd\xb5\xf6q\xfa\x00d\xa8\xab\xe4\rpp\b\x8c\x15){\xdd\xe3\xdd=\x9d\xbd\xfd{\xa6\xd5\xdf\x0fںr\xd0VwQ\xba\x00\x00\rxiTXtXML:com.adobe.xmp\x00\x00\x00\x00\x00\n\n \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\xf0C\xff\xd9\x00\x00\x00\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\x00\x00\tpHYs\x00\x00\v\x13\x00\x00\v\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\atIME\a\xe8\x02\x17\r$'\xdd\xf7ȗ\x00\x00\x13;IDATx\xda\xed\x9d]o\x14W\x9a\xc7\xff\xa7\xaamh\xbf\xc46,I`\x99\xa1\xc3\ni\xb5{1\x95O0\xe4\x1b\xc0'X\xf2\t`.W`hp\xa2\xb9\fH{O\xa3\xcc\xc5\xecJ3q\xa4\x1d\xed\xcdJx>Aj/\"EBJګL \xb1\x00g\xf1\v\xb6\xbb\xeb\xec\x85mb\f\xb6\xfb\xa5^Ω\xfa\xfd\xee\x928v\xf7\xa9z\xfe\xcfs\x9e\xa7ο$\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\u0603a\t\xc0g\xd6\x7f\x1f5\x92\x8e\"k\xd4\b\xa4s\xb2jH\x9afez\n\xfe\xdb\b\x00x\x81mF\xd3/CE]\xa3(\x94~c\xa5\x8b;\xc1\x0e\x83E\x7f{\xecF\xfcA\x8d\x95\x00g\xb3\xfb\\tQ\xd2o\xadtq]\xba(I\x81\x95,K345\xa3˒\x84\x00\x80SY~5Х0\xd0o\x13\xabK\x96R>\x9b\xe4o\xd4\x1a\xbd\x1e\xc7\b\x008\x93\xe9\xadtkM\x8a\x02i\xdaZ\x9aS\x99\x12\xea\xf6\xabJ\x80Հ\x02\xf7\xf4W\x13\xe9\xdan\xa6'\xe8sXw\xe9\xf6ؿ\xc6\xed_Z\x01\x00\x05d{\xed\xec\xe9!\xcf\xda\x7f\xbb\xf1\xf7Z/\x80U\x81<\x03\xdf\x12\xf8\xc5\xc5\x7f\xf2K\xe9O\x05\x00d\xfcje\xffx\xecF\xfc\xe1\xfe\x7fM\x05\x00\xd9\x04~3j$5ݲVWX\r\a\xe2?\xdc\x1e\xfb!\x00\x909ks\xd1\xd5Dj\x1a\xcb\x18ω\xe07j\xd5\xf74\xfe\x10\x00Ȅ\x95O\xa3(Ht_R\xc4\xdeҙҿ\xbdw췟\x80\x15\x82\xb4\xb2~\x90\xe8+I\x11\xab\xe1\x0e\xd6\xea\xc1A\xd9\x7f[\x1f\x00\x86\xdc\xeb\xdbP\xf7E\x93\xcf\xc9\xec\xbf\x7f\xec\xc7\x16\x00\xd2\v\xfe\xb9\xe8b\"}axd\xd7\xcd\xf8O\x0e.\xfd\xd9\x02\xc0\xb0\xc1\x7f\xcbJ\x0f\t~G\t4_\xbf\x19\xb7\x8e\xfa1*\x00\xe8\x9b\xd5O\xa2\xfb\x8c\xf7\x1c\xcf\xfe\x81~\xd7\xcb\xcf!\x00\xd03\xb6\x19M\xaf\x87\xfaB\x96\xfd\xbe\xd3\xc1\x7f\xc8؏-\x00\fV\xf27\xa3\xc6z\xa8\x87\xa2\xd9\xe7x\xf4\x1f>\xf6\xa3\x02\x80\x81\x82\xdf\xd6\xf4\x10\a\x1e\x0f\xe2?\xd1\xed\xfa\x8d\u07b2\xff\xb6^\x00\x10\xfc\xa5\xc9\xfeG\x8d\xfdJW\x01\xd8f4\xfd\xf2ؾNt\xe7\xe8\x9b5\f\xb4\xdc\r\xb4\xbc\xfb\xcf\xc77\xb4l\x9a\xf12wѾ=?\xc1\xef\xcf\xf5\xb2\xbd5\xfe\x9c\xac\x00\xd6\x7f\x1f5\xc2D\xd3[\x89\x1a\xd6jZ\x81\xa6\x8d\xd5t`tn\xe7\xcb5$M\xcb\xec\x04{\x867\xa5\x95\x96\x8dѲ\xacvK\xa9ec\xb4\x9cX-Z\xa3ec\xd5\x0e\xa4e\xd5\xd4\xee\xb5\xd9\xe2#ks\x11O\xf6\xf9\x92\xfc\x8dZ\xf5\x1b\xf1\xc7N\n\x80mF\xd3[#jlv\x15)\xd0\xf4\x1e\xfb憌\xa6\xbd\xcf0F\xed\x1d\xb1X\xb6\xd2\xffX\xabvh\xd4>\xdeU\xeckU\xb1v'\xfaLF\xd7\b-On\xc1\x9a>\x18$\x19\x99,\x82<\b\xf4\x1b\xb3\xed\xed\x16Y\xa9Q\xe5\x87E\xac\xb4l\xa4XFq\"-\x86V\xb1\xeb°\xf3\x90O\x93\xb0\xf2\xe6\x1e\xbb=>\x1b\x0ft\xbd\xcc\xc0\x81nuq'\x93Gv\xfb\xf4\x17O\x84\xf5G,\xa9\x9d\x18\xfd5\xb4\x8a\xeb\xb3\xf1\x82\v\x1fju.\xbad\xa4/\xb8<\xfeT\x9f\xf5\x8e>\x1c4\xa1\x98\xa3\x82}-\xd4Ek\xd4\xe0e\f\xb9\xb0 \xa3\xd8Z\xfdu\xac\xab\x85\xbc\xab\x04:\xfe\x1eƿ\xd5ǽ<\xf2ۓ\x00l~\x1aE\x9bV\x17\tv\x87\xaa\x04\xa3\x05c\xf5e\x1e\x15\xc2\xda'\xd1w\\s\xbf\xb2\x7f\xbfc\xbf7~\xc5\xda\\tU\xd2%\xcax/z\t\v\x89\u0557\xe1\x88\x16Ҟ>\xb0\xef\xf70\xfe\al\xfc\xbd\xf6;V>\x8d\"\x93p\xaa\xcb\xc7\xedBb\xf5 \r1\xd89\xd3\xff\x1dK\xeaQ\xf0\x0f8\xf6{\xeb\x16`ǹ\xf5!\xcbZM1X\x9d\x8b\xbe2\xcc\xfb\xbd\xaa\x06\x83\x9a>L\xa3\n|\xd5\x03X\xbf\x13]\xb1F\xf7Y^\uf677҃\xf1\xd9x\xbe\x97\x1f^\xb9\x13]\t\xb8\xee\xbe\t\xc0\xc0c\xbf\x03\x05\x00\x11([\x8d\xa8\xb6\x12͛\x11\xdd;,S\xd0\xf8\xf3\xef\xba\x0e\xdb\xf8\xdb\xcbkǁ\xeb7\xe3\x96U\xefG\t\xc1\xe94ѐ\xd15\xdb\xd1w\xab\x9fD\xf7w^\xb5\xfd\xfa\xde\x7f.\xbaE\xf0{\x16\xffI\xba\xf1i\x0e\xd8\x136\xcd\xf6\xdb\\\xa0d\xd9#It{\xe2fܢ\xf1\xe7\xe5\xf5[\x18\xbb\x11\x7f\x94\xb9\x00 \x02\x15\x10\x82\xe7\x13\xed`z\xe5\"\x8b\xe1\xd1eKa\xec׳\x00 \x02%\xde\x1dlִ\xf5\xf5\xafeF;\nO?Wp\xe2\x05\x8b\xe2z\xf0\xa74\xf6;\xb4\a\xb0\x9f\xf1ٸ\x99H\x0fX\xfer\xd1yt\xe6\x95\x10t\x16Oi\xeb\xeb_+y6\xc9\xc28\\\xb1\xf5c\xf3\x95\x9a\x00H\xd2\xc4l|\x05\x11(\x0fɳI\xd9\xcd\xda\x1b\x15\xc1\xae\x10ؕ:\x8b\xe4\x1e\xf7\xb2\xf2\x9d\xe8\xf94\xe0\xfa\\\xf4\x90w\xbb{^\xfaw\x03u\xbe9\xfb\x86\x00\xbc\x91\x15N\xac(<\xfd\\ft\x8bEs \xfb\xa79\xf6\xeb\xbb\x02\xd8\xe5eW\x97\xed\xf6\x11V\xf05\xfb\xff4ud\xf0oW\t\x13\xda\xfa\xfaW\xea>\x9eaъ\x8e\xff$۱|_~\x00ϛ\xd1\xf4h\xa8\x87<6\xeaa\xf6\xdfi\xfc\xf5}\x83\x8cvT\xbb\xf0\x98j\xa0\x88\xe0Ϩ\xf17P\x05 I3\xcdx9\xe8게\xda\\\x1e\xbf\x184\x9bo\vǯ\xd4\xfd\xfe\xa4l\x97\xd7H\xe4J\x98\xfdCy}_\xd1z3n\x9b\x8e>B\x04<\xca\xfe+\xf5\xa1\xbb\xfcݥ\xa9\x9d\xfe\xc1\b\v\x9a\xc75\x93n\xe7a8;\x90\xa4#\x02~\xd1Y<\x95\xe26\x82\xde@\xf6\xb5\xbf\xdaAM\xad<\xfe\xd4\xc05\x1d\"\xe0\ao\x1b\xfb\r\xbd\x9dx2\xa3-\xaa\x81\xec\xe2?\xc9'\xfbok͐`(\xe2p\x19\xb9YS\xe7љ\xd4\x05\xe0\xd5\xcd3\xdaQx\xf6\xa9\x82\xa9U\x16;\xc5\xec\x9f\xe5\xd8/\xb5\n`\x97\x89\xebql\x03}d%ު\xe3\x18\xdd\xc73\x99\x05\xff+\x81\xf9\xf6=\xb6\x04)R3\xba\x9c\xe7\xdfK\xa5\xad;q=\x8e%}\xcc\xe5s+\xfb\xe7\xf5xo\xf7Ɍ:\x8b\xef2%\x186\xf9\x1b\xb5F\xb7c\xc9/\x01\x90\xa4\xf1\xd9x\xdeXD\xc0\xa5\xec\x9fo\xafa\x82)\xc1\xb0\x84\xf9{q\xa4*\xd9\xf5\x9bqK\xa6\xff\x17\x14B\xda\xc18Y\xc8\xe1\x9e\xed\x9e\xc3iD`\x90\xb5S~\x8d\xbf\xd7[\x0e\x19\xc01\xe2b\xd9\xfa\xfaי\xee\xfd\x8f\xced\x89F.<\x96\xa9op1z\x8b\xc2\\\x1b\x7f\x99U\x00{\xb6\x03M\xac\xc5\n*\xfd{|\xde?\xdb\x0f\x11h\xeb\xd1i%?\x8fsAz\x89\xff\xa4\xb8X\xc9\xf4\xed\xc0T\x02E\x94\xe0g\x8a\x17\x80\xbd\xc5\xc0\xb9%\x85\x18\x8e\x1c\x16\x81\xf1؍\xf8â\xfe|\xa6m\xdb\x1dC\x91{\\\xe5\x9c\x12o\xc6c\xbf\x81>\xd3\xe2)u1\x1b98\xfe\xc3|\xc7~\xb9\n\x80$M\xcc\xc6\xd70\x14\xc9'\xfb\xbb\xea\xea\x83\b\x1c\x10\xfcF\xad\"\x1a\x7f\xb9\n\xc0\x8e\b\xe0*\x941\x9do\xdfw\xbb:A\x04\xf6\x97\xfe\xed\"\xc6~\x85\b\x80$muu\rC\x91lH\x9eMʮ\x8f\xba\xbfE\xf9\xfe\xa4\xec\xfa1.\x98$k\xf5\xa0\xe8쿭C9\x82\xa1HF\xe2Z\xf4د\x1f\xc2D#\xff\xf8\xb7j\x1b\x8c\x148\xf6+\xac\x02\x90\xb6\rE6\xbb\x9c L5\xab:\xd8\xf8;\xfc\x03\a\x95\x7fX\xa8ȱ_\xa1\x02\xb0+\x02\x1c#N\xa9\x8cܬ\xa9\xfbd\xc6\xcb\xcf\xdd\xf9\xf6\xbdJ\x9e\x1d0F\xad\xfa\u0378UY\x01\x90\xf0\x12H3\xfb{+^\xeb\xa3\xea~\xffwջh\xa1[\x0f\xc8\x15&\xc1\x88\xc0\xb0\x01t\xcc\xfb\x97y$\xcf&*u\x94\u0605\xb1\x9f3\x02\xb0W\x04\xf0\x12\xe8\x9fη\uf563\x8ay2S\x8d\x97\x9182\xf6sJ\x00vE\x00C\x91~3\xe7\xa4_\x8d\xbf#\xd8\xfa\xf6\xbd\xd27\x05\xf3\xb4\xf9\xeaO\x97\x1c\x01k\xb1\x1eK\x7f\a\x9f\xf7O\xe5F\x9cx\xa9\x91\v?\x946\xfb\xbb2\xf6s\xae\x02\xd8e\xe2z\x1c\a\x16/\x81#\xb3\xff\xd3\xc9\xd2\x05\xbf$ٕ\xe3\xea.M\x953\xfe\x1d6\xcaqj\x0eS\xbf\x19\xb7p\x15:<\xfb\xfb8\xf6\xeb\xb9\x1f\xf0\xfd\xc9\xd2\xf5\x03\x8cQ\xab>\x1b/ \x00}\x88\x00\xaeB\a\x04H\x05:\xe6\x9d\xc5S\xe5z> t\xdb\x17\xc3ɕ\x1e\xbb\x11\xdf\xc5Pd_\xf6\xffy\xdc\xfb\xb1_\xafUNR\x12\xa1+\xca\xe6\xcb{\x01\x90p\x15z#3~\x7f\xb2:\x95\xceҔ\xff[\x01\xa3\xf6XWw]\xff\x98N\xd7Z\x88\xc06e\x1b\xfbUA\xf0L\xa2ۦ\x19;?\xda6>,\xe6\xca\\t7\x90\xaeV\xb2\xf4/\xe9د\xa7\xed\xf3٧\nO\xfd\xecg\xf6wt\xec\xe7U\x05\xb0K\x95]\x85\xbc;\xed\x97\xf6w\xf7\xb0!hB}\xe4\xcbg\xf5fu\xab\xe8*\xe4\xb2\xcdW>\n\x10xw`\xc8\xc5\xe7\xfdK!\x00R\xf5\\\x85*yZn\x1fɳ\to\x1a\x82VZv}\xec\xe7\xb5\x00\xec\x1a\x8aTA\x04\x92g\x93J~\x1e\x13H\x1d\x7fƂ\xf7|\xca\xfe\x92'M\xc0\xfd\xac7\xa3\x86\xad顬\x1ae\xbd齲\xf9ʁ\x91\v\x8fe&\xd6]\x8e$o\x1a\x7f\xdeV\x00\xbb\x94\xddK\xa0ʍ?_\xab\x00\x97l\xbeJ/\x00e\x16\x01\xbbYSR\xd2C1C\xad\xcb\xcaqw{\x01\x81\xe6]\xb2\xf9\xaa\x84\x00\x94U\x04|\x1d}U\xb9\n0\x81\xbfgW\xbc\xbf\xd3\xca\xe4*T\xf9\xb1\x9f\x87U\x80oc\xbf\xd2\t\xc0\xae\b\x94\xc1U\xa8\xf3\xe8\fQ\xeeS\x15\xe0\xa8\xcdW\xe5\x04@\xda1\x14Q\xb1/Z\x1c\x86*>\xef\xef{\x15\xe0\xaa\xcdW%\x05@\x92\xea\xb3\U000423c6\"\xb6\x1bT\xca\x1d\xb7\x14U\x80Q\xdb\xd7\xc6_i\x05@\xf2\xd3U(\xf9i\x8a\xec\xdfo\x15P\xb0\x89\xa8-\x89}])\xdb\xcd\xf5\x9bq˗c\xc4e\xb7\xf9\xcan\xcb4Q\\\xf27j\x8d\xcf\xc6\xf3\b\x80\xc3\xf8\xe2%@\xe9?\xe0\xba\xfd4Uܸ4,\x8fGE\xa9\aή\x8b\x80]\xa93\xf6\x1bX\x01\x82B\xd6\xce\a\x9b/\x04\xc0\x13\x11\xe8,\x9e\"\x90\x87\xd9\x06,\x8f\xe7\\\xfb\xab\x1d\xd4\xd4*\xd3\x1aV⑳\xf1ٸ隗\x00c\xbf4*\xa8\xe3\xb2\xeb\xc7\xf2\x8b\xff\xa4\\ٿ2\x02 \xb9e(b7k\xec\xfd\xd3\x12Ҽ\x8eL\x97d\xecWY\x01\xd8\x15\x01#-\x14~\xd32\xf6K\xaf\x15\xf0S>\a\xa7j\xc6߇\xcc\x10\x80=\xbc\xec\xear\x91\x86\"v\xb3V\xdaW`\x15\xa3\x00A\xe6O\x06\x1a\xa3\xd6\xe8\xf58F\x00J@ѮB\x94\xfe\x19\xaci\xd6ۀ\xb0\xbc\xd6\xf4\x95@X\xbd\xec\x8f\x00d \x02\x8c\xfd\x1c\xe8\x03\xf4)\x02I\xc5\x1a\x7f\b\xc0\x80\"Ћ\xa1\bo\xf7q\xa1\x0f\xd0\xc76\xc0\xa8\x1d\xd6t\xb7\xaak\x85\x00\xf4\xc1Q\xaeB\xd8|\xb9\xb2\r\xe8\xdd&\xac\x8c6_\xfd`\xb8]\xfagu.\xfa\xcaH\xd1k7]7P盳\b\x80\v7\xf5hG#\xff\xfc\xbf=e\xff\xb1\x1b\xf1\aU^+*\x80\x01x\x9b\xa1\b6_\x0eU\x00\x9b\xb5\x9e\x9a\xb0>\xbeF\x0e\x01p\x80]W\xa1\xdd\x13\x84\xbc\xdd\xc7A\x8e\xd8\x06\x18\xa3V}6^@\x00``\x11\xd8=F\xcc\x13\x7f\xee\x91\x1c5\t\xa8\xe8\xd8\x0f\x01H\x91z3n'\x1b\xb5{\xae\xbc\xae\x1a\xf6l\x03\x0e\xa9\x00\xcan\xf3\x85\x00乀\xc7:Wk\x17~\x90\u0084\xc5pI\x00\xd6\x0e\xa8\x00\x8c\xdac\xdd\xea\x8e\xfd\x10\x80\x14Y\x9b\x8b\xaeʪaF;\x1a\xb9\xf0\x18\x11pI\x00\x0ehȚD\xb7M3^f\x85\x10\x80\xa1XoF\r\x19]{uc\xd574r\xfeG\x16\xc6\x15\xba\xc1\x9b\x0eA%}\xbb\x0f\x02P\x00IM\xb7d\xd5x\xed\xfe\x9aXWxn\x89\xc5q\xa6\x0f\xf0\xfa6\xc0\x84\xfa\x88UA\x00R\xc9\xfe\xc6\xea\xca\xdb\xfe[x\xe2\x05\"\xe0\xe06\xa0J6_\b@\xd6\xd9?\xd4\x17\x87\xfd\xf7\xf0\xc4\v\x85\xa7\x9f\xb3P\x8e\b\x80\x95\x96\x19\xfb!\x00\xa9\xb0r'\xba\xb2\xff1්\xc0\xfb\xcf\x11\x01w*\x80{d\x7f\x04 \x9d\x05\vt\xabןE\x04\nf\xedX%\xde\xee\x83\x00\xe4\xb5\xf7\x9f\x8b\xdeh\xfc!\x02\x0eW\x00ݠ\x926_\xfd\xc0i\xc0^\x83\xbf\x195l\xa8\xef\x06\xfd\xff;\x8b\xa70\n\xc9\xfb\xe6~g\xbd=\xd5\xfa\xaf\x0fX\t*\x80\xa1Ij\xbd\x97\xfeo\xa3vnI\xc1\x89\x17,d\x8e\x84'V\xc8\xfeT\x00\xc5g\xff\xbdl=:û\x02\xf2\xc8lc\x9b\xf3\xef\xfc\xe1?/\xb3\x12T\x00\xc3\xef%kz\x98\xd6\xef\x1a9\xffD\xa6\xbeɢf\x99\xd5F;\xeav&~\xc7J \x00C\xb3r'\xba\xd2o\xe3\xef\xf0\xba4\xd1ȅ\x1f\x10\x81,K\xff\xa9\xf5\xd6\xcc\x1f\xff\xd8f%\x10\x80\xa1K\xff~\xc6~\xfd\x88@\xed\xfc\x13\x99\xd1\x0e\x8b\x9c>\xed\xb0\xb1\xc4\xde\x1f\x01H#P\xf5/\xa9f\xff}ej\xed\xc2\x0f\x88@\xda7\xf4\xf4\x1ag\xfd\xfb\xb9\x0fY\x82\x83\xb3\x7fZ\x8d\xbfC\xfb\v\x9b5u\x1e\x9d\xc1O0\x8d\x9b\x99\xb1\x1f\x15@Z\f;\xf6\xa3\x12(\xe0f\x9e\xd8\xfa\x98U@\x00\x86fu.\xbat\xd0i\xbf\xccD\xe0\xfc\x8f\x18\x8a\fs#\x8fo\xb4&\xff\xed\xbf\x17X\t\x04`\xf8\x804\xfa,\xf7\xbfY\xdf\xc0Uh\b\x01\x1d9\xb9J\xe3\x0f\x01\x18\x9e\xd4\xc7~}\x8a@\r/\x81\xfe\x19\xedܮ\xdf]h\xb3\x10\b\xc0Pd6\xf6\xeb\xe7\x82L\xadb(\xd2\x1f\xedw\xce\xff\x80\xc9'\x020\xff\v\x8d?*\x80\xe1qe\xec\xd7\x0f\xb5\xb3O+\xed%\x10\x9e\xfa?J\x7f*\x80\x94\xb2\xbfcc\xbf\x9e\xe9\x06\xdb\xd6b\xeb\xa3\xd5\xcaV#\xc9\xfc;\xff>\x8f\xcd\x17\x15@\n\xd9\xff\x88\xb7\xfb\xb8\x9d\x06w\\\x85*t\x82Ќv\xd45DZ\xf9B\x00\x86\xa7\u05f7\xfb\xb8.\x02U:F\x8c\xcd\x17\x02\x90ޗv|\xec\xd7OV\xac\x88\b`\xf3\x85\x00\xa4\xb4\xf7\xf7d\xec\x87\b\xfc\x82\x95\xb0\xf9\xca\xea\xfe\xa9T\xf0\xfb\xdc\xf8;*H6k\xda\xfa\xe6\xac\xd4-\x97\xa6\xf3\xbc?\x15@j\xe4e\xf3UT%PFC\x11l\xbe\xa8\x00Ra\xe5\xd3(\n\x12}U\xf6\xefi\u05cfi\xeb\xd1\xe9RT\x02\xc1\xf8F\xeb\x9d\xcf\xff\x82\x00P\x01\xa4\xf0E\xad\xc7c\xbf~\x14\xbd\xbeQ\n/\x013\xdaQwk\x92\xc6\x1f\x02\x90B\xf6/\xd0\xe6\xab\b\xc2\x13/\xfcw\x15\x1a\xed\xdcf\xec\x87\x00\f\x8d\v6_\x85\x89\x80\xbf^\x02\xd8|!\x00)\xed\x89\x03]\xadR\xf6\x7fM\x04<5\x14\t\xde_\xc6\xe6+\xaf\xadVٳ\x7fY\xc7~\xfd\xe0\x93\xb5\x18c?*\x80\xd4(\xf3د\xac\x95\x006_\b@*\xac܉\xae\xe4\xf9v\x1f/D\xc0qW\xa1`|\x83\xe7\xfd\x11\x80\x94\xbeX@\xf6\x7fC\x04\xdcv\x15\xc2\xe6\v\x01H\x87\xb5\xb9\xa8\xb2\x8d\xbf\xa3\xa8\x9d[\x92\x99x\xe9\xde\xde\x1f\x9b\xafbֽl_\xc8G\x9b\xaf\xdcq\xccP\xc4Ԓ\xf6\xd4\x7f\xcc\xd3\xf8\xa3\x02\x18\x1e\x1fm\xbe\xf2\xdf\v\xec\x18\x8a8b-\x16\x9e}J\xe9O\x05\x90R\xf6g\xec\xd73v\xb3\xa6Σ3\xb2\x9bŽ\x1e\x02\x9b/*\x80\x14S\x89\xeesI\xfbP\xff\x82\xbd\x04\xb0\xf9B\x00Rc\xe5Nt\xc5J\x17\xb9\xa4\xfe\x88\x80\x19\xe92\xf6C\x00R\xfa\"\x8c\xfd|\x13\x01\xc6~\b@J{\xff\x92\xd9|\x15&\x02\xe7\x7f\xcc\xcdP\x04\x9b/G\xae\xbb\xf7\xc1ߌ\x1aI\xa8\xaf\x8c4\xcd\xe5L!0s0\x14\xe1\xed>T\x00\xa9\x91\xd4t\x8b\xe0O18\xeb\x1b\x1a9\xffc\xb67\xdd\xd4\x06.?T\x00\xe9d\x7f\xc6~\xd9\xd0}6\xa9\xee\xe2\xa9\xf4o8cZS\x7f\xfa\x13\x02@\x05\x90B\xb9Z\xd3C.a6d\xe1*dF;JFFh\xfc!\x00\xc3S5\x9b\xaf\xc2D \xcdc\xc4\xd8|!\x00\xa9d\xfef4\xcd\xd8/'\x11H\xcfK\x00\x9b/\x04 \x1d^\x86ⴟg\"\x10\x1c\xdf\xc2\xe6\xcbA\xbck\x02\xd2\xf8+\x8eA\xadŰ\xf9\xa2\x02H\rl\xbe\x8a\xad\x04\x061\x14\xc1\xe6\v\x01H\x85չ\xe8\x126_\xc5R;\xb7ԗ\b`\xf3\x85\x00\xa4\xb7_1\xfa\x8cK\xe6\x86\b\xf4\xe4%\x10&\xcb#'W\x19\xfb!\x00\xc3\xc3\xd8\xcf-z1\x141\xf5\xcd{\xf5\xbb\vd\x7f\x97\x93\xaa\x0f\x1f\x12\x9b/G\xe9\x06\xda\xfa\xe6\xecA\x86\"\xed\xe9?\xff\x99\xc6\x1f\x15\xc0\xf0`\xf3\xe5(ar\xe01\xe2Zc\x89ҟ\n \xa5\xec\xcf\xd8\xcfi\xf6[\x8ba\xf3E\x05\x90\xde\xcd\x15\xd2\xf8s>\x8b\xec1\x141a\x82͗G\xd4\\\xfep\xabs\xd1%\x19E\x92\xda\\*\xc7E\xe0XG#\xff\xf0X\x1b\x8b\xa7\xbe\x9c\xf9\xc3<\xd7\v\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00|\xe4\xff\x01\xf6P(\xf3)+S\x1f\x00\x00\x00\x00IEND\xaeB`\x82"), -} diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index 49b0f53cf..51eec59a5 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -1,4 +1,4 @@ -//go:build !(linux && 386) && !freebsd +//go:build !(linux && 386) package main @@ -21,6 +21,7 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/dialog" + "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" "fyne.io/systray" "github.com/cenkalti/backoff/v4" @@ -32,7 +33,8 @@ import ( "github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/proto" - "github.com/netbirdio/netbird/client/system" + "github.com/netbirdio/netbird/client/ui/desktop" + "github.com/netbirdio/netbird/client/ui/event" "github.com/netbirdio/netbird/util" "github.com/netbirdio/netbird/version" ) @@ -82,7 +84,7 @@ func main() { } a := app.NewWithID("NetBird") - a.SetIcon(fyne.NewStaticResource("netbird", iconDisconnectedPNG)) + a.SetIcon(fyne.NewStaticResource("netbird", iconDisconnected)) if errorMSG != "" { showErrorMSG(errorMSG) @@ -90,6 +92,14 @@ func main() { } client := newServiceClient(daemonAddr, a, showSettings, showRoutes) + settingsChangeChan := make(chan fyne.Settings) + a.Settings().AddChangeListener(settingsChangeChan) + go func() { + for range settingsChangeChan { + client.updateIcon() + } + }() + if showSettings || showRoutes { a.Run() } else { @@ -106,63 +116,56 @@ func main() { } } -//go:embed netbird-systemtray-connected.ico -var iconConnectedICO []byte +//go:embed netbird-systemtray-connected-macos.png +var iconConnectedMacOS []byte -//go:embed netbird-systemtray-connected.png -var iconConnectedPNG []byte +//go:embed netbird-systemtray-disconnected-macos.png +var iconDisconnectedMacOS []byte -//go:embed netbird-systemtray-disconnected.ico -var iconDisconnectedICO []byte +//go:embed netbird-systemtray-update-disconnected-macos.png +var iconUpdateDisconnectedMacOS []byte -//go:embed netbird-systemtray-disconnected.png -var iconDisconnectedPNG []byte +//go:embed netbird-systemtray-update-connected-macos.png +var iconUpdateConnectedMacOS []byte -//go:embed netbird-systemtray-update-disconnected.ico -var iconUpdateDisconnectedICO []byte +//go:embed netbird-systemtray-connecting-macos.png +var iconConnectingMacOS []byte -//go:embed netbird-systemtray-update-disconnected.png -var iconUpdateDisconnectedPNG []byte - -//go:embed netbird-systemtray-update-connected.ico -var iconUpdateConnectedICO []byte - -//go:embed netbird-systemtray-update-connected.png -var iconUpdateConnectedPNG []byte - -//go:embed netbird-systemtray-update-cloud.ico -var iconUpdateCloudICO []byte - -//go:embed netbird-systemtray-update-cloud.png -var iconUpdateCloudPNG []byte +//go:embed netbird-systemtray-error-macos.png +var iconErrorMacOS []byte type serviceClient struct { ctx context.Context addr string conn proto.DaemonServiceClient + icAbout []byte icConnected []byte icDisconnected []byte icUpdateConnected []byte icUpdateDisconnected []byte - icUpdateCloud []byte + icConnecting []byte + icError []byte // systray menu items - mStatus *systray.MenuItem - mUp *systray.MenuItem - mDown *systray.MenuItem - mAdminPanel *systray.MenuItem - mSettings *systray.MenuItem - mAbout *systray.MenuItem - mVersionUI *systray.MenuItem - mVersionDaemon *systray.MenuItem - mUpdate *systray.MenuItem - mQuit *systray.MenuItem - mRoutes *systray.MenuItem - mAllowSSH *systray.MenuItem - mAutoConnect *systray.MenuItem - mEnableRosenpass *systray.MenuItem - mAdvancedSettings *systray.MenuItem + mStatus *systray.MenuItem + mUp *systray.MenuItem + mDown *systray.MenuItem + mAdminPanel *systray.MenuItem + mSettings *systray.MenuItem + mAbout *systray.MenuItem + mVersionUI *systray.MenuItem + mVersionDaemon *systray.MenuItem + mUpdate *systray.MenuItem + mQuit *systray.MenuItem + mNetworks *systray.MenuItem + mAllowSSH *systray.MenuItem + mAutoConnect *systray.MenuItem + mEnableRosenpass *systray.MenuItem + mNotifications *systray.MenuItem + mAdvancedSettings *systray.MenuItem + mCreateDebugBundle *systray.MenuItem + mExitNode *systray.MenuItem // application with main windows. app fyne.App @@ -197,6 +200,16 @@ type serviceClient struct { isUpdateIconActive bool showRoutes bool wRoutes fyne.Window + + eventManager *event.Manager + + exitNodeMu sync.Mutex + mExitNodeItems []menuHandler +} + +type menuHandler struct { + *systray.MenuItem + cancel context.CancelFunc } // newServiceClient instance constructor @@ -214,20 +227,7 @@ func newServiceClient(addr string, a fyne.App, showSettings bool, showRoutes boo update: version.NewUpdate(), } - if runtime.GOOS == "windows" { - s.icConnected = iconConnectedICO - s.icDisconnected = iconDisconnectedICO - s.icUpdateConnected = iconUpdateConnectedICO - s.icUpdateDisconnected = iconUpdateDisconnectedICO - s.icUpdateCloud = iconUpdateCloudICO - - } else { - s.icConnected = iconConnectedPNG - s.icDisconnected = iconDisconnectedPNG - s.icUpdateConnected = iconUpdateConnectedPNG - s.icUpdateDisconnected = iconUpdateDisconnectedPNG - s.icUpdateCloud = iconUpdateCloudPNG - } + s.setNewIcons() if showSettings { s.showSettingsUI() @@ -239,6 +239,44 @@ func newServiceClient(addr string, a fyne.App, showSettings bool, showRoutes boo return s } +func (s *serviceClient) setNewIcons() { + s.icAbout = iconAbout + if s.app.Settings().ThemeVariant() == theme.VariantDark { + s.icConnected = iconConnectedDark + s.icDisconnected = iconDisconnected + s.icUpdateConnected = iconUpdateConnectedDark + s.icUpdateDisconnected = iconUpdateDisconnectedDark + s.icConnecting = iconConnectingDark + s.icError = iconErrorDark + } else { + s.icConnected = iconConnected + s.icDisconnected = iconDisconnected + s.icUpdateConnected = iconUpdateConnected + s.icUpdateDisconnected = iconUpdateDisconnected + s.icConnecting = iconConnecting + s.icError = iconError + } +} + +func (s *serviceClient) updateIcon() { + s.setNewIcons() + s.updateIndicationLock.Lock() + if s.connected { + if s.isUpdateIconActive { + systray.SetTemplateIcon(iconUpdateConnectedMacOS, s.icUpdateConnected) + } else { + systray.SetTemplateIcon(iconConnectedMacOS, s.icConnected) + } + } else { + if s.isUpdateIconActive { + systray.SetTemplateIcon(iconUpdateDisconnectedMacOS, s.icUpdateDisconnected) + } else { + systray.SetTemplateIcon(iconDisconnectedMacOS, s.icDisconnected) + } + } + s.updateIndicationLock.Unlock() +} + func (s *serviceClient) showSettingsUI() { // add settings window UI elements. s.wSettings = s.app.NewWindow("NetBird Settings") @@ -376,8 +414,10 @@ func (s *serviceClient) login() error { } func (s *serviceClient) menuUpClick() error { + systray.SetTemplateIcon(iconConnectingMacOS, s.icConnecting) conn, err := s.getSrvClient(defaultFailTimeout) if err != nil { + systray.SetTemplateIcon(iconErrorMacOS, s.icError) log.Errorf("get client: %v", err) return err } @@ -403,10 +443,12 @@ func (s *serviceClient) menuUpClick() error { log.Errorf("up service: %v", err) return err } + return nil } func (s *serviceClient) menuDownClick() error { + systray.SetTemplateIcon(iconConnectingMacOS, s.icConnecting) conn, err := s.getSrvClient(defaultFailTimeout) if err != nil { log.Errorf("get client: %v", err) @@ -441,6 +483,9 @@ func (s *serviceClient) updateStatus() error { status, err := conn.Status(s.ctx, &proto.StatusRequest{}) if err != nil { log.Errorf("get service status: %v", err) + if s.connected { + s.app.SendNotification(fyne.NewNotification("Error", "Connection to service lost")) + } s.setDisconnectedStatus() return err } @@ -458,15 +503,16 @@ func (s *serviceClient) updateStatus() error { s.connected = true s.sendNotification = true if s.isUpdateIconActive { - systray.SetIcon(s.icUpdateConnected) + systray.SetTemplateIcon(iconUpdateConnectedMacOS, s.icUpdateConnected) } else { - systray.SetIcon(s.icConnected) + systray.SetTemplateIcon(iconConnectedMacOS, s.icConnected) } systray.SetTooltip("NetBird (Connected)") s.mStatus.SetTitle("Connected") s.mUp.Disable() s.mDown.Enable() - s.mRoutes.Enable() + s.mNetworks.Enable() + go s.updateExitNodes() systrayIconState = true } else if status.Status != string(internal.StatusConnected) && s.mUp.Disabled() { s.setDisconnectedStatus() @@ -482,11 +528,9 @@ func (s *serviceClient) updateStatus() error { s.isUpdateIconActive = s.update.SetDaemonVersion(status.DaemonVersion) if !s.isUpdateIconActive { if systrayIconState { - systray.SetIcon(s.icConnected) - s.mAbout.SetIcon(s.icConnected) + systray.SetTemplateIcon(iconConnectedMacOS, s.icConnected) } else { - systray.SetIcon(s.icDisconnected) - s.mAbout.SetIcon(s.icDisconnected) + systray.SetTemplateIcon(iconDisconnectedMacOS, s.icDisconnected) } } @@ -506,7 +550,6 @@ func (s *serviceClient) updateStatus() error { Stop: backoff.Stop, Clock: backoff.SystemClock, }) - if err != nil { return err } @@ -517,19 +560,21 @@ func (s *serviceClient) updateStatus() error { func (s *serviceClient) setDisconnectedStatus() { s.connected = false if s.isUpdateIconActive { - systray.SetIcon(s.icUpdateDisconnected) + systray.SetTemplateIcon(iconUpdateDisconnectedMacOS, s.icUpdateDisconnected) } else { - systray.SetIcon(s.icDisconnected) + systray.SetTemplateIcon(iconDisconnectedMacOS, s.icDisconnected) } systray.SetTooltip("NetBird (Disconnected)") s.mStatus.SetTitle("Disconnected") s.mDown.Disable() s.mUp.Enable() - s.mRoutes.Disable() + s.mNetworks.Disable() + s.mExitNode.Disable() + go s.updateExitNodes() } func (s *serviceClient) onTrayReady() { - systray.SetIcon(s.icDisconnected) + systray.SetTemplateIcon(iconDisconnectedMacOS, s.icDisconnected) systray.SetTooltip("NetBird") // setup systray menu items @@ -546,15 +591,22 @@ func (s *serviceClient) onTrayReady() { s.mAllowSSH = s.mSettings.AddSubMenuItemCheckbox("Allow SSH", "Allow SSH connections", false) s.mAutoConnect = s.mSettings.AddSubMenuItemCheckbox("Connect on Startup", "Connect automatically when the service starts", false) s.mEnableRosenpass = s.mSettings.AddSubMenuItemCheckbox("Enable Quantum-Resistance", "Enable post-quantum security via Rosenpass", false) + s.mNotifications = s.mSettings.AddSubMenuItemCheckbox("Notifications", "Enable notifications", false) s.mAdvancedSettings = s.mSettings.AddSubMenuItem("Advanced Settings", "Advanced settings of the application") + s.mCreateDebugBundle = s.mSettings.AddSubMenuItem("Create Debug Bundle", "Create and open debug information bundle") s.loadSettings() - s.mRoutes = systray.AddMenuItem("Networks", "Open the networks management window") - s.mRoutes.Disable() + s.exitNodeMu.Lock() + s.mExitNode = systray.AddMenuItem("Exit Node", "Select exit node for routing traffic") + s.mExitNode.Disable() + s.exitNodeMu.Unlock() + + s.mNetworks = systray.AddMenuItem("Networks", "Open the networks management window") + s.mNetworks.Disable() systray.AddSeparator() s.mAbout = systray.AddMenuItem("About", "About") - s.mAbout.SetIcon(s.icDisconnected) + s.mAbout.SetIcon(s.icAbout) versionString := normalizedVersion(version.NetbirdVersion()) s.mVersionUI = s.mAbout.AddSubMenuItem(fmt.Sprintf("GUI: %s", versionString), fmt.Sprintf("GUI Version: %s", versionString)) s.mVersionUI.Disable() @@ -569,6 +621,9 @@ func (s *serviceClient) onTrayReady() { systray.AddSeparator() s.mQuit = systray.AddMenuItem("Quit", "Quit the client app") + // update exit node menu in case service is already connected + go s.updateExitNodes() + s.update.SetOnUpdateListener(s.onUpdateAvailable) go func() { s.getSrvConfig() @@ -582,6 +637,16 @@ func (s *serviceClient) onTrayReady() { } }() + s.eventManager = event.NewManager(s.app, s.addr) + s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked()) + s.eventManager.AddHandler(func(event *proto.SystemEvent) { + if event.Category == proto.SystemEvent_SYSTEM { + s.updateExitNodes() + } + }) + + go s.eventManager.Start(s.ctx) + go func() { var err error for { @@ -594,7 +659,7 @@ func (s *serviceClient) onTrayReady() { defer s.mUp.Enable() err := s.menuUpClick() if err != nil { - s.runSelfCommand("error-msg", err.Error()) + s.app.SendNotification(fyne.NewNotification("Error", "Failed to connect to NetBird service")) return } }() @@ -604,7 +669,7 @@ func (s *serviceClient) onTrayReady() { defer s.mDown.Enable() err := s.menuDownClick() if err != nil { - s.runSelfCommand("error-msg", err.Error()) + s.app.SendNotification(fyne.NewNotification("Error", "Failed to connect to NetBird service")) return } }() @@ -616,7 +681,6 @@ func (s *serviceClient) onTrayReady() { } if err := s.updateConfig(); err != nil { log.Errorf("failed to update config: %v", err) - return } case <-s.mAutoConnect.ClickedCh: if s.mAutoConnect.Checked() { @@ -626,7 +690,6 @@ func (s *serviceClient) onTrayReady() { } if err := s.updateConfig(); err != nil { log.Errorf("failed to update config: %v", err) - return } case <-s.mEnableRosenpass.ClickedCh: if s.mEnableRosenpass.Checked() { @@ -636,7 +699,6 @@ func (s *serviceClient) onTrayReady() { } if err := s.updateConfig(); err != nil { log.Errorf("failed to update config: %v", err) - return } case <-s.mAdvancedSettings.ClickedCh: s.mAdvancedSettings.Disable() @@ -645,6 +707,13 @@ func (s *serviceClient) onTrayReady() { defer s.getSrvConfig() s.runSelfCommand("settings", "true") }() + case <-s.mCreateDebugBundle.ClickedCh: + go func() { + if err := s.createAndOpenDebugBundle(); err != nil { + log.Errorf("Failed to create debug bundle: %v", err) + s.app.SendNotification(fyne.NewNotification("Error", "Failed to create debug bundle")) + } + }() case <-s.mQuit.ClickedCh: systray.Quit() return @@ -653,13 +722,26 @@ func (s *serviceClient) onTrayReady() { if err != nil { log.Errorf("%s", err) } - case <-s.mRoutes.ClickedCh: - s.mRoutes.Disable() + case <-s.mNetworks.ClickedCh: + s.mNetworks.Disable() go func() { - defer s.mRoutes.Enable() + defer s.mNetworks.Enable() s.runSelfCommand("networks", "true") }() + case <-s.mNotifications.ClickedCh: + if s.mNotifications.Checked() { + s.mNotifications.Uncheck() + } else { + s.mNotifications.Check() + } + if s.eventManager != nil { + s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked()) + } + if err := s.updateConfig(); err != nil { + log.Errorf("failed to update config: %v", err) + } } + if err != nil { log.Errorf("process connection: %v", err) } @@ -674,7 +756,11 @@ func (s *serviceClient) runSelfCommand(command, arg string) { return } - cmd := exec.Command(proc, fmt.Sprintf("--%s=%s", command, arg)) + cmd := exec.Command(proc, + fmt.Sprintf("--%s=%s", command, arg), + fmt.Sprintf("--daemon-addr=%s", s.addr), + ) + out, err := cmd.CombinedOutput() if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 { log.Errorf("start %s UI: %v, %s", command, err, string(out)) @@ -693,7 +779,12 @@ func normalizedVersion(version string) string { return versionString } -func (s *serviceClient) onTrayExit() {} +// onTrayExit is called when the tray icon is closed. +func (s *serviceClient) onTrayExit() { + for _, item := range s.mExitNodeItems { + item.cancel() + } +} // getSrvClient connection to the service. func (s *serviceClient) getSrvClient(timeout time.Duration) (proto.DaemonServiceClient, error) { @@ -709,7 +800,7 @@ func (s *serviceClient) getSrvClient(timeout time.Duration) (proto.DaemonService strings.TrimPrefix(s.addr, "tcp://"), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock(), - grpc.WithUserAgent(system.GetDesktopUIUserAgent()), + grpc.WithUserAgent(desktop.GetUIUserAgent()), ) if err != nil { return nil, fmt.Errorf("dial service: %w", err) @@ -759,8 +850,20 @@ func (s *serviceClient) getSrvConfig() { if !cfg.RosenpassEnabled { s.sRosenpassPermissive.Disable() } - } + + if s.mNotifications == nil { + return + } + if cfg.DisableNotifications { + s.mNotifications.Uncheck() + } else { + s.mNotifications.Check() + } + if s.eventManager != nil { + s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked()) + } + } func (s *serviceClient) onUpdateAvailable() { @@ -771,9 +874,9 @@ func (s *serviceClient) onUpdateAvailable() { s.isUpdateIconActive = true if s.connected { - systray.SetIcon(s.icUpdateConnected) + systray.SetTemplateIcon(iconUpdateConnectedMacOS, s.icUpdateConnected) } else { - systray.SetIcon(s.icUpdateDisconnected) + systray.SetTemplateIcon(iconUpdateDisconnectedMacOS, s.icUpdateDisconnected) } } @@ -825,6 +928,15 @@ func (s *serviceClient) loadSettings() { } else { s.mEnableRosenpass.Uncheck() } + + if cfg.DisableNotifications { + s.mNotifications.Uncheck() + } else { + s.mNotifications.Check() + } + if s.eventManager != nil { + s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked()) + } } // updateConfig updates the configuration parameters @@ -833,12 +945,14 @@ func (s *serviceClient) updateConfig() error { disableAutoStart := !s.mAutoConnect.Checked() sshAllowed := s.mAllowSSH.Checked() rosenpassEnabled := s.mEnableRosenpass.Checked() + notificationsDisabled := !s.mNotifications.Checked() loginRequest := proto.LoginRequest{ IsLinuxDesktopClient: runtime.GOOS == "linux", ServerSSHAllowed: &sshAllowed, RosenpassEnabled: &rosenpassEnabled, DisableAutoConnect: &disableAutoStart, + DisableNotifications: ¬ificationsDisabled, } if err := s.restartClient(&loginRequest); err != nil { @@ -851,17 +965,20 @@ func (s *serviceClient) updateConfig() error { // restartClient restarts the client connection. func (s *serviceClient) restartClient(loginRequest *proto.LoginRequest) error { + ctx, cancel := context.WithTimeout(s.ctx, defaultFailTimeout) + defer cancel() + client, err := s.getSrvClient(failFastTimeout) if err != nil { return err } - _, err = client.Login(s.ctx, loginRequest) + _, err = client.Login(ctx, loginRequest) if err != nil { return err } - _, err = client.Up(s.ctx, &proto.UpRequest{}) + _, err = client.Up(ctx, &proto.UpRequest{}) if err != nil { return err } @@ -876,7 +993,7 @@ func openURL(url string) error { err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() case "darwin": err = exec.Command("open", url).Start() - case "linux": + case "linux", "freebsd": err = exec.Command("xdg-open", url).Start() default: err = fmt.Errorf("unsupported platform") diff --git a/client/ui/config/config.go b/client/ui/config/config.go deleted file mode 100644 index fc3361b61..000000000 --- a/client/ui/config/config.go +++ /dev/null @@ -1,46 +0,0 @@ -package config - -import ( - "os" - "runtime" -) - -// ClientConfig basic settings for the UI application. -type ClientConfig struct { - configPath string - logFile string - daemonAddr string -} - -// Config object with default settings. -// -// We are creating this package to extract utility functions from the cmd package -// reading and parsing the configurations for the client should be done here -func Config() *ClientConfig { - defaultConfigPath := "/etc/wiretrustee/config.json" - defaultLogFile := "/var/log/wiretrustee/client.log" - if runtime.GOOS == "windows" { - defaultConfigPath = os.Getenv("PROGRAMDATA") + "\\Wiretrustee\\" + "config.json" - defaultLogFile = os.Getenv("PROGRAMDATA") + "\\Wiretrustee\\" + "client.log" - } - - defaultDaemonAddr := "unix:///var/run/wiretrustee.sock" - if runtime.GOOS == "windows" { - defaultDaemonAddr = "tcp://127.0.0.1:41731" - } - return &ClientConfig{ - configPath: defaultConfigPath, - logFile: defaultLogFile, - daemonAddr: defaultDaemonAddr, - } -} - -// DaemonAddr of the gRPC API. -func (c *ClientConfig) DaemonAddr() string { - return c.daemonAddr -} - -// LogFile path. -func (c *ClientConfig) LogFile() string { - return c.logFile -} diff --git a/client/ui/debug.go b/client/ui/debug.go new file mode 100644 index 000000000..845ea284c --- /dev/null +++ b/client/ui/debug.go @@ -0,0 +1,50 @@ +//go:build !(linux && 386) + +package main + +import ( + "fmt" + "path/filepath" + + "fyne.io/fyne/v2" + "github.com/skratchdot/open-golang/open" + + "github.com/netbirdio/netbird/client/proto" + nbstatus "github.com/netbirdio/netbird/client/status" +) + +func (s *serviceClient) createAndOpenDebugBundle() error { + conn, err := s.getSrvClient(failFastTimeout) + if err != nil { + return fmt.Errorf("get client: %v", err) + } + + statusResp, err := conn.Status(s.ctx, &proto.StatusRequest{GetFullPeerStatus: true}) + if err != nil { + return fmt.Errorf("failed to get status: %v", err) + } + + overview := nbstatus.ConvertToStatusOutputOverview(statusResp, true, "", nil, nil, nil) + statusOutput := nbstatus.ParseToFullDetailSummary(overview) + + resp, err := conn.DebugBundle(s.ctx, &proto.DebugBundleRequest{ + Anonymize: true, + Status: statusOutput, + SystemInfo: true, + }) + if err != nil { + return fmt.Errorf("failed to create debug bundle: %v", err) + } + + bundleDir := filepath.Dir(resp.GetPath()) + if err := open.Start(bundleDir); err != nil { + return fmt.Errorf("failed to open debug bundle directory: %v", err) + } + + s.app.SendNotification(fyne.NewNotification( + "Debug Bundle", + fmt.Sprintf("Debug bundle created at %s. Administrator privileges are required to access it.", resp.GetPath()), + )) + + return nil +} diff --git a/client/ui/desktop/desktop.go b/client/ui/desktop/desktop.go new file mode 100644 index 000000000..0c99e2f38 --- /dev/null +++ b/client/ui/desktop/desktop.go @@ -0,0 +1,8 @@ +package desktop + +import "github.com/netbirdio/netbird/version" + +// GetUIUserAgent returns the Desktop ui user agent +func GetUIUserAgent() string { + return "netbird-desktop-ui/" + version.NetbirdVersion() +} diff --git a/client/ui/event/event.go b/client/ui/event/event.go new file mode 100644 index 000000000..4d949416d --- /dev/null +++ b/client/ui/event/event.go @@ -0,0 +1,176 @@ +package event + +import ( + "context" + "fmt" + "slices" + "strings" + "sync" + "time" + + "fyne.io/fyne/v2" + "github.com/cenkalti/backoff/v4" + log "github.com/sirupsen/logrus" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "github.com/netbirdio/netbird/client/proto" + "github.com/netbirdio/netbird/client/ui/desktop" +) + +type Handler func(*proto.SystemEvent) + +type Manager struct { + app fyne.App + addr string + + mu sync.Mutex + ctx context.Context + cancel context.CancelFunc + enabled bool + handlers []Handler +} + +func NewManager(app fyne.App, addr string) *Manager { + return &Manager{ + app: app, + addr: addr, + } +} + +func (e *Manager) Start(ctx context.Context) { + e.mu.Lock() + e.ctx, e.cancel = context.WithCancel(ctx) + e.mu.Unlock() + + expBackOff := backoff.WithContext(&backoff.ExponentialBackOff{ + InitialInterval: time.Second, + RandomizationFactor: backoff.DefaultRandomizationFactor, + Multiplier: backoff.DefaultMultiplier, + MaxInterval: 10 * time.Second, + MaxElapsedTime: 0, + Stop: backoff.Stop, + Clock: backoff.SystemClock, + }, ctx) + + if err := backoff.Retry(e.streamEvents, expBackOff); err != nil { + log.Errorf("event stream ended: %v", err) + } +} + +func (e *Manager) streamEvents() error { + e.mu.Lock() + ctx := e.ctx + e.mu.Unlock() + + client, err := getClient(e.addr) + if err != nil { + return fmt.Errorf("create client: %w", err) + } + + stream, err := client.SubscribeEvents(ctx, &proto.SubscribeRequest{}) + if err != nil { + return fmt.Errorf("failed to subscribe to events: %w", err) + } + + log.Info("subscribed to daemon events") + defer func() { + log.Info("unsubscribed from daemon events") + }() + + for { + event, err := stream.Recv() + if err != nil { + return fmt.Errorf("error receiving event: %w", err) + } + e.handleEvent(event) + } +} + +func (e *Manager) Stop() { + e.mu.Lock() + defer e.mu.Unlock() + if e.cancel != nil { + e.cancel() + } +} + +func (e *Manager) SetNotificationsEnabled(enabled bool) { + e.mu.Lock() + defer e.mu.Unlock() + e.enabled = enabled +} + +func (e *Manager) handleEvent(event *proto.SystemEvent) { + e.mu.Lock() + enabled := e.enabled + handlers := slices.Clone(e.handlers) + e.mu.Unlock() + + // critical events are always shown + if !enabled && event.Severity != proto.SystemEvent_CRITICAL { + return + } + + if event.UserMessage != "" { + title := e.getEventTitle(event) + body := event.UserMessage + id := event.Metadata["id"] + if id != "" { + body += fmt.Sprintf(" ID: %s", id) + } + e.app.SendNotification(fyne.NewNotification(title, body)) + } + + for _, handler := range handlers { + go handler(event) + } +} + +func (e *Manager) AddHandler(handler Handler) { + e.mu.Lock() + defer e.mu.Unlock() + e.handlers = append(e.handlers, handler) +} + +func (e *Manager) getEventTitle(event *proto.SystemEvent) string { + var prefix string + switch event.Severity { + case proto.SystemEvent_CRITICAL: + prefix = "Critical" + case proto.SystemEvent_ERROR: + prefix = "Error" + case proto.SystemEvent_WARNING: + prefix = "Warning" + default: + prefix = "Info" + } + + var category string + switch event.Category { + case proto.SystemEvent_DNS: + category = "DNS" + case proto.SystemEvent_NETWORK: + category = "Network" + case proto.SystemEvent_AUTHENTICATION: + category = "Authentication" + case proto.SystemEvent_CONNECTIVITY: + category = "Connectivity" + default: + category = "System" + } + + return fmt.Sprintf("%s: %s", prefix, category) +} + +func getClient(addr string) (proto.DaemonServiceClient, error) { + conn, err := grpc.NewClient( + strings.TrimPrefix(addr, "tcp://"), + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithUserAgent(desktop.GetUIUserAgent()), + ) + if err != nil { + return nil, err + } + return proto.NewDaemonServiceClient(conn), nil +} diff --git a/client/ui/font_bsd.go b/client/ui/font_bsd.go index 84cb5993d..139f38f40 100644 --- a/client/ui/font_bsd.go +++ b/client/ui/font_bsd.go @@ -1,4 +1,4 @@ -//go:build darwin +//go:build freebsd || openbsd || netbsd || dragonfly package main @@ -9,18 +9,22 @@ import ( log "github.com/sirupsen/logrus" ) -const defaultFontPath = "/Library/Fonts/Arial Unicode.ttf" - func (s *serviceClient) setDefaultFonts() { - // TODO: add other bsd paths - if runtime.GOOS != "darwin" { - return + paths := []string{ + "/usr/local/share/fonts/TTF/DejaVuSans.ttf", + "/usr/local/share/fonts/dejavu/DejaVuSans.ttf", + "/usr/local/share/noto/NotoSans-Regular.ttf", + "/usr/local/share/fonts/noto/NotoSans-Regular.ttf", + "/usr/local/share/fonts/liberation-fonts-ttf/LiberationSans-Regular.ttf", } - if _, err := os.Stat(defaultFontPath); err != nil { - log.Errorf("Failed to find default font file: %v", err) - return + for _, fontPath := range paths { + if _, err := os.Stat(fontPath); err == nil { + os.Setenv("FYNE_FONT", fontPath) + log.Debugf("Using font: %s", fontPath) + return + } } - os.Setenv("FYNE_FONT", defaultFontPath) + log.Errorf("Failed to find any suitable font files for %s", runtime.GOOS) } diff --git a/client/ui/font_darwin.go b/client/ui/font_darwin.go new file mode 100644 index 000000000..cafb72f59 --- /dev/null +++ b/client/ui/font_darwin.go @@ -0,0 +1,18 @@ +package main + +import ( + "os" + + log "github.com/sirupsen/logrus" +) + +const defaultFontPath = "/Library/Fonts/Arial Unicode.ttf" + +func (s *serviceClient) setDefaultFonts() { + if _, err := os.Stat(defaultFontPath); err != nil { + log.Errorf("Failed to find default font file: %v", err) + return + } + + os.Setenv("FYNE_FONT", defaultFontPath) +} diff --git a/client/ui/icons.go b/client/ui/icons.go new file mode 100644 index 000000000..6f3a9dbc9 --- /dev/null +++ b/client/ui/icons.go @@ -0,0 +1,43 @@ +//go:build !(linux && 386) && !windows + +package main + +import ( + _ "embed" +) + +//go:embed netbird.png +var iconAbout []byte + +//go:embed netbird-systemtray-connected.png +var iconConnected []byte + +//go:embed netbird-systemtray-connected-dark.png +var iconConnectedDark []byte + +//go:embed netbird-systemtray-disconnected.png +var iconDisconnected []byte + +//go:embed netbird-systemtray-update-disconnected.png +var iconUpdateDisconnected []byte + +//go:embed netbird-systemtray-update-disconnected-dark.png +var iconUpdateDisconnectedDark []byte + +//go:embed netbird-systemtray-update-connected.png +var iconUpdateConnected []byte + +//go:embed netbird-systemtray-update-connected-dark.png +var iconUpdateConnectedDark []byte + +//go:embed netbird-systemtray-connecting.png +var iconConnecting []byte + +//go:embed netbird-systemtray-connecting-dark.png +var iconConnectingDark []byte + +//go:embed netbird-systemtray-error.png +var iconError []byte + +//go:embed netbird-systemtray-error-dark.png +var iconErrorDark []byte diff --git a/client/ui/icons_windows.go b/client/ui/icons_windows.go new file mode 100644 index 000000000..a2a924763 --- /dev/null +++ b/client/ui/icons_windows.go @@ -0,0 +1,41 @@ +package main + +import ( + _ "embed" +) + +//go:embed netbird.ico +var iconAbout []byte + +//go:embed netbird-systemtray-connected.ico +var iconConnected []byte + +//go:embed netbird-systemtray-connected-dark.ico +var iconConnectedDark []byte + +//go:embed netbird-systemtray-disconnected.ico +var iconDisconnected []byte + +//go:embed netbird-systemtray-update-disconnected.ico +var iconUpdateDisconnected []byte + +//go:embed netbird-systemtray-update-disconnected-dark.ico +var iconUpdateDisconnectedDark []byte + +//go:embed netbird-systemtray-update-connected.ico +var iconUpdateConnected []byte + +//go:embed netbird-systemtray-update-connected-dark.ico +var iconUpdateConnectedDark []byte + +//go:embed netbird-systemtray-connecting.ico +var iconConnecting []byte + +//go:embed netbird-systemtray-connecting-dark.ico +var iconConnectingDark []byte + +//go:embed netbird-systemtray-error.ico +var iconError []byte + +//go:embed netbird-systemtray-error-dark.ico +var iconErrorDark []byte diff --git a/client/ui/netbird-systemtray-connected-dark.ico b/client/ui/netbird-systemtray-connected-dark.ico new file mode 100644 index 000000000..0db8a0862 Binary files /dev/null and b/client/ui/netbird-systemtray-connected-dark.ico differ diff --git a/client/ui/netbird-systemtray-connected-dark.png b/client/ui/netbird-systemtray-connected-dark.png new file mode 100644 index 000000000..f18a929a0 Binary files /dev/null and b/client/ui/netbird-systemtray-connected-dark.png differ diff --git a/client/ui/netbird-systemtray-connected-macos.png b/client/ui/netbird-systemtray-connected-macos.png new file mode 100644 index 000000000..ead210250 Binary files /dev/null and b/client/ui/netbird-systemtray-connected-macos.png differ diff --git a/client/ui/netbird-systemtray-connected.ico b/client/ui/netbird-systemtray-connected.ico index 80550aa37..c16bec3f5 100644 Binary files a/client/ui/netbird-systemtray-connected.ico and b/client/ui/netbird-systemtray-connected.ico differ diff --git a/client/ui/netbird-systemtray-connected.png b/client/ui/netbird-systemtray-connected.png index f4d156da8..4258a5c1c 100644 Binary files a/client/ui/netbird-systemtray-connected.png and b/client/ui/netbird-systemtray-connected.png differ diff --git a/client/ui/netbird-systemtray-connecting-dark.ico b/client/ui/netbird-systemtray-connecting-dark.ico new file mode 100644 index 000000000..615d40f07 Binary files /dev/null and b/client/ui/netbird-systemtray-connecting-dark.ico differ diff --git a/client/ui/netbird-systemtray-connecting-dark.png b/client/ui/netbird-systemtray-connecting-dark.png new file mode 100644 index 000000000..a665eb61c Binary files /dev/null and b/client/ui/netbird-systemtray-connecting-dark.png differ diff --git a/client/ui/netbird-systemtray-connecting-macos.png b/client/ui/netbird-systemtray-connecting-macos.png new file mode 100644 index 000000000..0fe7fa0db Binary files /dev/null and b/client/ui/netbird-systemtray-connecting-macos.png differ diff --git a/client/ui/netbird-systemtray-connecting.ico b/client/ui/netbird-systemtray-connecting.ico new file mode 100644 index 000000000..4e4c3a9b1 Binary files /dev/null and b/client/ui/netbird-systemtray-connecting.ico differ diff --git a/client/ui/netbird-systemtray-connecting.png b/client/ui/netbird-systemtray-connecting.png new file mode 100644 index 000000000..4f607c997 Binary files /dev/null and b/client/ui/netbird-systemtray-connecting.png differ diff --git a/client/ui/netbird-systemtray-disconnected-macos.png b/client/ui/netbird-systemtray-disconnected-macos.png new file mode 100644 index 000000000..36b9a488f Binary files /dev/null and b/client/ui/netbird-systemtray-disconnected-macos.png differ diff --git a/client/ui/netbird-systemtray-disconnected.ico b/client/ui/netbird-systemtray-disconnected.ico index aa75268b0..dcb9f4bf8 100644 Binary files a/client/ui/netbird-systemtray-disconnected.ico and b/client/ui/netbird-systemtray-disconnected.ico differ diff --git a/client/ui/netbird-systemtray-disconnected.png b/client/ui/netbird-systemtray-disconnected.png index 3aae73231..a92e9ed4c 100644 Binary files a/client/ui/netbird-systemtray-disconnected.png and b/client/ui/netbird-systemtray-disconnected.png differ diff --git a/client/ui/netbird-systemtray-error-dark.ico b/client/ui/netbird-systemtray-error-dark.ico new file mode 100644 index 000000000..083816188 Binary files /dev/null and b/client/ui/netbird-systemtray-error-dark.ico differ diff --git a/client/ui/netbird-systemtray-error-dark.png b/client/ui/netbird-systemtray-error-dark.png new file mode 100644 index 000000000..969554b16 Binary files /dev/null and b/client/ui/netbird-systemtray-error-dark.png differ diff --git a/client/ui/netbird-systemtray-error-macos.png b/client/ui/netbird-systemtray-error-macos.png new file mode 100644 index 000000000..9a9998bcf Binary files /dev/null and b/client/ui/netbird-systemtray-error-macos.png differ diff --git a/client/ui/netbird-systemtray-error.ico b/client/ui/netbird-systemtray-error.ico new file mode 100644 index 000000000..1abc45c2a Binary files /dev/null and b/client/ui/netbird-systemtray-error.ico differ diff --git a/client/ui/netbird-systemtray-error.png b/client/ui/netbird-systemtray-error.png new file mode 100644 index 000000000..722342989 Binary files /dev/null and b/client/ui/netbird-systemtray-error.png differ diff --git a/client/ui/netbird-systemtray-update-cloud.ico b/client/ui/netbird-systemtray-update-cloud.ico deleted file mode 100644 index b87c6f4b5..000000000 Binary files a/client/ui/netbird-systemtray-update-cloud.ico and /dev/null differ diff --git a/client/ui/netbird-systemtray-update-cloud.png b/client/ui/netbird-systemtray-update-cloud.png deleted file mode 100644 index e9d0b8035..000000000 Binary files a/client/ui/netbird-systemtray-update-cloud.png and /dev/null differ diff --git a/client/ui/netbird-systemtray-update-connected-dark.ico b/client/ui/netbird-systemtray-update-connected-dark.ico new file mode 100644 index 000000000..b11bb5492 Binary files /dev/null and b/client/ui/netbird-systemtray-update-connected-dark.ico differ diff --git a/client/ui/netbird-systemtray-update-connected-dark.png b/client/ui/netbird-systemtray-update-connected-dark.png new file mode 100644 index 000000000..52ae621ac Binary files /dev/null and b/client/ui/netbird-systemtray-update-connected-dark.png differ diff --git a/client/ui/netbird-systemtray-update-connected-macos.png b/client/ui/netbird-systemtray-update-connected-macos.png new file mode 100644 index 000000000..8a6b2f2db Binary files /dev/null and b/client/ui/netbird-systemtray-update-connected-macos.png differ diff --git a/client/ui/netbird-systemtray-update-connected.ico b/client/ui/netbird-systemtray-update-connected.ico index cc056e68e..d3ce2f0f3 100644 Binary files a/client/ui/netbird-systemtray-update-connected.ico and b/client/ui/netbird-systemtray-update-connected.ico differ diff --git a/client/ui/netbird-systemtray-update-connected.png b/client/ui/netbird-systemtray-update-connected.png index a0c453340..90bb0b7f1 100644 Binary files a/client/ui/netbird-systemtray-update-connected.png and b/client/ui/netbird-systemtray-update-connected.png differ diff --git a/client/ui/netbird-systemtray-update-disconnected-dark.ico b/client/ui/netbird-systemtray-update-disconnected-dark.ico new file mode 100644 index 000000000..123237f66 Binary files /dev/null and b/client/ui/netbird-systemtray-update-disconnected-dark.ico differ diff --git a/client/ui/netbird-systemtray-update-disconnected-dark.png b/client/ui/netbird-systemtray-update-disconnected-dark.png new file mode 100644 index 000000000..9e05351f1 Binary files /dev/null and b/client/ui/netbird-systemtray-update-disconnected-dark.png differ diff --git a/client/ui/netbird-systemtray-update-disconnected-macos.png b/client/ui/netbird-systemtray-update-disconnected-macos.png new file mode 100644 index 000000000..8b190034e Binary files /dev/null and b/client/ui/netbird-systemtray-update-disconnected-macos.png differ diff --git a/client/ui/netbird-systemtray-update-disconnected.ico b/client/ui/netbird-systemtray-update-disconnected.ico index 04c35b058..968dc4105 100644 Binary files a/client/ui/netbird-systemtray-update-disconnected.ico and b/client/ui/netbird-systemtray-update-disconnected.ico differ diff --git a/client/ui/netbird-systemtray-update-disconnected.png b/client/ui/netbird-systemtray-update-disconnected.png index 3fbe88953..3adc39034 100644 Binary files a/client/ui/netbird-systemtray-update-disconnected.png and b/client/ui/netbird-systemtray-update-disconnected.png differ diff --git a/client/ui/netbird.png b/client/ui/netbird.png new file mode 100644 index 000000000..a92e9ed4c Binary files /dev/null and b/client/ui/netbird.png differ diff --git a/client/ui/network.go b/client/ui/network.go index e6f027f0e..750788cf3 100644 --- a/client/ui/network.go +++ b/client/ui/network.go @@ -1,9 +1,11 @@ -//go:build !(linux && 386) && !freebsd +//go:build !(linux && 386) package main import ( + "context" "fmt" + "runtime" "sort" "strings" "time" @@ -13,6 +15,7 @@ import ( "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/widget" + "fyne.io/systray" log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/client/proto" @@ -237,14 +240,14 @@ func (s *serviceClient) selectNetwork(id string, checked bool) { s.showError(fmt.Errorf("failed to select network: %v", err)) return } - log.Infof("Route %s selected", id) + log.Infof("Network '%s' selected", id) } else { if _, err := conn.DeselectNetworks(s.ctx, req); err != nil { log.Errorf("failed to deselect network: %v", err) s.showError(fmt.Errorf("failed to deselect network: %v", err)) return } - log.Infof("Network %s deselected", id) + log.Infof("Network '%s' deselected", id) } } @@ -324,6 +327,201 @@ func (s *serviceClient) updateNetworksBasedOnDisplayTab(tabs *container.AppTabs, s.updateNetworks(grid, f) } +func (s *serviceClient) updateExitNodes() { + conn, err := s.getSrvClient(defaultFailTimeout) + if err != nil { + log.Errorf("get client: %v", err) + return + } + + exitNodes, err := s.getExitNodes(conn) + if err != nil { + log.Errorf("get exit nodes: %v", err) + return + } + + s.exitNodeMu.Lock() + defer s.exitNodeMu.Unlock() + + s.recreateExitNodeMenu(exitNodes) + + if len(s.mExitNodeItems) > 0 { + s.mExitNode.Enable() + } else { + s.mExitNode.Disable() + } + + log.Debugf("Exit nodes updated: %d", len(s.mExitNodeItems)) +} + +func (s *serviceClient) recreateExitNodeMenu(exitNodes []*proto.Network) { + for _, node := range s.mExitNodeItems { + node.cancel() + node.Remove() + } + s.mExitNodeItems = nil + + if runtime.GOOS == "linux" || runtime.GOOS == "freebsd" { + s.mExitNode.Remove() + s.mExitNode = systray.AddMenuItem("Exit Node", "Select exit node for routing traffic") + } + + for _, node := range exitNodes { + menuItem := s.mExitNode.AddSubMenuItemCheckbox( + node.ID, + fmt.Sprintf("Use exit node %s", node.ID), + node.Selected, + ) + + ctx, cancel := context.WithCancel(context.Background()) + s.mExitNodeItems = append(s.mExitNodeItems, menuHandler{ + MenuItem: menuItem, + cancel: cancel, + }) + go s.handleChecked(ctx, node.ID, menuItem) + } + +} + +func (s *serviceClient) getExitNodes(conn proto.DaemonServiceClient) ([]*proto.Network, error) { + ctx, cancel := context.WithTimeout(s.ctx, defaultFailTimeout) + defer cancel() + + resp, err := conn.ListNetworks(ctx, &proto.ListNetworksRequest{}) + if err != nil { + return nil, fmt.Errorf("list networks: %v", err) + } + + var exitNodes []*proto.Network + for _, network := range resp.Routes { + if network.Range == "0.0.0.0/0" { + exitNodes = append(exitNodes, network) + } + } + return exitNodes, nil +} + +func (s *serviceClient) handleChecked(ctx context.Context, id string, item *systray.MenuItem) { + for { + select { + case <-ctx.Done(): + return + case _, ok := <-item.ClickedCh: + if !ok { + return + } + if err := s.toggleExitNode(id, item); err != nil { + log.Errorf("failed to toggle exit node: %v", err) + continue + } + } + } +} + +// Add function to toggle exit node selection +func (s *serviceClient) toggleExitNode(nodeID string, item *systray.MenuItem) error { + conn, err := s.getSrvClient(defaultFailTimeout) + if err != nil { + return fmt.Errorf("get client: %v", err) + } + + log.Infof("Toggling exit node '%s'", nodeID) + + s.exitNodeMu.Lock() + defer s.exitNodeMu.Unlock() + + exitNodes, err := s.getExitNodes(conn) + if err != nil { + return fmt.Errorf("get exit nodes: %v", err) + } + + var exitNode *proto.Network + // find other selected nodes and ours + ids := make([]string, 0, len(exitNodes)) + for _, node := range exitNodes { + if node.ID == nodeID { + // preserve original state + cp := *node //nolint:govet + exitNode = &cp + + // set desired state for recreation + node.Selected = true + continue + } + if node.Selected { + ids = append(ids, node.ID) + + // set desired state for recreation + node.Selected = false + } + } + + if item.Checked() && len(ids) == 0 { + // exit node is the only selected node, deselect it + ids = append(ids, nodeID) + exitNode = nil + } + + // deselect all other selected exit nodes + if err := s.deselectOtherExitNodes(conn, ids, item); err != nil { + return err + } + + if err := s.selectNewExitNode(conn, exitNode, nodeID, item); err != nil { + return err + } + + // linux/bsd doesn't handle Check/Uncheck well, so we recreate the menu + if runtime.GOOS == "linux" || runtime.GOOS == "freebsd" { + s.recreateExitNodeMenu(exitNodes) + } + + return nil +} + +func (s *serviceClient) deselectOtherExitNodes(conn proto.DaemonServiceClient, ids []string, currentItem *systray.MenuItem) error { + // deselect all other selected exit nodes + if len(ids) > 0 { + deselectReq := &proto.SelectNetworksRequest{ + NetworkIDs: ids, + } + if _, err := conn.DeselectNetworks(s.ctx, deselectReq); err != nil { + return fmt.Errorf("deselect networks: %v", err) + } + + log.Infof("Deselected exit nodes: %v", ids) + } + + // uncheck all other exit node menu items + for _, i := range s.mExitNodeItems { + if i.MenuItem == currentItem { + continue + } + i.Uncheck() + log.Infof("Unchecked exit node %v", i) + } + + return nil +} + +func (s *serviceClient) selectNewExitNode(conn proto.DaemonServiceClient, exitNode *proto.Network, nodeID string, item *systray.MenuItem) error { + if exitNode != nil && !exitNode.Selected { + selectReq := &proto.SelectNetworksRequest{ + NetworkIDs: []string{exitNode.ID}, + Append: true, + } + if _, err := conn.SelectNetworks(s.ctx, selectReq); err != nil { + return fmt.Errorf("select network: %v", err) + } + + log.Infof("Selected exit node '%s'", nodeID) + } + + item.Check() + + return nil +} + func getGridAndFilterFromTab(tabs *container.AppTabs, allGrid, overlappingGrid, exitNodesGrid *fyne.Container) (*fyne.Container, filter) { switch tabs.Selected().Text { case overlappingNetworksText: diff --git a/go.mod b/go.mod index 74b160b50..3d71e8eb1 100644 --- a/go.mod +++ b/go.mod @@ -19,13 +19,13 @@ require ( github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 github.com/vishvananda/netlink v1.2.1-beta.2 - golang.org/x/crypto v0.31.0 - golang.org/x/sys v0.28.0 + golang.org/x/crypto v0.32.0 + golang.org/x/sys v0.29.0 golang.zx2c4.com/wireguard v0.0.0-20230704135630-469159ecf7d1 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 golang.zx2c4.com/wireguard/windows v0.5.3 - google.golang.org/grpc v1.64.1 - google.golang.org/protobuf v1.34.2 + google.golang.org/grpc v1.70.0 + google.golang.org/protobuf v1.36.4 gopkg.in/natefinch/lumberjack.v2 v2.0.0 ) @@ -36,12 +36,13 @@ require ( github.com/c-robinson/iplib v1.0.3 github.com/caddyserver/certmagic v0.21.3 github.com/cilium/ebpf v0.15.0 + github.com/coder/websocket v1.8.12 github.com/coreos/go-iptables v0.7.0 github.com/creack/pty v1.1.18 github.com/davecgh/go-spew v1.1.1 github.com/eko/gocache/v3 v3.1.1 github.com/fsnotify/fsnotify v1.7.0 - github.com/gliderlabs/ssh v0.3.4 + github.com/gliderlabs/ssh v0.3.8 github.com/godbus/dbus/v5 v5.1.0 github.com/golang/mock v1.6.0 github.com/google/go-cmp v0.6.0 @@ -54,13 +55,12 @@ require ( github.com/hashicorp/go-version v1.6.0 github.com/libdns/route53 v1.5.0 github.com/libp2p/go-netroute v0.2.1 - github.com/magiconair/properties v1.8.7 - github.com/mattn/go-sqlite3 v1.14.19 + github.com/mattn/go-sqlite3 v1.14.22 github.com/mdlayher/socket v0.5.1 github.com/miekg/dns v1.1.59 github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/nadoo/ipset v0.5.0 - github.com/netbirdio/management-integrations/integrations v0.0.0-20250115083837-a09722b8d2a6 + github.com/netbirdio/management-integrations/integrations v0.0.0-20250220173202-e599d83524fc github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20241010133937-e0df50df217d github.com/okta/okta-sdk-golang/v2 v2.18.0 github.com/oschwald/maxminddb-golang v1.12.0 @@ -71,43 +71,44 @@ require ( github.com/pion/transport/v3 v3.0.1 github.com/pion/turn/v3 v3.0.1 github.com/prometheus/client_golang v1.19.1 + github.com/quic-go/quic-go v0.48.2 github.com/rs/xid v1.3.0 github.com/shirou/gopsutil/v3 v3.24.4 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 github.com/testcontainers/testcontainers-go v0.31.0 github.com/testcontainers/testcontainers-go/modules/mysql v0.31.0 github.com/testcontainers/testcontainers-go/modules/postgres v0.31.0 github.com/things-go/go-socks5 v0.0.4 github.com/yusufpapurcu/wmi v1.2.4 github.com/zcalusic/sysinfo v1.1.3 - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 - go.opentelemetry.io/otel v1.26.0 + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 + go.opentelemetry.io/otel v1.34.0 go.opentelemetry.io/otel/exporters/prometheus v0.48.0 - go.opentelemetry.io/otel/metric v1.26.0 - go.opentelemetry.io/otel/sdk/metric v1.26.0 + go.opentelemetry.io/otel/metric v1.34.0 + go.opentelemetry.io/otel/sdk/metric v1.32.0 go.uber.org/zap v1.27.0 goauthentik.io/api/v3 v3.2023051.3 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a - golang.org/x/net v0.30.0 - golang.org/x/oauth2 v0.19.0 + golang.org/x/net v0.34.0 + golang.org/x/oauth2 v0.26.0 golang.org/x/sync v0.10.0 - golang.org/x/term v0.27.0 - google.golang.org/api v0.177.0 + golang.org/x/term v0.28.0 + google.golang.org/api v0.220.0 gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/mysql v1.5.7 gorm.io/driver/postgres v1.5.7 - gorm.io/driver/sqlite v1.5.3 - gorm.io/gorm v1.25.7 - nhooyr.io/websocket v1.8.11 + gorm.io/driver/sqlite v1.5.7 + gorm.io/gorm v1.25.12 + gvisor.dev/gvisor v0.0.0-20231020174304-b8a429915ff1 ) require ( - cloud.google.com/go/auth v0.3.0 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect - cloud.google.com/go/compute/metadata v0.3.0 // indirect + cloud.google.com/go/auth v0.14.1 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect + cloud.google.com/go/compute/metadata v0.6.0 // indirect 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 @@ -150,19 +151,20 @@ require ( github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2 // indirect github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect - github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-redis/redis/v8 v8.11.5 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/go-text/render v0.2.0 // indirect github.com/go-text/typesetting v0.2.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/btree v1.1.2 // indirect - github.com/google/s2a-go v0.1.7 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect - github.com/googleapis/gax-go/v2 v2.12.3 // indirect + github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect + github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/gopherjs/gopherjs v1.17.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect @@ -182,6 +184,7 @@ require ( github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/libdns/libdns v0.2.2 // indirect github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect + github.com/magiconair/properties v1.8.7 // indirect github.com/mdlayher/genetlink v1.3.2 // indirect github.com/mdlayher/netlink v1.7.2 // indirect github.com/mholt/acmez/v2 v2.0.1 // indirect @@ -217,23 +220,22 @@ require ( github.com/vishvananda/netns v0.0.4 // indirect github.com/yuin/goldmark v1.7.1 // indirect github.com/zeebo/blake3 v0.2.3 // indirect - go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 // indirect - go.opentelemetry.io/otel/sdk v1.26.0 // indirect - go.opentelemetry.io/otel/trace v1.26.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect + go.opentelemetry.io/otel/sdk v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.34.0 // indirect + go.uber.org/mock v0.4.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/image v0.18.0 // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/text v0.21.0 // indirect - golang.org/x/time v0.5.0 // indirect + golang.org/x/time v0.10.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240509183442-62759503f434 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect - gvisor.dev/gvisor v0.0.0-20231020174304-b8a429915ff1 // indirect k8s.io/apimachinery v0.26.2 // indirect ) @@ -241,7 +243,7 @@ replace github.com/kardianos/service => github.com/netbirdio/service v0.0.0-2024 replace github.com/getlantern/systray => github.com/netbirdio/systray v0.0.0-20231030152038-ef1ed2a27949 -replace golang.zx2c4.com/wireguard => github.com/netbirdio/wireguard-go v0.0.0-20241125150134-f9cdce5e32e9 +replace golang.zx2c4.com/wireguard => github.com/netbirdio/wireguard-go v0.0.0-20241230120307-6a676aebaaf6 replace github.com/cloudflare/circl => github.com/cunicu/circl v0.0.0-20230801113412-fec58fc7b5f6 diff --git a/go.sum b/go.sum index f6d4590ee..36bca22d3 100644 --- a/go.sum +++ b/go.sum @@ -18,10 +18,10 @@ cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmW cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go/auth v0.3.0 h1:PRyzEpGfx/Z9e8+lHsbkoUVXD0gnu4MNmm7Gp8TQNIs= -cloud.google.com/go/auth v0.3.0/go.mod h1:lBv6NKTWp8E3LPzmO1TbiiRKc4drLOfHsgmlH9ogv5w= -cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= -cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= +cloud.google.com/go/auth v0.14.1 h1:AwoJbzUdxA/whv1qj3TLKwh3XX5sikny2fc40wUl+h0= +cloud.google.com/go/auth v0.14.1/go.mod h1:4JHUxlGXisL0AW8kXPtUF6ztuOksyfUQNFjfsOCXkPM= +cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= +cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -29,8 +29,8 @@ cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUM cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= -cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= -cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= @@ -137,6 +137,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= +github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/containerd/containerd v1.7.16 h1:7Zsfe8Fkj4Wi2My6DXGQ87hiqIrmOXolm72ZEkFU5Mg= github.com/containerd/containerd v1.7.16/go.mod h1:NL49g7A/Fui7ccmxV6zkBWwqMgmMxFWzujYCc+JLt7k= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= @@ -212,8 +214,8 @@ github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2H github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= -github.com/gliderlabs/ssh v0.3.4 h1:+AXBtim7MTKaLVPgvE+3mhewYRawNLTd+jEEz/wExZw= -github.com/gliderlabs/ssh v0.3.4/go.mod h1:ZSS+CUoKHDrqVakTfTWUlKSr9MtMFkC4UvtQKD7O914= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 h1:zDw5v7qm4yH7N8C8uWd+8Ii9rROdgWxQuGoJ9WDXxfk= github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= @@ -223,8 +225,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= @@ -261,14 +263,12 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69 github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68= -github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/glog v1.2.3 h1:oDTdz9f5VGVVNGu/Q7UXKWYsD0873HXLHdJUNBsSEKM= +github.com/golang/glog v1.2.3/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -343,18 +343,18 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y= github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= -github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= -github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= +github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA= -github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= +github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= +github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/gopacket/gopacket v1.1.1 h1:zbx9F9d6A7sWNkFKrvMBZTfGgxFoY4NgUudFVVHMfcw= github.com/gopacket/gopacket v1.1.1/go.mod h1:HavMeONEl7W9036of9LbSWoonqhH7HA1+ZRO+rMIvFs= @@ -405,6 +405,7 @@ github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/J github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= @@ -474,8 +475,8 @@ github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= -github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= -github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 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= @@ -526,14 +527,14 @@ github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944 h1:TDtJKmM6S github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944/go.mod h1:sHA6TRxjQ6RLbnI+3R4DZo2Eseg/iKiPRfNmcuNySVQ= github.com/netbirdio/ice/v3 v3.0.0-20240315174635-e72a50fcb64e h1:PURA50S8u4mF6RrkYYCAvvPCixhqqEiEy3Ej6avh04c= github.com/netbirdio/ice/v3 v3.0.0-20240315174635-e72a50fcb64e/go.mod h1:YMLU7qbKfVjmEv7EoZPIVEI+kNYxWCdPK3VS0BU+U4Q= -github.com/netbirdio/management-integrations/integrations v0.0.0-20250115083837-a09722b8d2a6 h1:I/ODkZ8rSDOzlJbhEjD2luSI71zl+s5JgNvFHY0+mBU= -github.com/netbirdio/management-integrations/integrations v0.0.0-20250115083837-a09722b8d2a6/go.mod h1:izUUs1NT7ja+PwSX3kJ7ox8Kkn478tboBJSjL4kU6J0= +github.com/netbirdio/management-integrations/integrations v0.0.0-20250220173202-e599d83524fc h1:18xvjOy2tZVIK7rihNpf9DF/3mAiljYKWaQlWa9vJgI= +github.com/netbirdio/management-integrations/integrations v0.0.0-20250220173202-e599d83524fc/go.mod h1:izUUs1NT7ja+PwSX3kJ7ox8Kkn478tboBJSjL4kU6J0= github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502 h1:3tHlFmhTdX9axERMVN63dqyFqnvuD+EMJHzM7mNGON8= github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20241010133937-e0df50df217d h1:bRq5TKgC7Iq20pDiuC54yXaWnAVeS5PdGpSokFTlR28= github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20241010133937-e0df50df217d/go.mod h1:5/sjFmLb8O96B5737VCqhHyGRzNFIaN/Bu7ZodXc3qQ= -github.com/netbirdio/wireguard-go v0.0.0-20241125150134-f9cdce5e32e9 h1:Pu/7EukijT09ynHUOzQYW7cC3M/BKU8O4qyN/TvTGoY= -github.com/netbirdio/wireguard-go v0.0.0-20241125150134-f9cdce5e32e9/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= +github.com/netbirdio/wireguard-go v0.0.0-20241230120307-6a676aebaaf6 h1:X5h5QgP7uHAv78FWgHV8+WYLjHxK9v3ilkVXT1cpCrQ= +github.com/netbirdio/wireguard-go v0.0.0-20241230120307-6a676aebaaf6/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM= github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -610,10 +611,12 @@ github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+a github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U= github.com/prometheus/procfs v0.15.0 h1:A82kmvXJq2jTu5YUhSGNlYoxh85zLnKgPz4bMZgI5Ek= github.com/prometheus/procfs v0.15.0/go.mod h1:Y0RJ/Y5g5wJpkTisOtqwDSo4HwhGmLB4VQSw2sQJLHk= +github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE= +github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rs/cors v1.8.0 h1:P2KMzcFwrPoSjkF1WLRPsp3UMLyql8L4v9hQpVeK5so= github.com/rs/cors v1.8.0/go.mod h1:EBwu+T5AvHOcXwvZIkQFjUN6s8Czyqw12GL/Y0tUyRM= github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4= @@ -678,11 +681,11 @@ 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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/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/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/testcontainers/testcontainers-go v0.31.0 h1:W0VwIhcEVhRflwL9as3dhY6jXjVCA27AkmbnZ+UTh3U= github.com/testcontainers/testcontainers-go v0.31.0/go.mod h1:D2lAoA0zUFiSY+eAflqK5mcUx/A5hrrORaEQrd0SefI= @@ -734,33 +737,35 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc= -go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs= -go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 h1:PS8wXpbyaDJQ2VDHHncMe9Vct0Zn1fEjpsjrLxGJoSc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= go.opentelemetry.io/otel/exporters/prometheus v0.48.0 h1:sBQe3VNGUjY9IKWQC6z2lNqa5iGbDSxhs60ABwK4y0s= go.opentelemetry.io/otel/exporters/prometheus v0.48.0/go.mod h1:DtrbMzoZWwQHyrQmCfLam5DZbnmorsGbOtTbYHycU5o= -go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30= -go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4= -go.opentelemetry.io/otel/sdk v1.26.0 h1:Y7bumHf5tAiDlRYFmGqetNcLaVUZmh4iYfmGxtmz7F8= -go.opentelemetry.io/otel/sdk v1.26.0/go.mod h1:0p8MXpqLeJ0pzcszQQN4F0S5FVjBLgypeGSngLsmirs= -go.opentelemetry.io/otel/sdk/metric v1.26.0 h1:cWSks5tfriHPdWFnl+qpX3P681aAYqlZHcAyHw5aU9Y= -go.opentelemetry.io/otel/sdk/metric v1.26.0/go.mod h1:ClMFFknnThJCksebJwz7KIyEDHO+nTB6gK8obLy8RyE= -go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA= -go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= +go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= @@ -776,14 +781,13 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/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-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -879,8 +883,8 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -894,8 +898,8 @@ golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= -golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= -golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= +golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= +golang.org/x/oauth2 v0.26.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-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -971,6 +975,7 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -982,8 +987,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -991,8 +996,8 @@ golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= -golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1012,8 +1017,8 @@ golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= +golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1107,8 +1112,8 @@ google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjR google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= -google.golang.org/api v0.177.0 h1:8a0p/BbPa65GlqGWtUKxot4p0TV8OGOfyTjtmkXNXmk= -google.golang.org/api v0.177.0/go.mod h1:srbhue4MLjkjbkux5p3dw/ocYOSZTaIEvf7bCOnFQDw= +google.golang.org/api v0.220.0 h1:3oMI4gdBgB72WFVwE1nerDD8W3HUOS4kypK6rRLbGns= +google.golang.org/api v0.220.0/go.mod h1:26ZAlY6aN/8WgpCzjPNy18QpYaz7Zgg1h0qe1GkZEmY= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1157,10 +1162,11 @@ google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto/googleapis/api v0.0.0-20240509183442-62759503f434 h1:OpXbo8JnN8+jZGPrL4SSfaDjSCjupr8lXyBAbexEm/U= -google.golang.org/genproto/googleapis/api v0.0.0-20240509183442-62759503f434/go.mod h1:FfiGhwUm6CJviekPrc0oJ+7h29e+DmWU6UtjX0ZvI7Y= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ= +google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= +google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 h1:J1H9f+LEdWAfHcez/4cvaVBox7cOYT+IU6rgqj5x++8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1181,8 +1187,8 @@ google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= -google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= +google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= +google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1197,8 +1203,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= +google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1236,10 +1242,11 @@ gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM= gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA= -gorm.io/driver/sqlite v1.5.3 h1:7/0dUgX28KAcopdfbRWWl68Rflh6osa4rDh+m51KL2g= -gorm.io/driver/sqlite v1.5.3/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4= -gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A= +gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I= +gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= gvisor.dev/gvisor v0.0.0-20231020174304-b8a429915ff1 h1:qDCwdCWECGnwQSQC01Dpnp09fRHxJs9PbktotUqG+hs= @@ -1258,8 +1265,6 @@ k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8 k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= -nhooyr.io/websocket v1.8.11 h1:f/qXNc2/3DpoSZkHt1DQu6rj4zGC8JmkkLkWss0MgN0= -nhooyr.io/websocket v1.8.11/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/infrastructure_files/docker-compose.yml.tmpl.traefik b/infrastructure_files/docker-compose.yml.tmpl.traefik index 71471c3ef..dcd3f955c 100644 --- a/infrastructure_files/docker-compose.yml.tmpl.traefik +++ b/infrastructure_files/docker-compose.yml.tmpl.traefik @@ -67,6 +67,10 @@ services: options: max-size: "500m" max-file: "2" + labels: + - traefik.enable=true + - traefik.http.routers.netbird-relay.rule=Host(`$NETBIRD_DOMAIN`) && PathPrefix(`/relay`) + - traefik.http.services.netbird-relay.loadbalancer.server.port=$NETBIRD_RELAY_PORT # Management management: diff --git a/management/README.md b/management/README.md index f0eb0cb70..1122a9e76 100644 --- a/management/README.md +++ b/management/README.md @@ -111,4 +111,3 @@ Generate gRpc code: #!/bin/bash protoc -I proto/ proto/management.proto --go_out=. --go-grpc_out=. ``` - diff --git a/management/client/client.go b/management/client/client.go index e79884292..950f6137e 100644 --- a/management/client/client.go +++ b/management/client/client.go @@ -7,6 +7,7 @@ import ( "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "github.com/netbirdio/netbird/client/system" + "github.com/netbirdio/netbird/management/domain" "github.com/netbirdio/netbird/management/proto" ) @@ -14,8 +15,8 @@ type Client interface { io.Closer Sync(ctx context.Context, sysInfo *system.Info, msgHandler func(msg *proto.SyncResponse) error) error GetServerPublicKey() (*wgtypes.Key, error) - Register(serverKey wgtypes.Key, setupKey string, jwtToken string, sysInfo *system.Info, sshKey []byte) (*proto.LoginResponse, error) - Login(serverKey wgtypes.Key, sysInfo *system.Info, sshKey []byte) (*proto.LoginResponse, error) + Register(serverKey wgtypes.Key, setupKey string, jwtToken string, sysInfo *system.Info, sshKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error) + Login(serverKey wgtypes.Key, sysInfo *system.Info, sshKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error) GetDeviceAuthorizationFlow(serverKey wgtypes.Key) (*proto.DeviceAuthorizationFlow, error) GetPKCEAuthorizationFlow(serverKey wgtypes.Key) (*proto.PKCEAuthorizationFlow, error) GetNetworkMap(sysInfo *system.Info) (*proto.NetworkMap, error) diff --git a/management/client/client_test.go b/management/client/client_test.go index 8bd8af8d2..21f6b79ad 100644 --- a/management/client/client_test.go +++ b/management/client/client_test.go @@ -78,7 +78,7 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) { } secretsManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay) - mgmtServer, err := mgmt.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil) + mgmtServer, err := mgmt.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil, nil) if err != nil { t.Fatal(err) } @@ -177,7 +177,7 @@ func TestClient_LoginUnregistered_ShouldThrow_401(t *testing.T) { t.Fatal(err) } sysInfo := system.GetInfo(context.TODO()) - _, err = client.Login(*key, sysInfo, nil) + _, err = client.Login(*key, sysInfo, nil, nil) if err == nil { t.Error("expecting err on unregistered login, got nil") } @@ -205,7 +205,7 @@ func TestClient_LoginRegistered(t *testing.T) { t.Error(err) } info := system.GetInfo(context.TODO()) - resp, err := client.Register(*key, ValidKey, "", info, nil) + resp, err := client.Register(*key, ValidKey, "", info, nil, nil) if err != nil { t.Error(err) } @@ -235,7 +235,7 @@ func TestClient_Sync(t *testing.T) { } info := system.GetInfo(context.TODO()) - _, err = client.Register(*serverKey, ValidKey, "", info, nil) + _, err = client.Register(*serverKey, ValidKey, "", info, nil, nil) if err != nil { t.Error(err) } @@ -251,15 +251,18 @@ func TestClient_Sync(t *testing.T) { } info = system.GetInfo(context.TODO()) - _, err = remoteClient.Register(*serverKey, ValidKey, "", info, nil) + _, err = remoteClient.Register(*serverKey, ValidKey, "", info, nil, nil) if err != nil { t.Fatal(err) } ch := make(chan *mgmtProto.SyncResponse, 1) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go func() { - err = client.Sync(context.Background(), info, func(msg *mgmtProto.SyncResponse) error { + err = client.Sync(ctx, info, func(msg *mgmtProto.SyncResponse) error { ch <- msg return nil }) @@ -273,8 +276,8 @@ func TestClient_Sync(t *testing.T) { if resp.GetPeerConfig() == nil { t.Error("expecting non nil PeerConfig got nil") } - if resp.GetWiretrusteeConfig() == nil { - t.Error("expecting non nil WiretrusteeConfig got nil") + if resp.GetNetbirdConfig() == nil { + t.Error("expecting non nil NetbirdConfig got nil") } if len(resp.GetRemotePeers()) != 1 { t.Errorf("expecting RemotePeers size %d got %d", 1, len(resp.GetRemotePeers())) @@ -349,7 +352,7 @@ func Test_SystemMetaDataFromClient(t *testing.T) { } info := system.GetInfo(context.TODO()) - _, err = testClient.Register(*key, ValidKey, "", info, nil) + _, err = testClient.Register(*key, ValidKey, "", info, nil, nil) if err != nil { t.Errorf("error while trying to register client: %v", err) } @@ -366,15 +369,15 @@ func Test_SystemMetaDataFromClient(t *testing.T) { } expectedMeta := &mgmtProto.PeerSystemMeta{ - Hostname: info.Hostname, - GoOS: info.GoOS, - Kernel: info.Kernel, - Platform: info.Platform, - OS: info.OS, - Core: info.OSVersion, - OSVersion: info.OSVersion, - WiretrusteeVersion: info.WiretrusteeVersion, - KernelVersion: info.KernelVersion, + Hostname: info.Hostname, + GoOS: info.GoOS, + Kernel: info.Kernel, + Platform: info.Platform, + OS: info.OS, + Core: info.OSVersion, + OSVersion: info.OSVersion, + NetbirdVersion: info.NetbirdVersion, + KernelVersion: info.KernelVersion, NetworkAddresses: protoNetAddr, SysSerialNumber: info.SystemSerialNumber, @@ -417,7 +420,7 @@ func isEqual(a, b *mgmtProto.PeerSystemMeta) bool { a.GetPlatform() == b.GetPlatform() && a.GetOS() == b.GetOS() && a.GetOSVersion() == b.GetOSVersion() && - a.GetWiretrusteeVersion() == b.GetWiretrusteeVersion() && + a.GetNetbirdVersion() == b.GetNetbirdVersion() && a.GetUiVersion() == b.GetUiVersion() && a.GetSysSerialNumber() == b.GetSysSerialNumber() && a.GetSysProductName() == b.GetSysProductName() && diff --git a/management/client/grpc.go b/management/client/grpc.go index 74e808c32..d3aaffec0 100644 --- a/management/client/grpc.go +++ b/management/client/grpc.go @@ -19,6 +19,7 @@ import ( "github.com/netbirdio/netbird/client/system" "github.com/netbirdio/netbird/encryption" + "github.com/netbirdio/netbird/management/domain" "github.com/netbirdio/netbird/management/proto" nbgrpc "github.com/netbirdio/netbird/util/grpc" ) @@ -364,21 +365,21 @@ func (c *GrpcClient) login(serverKey wgtypes.Key, req *proto.LoginRequest) (*pro // Register registers peer on Management Server. It actually calls a Login endpoint with a provided setup key // Takes care of encrypting and decrypting messages. // This method will also collect system info and send it with the request (e.g. hostname, os, etc) -func (c *GrpcClient) Register(serverKey wgtypes.Key, setupKey string, jwtToken string, sysInfo *system.Info, pubSSHKey []byte) (*proto.LoginResponse, error) { +func (c *GrpcClient) Register(serverKey wgtypes.Key, setupKey string, jwtToken string, sysInfo *system.Info, pubSSHKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error) { keys := &proto.PeerKeys{ SshPubKey: pubSSHKey, WgPubKey: []byte(c.key.PublicKey().String()), } - return c.login(serverKey, &proto.LoginRequest{SetupKey: setupKey, Meta: infoToMetaData(sysInfo), JwtToken: jwtToken, PeerKeys: keys}) + return c.login(serverKey, &proto.LoginRequest{SetupKey: setupKey, Meta: infoToMetaData(sysInfo), JwtToken: jwtToken, PeerKeys: keys, DnsLabels: dnsLabels.ToPunycodeList()}) } // Login attempts login to Management Server. Takes care of encrypting and decrypting messages. -func (c *GrpcClient) Login(serverKey wgtypes.Key, sysInfo *system.Info, pubSSHKey []byte) (*proto.LoginResponse, error) { +func (c *GrpcClient) Login(serverKey wgtypes.Key, sysInfo *system.Info, pubSSHKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error) { keys := &proto.PeerKeys{ SshPubKey: pubSSHKey, WgPubKey: []byte(c.key.PublicKey().String()), } - return c.login(serverKey, &proto.LoginRequest{Meta: infoToMetaData(sysInfo), PeerKeys: keys}) + return c.login(serverKey, &proto.LoginRequest{Meta: infoToMetaData(sysInfo), PeerKeys: keys, DnsLabels: dnsLabels.ToPunycodeList()}) } // GetDeviceAuthorizationFlow returns a device authorization flow information. @@ -521,24 +522,34 @@ func infoToMetaData(info *system.Info) *proto.PeerSystemMeta { } return &proto.PeerSystemMeta{ - Hostname: info.Hostname, - GoOS: info.GoOS, - OS: info.OS, - Core: info.OSVersion, - OSVersion: info.OSVersion, - Platform: info.Platform, - Kernel: info.Kernel, - WiretrusteeVersion: info.WiretrusteeVersion, - UiVersion: info.UIVersion, - KernelVersion: info.KernelVersion, - NetworkAddresses: addresses, - SysSerialNumber: info.SystemSerialNumber, - SysManufacturer: info.SystemManufacturer, - SysProductName: info.SystemProductName, + Hostname: info.Hostname, + GoOS: info.GoOS, + OS: info.OS, + Core: info.OSVersion, + OSVersion: info.OSVersion, + Platform: info.Platform, + Kernel: info.Kernel, + NetbirdVersion: info.NetbirdVersion, + UiVersion: info.UIVersion, + KernelVersion: info.KernelVersion, + NetworkAddresses: addresses, + SysSerialNumber: info.SystemSerialNumber, + SysManufacturer: info.SystemManufacturer, + SysProductName: info.SystemProductName, Environment: &proto.Environment{ Cloud: info.Environment.Cloud, Platform: info.Environment.Platform, }, Files: files, + + Flags: &proto.Flags{ + RosenpassEnabled: info.RosenpassEnabled, + RosenpassPermissive: info.RosenpassPermissive, + ServerSSHAllowed: info.ServerSSHAllowed, + DisableClientRoutes: info.DisableClientRoutes, + DisableServerRoutes: info.DisableServerRoutes, + DisableDNS: info.DisableDNS, + DisableFirewall: info.DisableFirewall, + }, } } diff --git a/management/client/mock.go b/management/client/mock.go index 73a7ac38f..9e1786f82 100644 --- a/management/client/mock.go +++ b/management/client/mock.go @@ -6,6 +6,7 @@ import ( "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "github.com/netbirdio/netbird/client/system" + "github.com/netbirdio/netbird/management/domain" "github.com/netbirdio/netbird/management/proto" ) @@ -13,8 +14,8 @@ type MockClient struct { CloseFunc func() error SyncFunc func(ctx context.Context, sysInfo *system.Info, msgHandler func(msg *proto.SyncResponse) error) error GetServerPublicKeyFunc func() (*wgtypes.Key, error) - RegisterFunc func(serverKey wgtypes.Key, setupKey string, jwtToken string, info *system.Info, sshKey []byte) (*proto.LoginResponse, error) - LoginFunc func(serverKey wgtypes.Key, info *system.Info, sshKey []byte) (*proto.LoginResponse, error) + RegisterFunc func(serverKey wgtypes.Key, setupKey string, jwtToken string, info *system.Info, sshKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error) + LoginFunc func(serverKey wgtypes.Key, info *system.Info, sshKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error) GetDeviceAuthorizationFlowFunc func(serverKey wgtypes.Key) (*proto.DeviceAuthorizationFlow, error) GetPKCEAuthorizationFlowFunc func(serverKey wgtypes.Key) (*proto.PKCEAuthorizationFlow, error) SyncMetaFunc func(sysInfo *system.Info) error @@ -45,18 +46,18 @@ func (m *MockClient) GetServerPublicKey() (*wgtypes.Key, error) { return m.GetServerPublicKeyFunc() } -func (m *MockClient) Register(serverKey wgtypes.Key, setupKey string, jwtToken string, info *system.Info, sshKey []byte) (*proto.LoginResponse, error) { +func (m *MockClient) Register(serverKey wgtypes.Key, setupKey string, jwtToken string, info *system.Info, sshKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error) { if m.RegisterFunc == nil { return nil, nil } - return m.RegisterFunc(serverKey, setupKey, jwtToken, info, sshKey) + return m.RegisterFunc(serverKey, setupKey, jwtToken, info, sshKey, dnsLabels) } -func (m *MockClient) Login(serverKey wgtypes.Key, info *system.Info, sshKey []byte) (*proto.LoginResponse, error) { +func (m *MockClient) Login(serverKey wgtypes.Key, info *system.Info, sshKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error) { if m.LoginFunc == nil { return nil, nil } - return m.LoginFunc(serverKey, info, sshKey) + return m.LoginFunc(serverKey, info, sshKey, dnsLabels) } func (m *MockClient) GetDeviceAuthorizationFlow(serverKey wgtypes.Key) (*proto.DeviceAuthorizationFlow, error) { diff --git a/management/client/rest/accounts.go b/management/client/rest/accounts.go new file mode 100644 index 000000000..f38b19f70 --- /dev/null +++ b/management/client/rest/accounts.go @@ -0,0 +1,54 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// AccountsAPI APIs for accounts, do not use directly +type AccountsAPI struct { + c *Client +} + +// List list all accounts, only returns one account always +// See more: https://docs.netbird.io/api/resources/accounts#list-all-accounts +func (a *AccountsAPI) List(ctx context.Context) ([]api.Account, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/accounts", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.Account](resp) + return ret, err +} + +// Update update account settings +// See more: https://docs.netbird.io/api/resources/accounts#update-an-account +func (a *AccountsAPI) Update(ctx context.Context, accountID string, request api.PutApiAccountsAccountIdJSONRequestBody) (*api.Account, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/accounts/"+accountID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Account](resp) + return &ret, err +} + +// Delete delete account +// See more: https://docs.netbird.io/api/resources/accounts#delete-an-account +func (a *AccountsAPI) Delete(ctx context.Context, accountID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/accounts/"+accountID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} diff --git a/management/client/rest/accounts_test.go b/management/client/rest/accounts_test.go new file mode 100644 index 000000000..621228261 --- /dev/null +++ b/management/client/rest/accounts_test.go @@ -0,0 +1,174 @@ +//go:build integration +// +build integration + +package rest_test + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" +) + +var ( + testAccount = api.Account{ + Id: "Test", + Settings: api.AccountSettings{ + Extra: &api.AccountExtraSettings{ + PeerApprovalEnabled: ptr(false), + }, + GroupsPropagationEnabled: ptr(true), + JwtGroupsEnabled: ptr(false), + PeerInactivityExpiration: 7, + PeerInactivityExpirationEnabled: true, + PeerLoginExpiration: 24, + PeerLoginExpirationEnabled: true, + RegularUsersViewBlocked: false, + RoutingPeerDnsResolutionEnabled: ptr(false), + }, + } +) + +func TestAccounts_List_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/accounts", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.Account{testAccount}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Accounts.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testAccount, ret[0]) + }) +} + +func TestAccounts_List_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/accounts", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Accounts.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestAccounts_Update_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/accounts/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiAccountsAccountIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, true, *req.Settings.RoutingPeerDnsResolutionEnabled) + retBytes, _ := json.Marshal(testAccount) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Accounts.Update(context.Background(), "Test", api.PutApiAccountsAccountIdJSONRequestBody{ + Settings: api.AccountSettings{ + RoutingPeerDnsResolutionEnabled: ptr(true), + }, + }) + require.NoError(t, err) + assert.Equal(t, testAccount, *ret) + }) + +} + +func TestAccounts_Update_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/accounts/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Accounts.Update(context.Background(), "Test", api.PutApiAccountsAccountIdJSONRequestBody{ + Settings: api.AccountSettings{ + RoutingPeerDnsResolutionEnabled: ptr(true), + }, + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestAccounts_Delete_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/accounts/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Accounts.Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestAccounts_Delete_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/accounts/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Accounts.Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestAccounts_Integration_List(t *testing.T) { + withBlackBoxServer(t, func(c *rest.Client) { + accounts, err := c.Accounts.List(context.Background()) + require.NoError(t, err) + assert.Len(t, accounts, 1) + assert.Equal(t, "bf1c8084-ba50-4ce7-9439-34653001fc3b", accounts[0].Id) + assert.Equal(t, false, *accounts[0].Settings.Extra.PeerApprovalEnabled) + }) +} + +func TestAccounts_Integration_Update(t *testing.T) { + withBlackBoxServer(t, func(c *rest.Client) { + accounts, err := c.Accounts.List(context.Background()) + require.NoError(t, err) + assert.Len(t, accounts, 1) + accounts[0].Settings.JwtAllowGroups = ptr([]string{"test"}) + account, err := c.Accounts.Update(context.Background(), accounts[0].Id, api.AccountRequest{ + Settings: accounts[0].Settings, + }) + require.NoError(t, err) + assert.Equal(t, accounts[0].Id, account.Id) + assert.Equal(t, []string{"test"}, *account.Settings.JwtAllowGroups) + }) +} + +// Account deletion on MySQL and PostgreSQL databases causes unknown errors +// func TestAccounts_Integration_Delete(t *testing.T) { +// withBlackBoxServer(t, func(c *rest.Client) { +// accounts, err := c.Accounts.List(context.Background()) +// require.NoError(t, err) +// assert.Len(t, accounts, 1) +// err = c.Accounts.Delete(context.Background(), accounts[0].Id) +// require.NoError(t, err) +// _, err = c.Accounts.List(context.Background()) +// assert.Error(t, err) +// }) +// } diff --git a/management/client/rest/client.go b/management/client/rest/client.go new file mode 100644 index 000000000..f55e2d11e --- /dev/null +++ b/management/client/rest/client.go @@ -0,0 +1,133 @@ +package rest + +import ( + "context" + "encoding/json" + "errors" + "io" + "net/http" + + "github.com/netbirdio/netbird/management/server/http/util" +) + +// Client Management service HTTP REST API Client +type Client struct { + managementURL string + authHeader string + + // Accounts NetBird account APIs + // see more: https://docs.netbird.io/api/resources/accounts + Accounts *AccountsAPI + + // Users NetBird users APIs + // see more: https://docs.netbird.io/api/resources/users + Users *UsersAPI + + // Tokens NetBird tokens APIs + // see more: https://docs.netbird.io/api/resources/tokens + Tokens *TokensAPI + + // Peers NetBird peers APIs + // see more: https://docs.netbird.io/api/resources/peers + Peers *PeersAPI + + // SetupKeys NetBird setup keys APIs + // see more: https://docs.netbird.io/api/resources/setup-keys + SetupKeys *SetupKeysAPI + + // Groups NetBird groups APIs + // see more: https://docs.netbird.io/api/resources/groups + Groups *GroupsAPI + + // Policies NetBird policies APIs + // see more: https://docs.netbird.io/api/resources/policies + Policies *PoliciesAPI + + // PostureChecks NetBird posture checks APIs + // see more: https://docs.netbird.io/api/resources/posture-checks + PostureChecks *PostureChecksAPI + + // Networks NetBird networks APIs + // see more: https://docs.netbird.io/api/resources/networks + Networks *NetworksAPI + + // Routes NetBird routes APIs + // see more: https://docs.netbird.io/api/resources/routes + Routes *RoutesAPI + + // DNS NetBird DNS APIs + // see more: https://docs.netbird.io/api/resources/routes + DNS *DNSAPI + + // GeoLocation NetBird Geo Location APIs + // see more: https://docs.netbird.io/api/resources/geo-locations + GeoLocation *GeoLocationAPI + + // Events NetBird Events APIs + // see more: https://docs.netbird.io/api/resources/events + Events *EventsAPI +} + +// New initialize new Client instance +func New(managementURL, token string) *Client { + client := &Client{ + managementURL: managementURL, + authHeader: "Token " + token, + } + client.Accounts = &AccountsAPI{client} + client.Users = &UsersAPI{client} + client.Tokens = &TokensAPI{client} + client.Peers = &PeersAPI{client} + client.SetupKeys = &SetupKeysAPI{client} + client.Groups = &GroupsAPI{client} + client.Policies = &PoliciesAPI{client} + client.PostureChecks = &PostureChecksAPI{client} + client.Networks = &NetworksAPI{client} + client.Routes = &RoutesAPI{client} + client.DNS = &DNSAPI{client} + client.GeoLocation = &GeoLocationAPI{client} + client.Events = &EventsAPI{client} + return client +} + +func (c *Client) newRequest(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) { + req, err := http.NewRequestWithContext(ctx, method, c.managementURL+path, body) + if err != nil { + return nil, err + } + + req.Header.Add("Authorization", c.authHeader) + req.Header.Add("Accept", "application/json") + if body != nil { + req.Header.Add("Content-Type", "application/json") + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + + if resp.StatusCode > 299 { + parsedErr, pErr := parseResponse[util.ErrorResponse](resp) + if pErr != nil { + return nil, err + } + return nil, errors.New(parsedErr.Message) + } + + return resp, nil +} + +func parseResponse[T any](resp *http.Response) (T, error) { + var ret T + if resp.Body == nil { + return ret, errors.New("No body") + } + bs, err := io.ReadAll(resp.Body) + if err != nil { + return ret, err + } + err = json.Unmarshal(bs, &ret) + + return ret, err +} diff --git a/management/client/rest/client_test.go b/management/client/rest/client_test.go new file mode 100644 index 000000000..70e6c73e1 --- /dev/null +++ b/management/client/rest/client_test.go @@ -0,0 +1,34 @@ +//go:build integration +// +build integration + +package rest_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/management/server/http/testing/testing_tools" +) + +func withMockClient(callback func(*rest.Client, *http.ServeMux)) { + mux := &http.ServeMux{} + server := httptest.NewServer(mux) + defer server.Close() + c := rest.New(server.URL, "ABC") + callback(c, mux) +} + +func ptr[T any, PT *T](x T) PT { + return &x +} + +func withBlackBoxServer(t *testing.T, callback func(*rest.Client)) { + t.Helper() + handler, _, _ := testing_tools.BuildApiBlackBoxWithDBState(t, "../../server/testdata/store.sql", nil, false) + server := httptest.NewServer(handler) + defer server.Close() + c := rest.New(server.URL, "nbp_apTmlmUXHSC4PKmHwtIZNaGr8eqcVI2gMURp") + callback(c) +} diff --git a/management/client/rest/dns.go b/management/client/rest/dns.go new file mode 100644 index 000000000..ef9923b1f --- /dev/null +++ b/management/client/rest/dns.go @@ -0,0 +1,110 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// DNSAPI APIs for DNS Management, do not use directly +type DNSAPI struct { + c *Client +} + +// ListNameserverGroups list all nameserver groups +// See more: https://docs.netbird.io/api/resources/dns#list-all-nameserver-groups +func (a *DNSAPI) ListNameserverGroups(ctx context.Context) ([]api.NameserverGroup, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/dns/nameservers", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.NameserverGroup](resp) + return ret, err +} + +// GetNameserverGroup get nameserver group info +// See more: https://docs.netbird.io/api/resources/dns#retrieve-a-nameserver-group +func (a *DNSAPI) GetNameserverGroup(ctx context.Context, nameserverGroupID string) (*api.NameserverGroup, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/dns/nameservers/"+nameserverGroupID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.NameserverGroup](resp) + return &ret, err +} + +// CreateNameserverGroup create new nameserver group +// See more: https://docs.netbird.io/api/resources/dns#create-a-nameserver-group +func (a *DNSAPI) CreateNameserverGroup(ctx context.Context, request api.PostApiDnsNameserversJSONRequestBody) (*api.NameserverGroup, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "POST", "/api/dns/nameservers", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.NameserverGroup](resp) + return &ret, err +} + +// UpdateNameserverGroup update nameserver group info +// See more: https://docs.netbird.io/api/resources/dns#update-a-nameserver-group +func (a *DNSAPI) UpdateNameserverGroup(ctx context.Context, nameserverGroupID string, request api.PutApiDnsNameserversNsgroupIdJSONRequestBody) (*api.NameserverGroup, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/dns/nameservers/"+nameserverGroupID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.NameserverGroup](resp) + return &ret, err +} + +// DeleteNameserverGroup delete nameserver group +// See more: https://docs.netbird.io/api/resources/dns#delete-a-nameserver-group +func (a *DNSAPI) DeleteNameserverGroup(ctx context.Context, nameserverGroupID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/dns/nameservers/"+nameserverGroupID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} + +// GetSettings get DNS settings +// See more: https://docs.netbird.io/api/resources/dns#retrieve-dns-settings +func (a *DNSAPI) GetSettings(ctx context.Context) (*api.DNSSettings, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/dns/settings", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.DNSSettings](resp) + return &ret, err +} + +// UpdateSettings update DNS settings +// See more: https://docs.netbird.io/api/resources/dns#update-dns-settings +func (a *DNSAPI) UpdateSettings(ctx context.Context, request api.PutApiDnsSettingsJSONRequestBody) (*api.DNSSettings, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/dns/settings", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.DNSSettings](resp) + return &ret, err +} diff --git a/management/client/rest/dns_test.go b/management/client/rest/dns_test.go new file mode 100644 index 000000000..b2e0a0bee --- /dev/null +++ b/management/client/rest/dns_test.go @@ -0,0 +1,301 @@ +//go:build integration +// +build integration + +package rest_test + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" +) + +var ( + testNameserverGroup = api.NameserverGroup{ + Id: "Test", + Name: "wow", + } + + testSettings = api.DNSSettings{ + DisabledManagementGroups: []string{"gone"}, + } +) + +func TestDNSNameserverGroup_List_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/nameservers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.NameserverGroup{testNameserverGroup}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.ListNameserverGroups(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testNameserverGroup, ret[0]) + }) +} + +func TestDNSNameserverGroup_List_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/nameservers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.ListNameserverGroups(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestDNSNameserverGroup_Get_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/nameservers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testNameserverGroup) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.GetNameserverGroup(context.Background(), "Test") + require.NoError(t, err) + assert.Equal(t, testNameserverGroup, *ret) + }) +} + +func TestDNSNameserverGroup_Get_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/nameservers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.GetNameserverGroup(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestDNSNameserverGroup_Create_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/nameservers", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostApiDnsNameserversJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testNameserverGroup) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.CreateNameserverGroup(context.Background(), api.PostApiDnsNameserversJSONRequestBody{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testNameserverGroup, *ret) + }) +} + +func TestDNSNameserverGroup_Create_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/nameservers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.CreateNameserverGroup(context.Background(), api.PostApiDnsNameserversJSONRequestBody{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestDNSNameserverGroup_Update_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/nameservers/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiDnsNameserversNsgroupIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testNameserverGroup) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.UpdateNameserverGroup(context.Background(), "Test", api.PutApiDnsNameserversNsgroupIdJSONRequestBody{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testNameserverGroup, *ret) + }) +} + +func TestDNSNameserverGroup_Update_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/nameservers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.UpdateNameserverGroup(context.Background(), "Test", api.PutApiDnsNameserversNsgroupIdJSONRequestBody{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestDNSNameserverGroup_Delete_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/nameservers/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.DNS.DeleteNameserverGroup(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestDNSNameserverGroup_Delete_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/nameservers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.DNS.DeleteNameserverGroup(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestDNSSettings_Get_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/settings", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testSettings) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.GetSettings(context.Background()) + require.NoError(t, err) + assert.Equal(t, testSettings, *ret) + }) +} + +func TestDNSSettings_Get_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/settings", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.GetSettings(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestDNSSettings_Update_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/settings", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiDnsSettingsJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, []string{"test"}, req.DisabledManagementGroups) + retBytes, _ := json.Marshal(testSettings) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.UpdateSettings(context.Background(), api.PutApiDnsSettingsJSONRequestBody{ + DisabledManagementGroups: []string{"test"}, + }) + require.NoError(t, err) + assert.Equal(t, testSettings, *ret) + }) +} + +func TestDNSSettings_Update_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/settings", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.UpdateSettings(context.Background(), api.PutApiDnsSettingsJSONRequestBody{ + DisabledManagementGroups: []string{"test"}, + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestDNS_Integration(t *testing.T) { + nsGroupReq := api.NameserverGroupRequest{ + Description: "Test", + Enabled: true, + Domains: []string{}, + Groups: []string{"cs1tnh0hhcjnqoiuebeg"}, + Name: "test", + Nameservers: []api.Nameserver{ + { + Ip: "8.8.8.8", + NsType: api.NameserverNsTypeUdp, + Port: 53, + }, + }, + Primary: true, + SearchDomainsEnabled: false, + } + withBlackBoxServer(t, func(c *rest.Client) { + // Create + nsGroup, err := c.DNS.CreateNameserverGroup(context.Background(), nsGroupReq) + require.NoError(t, err) + + // List + nsGroups, err := c.DNS.ListNameserverGroups(context.Background()) + require.NoError(t, err) + assert.Equal(t, *nsGroup, nsGroups[0]) + + // Update + nsGroupReq.Description = "TestUpdate" + nsGroup, err = c.DNS.UpdateNameserverGroup(context.Background(), nsGroup.Id, nsGroupReq) + require.NoError(t, err) + assert.Equal(t, "TestUpdate", nsGroup.Description) + + // Delete + err = c.DNS.DeleteNameserverGroup(context.Background(), nsGroup.Id) + require.NoError(t, err) + + // List again to ensure deletion + nsGroups, err = c.DNS.ListNameserverGroups(context.Background()) + require.NoError(t, err) + assert.Len(t, nsGroups, 0) + }) +} diff --git a/management/client/rest/events.go b/management/client/rest/events.go new file mode 100644 index 000000000..1157700ff --- /dev/null +++ b/management/client/rest/events.go @@ -0,0 +1,24 @@ +package rest + +import ( + "context" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// EventsAPI APIs for Events, do not use directly +type EventsAPI struct { + c *Client +} + +// List list all events +// See more: https://docs.netbird.io/api/resources/events#list-all-events +func (a *EventsAPI) List(ctx context.Context) ([]api.Event, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/events", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.Event](resp) + return ret, err +} diff --git a/management/client/rest/events_test.go b/management/client/rest/events_test.go new file mode 100644 index 000000000..2589193a2 --- /dev/null +++ b/management/client/rest/events_test.go @@ -0,0 +1,70 @@ +//go:build integration +// +build integration + +package rest_test + +import ( + "context" + "encoding/json" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" +) + +var ( + testEvent = api.Event{ + Activity: "AccountCreate", + ActivityCode: api.EventActivityCodeAccountCreate, + } +) + +func TestEvents_List_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/events", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.Event{testEvent}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Events.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testEvent, ret[0]) + }) +} + +func TestEvents_List_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/events", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Events.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestEvents_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *rest.Client) { + // Do something that would trigger any event + _, err := c.SetupKeys.Create(context.Background(), api.CreateSetupKeyRequest{ + Ephemeral: ptr(true), + Name: "TestSetupKey", + Type: "reusable", + }) + require.NoError(t, err) + + events, err := c.Events.List(context.Background()) + require.NoError(t, err) + assert.NotEmpty(t, events) + }) +} diff --git a/management/client/rest/geo.go b/management/client/rest/geo.go new file mode 100644 index 000000000..ed9090fe2 --- /dev/null +++ b/management/client/rest/geo.go @@ -0,0 +1,36 @@ +package rest + +import ( + "context" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// GeoLocationAPI APIs for Geo-Location, do not use directly +type GeoLocationAPI struct { + c *Client +} + +// ListCountries list all country codes +// See more: https://docs.netbird.io/api/resources/geo-locations#list-all-country-codes +func (a *GeoLocationAPI) ListCountries(ctx context.Context) ([]api.Country, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/locations/countries", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.Country](resp) + return ret, err +} + +// ListCountryCities Get a list of all English city names for a given country code +// See more: https://docs.netbird.io/api/resources/geo-locations#list-all-city-names-by-country +func (a *GeoLocationAPI) ListCountryCities(ctx context.Context, countryCode string) ([]api.City, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/locations/countries/"+countryCode+"/cities", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.City](resp) + return ret, err +} diff --git a/management/client/rest/geo_test.go b/management/client/rest/geo_test.go new file mode 100644 index 000000000..d24405094 --- /dev/null +++ b/management/client/rest/geo_test.go @@ -0,0 +1,101 @@ +//go:build integration +// +build integration + +package rest_test + +import ( + "context" + "encoding/json" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" +) + +var ( + testCountry = api.Country{ + CountryCode: "DE", + CountryName: "Germany", + } + + testCity = api.City{ + CityName: "Berlin", + GeonameId: 2950158, + } +) + +func TestGeo_ListCountries_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/locations/countries", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.Country{testCountry}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.GeoLocation.ListCountries(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testCountry, ret[0]) + }) +} + +func TestGeo_ListCountries_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/locations/countries", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.GeoLocation.ListCountries(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestGeo_ListCountryCities_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/locations/countries/Test/cities", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.City{testCity}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.GeoLocation.ListCountryCities(context.Background(), "Test") + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testCity, ret[0]) + }) +} + +func TestGeo_ListCountryCities_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/locations/countries/Test/cities", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.GeoLocation.ListCountryCities(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestGeo_Integration(t *testing.T) { + // Blackbox is initialized with empty GeoLocations + withBlackBoxServer(t, func(c *rest.Client) { + countries, err := c.GeoLocation.ListCountries(context.Background()) + require.NoError(t, err) + assert.Empty(t, countries) + + cities, err := c.GeoLocation.ListCountryCities(context.Background(), "DE") + require.NoError(t, err) + assert.Empty(t, cities) + }) +} diff --git a/management/client/rest/groups.go b/management/client/rest/groups.go new file mode 100644 index 000000000..feb664273 --- /dev/null +++ b/management/client/rest/groups.go @@ -0,0 +1,82 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// GroupsAPI APIs for Groups, do not use directly +type GroupsAPI struct { + c *Client +} + +// List list all groups +// See more: https://docs.netbird.io/api/resources/groups#list-all-groups +func (a *GroupsAPI) List(ctx context.Context) ([]api.Group, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/groups", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.Group](resp) + return ret, err +} + +// Get get group info +// See more: https://docs.netbird.io/api/resources/groups#retrieve-a-group +func (a *GroupsAPI) Get(ctx context.Context, groupID string) (*api.Group, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/groups/"+groupID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Group](resp) + return &ret, err +} + +// Create create new group +// See more: https://docs.netbird.io/api/resources/groups#create-a-group +func (a *GroupsAPI) Create(ctx context.Context, request api.PostApiGroupsJSONRequestBody) (*api.Group, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "POST", "/api/groups", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Group](resp) + return &ret, err +} + +// Update update group info +// See more: https://docs.netbird.io/api/resources/groups#update-a-group +func (a *GroupsAPI) Update(ctx context.Context, groupID string, request api.PutApiGroupsGroupIdJSONRequestBody) (*api.Group, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/groups/"+groupID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Group](resp) + return &ret, err +} + +// Delete delete group +// See more: https://docs.netbird.io/api/resources/groups#delete-a-group +func (a *GroupsAPI) Delete(ctx context.Context, groupID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/groups/"+groupID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} diff --git a/management/client/rest/groups_test.go b/management/client/rest/groups_test.go new file mode 100644 index 000000000..d6a5410e0 --- /dev/null +++ b/management/client/rest/groups_test.go @@ -0,0 +1,215 @@ +//go:build integration +// +build integration + +package rest_test + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" +) + +var ( + testGroup = api.Group{ + Id: "Test", + Name: "wow", + PeersCount: 0, + } +) + +func TestGroups_List_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.Group{testGroup}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Groups.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testGroup, ret[0]) + }) +} + +func TestGroups_List_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Groups.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestGroups_Get_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/groups/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testGroup) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Groups.Get(context.Background(), "Test") + require.NoError(t, err) + assert.Equal(t, testGroup, *ret) + }) +} + +func TestGroups_Get_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/groups/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Groups.Get(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestGroups_Create_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostApiGroupsJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testGroup) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Groups.Create(context.Background(), api.PostApiGroupsJSONRequestBody{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testGroup, *ret) + }) +} + +func TestGroups_Create_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Groups.Create(context.Background(), api.PostApiGroupsJSONRequestBody{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestGroups_Update_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/groups/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiGroupsGroupIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testGroup) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Groups.Update(context.Background(), "Test", api.PutApiGroupsGroupIdJSONRequestBody{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testGroup, *ret) + }) +} + +func TestGroups_Update_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/groups/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Groups.Update(context.Background(), "Test", api.PutApiGroupsGroupIdJSONRequestBody{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestGroups_Delete_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/groups/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Groups.Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestGroups_Delete_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/groups/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Groups.Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestGroups_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *rest.Client) { + groups, err := c.Groups.List(context.Background()) + require.NoError(t, err) + assert.Len(t, groups, 1) + + group, err := c.Groups.Create(context.Background(), api.GroupRequest{ + Name: "Test", + }) + require.NoError(t, err) + assert.Equal(t, "Test", group.Name) + assert.NotEmpty(t, group.Id) + + group, err = c.Groups.Update(context.Background(), group.Id, api.GroupRequest{ + Name: "Testnt", + }) + require.NoError(t, err) + assert.Equal(t, "Testnt", group.Name) + + err = c.Groups.Delete(context.Background(), group.Id) + require.NoError(t, err) + + groups, err = c.Groups.List(context.Background()) + require.NoError(t, err) + assert.Len(t, groups, 1) + }) +} diff --git a/management/client/rest/networks.go b/management/client/rest/networks.go new file mode 100644 index 000000000..2cdd6d73d --- /dev/null +++ b/management/client/rest/networks.go @@ -0,0 +1,246 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// NetworksAPI APIs for Networks, do not use directly +type NetworksAPI struct { + c *Client +} + +// List list all networks +// See more: https://docs.netbird.io/api/resources/networks#list-all-networks +func (a *NetworksAPI) List(ctx context.Context) ([]api.Network, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/networks", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.Network](resp) + return ret, err +} + +// Get get network info +// See more: https://docs.netbird.io/api/resources/networks#retrieve-a-network +func (a *NetworksAPI) Get(ctx context.Context, networkID string) (*api.Network, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/networks/"+networkID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Network](resp) + return &ret, err +} + +// Create create new network +// See more: https://docs.netbird.io/api/resources/networks#create-a-network +func (a *NetworksAPI) Create(ctx context.Context, request api.PostApiNetworksJSONRequestBody) (*api.Network, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "POST", "/api/networks", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Network](resp) + return &ret, err +} + +// Update update network +// See more: https://docs.netbird.io/api/resources/networks#update-a-network +func (a *NetworksAPI) Update(ctx context.Context, networkID string, request api.PutApiNetworksNetworkIdJSONRequestBody) (*api.Network, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/networks/"+networkID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Network](resp) + return &ret, err +} + +// Delete delete network +// See more: https://docs.netbird.io/api/resources/networks#delete-a-network +func (a *NetworksAPI) Delete(ctx context.Context, networkID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/networks/"+networkID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} + +// NetworkResourcesAPI APIs for Network Resources, do not use directly +type NetworkResourcesAPI struct { + c *Client + networkID string +} + +// Resources APIs for network resources +func (a *NetworksAPI) Resources(networkID string) *NetworkResourcesAPI { + return &NetworkResourcesAPI{ + c: a.c, + networkID: networkID, + } +} + +// List list all resources in networks +// See more: https://docs.netbird.io/api/resources/networks#list-all-network-resources +func (a *NetworkResourcesAPI) List(ctx context.Context) ([]api.NetworkResource, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/networks/"+a.networkID+"/resources", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.NetworkResource](resp) + return ret, err +} + +// Get get network resource info +// See more: https://docs.netbird.io/api/resources/networks#retrieve-a-network-resource +func (a *NetworkResourcesAPI) Get(ctx context.Context, networkResourceID string) (*api.NetworkResource, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/networks/"+a.networkID+"/resources/"+networkResourceID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.NetworkResource](resp) + return &ret, err +} + +// Create create new network resource +// See more: https://docs.netbird.io/api/resources/networks#create-a-network-resource +func (a *NetworkResourcesAPI) Create(ctx context.Context, request api.PostApiNetworksNetworkIdResourcesJSONRequestBody) (*api.NetworkResource, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "POST", "/api/networks/"+a.networkID+"/resources", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.NetworkResource](resp) + return &ret, err +} + +// Update update network resource +// See more: https://docs.netbird.io/api/resources/networks#update-a-network-resource +func (a *NetworkResourcesAPI) Update(ctx context.Context, networkResourceID string, request api.PutApiNetworksNetworkIdResourcesResourceIdJSONRequestBody) (*api.NetworkResource, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/networks/"+a.networkID+"/resources/"+networkResourceID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.NetworkResource](resp) + return &ret, err +} + +// Delete delete network resource +// See more: https://docs.netbird.io/api/resources/networks#delete-a-network-resource +func (a *NetworkResourcesAPI) Delete(ctx context.Context, networkResourceID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/networks/"+a.networkID+"/resources/"+networkResourceID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} + +// NetworkRoutersAPI APIs for Network Routers, do not use directly +type NetworkRoutersAPI struct { + c *Client + networkID string +} + +// Routers APIs for network routers +func (a *NetworksAPI) Routers(networkID string) *NetworkRoutersAPI { + return &NetworkRoutersAPI{ + c: a.c, + networkID: networkID, + } +} + +// List list all routers in networks +// See more: https://docs.netbird.io/api/routers/networks#list-all-network-routers +func (a *NetworkRoutersAPI) List(ctx context.Context) ([]api.NetworkRouter, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/networks/"+a.networkID+"/routers", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.NetworkRouter](resp) + return ret, err +} + +// Get get network router info +// See more: https://docs.netbird.io/api/routers/networks#retrieve-a-network-router +func (a *NetworkRoutersAPI) Get(ctx context.Context, networkRouterID string) (*api.NetworkRouter, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/networks/"+a.networkID+"/routers/"+networkRouterID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.NetworkRouter](resp) + return &ret, err +} + +// Create create new network router +// See more: https://docs.netbird.io/api/routers/networks#create-a-network-router +func (a *NetworkRoutersAPI) Create(ctx context.Context, request api.PostApiNetworksNetworkIdRoutersJSONRequestBody) (*api.NetworkRouter, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "POST", "/api/networks/"+a.networkID+"/routers", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.NetworkRouter](resp) + return &ret, err +} + +// Update update network router +// See more: https://docs.netbird.io/api/routers/networks#update-a-network-router +func (a *NetworkRoutersAPI) Update(ctx context.Context, networkRouterID string, request api.PutApiNetworksNetworkIdRoutersRouterIdJSONRequestBody) (*api.NetworkRouter, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/networks/"+a.networkID+"/routers/"+networkRouterID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.NetworkRouter](resp) + return &ret, err +} + +// Delete delete network router +// See more: https://docs.netbird.io/api/routers/networks#delete-a-network-router +func (a *NetworkRoutersAPI) Delete(ctx context.Context, networkRouterID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/networks/"+a.networkID+"/routers/"+networkRouterID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} diff --git a/management/client/rest/networks_test.go b/management/client/rest/networks_test.go new file mode 100644 index 000000000..0772d7540 --- /dev/null +++ b/management/client/rest/networks_test.go @@ -0,0 +1,594 @@ +//go:build integration +// +build integration + +package rest_test + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" +) + +var ( + testNetwork = api.Network{ + Id: "Test", + Name: "wow", + } + + testNetworkResource = api.NetworkResource{ + Description: ptr("meaw"), + Id: "awa", + } + + testNetworkRouter = api.NetworkRouter{ + Id: "ouch", + } +) + +func TestNetworks_List_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.Network{testNetwork}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testNetwork, ret[0]) + }) +} + +func TestNetworks_List_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestNetworks_Get_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testNetwork) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Get(context.Background(), "Test") + require.NoError(t, err) + assert.Equal(t, testNetwork, *ret) + }) +} + +func TestNetworks_Get_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Get(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestNetworks_Create_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostApiNetworksJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testNetwork) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Create(context.Background(), api.PostApiNetworksJSONRequestBody{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testNetwork, *ret) + }) +} + +func TestNetworks_Create_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Create(context.Background(), api.PostApiNetworksJSONRequestBody{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestNetworks_Update_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiNetworksNetworkIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testNetwork) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Update(context.Background(), "Test", api.PutApiNetworksNetworkIdJSONRequestBody{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testNetwork, *ret) + }) +} + +func TestNetworks_Update_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Update(context.Background(), "Test", api.PutApiNetworksNetworkIdJSONRequestBody{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestNetworks_Delete_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Networks.Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestNetworks_Delete_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Networks.Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestNetworks_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *rest.Client) { + network, err := c.Networks.Create(context.Background(), api.NetworkRequest{ + Description: ptr("TestNetwork"), + Name: "Test", + }) + assert.NoError(t, err) + assert.Equal(t, "Test", network.Name) + + networks, err := c.Networks.List(context.Background()) + assert.NoError(t, err) + assert.Empty(t, networks) + + network, err = c.Networks.Update(context.Background(), "TestID", api.NetworkRequest{ + Description: ptr("TestNetwork?"), + Name: "Test", + }) + + assert.NoError(t, err) + assert.Equal(t, "TestNetwork?", *network.Description) + + err = c.Networks.Delete(context.Background(), "TestID") + assert.NoError(t, err) + }) +} + +func TestNetworkResources_List_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/resources", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.NetworkResource{testNetworkResource}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Resources("Meow").List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testNetworkResource, ret[0]) + }) +} + +func TestNetworkResources_List_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/resources", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Resources("Meow").List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestNetworkResources_Get_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/resources/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testNetworkResource) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Resources("Meow").Get(context.Background(), "Test") + require.NoError(t, err) + assert.Equal(t, testNetworkResource, *ret) + }) +} + +func TestNetworkResources_Get_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/resources/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Resources("Meow").Get(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestNetworkResources_Create_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/resources", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostApiNetworksNetworkIdResourcesJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testNetworkResource) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Resources("Meow").Create(context.Background(), api.PostApiNetworksNetworkIdResourcesJSONRequestBody{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testNetworkResource, *ret) + }) +} + +func TestNetworkResources_Create_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/resources", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Resources("Meow").Create(context.Background(), api.PostApiNetworksNetworkIdResourcesJSONRequestBody{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestNetworkResources_Update_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/resources/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiNetworksNetworkIdResourcesResourceIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testNetworkResource) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Resources("Meow").Update(context.Background(), "Test", api.PutApiNetworksNetworkIdResourcesResourceIdJSONRequestBody{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testNetworkResource, *ret) + }) +} + +func TestNetworkResources_Update_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/resources/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Resources("Meow").Update(context.Background(), "Test", api.PutApiNetworksNetworkIdResourcesResourceIdJSONRequestBody{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestNetworkResources_Delete_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/resources/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Networks.Resources("Meow").Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestNetworkResources_Delete_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/resources/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Networks.Resources("Meow").Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestNetworkResources_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *rest.Client) { + _, err := c.Networks.Resources("TestNetwork").Create(context.Background(), api.NetworkResourceRequest{ + Address: "test.com", + Description: ptr("Description"), + Enabled: false, + Groups: []string{"test"}, + Name: "test", + }) + assert.NoError(t, err) + + _, err = c.Networks.Resources("TestNetwork").List(context.Background()) + assert.NoError(t, err) + + _, err = c.Networks.Resources("TestNetwork").Get(context.Background(), "TestResource") + assert.NoError(t, err) + + _, err = c.Networks.Resources("TestNetwork").Update(context.Background(), "TestResource", api.NetworkResourceRequest{ + Address: "testnt.com", + }) + assert.NoError(t, err) + + err = c.Networks.Resources("TestNetwork").Delete(context.Background(), "TestResource") + assert.NoError(t, err) + }) +} + +func TestNetworkRouters_List_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/routers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.NetworkRouter{testNetworkRouter}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Routers("Meow").List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testNetworkRouter, ret[0]) + }) +} + +func TestNetworkRouters_List_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/routers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Routers("Meow").List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestNetworkRouters_Get_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/routers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testNetworkRouter) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Routers("Meow").Get(context.Background(), "Test") + require.NoError(t, err) + assert.Equal(t, testNetworkRouter, *ret) + }) +} + +func TestNetworkRouters_Get_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/routers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Routers("Meow").Get(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestNetworkRouters_Create_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/routers", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostApiNetworksNetworkIdRoutersJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "test", *req.Peer) + retBytes, _ := json.Marshal(testNetworkRouter) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Routers("Meow").Create(context.Background(), api.PostApiNetworksNetworkIdRoutersJSONRequestBody{ + Peer: ptr("test"), + }) + require.NoError(t, err) + assert.Equal(t, testNetworkRouter, *ret) + }) +} + +func TestNetworkRouters_Create_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/routers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Routers("Meow").Create(context.Background(), api.PostApiNetworksNetworkIdRoutersJSONRequestBody{ + Peer: ptr("test"), + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestNetworkRouters_Update_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/routers/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiNetworksNetworkIdRoutersRouterIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "test", *req.Peer) + retBytes, _ := json.Marshal(testNetworkRouter) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Routers("Meow").Update(context.Background(), "Test", api.PutApiNetworksNetworkIdRoutersRouterIdJSONRequestBody{ + Peer: ptr("test"), + }) + require.NoError(t, err) + assert.Equal(t, testNetworkRouter, *ret) + }) +} + +func TestNetworkRouters_Update_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/routers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Routers("Meow").Update(context.Background(), "Test", api.PutApiNetworksNetworkIdRoutersRouterIdJSONRequestBody{ + Peer: ptr("test"), + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestNetworkRouters_Delete_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/routers/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Networks.Routers("Meow").Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestNetworkRouters_Delete_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/routers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Networks.Routers("Meow").Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestNetworkRouters_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *rest.Client) { + _, err := c.Networks.Routers("TestNetwork").Create(context.Background(), api.NetworkRouterRequest{ + Enabled: false, + Masquerade: false, + Metric: 9999, + PeerGroups: ptr([]string{"test"}), + }) + assert.NoError(t, err) + + _, err = c.Networks.Routers("TestNetwork").List(context.Background()) + assert.NoError(t, err) + + _, err = c.Networks.Routers("TestNetwork").Get(context.Background(), "TestRouter") + assert.NoError(t, err) + + _, err = c.Networks.Routers("TestNetwork").Update(context.Background(), "TestRouter", api.NetworkRouterRequest{ + Enabled: true, + }) + assert.NoError(t, err) + + err = c.Networks.Routers("TestNetwork").Delete(context.Background(), "TestRouter") + assert.NoError(t, err) + }) +} diff --git a/management/client/rest/peers.go b/management/client/rest/peers.go new file mode 100644 index 000000000..9d35f013c --- /dev/null +++ b/management/client/rest/peers.go @@ -0,0 +1,78 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// PeersAPI APIs for peers, do not use directly +type PeersAPI struct { + c *Client +} + +// List list all peers +// See more: https://docs.netbird.io/api/resources/peers#list-all-peers +func (a *PeersAPI) List(ctx context.Context) ([]api.Peer, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/peers", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.Peer](resp) + return ret, err +} + +// Get retrieve a peer +// See more: https://docs.netbird.io/api/resources/peers#retrieve-a-peer +func (a *PeersAPI) Get(ctx context.Context, peerID string) (*api.Peer, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/peers/"+peerID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Peer](resp) + return &ret, err +} + +// Update update information for a peer +// See more: https://docs.netbird.io/api/resources/peers#update-a-peer +func (a *PeersAPI) Update(ctx context.Context, peerID string, request api.PutApiPeersPeerIdJSONRequestBody) (*api.Peer, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/peers/"+peerID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Peer](resp) + return &ret, err +} + +// Delete delete a peer +// See more: https://docs.netbird.io/api/resources/peers#delete-a-peer +func (a *PeersAPI) Delete(ctx context.Context, peerID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/peers/"+peerID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} + +// ListAccessiblePeers list all peers that the specified peer can connect to within the network +// See more: https://docs.netbird.io/api/resources/peers#list-accessible-peers +func (a *PeersAPI) ListAccessiblePeers(ctx context.Context, peerID string) ([]api.Peer, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/peers/"+peerID+"/accessible-peers", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.Peer](resp) + return ret, err +} diff --git a/management/client/rest/peers_test.go b/management/client/rest/peers_test.go new file mode 100644 index 000000000..4c5cd1e60 --- /dev/null +++ b/management/client/rest/peers_test.go @@ -0,0 +1,208 @@ +//go:build integration +// +build integration + +package rest_test + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" +) + +var ( + testPeer = api.Peer{ + ApprovalRequired: false, + Connected: false, + ConnectionIp: "127.0.0.1", + DnsLabel: "test", + Id: "Test", + } +) + +func TestPeers_List_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.Peer{testPeer}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testPeer, ret[0]) + }) +} + +func TestPeers_List_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestPeers_Get_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testPeer) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.Get(context.Background(), "Test") + require.NoError(t, err) + assert.Equal(t, testPeer, *ret) + }) +} + +func TestPeers_Get_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.Get(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestPeers_Update_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiPeersPeerIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, true, req.InactivityExpirationEnabled) + retBytes, _ := json.Marshal(testPeer) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.Update(context.Background(), "Test", api.PutApiPeersPeerIdJSONRequestBody{ + InactivityExpirationEnabled: true, + }) + require.NoError(t, err) + assert.Equal(t, testPeer, *ret) + }) +} + +func TestPeers_Update_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.Update(context.Background(), "Test", api.PutApiPeersPeerIdJSONRequestBody{ + InactivityExpirationEnabled: false, + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestPeers_Delete_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Peers.Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestPeers_Delete_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Peers.Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestPeers_ListAccessiblePeers_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test/accessible-peers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.Peer{testPeer}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.ListAccessiblePeers(context.Background(), "Test") + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testPeer, ret[0]) + }) +} + +func TestPeers_ListAccessiblePeers_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test/accessible-peers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.ListAccessiblePeers(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestPeers_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *rest.Client) { + peers, err := c.Peers.List(context.Background()) + require.NoError(t, err) + require.NotEmpty(t, peers) + + peer, err := c.Peers.Get(context.Background(), peers[0].Id) + require.NoError(t, err) + assert.Equal(t, peers[0].Id, peer.Id) + + peer, err = c.Peers.Update(context.Background(), peer.Id, api.PeerRequest{ + LoginExpirationEnabled: true, + Name: "Test", + SshEnabled: false, + ApprovalRequired: ptr(false), + InactivityExpirationEnabled: false, + }) + require.NoError(t, err) + assert.Equal(t, true, peer.LoginExpirationEnabled) + + accessiblePeers, err := c.Peers.ListAccessiblePeers(context.Background(), peer.Id) + require.NoError(t, err) + assert.Empty(t, accessiblePeers) + + err = c.Peers.Delete(context.Background(), peer.Id) + require.NoError(t, err) + }) +} diff --git a/management/client/rest/policies.go b/management/client/rest/policies.go new file mode 100644 index 000000000..be6abafaf --- /dev/null +++ b/management/client/rest/policies.go @@ -0,0 +1,82 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// PoliciesAPI APIs for Policies, do not use directly +type PoliciesAPI struct { + c *Client +} + +// List list all policies +// See more: https://docs.netbird.io/api/resources/policies#list-all-policies +func (a *PoliciesAPI) List(ctx context.Context) ([]api.Policy, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/policies", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.Policy](resp) + return ret, err +} + +// Get get policy info +// See more: https://docs.netbird.io/api/resources/policies#retrieve-a-policy +func (a *PoliciesAPI) Get(ctx context.Context, policyID string) (*api.Policy, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/policies/"+policyID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Policy](resp) + return &ret, err +} + +// Create create new policy +// See more: https://docs.netbird.io/api/resources/policies#create-a-policy +func (a *PoliciesAPI) Create(ctx context.Context, request api.PostApiPoliciesJSONRequestBody) (*api.Policy, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "POST", "/api/policies", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Policy](resp) + return &ret, err +} + +// Update update policy info +// See more: https://docs.netbird.io/api/resources/policies#update-a-policy +func (a *PoliciesAPI) Update(ctx context.Context, policyID string, request api.PutApiPoliciesPolicyIdJSONRequestBody) (*api.Policy, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/policies/"+policyID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Policy](resp) + return &ret, err +} + +// Delete delete policy +// See more: https://docs.netbird.io/api/resources/policies#delete-a-policy +func (a *PoliciesAPI) Delete(ctx context.Context, policyID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/policies/"+policyID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} diff --git a/management/client/rest/policies_test.go b/management/client/rest/policies_test.go new file mode 100644 index 000000000..5792048df --- /dev/null +++ b/management/client/rest/policies_test.go @@ -0,0 +1,241 @@ +//go:build integration +// +build integration + +package rest_test + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" +) + +var ( + testPolicy = api.Policy{ + Name: "wow", + Id: ptr("Test"), + Enabled: false, + } +) + +func TestPolicies_List_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/policies", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.Policy{testPolicy}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Policies.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testPolicy, ret[0]) + }) +} + +func TestPolicies_List_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/policies", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Policies.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestPolicies_Get_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/policies/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testPolicy) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Policies.Get(context.Background(), "Test") + require.NoError(t, err) + assert.Equal(t, testPolicy, *ret) + }) +} + +func TestPolicies_Get_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/policies/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Policies.Get(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestPolicies_Create_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/policies", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiPoliciesPolicyIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testPolicy) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Policies.Create(context.Background(), api.PostApiPoliciesJSONRequestBody{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testPolicy, *ret) + }) +} + +func TestPolicies_Create_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/policies", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Policies.Create(context.Background(), api.PostApiPoliciesJSONRequestBody{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestPolicies_Update_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/policies/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiPoliciesPolicyIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testPolicy) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Policies.Update(context.Background(), "Test", api.PutApiPoliciesPolicyIdJSONRequestBody{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testPolicy, *ret) + }) +} + +func TestPolicies_Update_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/policies/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Policies.Update(context.Background(), "Test", api.PutApiPoliciesPolicyIdJSONRequestBody{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestPolicies_Delete_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/policies/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Policies.Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestPolicies_Delete_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/policies/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Policies.Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestPolicies_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *rest.Client) { + policies, err := c.Policies.List(context.Background()) + require.NoError(t, err) + require.NotEmpty(t, policies) + + policy, err := c.Policies.Get(context.Background(), *policies[0].Id) + require.NoError(t, err) + assert.Equal(t, *policies[0].Id, *policy.Id) + + policy, err = c.Policies.Update(context.Background(), *policy.Id, api.PolicyCreate{ + Description: ptr("Test Policy"), + Enabled: false, + Name: "Test", + Rules: []api.PolicyRuleUpdate{ + { + Action: api.PolicyRuleUpdateAction(policy.Rules[0].Action), + Bidirectional: true, + Description: ptr("Test Policy"), + Sources: ptr([]string{(*policy.Rules[0].Sources)[0].Id}), + Destinations: ptr([]string{(*policy.Rules[0].Destinations)[0].Id}), + Enabled: false, + Protocol: api.PolicyRuleUpdateProtocolAll, + }, + }, + SourcePostureChecks: nil, + }) + require.NoError(t, err) + assert.Equal(t, "Test Policy", *policy.Rules[0].Description) + + policy, err = c.Policies.Create(context.Background(), api.PolicyUpdate{ + Description: ptr("Test Policy 2"), + Enabled: false, + Name: "Test", + Rules: []api.PolicyRuleUpdate{ + { + Action: api.PolicyRuleUpdateAction(policy.Rules[0].Action), + Bidirectional: true, + Description: ptr("Test Policy 2"), + Sources: ptr([]string{(*policy.Rules[0].Sources)[0].Id}), + Destinations: ptr([]string{(*policy.Rules[0].Destinations)[0].Id}), + Enabled: false, + Protocol: api.PolicyRuleUpdateProtocolAll, + }, + }, + SourcePostureChecks: nil, + }) + require.NoError(t, err) + + err = c.Policies.Delete(context.Background(), *policy.Id) + require.NoError(t, err) + }) +} diff --git a/management/client/rest/posturechecks.go b/management/client/rest/posturechecks.go new file mode 100644 index 000000000..950d17ba0 --- /dev/null +++ b/management/client/rest/posturechecks.go @@ -0,0 +1,82 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// PostureChecksAPI APIs for PostureChecks, do not use directly +type PostureChecksAPI struct { + c *Client +} + +// List list all posture checks +// See more: https://docs.netbird.io/api/resources/posture-checks#list-all-posture-checks +func (a *PostureChecksAPI) List(ctx context.Context) ([]api.PostureCheck, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/posture-checks", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.PostureCheck](resp) + return ret, err +} + +// Get get posture check info +// See more: https://docs.netbird.io/api/resources/posture-checks#retrieve-a-posture-check +func (a *PostureChecksAPI) Get(ctx context.Context, postureCheckID string) (*api.PostureCheck, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/posture-checks/"+postureCheckID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.PostureCheck](resp) + return &ret, err +} + +// Create create new posture check +// See more: https://docs.netbird.io/api/resources/posture-checks#create-a-posture-check +func (a *PostureChecksAPI) Create(ctx context.Context, request api.PostApiPostureChecksJSONRequestBody) (*api.PostureCheck, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "POST", "/api/posture-checks", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.PostureCheck](resp) + return &ret, err +} + +// Update update posture check info +// See more: https://docs.netbird.io/api/resources/posture-checks#update-a-posture-check +func (a *PostureChecksAPI) Update(ctx context.Context, postureCheckID string, request api.PutApiPostureChecksPostureCheckIdJSONRequestBody) (*api.PostureCheck, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/posture-checks/"+postureCheckID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.PostureCheck](resp) + return &ret, err +} + +// Delete delete posture check +// See more: https://docs.netbird.io/api/resources/posture-checks#delete-a-posture-check +func (a *PostureChecksAPI) Delete(ctx context.Context, postureCheckID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/posture-checks/"+postureCheckID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} diff --git a/management/client/rest/posturechecks_test.go b/management/client/rest/posturechecks_test.go new file mode 100644 index 000000000..a891d6ac9 --- /dev/null +++ b/management/client/rest/posturechecks_test.go @@ -0,0 +1,233 @@ +//go:build integration +// +build integration + +package rest_test + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" +) + +var ( + testPostureCheck = api.PostureCheck{ + Id: "Test", + Name: "wow", + } +) + +func TestPostureChecks_List_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/posture-checks", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.PostureCheck{testPostureCheck}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.PostureChecks.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testPostureCheck, ret[0]) + }) +} + +func TestPostureChecks_List_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/posture-checks", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.PostureChecks.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestPostureChecks_Get_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/posture-checks/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testPostureCheck) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.PostureChecks.Get(context.Background(), "Test") + require.NoError(t, err) + assert.Equal(t, testPostureCheck, *ret) + }) +} + +func TestPostureChecks_Get_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/posture-checks/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.PostureChecks.Get(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestPostureChecks_Create_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/posture-checks", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostureCheckUpdate + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testPostureCheck) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.PostureChecks.Create(context.Background(), api.PostureCheckUpdate{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testPostureCheck, *ret) + }) +} + +func TestPostureChecks_Create_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/posture-checks", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.PostureChecks.Create(context.Background(), api.PostureCheckUpdate{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestPostureChecks_Update_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/posture-checks/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostureCheckUpdate + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testPostureCheck) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.PostureChecks.Update(context.Background(), "Test", api.PostureCheckUpdate{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testPostureCheck, *ret) + }) +} + +func TestPostureChecks_Update_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/posture-checks/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.PostureChecks.Update(context.Background(), "Test", api.PostureCheckUpdate{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestPostureChecks_Delete_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/posture-checks/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.PostureChecks.Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestPostureChecks_Delete_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/posture-checks/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.PostureChecks.Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestPostureChecks_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *rest.Client) { + check, err := c.PostureChecks.Create(context.Background(), api.PostureCheckUpdate{ + Name: "Test", + Description: "Testing", + Checks: &api.Checks{ + OsVersionCheck: &api.OSVersionCheck{ + Windows: &api.MinKernelVersionCheck{ + MinKernelVersion: "0.0.0", + }, + }, + }, + }) + require.NoError(t, err) + assert.Equal(t, "Test", check.Name) + + checks, err := c.PostureChecks.List(context.Background()) + require.NoError(t, err) + assert.Len(t, checks, 1) + + check, err = c.PostureChecks.Update(context.Background(), check.Id, api.PostureCheckUpdate{ + Name: "Tests", + Description: "Testings", + Checks: &api.Checks{ + GeoLocationCheck: &api.GeoLocationCheck{ + Action: api.GeoLocationCheckActionAllow, Locations: []api.Location{ + { + CityName: ptr("Cairo"), + CountryCode: "EG", + }, + }, + }, + }, + }) + + require.NoError(t, err) + assert.Equal(t, "Testings", *check.Description) + + check, err = c.PostureChecks.Get(context.Background(), check.Id) + require.NoError(t, err) + assert.Equal(t, "Tests", check.Name) + + err = c.PostureChecks.Delete(context.Background(), check.Id) + require.NoError(t, err) + }) +} diff --git a/management/client/rest/routes.go b/management/client/rest/routes.go new file mode 100644 index 000000000..bccbb8847 --- /dev/null +++ b/management/client/rest/routes.go @@ -0,0 +1,82 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// RoutesAPI APIs for Routes, do not use directly +type RoutesAPI struct { + c *Client +} + +// List list all routes +// See more: https://docs.netbird.io/api/resources/routes#list-all-routes +func (a *RoutesAPI) List(ctx context.Context) ([]api.Route, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/routes", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.Route](resp) + return ret, err +} + +// Get get route info +// See more: https://docs.netbird.io/api/resources/routes#retrieve-a-route +func (a *RoutesAPI) Get(ctx context.Context, routeID string) (*api.Route, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/routes/"+routeID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Route](resp) + return &ret, err +} + +// Create create new route +// See more: https://docs.netbird.io/api/resources/routes#create-a-route +func (a *RoutesAPI) Create(ctx context.Context, request api.PostApiRoutesJSONRequestBody) (*api.Route, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "POST", "/api/routes", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Route](resp) + return &ret, err +} + +// Update update route info +// See more: https://docs.netbird.io/api/resources/routes#update-a-route +func (a *RoutesAPI) Update(ctx context.Context, routeID string, request api.PutApiRoutesRouteIdJSONRequestBody) (*api.Route, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/routes/"+routeID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Route](resp) + return &ret, err +} + +// Delete delete route +// See more: https://docs.netbird.io/api/resources/routes#delete-a-route +func (a *RoutesAPI) Delete(ctx context.Context, routeID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/routes/"+routeID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} diff --git a/management/client/rest/routes_test.go b/management/client/rest/routes_test.go new file mode 100644 index 000000000..1c698a7fb --- /dev/null +++ b/management/client/rest/routes_test.go @@ -0,0 +1,231 @@ +//go:build integration +// +build integration + +package rest_test + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" +) + +var ( + testRoute = api.Route{ + Id: "Test", + Domains: ptr([]string{"google.com"}), + } +) + +func TestRoutes_List_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/routes", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.Route{testRoute}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Routes.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testRoute, ret[0]) + }) +} + +func TestRoutes_List_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/routes", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Routes.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestRoutes_Get_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/routes/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testRoute) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Routes.Get(context.Background(), "Test") + require.NoError(t, err) + assert.Equal(t, testRoute, *ret) + }) +} + +func TestRoutes_Get_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/routes/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Routes.Get(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestRoutes_Create_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/routes", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostApiRoutesJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "meow", req.Description) + retBytes, _ := json.Marshal(testRoute) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Routes.Create(context.Background(), api.PostApiRoutesJSONRequestBody{ + Description: "meow", + }) + require.NoError(t, err) + assert.Equal(t, testRoute, *ret) + }) +} + +func TestRoutes_Create_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/routes", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Routes.Create(context.Background(), api.PostApiRoutesJSONRequestBody{ + Description: "meow", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestRoutes_Update_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/routes/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiRoutesRouteIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "meow", req.Description) + retBytes, _ := json.Marshal(testRoute) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Routes.Update(context.Background(), "Test", api.PutApiRoutesRouteIdJSONRequestBody{ + Description: "meow", + }) + require.NoError(t, err) + assert.Equal(t, testRoute, *ret) + }) +} + +func TestRoutes_Update_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/routes/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Routes.Update(context.Background(), "Test", api.PutApiRoutesRouteIdJSONRequestBody{ + Description: "meow", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestRoutes_Delete_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/routes/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Routes.Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestRoutes_Delete_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/routes/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Routes.Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestRoutes_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *rest.Client) { + route, err := c.Routes.Create(context.Background(), api.RouteRequest{ + Description: "Meow", + Enabled: false, + Groups: []string{"cs1tnh0hhcjnqoiuebeg"}, + PeerGroups: ptr([]string{"cs1tnh0hhcjnqoiuebeg"}), + Domains: ptr([]string{"google.com"}), + Masquerade: true, + Metric: 9999, + KeepRoute: false, + NetworkId: "Test", + }) + + require.NoError(t, err) + assert.Equal(t, "Test", route.NetworkId) + + routes, err := c.Routes.List(context.Background()) + require.NoError(t, err) + assert.Len(t, routes, 1) + + route, err = c.Routes.Update(context.Background(), route.Id, api.RouteRequest{ + Description: "Testings", + Enabled: false, + Groups: []string{"cs1tnh0hhcjnqoiuebeg"}, + PeerGroups: ptr([]string{"cs1tnh0hhcjnqoiuebeg"}), + Domains: ptr([]string{"google.com"}), + Masquerade: true, + Metric: 9999, + KeepRoute: false, + NetworkId: "Tests", + }) + + require.NoError(t, err) + assert.Equal(t, "Testings", route.Description) + + route, err = c.Routes.Get(context.Background(), route.Id) + require.NoError(t, err) + assert.Equal(t, "Tests", route.NetworkId) + + err = c.Routes.Delete(context.Background(), route.Id) + require.NoError(t, err) + }) +} diff --git a/management/client/rest/setupkeys.go b/management/client/rest/setupkeys.go new file mode 100644 index 000000000..645614fcf --- /dev/null +++ b/management/client/rest/setupkeys.go @@ -0,0 +1,82 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// SetupKeysAPI APIs for Setup keys, do not use directly +type SetupKeysAPI struct { + c *Client +} + +// List list all setup keys +// See more: https://docs.netbird.io/api/resources/setup-keys#list-all-setup-keys +func (a *SetupKeysAPI) List(ctx context.Context) ([]api.SetupKey, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/setup-keys", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.SetupKey](resp) + return ret, err +} + +// Get get setup key info +// See more: https://docs.netbird.io/api/resources/setup-keys#retrieve-a-setup-key +func (a *SetupKeysAPI) Get(ctx context.Context, setupKeyID string) (*api.SetupKey, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/setup-keys/"+setupKeyID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.SetupKey](resp) + return &ret, err +} + +// Create generate new Setup Key +// See more: https://docs.netbird.io/api/resources/setup-keys#create-a-setup-key +func (a *SetupKeysAPI) Create(ctx context.Context, request api.PostApiSetupKeysJSONRequestBody) (*api.SetupKeyClear, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "POST", "/api/setup-keys", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.SetupKeyClear](resp) + return &ret, err +} + +// Update generate new Setup Key +// See more: https://docs.netbird.io/api/resources/setup-keys#update-a-setup-key +func (a *SetupKeysAPI) Update(ctx context.Context, setupKeyID string, request api.PutApiSetupKeysKeyIdJSONRequestBody) (*api.SetupKey, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/setup-keys/"+setupKeyID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.SetupKey](resp) + return &ret, err +} + +// Delete delete setup key +// See more: https://docs.netbird.io/api/resources/setup-keys#delete-a-setup-key +func (a *SetupKeysAPI) Delete(ctx context.Context, setupKeyID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/setup-keys/"+setupKeyID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} diff --git a/management/client/rest/setupkeys_test.go b/management/client/rest/setupkeys_test.go new file mode 100644 index 000000000..8edce8428 --- /dev/null +++ b/management/client/rest/setupkeys_test.go @@ -0,0 +1,232 @@ +//go:build integration +// +build integration + +package rest_test + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" +) + +var ( + testSetupKey = api.SetupKey{ + Id: "Test", + Name: "wow", + AutoGroups: []string{"meow"}, + Ephemeral: true, + } + + testSteupKeyGenerated = api.SetupKeyClear{ + Id: "Test", + Name: "wow", + AutoGroups: []string{"meow"}, + Ephemeral: true, + Key: "shhh", + } +) + +func TestSetupKeys_List_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/setup-keys", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.SetupKey{testSetupKey}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SetupKeys.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testSetupKey, ret[0]) + }) +} + +func TestSetupKeys_List_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/setup-keys", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SetupKeys.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestSetupKeys_Get_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/setup-keys/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testSetupKey) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SetupKeys.Get(context.Background(), "Test") + require.NoError(t, err) + assert.Equal(t, testSetupKey, *ret) + }) +} + +func TestSetupKeys_Get_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/setup-keys/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SetupKeys.Get(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestSetupKeys_Create_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/setup-keys", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostApiSetupKeysJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, 5, req.ExpiresIn) + retBytes, _ := json.Marshal(testSteupKeyGenerated) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SetupKeys.Create(context.Background(), api.PostApiSetupKeysJSONRequestBody{ + ExpiresIn: 5, + }) + require.NoError(t, err) + assert.Equal(t, testSteupKeyGenerated, *ret) + }) +} + +func TestSetupKeys_Create_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/setup-keys", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SetupKeys.Create(context.Background(), api.PostApiSetupKeysJSONRequestBody{ + ExpiresIn: 5, + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestSetupKeys_Update_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/setup-keys/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiSetupKeysKeyIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, true, req.Revoked) + retBytes, _ := json.Marshal(testSetupKey) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SetupKeys.Update(context.Background(), "Test", api.PutApiSetupKeysKeyIdJSONRequestBody{ + Revoked: true, + }) + require.NoError(t, err) + assert.Equal(t, testSetupKey, *ret) + }) +} + +func TestSetupKeys_Update_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/setup-keys/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SetupKeys.Update(context.Background(), "Test", api.PutApiSetupKeysKeyIdJSONRequestBody{ + Revoked: true, + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestSetupKeys_Delete_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/setup-keys/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.SetupKeys.Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestSetupKeys_Delete_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/setup-keys/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.SetupKeys.Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestSetupKeys_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *rest.Client) { + group, err := c.Groups.Create(context.Background(), api.GroupRequest{ + Name: "Test", + }) + require.NoError(t, err) + + skClear, err := c.SetupKeys.Create(context.Background(), api.CreateSetupKeyRequest{ + AutoGroups: []string{group.Id}, + Ephemeral: ptr(false), + Name: "test", + Type: "reusable", + }) + + require.NoError(t, err) + assert.Equal(t, true, skClear.Valid) + + keys, err := c.SetupKeys.List(context.Background()) + require.NoError(t, err) + assert.Len(t, keys, 2) + + sk, err := c.SetupKeys.Update(context.Background(), skClear.Id, api.SetupKeyRequest{ + Revoked: true, + AutoGroups: []string{group.Id}, + }) + require.NoError(t, err) + + sk, err = c.SetupKeys.Get(context.Background(), sk.Id) + require.NoError(t, err) + assert.Equal(t, false, sk.Valid) + + err = c.SetupKeys.Delete(context.Background(), sk.Id) + require.NoError(t, err) + }) +} diff --git a/management/client/rest/tokens.go b/management/client/rest/tokens.go new file mode 100644 index 000000000..3275bea81 --- /dev/null +++ b/management/client/rest/tokens.go @@ -0,0 +1,66 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// TokensAPI APIs for PATs, do not use directly +type TokensAPI struct { + c *Client +} + +// List list user tokens +// See more: https://docs.netbird.io/api/resources/tokens#list-all-tokens +func (a *TokensAPI) List(ctx context.Context, userID string) ([]api.PersonalAccessToken, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/users/"+userID+"/tokens", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.PersonalAccessToken](resp) + return ret, err +} + +// Get get user token info +// See more: https://docs.netbird.io/api/resources/tokens#retrieve-a-token +func (a *TokensAPI) Get(ctx context.Context, userID, tokenID string) (*api.PersonalAccessToken, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/users/"+userID+"/tokens/"+tokenID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.PersonalAccessToken](resp) + return &ret, err +} + +// Create generate new PAT for user +// See more: https://docs.netbird.io/api/resources/tokens#create-a-token +func (a *TokensAPI) Create(ctx context.Context, userID string, request api.PostApiUsersUserIdTokensJSONRequestBody) (*api.PersonalAccessTokenGenerated, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "POST", "/api/users/"+userID+"/tokens", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.PersonalAccessTokenGenerated](resp) + return &ret, err +} + +// Delete delete user token +// See more: https://docs.netbird.io/api/resources/tokens#delete-a-token +func (a *TokensAPI) Delete(ctx context.Context, userID, tokenID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/users/"+userID+"/tokens/"+tokenID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} diff --git a/management/client/rest/tokens_test.go b/management/client/rest/tokens_test.go new file mode 100644 index 000000000..eea55d22f --- /dev/null +++ b/management/client/rest/tokens_test.go @@ -0,0 +1,180 @@ +//go:build integration +// +build integration + +package rest_test + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" +) + +var ( + testToken = api.PersonalAccessToken{ + Id: "Test", + CreatedAt: time.Time{}, + CreatedBy: "meow", + ExpirationDate: time.Time{}, + LastUsed: nil, + Name: "wow", + } + + testTokenGenerated = api.PersonalAccessTokenGenerated{ + PersonalAccessToken: testToken, + PlainToken: "shhh", + } +) + +func TestTokens_List_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/meow/tokens", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.PersonalAccessToken{testToken}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Tokens.List(context.Background(), "meow") + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testToken, ret[0]) + }) +} + +func TestTokens_List_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/meow/tokens", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Tokens.List(context.Background(), "meow") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestTokens_Get_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/meow/tokens/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testToken) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Tokens.Get(context.Background(), "meow", "Test") + require.NoError(t, err) + assert.Equal(t, testToken, *ret) + }) +} + +func TestTokens_Get_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/meow/tokens/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Tokens.Get(context.Background(), "meow", "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestTokens_Create_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/meow/tokens", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostApiUsersUserIdTokensJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, 5, req.ExpiresIn) + retBytes, _ := json.Marshal(testTokenGenerated) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Tokens.Create(context.Background(), "meow", api.PostApiUsersUserIdTokensJSONRequestBody{ + ExpiresIn: 5, + }) + require.NoError(t, err) + assert.Equal(t, testTokenGenerated, *ret) + }) +} + +func TestTokens_Create_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/meow/tokens", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Tokens.Create(context.Background(), "meow", api.PostApiUsersUserIdTokensJSONRequestBody{ + ExpiresIn: 5, + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestTokens_Delete_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/meow/tokens/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Tokens.Delete(context.Background(), "meow", "Test") + require.NoError(t, err) + }) +} + +func TestTokens_Delete_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/meow/tokens/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Tokens.Delete(context.Background(), "meow", "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestTokens_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *rest.Client) { + tokenClear, err := c.Tokens.Create(context.Background(), "a23efe53-63fb-11ec-90d6-0242ac120003", api.PersonalAccessTokenRequest{ + Name: "Test", + ExpiresIn: 365, + }) + + require.NoError(t, err) + assert.Equal(t, "Test", tokenClear.PersonalAccessToken.Name) + + tokens, err := c.Tokens.List(context.Background(), "a23efe53-63fb-11ec-90d6-0242ac120003") + require.NoError(t, err) + assert.Len(t, tokens, 2) + + token, err := c.Tokens.Get(context.Background(), "a23efe53-63fb-11ec-90d6-0242ac120003", tokenClear.PersonalAccessToken.Id) + require.NoError(t, err) + assert.Equal(t, "Test", token.Name) + + err = c.Tokens.Delete(context.Background(), "a23efe53-63fb-11ec-90d6-0242ac120003", token.Id) + require.NoError(t, err) + }) +} diff --git a/management/client/rest/users.go b/management/client/rest/users.go new file mode 100644 index 000000000..372bcee45 --- /dev/null +++ b/management/client/rest/users.go @@ -0,0 +1,82 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// UsersAPI APIs for users, do not use directly +type UsersAPI struct { + c *Client +} + +// List list all users, only returns one user always +// See more: https://docs.netbird.io/api/resources/users#list-all-users +func (a *UsersAPI) List(ctx context.Context) ([]api.User, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/users", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.User](resp) + return ret, err +} + +// Create create user +// See more: https://docs.netbird.io/api/resources/users#create-a-user +func (a *UsersAPI) Create(ctx context.Context, request api.PostApiUsersJSONRequestBody) (*api.User, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "POST", "/api/users", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.User](resp) + return &ret, err +} + +// Update update user settings +// See more: https://docs.netbird.io/api/resources/users#update-a-user +func (a *UsersAPI) Update(ctx context.Context, userID string, request api.PutApiUsersUserIdJSONRequestBody) (*api.User, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/users/"+userID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.User](resp) + return &ret, err +} + +// Delete delete user +// See more: https://docs.netbird.io/api/resources/users#delete-a-user +func (a *UsersAPI) Delete(ctx context.Context, userID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/users/"+userID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} + +// ResendInvitation resend user invitation +// See more: https://docs.netbird.io/api/resources/users#resend-user-invitation +func (a *UsersAPI) ResendInvitation(ctx context.Context, userID string) error { + resp, err := a.c.newRequest(ctx, "POST", "/api/users/"+userID+"/invite", nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} diff --git a/management/client/rest/users_test.go b/management/client/rest/users_test.go new file mode 100644 index 000000000..2ff8a0327 --- /dev/null +++ b/management/client/rest/users_test.go @@ -0,0 +1,227 @@ +//go:build integration +// +build integration + +package rest_test + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" +) + +var ( + testUser = api.User{ + Id: "Test", + AutoGroups: []string{"test-group"}, + Email: "test@test.com", + IsBlocked: false, + IsCurrent: ptr(false), + IsServiceUser: ptr(false), + Issued: ptr("api"), + LastLogin: &time.Time{}, + Name: "M. Essam", + Permissions: &api.UserPermissions{ + DashboardView: ptr(api.UserPermissionsDashboardViewFull), + }, + Role: "user", + Status: api.UserStatusActive, + } +) + +func TestUsers_List_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.User{testUser}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Users.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testUser, ret[0]) + }) +} + +func TestUsers_List_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Users.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestUsers_Create_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostApiUsersJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, []string{"meow"}, req.AutoGroups) + retBytes, _ := json.Marshal(testUser) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Users.Create(context.Background(), api.PostApiUsersJSONRequestBody{ + AutoGroups: []string{"meow"}, + }) + require.NoError(t, err) + assert.Equal(t, testUser, *ret) + }) +} + +func TestUsers_Create_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Users.Create(context.Background(), api.PostApiUsersJSONRequestBody{ + AutoGroups: []string{"meow"}, + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestUsers_Update_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiUsersUserIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, true, req.IsBlocked) + retBytes, _ := json.Marshal(testUser) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Users.Update(context.Background(), "Test", api.PutApiUsersUserIdJSONRequestBody{ + IsBlocked: true, + }) + require.NoError(t, err) + assert.Equal(t, testUser, *ret) + }) + +} + +func TestUsers_Update_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Users.Update(context.Background(), "Test", api.PutApiUsersUserIdJSONRequestBody{ + IsBlocked: true, + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestUsers_Delete_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Users.Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestUsers_Delete_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Users.Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestUsers_ResendInvitation_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/Test/invite", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + w.WriteHeader(200) + }) + err := c.Users.ResendInvitation(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestUsers_ResendInvitation_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/Test/invite", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Users.ResendInvitation(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestUsers_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *rest.Client) { + user, err := c.Users.Create(context.Background(), api.UserCreateRequest{ + AutoGroups: []string{}, + Email: ptr("test@example.com"), + IsServiceUser: true, + Name: ptr("Nobody"), + Role: "user", + }) + + require.NoError(t, err) + assert.Equal(t, "Nobody", user.Name) + + users, err := c.Users.List(context.Background()) + require.NoError(t, err) + assert.NotEmpty(t, users) + + user, err = c.Users.Update(context.Background(), user.Id, api.UserRequest{ + AutoGroups: []string{}, + Role: "admin", + }) + + require.NoError(t, err) + assert.Equal(t, "admin", user.Role) + + err = c.Users.Delete(context.Background(), user.Id) + require.NoError(t, err) + }) +} diff --git a/management/cmd/management.go b/management/cmd/management.go index 1c8fca8dc..9712f04aa 100644 --- a/management/cmd/management.go +++ b/management/cmd/management.go @@ -39,13 +39,12 @@ import ( "github.com/netbirdio/netbird/formatter" mgmtProto "github.com/netbirdio/netbird/management/proto" "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/auth" nbContext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/geolocation" "github.com/netbirdio/netbird/management/server/groups" nbhttp "github.com/netbirdio/netbird/management/server/http" - "github.com/netbirdio/netbird/management/server/http/configs" "github.com/netbirdio/netbird/management/server/idp" - "github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/metrics" "github.com/netbirdio/netbird/management/server/networks" "github.com/netbirdio/netbird/management/server/networks/resources" @@ -255,24 +254,13 @@ var ( tlsEnabled = true } - jwtValidator, err := jwtclaims.NewJWTValidator( - ctx, + authManager := auth.NewManager(store, config.HttpConfig.AuthIssuer, - config.GetAuthAudiences(), + config.HttpConfig.AuthAudience, config.HttpConfig.AuthKeysLocation, - config.HttpConfig.IdpSignKeyRefreshEnabled, - ) - if err != nil { - return fmt.Errorf("failed creating JWT validator: %v", err) - } - - httpAPIAuthCfg := configs.AuthCfg{ - Issuer: config.HttpConfig.AuthIssuer, - Audience: config.HttpConfig.AuthAudience, - UserIDClaim: config.HttpConfig.AuthUserIDClaim, - KeysLocation: config.HttpConfig.AuthKeysLocation, - } - + config.HttpConfig.AuthUserIDClaim, + config.GetAuthAudiences(), + config.HttpConfig.IdpSignKeyRefreshEnabled) userManager := users.NewManager(store) settingsManager := settings.NewManager(store) permissionsManager := permissions.NewManager(userManager, settingsManager) @@ -281,7 +269,7 @@ var ( routersManager := routers.NewManager(store, permissionsManager, accountManager) networksManager := networks.NewManager(store, permissionsManager, resourcesManager, routersManager, accountManager) - httpAPIHandler, err := nbhttp.NewAPIHandler(ctx, accountManager, networksManager, resourcesManager, routersManager, groupsManager, geo, jwtValidator, appMetrics, httpAPIAuthCfg, integratedPeerValidator) + httpAPIHandler, err := nbhttp.NewAPIHandler(ctx, accountManager, networksManager, resourcesManager, routersManager, groupsManager, geo, authManager, appMetrics, config, integratedPeerValidator) if err != nil { return fmt.Errorf("failed creating HTTP API handler: %v", err) } @@ -290,7 +278,7 @@ var ( ephemeralManager.LoadInitialPeers(ctx) gRPCAPIHandler := grpc.NewServer(gRPCOpts...) - srv, err := server.NewServer(ctx, config, accountManager, settingsManager, peersUpdateManager, secretsManager, appMetrics, ephemeralManager) + srv, err := server.NewServer(ctx, config, accountManager, settingsManager, peersUpdateManager, secretsManager, appMetrics, ephemeralManager, authManager) if err != nil { return fmt.Errorf("failed creating gRPC API handler: %v", err) } diff --git a/management/domain/validate.go b/management/domain/validate.go new file mode 100644 index 000000000..bcbf26e05 --- /dev/null +++ b/management/domain/validate.go @@ -0,0 +1,65 @@ +package domain + +import ( + "fmt" + "regexp" + "strings" +) + +const maxDomains = 32 + +// ValidateDomains checks if each domain in the list is valid and returns a punycode-encoded DomainList. +func ValidateDomains(domains []string) (List, error) { + if len(domains) == 0 { + return nil, fmt.Errorf("domains list is empty") + } + if len(domains) > maxDomains { + return nil, fmt.Errorf("domains list exceeds maximum allowed domains: %d", maxDomains) + } + + domainRegex := regexp.MustCompile(`^(?:\*\.)?(?:(?:xn--)?[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?\.)*(?:xn--)?[a-zA-Z0-9](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?$`) + + var domainList List + + for _, d := range domains { + d := strings.ToLower(d) + + // handles length and idna conversion + punycode, err := FromString(d) + if err != nil { + return domainList, fmt.Errorf("convert domain to punycode: %s: %w", d, err) + } + + if !domainRegex.MatchString(string(punycode)) { + return domainList, fmt.Errorf("invalid domain format: %s", d) + } + + domainList = append(domainList, punycode) + } + return domainList, nil +} + +// ValidateDomainsStrSlice checks if each domain in the list is valid +func ValidateDomainsStrSlice(domains []string) ([]string, error) { + if len(domains) == 0 { + return nil, nil + } + if len(domains) > maxDomains { + return nil, fmt.Errorf("domains list exceeds maximum allowed domains: %d", maxDomains) + } + + domainRegex := regexp.MustCompile(`^(?:\*\.)?(?:(?:xn--)?[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?\.)*(?:xn--)?[a-zA-Z0-9](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?$`) + + var domainList []string + + for _, d := range domains { + d := strings.ToLower(d) + + if !domainRegex.MatchString(d) { + return domainList, fmt.Errorf("invalid domain format: %s", d) + } + + domainList = append(domainList, d) + } + return domainList, nil +} diff --git a/management/domain/validate_test.go b/management/domain/validate_test.go new file mode 100644 index 000000000..c9c042d9d --- /dev/null +++ b/management/domain/validate_test.go @@ -0,0 +1,206 @@ +package domain + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestValidateDomains(t *testing.T) { + tests := []struct { + name string + domains []string + expected List + wantErr bool + }{ + { + name: "Empty list", + domains: nil, + expected: nil, + wantErr: true, + }, + { + name: "Valid ASCII domain", + domains: []string{"sub.ex-ample.com"}, + expected: List{"sub.ex-ample.com"}, + wantErr: false, + }, + { + name: "Valid Unicode domain", + domains: []string{"münchen.de"}, + expected: List{"xn--mnchen-3ya.de"}, + wantErr: false, + }, + { + name: "Valid Unicode, all labels", + domains: []string{"中国.中国.中国"}, + expected: List{"xn--fiqs8s.xn--fiqs8s.xn--fiqs8s"}, + wantErr: false, + }, + { + name: "With underscores", + domains: []string{"_jabber._tcp.gmail.com"}, + expected: List{"_jabber._tcp.gmail.com"}, + wantErr: false, + }, + { + name: "Invalid domain format", + domains: []string{"-example.com"}, + expected: nil, + wantErr: true, + }, + { + name: "Invalid domain format 2", + domains: []string{"example.com-"}, + expected: nil, + wantErr: true, + }, + { + name: "Multiple domains valid and invalid", + domains: []string{"google.com", "invalid,nbdomain.com", "münchen.de"}, + expected: List{"google.com"}, + wantErr: true, + }, + { + name: "Valid wildcard domain", + domains: []string{"*.example.com"}, + expected: List{"*.example.com"}, + wantErr: false, + }, + { + name: "Wildcard with dot domain", + domains: []string{".*.example.com"}, + expected: nil, + wantErr: true, + }, + { + name: "Wildcard with dot domain", + domains: []string{".*.example.com"}, + expected: nil, + wantErr: true, + }, + { + name: "Invalid wildcard domain", + domains: []string{"a.*.example.com"}, + expected: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ValidateDomains(tt.domains) + assert.Equal(t, tt.wantErr, err != nil) + assert.Equal(t, got, tt.expected) + }) + } +} + +// TestValidateDomainsStrSlice tests the ValidateDomainsStrSlice function. +func TestValidateDomainsStrSlice(t *testing.T) { + // Generate a slice of valid domains up to maxDomains + validDomains := make([]string, maxDomains) + for i := 0; i < maxDomains; i++ { + validDomains[i] = fmt.Sprintf("example%d.com", i) + } + + tests := []struct { + name string + domains []string + expected []string + wantErr bool + }{ + { + name: "Empty list", + domains: nil, + expected: nil, + wantErr: false, + }, + { + name: "Single valid ASCII domain", + domains: []string{"sub.ex-ample.com"}, + expected: []string{"sub.ex-ample.com"}, + wantErr: false, + }, + { + name: "Underscores in labels", + domains: []string{"_jabber._tcp.gmail.com"}, + expected: []string{"_jabber._tcp.gmail.com"}, + wantErr: false, + }, + { + // Unlike ValidateDomains (which converts to punycode), + // ValidateDomainsStrSlice will fail on non-ASCII domain chars. + name: "Unicode domain fails (no punycode conversion)", + domains: []string{"münchen.de"}, + expected: nil, + wantErr: true, + }, + { + name: "Invalid domain format - leading dash", + domains: []string{"-example.com"}, + expected: nil, + wantErr: true, + }, + { + name: "Invalid domain format - trailing dash", + domains: []string{"example-.com"}, + expected: nil, + wantErr: true, + }, + { + // The function stops on the first invalid domain and returns an error, + // so only the first domain is definitely valid, but the second is invalid. + name: "Multiple domains with a valid one, then invalid", + domains: []string{"google.com", "invalid_domain.com-"}, + expected: []string{"google.com"}, + wantErr: true, + }, + { + name: "Valid wildcard domain", + domains: []string{"*.example.com"}, + expected: []string{"*.example.com"}, + wantErr: false, + }, + { + name: "Wildcard with leading dot - invalid", + domains: []string{".*.example.com"}, + expected: nil, + wantErr: true, + }, + { + name: "Invalid wildcard with multiple asterisks", + domains: []string{"a.*.example.com"}, + expected: nil, + wantErr: true, + }, + { + name: "Exactly maxDomains items (valid)", + domains: validDomains, + expected: validDomains, + wantErr: false, + }, + { + name: "Exceeds maxDomains items", + domains: append(validDomains, "extra.com"), + expected: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ValidateDomainsStrSlice(tt.domains) + // Check if we got an error where expected + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + // Compare the returned domains to what we expect + assert.Equal(t, tt.expected, got) + }) + } +} diff --git a/management/proto/management.pb.go b/management/proto/management.pb.go index b4ff16e6d..2cd00783e 100644 --- a/management/proto/management.pb.go +++ b/management/proto/management.pb.go @@ -223,7 +223,7 @@ func (x HostConfig_Protocol) Number() protoreflect.EnumNumber { // Deprecated: Use HostConfig_Protocol.Descriptor instead. func (HostConfig_Protocol) EnumDescriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{13, 0} + return file_management_proto_rawDescGZIP(), []int{14, 0} } type DeviceAuthorizationFlowProvider int32 @@ -266,7 +266,7 @@ func (x DeviceAuthorizationFlowProvider) Number() protoreflect.EnumNumber { // Deprecated: Use DeviceAuthorizationFlowProvider.Descriptor instead. func (DeviceAuthorizationFlowProvider) EnumDescriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{21, 0} + return file_management_proto_rawDescGZIP(), []int{22, 0} } type EncryptedMessage struct { @@ -278,7 +278,7 @@ type EncryptedMessage struct { WgPubKey string `protobuf:"bytes,1,opt,name=wgPubKey,proto3" json:"wgPubKey,omitempty"` // encrypted message Body Body []byte `protobuf:"bytes,2,opt,name=body,proto3" json:"body,omitempty"` - // Version of the Wiretrustee Management Service protocol + // Version of the Netbird Management Service protocol Version int32 `protobuf:"varint,3,opt,name=version,proto3" json:"version,omitempty"` } @@ -383,14 +383,14 @@ func (x *SyncRequest) GetMeta() *PeerSystemMeta { return nil } -// SyncResponse represents a state that should be applied to the local peer (e.g. Wiretrustee servers config as well as local peer and remote peers configs) +// SyncResponse represents a state that should be applied to the local peer (e.g. Netbird servers config as well as local peer and remote peers configs) type SyncResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Global config - WiretrusteeConfig *WiretrusteeConfig `protobuf:"bytes,1,opt,name=wiretrusteeConfig,proto3" json:"wiretrusteeConfig,omitempty"` + NetbirdConfig *NetbirdConfig `protobuf:"bytes,1,opt,name=netbirdConfig,proto3" json:"netbirdConfig,omitempty"` // Deprecated. Use NetworkMap.PeerConfig PeerConfig *PeerConfig `protobuf:"bytes,2,opt,name=peerConfig,proto3" json:"peerConfig,omitempty"` // Deprecated. Use NetworkMap.RemotePeerConfig @@ -435,9 +435,9 @@ func (*SyncResponse) Descriptor() ([]byte, []int) { return file_management_proto_rawDescGZIP(), []int{2} } -func (x *SyncResponse) GetWiretrusteeConfig() *WiretrusteeConfig { +func (x *SyncResponse) GetNetbirdConfig() *NetbirdConfig { if x != nil { - return x.WiretrusteeConfig + return x.NetbirdConfig } return nil } @@ -537,7 +537,8 @@ type LoginRequest struct { // SSO token (can be empty) JwtToken string `protobuf:"bytes,3,opt,name=jwtToken,proto3" json:"jwtToken,omitempty"` // Can be absent for now. - PeerKeys *PeerKeys `protobuf:"bytes,4,opt,name=peerKeys,proto3" json:"peerKeys,omitempty"` + PeerKeys *PeerKeys `protobuf:"bytes,4,opt,name=peerKeys,proto3" json:"peerKeys,omitempty"` + DnsLabels []string `protobuf:"bytes,5,rep,name=dnsLabels,proto3" json:"dnsLabels,omitempty"` } func (x *LoginRequest) Reset() { @@ -600,6 +601,13 @@ func (x *LoginRequest) GetPeerKeys() *PeerKeys { return nil } +func (x *LoginRequest) GetDnsLabels() []string { + if x != nil { + return x.DnsLabels + } + return nil +} + // PeerKeys is additional peer info like SSH pub key and WireGuard public key. // This message is sent on Login or register requests, or when a key rotation has to happen. type PeerKeys struct { @@ -784,34 +792,130 @@ func (x *File) GetProcessIsRunning() bool { return false } +type Flags struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RosenpassEnabled bool `protobuf:"varint,1,opt,name=rosenpassEnabled,proto3" json:"rosenpassEnabled,omitempty"` + RosenpassPermissive bool `protobuf:"varint,2,opt,name=rosenpassPermissive,proto3" json:"rosenpassPermissive,omitempty"` + ServerSSHAllowed bool `protobuf:"varint,3,opt,name=serverSSHAllowed,proto3" json:"serverSSHAllowed,omitempty"` + DisableClientRoutes bool `protobuf:"varint,4,opt,name=disableClientRoutes,proto3" json:"disableClientRoutes,omitempty"` + DisableServerRoutes bool `protobuf:"varint,5,opt,name=disableServerRoutes,proto3" json:"disableServerRoutes,omitempty"` + DisableDNS bool `protobuf:"varint,6,opt,name=disableDNS,proto3" json:"disableDNS,omitempty"` + DisableFirewall bool `protobuf:"varint,7,opt,name=disableFirewall,proto3" json:"disableFirewall,omitempty"` +} + +func (x *Flags) Reset() { + *x = Flags{} + if protoimpl.UnsafeEnabled { + mi := &file_management_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Flags) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Flags) ProtoMessage() {} + +func (x *Flags) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[8] + 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 Flags.ProtoReflect.Descriptor instead. +func (*Flags) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{8} +} + +func (x *Flags) GetRosenpassEnabled() bool { + if x != nil { + return x.RosenpassEnabled + } + return false +} + +func (x *Flags) GetRosenpassPermissive() bool { + if x != nil { + return x.RosenpassPermissive + } + return false +} + +func (x *Flags) GetServerSSHAllowed() bool { + if x != nil { + return x.ServerSSHAllowed + } + return false +} + +func (x *Flags) GetDisableClientRoutes() bool { + if x != nil { + return x.DisableClientRoutes + } + return false +} + +func (x *Flags) GetDisableServerRoutes() bool { + if x != nil { + return x.DisableServerRoutes + } + return false +} + +func (x *Flags) GetDisableDNS() bool { + if x != nil { + return x.DisableDNS + } + return false +} + +func (x *Flags) GetDisableFirewall() bool { + if x != nil { + return x.DisableFirewall + } + return false +} + // PeerSystemMeta is machine meta data like OS and version. type PeerSystemMeta struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Hostname string `protobuf:"bytes,1,opt,name=hostname,proto3" json:"hostname,omitempty"` - GoOS string `protobuf:"bytes,2,opt,name=goOS,proto3" json:"goOS,omitempty"` - Kernel string `protobuf:"bytes,3,opt,name=kernel,proto3" json:"kernel,omitempty"` - Core string `protobuf:"bytes,4,opt,name=core,proto3" json:"core,omitempty"` - Platform string `protobuf:"bytes,5,opt,name=platform,proto3" json:"platform,omitempty"` - OS string `protobuf:"bytes,6,opt,name=OS,proto3" json:"OS,omitempty"` - WiretrusteeVersion string `protobuf:"bytes,7,opt,name=wiretrusteeVersion,proto3" json:"wiretrusteeVersion,omitempty"` - UiVersion string `protobuf:"bytes,8,opt,name=uiVersion,proto3" json:"uiVersion,omitempty"` - KernelVersion string `protobuf:"bytes,9,opt,name=kernelVersion,proto3" json:"kernelVersion,omitempty"` - OSVersion string `protobuf:"bytes,10,opt,name=OSVersion,proto3" json:"OSVersion,omitempty"` - NetworkAddresses []*NetworkAddress `protobuf:"bytes,11,rep,name=networkAddresses,proto3" json:"networkAddresses,omitempty"` - SysSerialNumber string `protobuf:"bytes,12,opt,name=sysSerialNumber,proto3" json:"sysSerialNumber,omitempty"` - SysProductName string `protobuf:"bytes,13,opt,name=sysProductName,proto3" json:"sysProductName,omitempty"` - SysManufacturer string `protobuf:"bytes,14,opt,name=sysManufacturer,proto3" json:"sysManufacturer,omitempty"` - Environment *Environment `protobuf:"bytes,15,opt,name=environment,proto3" json:"environment,omitempty"` - Files []*File `protobuf:"bytes,16,rep,name=files,proto3" json:"files,omitempty"` + Hostname string `protobuf:"bytes,1,opt,name=hostname,proto3" json:"hostname,omitempty"` + GoOS string `protobuf:"bytes,2,opt,name=goOS,proto3" json:"goOS,omitempty"` + Kernel string `protobuf:"bytes,3,opt,name=kernel,proto3" json:"kernel,omitempty"` + Core string `protobuf:"bytes,4,opt,name=core,proto3" json:"core,omitempty"` + Platform string `protobuf:"bytes,5,opt,name=platform,proto3" json:"platform,omitempty"` + OS string `protobuf:"bytes,6,opt,name=OS,proto3" json:"OS,omitempty"` + NetbirdVersion string `protobuf:"bytes,7,opt,name=netbirdVersion,proto3" json:"netbirdVersion,omitempty"` + UiVersion string `protobuf:"bytes,8,opt,name=uiVersion,proto3" json:"uiVersion,omitempty"` + KernelVersion string `protobuf:"bytes,9,opt,name=kernelVersion,proto3" json:"kernelVersion,omitempty"` + OSVersion string `protobuf:"bytes,10,opt,name=OSVersion,proto3" json:"OSVersion,omitempty"` + NetworkAddresses []*NetworkAddress `protobuf:"bytes,11,rep,name=networkAddresses,proto3" json:"networkAddresses,omitempty"` + SysSerialNumber string `protobuf:"bytes,12,opt,name=sysSerialNumber,proto3" json:"sysSerialNumber,omitempty"` + SysProductName string `protobuf:"bytes,13,opt,name=sysProductName,proto3" json:"sysProductName,omitempty"` + SysManufacturer string `protobuf:"bytes,14,opt,name=sysManufacturer,proto3" json:"sysManufacturer,omitempty"` + Environment *Environment `protobuf:"bytes,15,opt,name=environment,proto3" json:"environment,omitempty"` + Files []*File `protobuf:"bytes,16,rep,name=files,proto3" json:"files,omitempty"` + Flags *Flags `protobuf:"bytes,17,opt,name=flags,proto3" json:"flags,omitempty"` } func (x *PeerSystemMeta) Reset() { *x = PeerSystemMeta{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[8] + mi := &file_management_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -824,7 +928,7 @@ func (x *PeerSystemMeta) String() string { func (*PeerSystemMeta) ProtoMessage() {} func (x *PeerSystemMeta) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[8] + mi := &file_management_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -837,7 +941,7 @@ func (x *PeerSystemMeta) ProtoReflect() protoreflect.Message { // Deprecated: Use PeerSystemMeta.ProtoReflect.Descriptor instead. func (*PeerSystemMeta) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{8} + return file_management_proto_rawDescGZIP(), []int{9} } func (x *PeerSystemMeta) GetHostname() string { @@ -882,9 +986,9 @@ func (x *PeerSystemMeta) GetOS() string { return "" } -func (x *PeerSystemMeta) GetWiretrusteeVersion() string { +func (x *PeerSystemMeta) GetNetbirdVersion() string { if x != nil { - return x.WiretrusteeVersion + return x.NetbirdVersion } return "" } @@ -952,13 +1056,20 @@ func (x *PeerSystemMeta) GetFiles() []*File { return nil } +func (x *PeerSystemMeta) GetFlags() *Flags { + if x != nil { + return x.Flags + } + return nil +} + type LoginResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Global config - WiretrusteeConfig *WiretrusteeConfig `protobuf:"bytes,1,opt,name=wiretrusteeConfig,proto3" json:"wiretrusteeConfig,omitempty"` + NetbirdConfig *NetbirdConfig `protobuf:"bytes,1,opt,name=netbirdConfig,proto3" json:"netbirdConfig,omitempty"` // Peer local config PeerConfig *PeerConfig `protobuf:"bytes,2,opt,name=peerConfig,proto3" json:"peerConfig,omitempty"` // Posture checks to be evaluated by client @@ -968,7 +1079,7 @@ type LoginResponse struct { func (x *LoginResponse) Reset() { *x = LoginResponse{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[9] + mi := &file_management_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -981,7 +1092,7 @@ func (x *LoginResponse) String() string { func (*LoginResponse) ProtoMessage() {} func (x *LoginResponse) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[9] + mi := &file_management_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -994,12 +1105,12 @@ func (x *LoginResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use LoginResponse.ProtoReflect.Descriptor instead. func (*LoginResponse) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{9} + return file_management_proto_rawDescGZIP(), []int{10} } -func (x *LoginResponse) GetWiretrusteeConfig() *WiretrusteeConfig { +func (x *LoginResponse) GetNetbirdConfig() *NetbirdConfig { if x != nil { - return x.WiretrusteeConfig + return x.NetbirdConfig } return nil } @@ -1027,14 +1138,14 @@ type ServerKeyResponse struct { Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` // Key expiration timestamp after which the key should be fetched again by the client ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=expiresAt,proto3" json:"expiresAt,omitempty"` - // Version of the Wiretrustee Management Service protocol + // Version of the Netbird Management Service protocol Version int32 `protobuf:"varint,3,opt,name=version,proto3" json:"version,omitempty"` } func (x *ServerKeyResponse) Reset() { *x = ServerKeyResponse{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[10] + mi := &file_management_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1047,7 +1158,7 @@ func (x *ServerKeyResponse) String() string { func (*ServerKeyResponse) ProtoMessage() {} func (x *ServerKeyResponse) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[10] + mi := &file_management_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1060,7 +1171,7 @@ func (x *ServerKeyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ServerKeyResponse.ProtoReflect.Descriptor instead. func (*ServerKeyResponse) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{10} + return file_management_proto_rawDescGZIP(), []int{11} } func (x *ServerKeyResponse) GetKey() string { @@ -1093,7 +1204,7 @@ type Empty struct { func (x *Empty) Reset() { *x = Empty{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[11] + mi := &file_management_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1106,7 +1217,7 @@ func (x *Empty) String() string { func (*Empty) ProtoMessage() {} func (x *Empty) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[11] + mi := &file_management_proto_msgTypes[12] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1119,11 +1230,11 @@ func (x *Empty) ProtoReflect() protoreflect.Message { // Deprecated: Use Empty.ProtoReflect.Descriptor instead. func (*Empty) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{11} + return file_management_proto_rawDescGZIP(), []int{12} } -// WiretrusteeConfig is a common configuration of any Wiretrustee peer. It contains STUN, TURN, Signal and Management servers configurations -type WiretrusteeConfig struct { +// NetbirdConfig is a common configuration of any Netbird peer. It contains STUN, TURN, Signal and Management servers configurations +type NetbirdConfig struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields @@ -1137,23 +1248,23 @@ type WiretrusteeConfig struct { Relay *RelayConfig `protobuf:"bytes,4,opt,name=relay,proto3" json:"relay,omitempty"` } -func (x *WiretrusteeConfig) Reset() { - *x = WiretrusteeConfig{} +func (x *NetbirdConfig) Reset() { + *x = NetbirdConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[12] + mi := &file_management_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *WiretrusteeConfig) String() string { +func (x *NetbirdConfig) String() string { return protoimpl.X.MessageStringOf(x) } -func (*WiretrusteeConfig) ProtoMessage() {} +func (*NetbirdConfig) ProtoMessage() {} -func (x *WiretrusteeConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[12] +func (x *NetbirdConfig) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1164,33 +1275,33 @@ func (x *WiretrusteeConfig) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use WiretrusteeConfig.ProtoReflect.Descriptor instead. -func (*WiretrusteeConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{12} +// Deprecated: Use NetbirdConfig.ProtoReflect.Descriptor instead. +func (*NetbirdConfig) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{13} } -func (x *WiretrusteeConfig) GetStuns() []*HostConfig { +func (x *NetbirdConfig) GetStuns() []*HostConfig { if x != nil { return x.Stuns } return nil } -func (x *WiretrusteeConfig) GetTurns() []*ProtectedHostConfig { +func (x *NetbirdConfig) GetTurns() []*ProtectedHostConfig { if x != nil { return x.Turns } return nil } -func (x *WiretrusteeConfig) GetSignal() *HostConfig { +func (x *NetbirdConfig) GetSignal() *HostConfig { if x != nil { return x.Signal } return nil } -func (x *WiretrusteeConfig) GetRelay() *RelayConfig { +func (x *NetbirdConfig) GetRelay() *RelayConfig { if x != nil { return x.Relay } @@ -1203,7 +1314,7 @@ type HostConfig struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // URI of the resource e.g. turns://stun.wiretrustee.com:4430 or signal.wiretrustee.com:10000 + // URI of the resource e.g. turns://stun.netbird.io:4430 or signal.netbird.io:10000 Uri string `protobuf:"bytes,1,opt,name=uri,proto3" json:"uri,omitempty"` Protocol HostConfig_Protocol `protobuf:"varint,2,opt,name=protocol,proto3,enum=management.HostConfig_Protocol" json:"protocol,omitempty"` } @@ -1211,7 +1322,7 @@ type HostConfig struct { func (x *HostConfig) Reset() { *x = HostConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[13] + mi := &file_management_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1224,7 +1335,7 @@ func (x *HostConfig) String() string { func (*HostConfig) ProtoMessage() {} func (x *HostConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[13] + mi := &file_management_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1237,7 +1348,7 @@ func (x *HostConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use HostConfig.ProtoReflect.Descriptor instead. func (*HostConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{13} + return file_management_proto_rawDescGZIP(), []int{14} } func (x *HostConfig) GetUri() string { @@ -1267,7 +1378,7 @@ type RelayConfig struct { func (x *RelayConfig) Reset() { *x = RelayConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[14] + mi := &file_management_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1280,7 +1391,7 @@ func (x *RelayConfig) String() string { func (*RelayConfig) ProtoMessage() {} func (x *RelayConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[14] + mi := &file_management_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1293,7 +1404,7 @@ func (x *RelayConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayConfig.ProtoReflect.Descriptor instead. func (*RelayConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{14} + return file_management_proto_rawDescGZIP(), []int{15} } func (x *RelayConfig) GetUrls() []string { @@ -1332,7 +1443,7 @@ type ProtectedHostConfig struct { func (x *ProtectedHostConfig) Reset() { *x = ProtectedHostConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[15] + mi := &file_management_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1345,7 +1456,7 @@ func (x *ProtectedHostConfig) String() string { func (*ProtectedHostConfig) ProtoMessage() {} func (x *ProtectedHostConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[15] + mi := &file_management_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1358,7 +1469,7 @@ func (x *ProtectedHostConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use ProtectedHostConfig.ProtoReflect.Descriptor instead. func (*ProtectedHostConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{15} + return file_management_proto_rawDescGZIP(), []int{16} } func (x *ProtectedHostConfig) GetHostConfig() *HostConfig { @@ -1389,9 +1500,9 @@ type PeerConfig struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // Peer's virtual IP address within the Wiretrustee VPN (a Wireguard address config) + // Peer's virtual IP address within the Netbird VPN (a Wireguard address config) Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` - // Wiretrustee DNS server (a Wireguard DNS config) + // Netbird DNS server (a Wireguard DNS config) Dns string `protobuf:"bytes,2,opt,name=dns,proto3" json:"dns,omitempty"` // SSHConfig of the peer. SshConfig *SSHConfig `protobuf:"bytes,3,opt,name=sshConfig,proto3" json:"sshConfig,omitempty"` @@ -1403,7 +1514,7 @@ type PeerConfig struct { func (x *PeerConfig) Reset() { *x = PeerConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[16] + mi := &file_management_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1416,7 +1527,7 @@ func (x *PeerConfig) String() string { func (*PeerConfig) ProtoMessage() {} func (x *PeerConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[16] + mi := &file_management_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1429,7 +1540,7 @@ func (x *PeerConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use PeerConfig.ProtoReflect.Descriptor instead. func (*PeerConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{16} + return file_management_proto_rawDescGZIP(), []int{17} } func (x *PeerConfig) GetAddress() string { @@ -1502,7 +1613,7 @@ type NetworkMap struct { func (x *NetworkMap) Reset() { *x = NetworkMap{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[17] + mi := &file_management_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1515,7 +1626,7 @@ func (x *NetworkMap) String() string { func (*NetworkMap) ProtoMessage() {} func (x *NetworkMap) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[17] + mi := &file_management_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1528,7 +1639,7 @@ func (x *NetworkMap) ProtoReflect() protoreflect.Message { // Deprecated: Use NetworkMap.ProtoReflect.Descriptor instead. func (*NetworkMap) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{17} + return file_management_proto_rawDescGZIP(), []int{18} } func (x *NetworkMap) GetSerial() uint64 { @@ -1628,7 +1739,7 @@ type RemotePeerConfig struct { func (x *RemotePeerConfig) Reset() { *x = RemotePeerConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[18] + mi := &file_management_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1641,7 +1752,7 @@ func (x *RemotePeerConfig) String() string { func (*RemotePeerConfig) ProtoMessage() {} func (x *RemotePeerConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[18] + mi := &file_management_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1654,7 +1765,7 @@ func (x *RemotePeerConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use RemotePeerConfig.ProtoReflect.Descriptor instead. func (*RemotePeerConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{18} + return file_management_proto_rawDescGZIP(), []int{19} } func (x *RemotePeerConfig) GetWgPubKey() string { @@ -1701,7 +1812,7 @@ type SSHConfig struct { func (x *SSHConfig) Reset() { *x = SSHConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[19] + mi := &file_management_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1714,7 +1825,7 @@ func (x *SSHConfig) String() string { func (*SSHConfig) ProtoMessage() {} func (x *SSHConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[19] + mi := &file_management_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1727,7 +1838,7 @@ func (x *SSHConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use SSHConfig.ProtoReflect.Descriptor instead. func (*SSHConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{19} + return file_management_proto_rawDescGZIP(), []int{20} } func (x *SSHConfig) GetSshEnabled() bool { @@ -1754,7 +1865,7 @@ type DeviceAuthorizationFlowRequest struct { func (x *DeviceAuthorizationFlowRequest) Reset() { *x = DeviceAuthorizationFlowRequest{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[20] + mi := &file_management_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1767,7 +1878,7 @@ func (x *DeviceAuthorizationFlowRequest) String() string { func (*DeviceAuthorizationFlowRequest) ProtoMessage() {} func (x *DeviceAuthorizationFlowRequest) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[20] + mi := &file_management_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1780,7 +1891,7 @@ func (x *DeviceAuthorizationFlowRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeviceAuthorizationFlowRequest.ProtoReflect.Descriptor instead. func (*DeviceAuthorizationFlowRequest) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{20} + return file_management_proto_rawDescGZIP(), []int{21} } // DeviceAuthorizationFlow represents Device Authorization Flow information @@ -1799,7 +1910,7 @@ type DeviceAuthorizationFlow struct { func (x *DeviceAuthorizationFlow) Reset() { *x = DeviceAuthorizationFlow{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[21] + mi := &file_management_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1812,7 +1923,7 @@ func (x *DeviceAuthorizationFlow) String() string { func (*DeviceAuthorizationFlow) ProtoMessage() {} func (x *DeviceAuthorizationFlow) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[21] + mi := &file_management_proto_msgTypes[22] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1825,7 +1936,7 @@ func (x *DeviceAuthorizationFlow) ProtoReflect() protoreflect.Message { // Deprecated: Use DeviceAuthorizationFlow.ProtoReflect.Descriptor instead. func (*DeviceAuthorizationFlow) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{21} + return file_management_proto_rawDescGZIP(), []int{22} } func (x *DeviceAuthorizationFlow) GetProvider() DeviceAuthorizationFlowProvider { @@ -1852,7 +1963,7 @@ type PKCEAuthorizationFlowRequest struct { func (x *PKCEAuthorizationFlowRequest) Reset() { *x = PKCEAuthorizationFlowRequest{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[22] + mi := &file_management_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1865,7 +1976,7 @@ func (x *PKCEAuthorizationFlowRequest) String() string { func (*PKCEAuthorizationFlowRequest) ProtoMessage() {} func (x *PKCEAuthorizationFlowRequest) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[22] + mi := &file_management_proto_msgTypes[23] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1878,7 +1989,7 @@ func (x *PKCEAuthorizationFlowRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PKCEAuthorizationFlowRequest.ProtoReflect.Descriptor instead. func (*PKCEAuthorizationFlowRequest) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{22} + return file_management_proto_rawDescGZIP(), []int{23} } // PKCEAuthorizationFlow represents Authorization Code Flow information @@ -1895,7 +2006,7 @@ type PKCEAuthorizationFlow struct { func (x *PKCEAuthorizationFlow) Reset() { *x = PKCEAuthorizationFlow{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[23] + mi := &file_management_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1908,7 +2019,7 @@ func (x *PKCEAuthorizationFlow) String() string { func (*PKCEAuthorizationFlow) ProtoMessage() {} func (x *PKCEAuthorizationFlow) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[23] + mi := &file_management_proto_msgTypes[24] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1921,7 +2032,7 @@ func (x *PKCEAuthorizationFlow) ProtoReflect() protoreflect.Message { // Deprecated: Use PKCEAuthorizationFlow.ProtoReflect.Descriptor instead. func (*PKCEAuthorizationFlow) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{23} + return file_management_proto_rawDescGZIP(), []int{24} } func (x *PKCEAuthorizationFlow) GetProviderConfig() *ProviderConfig { @@ -1963,7 +2074,7 @@ type ProviderConfig struct { func (x *ProviderConfig) Reset() { *x = ProviderConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[24] + mi := &file_management_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1976,7 +2087,7 @@ func (x *ProviderConfig) String() string { func (*ProviderConfig) ProtoMessage() {} func (x *ProviderConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[24] + mi := &file_management_proto_msgTypes[25] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1989,7 +2100,7 @@ func (x *ProviderConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderConfig.ProtoReflect.Descriptor instead. func (*ProviderConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{24} + return file_management_proto_rawDescGZIP(), []int{25} } func (x *ProviderConfig) GetClientID() string { @@ -2082,7 +2193,7 @@ type Route struct { func (x *Route) Reset() { *x = Route{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[25] + mi := &file_management_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2095,7 +2206,7 @@ func (x *Route) String() string { func (*Route) ProtoMessage() {} func (x *Route) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[25] + mi := &file_management_proto_msgTypes[26] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2108,7 +2219,7 @@ func (x *Route) ProtoReflect() protoreflect.Message { // Deprecated: Use Route.ProtoReflect.Descriptor instead. func (*Route) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{25} + return file_management_proto_rawDescGZIP(), []int{26} } func (x *Route) GetID() string { @@ -2188,7 +2299,7 @@ type DNSConfig struct { func (x *DNSConfig) Reset() { *x = DNSConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[26] + mi := &file_management_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2201,7 +2312,7 @@ func (x *DNSConfig) String() string { func (*DNSConfig) ProtoMessage() {} func (x *DNSConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[26] + mi := &file_management_proto_msgTypes[27] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2214,7 +2325,7 @@ func (x *DNSConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use DNSConfig.ProtoReflect.Descriptor instead. func (*DNSConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{26} + return file_management_proto_rawDescGZIP(), []int{27} } func (x *DNSConfig) GetServiceEnable() bool { @@ -2251,7 +2362,7 @@ type CustomZone struct { func (x *CustomZone) Reset() { *x = CustomZone{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[27] + mi := &file_management_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2264,7 +2375,7 @@ func (x *CustomZone) String() string { func (*CustomZone) ProtoMessage() {} func (x *CustomZone) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[27] + mi := &file_management_proto_msgTypes[28] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2277,7 +2388,7 @@ func (x *CustomZone) ProtoReflect() protoreflect.Message { // Deprecated: Use CustomZone.ProtoReflect.Descriptor instead. func (*CustomZone) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{27} + return file_management_proto_rawDescGZIP(), []int{28} } func (x *CustomZone) GetDomain() string { @@ -2310,7 +2421,7 @@ type SimpleRecord struct { func (x *SimpleRecord) Reset() { *x = SimpleRecord{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[28] + mi := &file_management_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2323,7 +2434,7 @@ func (x *SimpleRecord) String() string { func (*SimpleRecord) ProtoMessage() {} func (x *SimpleRecord) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[28] + mi := &file_management_proto_msgTypes[29] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2336,7 +2447,7 @@ func (x *SimpleRecord) ProtoReflect() protoreflect.Message { // Deprecated: Use SimpleRecord.ProtoReflect.Descriptor instead. func (*SimpleRecord) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{28} + return file_management_proto_rawDescGZIP(), []int{29} } func (x *SimpleRecord) GetName() string { @@ -2389,7 +2500,7 @@ type NameServerGroup struct { func (x *NameServerGroup) Reset() { *x = NameServerGroup{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[29] + mi := &file_management_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2402,7 +2513,7 @@ func (x *NameServerGroup) String() string { func (*NameServerGroup) ProtoMessage() {} func (x *NameServerGroup) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[29] + mi := &file_management_proto_msgTypes[30] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2415,7 +2526,7 @@ func (x *NameServerGroup) ProtoReflect() protoreflect.Message { // Deprecated: Use NameServerGroup.ProtoReflect.Descriptor instead. func (*NameServerGroup) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{29} + return file_management_proto_rawDescGZIP(), []int{30} } func (x *NameServerGroup) GetNameServers() []*NameServer { @@ -2460,7 +2571,7 @@ type NameServer struct { func (x *NameServer) Reset() { *x = NameServer{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[30] + mi := &file_management_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2473,7 +2584,7 @@ func (x *NameServer) String() string { func (*NameServer) ProtoMessage() {} func (x *NameServer) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[30] + mi := &file_management_proto_msgTypes[31] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2486,7 +2597,7 @@ func (x *NameServer) ProtoReflect() protoreflect.Message { // Deprecated: Use NameServer.ProtoReflect.Descriptor instead. func (*NameServer) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{30} + return file_management_proto_rawDescGZIP(), []int{31} } func (x *NameServer) GetIP() string { @@ -2521,12 +2632,13 @@ type FirewallRule struct { Action RuleAction `protobuf:"varint,3,opt,name=Action,proto3,enum=management.RuleAction" json:"Action,omitempty"` Protocol RuleProtocol `protobuf:"varint,4,opt,name=Protocol,proto3,enum=management.RuleProtocol" json:"Protocol,omitempty"` Port string `protobuf:"bytes,5,opt,name=Port,proto3" json:"Port,omitempty"` + PortInfo *PortInfo `protobuf:"bytes,6,opt,name=PortInfo,proto3" json:"PortInfo,omitempty"` } func (x *FirewallRule) Reset() { *x = FirewallRule{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[31] + mi := &file_management_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2539,7 +2651,7 @@ func (x *FirewallRule) String() string { func (*FirewallRule) ProtoMessage() {} func (x *FirewallRule) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[31] + mi := &file_management_proto_msgTypes[32] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2552,7 +2664,7 @@ func (x *FirewallRule) ProtoReflect() protoreflect.Message { // Deprecated: Use FirewallRule.ProtoReflect.Descriptor instead. func (*FirewallRule) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{31} + return file_management_proto_rawDescGZIP(), []int{32} } func (x *FirewallRule) GetPeerIP() string { @@ -2590,6 +2702,13 @@ func (x *FirewallRule) GetPort() string { return "" } +func (x *FirewallRule) GetPortInfo() *PortInfo { + if x != nil { + return x.PortInfo + } + return nil +} + type NetworkAddress struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2602,7 +2721,7 @@ type NetworkAddress struct { func (x *NetworkAddress) Reset() { *x = NetworkAddress{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[32] + mi := &file_management_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2615,7 +2734,7 @@ func (x *NetworkAddress) String() string { func (*NetworkAddress) ProtoMessage() {} func (x *NetworkAddress) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[32] + mi := &file_management_proto_msgTypes[33] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2628,7 +2747,7 @@ func (x *NetworkAddress) ProtoReflect() protoreflect.Message { // Deprecated: Use NetworkAddress.ProtoReflect.Descriptor instead. func (*NetworkAddress) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{32} + return file_management_proto_rawDescGZIP(), []int{33} } func (x *NetworkAddress) GetNetIP() string { @@ -2656,7 +2775,7 @@ type Checks struct { func (x *Checks) Reset() { *x = Checks{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[33] + mi := &file_management_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2669,7 +2788,7 @@ func (x *Checks) String() string { func (*Checks) ProtoMessage() {} func (x *Checks) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[33] + mi := &file_management_proto_msgTypes[34] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2682,7 +2801,7 @@ func (x *Checks) ProtoReflect() protoreflect.Message { // Deprecated: Use Checks.ProtoReflect.Descriptor instead. func (*Checks) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{33} + return file_management_proto_rawDescGZIP(), []int{34} } func (x *Checks) GetFiles() []string { @@ -2707,7 +2826,7 @@ type PortInfo struct { func (x *PortInfo) Reset() { *x = PortInfo{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[34] + mi := &file_management_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2720,7 +2839,7 @@ func (x *PortInfo) String() string { func (*PortInfo) ProtoMessage() {} func (x *PortInfo) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[34] + mi := &file_management_proto_msgTypes[35] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2733,7 +2852,7 @@ func (x *PortInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use PortInfo.ProtoReflect.Descriptor instead. func (*PortInfo) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{34} + return file_management_proto_rawDescGZIP(), []int{35} } func (m *PortInfo) GetPortSelection() isPortInfo_PortSelection { @@ -2800,7 +2919,7 @@ type RouteFirewallRule struct { func (x *RouteFirewallRule) Reset() { *x = RouteFirewallRule{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[35] + mi := &file_management_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2813,7 +2932,7 @@ func (x *RouteFirewallRule) String() string { func (*RouteFirewallRule) ProtoMessage() {} func (x *RouteFirewallRule) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[35] + mi := &file_management_proto_msgTypes[36] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2826,7 +2945,7 @@ func (x *RouteFirewallRule) ProtoReflect() protoreflect.Message { // Deprecated: Use RouteFirewallRule.ProtoReflect.Descriptor instead. func (*RouteFirewallRule) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{35} + return file_management_proto_rawDescGZIP(), []int{36} } func (x *RouteFirewallRule) GetSourceRanges() []string { @@ -2897,7 +3016,7 @@ type PortInfo_Range struct { func (x *PortInfo_Range) Reset() { *x = PortInfo_Range{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[36] + mi := &file_management_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2910,7 +3029,7 @@ func (x *PortInfo_Range) String() string { func (*PortInfo_Range) ProtoMessage() {} func (x *PortInfo_Range) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[36] + mi := &file_management_proto_msgTypes[37] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2923,7 +3042,7 @@ func (x *PortInfo_Range) ProtoReflect() protoreflect.Message { // Deprecated: Use PortInfo_Range.ProtoReflect.Descriptor instead. func (*PortInfo_Range) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{34, 0} + return file_management_proto_rawDescGZIP(), []int{35, 0} } func (x *PortInfo_Range) GetStart() uint32 { @@ -2956,416 +3075,441 @@ var file_management_proto_rawDesc = []byte{ 0x0b, 0x53, 0x79, 0x6e, 0x63, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, 0x74, - 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x22, 0xe7, 0x02, 0x0a, - 0x0c, 0x53, 0x79, 0x6e, 0x63, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, - 0x11, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x57, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, - 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x11, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, - 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, - 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x12, 0x3e, 0x0a, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, - 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, - 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, - 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, - 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x12, 0x36, 0x0a, 0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x52, 0x0a, - 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x12, 0x2a, 0x0a, 0x06, 0x43, 0x68, - 0x65, 0x63, 0x6b, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x52, 0x06, - 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x22, 0x41, 0x0a, 0x0f, 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x65, - 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, - 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x4d, - 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x22, 0xa8, 0x01, 0x0a, 0x0c, 0x4c, 0x6f, - 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, - 0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65, - 0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61, - 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x1a, 0x0a, 0x08, 0x6a, 0x77, 0x74, 0x54, 0x6f, 0x6b, - 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6a, 0x77, 0x74, 0x54, 0x6f, 0x6b, - 0x65, 0x6e, 0x12, 0x30, 0x0a, 0x08, 0x70, 0x65, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x73, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x08, 0x70, 0x65, 0x65, 0x72, - 0x4b, 0x65, 0x79, 0x73, 0x22, 0x44, 0x0a, 0x08, 0x50, 0x65, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x73, - 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1a, - 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, - 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x3f, 0x0a, 0x0b, 0x45, 0x6e, - 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6c, 0x6f, - 0x75, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x12, - 0x1a, 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x22, 0x5c, 0x0a, 0x04, 0x46, - 0x69, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x78, 0x69, 0x73, 0x74, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x65, 0x78, 0x69, 0x73, 0x74, 0x12, 0x2a, 0x0a, - 0x10, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x49, 0x73, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, - 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, - 0x49, 0x73, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x22, 0xd1, 0x04, 0x0a, 0x0e, 0x50, 0x65, - 0x65, 0x72, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1a, 0x0a, 0x08, - 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x67, 0x6f, 0x4f, 0x53, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x67, 0x6f, 0x4f, 0x53, 0x12, 0x16, 0x0a, 0x06, - 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6b, 0x65, - 0x72, 0x6e, 0x65, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, - 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, - 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x4f, 0x53, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x02, 0x4f, 0x53, 0x12, 0x2e, 0x0a, 0x12, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, - 0x74, 0x65, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x12, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x56, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, - 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x56, 0x65, 0x72, 0x73, - 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65, - 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x4f, 0x53, 0x56, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4f, 0x53, 0x56, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x46, 0x0a, 0x10, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, - 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x10, 0x6e, 0x65, - 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x12, 0x28, - 0x0a, 0x0f, 0x73, 0x79, 0x73, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, - 0x72, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x79, 0x73, 0x53, 0x65, 0x72, 0x69, - 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x0e, 0x73, 0x79, 0x73, 0x50, - 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0e, 0x73, 0x79, 0x73, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, - 0x12, 0x28, 0x0a, 0x0f, 0x73, 0x79, 0x73, 0x4d, 0x61, 0x6e, 0x75, 0x66, 0x61, 0x63, 0x74, 0x75, - 0x72, 0x65, 0x72, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x79, 0x73, 0x4d, 0x61, - 0x6e, 0x75, 0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65, 0x72, 0x12, 0x39, 0x0a, 0x0b, 0x65, 0x6e, - 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x17, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x76, - 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, - 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x26, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x10, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x22, 0xc0, 0x01, - 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x4b, 0x0a, 0x11, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x57, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, - 0x74, 0x65, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x11, 0x77, 0x69, 0x72, 0x65, 0x74, - 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, - 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, - 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2a, 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x18, 0x03, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x52, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, - 0x22, 0x79, 0x0a, 0x11, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x38, 0x0a, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, - 0x65, 0x73, 0x41, 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, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, - 0x74, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x07, 0x0a, 0x05, 0x45, - 0x6d, 0x70, 0x74, 0x79, 0x22, 0xd7, 0x01, 0x0a, 0x11, 0x57, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, - 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2c, 0x0a, 0x05, 0x73, 0x74, - 0x75, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x52, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x12, 0x35, 0x0a, 0x05, 0x74, 0x75, 0x72, 0x6e, - 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, - 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x74, 0x75, 0x72, 0x6e, 0x73, 0x12, - 0x2e, 0x0a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, - 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x12, - 0x2d, 0x0a, 0x05, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6c, 0x61, - 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x22, 0x98, - 0x01, 0x0a, 0x0a, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x10, 0x0a, - 0x03, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, - 0x3b, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0e, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, - 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, - 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x22, 0x3b, 0x0a, 0x08, - 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, - 0x00, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x54, - 0x54, 0x50, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x48, 0x54, 0x54, 0x50, 0x53, 0x10, 0x03, 0x12, - 0x08, 0x0a, 0x04, 0x44, 0x54, 0x4c, 0x53, 0x10, 0x04, 0x22, 0x6d, 0x0a, 0x0b, 0x52, 0x65, 0x6c, - 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x72, 0x6c, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x75, 0x72, 0x6c, 0x73, 0x12, 0x22, 0x0a, 0x0c, - 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, - 0x12, 0x26, 0x0a, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, - 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, - 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0x7d, 0x0a, 0x13, 0x50, 0x72, 0x6f, 0x74, - 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, - 0x36, 0x0a, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x68, 0x6f, 0x73, - 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x70, - 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, - 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0xcb, 0x01, 0x0a, 0x0a, 0x50, 0x65, 0x65, 0x72, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, - 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x12, 0x10, 0x0a, 0x03, 0x64, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x64, - 0x6e, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, 0x73, - 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x48, 0x0a, 0x1f, 0x52, - 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x65, 0x65, 0x72, 0x44, 0x6e, 0x73, 0x52, 0x65, 0x73, - 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x1f, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x65, 0x65, - 0x72, 0x44, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, - 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0xf3, 0x04, 0x0a, 0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x4d, 0x61, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x36, 0x0a, 0x0a, - 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, - 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3e, 0x0a, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, - 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, - 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, - 0x65, 0x65, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, - 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, - 0x6d, 0x70, 0x74, 0x79, 0x12, 0x29, 0x0a, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, - 0x33, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x12, 0x40, 0x0a, 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, - 0x65, 0x65, 0x72, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, - 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, - 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x3e, 0x0a, 0x0d, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, - 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, - 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, - 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, - 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x09, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, - 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x4f, 0x0a, 0x13, 0x72, 0x6f, - 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, - 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, - 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x13, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, - 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x3e, 0x0a, 0x1a, 0x72, - 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, - 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, - 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x97, 0x01, 0x0a, 0x10, - 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, - 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, - 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x12, 0x33, 0x0a, 0x09, - 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x66, 0x71, 0x64, 0x6e, 0x22, 0x49, 0x0a, 0x09, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, - 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, - 0x22, 0x20, 0x0a, 0x1e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, - 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x22, 0xbf, 0x01, 0x0a, 0x17, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, - 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x48, - 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x2c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x08, - 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, - 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, - 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, - 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x16, 0x0a, 0x08, - 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x0a, 0x0a, 0x06, 0x48, 0x4f, 0x53, 0x54, - 0x45, 0x44, 0x10, 0x00, 0x22, 0x1e, 0x0a, 0x1c, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, - 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x22, 0x5b, 0x0a, 0x15, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, - 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x42, 0x0a, + 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x22, 0xdb, 0x02, 0x0a, + 0x0c, 0x53, 0x79, 0x6e, 0x63, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, + 0x0d, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, + 0x0d, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, + 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3e, 0x0a, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, + 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, + 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, + 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, + 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, + 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x36, 0x0a, 0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x4d, 0x61, 0x70, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, + 0x61, 0x70, 0x52, 0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x12, 0x2a, + 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x68, 0x65, 0x63, + 0x6b, 0x73, 0x52, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x22, 0x41, 0x0a, 0x0f, 0x53, 0x79, + 0x6e, 0x63, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, + 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, + 0x74, 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x22, 0xc6, 0x01, + 0x0a, 0x0c, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, + 0x0a, 0x08, 0x73, 0x65, 0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x73, 0x65, 0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, + 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, + 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x1a, 0x0a, 0x08, 0x6a, 0x77, + 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6a, 0x77, + 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x30, 0x0a, 0x08, 0x70, 0x65, 0x65, 0x72, 0x4b, 0x65, + 0x79, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x08, + 0x70, 0x65, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x6e, 0x73, 0x4c, + 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x64, 0x6e, 0x73, + 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x22, 0x44, 0x0a, 0x08, 0x50, 0x65, 0x65, 0x72, 0x4b, 0x65, + 0x79, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, + 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x3f, 0x0a, 0x0b, + 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x63, + 0x6c, 0x6f, 0x75, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x63, 0x6c, 0x6f, 0x75, + 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x22, 0x5c, 0x0a, + 0x04, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x78, 0x69, + 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x65, 0x78, 0x69, 0x73, 0x74, 0x12, + 0x2a, 0x0a, 0x10, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x49, 0x73, 0x52, 0x75, 0x6e, 0x6e, + 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x70, 0x72, 0x6f, 0x63, 0x65, + 0x73, 0x73, 0x49, 0x73, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x22, 0xbf, 0x02, 0x0a, 0x05, + 0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, + 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, + 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, + 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, + 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, + 0x69, 0x76, 0x65, 0x12, 0x2a, 0x0a, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, + 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x73, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, + 0x30, 0x0a, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x64, 0x69, + 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, + 0x73, 0x12, 0x30, 0x0a, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, + 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x6f, 0x75, + 0x74, 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x44, 0x4e, + 0x53, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, + 0x44, 0x4e, 0x53, 0x12, 0x28, 0x0a, 0x0f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, + 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x64, 0x69, + 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x22, 0xf2, 0x04, + 0x0a, 0x0e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61, + 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, + 0x67, 0x6f, 0x4f, 0x53, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x67, 0x6f, 0x4f, 0x53, + 0x12, 0x16, 0x0a, 0x06, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x72, 0x65, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x1a, 0x0a, 0x08, + 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x4f, 0x53, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x4f, 0x53, 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x65, 0x74, 0x62, + 0x69, 0x72, 0x64, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0e, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x12, 0x1c, 0x0a, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x24, + 0x0a, 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x4f, 0x53, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4f, 0x53, 0x56, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x12, 0x46, 0x0a, 0x10, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, + 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x10, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x0f, 0x73, 0x79, + 0x73, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x0c, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x79, 0x73, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, + 0x6d, 0x62, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x0e, 0x73, 0x79, 0x73, 0x50, 0x72, 0x6f, 0x64, 0x75, + 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x79, + 0x73, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x28, 0x0a, 0x0f, + 0x73, 0x79, 0x73, 0x4d, 0x61, 0x6e, 0x75, 0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65, 0x72, 0x18, + 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x79, 0x73, 0x4d, 0x61, 0x6e, 0x75, 0x66, 0x61, + 0x63, 0x74, 0x75, 0x72, 0x65, 0x72, 0x12, 0x39, 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, + 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, + 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, + 0x74, 0x12, 0x26, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x10, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, + 0x6c, 0x65, 0x52, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x27, 0x0a, 0x05, 0x66, 0x6c, 0x61, + 0x67, 0x73, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x52, 0x05, 0x66, 0x6c, 0x61, + 0x67, 0x73, 0x22, 0xb4, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x0d, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0d, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2a, 0x0a, + 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, + 0x73, 0x52, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x22, 0x79, 0x0a, 0x11, 0x53, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, + 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x38, 0x0a, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 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, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0xd3, 0x01, + 0x0a, 0x0d, 0x4e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, + 0x2c, 0x0a, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x12, 0x35, 0x0a, + 0x05, 0x74, 0x75, 0x72, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, + 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x74, + 0x75, 0x72, 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x73, 0x69, + 0x67, 0x6e, 0x61, 0x6c, 0x12, 0x2d, 0x0a, 0x05, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x72, 0x65, + 0x6c, 0x61, 0x79, 0x22, 0x98, 0x01, 0x0a, 0x0a, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x75, 0x72, 0x69, 0x12, 0x3b, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x50, + 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, + 0x6c, 0x22, 0x3b, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x07, 0x0a, + 0x03, 0x55, 0x44, 0x50, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x01, 0x12, + 0x08, 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x48, 0x54, 0x54, + 0x50, 0x53, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x54, 0x4c, 0x53, 0x10, 0x04, 0x22, 0x6d, + 0x0a, 0x0b, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, + 0x04, 0x75, 0x72, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x75, 0x72, 0x6c, + 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, + 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, + 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x26, 0x0a, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, + 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x74, + 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0x7d, 0x0a, + 0x13, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x52, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, + 0x75, 0x73, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, + 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0xcb, 0x01, 0x0a, + 0x0a, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x61, + 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x64, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x64, 0x6e, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x52, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, + 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, + 0x12, 0x48, 0x0a, 0x1f, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x65, 0x65, 0x72, 0x44, + 0x6e, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, + 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1f, 0x52, 0x6f, 0x75, 0x74, 0x69, + 0x6e, 0x67, 0x50, 0x65, 0x65, 0x72, 0x44, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, + 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0xf3, 0x04, 0x0a, 0x0a, 0x4e, + 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x65, 0x72, + 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, + 0x6c, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, + 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3e, 0x0a, 0x0b, 0x72, 0x65, 0x6d, + 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, + 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, 0x72, 0x65, + 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x72, 0x65, 0x6d, + 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, + 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x29, 0x0a, 0x06, 0x52, 0x6f, 0x75, + 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x06, 0x52, 0x6f, + 0x75, 0x74, 0x65, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, + 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x40, 0x0a, 0x0c, 0x6f, 0x66, 0x66, + 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, + 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0c, 0x6f, + 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x3e, 0x0a, 0x0d, 0x46, + 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d, 0x46, 0x69, + 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x66, + 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x66, 0x69, 0x72, 0x65, 0x77, + 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, + 0x4f, 0x0a, 0x13, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, + 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, + 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x13, 0x72, 0x6f, 0x75, + 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, + 0x12, 0x3e, 0x0a, 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, + 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x0b, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, + 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x22, 0x97, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, + 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, + 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, 0x73, 0x68, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x22, 0x49, 0x0a, 0x09, 0x53, 0x53, + 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, + 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x73, 0x68, + 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, + 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, 0x68, 0x50, + 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x20, 0x0a, 0x1e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xbf, 0x01, 0x0a, 0x17, 0x44, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, + 0x6c, 0x6f, 0x77, 0x12, 0x48, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, + 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x22, 0xea, 0x02, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, - 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, - 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, - 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x44, 0x65, 0x76, 0x69, - 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, - 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x24, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, - 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x14, - 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x53, - 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, - 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, 0x54, - 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x34, 0x0a, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x09, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x52, 0x65, - 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, - 0x52, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, 0x22, 0xed, - 0x01, 0x0a, 0x05, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, - 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x12, 0x20, 0x0a, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, - 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, - 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, - 0x69, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, - 0x12, 0x1e, 0x0a, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, - 0x12, 0x14, 0x0a, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, - 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, - 0x12, 0x1c, 0x0a, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x18, 0x09, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x22, 0xb4, - 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x0d, - 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, - 0x6c, 0x65, 0x12, 0x47, 0x0a, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x38, 0x0a, 0x0b, 0x43, - 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x75, - 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, - 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x22, 0x58, 0x0a, 0x0a, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, - 0x6f, 0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x32, 0x0a, 0x07, 0x52, - 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, - 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, - 0x74, 0x0a, 0x0c, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, - 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, - 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x12, 0x10, 0x0a, - 0x03, 0x54, 0x54, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x54, 0x54, 0x4c, 0x12, - 0x14, 0x0a, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x52, 0x44, 0x61, 0x74, 0x61, 0x22, 0xb3, 0x01, 0x0a, 0x0f, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x38, 0x0a, 0x0b, 0x4e, 0x61, 0x6d, - 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, - 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, - 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x18, 0x0a, - 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, - 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, - 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, - 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x48, 0x0a, 0x0a, 0x4e, - 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x4e, 0x53, 0x54, - 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, - 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0xd9, 0x01, 0x0a, 0x0c, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, - 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x12, 0x37, - 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0e, 0x32, 0x19, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, - 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x44, 0x69, - 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, - 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, + 0x67, 0x22, 0x16, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x0a, 0x0a, + 0x06, 0x48, 0x4f, 0x53, 0x54, 0x45, 0x44, 0x10, 0x00, 0x22, 0x1e, 0x0a, 0x1c, 0x50, 0x4b, 0x43, + 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, + 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x5b, 0x0a, 0x15, 0x50, 0x4b, 0x43, + 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, + 0x6f, 0x77, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xea, 0x02, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x43, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, + 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x43, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, + 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, + 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x2e, 0x0a, + 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, + 0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x24, 0x0a, + 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, + 0x69, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x55, 0x73, 0x65, + 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x55, + 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x34, 0x0a, 0x15, 0x41, 0x75, 0x74, + 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, + 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, + 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, + 0x22, 0x0a, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, 0x18, + 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, + 0x52, 0x4c, 0x73, 0x22, 0xed, 0x01, 0x0a, 0x05, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x0e, 0x0a, + 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x18, 0x0a, + 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, + 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x20, 0x0a, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, + 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x4e, 0x65, + 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65, + 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12, 0x16, 0x0a, + 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4d, + 0x65, 0x74, 0x72, 0x69, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, + 0x61, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, + 0x65, 0x72, 0x61, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x44, + 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, + 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75, + 0x74, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, + 0x75, 0x74, 0x65, 0x22, 0xb4, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, + 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x47, 0x0a, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, + 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x10, + 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, + 0x12, 0x38, 0x0a, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x18, + 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x0b, 0x43, + 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x22, 0x58, 0x0a, 0x0a, 0x43, 0x75, + 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, + 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, + 0x12, 0x32, 0x0a, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, + 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x52, 0x65, 0x63, + 0x6f, 0x72, 0x64, 0x73, 0x22, 0x74, 0x0a, 0x0c, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, + 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, + 0x43, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x43, 0x6c, 0x61, + 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x54, 0x54, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x03, 0x54, 0x54, 0x4c, 0x12, 0x14, 0x0a, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x22, 0xb3, 0x01, 0x0a, 0x0f, 0x4e, + 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x38, + 0x0a, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0b, 0x4e, 0x61, 0x6d, + 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72, 0x69, 0x6d, + 0x61, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, + 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x32, 0x0a, 0x14, + 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, + 0x62, 0x6c, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x53, 0x65, 0x61, 0x72, + 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, + 0x22, 0x48, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e, + 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, + 0x0a, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, + 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0x8b, 0x02, 0x0a, 0x0c, 0x46, + 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x50, + 0x65, 0x65, 0x72, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x65, 0x65, + 0x72, 0x49, 0x50, 0x12, 0x37, 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x06, + 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, + 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, + 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, + 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x30, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, + 0x66, 0x6f, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, + 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x38, 0x0a, 0x0e, 0x4e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x65, + 0x74, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, + 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x61, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, + 0x61, 0x63, 0x22, 0x1e, 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x12, 0x14, 0x0a, 0x05, + 0x46, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x46, 0x69, 0x6c, + 0x65, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, + 0x14, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, + 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x32, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, + 0x48, 0x00, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x1a, 0x2f, 0x0a, 0x05, 0x52, 0x61, 0x6e, + 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x42, 0x0f, 0x0a, 0x0d, 0x70, 0x6f, + 0x72, 0x74, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xd1, 0x02, 0x0a, 0x11, + 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, + 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, + 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x74, + 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, - 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x12, 0x0a, - 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x6f, 0x72, - 0x74, 0x22, 0x38, 0x0a, 0x0e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, - 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x61, 0x63, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x61, 0x63, 0x22, 0x1e, 0x0a, 0x06, 0x43, - 0x68, 0x65, 0x63, 0x6b, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, - 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x08, - 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x32, - 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, - 0x6e, 0x66, 0x6f, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x05, 0x72, 0x61, 0x6e, - 0x67, 0x65, 0x1a, 0x2f, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, - 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, - 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, - 0x65, 0x6e, 0x64, 0x42, 0x0f, 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, 0x6c, 0x65, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xd1, 0x02, 0x0a, 0x11, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x69, - 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, - 0x52, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x2e, - 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, - 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, - 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x12, 0x34, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x30, 0x0a, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, - 0x66, 0x6f, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, - 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x73, 0x44, 0x79, - 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x44, - 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, - 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, - 0x12, 0x26, 0x0a, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, - 0x6f, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, - 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2a, 0x4c, 0x0a, 0x0c, 0x52, 0x75, 0x6c, 0x65, - 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, - 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, - 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, - 0x12, 0x08, 0x0a, 0x04, 0x49, 0x43, 0x4d, 0x50, 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55, - 0x53, 0x54, 0x4f, 0x4d, 0x10, 0x05, 0x2a, 0x20, 0x0a, 0x0d, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, - 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, - 0x07, 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x2a, 0x22, 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65, - 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, - 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x32, 0x90, 0x04, 0x0a, - 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, - 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, - 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, - 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, - 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, - 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, - 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, - 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, - 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, - 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, - 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, - 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, - 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, - 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x30, 0x0a, + 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, + 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, + 0x1c, 0x0a, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x12, 0x18, 0x0a, + 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, + 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, + 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, + 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2a, + 0x4c, 0x0a, 0x0c, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, + 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, + 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, 0x07, + 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x43, 0x4d, 0x50, 0x10, + 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55, 0x53, 0x54, 0x4f, 0x4d, 0x10, 0x05, 0x2a, 0x20, 0x0a, + 0x0d, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x06, + 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x2a, + 0x22, 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, + 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52, 0x4f, + 0x50, 0x10, 0x01, 0x32, 0x90, 0x04, 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67, + 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, - 0x12, 0x3d, 0x0a, 0x08, 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1c, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, - 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, - 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, + 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, + 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09, + 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, + 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, + 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, + 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, + 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, + 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, + 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x08, 0x53, 0x79, 0x6e, 0x63, 0x4d, + 0x65, 0x74, 0x61, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, + 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -3381,7 +3525,7 @@ func file_management_proto_rawDescGZIP() []byte { } var file_management_proto_enumTypes = make([]protoimpl.EnumInfo, 5) -var file_management_proto_msgTypes = make([]protoimpl.MessageInfo, 37) +var file_management_proto_msgTypes = make([]protoimpl.MessageInfo, 38) var file_management_proto_goTypes = []interface{}{ (RuleProtocol)(0), // 0: management.RuleProtocol (RuleDirection)(0), // 1: management.RuleDirection @@ -3396,102 +3540,105 @@ var file_management_proto_goTypes = []interface{}{ (*PeerKeys)(nil), // 10: management.PeerKeys (*Environment)(nil), // 11: management.Environment (*File)(nil), // 12: management.File - (*PeerSystemMeta)(nil), // 13: management.PeerSystemMeta - (*LoginResponse)(nil), // 14: management.LoginResponse - (*ServerKeyResponse)(nil), // 15: management.ServerKeyResponse - (*Empty)(nil), // 16: management.Empty - (*WiretrusteeConfig)(nil), // 17: management.WiretrusteeConfig - (*HostConfig)(nil), // 18: management.HostConfig - (*RelayConfig)(nil), // 19: management.RelayConfig - (*ProtectedHostConfig)(nil), // 20: management.ProtectedHostConfig - (*PeerConfig)(nil), // 21: management.PeerConfig - (*NetworkMap)(nil), // 22: management.NetworkMap - (*RemotePeerConfig)(nil), // 23: management.RemotePeerConfig - (*SSHConfig)(nil), // 24: management.SSHConfig - (*DeviceAuthorizationFlowRequest)(nil), // 25: management.DeviceAuthorizationFlowRequest - (*DeviceAuthorizationFlow)(nil), // 26: management.DeviceAuthorizationFlow - (*PKCEAuthorizationFlowRequest)(nil), // 27: management.PKCEAuthorizationFlowRequest - (*PKCEAuthorizationFlow)(nil), // 28: management.PKCEAuthorizationFlow - (*ProviderConfig)(nil), // 29: management.ProviderConfig - (*Route)(nil), // 30: management.Route - (*DNSConfig)(nil), // 31: management.DNSConfig - (*CustomZone)(nil), // 32: management.CustomZone - (*SimpleRecord)(nil), // 33: management.SimpleRecord - (*NameServerGroup)(nil), // 34: management.NameServerGroup - (*NameServer)(nil), // 35: management.NameServer - (*FirewallRule)(nil), // 36: management.FirewallRule - (*NetworkAddress)(nil), // 37: management.NetworkAddress - (*Checks)(nil), // 38: management.Checks - (*PortInfo)(nil), // 39: management.PortInfo - (*RouteFirewallRule)(nil), // 40: management.RouteFirewallRule - (*PortInfo_Range)(nil), // 41: management.PortInfo.Range - (*timestamppb.Timestamp)(nil), // 42: google.protobuf.Timestamp + (*Flags)(nil), // 13: management.Flags + (*PeerSystemMeta)(nil), // 14: management.PeerSystemMeta + (*LoginResponse)(nil), // 15: management.LoginResponse + (*ServerKeyResponse)(nil), // 16: management.ServerKeyResponse + (*Empty)(nil), // 17: management.Empty + (*NetbirdConfig)(nil), // 18: management.NetbirdConfig + (*HostConfig)(nil), // 19: management.HostConfig + (*RelayConfig)(nil), // 20: management.RelayConfig + (*ProtectedHostConfig)(nil), // 21: management.ProtectedHostConfig + (*PeerConfig)(nil), // 22: management.PeerConfig + (*NetworkMap)(nil), // 23: management.NetworkMap + (*RemotePeerConfig)(nil), // 24: management.RemotePeerConfig + (*SSHConfig)(nil), // 25: management.SSHConfig + (*DeviceAuthorizationFlowRequest)(nil), // 26: management.DeviceAuthorizationFlowRequest + (*DeviceAuthorizationFlow)(nil), // 27: management.DeviceAuthorizationFlow + (*PKCEAuthorizationFlowRequest)(nil), // 28: management.PKCEAuthorizationFlowRequest + (*PKCEAuthorizationFlow)(nil), // 29: management.PKCEAuthorizationFlow + (*ProviderConfig)(nil), // 30: management.ProviderConfig + (*Route)(nil), // 31: management.Route + (*DNSConfig)(nil), // 32: management.DNSConfig + (*CustomZone)(nil), // 33: management.CustomZone + (*SimpleRecord)(nil), // 34: management.SimpleRecord + (*NameServerGroup)(nil), // 35: management.NameServerGroup + (*NameServer)(nil), // 36: management.NameServer + (*FirewallRule)(nil), // 37: management.FirewallRule + (*NetworkAddress)(nil), // 38: management.NetworkAddress + (*Checks)(nil), // 39: management.Checks + (*PortInfo)(nil), // 40: management.PortInfo + (*RouteFirewallRule)(nil), // 41: management.RouteFirewallRule + (*PortInfo_Range)(nil), // 42: management.PortInfo.Range + (*timestamppb.Timestamp)(nil), // 43: google.protobuf.Timestamp } var file_management_proto_depIdxs = []int32{ - 13, // 0: management.SyncRequest.meta:type_name -> management.PeerSystemMeta - 17, // 1: management.SyncResponse.wiretrusteeConfig:type_name -> management.WiretrusteeConfig - 21, // 2: management.SyncResponse.peerConfig:type_name -> management.PeerConfig - 23, // 3: management.SyncResponse.remotePeers:type_name -> management.RemotePeerConfig - 22, // 4: management.SyncResponse.NetworkMap:type_name -> management.NetworkMap - 38, // 5: management.SyncResponse.Checks:type_name -> management.Checks - 13, // 6: management.SyncMetaRequest.meta:type_name -> management.PeerSystemMeta - 13, // 7: management.LoginRequest.meta:type_name -> management.PeerSystemMeta + 14, // 0: management.SyncRequest.meta:type_name -> management.PeerSystemMeta + 18, // 1: management.SyncResponse.netbirdConfig:type_name -> management.NetbirdConfig + 22, // 2: management.SyncResponse.peerConfig:type_name -> management.PeerConfig + 24, // 3: management.SyncResponse.remotePeers:type_name -> management.RemotePeerConfig + 23, // 4: management.SyncResponse.NetworkMap:type_name -> management.NetworkMap + 39, // 5: management.SyncResponse.Checks:type_name -> management.Checks + 14, // 6: management.SyncMetaRequest.meta:type_name -> management.PeerSystemMeta + 14, // 7: management.LoginRequest.meta:type_name -> management.PeerSystemMeta 10, // 8: management.LoginRequest.peerKeys:type_name -> management.PeerKeys - 37, // 9: management.PeerSystemMeta.networkAddresses:type_name -> management.NetworkAddress + 38, // 9: management.PeerSystemMeta.networkAddresses:type_name -> management.NetworkAddress 11, // 10: management.PeerSystemMeta.environment:type_name -> management.Environment 12, // 11: management.PeerSystemMeta.files:type_name -> management.File - 17, // 12: management.LoginResponse.wiretrusteeConfig:type_name -> management.WiretrusteeConfig - 21, // 13: management.LoginResponse.peerConfig:type_name -> management.PeerConfig - 38, // 14: management.LoginResponse.Checks:type_name -> management.Checks - 42, // 15: management.ServerKeyResponse.expiresAt:type_name -> google.protobuf.Timestamp - 18, // 16: management.WiretrusteeConfig.stuns:type_name -> management.HostConfig - 20, // 17: management.WiretrusteeConfig.turns:type_name -> management.ProtectedHostConfig - 18, // 18: management.WiretrusteeConfig.signal:type_name -> management.HostConfig - 19, // 19: management.WiretrusteeConfig.relay:type_name -> management.RelayConfig - 3, // 20: management.HostConfig.protocol:type_name -> management.HostConfig.Protocol - 18, // 21: management.ProtectedHostConfig.hostConfig:type_name -> management.HostConfig - 24, // 22: management.PeerConfig.sshConfig:type_name -> management.SSHConfig - 21, // 23: management.NetworkMap.peerConfig:type_name -> management.PeerConfig - 23, // 24: management.NetworkMap.remotePeers:type_name -> management.RemotePeerConfig - 30, // 25: management.NetworkMap.Routes:type_name -> management.Route - 31, // 26: management.NetworkMap.DNSConfig:type_name -> management.DNSConfig - 23, // 27: management.NetworkMap.offlinePeers:type_name -> management.RemotePeerConfig - 36, // 28: management.NetworkMap.FirewallRules:type_name -> management.FirewallRule - 40, // 29: management.NetworkMap.routesFirewallRules:type_name -> management.RouteFirewallRule - 24, // 30: management.RemotePeerConfig.sshConfig:type_name -> management.SSHConfig - 4, // 31: management.DeviceAuthorizationFlow.Provider:type_name -> management.DeviceAuthorizationFlow.provider - 29, // 32: management.DeviceAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig - 29, // 33: management.PKCEAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig - 34, // 34: management.DNSConfig.NameServerGroups:type_name -> management.NameServerGroup - 32, // 35: management.DNSConfig.CustomZones:type_name -> management.CustomZone - 33, // 36: management.CustomZone.Records:type_name -> management.SimpleRecord - 35, // 37: management.NameServerGroup.NameServers:type_name -> management.NameServer - 1, // 38: management.FirewallRule.Direction:type_name -> management.RuleDirection - 2, // 39: management.FirewallRule.Action:type_name -> management.RuleAction - 0, // 40: management.FirewallRule.Protocol:type_name -> management.RuleProtocol - 41, // 41: management.PortInfo.range:type_name -> management.PortInfo.Range - 2, // 42: management.RouteFirewallRule.action:type_name -> management.RuleAction - 0, // 43: management.RouteFirewallRule.protocol:type_name -> management.RuleProtocol - 39, // 44: management.RouteFirewallRule.portInfo:type_name -> management.PortInfo - 5, // 45: management.ManagementService.Login:input_type -> management.EncryptedMessage - 5, // 46: management.ManagementService.Sync:input_type -> management.EncryptedMessage - 16, // 47: management.ManagementService.GetServerKey:input_type -> management.Empty - 16, // 48: management.ManagementService.isHealthy:input_type -> management.Empty - 5, // 49: management.ManagementService.GetDeviceAuthorizationFlow:input_type -> management.EncryptedMessage - 5, // 50: management.ManagementService.GetPKCEAuthorizationFlow:input_type -> management.EncryptedMessage - 5, // 51: management.ManagementService.SyncMeta:input_type -> management.EncryptedMessage - 5, // 52: management.ManagementService.Login:output_type -> management.EncryptedMessage - 5, // 53: management.ManagementService.Sync:output_type -> management.EncryptedMessage - 15, // 54: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse - 16, // 55: management.ManagementService.isHealthy:output_type -> management.Empty - 5, // 56: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage - 5, // 57: management.ManagementService.GetPKCEAuthorizationFlow:output_type -> management.EncryptedMessage - 16, // 58: management.ManagementService.SyncMeta:output_type -> management.Empty - 52, // [52:59] is the sub-list for method output_type - 45, // [45:52] is the sub-list for method input_type - 45, // [45:45] is the sub-list for extension type_name - 45, // [45:45] is the sub-list for extension extendee - 0, // [0:45] is the sub-list for field type_name + 13, // 12: management.PeerSystemMeta.flags:type_name -> management.Flags + 18, // 13: management.LoginResponse.netbirdConfig:type_name -> management.NetbirdConfig + 22, // 14: management.LoginResponse.peerConfig:type_name -> management.PeerConfig + 39, // 15: management.LoginResponse.Checks:type_name -> management.Checks + 43, // 16: management.ServerKeyResponse.expiresAt:type_name -> google.protobuf.Timestamp + 19, // 17: management.NetbirdConfig.stuns:type_name -> management.HostConfig + 21, // 18: management.NetbirdConfig.turns:type_name -> management.ProtectedHostConfig + 19, // 19: management.NetbirdConfig.signal:type_name -> management.HostConfig + 20, // 20: management.NetbirdConfig.relay:type_name -> management.RelayConfig + 3, // 21: management.HostConfig.protocol:type_name -> management.HostConfig.Protocol + 19, // 22: management.ProtectedHostConfig.hostConfig:type_name -> management.HostConfig + 25, // 23: management.PeerConfig.sshConfig:type_name -> management.SSHConfig + 22, // 24: management.NetworkMap.peerConfig:type_name -> management.PeerConfig + 24, // 25: management.NetworkMap.remotePeers:type_name -> management.RemotePeerConfig + 31, // 26: management.NetworkMap.Routes:type_name -> management.Route + 32, // 27: management.NetworkMap.DNSConfig:type_name -> management.DNSConfig + 24, // 28: management.NetworkMap.offlinePeers:type_name -> management.RemotePeerConfig + 37, // 29: management.NetworkMap.FirewallRules:type_name -> management.FirewallRule + 41, // 30: management.NetworkMap.routesFirewallRules:type_name -> management.RouteFirewallRule + 25, // 31: management.RemotePeerConfig.sshConfig:type_name -> management.SSHConfig + 4, // 32: management.DeviceAuthorizationFlow.Provider:type_name -> management.DeviceAuthorizationFlow.provider + 30, // 33: management.DeviceAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig + 30, // 34: management.PKCEAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig + 35, // 35: management.DNSConfig.NameServerGroups:type_name -> management.NameServerGroup + 33, // 36: management.DNSConfig.CustomZones:type_name -> management.CustomZone + 34, // 37: management.CustomZone.Records:type_name -> management.SimpleRecord + 36, // 38: management.NameServerGroup.NameServers:type_name -> management.NameServer + 1, // 39: management.FirewallRule.Direction:type_name -> management.RuleDirection + 2, // 40: management.FirewallRule.Action:type_name -> management.RuleAction + 0, // 41: management.FirewallRule.Protocol:type_name -> management.RuleProtocol + 40, // 42: management.FirewallRule.PortInfo:type_name -> management.PortInfo + 42, // 43: management.PortInfo.range:type_name -> management.PortInfo.Range + 2, // 44: management.RouteFirewallRule.action:type_name -> management.RuleAction + 0, // 45: management.RouteFirewallRule.protocol:type_name -> management.RuleProtocol + 40, // 46: management.RouteFirewallRule.portInfo:type_name -> management.PortInfo + 5, // 47: management.ManagementService.Login:input_type -> management.EncryptedMessage + 5, // 48: management.ManagementService.Sync:input_type -> management.EncryptedMessage + 17, // 49: management.ManagementService.GetServerKey:input_type -> management.Empty + 17, // 50: management.ManagementService.isHealthy:input_type -> management.Empty + 5, // 51: management.ManagementService.GetDeviceAuthorizationFlow:input_type -> management.EncryptedMessage + 5, // 52: management.ManagementService.GetPKCEAuthorizationFlow:input_type -> management.EncryptedMessage + 5, // 53: management.ManagementService.SyncMeta:input_type -> management.EncryptedMessage + 5, // 54: management.ManagementService.Login:output_type -> management.EncryptedMessage + 5, // 55: management.ManagementService.Sync:output_type -> management.EncryptedMessage + 16, // 56: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse + 17, // 57: management.ManagementService.isHealthy:output_type -> management.Empty + 5, // 58: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage + 5, // 59: management.ManagementService.GetPKCEAuthorizationFlow:output_type -> management.EncryptedMessage + 17, // 60: management.ManagementService.SyncMeta:output_type -> management.Empty + 54, // [54:61] is the sub-list for method output_type + 47, // [47:54] is the sub-list for method input_type + 47, // [47:47] is the sub-list for extension type_name + 47, // [47:47] is the sub-list for extension extendee + 0, // [0:47] is the sub-list for field type_name } func init() { file_management_proto_init() } @@ -3597,7 +3744,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PeerSystemMeta); i { + switch v := v.(*Flags); i { case 0: return &v.state case 1: @@ -3609,7 +3756,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*LoginResponse); i { + switch v := v.(*PeerSystemMeta); i { case 0: return &v.state case 1: @@ -3621,7 +3768,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ServerKeyResponse); i { + switch v := v.(*LoginResponse); i { case 0: return &v.state case 1: @@ -3633,7 +3780,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Empty); i { + switch v := v.(*ServerKeyResponse); i { case 0: return &v.state case 1: @@ -3645,7 +3792,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*WiretrusteeConfig); i { + switch v := v.(*Empty); i { case 0: return &v.state case 1: @@ -3657,7 +3804,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*HostConfig); i { + switch v := v.(*NetbirdConfig); i { case 0: return &v.state case 1: @@ -3669,7 +3816,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RelayConfig); i { + switch v := v.(*HostConfig); i { case 0: return &v.state case 1: @@ -3681,7 +3828,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ProtectedHostConfig); i { + switch v := v.(*RelayConfig); i { case 0: return &v.state case 1: @@ -3693,7 +3840,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PeerConfig); i { + switch v := v.(*ProtectedHostConfig); i { case 0: return &v.state case 1: @@ -3705,7 +3852,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NetworkMap); i { + switch v := v.(*PeerConfig); i { case 0: return &v.state case 1: @@ -3717,7 +3864,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RemotePeerConfig); i { + switch v := v.(*NetworkMap); i { case 0: return &v.state case 1: @@ -3729,7 +3876,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SSHConfig); i { + switch v := v.(*RemotePeerConfig); i { case 0: return &v.state case 1: @@ -3741,7 +3888,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeviceAuthorizationFlowRequest); i { + switch v := v.(*SSHConfig); i { case 0: return &v.state case 1: @@ -3753,7 +3900,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeviceAuthorizationFlow); i { + switch v := v.(*DeviceAuthorizationFlowRequest); i { case 0: return &v.state case 1: @@ -3765,7 +3912,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PKCEAuthorizationFlowRequest); i { + switch v := v.(*DeviceAuthorizationFlow); i { case 0: return &v.state case 1: @@ -3777,7 +3924,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PKCEAuthorizationFlow); i { + switch v := v.(*PKCEAuthorizationFlowRequest); i { case 0: return &v.state case 1: @@ -3789,7 +3936,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ProviderConfig); i { + switch v := v.(*PKCEAuthorizationFlow); i { case 0: return &v.state case 1: @@ -3801,7 +3948,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Route); i { + switch v := v.(*ProviderConfig); i { case 0: return &v.state case 1: @@ -3813,7 +3960,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DNSConfig); i { + switch v := v.(*Route); i { case 0: return &v.state case 1: @@ -3825,7 +3972,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CustomZone); i { + switch v := v.(*DNSConfig); i { case 0: return &v.state case 1: @@ -3837,7 +3984,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SimpleRecord); i { + switch v := v.(*CustomZone); i { case 0: return &v.state case 1: @@ -3849,7 +3996,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NameServerGroup); i { + switch v := v.(*SimpleRecord); i { case 0: return &v.state case 1: @@ -3861,7 +4008,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NameServer); i { + switch v := v.(*NameServerGroup); i { case 0: return &v.state case 1: @@ -3873,7 +4020,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FirewallRule); i { + switch v := v.(*NameServer); i { case 0: return &v.state case 1: @@ -3885,7 +4032,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NetworkAddress); i { + switch v := v.(*FirewallRule); i { case 0: return &v.state case 1: @@ -3897,7 +4044,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[33].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Checks); i { + switch v := v.(*NetworkAddress); i { case 0: return &v.state case 1: @@ -3909,7 +4056,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[34].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PortInfo); i { + switch v := v.(*Checks); i { case 0: return &v.state case 1: @@ -3921,7 +4068,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[35].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RouteFirewallRule); i { + switch v := v.(*PortInfo); i { case 0: return &v.state case 1: @@ -3933,6 +4080,18 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[36].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RouteFirewallRule); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_management_proto_msgTypes[37].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*PortInfo_Range); i { case 0: return &v.state @@ -3945,7 +4104,7 @@ func file_management_proto_init() { } } } - file_management_proto_msgTypes[34].OneofWrappers = []interface{}{ + file_management_proto_msgTypes[35].OneofWrappers = []interface{}{ (*PortInfo_Port)(nil), (*PortInfo_Range_)(nil), } @@ -3955,7 +4114,7 @@ func file_management_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_management_proto_rawDesc, NumEnums: 5, - NumMessages: 37, + NumMessages: 38, NumExtensions: 0, NumServices: 1, }, diff --git a/management/proto/management.proto b/management/proto/management.proto index 5f4e0df46..cd207136f 100644 --- a/management/proto/management.proto +++ b/management/proto/management.proto @@ -52,7 +52,7 @@ message EncryptedMessage { // encrypted message Body bytes body = 2; - // Version of the Wiretrustee Management Service protocol + // Version of the Netbird Management Service protocol int32 version = 3; } @@ -61,11 +61,11 @@ message SyncRequest { PeerSystemMeta meta = 1; } -// SyncResponse represents a state that should be applied to the local peer (e.g. Wiretrustee servers config as well as local peer and remote peers configs) +// SyncResponse represents a state that should be applied to the local peer (e.g. Netbird servers config as well as local peer and remote peers configs) message SyncResponse { // Global config - WiretrusteeConfig wiretrusteeConfig = 1; + NetbirdConfig netbirdConfig = 1; // Deprecated. Use NetworkMap.PeerConfig PeerConfig peerConfig = 2; @@ -97,7 +97,8 @@ message LoginRequest { string jwtToken = 3; // Can be absent for now. PeerKeys peerKeys = 4; - + + repeated string dnsLabels = 5; } // PeerKeys is additional peer info like SSH pub key and WireGuard public key. @@ -128,6 +129,16 @@ message File { bool processIsRunning = 3; } +message Flags { + bool rosenpassEnabled = 1; + bool rosenpassPermissive = 2; + bool serverSSHAllowed = 3; + bool disableClientRoutes = 4; + bool disableServerRoutes = 5; + bool disableDNS = 6; + bool disableFirewall = 7; +} + // PeerSystemMeta is machine meta data like OS and version. message PeerSystemMeta { string hostname = 1; @@ -136,7 +147,7 @@ message PeerSystemMeta { string core = 4; string platform = 5; string OS = 6; - string wiretrusteeVersion = 7; + string netbirdVersion = 7; string uiVersion = 8; string kernelVersion = 9; string OSVersion = 10; @@ -146,11 +157,12 @@ message PeerSystemMeta { string sysManufacturer = 14; Environment environment = 15; repeated File files = 16; + Flags flags = 17; } message LoginResponse { // Global config - WiretrusteeConfig wiretrusteeConfig = 1; + NetbirdConfig netbirdConfig = 1; // Peer local config PeerConfig peerConfig = 2; // Posture checks to be evaluated by client @@ -162,14 +174,14 @@ message ServerKeyResponse { string key = 1; // Key expiration timestamp after which the key should be fetched again by the client google.protobuf.Timestamp expiresAt = 2; - // Version of the Wiretrustee Management Service protocol + // Version of the Netbird Management Service protocol int32 version = 3; } message Empty {} -// WiretrusteeConfig is a common configuration of any Wiretrustee peer. It contains STUN, TURN, Signal and Management servers configurations -message WiretrusteeConfig { +// NetbirdConfig is a common configuration of any Netbird peer. It contains STUN, TURN, Signal and Management servers configurations +message NetbirdConfig { // a list of STUN servers repeated HostConfig stuns = 1; // a list of TURN servers @@ -183,7 +195,7 @@ message WiretrusteeConfig { // HostConfig describes connection properties of some server (e.g. STUN, Signal, Management) message HostConfig { - // URI of the resource e.g. turns://stun.wiretrustee.com:4430 or signal.wiretrustee.com:10000 + // URI of the resource e.g. turns://stun.netbird.io:4430 or signal.netbird.io:10000 string uri = 1; Protocol protocol = 2; @@ -213,9 +225,9 @@ message ProtectedHostConfig { // PeerConfig represents a configuration of a "our" peer. // The properties are used to configure local Wireguard message PeerConfig { - // Peer's virtual IP address within the Wiretrustee VPN (a Wireguard address config) + // Peer's virtual IP address within the Netbird VPN (a Wireguard address config) string address = 1; - // Wiretrustee DNS server (a Wireguard DNS config) + // Netbird DNS server (a Wireguard DNS config) string dns = 2; // SSHConfig of the peer. @@ -419,6 +431,7 @@ message FirewallRule { RuleAction Action = 3; RuleProtocol Protocol = 4; string Port = 5; + PortInfo PortInfo = 6; } message NetworkAddress { diff --git a/management/server/account.go b/management/server/account.go index 17fee541e..332d356e2 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -2,11 +2,8 @@ package server import ( "context" - "crypto/sha256" - b64 "encoding/base64" "errors" "fmt" - "hash/crc32" "math/rand" "net" "net/netip" @@ -24,14 +21,13 @@ import ( log "github.com/sirupsen/logrus" "golang.org/x/exp/maps" - "github.com/netbirdio/netbird/base62" nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/domain" "github.com/netbirdio/netbird/management/server/activity" + nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/geolocation" "github.com/netbirdio/netbird/management/server/idp" "github.com/netbirdio/netbird/management/server/integrated_validator" - "github.com/netbirdio/netbird/management/server/jwtclaims" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/status" @@ -63,11 +59,11 @@ type AccountManager interface { GetOrCreateAccountByUser(ctx context.Context, userId, domain string) (*types.Account, error) GetAccount(ctx context.Context, accountID string) (*types.Account, error) CreateSetupKey(ctx context.Context, accountID string, keyName string, keyType types.SetupKeyType, expiresIn time.Duration, - autoGroups []string, usageLimit int, userID string, ephemeral bool) (*types.SetupKey, error) + autoGroups []string, usageLimit int, userID string, ephemeral bool, allowExtraDNSLabels bool) (*types.SetupKey, error) SaveSetupKey(ctx context.Context, accountID string, key *types.SetupKey, userID string) (*types.SetupKey, error) CreateUser(ctx context.Context, accountID, initiatorUserID string, key *types.UserInfo) (*types.UserInfo, error) DeleteUser(ctx context.Context, accountID, initiatorUserID string, targetUserID string) error - DeleteRegularUsers(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string) error + DeleteRegularUsers(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string, userInfos map[string]*types.UserInfo) error InviteUser(ctx context.Context, accountID string, initiatorUserID string, targetUserID string) error ListSetupKeys(ctx context.Context, accountID, userID string) ([]*types.SetupKey, error) SaveUser(ctx context.Context, accountID, initiatorUserID string, update *types.User) (*types.UserInfo, error) @@ -77,13 +73,10 @@ type AccountManager interface { GetAccountByID(ctx context.Context, accountID string, userID string) (*types.Account, error) AccountExists(ctx context.Context, accountID string) (bool, error) GetAccountIDByUserID(ctx context.Context, userID, domain string) (string, error) - GetAccountIDFromToken(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) - CheckUserAccessByJWTGroups(ctx context.Context, claims jwtclaims.AuthorizationClaims) error - GetAccountInfoFromPAT(ctx context.Context, token string) (*types.User, *types.PersonalAccessToken, string, string, error) + GetAccountIDFromUserAuth(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error) DeleteAccount(ctx context.Context, accountID, userID string) error - MarkPATUsed(ctx context.Context, tokenID string) error GetUserByID(ctx context.Context, id string) (*types.User, error) - GetUser(ctx context.Context, claims jwtclaims.AuthorizationClaims) (*types.User, error) + GetUserFromUserAuth(ctx context.Context, userAuth nbcontext.UserAuth) (*types.User, error) ListUsers(ctx context.Context, accountID string) ([]*types.User, error) GetPeers(ctx context.Context, accountID, userID string) ([]*nbpeer.Peer, error) MarkPeerConnected(ctx context.Context, peerKey string, connected bool, realIP net.IP, accountID string) error @@ -96,7 +89,7 @@ type AccountManager interface { DeletePAT(ctx context.Context, accountID string, initiatorUserID string, targetUserID string, tokenID string) error GetPAT(ctx context.Context, accountID string, initiatorUserID string, targetUserID string, tokenID string) (*types.PersonalAccessToken, error) GetAllPATs(ctx context.Context, accountID string, initiatorUserID string, targetUserID string) ([]*types.PersonalAccessToken, error) - GetUsersFromAccount(ctx context.Context, accountID, userID string) ([]*types.UserInfo, error) + GetUsersFromAccount(ctx context.Context, accountID, userID string) (map[string]*types.UserInfo, error) GetGroup(ctx context.Context, accountId, groupID, userID string) (*types.Group, error) GetAllGroups(ctx context.Context, accountID, userID string) ([]*types.Group, error) GetGroupByName(ctx context.Context, groupName, accountID string) (*types.Group, error) @@ -149,6 +142,8 @@ type AccountManager interface { GetAccountSettings(ctx context.Context, accountID string, userID string) (*types.Settings, error) DeleteSetupKey(ctx context.Context, accountID, userID, keyID string) error UpdateAccountPeers(ctx context.Context, accountID string) + BuildUserInfosForAccount(ctx context.Context, accountID, initiatorUserID string, accountUsers []*types.User) (map[string]*types.UserInfo, error) + SyncUserJWTGroups(ctx context.Context, userAuth nbcontext.UserAuth) error } type DefaultAccountManager struct { @@ -251,6 +246,11 @@ func BuildManager( integratedPeerValidator integrated_validator.IntegratedValidator, metrics telemetry.AppMetrics, ) (*DefaultAccountManager, error) { + start := time.Now() + defer func() { + log.WithContext(ctx).Debugf("took %v to instantiate account manager", time.Since(start)) + }() + am := &DefaultAccountManager{ Store: store, geo: geo, @@ -268,39 +268,21 @@ func BuildManager( metrics: metrics, requestBuffer: NewAccountRequestBuffer(ctx, store), } - allAccounts := store.GetAllAccounts(ctx) + accountsCounter, err := store.GetAccountsCounter(ctx) + if err != nil { + log.WithContext(ctx).Error(err) + } + // enable single account mode only if configured by user and number of existing accounts is not grater than 1 - am.singleAccountMode = singleAccountModeDomain != "" && len(allAccounts) <= 1 + am.singleAccountMode = singleAccountModeDomain != "" && accountsCounter <= 1 if am.singleAccountMode { if !isDomainValid(singleAccountModeDomain) { return nil, status.Errorf(status.InvalidArgument, "invalid domain \"%s\" provided for a single account mode. Please review your input for --single-account-mode-domain", singleAccountModeDomain) } am.singleAccountModeDomain = singleAccountModeDomain - log.WithContext(ctx).Infof("single account mode enabled, accounts number %d", len(allAccounts)) + log.WithContext(ctx).Infof("single account mode enabled, accounts number %d", accountsCounter) } else { - log.WithContext(ctx).Infof("single account mode disabled, accounts number %d", len(allAccounts)) - } - - // if account doesn't have a default group - // we create 'all' group and add all peers into it - // also we create default rule with source as destination - for _, account := range allAccounts { - shouldSave := false - - _, err := account.GetGroupAll() - if err != nil { - if err := addAllGroup(account); err != nil { - return nil, err - } - shouldSave = true - } - - if shouldSave { - err = store.SaveAccount(ctx, account) - if err != nil { - return nil, err - } - } + log.WithContext(ctx).Infof("single account mode disabled, accounts number %d", accountsCounter) } goCacheClient := gocache.New(CacheExpirationMax, 30*time.Minute) @@ -617,6 +599,12 @@ func (am *DefaultAccountManager) DeleteAccount(ctx context.Context, accountID, u if user.Role != types.UserRoleOwner { return status.Errorf(status.PermissionDenied, "user is not allowed to delete account. Only account owner can delete account") } + + userInfosMap, err := am.BuildUserInfosForAccount(ctx, accountID, userID, maps.Values(account.Users)) + if err != nil { + return status.Errorf(status.Internal, "failed to build user infos for account %s: %v", accountID, err) + } + for _, otherUser := range account.Users { if otherUser.IsServiceUser { continue @@ -626,13 +614,23 @@ func (am *DefaultAccountManager) DeleteAccount(ctx context.Context, accountID, u continue } - _, deleteUserErr := am.deleteRegularUser(ctx, accountID, userID, otherUser.Id) + userInfo, ok := userInfosMap[otherUser.Id] + if !ok { + return status.Errorf(status.NotFound, "user info not found for user %s", otherUser.Id) + } + + _, deleteUserErr := am.deleteRegularUser(ctx, accountID, userID, userInfo) if deleteUserErr != nil { return deleteUserErr } } - _, err = am.deleteRegularUser(ctx, accountID, userID, userID) + userInfo, ok := userInfosMap[userID] + if !ok { + return status.Errorf(status.NotFound, "user info not found for user %s", userID) + } + + _, err = am.deleteRegularUser(ctx, accountID, userID, userInfo) if err != nil { log.WithContext(ctx).Errorf("failed deleting user %s. error: %s", userID, err) return err @@ -690,7 +688,7 @@ func isNil(i idp.Manager) bool { func (am *DefaultAccountManager) addAccountIDToIDPAppMeta(ctx context.Context, userID string, accountID string) error { if !isNil(am.idpManager) { // user can be nil if it wasn't found (e.g., just created) - user, err := am.lookupUserInCache(ctx, am.Store, userID, accountID) + user, err := am.lookupUserInCache(ctx, userID, accountID) if err != nil { return err } @@ -766,8 +764,8 @@ func (am *DefaultAccountManager) lookupUserInCacheByEmail(ctx context.Context, e } // lookupUserInCache looks up user in the IdP cache and returns it. If the user wasn't found, the function returns nil -func (am *DefaultAccountManager) lookupUserInCache(ctx context.Context, transaction store.Store, userID string, accountID string) (*idp.UserData, error) { - accountUsers, err := transaction.GetAccountUsers(ctx, store.LockingStrengthShare, accountID) +func (am *DefaultAccountManager) lookupUserInCache(ctx context.Context, userID string, accountID string) (*idp.UserData, error) { + accountUsers, err := am.Store.GetAccountUsers(ctx, store.LockingStrengthShare, accountID) if err != nil { return nil, err } @@ -797,7 +795,7 @@ func (am *DefaultAccountManager) lookupUserInCache(ctx context.Context, transact // add extra check on external cache manager. We may get to this point when the user is not yet findable in IDP, // or it didn't have its metadata updated with am.addAccountIDToIDPAppMeta - user, err := transaction.GetUserByUserID(ctx, store.LockingStrengthShare, userID) + user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID) if err != nil { log.WithContext(ctx).Errorf("failed finding user %s in account %s", userID, accountID) return nil, err @@ -937,11 +935,11 @@ func (am *DefaultAccountManager) removeUserFromCache(ctx context.Context, accoun } // updateAccountDomainAttributesIfNotUpToDate updates the account domain attributes if they are not up to date and then, saves the account changes -func (am *DefaultAccountManager) updateAccountDomainAttributesIfNotUpToDate(ctx context.Context, accountID string, claims jwtclaims.AuthorizationClaims, +func (am *DefaultAccountManager) updateAccountDomainAttributesIfNotUpToDate(ctx context.Context, accountID string, userAuth nbcontext.UserAuth, primaryDomain bool, ) error { - if claims.Domain == "" { - log.WithContext(ctx).Errorf("claims don't contain a valid domain, skipping domain attributes update. Received claims: %v", claims) + if userAuth.Domain == "" { + log.WithContext(ctx).Errorf("claims don't contain a valid domain, skipping domain attributes update. Received claims: %v", userAuth) return nil } @@ -954,11 +952,11 @@ func (am *DefaultAccountManager) updateAccountDomainAttributesIfNotUpToDate(ctx return err } - if domainIsUpToDate(accountDomain, domainCategory, claims) { + if domainIsUpToDate(accountDomain, domainCategory, userAuth) { return nil } - user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, claims.UserId) + user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userAuth.UserId) if err != nil { log.WithContext(ctx).Errorf("error getting user: %v", err) return err @@ -967,13 +965,13 @@ func (am *DefaultAccountManager) updateAccountDomainAttributesIfNotUpToDate(ctx newDomain := accountDomain newCategoty := domainCategory - lowerDomain := strings.ToLower(claims.Domain) + lowerDomain := strings.ToLower(userAuth.Domain) if accountDomain != lowerDomain && user.HasAdminPower() { newDomain = lowerDomain } if accountDomain == lowerDomain { - newCategoty = claims.DomainCategory + newCategoty = userAuth.DomainCategory } return am.Store.UpdateAccountDomainAttributes(ctx, accountID, newDomain, newCategoty, primaryDomain) @@ -989,16 +987,16 @@ func (am *DefaultAccountManager) handleExistingUserAccount( ctx context.Context, userAccountID string, domainAccountID string, - claims jwtclaims.AuthorizationClaims, + userAuth nbcontext.UserAuth, ) error { primaryDomain := domainAccountID == "" || userAccountID == domainAccountID - err := am.updateAccountDomainAttributesIfNotUpToDate(ctx, userAccountID, claims, primaryDomain) + err := am.updateAccountDomainAttributesIfNotUpToDate(ctx, userAccountID, userAuth, primaryDomain) if err != nil { return err } // we should register the account ID to this user's metadata in our IDP manager - err = am.addAccountIDToIDPAppMeta(ctx, claims.UserId, userAccountID) + err = am.addAccountIDToIDPAppMeta(ctx, userAuth.UserId, userAccountID) if err != nil { return err } @@ -1008,20 +1006,20 @@ func (am *DefaultAccountManager) handleExistingUserAccount( // addNewPrivateAccount validates if there is an existing primary account for the domain, if so it adds the new user to that account, // otherwise it will create a new account and make it primary account for the domain. -func (am *DefaultAccountManager) addNewPrivateAccount(ctx context.Context, domainAccountID string, claims jwtclaims.AuthorizationClaims) (string, error) { - if claims.UserId == "" { +func (am *DefaultAccountManager) addNewPrivateAccount(ctx context.Context, domainAccountID string, userAuth nbcontext.UserAuth) (string, error) { + if userAuth.UserId == "" { return "", fmt.Errorf("user ID is empty") } - lowerDomain := strings.ToLower(claims.Domain) + lowerDomain := strings.ToLower(userAuth.Domain) - newAccount, err := am.newAccount(ctx, claims.UserId, lowerDomain) + newAccount, err := am.newAccount(ctx, userAuth.UserId, lowerDomain) if err != nil { return "", err } newAccount.Domain = lowerDomain - newAccount.DomainCategory = claims.DomainCategory + newAccount.DomainCategory = userAuth.DomainCategory newAccount.IsDomainPrimaryAccount = true err = am.Store.SaveAccount(ctx, newAccount) @@ -1029,33 +1027,33 @@ func (am *DefaultAccountManager) addNewPrivateAccount(ctx context.Context, domai return "", err } - err = am.addAccountIDToIDPAppMeta(ctx, claims.UserId, newAccount.Id) + err = am.addAccountIDToIDPAppMeta(ctx, userAuth.UserId, newAccount.Id) if err != nil { return "", err } - am.StoreEvent(ctx, claims.UserId, claims.UserId, newAccount.Id, activity.UserJoined, nil) + am.StoreEvent(ctx, userAuth.UserId, userAuth.UserId, newAccount.Id, activity.UserJoined, nil) return newAccount.Id, nil } -func (am *DefaultAccountManager) addNewUserToDomainAccount(ctx context.Context, domainAccountID string, claims jwtclaims.AuthorizationClaims) (string, error) { +func (am *DefaultAccountManager) addNewUserToDomainAccount(ctx context.Context, domainAccountID string, userAuth nbcontext.UserAuth) (string, error) { unlockAccount := am.Store.AcquireWriteLockByUID(ctx, domainAccountID) defer unlockAccount() - newUser := types.NewRegularUser(claims.UserId) + newUser := types.NewRegularUser(userAuth.UserId) newUser.AccountID = domainAccountID err := am.Store.SaveUser(ctx, store.LockingStrengthUpdate, newUser) if err != nil { return "", err } - err = am.addAccountIDToIDPAppMeta(ctx, claims.UserId, domainAccountID) + err = am.addAccountIDToIDPAppMeta(ctx, userAuth.UserId, domainAccountID) if err != nil { return "", err } - am.StoreEvent(ctx, claims.UserId, claims.UserId, domainAccountID, activity.UserJoined, nil) + am.StoreEvent(ctx, userAuth.UserId, userAuth.UserId, domainAccountID, activity.UserJoined, nil) return domainAccountID, nil } @@ -1068,7 +1066,7 @@ func (am *DefaultAccountManager) redeemInvite(ctx context.Context, accountID str return nil } - user, err := am.lookupUserInCache(ctx, am.Store, userID, accountID) + user, err := am.lookupUserInCache(ctx, userID, accountID) if err != nil { return err } @@ -1095,76 +1093,11 @@ func (am *DefaultAccountManager) redeemInvite(ctx context.Context, accountID str return nil } -// MarkPATUsed marks a personal access token as used -func (am *DefaultAccountManager) MarkPATUsed(ctx context.Context, tokenID string) error { - return am.Store.MarkPATUsed(ctx, store.LockingStrengthUpdate, tokenID) -} - // GetAccount returns an account associated with this account ID. func (am *DefaultAccountManager) GetAccount(ctx context.Context, accountID string) (*types.Account, error) { return am.Store.GetAccount(ctx, accountID) } -// GetAccountInfoFromPAT retrieves user, personal access token, domain, and category details from a personal access token. -func (am *DefaultAccountManager) GetAccountInfoFromPAT(ctx context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error) { - user, pat, err = am.extractPATFromToken(ctx, token) - if err != nil { - return nil, nil, "", "", err - } - - domain, category, err = am.Store.GetAccountDomainAndCategory(ctx, store.LockingStrengthShare, user.AccountID) - if err != nil { - return nil, nil, "", "", err - } - - return user, pat, domain, category, nil -} - -// extractPATFromToken validates the token structure and retrieves associated User and PAT. -func (am *DefaultAccountManager) extractPATFromToken(ctx context.Context, token string) (*types.User, *types.PersonalAccessToken, error) { - if len(token) != types.PATLength { - return nil, nil, fmt.Errorf("token has incorrect length") - } - - prefix := token[:len(types.PATPrefix)] - if prefix != types.PATPrefix { - return nil, nil, fmt.Errorf("token has wrong prefix") - } - secret := token[len(types.PATPrefix) : len(types.PATPrefix)+types.PATSecretLength] - encodedChecksum := token[len(types.PATPrefix)+types.PATSecretLength : len(types.PATPrefix)+types.PATSecretLength+types.PATChecksumLength] - - verificationChecksum, err := base62.Decode(encodedChecksum) - if err != nil { - return nil, nil, fmt.Errorf("token checksum decoding failed: %w", err) - } - - secretChecksum := crc32.ChecksumIEEE([]byte(secret)) - if secretChecksum != verificationChecksum { - return nil, nil, fmt.Errorf("token checksum does not match") - } - - hashedToken := sha256.Sum256([]byte(token)) - encodedHashedToken := b64.StdEncoding.EncodeToString(hashedToken[:]) - - var user *types.User - var pat *types.PersonalAccessToken - - err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { - pat, err = transaction.GetPATByHashedToken(ctx, store.LockingStrengthShare, encodedHashedToken) - if err != nil { - return err - } - - user, err = transaction.GetUserByPATID(ctx, store.LockingStrengthShare, pat.ID) - return err - }) - if err != nil { - return nil, nil, err - } - - return user, pat, nil -} - // GetAccountByID returns an account associated with this account ID. func (am *DefaultAccountManager) GetAccountByID(ctx context.Context, accountID string, userID string) (*types.Account, error) { user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID) @@ -1179,58 +1112,56 @@ func (am *DefaultAccountManager) GetAccountByID(ctx context.Context, accountID s return am.Store.GetAccount(ctx, accountID) } -// GetAccountIDFromToken returns an account ID associated with this token. -func (am *DefaultAccountManager) GetAccountIDFromToken(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) { - if claims.UserId == "" { +func (am *DefaultAccountManager) GetAccountIDFromUserAuth(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error) { + if userAuth.UserId == "" { return "", "", errors.New(emptyUserID) } if am.singleAccountMode && am.singleAccountModeDomain != "" { // This section is mostly related to self-hosted installations. // We override incoming domain claims to group users under a single account. - claims.Domain = am.singleAccountModeDomain - claims.DomainCategory = types.PrivateCategory + userAuth.Domain = am.singleAccountModeDomain + userAuth.DomainCategory = types.PrivateCategory log.WithContext(ctx).Debugf("overriding JWT Domain and DomainCategory claims since single account mode is enabled") } - accountID, err := am.getAccountIDWithAuthorizationClaims(ctx, claims) + accountID, err := am.getAccountIDWithAuthorizationClaims(ctx, userAuth) if err != nil { return "", "", err } - user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, claims.UserId) + user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userAuth.UserId) if err != nil { // this is not really possible because we got an account by user ID - return "", "", status.Errorf(status.NotFound, "user %s not found", claims.UserId) + return "", "", status.Errorf(status.NotFound, "user %s not found", userAuth.UserId) + } + + if userAuth.IsChild { + return accountID, user.Id, nil } if user.AccountID != accountID { - return "", "", status.Errorf(status.PermissionDenied, "user %s is not part of the account %s", claims.UserId, accountID) + return "", "", status.Errorf(status.PermissionDenied, "user %s is not part of the account %s", userAuth.UserId, accountID) } - if !user.IsServiceUser && claims.Invited { + if !user.IsServiceUser && userAuth.Invited { err = am.redeemInvite(ctx, accountID, user.Id) if err != nil { return "", "", err } } - if err = am.syncJWTGroups(ctx, accountID, claims); err != nil { - return "", "", err - } - return accountID, user.Id, nil } // syncJWTGroups processes the JWT groups for a user, updates the account based on the groups, // and propagates changes to peers if group propagation is enabled. -func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID string, claims jwtclaims.AuthorizationClaims) error { - if claim, exists := claims.Raw[jwtclaims.IsToken]; exists { - if isToken, ok := claim.(bool); ok && isToken { - return nil - } +// requires userAuth to have been ValidateAndParseToken and EnsureUserAccessByJWTGroups by the AuthManager +func (am *DefaultAccountManager) SyncUserJWTGroups(ctx context.Context, userAuth nbcontext.UserAuth) error { + if userAuth.IsChild || userAuth.IsPAT { + return nil } - settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, userAuth.AccountId) if err != nil { return err } @@ -1244,9 +1175,7 @@ func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID st return nil } - jwtGroupsNames := extractJWTGroups(ctx, settings.JWTGroupsClaimName, claims) - - unlockAccount := am.Store.AcquireWriteLockByUID(ctx, accountID) + unlockAccount := am.Store.AcquireWriteLockByUID(ctx, userAuth.AccountId) defer func() { if unlockAccount != nil { unlockAccount() @@ -1258,17 +1187,17 @@ func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID st var hasChanges bool var user *types.User err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { - user, err = transaction.GetUserByUserID(ctx, store.LockingStrengthShare, claims.UserId) + user, err = transaction.GetUserByUserID(ctx, store.LockingStrengthShare, userAuth.UserId) if err != nil { return fmt.Errorf("error getting user: %w", err) } - groups, err := transaction.GetAccountGroups(ctx, store.LockingStrengthShare, accountID) + groups, err := transaction.GetAccountGroups(ctx, store.LockingStrengthShare, userAuth.AccountId) if err != nil { return fmt.Errorf("error getting account groups: %w", err) } - changed, updatedAutoGroups, newGroupsToCreate, err := am.getJWTGroupsChanges(user, groups, jwtGroupsNames) + changed, updatedAutoGroups, newGroupsToCreate, err := am.getJWTGroupsChanges(user, groups, userAuth.Groups) if err != nil { return fmt.Errorf("error getting JWT groups changes: %w", err) } @@ -1293,7 +1222,7 @@ func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID st // Propagate changes to peers if group propagation is enabled if settings.GroupsPropagationEnabled { - groups, err = transaction.GetAccountGroups(ctx, store.LockingStrengthShare, accountID) + groups, err = transaction.GetAccountGroups(ctx, store.LockingStrengthShare, userAuth.AccountId) if err != nil { return fmt.Errorf("error getting account groups: %w", err) } @@ -1303,7 +1232,7 @@ func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID st groupsMap[group.ID] = group } - peers, err := transaction.GetUserPeers(ctx, store.LockingStrengthShare, accountID, claims.UserId) + peers, err := transaction.GetUserPeers(ctx, store.LockingStrengthShare, userAuth.AccountId, userAuth.UserId) if err != nil { return fmt.Errorf("error getting user peers: %w", err) } @@ -1317,7 +1246,7 @@ func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID st return fmt.Errorf("error saving groups: %w", err) } - if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, userAuth.AccountId); err != nil { return fmt.Errorf("error incrementing network serial: %w", err) } } @@ -1335,45 +1264,45 @@ func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID st } for _, g := range addNewGroups { - group, err := am.Store.GetGroupByID(ctx, store.LockingStrengthShare, accountID, g) + group, err := am.Store.GetGroupByID(ctx, store.LockingStrengthShare, userAuth.AccountId, g) if err != nil { - log.WithContext(ctx).Debugf("group %s not found while saving user activity event of account %s", g, accountID) + log.WithContext(ctx).Debugf("group %s not found while saving user activity event of account %s", g, userAuth.AccountId) } else { meta := map[string]any{ "group": group.Name, "group_id": group.ID, "is_service_user": user.IsServiceUser, "user_name": user.ServiceUserName, } - am.StoreEvent(ctx, user.Id, user.Id, accountID, activity.GroupAddedToUser, meta) + am.StoreEvent(ctx, user.Id, user.Id, userAuth.AccountId, activity.GroupAddedToUser, meta) } } for _, g := range removeOldGroups { - group, err := am.Store.GetGroupByID(ctx, store.LockingStrengthShare, accountID, g) + group, err := am.Store.GetGroupByID(ctx, store.LockingStrengthShare, userAuth.AccountId, g) if err != nil { - log.WithContext(ctx).Debugf("group %s not found while saving user activity event of account %s", g, accountID) + log.WithContext(ctx).Debugf("group %s not found while saving user activity event of account %s", g, userAuth.AccountId) } else { meta := map[string]any{ "group": group.Name, "group_id": group.ID, "is_service_user": user.IsServiceUser, "user_name": user.ServiceUserName, } - am.StoreEvent(ctx, user.Id, user.Id, accountID, activity.GroupRemovedFromUser, meta) + am.StoreEvent(ctx, user.Id, user.Id, userAuth.AccountId, activity.GroupRemovedFromUser, meta) } } if settings.GroupsPropagationEnabled { - removedGroupAffectsPeers, err := areGroupChangesAffectPeers(ctx, am.Store, accountID, removeOldGroups) + removedGroupAffectsPeers, err := areGroupChangesAffectPeers(ctx, am.Store, userAuth.AccountId, removeOldGroups) if err != nil { return err } - newGroupsAffectsPeers, err := areGroupChangesAffectPeers(ctx, am.Store, accountID, addNewGroups) + newGroupsAffectsPeers, err := areGroupChangesAffectPeers(ctx, am.Store, userAuth.AccountId, addNewGroups) if err != nil { return err } if removedGroupAffectsPeers || newGroupsAffectsPeers { - log.WithContext(ctx).Tracef("user %s: JWT group membership changed, updating account peers", claims.UserId) - am.UpdateAccountPeers(ctx, accountID) + log.WithContext(ctx).Tracef("user %s: JWT group membership changed, updating account peers", userAuth.UserId) + am.UpdateAccountPeers(ctx, userAuth.AccountId) } } @@ -1398,24 +1327,34 @@ func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID st // Existing user + Existing account + Existing Indexed Domain -> Nothing changes // // Existing user + Existing account + Existing domain reclassified Domain as private -> Nothing changes (index domain) -func (am *DefaultAccountManager) getAccountIDWithAuthorizationClaims(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, error) { +// +// UserAuth IsChild -> checks that account exists +func (am *DefaultAccountManager) getAccountIDWithAuthorizationClaims(ctx context.Context, userAuth nbcontext.UserAuth) (string, error) { log.WithContext(ctx).Tracef("getting account with authorization claims. User ID: \"%s\", Account ID: \"%s\", Domain: \"%s\", Domain Category: \"%s\"", - claims.UserId, claims.AccountId, claims.Domain, claims.DomainCategory) + userAuth.UserId, userAuth.AccountId, userAuth.Domain, userAuth.DomainCategory) - if claims.UserId == "" { + if userAuth.UserId == "" { return "", errors.New(emptyUserID) } - if claims.DomainCategory != types.PrivateCategory || !isDomainValid(claims.Domain) { - return am.GetAccountIDByUserID(ctx, claims.UserId, claims.Domain) + if userAuth.IsChild { + exists, err := am.Store.AccountExists(ctx, store.LockingStrengthShare, userAuth.AccountId) + if err != nil || !exists { + return "", err + } + return userAuth.AccountId, nil } - if claims.AccountId != "" { - return am.handlePrivateAccountWithIDFromClaim(ctx, claims) + if userAuth.DomainCategory != types.PrivateCategory || !isDomainValid(userAuth.Domain) { + return am.GetAccountIDByUserID(ctx, userAuth.UserId, userAuth.Domain) + } + + if userAuth.AccountId != "" { + return am.handlePrivateAccountWithIDFromClaim(ctx, userAuth) } // We checked if the domain has a primary account already - domainAccountID, cancel, err := am.getPrivateDomainWithGlobalLock(ctx, claims.Domain) + domainAccountID, cancel, err := am.getPrivateDomainWithGlobalLock(ctx, userAuth.Domain) if cancel != nil { defer cancel() } @@ -1423,14 +1362,14 @@ func (am *DefaultAccountManager) getAccountIDWithAuthorizationClaims(ctx context return "", err } - userAccountID, err := am.Store.GetAccountIDByUserID(ctx, store.LockingStrengthShare, claims.UserId) + userAccountID, err := am.Store.GetAccountIDByUserID(ctx, store.LockingStrengthShare, userAuth.UserId) if handleNotFound(err) != nil { log.WithContext(ctx).Errorf("error getting account ID by user ID: %v", err) return "", err } if userAccountID != "" { - if err = am.handleExistingUserAccount(ctx, userAccountID, domainAccountID, claims); err != nil { + if err = am.handleExistingUserAccount(ctx, userAccountID, domainAccountID, userAuth); err != nil { return "", err } @@ -1438,10 +1377,10 @@ func (am *DefaultAccountManager) getAccountIDWithAuthorizationClaims(ctx context } if domainAccountID != "" { - return am.addNewUserToDomainAccount(ctx, domainAccountID, claims) + return am.addNewUserToDomainAccount(ctx, domainAccountID, userAuth) } - return am.addNewPrivateAccount(ctx, domainAccountID, claims) + return am.addNewPrivateAccount(ctx, domainAccountID, userAuth) } func (am *DefaultAccountManager) getPrivateDomainWithGlobalLock(ctx context.Context, domain string) (string, context.CancelFunc, error) { domainAccountID, err := am.Store.GetAccountIDByPrivateDomain(ctx, store.LockingStrengthShare, domain) @@ -1469,40 +1408,40 @@ func (am *DefaultAccountManager) getPrivateDomainWithGlobalLock(ctx context.Cont return domainAccountID, cancel, nil } -func (am *DefaultAccountManager) handlePrivateAccountWithIDFromClaim(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, error) { - userAccountID, err := am.Store.GetAccountIDByUserID(ctx, store.LockingStrengthShare, claims.UserId) +func (am *DefaultAccountManager) handlePrivateAccountWithIDFromClaim(ctx context.Context, userAuth nbcontext.UserAuth) (string, error) { + userAccountID, err := am.Store.GetAccountIDByUserID(ctx, store.LockingStrengthShare, userAuth.UserId) if err != nil { log.WithContext(ctx).Errorf("error getting account ID by user ID: %v", err) return "", err } - if userAccountID != claims.AccountId { - return "", fmt.Errorf("user %s is not part of the account id %s", claims.UserId, claims.AccountId) + if userAccountID != userAuth.AccountId { + return "", fmt.Errorf("user %s is not part of the account id %s", userAuth.UserId, userAuth.AccountId) } - accountDomain, domainCategory, err := am.Store.GetAccountDomainAndCategory(ctx, store.LockingStrengthShare, claims.AccountId) + accountDomain, domainCategory, err := am.Store.GetAccountDomainAndCategory(ctx, store.LockingStrengthShare, userAuth.AccountId) if handleNotFound(err) != nil { log.WithContext(ctx).Errorf("error getting account domain and category: %v", err) return "", err } - if domainIsUpToDate(accountDomain, domainCategory, claims) { - return claims.AccountId, nil + if domainIsUpToDate(accountDomain, domainCategory, userAuth) { + return userAuth.AccountId, nil } // We checked if the domain has a primary account already - domainAccountID, err := am.Store.GetAccountIDByPrivateDomain(ctx, store.LockingStrengthShare, claims.Domain) + domainAccountID, err := am.Store.GetAccountIDByPrivateDomain(ctx, store.LockingStrengthShare, userAuth.Domain) if handleNotFound(err) != nil { log.WithContext(ctx).Errorf(errorGettingDomainAccIDFmt, err) return "", err } - err = am.handleExistingUserAccount(ctx, claims.AccountId, domainAccountID, claims) + err = am.handleExistingUserAccount(ctx, userAuth.AccountId, domainAccountID, userAuth) if err != nil { return "", err } - return claims.AccountId, nil + return userAuth.AccountId, nil } func handleNotFound(err error) error { @@ -1517,11 +1456,16 @@ func handleNotFound(err error) error { return nil } -func domainIsUpToDate(domain string, domainCategory string, claims jwtclaims.AuthorizationClaims) bool { - return domainCategory == types.PrivateCategory || claims.DomainCategory != types.PrivateCategory || domain != claims.Domain +func domainIsUpToDate(domain string, domainCategory string, userAuth nbcontext.UserAuth) bool { + return domainCategory == types.PrivateCategory || userAuth.DomainCategory != types.PrivateCategory || domain != userAuth.Domain } func (am *DefaultAccountManager) SyncAndMarkPeer(ctx context.Context, accountID string, peerPubKey string, meta nbpeer.PeerSystemMeta, realIP net.IP) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) { + start := time.Now() + defer func() { + log.WithContext(ctx).Debugf("SyncAndMarkPeer: took %v", time.Since(start)) + }() + accountUnlock := am.Store.AcquireReadLockByUID(ctx, accountID) defer accountUnlock() peerUnlock := am.Store.AcquireWriteLockByUID(ctx, peerPubKey) @@ -1595,34 +1539,6 @@ func (am *DefaultAccountManager) GetDNSDomain() string { return am.dnsDomain } -// CheckUserAccessByJWTGroups checks if the user has access, particularly in cases where the admin enabled JWT -// group propagation and set the list of groups with access permissions. -func (am *DefaultAccountManager) CheckUserAccessByJWTGroups(ctx context.Context, claims jwtclaims.AuthorizationClaims) error { - accountID, _, err := am.GetAccountIDFromToken(ctx, claims) - if err != nil { - return err - } - - settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) - if err != nil { - return err - } - - // Ensures JWT group synchronization to the management is enabled before, - // filtering access based on the allowed groups. - if settings != nil && settings.JWTGroupsEnabled { - if allowedGroups := settings.JWTAllowGroups; len(allowedGroups) > 0 { - userJWTGroups := extractJWTGroups(ctx, settings.JWTGroupsClaimName, claims) - - if !userHasAllowedGroup(allowedGroups, userJWTGroups) { - return fmt.Errorf("user does not belong to any of the allowed JWT groups") - } - } - } - - return nil -} - func (am *DefaultAccountManager) onPeersInvalidated(ctx context.Context, accountID string) { log.WithContext(ctx).Debugf("validated peers has been invalidated for account %s", accountID) am.UpdateAccountPeers(ctx, accountID) @@ -1690,46 +1606,6 @@ func (am *DefaultAccountManager) GetAccountSettings(ctx context.Context, account return am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) } -// addAllGroup to account object if it doesn't exist -func addAllGroup(account *types.Account) error { - if len(account.Groups) == 0 { - allGroup := &types.Group{ - ID: xid.New().String(), - Name: "All", - Issued: types.GroupIssuedAPI, - } - for _, peer := range account.Peers { - allGroup.Peers = append(allGroup.Peers, peer.ID) - } - account.Groups = map[string]*types.Group{allGroup.ID: allGroup} - - id := xid.New().String() - - defaultPolicy := &types.Policy{ - ID: id, - Name: types.DefaultRuleName, - Description: types.DefaultRuleDescription, - Enabled: true, - Rules: []*types.PolicyRule{ - { - ID: id, - Name: types.DefaultRuleName, - Description: types.DefaultRuleDescription, - Enabled: true, - Sources: []string{allGroup.ID}, - Destinations: []string{allGroup.ID}, - Bidirectional: true, - Protocol: types.PolicyRuleProtocolALL, - Action: types.PolicyTrafficActionAccept, - }, - }, - } - - account.Policies = []*types.Policy{defaultPolicy} - } - return nil -} - // newAccountWithId creates a new Account with a default SetupKey (doesn't store in a Store) and provided id func newAccountWithId(ctx context.Context, accountID, userID, domain string) *types.Account { log.WithContext(ctx).Debugf("creating new account") @@ -1774,45 +1650,12 @@ func newAccountWithId(ctx context.Context, accountID, userID, domain string) *ty }, } - if err := addAllGroup(acc); err != nil { + if err := acc.AddAllGroup(); err != nil { log.WithContext(ctx).Errorf("error adding all group to account %s: %v", acc.Id, err) } return acc } -// extractJWTGroups extracts the group names from a JWT token's claims. -func extractJWTGroups(ctx context.Context, claimName string, claims jwtclaims.AuthorizationClaims) []string { - userJWTGroups := make([]string, 0) - - if claim, ok := claims.Raw[claimName]; ok { - if claimGroups, ok := claim.([]interface{}); ok { - for _, g := range claimGroups { - if group, ok := g.(string); ok { - userJWTGroups = append(userJWTGroups, group) - } else { - log.WithContext(ctx).Debugf("JWT claim %q contains a non-string group (type: %T): %v", claimName, g, g) - } - } - } - } else { - log.WithContext(ctx).Debugf("JWT claim %q is not a string array", claimName) - } - - return userJWTGroups -} - -// userHasAllowedGroup checks if a user belongs to any of the allowed groups. -func userHasAllowedGroup(allowedGroups []string, userGroups []string) bool { - for _, userGroup := range userGroups { - for _, allowedGroup := range allowedGroups { - if userGroup == allowedGroup { - return true - } - } - } - return false -} - // separateGroups separates user's auto groups into non-JWT and JWT groups. // Returns the list of standard auto groups and a map of JWT auto groups, // where the keys are the group names and the values are the group IDs. diff --git a/management/server/account_test.go b/management/server/account_test.go index 08bdc8821..f203e2066 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -2,8 +2,6 @@ package server import ( "context" - "crypto/sha256" - b64 "encoding/base64" "encoding/json" "fmt" "io" @@ -15,8 +13,6 @@ import ( "testing" "time" - "github.com/golang-jwt/jwt" - "github.com/netbirdio/netbird/management/server/util" resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" @@ -30,7 +26,7 @@ import ( nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/server/activity" - "github.com/netbirdio/netbird/management/server/jwtclaims" + nbcontext "github.com/netbirdio/netbird/management/server/context" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/store" @@ -437,7 +433,7 @@ func TestAccountManager_GetOrCreateAccountByUser(t *testing.T) { } func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) { - type initUserParams jwtclaims.AuthorizationClaims + type initUserParams nbcontext.UserAuth var ( publicDomain = "public.com" @@ -460,7 +456,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) { testCases := []struct { name string - inputClaims jwtclaims.AuthorizationClaims + inputClaims nbcontext.UserAuth inputInitUserParams initUserParams inputUpdateAttrs bool inputUpdateClaimAccount bool @@ -475,7 +471,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) { }{ { name: "New User With Public Domain", - inputClaims: jwtclaims.AuthorizationClaims{ + inputClaims: nbcontext.UserAuth{ Domain: publicDomain, UserId: "pub-domain-user", DomainCategory: types.PublicCategory, @@ -492,7 +488,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) { }, { name: "New User With Unknown Domain", - inputClaims: jwtclaims.AuthorizationClaims{ + inputClaims: nbcontext.UserAuth{ Domain: unknownDomain, UserId: "unknown-domain-user", DomainCategory: types.UnknownCategory, @@ -509,7 +505,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) { }, { name: "New User With Private Domain", - inputClaims: jwtclaims.AuthorizationClaims{ + inputClaims: nbcontext.UserAuth{ Domain: privateDomain, UserId: "pvt-domain-user", DomainCategory: types.PrivateCategory, @@ -526,7 +522,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) { }, { name: "New Regular User With Existing Private Domain", - inputClaims: jwtclaims.AuthorizationClaims{ + inputClaims: nbcontext.UserAuth{ Domain: privateDomain, UserId: "new-pvt-domain-user", DomainCategory: types.PrivateCategory, @@ -544,7 +540,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) { }, { name: "Existing User With Existing Reclassified Private Domain", - inputClaims: jwtclaims.AuthorizationClaims{ + inputClaims: nbcontext.UserAuth{ Domain: defaultInitAccount.Domain, UserId: defaultInitAccount.UserId, DomainCategory: types.PrivateCategory, @@ -561,7 +557,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) { }, { name: "Existing Account Id With Existing Reclassified Private Domain", - inputClaims: jwtclaims.AuthorizationClaims{ + inputClaims: nbcontext.UserAuth{ Domain: defaultInitAccount.Domain, UserId: defaultInitAccount.UserId, DomainCategory: types.PrivateCategory, @@ -579,7 +575,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) { }, { name: "User With Private Category And Empty Domain", - inputClaims: jwtclaims.AuthorizationClaims{ + inputClaims: nbcontext.UserAuth{ Domain: "", UserId: "pvt-domain-user", DomainCategory: types.PrivateCategory, @@ -608,7 +604,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) { require.NoError(t, err, "get init account failed") if testCase.inputUpdateAttrs { - err = manager.updateAccountDomainAttributesIfNotUpToDate(context.Background(), initAccount.Id, jwtclaims.AuthorizationClaims{UserId: testCase.inputInitUserParams.UserId, Domain: testCase.inputInitUserParams.Domain, DomainCategory: testCase.inputInitUserParams.DomainCategory}, true) + err = manager.updateAccountDomainAttributesIfNotUpToDate(context.Background(), initAccount.Id, nbcontext.UserAuth{UserId: testCase.inputInitUserParams.UserId, Domain: testCase.inputInitUserParams.Domain, DomainCategory: testCase.inputInitUserParams.DomainCategory}, true) require.NoError(t, err, "update init user failed") } @@ -616,7 +612,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) { testCase.inputClaims.AccountId = initAccount.Id } - accountID, _, err = manager.GetAccountIDFromToken(context.Background(), testCase.inputClaims) + accountID, _, err = manager.GetAccountIDFromUserAuth(context.Background(), testCase.inputClaims) require.NoError(t, err, "support function failed") account, err := manager.Store.GetAccount(context.Background(), accountID) @@ -635,14 +631,12 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) { } } -func TestDefaultAccountManager_GetGroupsFromTheToken(t *testing.T) { +func TestDefaultAccountManager_SyncUserJWTGroups(t *testing.T) { userId := "user-id" domain := "test.domain" - _ = newAccountWithId(context.Background(), "", userId, domain) manager, err := createManager(t) require.NoError(t, err, "unable to create account manager") - accountID, err := manager.GetAccountIDByUserID(context.Background(), userId, domain) require.NoError(t, err, "create init user failed") // as initAccount was created without account id we have to take the id after account initialization @@ -650,65 +644,50 @@ func TestDefaultAccountManager_GetGroupsFromTheToken(t *testing.T) { // it is important to set the id as it help to avoid creating additional account with empty Id and re-pointing indices to it initAccount, err := manager.Store.GetAccount(context.Background(), accountID) require.NoError(t, err, "get init account failed") - - claims := jwtclaims.AuthorizationClaims{ + claims := nbcontext.UserAuth{ AccountId: accountID, // is empty as it is based on accountID right after initialization of initAccount Domain: domain, UserId: userId, DomainCategory: "test-category", - Raw: jwt.MapClaims{"idp-groups": []interface{}{"group1", "group2"}}, + Groups: []string{"group1", "group2"}, } - t.Run("JWT groups disabled", func(t *testing.T) { - accountID, _, err := manager.GetAccountIDFromToken(context.Background(), claims) - require.NoError(t, err, "get account by token failed") - + err := manager.SyncUserJWTGroups(context.Background(), claims) + require.NoError(t, err, "synt user jwt groups failed") account, err := manager.Store.GetAccount(context.Background(), accountID) require.NoError(t, err, "get account failed") - require.Len(t, account.Groups, 1, "only ALL group should exists") }) - t.Run("JWT groups enabled without claim name", func(t *testing.T) { initAccount.Settings.JWTGroupsEnabled = true err := manager.Store.SaveAccount(context.Background(), initAccount) require.NoError(t, err, "save account failed") require.Len(t, manager.Store.GetAllAccounts(context.Background()), 1, "only one account should exist") - - accountID, _, err := manager.GetAccountIDFromToken(context.Background(), claims) - require.NoError(t, err, "get account by token failed") - + err = manager.SyncUserJWTGroups(context.Background(), claims) + require.NoError(t, err, "synt user jwt groups failed") account, err := manager.Store.GetAccount(context.Background(), accountID) require.NoError(t, err, "get account failed") - require.Len(t, account.Groups, 1, "if group claim is not set no group added from JWT") }) - t.Run("JWT groups enabled", func(t *testing.T) { initAccount.Settings.JWTGroupsEnabled = true initAccount.Settings.JWTGroupsClaimName = "idp-groups" err := manager.Store.SaveAccount(context.Background(), initAccount) require.NoError(t, err, "save account failed") require.Len(t, manager.Store.GetAllAccounts(context.Background()), 1, "only one account should exist") - - accountID, _, err := manager.GetAccountIDFromToken(context.Background(), claims) - require.NoError(t, err, "get account by token failed") - + err = manager.SyncUserJWTGroups(context.Background(), claims) + require.NoError(t, err, "synt user jwt groups failed") account, err := manager.Store.GetAccount(context.Background(), accountID) require.NoError(t, err, "get account failed") - require.Len(t, account.Groups, 3, "groups should be added to the account") - groupsByNames := map[string]*types.Group{} for _, g := range account.Groups { groupsByNames[g.Name] = g } - g1, ok := groupsByNames["group1"] require.True(t, ok, "group1 should be added to the account") require.Equal(t, g1.Name, "group1", "group1 name should match") require.Equal(t, g1.Issued, types.GroupIssuedJWT, "group1 issued should match") - g2, ok := groupsByNames["group2"] require.True(t, ok, "group2 should be added to the account") require.Equal(t, g2.Name, "group2", "group2 name should match") @@ -716,88 +695,6 @@ func TestDefaultAccountManager_GetGroupsFromTheToken(t *testing.T) { }) } -func TestAccountManager_GetAccountFromPAT(t *testing.T) { - store, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir()) - if err != nil { - t.Fatalf("Error when creating store: %s", err) - } - t.Cleanup(cleanup) - account := newAccountWithId(context.Background(), "account_id", "testuser", "") - - token := "nbp_9999EUDNdkeusjentDLSJEn1902u84390W6W" - hashedToken := sha256.Sum256([]byte(token)) - encodedHashedToken := b64.StdEncoding.EncodeToString(hashedToken[:]) - account.Users["someUser"] = &types.User{ - Id: "someUser", - PATs: map[string]*types.PersonalAccessToken{ - "tokenId": { - ID: "tokenId", - UserID: "someUser", - HashedToken: encodedHashedToken, - }, - }, - } - err = store.SaveAccount(context.Background(), account) - if err != nil { - t.Fatalf("Error when saving account: %s", err) - } - - am := DefaultAccountManager{ - Store: store, - } - - user, pat, _, _, err := am.GetAccountInfoFromPAT(context.Background(), token) - if err != nil { - t.Fatalf("Error when getting Account from PAT: %s", err) - } - - assert.Equal(t, "account_id", user.AccountID) - assert.Equal(t, "someUser", user.Id) - assert.Equal(t, account.Users["someUser"].PATs["tokenId"].ID, pat.ID) -} - -func TestDefaultAccountManager_MarkPATUsed(t *testing.T) { - store, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir()) - if err != nil { - t.Fatalf("Error when creating store: %s", err) - } - t.Cleanup(cleanup) - - account := newAccountWithId(context.Background(), "account_id", "testuser", "") - - token := "nbp_9999EUDNdkeusjentDLSJEn1902u84390W6W" - hashedToken := sha256.Sum256([]byte(token)) - encodedHashedToken := b64.StdEncoding.EncodeToString(hashedToken[:]) - account.Users["someUser"] = &types.User{ - Id: "someUser", - PATs: map[string]*types.PersonalAccessToken{ - "tokenId": { - ID: "tokenId", - HashedToken: encodedHashedToken, - }, - }, - } - err = store.SaveAccount(context.Background(), account) - if err != nil { - t.Fatalf("Error when saving account: %s", err) - } - - am := DefaultAccountManager{ - Store: store, - } - - err = am.MarkPATUsed(context.Background(), "tokenId") - if err != nil { - t.Fatalf("Error when marking PAT used: %s", err) - } - - account, err = am.Store.GetAccount(context.Background(), "account_id") - if err != nil { - t.Fatalf("Error when getting account: %s", err) - } - assert.True(t, !account.Users["someUser"].PATs["tokenId"].GetLastUsed().IsZero()) -} - func TestAccountManager_PrivateAccount(t *testing.T) { manager, err := createManager(t) if err != nil { @@ -962,13 +859,13 @@ func TestAccountManager_DeleteAccount(t *testing.T) { } func BenchmarkTest_GetAccountWithclaims(b *testing.B) { - claims := jwtclaims.AuthorizationClaims{ + claims := nbcontext.UserAuth{ Domain: "example.com", UserId: "pvt-domain-user", DomainCategory: types.PrivateCategory, } - publicClaims := jwtclaims.AuthorizationClaims{ + publicClaims := nbcontext.UserAuth{ Domain: "test.com", UserId: "public-domain-user", DomainCategory: types.PublicCategory, @@ -1080,7 +977,7 @@ func TestAccountManager_AddPeer(t *testing.T) { serial := account.Network.CurrentSerial() // should be 0 - setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userID, false) + setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userID, false, false) if err != nil { t.Fatal("error creating setup key") return @@ -1456,7 +1353,7 @@ func TestAccountManager_DeletePeer(t *testing.T) { t.Fatal(err) } - setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userID, false) + setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userID, false, false) if err != nil { t.Fatal("error creating setup key") return @@ -2683,11 +2580,13 @@ func TestAccount_SetJWTGroups(t *testing.T) { assert.NoError(t, manager.Store.SaveAccount(context.Background(), account), "unable to save account") t.Run("skip sync for token auth type", func(t *testing.T) { - claims := jwtclaims.AuthorizationClaims{ - UserId: "user1", - Raw: jwt.MapClaims{"groups": []interface{}{"group3"}, "is_token": true}, + claims := nbcontext.UserAuth{ + UserId: "user1", + AccountId: "accountID", + Groups: []string{"group3"}, + IsPAT: true, } - err = manager.syncJWTGroups(context.Background(), "accountID", claims) + err = manager.SyncUserJWTGroups(context.Background(), claims) assert.NoError(t, err, "unable to sync jwt groups") user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1") @@ -2696,11 +2595,12 @@ func TestAccount_SetJWTGroups(t *testing.T) { }) t.Run("empty jwt groups", func(t *testing.T) { - claims := jwtclaims.AuthorizationClaims{ - UserId: "user1", - Raw: jwt.MapClaims{"groups": []interface{}{}}, + claims := nbcontext.UserAuth{ + UserId: "user1", + AccountId: "accountID", + Groups: []string{}, } - err := manager.syncJWTGroups(context.Background(), "accountID", claims) + err := manager.SyncUserJWTGroups(context.Background(), claims) assert.NoError(t, err, "unable to sync jwt groups") user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1") @@ -2709,11 +2609,12 @@ func TestAccount_SetJWTGroups(t *testing.T) { }) t.Run("jwt match existing api group", func(t *testing.T) { - claims := jwtclaims.AuthorizationClaims{ - UserId: "user1", - Raw: jwt.MapClaims{"groups": []interface{}{"group1"}}, + claims := nbcontext.UserAuth{ + UserId: "user1", + AccountId: "accountID", + Groups: []string{"group1"}, } - err := manager.syncJWTGroups(context.Background(), "accountID", claims) + err := manager.SyncUserJWTGroups(context.Background(), claims) assert.NoError(t, err, "unable to sync jwt groups") user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1") @@ -2729,11 +2630,12 @@ func TestAccount_SetJWTGroups(t *testing.T) { account.Users["user1"].AutoGroups = []string{"group1"} assert.NoError(t, manager.Store.SaveUser(context.Background(), store.LockingStrengthUpdate, account.Users["user1"])) - claims := jwtclaims.AuthorizationClaims{ - UserId: "user1", - Raw: jwt.MapClaims{"groups": []interface{}{"group1"}}, + claims := nbcontext.UserAuth{ + UserId: "user1", + AccountId: "accountID", + Groups: []string{"group1"}, } - err = manager.syncJWTGroups(context.Background(), "accountID", claims) + err = manager.SyncUserJWTGroups(context.Background(), claims) assert.NoError(t, err, "unable to sync jwt groups") user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1") @@ -2746,11 +2648,12 @@ func TestAccount_SetJWTGroups(t *testing.T) { }) t.Run("add jwt group", func(t *testing.T) { - claims := jwtclaims.AuthorizationClaims{ - UserId: "user1", - Raw: jwt.MapClaims{"groups": []interface{}{"group1", "group2"}}, + claims := nbcontext.UserAuth{ + UserId: "user1", + AccountId: "accountID", + Groups: []string{"group1", "group2"}, } - err = manager.syncJWTGroups(context.Background(), "accountID", claims) + err = manager.SyncUserJWTGroups(context.Background(), claims) assert.NoError(t, err, "unable to sync jwt groups") user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1") @@ -2759,11 +2662,12 @@ func TestAccount_SetJWTGroups(t *testing.T) { }) t.Run("existed group not update", func(t *testing.T) { - claims := jwtclaims.AuthorizationClaims{ - UserId: "user1", - Raw: jwt.MapClaims{"groups": []interface{}{"group2"}}, + claims := nbcontext.UserAuth{ + UserId: "user1", + AccountId: "accountID", + Groups: []string{"group2"}, } - err = manager.syncJWTGroups(context.Background(), "accountID", claims) + err = manager.SyncUserJWTGroups(context.Background(), claims) assert.NoError(t, err, "unable to sync jwt groups") user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1") @@ -2772,11 +2676,12 @@ func TestAccount_SetJWTGroups(t *testing.T) { }) t.Run("add new group", func(t *testing.T) { - claims := jwtclaims.AuthorizationClaims{ - UserId: "user2", - Raw: jwt.MapClaims{"groups": []interface{}{"group1", "group3"}}, + claims := nbcontext.UserAuth{ + UserId: "user2", + AccountId: "accountID", + Groups: []string{"group1", "group3"}, } - err = manager.syncJWTGroups(context.Background(), "accountID", claims) + err = manager.SyncUserJWTGroups(context.Background(), claims) assert.NoError(t, err, "unable to sync jwt groups") groups, err := manager.Store.GetAccountGroups(context.Background(), store.LockingStrengthShare, "accountID") @@ -2789,11 +2694,12 @@ func TestAccount_SetJWTGroups(t *testing.T) { }) t.Run("remove all JWT groups when list is empty", func(t *testing.T) { - claims := jwtclaims.AuthorizationClaims{ - UserId: "user1", - Raw: jwt.MapClaims{"groups": []interface{}{}}, + claims := nbcontext.UserAuth{ + UserId: "user1", + AccountId: "accountID", + Groups: []string{}, } - err = manager.syncJWTGroups(context.Background(), "accountID", claims) + err = manager.SyncUserJWTGroups(context.Background(), claims) assert.NoError(t, err, "unable to sync jwt groups") user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1") @@ -2803,11 +2709,12 @@ func TestAccount_SetJWTGroups(t *testing.T) { }) t.Run("remove all JWT groups when claim does not exist", func(t *testing.T) { - claims := jwtclaims.AuthorizationClaims{ - UserId: "user2", - Raw: jwt.MapClaims{}, + claims := nbcontext.UserAuth{ + UserId: "user2", + AccountId: "accountID", + Groups: []string{}, } - err = manager.syncJWTGroups(context.Background(), "accountID", claims) + err = manager.SyncUserJWTGroups(context.Background(), claims) assert.NoError(t, err, "unable to sync jwt groups") user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user2") @@ -2948,7 +2855,7 @@ func setupNetworkMapTest(t *testing.T) (*DefaultAccountManager, *types.Account, t.Fatal(err) } - setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userID, false) + setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userID, false, false) if err != nil { t.Fatal("error creating setup key") } @@ -3006,6 +2913,8 @@ func peerShouldReceiveUpdate(t *testing.T, updateMessage <-chan *UpdateMessage) } func BenchmarkSyncAndMarkPeer(b *testing.B) { + b.Setenv("NB_GET_ACCOUNT_BUFFER_INTERVAL", "0") + benchCases := []struct { name string peers int @@ -3016,11 +2925,11 @@ func BenchmarkSyncAndMarkPeer(b *testing.B) { minMsPerOpCICD float64 maxMsPerOpCICD float64 }{ - {"Small", 50, 5, 1, 3, 3, 19}, - {"Medium", 500, 100, 7, 13, 10, 90}, - {"Large", 5000, 200, 65, 80, 60, 240}, - {"Small single", 50, 10, 1, 3, 3, 80}, - {"Medium single", 500, 10, 7, 13, 10, 37}, + {"Small", 50, 5, 1, 5, 3, 24}, + {"Medium", 500, 100, 7, 22, 10, 135}, + {"Large", 5000, 200, 65, 110, 60, 320}, + {"Small single", 50, 10, 1, 4, 3, 80}, + {"Medium single", 500, 10, 7, 13, 10, 43}, {"Large 5", 5000, 15, 65, 80, 60, 220}, } @@ -3073,6 +2982,7 @@ func BenchmarkSyncAndMarkPeer(b *testing.B) { } func BenchmarkLoginPeer_ExistingPeer(b *testing.B) { + b.Setenv("NB_GET_ACCOUNT_BUFFER_INTERVAL", "0") benchCases := []struct { name string peers int @@ -3083,12 +2993,12 @@ func BenchmarkLoginPeer_ExistingPeer(b *testing.B) { minMsPerOpCICD float64 maxMsPerOpCICD float64 }{ - {"Small", 50, 5, 102, 110, 3, 20}, - {"Medium", 500, 100, 105, 140, 20, 110}, - {"Large", 5000, 200, 160, 200, 120, 260}, - {"Small single", 50, 10, 102, 110, 5, 40}, - {"Medium single", 500, 10, 105, 140, 10, 60}, - {"Large 5", 5000, 15, 160, 200, 60, 180}, + {"Small", 50, 5, 2, 10, 3, 35}, + {"Medium", 500, 100, 5, 40, 20, 140}, + {"Large", 5000, 200, 60, 100, 120, 320}, + {"Small single", 50, 10, 2, 10, 5, 40}, + {"Medium single", 500, 10, 5, 40, 10, 60}, + {"Large 5", 5000, 15, 60, 100, 60, 180}, } log.SetOutput(io.Discard) @@ -3147,6 +3057,7 @@ func BenchmarkLoginPeer_ExistingPeer(b *testing.B) { } func BenchmarkLoginPeer_NewPeer(b *testing.B) { + b.Setenv("NB_GET_ACCOUNT_BUFFER_INTERVAL", "0") benchCases := []struct { name string peers int @@ -3157,12 +3068,12 @@ func BenchmarkLoginPeer_NewPeer(b *testing.B) { minMsPerOpCICD float64 maxMsPerOpCICD float64 }{ - {"Small", 50, 5, 107, 120, 10, 80}, - {"Medium", 500, 100, 105, 140, 30, 140}, - {"Large", 5000, 200, 180, 220, 140, 300}, - {"Small single", 50, 10, 107, 120, 10, 80}, - {"Medium single", 500, 10, 105, 140, 20, 60}, - {"Large 5", 5000, 15, 180, 220, 80, 200}, + {"Small", 50, 5, 7, 20, 10, 80}, + {"Medium", 500, 100, 5, 40, 30, 140}, + {"Large", 5000, 200, 80, 120, 140, 390}, + {"Small single", 50, 10, 7, 20, 10, 80}, + {"Medium single", 500, 10, 5, 40, 20, 85}, + {"Large 5", 5000, 15, 80, 120, 80, 200}, } log.SetOutput(io.Discard) diff --git a/management/server/activity/sqlite/sqlite.go b/management/server/activity/sqlite/sqlite.go index 823e0b4ac..ffb863de9 100644 --- a/management/server/activity/sqlite/sqlite.go +++ b/management/server/activity/sqlite/sqlite.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "path/filepath" + "runtime" "time" _ "github.com/mattn/go-sqlite3" @@ -95,6 +96,7 @@ func NewSQLiteStore(ctx context.Context, dataDir string, encryptionKey string) ( if err != nil { return nil, err } + db.SetMaxOpenConns(runtime.NumCPU()) crypt, err := NewFieldEncrypt(encryptionKey) if err != nil { diff --git a/management/server/jwtclaims/extractor.go b/management/server/auth/jwt/extractor.go similarity index 51% rename from management/server/jwtclaims/extractor.go rename to management/server/auth/jwt/extractor.go index 18214b434..fab429125 100644 --- a/management/server/jwtclaims/extractor.go +++ b/management/server/auth/jwt/extractor.go @@ -1,15 +1,17 @@ -package jwtclaims +package jwt import ( - "net/http" + "errors" + "net/url" "time" "github.com/golang-jwt/jwt" + log "github.com/sirupsen/logrus" + + nbcontext "github.com/netbirdio/netbird/management/server/context" ) const ( - // TokenUserProperty key for the user property in the request context - TokenUserProperty = "user" // AccountIDSuffix suffix for the account id claim AccountIDSuffix = "wt_account_id" // DomainIDSuffix suffix for the domain id claim @@ -22,19 +24,16 @@ const ( LastLoginSuffix = "nb_last_login" // Invited claim indicates that an incoming JWT is from a user that just accepted an invitation Invited = "nb_invited" - // IsToken claim indicates that auth type from the user is a token - IsToken = "is_token" ) -// ExtractClaims Extract function type -type ExtractClaims func(r *http.Request) AuthorizationClaims +var ( + errUserIDClaimEmpty = errors.New("user ID claim token value is empty") +) // ClaimsExtractor struct that holds the extract function type ClaimsExtractor struct { authAudience string userIDClaim string - - FromRequestContext ExtractClaims } // ClaimsExtractorOption is a function that configures the ClaimsExtractor @@ -54,13 +53,6 @@ func WithUserIDClaim(userIDClaim string) ClaimsExtractorOption { } } -// WithFromRequestContext sets the function that extracts claims from the request context -func WithFromRequestContext(ec ExtractClaims) ClaimsExtractorOption { - return func(c *ClaimsExtractor) { - c.FromRequestContext = ec - } -} - // NewClaimsExtractor returns an extractor, and if provided with a function with ExtractClaims signature, // then it will use that logic. Uses ExtractClaimsFromRequestContext by default func NewClaimsExtractor(options ...ClaimsExtractorOption) *ClaimsExtractor { @@ -68,49 +60,13 @@ func NewClaimsExtractor(options ...ClaimsExtractorOption) *ClaimsExtractor { for _, option := range options { option(ce) } - if ce.FromRequestContext == nil { - ce.FromRequestContext = ce.fromRequestContext - } + if ce.userIDClaim == "" { ce.userIDClaim = UserIDClaim } return ce } -// FromToken extracts claims from the token (after auth) -func (c *ClaimsExtractor) FromToken(token *jwt.Token) AuthorizationClaims { - claims := token.Claims.(jwt.MapClaims) - jwtClaims := AuthorizationClaims{ - Raw: claims, - } - userID, ok := claims[c.userIDClaim].(string) - if !ok { - return jwtClaims - } - jwtClaims.UserId = userID - accountIDClaim, ok := claims[c.authAudience+AccountIDSuffix] - if ok { - jwtClaims.AccountId = accountIDClaim.(string) - } - domainClaim, ok := claims[c.authAudience+DomainIDSuffix] - if ok { - jwtClaims.Domain = domainClaim.(string) - } - domainCategoryClaim, ok := claims[c.authAudience+DomainCategorySuffix] - if ok { - jwtClaims.DomainCategory = domainCategoryClaim.(string) - } - LastLoginClaimString, ok := claims[c.authAudience+LastLoginSuffix] - if ok { - jwtClaims.LastLogin = parseTime(LastLoginClaimString.(string)) - } - invitedBool, ok := claims[c.authAudience+Invited] - if ok { - jwtClaims.Invited = invitedBool.(bool) - } - return jwtClaims -} - func parseTime(timeString string) time.Time { if timeString == "" { return time.Time{} @@ -122,11 +78,67 @@ func parseTime(timeString string) time.Time { return parsedTime } -// fromRequestContext extracts claims from the request context previously filled by the JWT token (after auth) -func (c *ClaimsExtractor) fromRequestContext(r *http.Request) AuthorizationClaims { - if r.Context().Value(TokenUserProperty) == nil { - return AuthorizationClaims{} +func (c ClaimsExtractor) audienceClaim(claimName string) string { + url, err := url.JoinPath(c.authAudience, claimName) + if err != nil { + return c.authAudience + claimName // as it was previously } - token := r.Context().Value(TokenUserProperty).(*jwt.Token) - return c.FromToken(token) + + return url +} + +func (c *ClaimsExtractor) ToUserAuth(token *jwt.Token) (nbcontext.UserAuth, error) { + claims := token.Claims.(jwt.MapClaims) + userAuth := nbcontext.UserAuth{} + + userID, ok := claims[c.userIDClaim].(string) + if !ok { + return userAuth, errUserIDClaimEmpty + } + userAuth.UserId = userID + + if accountIDClaim, ok := claims[c.audienceClaim(AccountIDSuffix)]; ok { + userAuth.AccountId = accountIDClaim.(string) + } + + if domainClaim, ok := claims[c.audienceClaim(DomainIDSuffix)]; ok { + userAuth.Domain = domainClaim.(string) + } + + if domainCategoryClaim, ok := claims[c.audienceClaim(DomainCategorySuffix)]; ok { + userAuth.DomainCategory = domainCategoryClaim.(string) + } + + if lastLoginClaimString, ok := claims[c.audienceClaim(LastLoginSuffix)]; ok { + userAuth.LastLogin = parseTime(lastLoginClaimString.(string)) + } + + if invitedBool, ok := claims[c.audienceClaim(Invited)]; ok { + if value, ok := invitedBool.(bool); ok { + userAuth.Invited = value + } + } + + return userAuth, nil +} + +func (c *ClaimsExtractor) ToGroups(token *jwt.Token, claimName string) []string { + claims := token.Claims.(jwt.MapClaims) + userJWTGroups := make([]string, 0) + + if claim, ok := claims[claimName]; ok { + if claimGroups, ok := claim.([]interface{}); ok { + for _, g := range claimGroups { + if group, ok := g.(string); ok { + userJWTGroups = append(userJWTGroups, group) + } else { + log.Debugf("JWT claim %q contains a non-string group (type: %T): %v", claimName, g, g) + } + } + } + } else { + log.Debugf("JWT claim %q is not a string array", claimName) + } + + return userJWTGroups } diff --git a/management/server/auth/jwt/validator.go b/management/server/auth/jwt/validator.go new file mode 100644 index 000000000..5b38ca786 --- /dev/null +++ b/management/server/auth/jwt/validator.go @@ -0,0 +1,302 @@ +package jwt + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rsa" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "math/big" + "net/http" + "net/url" + "strconv" + "strings" + "sync" + "time" + + "github.com/golang-jwt/jwt" + + log "github.com/sirupsen/logrus" +) + +// Jwks is a collection of JSONWebKey obtained from Config.HttpServerConfig.AuthKeysLocation +type Jwks struct { + Keys []JSONWebKey `json:"keys"` + expiresInTime time.Time +} + +// The supported elliptic curves types +const ( + // p256 represents a cryptographic elliptical curve type. + p256 = "P-256" + + // p384 represents a cryptographic elliptical curve type. + p384 = "P-384" + + // p521 represents a cryptographic elliptical curve type. + p521 = "P-521" +) + +// JSONWebKey is a representation of a Jason Web Key +type JSONWebKey struct { + Kty string `json:"kty"` + Kid string `json:"kid"` + Use string `json:"use"` + N string `json:"n"` + E string `json:"e"` + Crv string `json:"crv"` + X string `json:"x"` + Y string `json:"y"` + X5c []string `json:"x5c"` +} + +type Validator struct { + lock sync.Mutex + issuer string + audienceList []string + keysLocation string + idpSignkeyRefreshEnabled bool + keys *Jwks +} + +var ( + errKeyNotFound = errors.New("unable to find appropriate key") + errInvalidAudience = errors.New("invalid audience") + errInvalidIssuer = errors.New("invalid issuer") + errTokenEmpty = errors.New("required authorization token not found") + errTokenInvalid = errors.New("token is invalid") + errTokenParsing = errors.New("token could not be parsed") +) + +func NewValidator(issuer string, audienceList []string, keysLocation string, idpSignkeyRefreshEnabled bool) *Validator { + keys, err := getPemKeys(keysLocation) + if err != nil { + log.WithField("keysLocation", keysLocation).Errorf("could not get keys from location: %s", err) + } + + return &Validator{ + keys: keys, + issuer: issuer, + audienceList: audienceList, + keysLocation: keysLocation, + idpSignkeyRefreshEnabled: idpSignkeyRefreshEnabled, + } +} + +func (v *Validator) getKeyFunc(ctx context.Context) jwt.Keyfunc { + return func(token *jwt.Token) (interface{}, error) { + // Verify 'aud' claim + var checkAud bool + for _, audience := range v.audienceList { + checkAud = token.Claims.(jwt.MapClaims).VerifyAudience(audience, false) + if checkAud { + break + } + } + if !checkAud { + return token, errInvalidAudience + } + + // Verify 'issuer' claim + checkIss := token.Claims.(jwt.MapClaims).VerifyIssuer(v.issuer, false) + if !checkIss { + return token, errInvalidIssuer + } + + // If keys are rotated, verify the keys prior to token validation + if v.idpSignkeyRefreshEnabled { + // If the keys are invalid, retrieve new ones + // @todo propose a separate go routine to regularly check these to prevent blocking when actually + // validating the token + if !v.keys.stillValid() { + v.lock.Lock() + defer v.lock.Unlock() + + refreshedKeys, err := getPemKeys(v.keysLocation) + if err != nil { + log.WithContext(ctx).Debugf("cannot get JSONWebKey: %v, falling back to old keys", err) + refreshedKeys = v.keys + } + + log.WithContext(ctx).Debugf("keys refreshed, new UTC expiration time: %s", refreshedKeys.expiresInTime.UTC()) + + v.keys = refreshedKeys + } + } + + publicKey, err := getPublicKey(token, v.keys) + if err == nil { + return publicKey, nil + } + + msg := fmt.Sprintf("getPublicKey error: %s", err) + if errors.Is(err, errKeyNotFound) && !v.idpSignkeyRefreshEnabled { + msg = fmt.Sprintf("getPublicKey error: %s. You can enable key refresh by setting HttpServerConfig.IdpSignKeyRefreshEnabled to true in your management.json file and restart the service", err) + } + + log.WithContext(ctx).Error(msg) + + return nil, err + } +} + +// ValidateAndParse validates the token and returns the parsed token +func (m *Validator) ValidateAndParse(ctx context.Context, token string) (*jwt.Token, error) { + // If the token is empty... + if token == "" { + // If we get here, the required token is missing + log.WithContext(ctx).Debugf(" Error: No credentials found (CredentialsOptional=false)") + return nil, errTokenEmpty + } + + // Now parse the token + parsedToken, err := jwt.Parse(token, m.getKeyFunc(ctx)) + + // Check if there was an error in parsing... + if err != nil { + err = fmt.Errorf("%w: %s", errTokenParsing, err) + log.WithContext(ctx).Error(err.Error()) + return nil, err + } + + // Check if the parsed token is valid... + if !parsedToken.Valid { + log.WithContext(ctx).Debug(errTokenInvalid.Error()) + return nil, errTokenInvalid + } + + return parsedToken, nil +} + +// stillValid returns true if the JSONWebKey still valid and have enough time to be used +func (jwks *Jwks) stillValid() bool { + return !jwks.expiresInTime.IsZero() && time.Now().Add(5*time.Second).Before(jwks.expiresInTime) +} + +func getPemKeys(keysLocation string) (*Jwks, error) { + jwks := &Jwks{} + + url, err := url.ParseRequestURI(keysLocation) + if err != nil { + return jwks, err + } + + resp, err := http.Get(url.String()) + if err != nil { + return jwks, err + } + defer resp.Body.Close() + + err = json.NewDecoder(resp.Body).Decode(jwks) + if err != nil { + return jwks, err + } + + cacheControlHeader := resp.Header.Get("Cache-Control") + expiresIn := getMaxAgeFromCacheHeader(cacheControlHeader) + jwks.expiresInTime = time.Now().Add(time.Duration(expiresIn) * time.Second) + + return jwks, nil +} + +func getPublicKey(token *jwt.Token, jwks *Jwks) (interface{}, error) { + // todo as we load the jkws when the server is starting, we should build a JKS map with the pem cert at the boot time + for k := range jwks.Keys { + if token.Header["kid"] != jwks.Keys[k].Kid { + continue + } + + if len(jwks.Keys[k].X5c) != 0 { + cert := "-----BEGIN CERTIFICATE-----\n" + jwks.Keys[k].X5c[0] + "\n-----END CERTIFICATE-----" + return jwt.ParseRSAPublicKeyFromPEM([]byte(cert)) + } + + if jwks.Keys[k].Kty == "RSA" { + return getPublicKeyFromRSA(jwks.Keys[k]) + } + if jwks.Keys[k].Kty == "EC" { + return getPublicKeyFromECDSA(jwks.Keys[k]) + } + } + + return nil, errKeyNotFound +} + +func getPublicKeyFromECDSA(jwk JSONWebKey) (publicKey *ecdsa.PublicKey, err error) { + if jwk.X == "" || jwk.Y == "" || jwk.Crv == "" { + return nil, fmt.Errorf("ecdsa key incomplete") + } + + var xCoordinate []byte + if xCoordinate, err = base64.RawURLEncoding.DecodeString(jwk.X); err != nil { + return nil, err + } + + var yCoordinate []byte + if yCoordinate, err = base64.RawURLEncoding.DecodeString(jwk.Y); err != nil { + return nil, err + } + + publicKey = &ecdsa.PublicKey{} + + var curve elliptic.Curve + switch jwk.Crv { + case p256: + curve = elliptic.P256() + case p384: + curve = elliptic.P384() + case p521: + curve = elliptic.P521() + } + + publicKey.Curve = curve + publicKey.X = big.NewInt(0).SetBytes(xCoordinate) + publicKey.Y = big.NewInt(0).SetBytes(yCoordinate) + + return publicKey, nil +} + +func getPublicKeyFromRSA(jwk JSONWebKey) (*rsa.PublicKey, error) { + decodedE, err := base64.RawURLEncoding.DecodeString(jwk.E) + if err != nil { + return nil, err + } + decodedN, err := base64.RawURLEncoding.DecodeString(jwk.N) + if err != nil { + return nil, err + } + + var n, e big.Int + e.SetBytes(decodedE) + n.SetBytes(decodedN) + + return &rsa.PublicKey{ + E: int(e.Int64()), + N: &n, + }, nil +} + +// getMaxAgeFromCacheHeader extracts max-age directive from the Cache-Control header +func getMaxAgeFromCacheHeader(cacheControl string) int { + // Split into individual directives + directives := strings.Split(cacheControl, ",") + + for _, directive := range directives { + directive = strings.TrimSpace(directive) + if strings.HasPrefix(directive, "max-age=") { + // Extract the max-age value + maxAgeStr := strings.TrimPrefix(directive, "max-age=") + maxAge, err := strconv.Atoi(maxAgeStr) + if err != nil { + return 0 + } + + return maxAge + } + } + + return 0 +} diff --git a/management/server/auth/manager.go b/management/server/auth/manager.go new file mode 100644 index 000000000..6835a3ced --- /dev/null +++ b/management/server/auth/manager.go @@ -0,0 +1,170 @@ +package auth + +import ( + "context" + "crypto/sha256" + "encoding/base64" + "fmt" + "hash/crc32" + + "github.com/golang-jwt/jwt" + + "github.com/netbirdio/netbird/base62" + nbjwt "github.com/netbirdio/netbird/management/server/auth/jwt" + nbcontext "github.com/netbirdio/netbird/management/server/context" + "github.com/netbirdio/netbird/management/server/store" + "github.com/netbirdio/netbird/management/server/types" +) + +var _ Manager = (*manager)(nil) + +type Manager interface { + ValidateAndParseToken(ctx context.Context, value string) (nbcontext.UserAuth, *jwt.Token, error) + EnsureUserAccessByJWTGroups(ctx context.Context, userAuth nbcontext.UserAuth, token *jwt.Token) (nbcontext.UserAuth, error) + MarkPATUsed(ctx context.Context, tokenID string) error + GetPATInfo(ctx context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error) +} + +type manager struct { + store store.Store + + validator *nbjwt.Validator + extractor *nbjwt.ClaimsExtractor +} + +func NewManager(store store.Store, issuer, audience, keysLocation, userIdClaim string, allAudiences []string, idpRefreshKeys bool) Manager { + // @note if invalid/missing parameters are sent the validator will instantiate + // but it will fail when validating and parsing the token + jwtValidator := nbjwt.NewValidator( + issuer, + allAudiences, + keysLocation, + idpRefreshKeys, + ) + + claimsExtractor := nbjwt.NewClaimsExtractor( + nbjwt.WithAudience(audience), + nbjwt.WithUserIDClaim(userIdClaim), + ) + + return &manager{ + store: store, + + validator: jwtValidator, + extractor: claimsExtractor, + } +} + +func (m *manager) ValidateAndParseToken(ctx context.Context, value string) (nbcontext.UserAuth, *jwt.Token, error) { + token, err := m.validator.ValidateAndParse(ctx, value) + if err != nil { + return nbcontext.UserAuth{}, nil, err + } + + userAuth, err := m.extractor.ToUserAuth(token) + if err != nil { + return nbcontext.UserAuth{}, nil, err + } + return userAuth, token, err +} + +func (m *manager) EnsureUserAccessByJWTGroups(ctx context.Context, userAuth nbcontext.UserAuth, token *jwt.Token) (nbcontext.UserAuth, error) { + if userAuth.IsChild || userAuth.IsPAT { + return userAuth, nil + } + + settings, err := m.store.GetAccountSettings(ctx, store.LockingStrengthShare, userAuth.AccountId) + if err != nil { + return userAuth, err + } + + // Ensures JWT group synchronization to the management is enabled before, + // filtering access based on the allowed groups. + if settings != nil && settings.JWTGroupsEnabled { + userAuth.Groups = m.extractor.ToGroups(token, settings.JWTGroupsClaimName) + if allowedGroups := settings.JWTAllowGroups; len(allowedGroups) > 0 { + if !userHasAllowedGroup(allowedGroups, userAuth.Groups) { + return userAuth, fmt.Errorf("user does not belong to any of the allowed JWT groups") + } + } + } + + return userAuth, nil +} + +// MarkPATUsed marks a personal access token as used +func (am *manager) MarkPATUsed(ctx context.Context, tokenID string) error { + return am.store.MarkPATUsed(ctx, store.LockingStrengthUpdate, tokenID) +} + +// GetPATInfo retrieves user, personal access token, domain, and category details from a personal access token. +func (am *manager) GetPATInfo(ctx context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error) { + user, pat, err = am.extractPATFromToken(ctx, token) + if err != nil { + return nil, nil, "", "", err + } + + domain, category, err = am.store.GetAccountDomainAndCategory(ctx, store.LockingStrengthShare, user.AccountID) + if err != nil { + return nil, nil, "", "", err + } + + return user, pat, domain, category, nil +} + +// extractPATFromToken validates the token structure and retrieves associated User and PAT. +func (am *manager) extractPATFromToken(ctx context.Context, token string) (*types.User, *types.PersonalAccessToken, error) { + if len(token) != types.PATLength { + return nil, nil, fmt.Errorf("PAT has incorrect length") + } + + prefix := token[:len(types.PATPrefix)] + if prefix != types.PATPrefix { + return nil, nil, fmt.Errorf("PAT has wrong prefix") + } + secret := token[len(types.PATPrefix) : len(types.PATPrefix)+types.PATSecretLength] + encodedChecksum := token[len(types.PATPrefix)+types.PATSecretLength : len(types.PATPrefix)+types.PATSecretLength+types.PATChecksumLength] + + verificationChecksum, err := base62.Decode(encodedChecksum) + if err != nil { + return nil, nil, fmt.Errorf("PAT checksum decoding failed: %w", err) + } + + secretChecksum := crc32.ChecksumIEEE([]byte(secret)) + if secretChecksum != verificationChecksum { + return nil, nil, fmt.Errorf("PAT checksum does not match") + } + + hashedToken := sha256.Sum256([]byte(token)) + encodedHashedToken := base64.StdEncoding.EncodeToString(hashedToken[:]) + + var user *types.User + var pat *types.PersonalAccessToken + + err = am.store.ExecuteInTransaction(ctx, func(transaction store.Store) error { + pat, err = transaction.GetPATByHashedToken(ctx, store.LockingStrengthShare, encodedHashedToken) + if err != nil { + return err + } + + user, err = transaction.GetUserByPATID(ctx, store.LockingStrengthShare, pat.ID) + return err + }) + if err != nil { + return nil, nil, err + } + + return user, pat, nil +} + +// userHasAllowedGroup checks if a user belongs to any of the allowed groups. +func userHasAllowedGroup(allowedGroups []string, userGroups []string) bool { + for _, userGroup := range userGroups { + for _, allowedGroup := range allowedGroups { + if userGroup == allowedGroup { + return true + } + } + } + return false +} diff --git a/management/server/auth/manager_mock.go b/management/server/auth/manager_mock.go new file mode 100644 index 000000000..bc7066548 --- /dev/null +++ b/management/server/auth/manager_mock.go @@ -0,0 +1,54 @@ +package auth + +import ( + "context" + + "github.com/golang-jwt/jwt" + + nbcontext "github.com/netbirdio/netbird/management/server/context" + "github.com/netbirdio/netbird/management/server/types" +) + +var ( + _ Manager = (*MockManager)(nil) +) + +// @note really dislike this mocking approach but rather than have to do additional test refactoring. +type MockManager struct { + ValidateAndParseTokenFunc func(ctx context.Context, value string) (nbcontext.UserAuth, *jwt.Token, error) + EnsureUserAccessByJWTGroupsFunc func(ctx context.Context, userAuth nbcontext.UserAuth, token *jwt.Token) (nbcontext.UserAuth, error) + MarkPATUsedFunc func(ctx context.Context, tokenID string) error + GetPATInfoFunc func(ctx context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error) +} + +// EnsureUserAccessByJWTGroups implements Manager. +func (m *MockManager) EnsureUserAccessByJWTGroups(ctx context.Context, userAuth nbcontext.UserAuth, token *jwt.Token) (nbcontext.UserAuth, error) { + if m.EnsureUserAccessByJWTGroupsFunc != nil { + return m.EnsureUserAccessByJWTGroupsFunc(ctx, userAuth, token) + } + return nbcontext.UserAuth{}, nil +} + +// GetPATInfo implements Manager. +func (m *MockManager) GetPATInfo(ctx context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error) { + if m.GetPATInfoFunc != nil { + return m.GetPATInfoFunc(ctx, token) + } + return &types.User{}, &types.PersonalAccessToken{}, "", "", nil +} + +// MarkPATUsed implements Manager. +func (m *MockManager) MarkPATUsed(ctx context.Context, tokenID string) error { + if m.MarkPATUsedFunc != nil { + return m.MarkPATUsedFunc(ctx, tokenID) + } + return nil +} + +// ValidateAndParseToken implements Manager. +func (m *MockManager) ValidateAndParseToken(ctx context.Context, value string) (nbcontext.UserAuth, *jwt.Token, error) { + if m.ValidateAndParseTokenFunc != nil { + return m.ValidateAndParseTokenFunc(ctx, value) + } + return nbcontext.UserAuth{}, &jwt.Token{}, nil +} diff --git a/management/server/auth/manager_test.go b/management/server/auth/manager_test.go new file mode 100644 index 000000000..55fb1e31a --- /dev/null +++ b/management/server/auth/manager_test.go @@ -0,0 +1,407 @@ +package auth_test + +import ( + "context" + "crypto/sha256" + "encoding/base64" + "fmt" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + "time" + + "github.com/golang-jwt/jwt" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/management/server/auth" + nbjwt "github.com/netbirdio/netbird/management/server/auth/jwt" + nbcontext "github.com/netbirdio/netbird/management/server/context" + "github.com/netbirdio/netbird/management/server/store" + "github.com/netbirdio/netbird/management/server/types" +) + +func TestAuthManager_GetAccountInfoFromPAT(t *testing.T) { + store, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir()) + if err != nil { + t.Fatalf("Error when creating store: %s", err) + } + t.Cleanup(cleanup) + + token := "nbp_9999EUDNdkeusjentDLSJEn1902u84390W6W" + hashedToken := sha256.Sum256([]byte(token)) + encodedHashedToken := base64.StdEncoding.EncodeToString(hashedToken[:]) + account := &types.Account{ + Id: "account_id", + Users: map[string]*types.User{"someUser": { + Id: "someUser", + PATs: map[string]*types.PersonalAccessToken{ + "tokenId": { + ID: "tokenId", + UserID: "someUser", + HashedToken: encodedHashedToken, + }, + }, + }}, + } + + err = store.SaveAccount(context.Background(), account) + if err != nil { + t.Fatalf("Error when saving account: %s", err) + } + + manager := auth.NewManager(store, "", "", "", "", []string{}, false) + + user, pat, _, _, err := manager.GetPATInfo(context.Background(), token) + if err != nil { + t.Fatalf("Error when getting Account from PAT: %s", err) + } + + assert.Equal(t, "account_id", user.AccountID) + assert.Equal(t, "someUser", user.Id) + assert.Equal(t, account.Users["someUser"].PATs["tokenId"].ID, pat.ID) +} + +func TestAuthManager_MarkPATUsed(t *testing.T) { + store, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir()) + if err != nil { + t.Fatalf("Error when creating store: %s", err) + } + t.Cleanup(cleanup) + + token := "nbp_9999EUDNdkeusjentDLSJEn1902u84390W6W" + hashedToken := sha256.Sum256([]byte(token)) + encodedHashedToken := base64.StdEncoding.EncodeToString(hashedToken[:]) + account := &types.Account{ + Id: "account_id", + Users: map[string]*types.User{"someUser": { + Id: "someUser", + PATs: map[string]*types.PersonalAccessToken{ + "tokenId": { + ID: "tokenId", + HashedToken: encodedHashedToken, + }, + }, + }}, + } + + err = store.SaveAccount(context.Background(), account) + if err != nil { + t.Fatalf("Error when saving account: %s", err) + } + + manager := auth.NewManager(store, "", "", "", "", []string{}, false) + + err = manager.MarkPATUsed(context.Background(), "tokenId") + if err != nil { + t.Fatalf("Error when marking PAT used: %s", err) + } + + account, err = store.GetAccount(context.Background(), "account_id") + if err != nil { + t.Fatalf("Error when getting account: %s", err) + } + assert.True(t, !account.Users["someUser"].PATs["tokenId"].GetLastUsed().IsZero()) +} + +func TestAuthManager_EnsureUserAccessByJWTGroups(t *testing.T) { + store, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir()) + if err != nil { + t.Fatalf("Error when creating store: %s", err) + } + t.Cleanup(cleanup) + + userId := "user-id" + domain := "test.domain" + + account := &types.Account{ + Id: "account_id", + Domain: domain, + Users: map[string]*types.User{"someUser": { + Id: "someUser", + }}, + Settings: &types.Settings{}, + } + + err = store.SaveAccount(context.Background(), account) + if err != nil { + t.Fatalf("Error when saving account: %s", err) + } + + // this has been validated and parsed by ValidateAndParseToken + userAuth := nbcontext.UserAuth{ + AccountId: account.Id, + Domain: domain, + UserId: userId, + DomainCategory: "test-category", + // Groups: []string{"group1", "group2"}, + } + + // these tests only assert groups are parsed from token as per account settings + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{"idp-groups": []interface{}{"group1", "group2"}}) + + manager := auth.NewManager(store, "", "", "", "", []string{}, false) + + t.Run("JWT groups disabled", func(t *testing.T) { + userAuth, err := manager.EnsureUserAccessByJWTGroups(context.Background(), userAuth, token) + require.NoError(t, err, "ensure user access by JWT groups failed") + require.Len(t, userAuth.Groups, 0, "account not enabled to ensure access by groups") + }) + + t.Run("User impersonated", func(t *testing.T) { + userAuth, err := manager.EnsureUserAccessByJWTGroups(context.Background(), userAuth, token) + require.NoError(t, err, "ensure user access by JWT groups failed") + require.Len(t, userAuth.Groups, 0, "account not enabled to ensure access by groups") + }) + + t.Run("User PAT", func(t *testing.T) { + userAuth, err := manager.EnsureUserAccessByJWTGroups(context.Background(), userAuth, token) + require.NoError(t, err, "ensure user access by JWT groups failed") + require.Len(t, userAuth.Groups, 0, "account not enabled to ensure access by groups") + }) + + t.Run("JWT groups enabled without claim name", func(t *testing.T) { + account.Settings.JWTGroupsEnabled = true + err := store.SaveAccount(context.Background(), account) + require.NoError(t, err, "save account failed") + + userAuth, err := manager.EnsureUserAccessByJWTGroups(context.Background(), userAuth, token) + require.NoError(t, err, "ensure user access by JWT groups failed") + require.Len(t, userAuth.Groups, 0, "account missing groups claim name") + }) + + t.Run("JWT groups enabled without allowed groups", func(t *testing.T) { + account.Settings.JWTGroupsEnabled = true + account.Settings.JWTGroupsClaimName = "idp-groups" + err := store.SaveAccount(context.Background(), account) + require.NoError(t, err, "save account failed") + + userAuth, err := manager.EnsureUserAccessByJWTGroups(context.Background(), userAuth, token) + require.NoError(t, err, "ensure user access by JWT groups failed") + require.Equal(t, []string{"group1", "group2"}, userAuth.Groups, "group parsed do not match") + }) + + t.Run("User in allowed JWT groups", func(t *testing.T) { + account.Settings.JWTGroupsEnabled = true + account.Settings.JWTGroupsClaimName = "idp-groups" + account.Settings.JWTAllowGroups = []string{"group1"} + err := store.SaveAccount(context.Background(), account) + require.NoError(t, err, "save account failed") + + userAuth, err := manager.EnsureUserAccessByJWTGroups(context.Background(), userAuth, token) + require.NoError(t, err, "ensure user access by JWT groups failed") + + require.Equal(t, []string{"group1", "group2"}, userAuth.Groups, "group parsed do not match") + }) + + t.Run("User not in allowed JWT groups", func(t *testing.T) { + account.Settings.JWTGroupsEnabled = true + account.Settings.JWTGroupsClaimName = "idp-groups" + account.Settings.JWTAllowGroups = []string{"not-a-group"} + err := store.SaveAccount(context.Background(), account) + require.NoError(t, err, "save account failed") + + _, err = manager.EnsureUserAccessByJWTGroups(context.Background(), userAuth, token) + require.Error(t, err, "ensure user access is not in allowed groups") + }) +} + +func TestAuthManager_ValidateAndParseToken(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Cache-Control", "max-age=30") // set a 30s expiry to these keys + http.ServeFile(w, r, "test_data/jwks.json") + })) + defer server.Close() + + issuer := "http://issuer.local" + audience := "http://audience.local" + userIdClaim := "" // defaults to "sub" + + // we're only testing with RSA256 + keyData, _ := os.ReadFile("test_data/sample_key") + key, _ := jwt.ParseRSAPrivateKeyFromPEM(keyData) + keyId := "test-key" + + // note, we can use a nil store because ValidateAndParseToken does not use it in it's flow + manager := auth.NewManager(nil, issuer, audience, server.URL, userIdClaim, []string{audience}, false) + + customClaim := func(name string) string { + return fmt.Sprintf("%s/%s", audience, name) + } + + lastLogin := time.Date(2025, 2, 12, 14, 25, 26, 0, time.UTC) //"2025-02-12T14:25:26.186Z" + + tests := []struct { + name string + tokenFunc func() string + expected *nbcontext.UserAuth // nil indicates expected error + }{ + { + name: "Valid with custom claims", + tokenFunc: func() string { + token := jwt.New(jwt.SigningMethodRS256) + token.Header["kid"] = keyId + token.Claims = jwt.MapClaims{ + "iss": issuer, + "aud": []string{audience}, + "iat": time.Now().Unix(), + "exp": time.Now().Add(time.Hour * 1).Unix(), + "sub": "user-id|123", + customClaim(nbjwt.AccountIDSuffix): "account-id|567", + customClaim(nbjwt.DomainIDSuffix): "http://localhost", + customClaim(nbjwt.DomainCategorySuffix): "private", + customClaim(nbjwt.LastLoginSuffix): lastLogin.Format(time.RFC3339), + customClaim(nbjwt.Invited): false, + } + tokenString, _ := token.SignedString(key) + return tokenString + }, + expected: &nbcontext.UserAuth{ + UserId: "user-id|123", + AccountId: "account-id|567", + Domain: "http://localhost", + DomainCategory: "private", + LastLogin: lastLogin, + Invited: false, + }, + }, + { + name: "Valid without custom claims", + tokenFunc: func() string { + token := jwt.New(jwt.SigningMethodRS256) + token.Header["kid"] = keyId + token.Claims = jwt.MapClaims{ + "iss": issuer, + "aud": []string{audience}, + "iat": time.Now().Unix(), + "exp": time.Now().Add(time.Hour).Unix(), + "sub": "user-id|123", + } + tokenString, _ := token.SignedString(key) + return tokenString + }, + expected: &nbcontext.UserAuth{ + UserId: "user-id|123", + }, + }, + { + name: "Expired token", + tokenFunc: func() string { + token := jwt.New(jwt.SigningMethodRS256) + token.Header["kid"] = keyId + token.Claims = jwt.MapClaims{ + "iss": issuer, + "aud": []string{audience}, + "iat": time.Now().Add(time.Hour * -2).Unix(), + "exp": time.Now().Add(time.Hour * -1).Unix(), + "sub": "user-id|123", + } + tokenString, _ := token.SignedString(key) + return tokenString + }, + }, + { + name: "Not yet valid", + tokenFunc: func() string { + token := jwt.New(jwt.SigningMethodRS256) + token.Header["kid"] = keyId + token.Claims = jwt.MapClaims{ + "iss": issuer, + "aud": []string{audience}, + "iat": time.Now().Add(time.Hour).Unix(), + "exp": time.Now().Add(time.Hour * 2).Unix(), + "sub": "user-id|123", + } + tokenString, _ := token.SignedString(key) + return tokenString + }, + }, + { + name: "Invalid signature", + tokenFunc: func() string { + token := jwt.New(jwt.SigningMethodRS256) + token.Header["kid"] = keyId + token.Claims = jwt.MapClaims{ + "iss": issuer, + "aud": []string{audience}, + "iat": time.Now().Unix(), + "exp": time.Now().Add(time.Hour).Unix(), + "sub": "user-id|123", + } + tokenString, _ := token.SignedString(key) + parts := strings.Split(tokenString, ".") + parts[2] = "invalid-signature" + return strings.Join(parts, ".") + }, + }, + { + name: "Invalid issuer", + tokenFunc: func() string { + token := jwt.New(jwt.SigningMethodRS256) + token.Header["kid"] = keyId + token.Claims = jwt.MapClaims{ + "iss": "not-the-issuer", + "aud": []string{audience}, + "iat": time.Now().Unix(), + "exp": time.Now().Add(time.Hour).Unix(), + "sub": "user-id|123", + } + tokenString, _ := token.SignedString(key) + return tokenString + }, + }, + { + name: "Invalid audience", + tokenFunc: func() string { + token := jwt.New(jwt.SigningMethodRS256) + token.Header["kid"] = keyId + token.Claims = jwt.MapClaims{ + "iss": issuer, + "aud": []string{"not-the-audience"}, + "iat": time.Now().Unix(), + "exp": time.Now().Add(time.Hour).Unix(), + "sub": "user-id|123", + } + tokenString, _ := token.SignedString(key) + return tokenString + }, + }, + { + name: "Invalid user claim", + tokenFunc: func() string { + token := jwt.New(jwt.SigningMethodRS256) + token.Header["kid"] = keyId + token.Claims = jwt.MapClaims{ + "iss": issuer, + "aud": []string{audience}, + "iat": time.Now().Unix(), + "exp": time.Now().Add(time.Hour).Unix(), + "not-sub": "user-id|123", + } + tokenString, _ := token.SignedString(key) + return tokenString + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tokenString := tt.tokenFunc() + + userAuth, token, err := manager.ValidateAndParseToken(context.Background(), tokenString) + + if tt.expected != nil { + assert.NoError(t, err) + assert.True(t, token.Valid) + assert.Equal(t, *tt.expected, userAuth) + } else { + assert.Error(t, err) + assert.Nil(t, token) + assert.Empty(t, userAuth) + } + }) + } + +} diff --git a/management/server/auth/test_data/jwks.json b/management/server/auth/test_data/jwks.json new file mode 100644 index 000000000..8080f5599 --- /dev/null +++ b/management/server/auth/test_data/jwks.json @@ -0,0 +1,11 @@ +{ + "keys": [ + { + "kty": "RSA", + "kid": "test-key", + "use": "sig", + "n": "4f5wg5l2hKsTeNem_V41fGnJm6gOdrj8ym3rFkEU_wT8RDtnSgFEZOQpHEgQ7JL38xUfU0Y3g6aYw9QT0hJ7mCpz9Er5qLaMXJwZxzHzAahlfA0icqabvJOMvQtzD6uQv6wPEyZtDTWiQi9AXwBpHssPnpYGIn20ZZuNlX2BrClciHhCPUIIZOQn_MmqTD31jSyjoQoV7MhhMTATKJx2XrHhR-1DcKJzQBSTAGnpYVaqpsARap-nwRipr3nUTuxyGohBTSmjJ2usSeQXHI3bODIRe1AuTyHceAbewn8b462yEWKARdpd9AjQW5SIVPfdsz5B6GlYQ5LdYKtznTuy7w", + "e": "AQAB" + } + ] +} \ No newline at end of file diff --git a/management/server/auth/test_data/sample_key b/management/server/auth/test_data/sample_key new file mode 100644 index 000000000..e69284a3f --- /dev/null +++ b/management/server/auth/test_data/sample_key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA4f5wg5l2hKsTeNem/V41fGnJm6gOdrj8ym3rFkEU/wT8RDtn +SgFEZOQpHEgQ7JL38xUfU0Y3g6aYw9QT0hJ7mCpz9Er5qLaMXJwZxzHzAahlfA0i +cqabvJOMvQtzD6uQv6wPEyZtDTWiQi9AXwBpHssPnpYGIn20ZZuNlX2BrClciHhC +PUIIZOQn/MmqTD31jSyjoQoV7MhhMTATKJx2XrHhR+1DcKJzQBSTAGnpYVaqpsAR +ap+nwRipr3nUTuxyGohBTSmjJ2usSeQXHI3bODIRe1AuTyHceAbewn8b462yEWKA +Rdpd9AjQW5SIVPfdsz5B6GlYQ5LdYKtznTuy7wIDAQABAoIBAQCwia1k7+2oZ2d3 +n6agCAbqIE1QXfCmh41ZqJHbOY3oRQG3X1wpcGH4Gk+O+zDVTV2JszdcOt7E5dAy +MaomETAhRxB7hlIOnEN7WKm+dGNrKRvV0wDU5ReFMRHg31/Lnu8c+5BvGjZX+ky9 +POIhFFYJqwCRlopGSUIxmVj5rSgtzk3iWOQXr+ah1bjEXvlxDOWkHN6YfpV5ThdE +KdBIPGEVqa63r9n2h+qazKrtiRqJqGnOrHzOECYbRFYhexsNFz7YT02xdfSHn7gM +IvabDDP/Qp0PjE1jdouiMaFHYnLBbgvlnZW9yuVf/rpXTUq/njxIXMmvmEyyvSDn +FcFikB8pAoGBAPF77hK4m3/rdGT7X8a/gwvZ2R121aBcdPwEaUhvj/36dx596zvY +mEOjrWfZhF083/nYWE2kVquj2wjs+otCLfifEEgXcVPTnEOPO9Zg3uNSL0nNQghj +FuD3iGLTUBCtM66oTe0jLSslHe8gLGEQqyMzHOzYxNqibxcOZIe8Qt0NAoGBAO+U +I5+XWjWEgDmvyC3TrOSf/KCGjtu0TSv30ipv27bDLMrpvPmD/5lpptTFwcxvVhCs +2b+chCjlghFSWFbBULBrfci2FtliClOVMYrlNBdUSJhf3aYSG2Doe6Bgt1n2CpNn +/iu37Y3NfemZBJA7hNl4dYe+f+uzM87cdQ214+jrAoGAXA0XxX8ll2+ToOLJsaNT +OvNB9h9Uc5qK5X5w+7G7O998BN2PC/MWp8H+2fVqpXgNENpNXttkRm1hk1dych86 +EunfdPuqsX+as44oCyJGFHVBnWpm33eWQw9YqANRI+pCJzP08I5WK3osnPiwshd+ +hR54yjgfYhBFNI7B95PmEQkCgYBzFSz7h1+s34Ycr8SvxsOBWxymG5zaCsUbPsL0 +4aCgLScCHb9J+E86aVbbVFdglYa5Id7DPTL61ixhl7WZjujspeXZGSbmq0Kcnckb +mDgqkLECiOJW2NHP/j0McAkDLL4tysF8TLDO8gvuvzNC+WQ6drO2ThrypLVZQ+ry +eBIPmwKBgEZxhqa0gVvHQG/7Od69KWj4eJP28kq13RhKay8JOoN0vPmspXJo1HY3 +CKuHRG+AP579dncdUnOMvfXOtkdM4vk0+hWASBQzM9xzVcztCa+koAugjVaLS9A+ +9uQoqEeVNTckxx0S2bYevRy7hGQmUJTyQm3j1zEUR5jpdbL83Fbq +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/management/server/auth/test_data/sample_key.pub b/management/server/auth/test_data/sample_key.pub new file mode 100644 index 000000000..d5b7f7102 --- /dev/null +++ b/management/server/auth/test_data/sample_key.pub @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4f5wg5l2hKsTeNem/V41 +fGnJm6gOdrj8ym3rFkEU/wT8RDtnSgFEZOQpHEgQ7JL38xUfU0Y3g6aYw9QT0hJ7 +mCpz9Er5qLaMXJwZxzHzAahlfA0icqabvJOMvQtzD6uQv6wPEyZtDTWiQi9AXwBp +HssPnpYGIn20ZZuNlX2BrClciHhCPUIIZOQn/MmqTD31jSyjoQoV7MhhMTATKJx2 +XrHhR+1DcKJzQBSTAGnpYVaqpsARap+nwRipr3nUTuxyGohBTSmjJ2usSeQXHI3b +ODIRe1AuTyHceAbewn8b462yEWKARdpd9AjQW5SIVPfdsz5B6GlYQ5LdYKtznTuy +7wIDAQAB +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/management/server/config.go b/management/server/config.go index f3555b92b..ce2ff4d16 100644 --- a/management/server/config.go +++ b/management/server/config.go @@ -2,7 +2,6 @@ package server import ( "net/netip" - "net/url" "github.com/netbirdio/netbird/management/server/idp" "github.com/netbirdio/netbird/management/server/store" @@ -106,10 +105,10 @@ type HttpServerConfig struct { ExtraAuthAudience string } -// Host represents a Wiretrustee host (e.g. STUN, TURN, Signal) +// Host represents a Netbird host (e.g. STUN, TURN, Signal) type Host struct { Proto Protocol - // URI e.g. turns://stun.wiretrustee.com:4430 or signal.wiretrustee.com:10000 + // URI e.g. turns://stun.netbird.io:4430 or signal.netbird.io:10000 URI string Username string Password string @@ -180,9 +179,3 @@ type ReverseProxy struct { // trusted IP prefixes. TrustedPeers []netip.Prefix } - -// validateURL validates input http url -func validateURL(httpURL string) bool { - _, err := url.ParseRequestURI(httpURL) - return err == nil -} diff --git a/management/server/context/auth.go b/management/server/context/auth.go new file mode 100644 index 000000000..5cb28ddb7 --- /dev/null +++ b/management/server/context/auth.go @@ -0,0 +1,60 @@ +package context + +import ( + "context" + "fmt" + "net/http" + "time" +) + +type key int + +const ( + UserAuthContextKey key = iota +) + +type UserAuth struct { + // The account id the user is accessing + AccountId string + // The account domain + Domain string + // The account domain category, TBC values + DomainCategory string + // Indicates whether this user was invited, TBC logic + Invited bool + // Indicates whether this is a child account + IsChild bool + + // The user id + UserId string + // Last login time for this user + LastLogin time.Time + // The Groups the user belongs to on this account + Groups []string + + // Indicates whether this user has authenticated with a Personal Access Token + IsPAT bool +} + +func GetUserAuthFromRequest(r *http.Request) (UserAuth, error) { + return GetUserAuthFromContext(r.Context()) +} + +func SetUserAuthInRequest(r *http.Request, userAuth UserAuth) *http.Request { + return r.WithContext(SetUserAuthInContext(r.Context(), userAuth)) +} + +func GetUserAuthFromContext(ctx context.Context) (UserAuth, error) { + if userAuth, ok := ctx.Value(UserAuthContextKey).(UserAuth); ok { + return userAuth, nil + } + return UserAuth{}, fmt.Errorf("user auth not in context") +} + +func SetUserAuthInContext(ctx context.Context, userAuth UserAuth) context.Context { + //nolint + ctx = context.WithValue(ctx, UserIDKey, userAuth.UserId) + //nolint + ctx = context.WithValue(ctx, AccountIDKey, userAuth.AccountId) + return context.WithValue(ctx, UserAuthContextKey, userAuth) +} diff --git a/management/server/dns_test.go b/management/server/dns_test.go index 6fb9f6a29..c40f62324 100644 --- a/management/server/dns_test.go +++ b/management/server/dns_test.go @@ -42,7 +42,7 @@ func TestGetDNSSettings(t *testing.T) { account, err := initTestDNSAccount(t, am) if err != nil { - t.Fatal("failed to init testing account") + t.Fatalf("failed to init testing account: %s", err) } dnsSettings, err := am.GetDNSSettings(context.Background(), account.Id, dnsAdminUserID) @@ -124,12 +124,12 @@ func TestSaveDNSSettings(t *testing.T) { t.Run(testCase.name, func(t *testing.T) { am, err := createDNSManager(t) if err != nil { - t.Error("failed to create account manager") + t.Fatalf("failed to create account manager") } account, err := initTestDNSAccount(t, am) if err != nil { - t.Error("failed to init testing account") + t.Fatalf("failed to init testing account: %v", err) } err = am.SaveDNSSettings(context.Background(), account.Id, testCase.userID, testCase.inputSettings) @@ -156,22 +156,22 @@ func TestGetNetworkMap_DNSConfigSync(t *testing.T) { am, err := createDNSManager(t) if err != nil { - t.Error("failed to create account manager") + t.Fatalf("failed to create account manager: %s", err) } account, err := initTestDNSAccount(t, am) if err != nil { - t.Error("failed to init testing account") + t.Fatalf("failed to init testing account: %s", err) } peer1, err := account.FindPeerByPubKey(dnsPeer1Key) if err != nil { - t.Error("failed to init testing account") + t.Fatalf("failed to init testing account: %s", err) } peer2, err := account.FindPeerByPubKey(dnsPeer2Key) if err != nil { - t.Error("failed to init testing account") + t.Fatalf("failed to init testing account: %s", err) } newAccountDNSConfig, err := am.GetNetworkMap(context.Background(), peer1.ID) diff --git a/management/server/geolocation/database.go b/management/server/geolocation/database.go index 21ae93b9d..97ab398fb 100644 --- a/management/server/geolocation/database.go +++ b/management/server/geolocation/database.go @@ -123,7 +123,6 @@ func importCsvToSqlite(dataDir string, csvFile string, geonamesdbFile string) er db, err := gorm.Open(sqlite.Open(path.Join(dataDir, geonamesdbFile)), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), CreateBatchSize: 1000, - PrepareStmt: true, }) if err != nil { return err diff --git a/management/server/geolocation/store.go b/management/server/geolocation/store.go index 1f94bf47e..5af8276b5 100644 --- a/management/server/geolocation/store.go +++ b/management/server/geolocation/store.go @@ -132,8 +132,7 @@ func connectDB(ctx context.Context, filePath string) (*gorm.DB, error) { } db, err := gorm.Open(sqlite.Open(storeStr), &gorm.Config{ - Logger: logger.Default.LogMode(logger.Silent), - PrepareStmt: true, + Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { return nil, err diff --git a/management/server/group.go b/management/server/group.go index 6a70e26bd..daff55b0c 100644 --- a/management/server/group.go +++ b/management/server/group.go @@ -463,7 +463,7 @@ func validateDeleteGroup(ctx context.Context, transaction store.Store, group *ty if group.Issued == types.GroupIssuedIntegration { executingUser, err := transaction.GetUserByUserID(ctx, store.LockingStrengthShare, userID) if err != nil { - return err + return status.Errorf(status.Internal, "failed to get user") } if executingUser.Role != types.UserRoleAdmin || !executingUser.IsServiceUser { return status.Errorf(status.PermissionDenied, "only service users with admin power can delete integration group") @@ -505,7 +505,7 @@ func validateDeleteGroup(ctx context.Context, transaction store.Store, group *ty func checkGroupLinkedToSettings(ctx context.Context, transaction store.Store, group *types.Group) error { dnsSettings, err := transaction.GetAccountDNSSettings(ctx, store.LockingStrengthShare, group.AccountID) if err != nil { - return err + return status.Errorf(status.Internal, "failed to get DNS settings") } if slices.Contains(dnsSettings.DisabledManagementGroups, group.ID) { @@ -514,7 +514,7 @@ func checkGroupLinkedToSettings(ctx context.Context, transaction store.Store, gr settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthShare, group.AccountID) if err != nil { - return err + return status.Errorf(status.Internal, "failed to get account settings") } if settings.Extra != nil && slices.Contains(settings.Extra.IntegratedValidatorGroups, group.ID) { diff --git a/management/server/group_test.go b/management/server/group_test.go index cc90f187b..b21b5e834 100644 --- a/management/server/group_test.go +++ b/management/server/group_test.go @@ -29,7 +29,7 @@ func TestDefaultAccountManager_CreateGroup(t *testing.T) { _, account, err := initTestGroupAccount(am) if err != nil { - t.Error("failed to init testing account") + t.Fatalf("failed to init testing account: %s", err) } for _, group := range account.Groups { group.Issued = types.GroupIssuedIntegration @@ -59,12 +59,12 @@ func TestDefaultAccountManager_CreateGroup(t *testing.T) { func TestDefaultAccountManager_DeleteGroup(t *testing.T) { am, err := createManager(t) if err != nil { - t.Error("failed to create account manager") + t.Fatalf("failed to create account manager: %s", err) } _, account, err := initTestGroupAccount(am) if err != nil { - t.Error("failed to init testing account") + t.Fatalf("failed to init testing account: %s", err) } testCases := []struct { diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go index daa23d2ab..3d170afa4 100644 --- a/management/server/grpcserver.go +++ b/management/server/grpcserver.go @@ -15,12 +15,13 @@ import ( log "github.com/sirupsen/logrus" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "google.golang.org/grpc/codes" + "google.golang.org/grpc/peer" "google.golang.org/grpc/status" "github.com/netbirdio/netbird/encryption" "github.com/netbirdio/netbird/management/proto" + "github.com/netbirdio/netbird/management/server/auth" nbContext "github.com/netbirdio/netbird/management/server/context" - "github.com/netbirdio/netbird/management/server/jwtclaims" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/settings" @@ -38,11 +39,10 @@ type GRPCServer struct { peersUpdateManager *PeersUpdateManager config *Config secretsManager SecretsManager - jwtValidator jwtclaims.JWTValidator - jwtClaimsExtractor *jwtclaims.ClaimsExtractor appMetrics telemetry.AppMetrics ephemeralManager *EphemeralManager peerLocks sync.Map + authManager auth.Manager } // NewServer creates a new Management server @@ -55,29 +55,13 @@ func NewServer( secretsManager SecretsManager, appMetrics telemetry.AppMetrics, ephemeralManager *EphemeralManager, + authManager auth.Manager, ) (*GRPCServer, error) { key, err := wgtypes.GeneratePrivateKey() if err != nil { return nil, err } - var jwtValidator jwtclaims.JWTValidator - - if config.HttpConfig != nil && config.HttpConfig.AuthIssuer != "" && config.HttpConfig.AuthAudience != "" && validateURL(config.HttpConfig.AuthKeysLocation) { - jwtValidator, err = jwtclaims.NewJWTValidator( - ctx, - config.HttpConfig.AuthIssuer, - config.GetAuthAudiences(), - config.HttpConfig.AuthKeysLocation, - config.HttpConfig.IdpSignKeyRefreshEnabled, - ) - if err != nil { - return nil, status.Errorf(codes.Internal, "unable to create new jwt middleware, err: %v", err) - } - } else { - log.WithContext(ctx).Debug("unable to use http config to create new jwt middleware") - } - if appMetrics != nil { // update gauge based on number of connected peers which is equal to open gRPC streams err = appMetrics.GRPCMetrics().RegisterConnectedStreams(func() int64 { @@ -88,16 +72,6 @@ func NewServer( } } - var audience, userIDClaim string - if config.HttpConfig != nil { - audience = config.HttpConfig.AuthAudience - userIDClaim = config.HttpConfig.AuthUserIDClaim - } - jwtClaimsExtractor := jwtclaims.NewClaimsExtractor( - jwtclaims.WithAudience(audience), - jwtclaims.WithUserIDClaim(userIDClaim), - ) - return &GRPCServer{ wgKey: key, // peerKey -> event channel @@ -106,14 +80,25 @@ func NewServer( settingsManager: settingsManager, config: config, secretsManager: secretsManager, - jwtValidator: jwtValidator, - jwtClaimsExtractor: jwtClaimsExtractor, + authManager: authManager, appMetrics: appMetrics, ephemeralManager: ephemeralManager, }, nil } func (s *GRPCServer) GetServerKey(ctx context.Context, req *proto.Empty) (*proto.ServerKeyResponse, error) { + ip := "" + p, ok := peer.FromContext(ctx) + if ok { + ip = p.Addr.String() + } + + log.WithContext(ctx).Tracef("GetServerKey request from %s", ip) + start := time.Now() + defer func() { + log.WithContext(ctx).Tracef("GetServerKey from %s took %v", ip, time.Since(start)) + }() + // todo introduce something more meaningful with the key expiration/rotation if s.appMetrics != nil { s.appMetrics.GRPCMetrics().CountGetKeyRequest() @@ -208,6 +193,8 @@ func (s *GRPCServer) Sync(req *proto.EncryptedMessage, srv proto.ManagementServi unlock() unlock = nil + log.WithContext(ctx).Debugf("Sync: took %v", time.Since(reqStart)) + return s.handleUpdates(ctx, accountID, peerKey, peer, updates, srv) } @@ -279,26 +266,37 @@ func (s *GRPCServer) cancelPeerRoutines(ctx context.Context, accountID string, p } func (s *GRPCServer) validateToken(ctx context.Context, jwtToken string) (string, error) { - if s.jwtValidator == nil { - return "", status.Error(codes.Internal, "no jwt validator set") + if s.authManager == nil { + return "", status.Errorf(codes.Internal, "missing auth manager") } - token, err := s.jwtValidator.ValidateAndParse(ctx, jwtToken) + userAuth, token, err := s.authManager.ValidateAndParseToken(ctx, jwtToken) if err != nil { return "", status.Errorf(codes.InvalidArgument, "invalid jwt token, err: %v", err) } - claims := s.jwtClaimsExtractor.FromToken(token) + // we need to call this method because if user is new, we will automatically add it to existing or create a new account - _, _, err = s.accountManager.GetAccountIDFromToken(ctx, claims) + accountId, _, err := s.accountManager.GetAccountIDFromUserAuth(ctx, userAuth) if err != nil { return "", status.Errorf(codes.Internal, "unable to fetch account with claims, err: %v", err) } - if err := s.accountManager.CheckUserAccessByJWTGroups(ctx, claims); err != nil { + if userAuth.AccountId != accountId { + log.WithContext(ctx).Debugf("gRPC server sets accountId from ensure, before %s, now %s", userAuth.AccountId, accountId) + userAuth.AccountId = accountId + } + + userAuth, err = s.authManager.EnsureUserAccessByJWTGroups(ctx, userAuth, token) + if err != nil { return "", status.Error(codes.PermissionDenied, err.Error()) } - return claims.UserId, nil + err = s.accountManager.SyncUserJWTGroups(ctx, userAuth) + if err != nil { + log.WithContext(ctx).Errorf("gRPC server failed to sync user JWT groups: %s", err) + } + + return userAuth.UserId, nil } func (s *GRPCServer) acquirePeerLockByUID(ctx context.Context, uniqueID string) (unlock func()) { @@ -379,7 +377,7 @@ func extractPeerMeta(ctx context.Context, meta *proto.PeerSystemMeta) nbpeer.Pee Platform: meta.GetPlatform(), OS: meta.GetOS(), OSVersion: osVersion, - WtVersion: meta.GetWiretrusteeVersion(), + WtVersion: meta.GetNetbirdVersion(), UIVersion: meta.GetUiVersion(), KernelVersion: meta.GetKernelVersion(), NetworkAddresses: networkAddresses, @@ -466,6 +464,7 @@ func (s *GRPCServer) Login(ctx context.Context, req *proto.EncryptedMessage) (*p UserID: userID, SetupKey: loginReq.GetSetupKey(), ConnectionIP: realIP, + ExtraDNSLabels: loginReq.GetDnsLabels(), }) if err != nil { log.WithContext(ctx).Warnf("failed logging in peer %s: %s", peerKey, err) @@ -487,9 +486,9 @@ func (s *GRPCServer) Login(ctx context.Context, req *proto.EncryptedMessage) (*p // if peer has reached this point then it has logged in loginResp := &proto.LoginResponse{ - WiretrusteeConfig: toWiretrusteeConfig(s.config, nil, relayToken), - PeerConfig: toPeerConfig(peer, netMap.Network, s.accountManager.GetDNSDomain(), false), - Checks: toProtocolChecks(ctx, postureChecks), + NetbirdConfig: toNetbirdConfig(s.config, nil, relayToken), + PeerConfig: toPeerConfig(peer, netMap.Network, s.accountManager.GetDNSDomain(), false), + Checks: toProtocolChecks(ctx, postureChecks), } encryptedResp, err := encryption.EncryptMessage(peerKey, s.wgKey, loginResp) if err != nil { @@ -545,7 +544,7 @@ func ToResponseProto(configProto Protocol) proto.HostConfig_Protocol { } } -func toWiretrusteeConfig(config *Config, turnCredentials *Token, relayToken *Token) *proto.WiretrusteeConfig { +func toNetbirdConfig(config *Config, turnCredentials *Token, relayToken *Token) *proto.NetbirdConfig { if config == nil { return nil } @@ -593,7 +592,7 @@ func toWiretrusteeConfig(config *Config, turnCredentials *Token, relayToken *Tok } } - return &proto.WiretrusteeConfig{ + return &proto.NetbirdConfig{ Stuns: stuns, Turns: turns, Signal: &proto.HostConfig{ @@ -617,8 +616,8 @@ func toPeerConfig(peer *nbpeer.Peer, network *types.Network, dnsName string, dns func toSyncResponse(ctx context.Context, config *Config, peer *nbpeer.Peer, turnCredentials *Token, relayCredentials *Token, networkMap *types.NetworkMap, dnsName string, checks []*posture.Checks, dnsCache *DNSConfigCache, dnsResolutionOnRoutingPeerEnbled bool) *proto.SyncResponse { response := &proto.SyncResponse{ - WiretrusteeConfig: toWiretrusteeConfig(config, turnCredentials, relayCredentials), - PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, dnsResolutionOnRoutingPeerEnbled), + NetbirdConfig: toNetbirdConfig(config, turnCredentials, relayCredentials), + PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, dnsResolutionOnRoutingPeerEnbled), NetworkMap: &proto.NetworkMap{ Serial: networkMap.Network.CurrentSerial(), Routes: toProtocolRoutes(networkMap.Routes), @@ -715,6 +714,12 @@ func (s *GRPCServer) sendInitialSync(ctx context.Context, peerKey wgtypes.Key, p // This is used for initiating an Oauth 2 device authorization grant flow // which will be used by our clients to Login func (s *GRPCServer) GetDeviceAuthorizationFlow(ctx context.Context, req *proto.EncryptedMessage) (*proto.EncryptedMessage, error) { + log.WithContext(ctx).Tracef("GetDeviceAuthorizationFlow request for pubKey: %s", req.WgPubKey) + start := time.Now() + defer func() { + log.WithContext(ctx).Tracef("GetDeviceAuthorizationFlow for pubKey: %s took %v", req.WgPubKey, time.Since(start)) + }() + peerKey, err := wgtypes.ParseKey(req.GetWgPubKey()) if err != nil { errMSG := fmt.Sprintf("error while parsing peer's Wireguard public key %s on GetDeviceAuthorizationFlow request.", req.WgPubKey) @@ -767,6 +772,12 @@ func (s *GRPCServer) GetDeviceAuthorizationFlow(ctx context.Context, req *proto. // This is used for initiating an Oauth 2 pkce authorization grant flow // which will be used by our clients to Login func (s *GRPCServer) GetPKCEAuthorizationFlow(ctx context.Context, req *proto.EncryptedMessage) (*proto.EncryptedMessage, error) { + log.WithContext(ctx).Tracef("GetPKCEAuthorizationFlow request for pubKey: %s", req.WgPubKey) + start := time.Now() + defer func() { + log.WithContext(ctx).Tracef("GetPKCEAuthorizationFlow for pubKey %s took %v", req.WgPubKey, time.Since(start)) + }() + peerKey, err := wgtypes.ParseKey(req.GetWgPubKey()) if err != nil { errMSG := fmt.Sprintf("error while parsing peer's Wireguard public key %s on GetPKCEAuthorizationFlow request.", req.WgPubKey) diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index f53092415..83f45ef91 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -361,6 +361,12 @@ components: description: System serial number type: string example: "C02XJ0J0JGH7" + extra_dns_labels: + description: Extra DNS labels added to the peer + type: array + items: + type: string + example: "stage-host-1" required: - city_name - connected @@ -384,6 +390,7 @@ components: - ui_version - approval_required - serial_number + - extra_dns_labels AccessiblePeer: allOf: - $ref: '#/components/schemas/PeerMinimum' @@ -503,6 +510,10 @@ components: description: Indicate that the peer will be ephemeral or not type: boolean example: true + allow_extra_dns_labels: + description: Allow extra DNS labels to be added to the peer + type: boolean + example: true required: - id - key @@ -518,6 +529,7 @@ components: - updated_at - usage_limit - ephemeral + - allow_extra_dns_labels SetupKeyClear: allOf: - $ref: '#/components/schemas/SetupKeyBase' @@ -587,6 +599,10 @@ components: description: Indicate that the peer will be ephemeral or not type: boolean example: true + allow_extra_dns_labels: + description: Allow extra DNS labels to be added to the peer + type: boolean + example: true required: - name - type diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index 943d1b327..eb57d5d66 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -297,6 +297,9 @@ type CountryCode = string // CreateSetupKeyRequest defines model for CreateSetupKeyRequest. type CreateSetupKeyRequest struct { + // AllowExtraDnsLabels Allow extra DNS labels to be added to the peer + AllowExtraDnsLabels *bool `json:"allow_extra_dns_labels,omitempty"` + // AutoGroups List of group IDs to auto-assign to peers registered with this key AutoGroups []string `json:"auto_groups"` @@ -689,6 +692,9 @@ type Peer struct { // DnsLabel Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud DnsLabel string `json:"dns_label"` + // ExtraDnsLabels Extra DNS labels added to the peer + ExtraDnsLabels []string `json:"extra_dns_labels"` + // GeonameId Unique identifier from the GeoNames database for a specific geographical location. GeonameId int `json:"geoname_id"` @@ -767,6 +773,9 @@ type PeerBatch struct { // DnsLabel Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud DnsLabel string `json:"dns_label"` + // ExtraDnsLabels Extra DNS labels added to the peer + ExtraDnsLabels []string `json:"extra_dns_labels"` + // GeonameId Unique identifier from the GeoNames database for a specific geographical location. GeonameId int `json:"geoname_id"` @@ -1230,6 +1239,9 @@ type RulePortRange struct { // SetupKey defines model for SetupKey. type SetupKey struct { + // AllowExtraDnsLabels Allow extra DNS labels to be added to the peer + AllowExtraDnsLabels bool `json:"allow_extra_dns_labels"` + // AutoGroups List of group IDs to auto-assign to peers registered with this key AutoGroups []string `json:"auto_groups"` @@ -1275,6 +1287,9 @@ type SetupKey struct { // SetupKeyBase defines model for SetupKeyBase. type SetupKeyBase struct { + // AllowExtraDnsLabels Allow extra DNS labels to be added to the peer + AllowExtraDnsLabels bool `json:"allow_extra_dns_labels"` + // AutoGroups List of group IDs to auto-assign to peers registered with this key AutoGroups []string `json:"auto_groups"` @@ -1317,6 +1332,9 @@ type SetupKeyBase struct { // SetupKeyClear defines model for SetupKeyClear. type SetupKeyClear struct { + // AllowExtraDnsLabels Allow extra DNS labels to be added to the peer + AllowExtraDnsLabels bool `json:"allow_extra_dns_labels"` + // AutoGroups List of group IDs to auto-assign to peers registered with this key AutoGroups []string `json:"auto_groups"` diff --git a/management/server/http/handler.go b/management/server/http/handler.go index 1ddf10a6c..2b87c5f25 100644 --- a/management/server/http/handler.go +++ b/management/server/http/handler.go @@ -11,9 +11,9 @@ import ( "github.com/netbirdio/management-integrations/integrations" s "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/auth" "github.com/netbirdio/netbird/management/server/geolocation" nbgroups "github.com/netbirdio/netbird/management/server/groups" - "github.com/netbirdio/netbird/management/server/http/configs" "github.com/netbirdio/netbird/management/server/http/handlers/accounts" "github.com/netbirdio/netbird/management/server/http/handlers/dns" "github.com/netbirdio/netbird/management/server/http/handlers/events" @@ -26,7 +26,6 @@ import ( "github.com/netbirdio/netbird/management/server/http/handlers/users" "github.com/netbirdio/netbird/management/server/http/middleware" "github.com/netbirdio/netbird/management/server/integrated_validator" - "github.com/netbirdio/netbird/management/server/jwtclaims" nbnetworks "github.com/netbirdio/netbird/management/server/networks" "github.com/netbirdio/netbird/management/server/networks/resources" "github.com/netbirdio/netbird/management/server/networks/routers" @@ -36,55 +35,51 @@ import ( const apiPrefix = "/api" // NewAPIHandler creates the Management service HTTP API handler registering all the available endpoints. -func NewAPIHandler(ctx context.Context, accountManager s.AccountManager, networksManager nbnetworks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager nbgroups.Manager, LocationManager geolocation.Geolocation, jwtValidator jwtclaims.JWTValidator, appMetrics telemetry.AppMetrics, authCfg configs.AuthCfg, integratedValidator integrated_validator.IntegratedValidator) (http.Handler, error) { - claimsExtractor := jwtclaims.NewClaimsExtractor( - jwtclaims.WithAudience(authCfg.Audience), - jwtclaims.WithUserIDClaim(authCfg.UserIDClaim), - ) +func NewAPIHandler( + ctx context.Context, + accountManager s.AccountManager, + networksManager nbnetworks.Manager, + resourceManager resources.Manager, + routerManager routers.Manager, + groupsManager nbgroups.Manager, + LocationManager geolocation.Geolocation, + authManager auth.Manager, + appMetrics telemetry.AppMetrics, + config *s.Config, + integratedValidator integrated_validator.IntegratedValidator) (http.Handler, error) { authMiddleware := middleware.NewAuthMiddleware( - accountManager.GetAccountInfoFromPAT, - jwtValidator.ValidateAndParse, - accountManager.MarkPATUsed, - accountManager.CheckUserAccessByJWTGroups, - claimsExtractor, - authCfg.Audience, - authCfg.UserIDClaim, + authManager, + accountManager.GetAccountIDFromUserAuth, + accountManager.SyncUserJWTGroups, ) corsMiddleware := cors.AllowAll() - claimsExtractor = jwtclaims.NewClaimsExtractor( - jwtclaims.WithAudience(authCfg.Audience), - jwtclaims.WithUserIDClaim(authCfg.UserIDClaim), - ) - - acMiddleware := middleware.NewAccessControl( - authCfg.Audience, - authCfg.UserIDClaim, - accountManager.GetUser) + acMiddleware := middleware.NewAccessControl(accountManager.GetUserFromUserAuth) rootRouter := mux.NewRouter() metricsMiddleware := appMetrics.HTTPMiddleware() prefix := apiPrefix router := rootRouter.PathPrefix(prefix).Subrouter() + router.Use(metricsMiddleware.Handler, corsMiddleware.Handler, authMiddleware.Handler, acMiddleware.Handler) - if _, err := integrations.RegisterHandlers(ctx, prefix, router, accountManager, claimsExtractor, integratedValidator, appMetrics.GetMeter()); err != nil { + if _, err := integrations.RegisterHandlers(ctx, prefix, router, accountManager, integratedValidator, appMetrics.GetMeter()); err != nil { return nil, fmt.Errorf("register integrations endpoints: %w", err) } - accounts.AddEndpoints(accountManager, authCfg, router) - peers.AddEndpoints(accountManager, authCfg, router) - users.AddEndpoints(accountManager, authCfg, router) - setup_keys.AddEndpoints(accountManager, authCfg, router) - policies.AddEndpoints(accountManager, LocationManager, authCfg, router) - groups.AddEndpoints(accountManager, authCfg, router) - routes.AddEndpoints(accountManager, authCfg, router) - dns.AddEndpoints(accountManager, authCfg, router) - events.AddEndpoints(accountManager, authCfg, router) - networks.AddEndpoints(networksManager, resourceManager, routerManager, groupsManager, accountManager, accountManager.GetAccountIDFromToken, authCfg, router) + accounts.AddEndpoints(accountManager, router) + peers.AddEndpoints(accountManager, router) + users.AddEndpoints(accountManager, router) + setup_keys.AddEndpoints(accountManager, router) + policies.AddEndpoints(accountManager, LocationManager, router) + groups.AddEndpoints(accountManager, router) + routes.AddEndpoints(accountManager, router) + dns.AddEndpoints(accountManager, router) + events.AddEndpoints(accountManager, router) + networks.AddEndpoints(networksManager, resourceManager, routerManager, groupsManager, accountManager, router) return rootRouter, nil } diff --git a/management/server/http/handlers/accounts/accounts_handler.go b/management/server/http/handlers/accounts/accounts_handler.go index a23628cdc..bc0054a7f 100644 --- a/management/server/http/handlers/accounts/accounts_handler.go +++ b/management/server/http/handlers/accounts/accounts_handler.go @@ -9,47 +9,42 @@ import ( "github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server/account" + nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/configs" "github.com/netbirdio/netbird/management/server/http/util" - "github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/types" ) // handler is a handler that handles the server.Account HTTP endpoints type handler struct { - accountManager server.AccountManager - claimsExtractor *jwtclaims.ClaimsExtractor + accountManager server.AccountManager } -func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) { - accountsHandler := newHandler(accountManager, authCfg) +func AddEndpoints(accountManager server.AccountManager, router *mux.Router) { + accountsHandler := newHandler(accountManager) router.HandleFunc("/accounts/{accountId}", accountsHandler.updateAccount).Methods("PUT", "OPTIONS") router.HandleFunc("/accounts/{accountId}", accountsHandler.deleteAccount).Methods("DELETE", "OPTIONS") router.HandleFunc("/accounts", accountsHandler.getAllAccounts).Methods("GET", "OPTIONS") } // newHandler creates a new handler HTTP handler -func newHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *handler { +func newHandler(accountManager server.AccountManager) *handler { return &handler{ accountManager: accountManager, - claimsExtractor: jwtclaims.NewClaimsExtractor( - jwtclaims.WithAudience(authCfg.Audience), - jwtclaims.WithUserIDClaim(authCfg.UserIDClaim), - ), } } // getAllAccounts is HTTP GET handler that returns a list of accounts. Effectively returns just a single account. func (h *handler) getAllAccounts(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId + settings, err := h.accountManager.GetAccountSettings(r.Context(), accountID, userID) if err != nil { util.WriteError(r.Context(), err, w) @@ -62,13 +57,14 @@ func (h *handler) getAllAccounts(w http.ResponseWriter, r *http.Request) { // updateAccount is HTTP PUT handler that updates the provided account. Updates only account settings (server.Settings) func (h *handler) updateAccount(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - _, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + _, userID := userAuth.AccountId, userAuth.UserId + vars := mux.Vars(r) accountID := vars["accountId"] if len(accountID) == 0 { @@ -125,7 +121,12 @@ func (h *handler) updateAccount(w http.ResponseWriter, r *http.Request) { // deleteAccount is a HTTP DELETE handler to delete an account func (h *handler) deleteAccount(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + vars := mux.Vars(r) targetAccountID := vars["accountId"] if len(targetAccountID) == 0 { @@ -133,7 +134,7 @@ func (h *handler) deleteAccount(w http.ResponseWriter, r *http.Request) { return } - err := h.accountManager.DeleteAccount(r.Context(), targetAccountID, claims.UserId) + err = h.accountManager.DeleteAccount(r.Context(), targetAccountID, userAuth.UserId) if err != nil { util.WriteError(r.Context(), err, w) return diff --git a/management/server/http/handlers/accounts/accounts_handler_test.go b/management/server/http/handlers/accounts/accounts_handler_test.go index e8a599863..a8d57a13f 100644 --- a/management/server/http/handlers/accounts/accounts_handler_test.go +++ b/management/server/http/handlers/accounts/accounts_handler_test.go @@ -13,19 +13,16 @@ import ( "github.com/gorilla/mux" "github.com/stretchr/testify/assert" + nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/mock_server" "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/types" ) -func initAccountsTestData(account *types.Account, admin *types.User) *handler { +func initAccountsTestData(account *types.Account) *handler { return &handler{ accountManager: &mock_server.MockAccountManager{ - GetAccountIDFromTokenFunc: func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) { - return account.Id, admin.Id, nil - }, GetAccountSettingsFunc: func(ctx context.Context, accountID string, userID string) (*types.Settings, error) { return account.Settings, nil }, @@ -44,15 +41,6 @@ func initAccountsTestData(account *types.Account, admin *types.User) *handler { return accCopy, nil }, }, - claimsExtractor: jwtclaims.NewClaimsExtractor( - jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims { - return jwtclaims.AuthorizationClaims{ - UserId: "test_user", - Domain: "hotmail.com", - AccountId: "test_account", - } - }), - ), } } @@ -75,7 +63,7 @@ func TestAccounts_AccountsHandler(t *testing.T) { PeerLoginExpiration: time.Hour, RegularUsersViewBlocked: true, }, - }, adminUser) + }) tt := []struct { name string @@ -191,6 +179,11 @@ func TestAccounts_AccountsHandler(t *testing.T) { t.Run(tc.name, func(t *testing.T) { recorder := httptest.NewRecorder() req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody) + req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{ + UserId: adminUser.Id, + AccountId: accountID, + Domain: "hotmail.com", + }) router := mux.NewRouter() router.HandleFunc("/api/accounts", handler.getAllAccounts).Methods("GET") diff --git a/management/server/http/handlers/dns/dns_settings_handler.go b/management/server/http/handlers/dns/dns_settings_handler.go index 112eee179..6ff938369 100644 --- a/management/server/http/handlers/dns/dns_settings_handler.go +++ b/management/server/http/handlers/dns/dns_settings_handler.go @@ -8,51 +8,44 @@ import ( log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/management/server" + nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/configs" "github.com/netbirdio/netbird/management/server/http/util" - "github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/types" ) // dnsSettingsHandler is a handler that returns the DNS settings of the account type dnsSettingsHandler struct { - accountManager server.AccountManager - claimsExtractor *jwtclaims.ClaimsExtractor + accountManager server.AccountManager } -func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) { - addDNSSettingEndpoint(accountManager, authCfg, router) - addDNSNameserversEndpoint(accountManager, authCfg, router) +func AddEndpoints(accountManager server.AccountManager, router *mux.Router) { + addDNSSettingEndpoint(accountManager, router) + addDNSNameserversEndpoint(accountManager, router) } -func addDNSSettingEndpoint(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) { - dnsSettingsHandler := newDNSSettingsHandler(accountManager, authCfg) +func addDNSSettingEndpoint(accountManager server.AccountManager, router *mux.Router) { + dnsSettingsHandler := newDNSSettingsHandler(accountManager) router.HandleFunc("/dns/settings", dnsSettingsHandler.getDNSSettings).Methods("GET", "OPTIONS") router.HandleFunc("/dns/settings", dnsSettingsHandler.updateDNSSettings).Methods("PUT", "OPTIONS") } // newDNSSettingsHandler returns a new instance of dnsSettingsHandler handler -func newDNSSettingsHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *dnsSettingsHandler { - return &dnsSettingsHandler{ - accountManager: accountManager, - claimsExtractor: jwtclaims.NewClaimsExtractor( - jwtclaims.WithAudience(authCfg.Audience), - jwtclaims.WithUserIDClaim(authCfg.UserIDClaim), - ), - } +func newDNSSettingsHandler(accountManager server.AccountManager) *dnsSettingsHandler { + return &dnsSettingsHandler{accountManager: accountManager} } // getDNSSettings returns the DNS settings for the account func (h *dnsSettingsHandler) getDNSSettings(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { log.WithContext(r.Context()).Error(err) http.Redirect(w, r, "/", http.StatusInternalServerError) return } + accountID, userID := userAuth.AccountId, userAuth.UserId + dnsSettings, err := h.accountManager.GetDNSSettings(r.Context(), accountID, userID) if err != nil { util.WriteError(r.Context(), err, w) @@ -68,13 +61,14 @@ func (h *dnsSettingsHandler) getDNSSettings(w http.ResponseWriter, r *http.Reque // updateDNSSettings handles update to DNS settings of an account func (h *dnsSettingsHandler) updateDNSSettings(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId + var req api.PutApiDnsSettingsJSONRequestBody err = json.NewDecoder(r.Body).Decode(&req) if err != nil { diff --git a/management/server/http/handlers/dns/dns_settings_handler_test.go b/management/server/http/handlers/dns/dns_settings_handler_test.go index 9ca1dc032..ca81adf43 100644 --- a/management/server/http/handlers/dns/dns_settings_handler_test.go +++ b/management/server/http/handlers/dns/dns_settings_handler_test.go @@ -17,7 +17,8 @@ import ( "github.com/gorilla/mux" - "github.com/netbirdio/netbird/management/server/jwtclaims" + nbcontext "github.com/netbirdio/netbird/management/server/context" + "github.com/netbirdio/netbird/management/server/mock_server" ) @@ -52,19 +53,7 @@ func initDNSSettingsTestData() *dnsSettingsHandler { } return status.Errorf(status.InvalidArgument, "the dns settings provided are nil") }, - GetAccountIDFromTokenFunc: func(ctx context.Context, _ jwtclaims.AuthorizationClaims) (string, string, error) { - return testingDNSSettingsAccount.Id, testingDNSSettingsAccount.Users[testDNSSettingsUserID].Id, nil - }, }, - claimsExtractor: jwtclaims.NewClaimsExtractor( - jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims { - return jwtclaims.AuthorizationClaims{ - UserId: "test_user", - Domain: "hotmail.com", - AccountId: testDNSSettingsAccountID, - } - }), - ), } } @@ -118,6 +107,11 @@ func TestDNSSettingsHandlers(t *testing.T) { t.Run(tc.name, func(t *testing.T) { recorder := httptest.NewRecorder() req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody) + req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{ + UserId: testingDNSSettingsAccount.Users[testDNSSettingsUserID].Id, + AccountId: testingDNSSettingsAccount.Id, + Domain: testingDNSSettingsAccount.Domain, + }) router := mux.NewRouter() router.HandleFunc("/api/dns/settings", p.getDNSSettings).Methods("GET") diff --git a/management/server/http/handlers/dns/nameservers_handler.go b/management/server/http/handlers/dns/nameservers_handler.go index 09047e231..33d070477 100644 --- a/management/server/http/handlers/dns/nameservers_handler.go +++ b/management/server/http/handlers/dns/nameservers_handler.go @@ -10,21 +10,19 @@ import ( nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/server" + nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/configs" "github.com/netbirdio/netbird/management/server/http/util" - "github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/status" ) // nameserversHandler is the nameserver group handler of the account type nameserversHandler struct { - accountManager server.AccountManager - claimsExtractor *jwtclaims.ClaimsExtractor + accountManager server.AccountManager } -func addDNSNameserversEndpoint(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) { - nameserversHandler := newNameserversHandler(accountManager, authCfg) +func addDNSNameserversEndpoint(accountManager server.AccountManager, router *mux.Router) { + nameserversHandler := newNameserversHandler(accountManager) router.HandleFunc("/dns/nameservers", nameserversHandler.getAllNameservers).Methods("GET", "OPTIONS") router.HandleFunc("/dns/nameservers", nameserversHandler.createNameserverGroup).Methods("POST", "OPTIONS") router.HandleFunc("/dns/nameservers/{nsgroupId}", nameserversHandler.updateNameserverGroup).Methods("PUT", "OPTIONS") @@ -33,26 +31,21 @@ func addDNSNameserversEndpoint(accountManager server.AccountManager, authCfg con } // newNameserversHandler returns a new instance of nameserversHandler handler -func newNameserversHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *nameserversHandler { - return &nameserversHandler{ - accountManager: accountManager, - claimsExtractor: jwtclaims.NewClaimsExtractor( - jwtclaims.WithAudience(authCfg.Audience), - jwtclaims.WithUserIDClaim(authCfg.UserIDClaim), - ), - } +func newNameserversHandler(accountManager server.AccountManager) *nameserversHandler { + return &nameserversHandler{accountManager: accountManager} } // getAllNameservers returns the list of nameserver groups for the account func (h *nameserversHandler) getAllNameservers(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { log.WithContext(r.Context()).Error(err) http.Redirect(w, r, "/", http.StatusInternalServerError) return } + accountID, userID := userAuth.AccountId, userAuth.UserId + nsGroups, err := h.accountManager.ListNameServerGroups(r.Context(), accountID, userID) if err != nil { util.WriteError(r.Context(), err, w) @@ -69,13 +62,14 @@ func (h *nameserversHandler) getAllNameservers(w http.ResponseWriter, r *http.Re // createNameserverGroup handles nameserver group creation request func (h *nameserversHandler) createNameserverGroup(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId + var req api.PostApiDnsNameserversJSONRequestBody err = json.NewDecoder(r.Body).Decode(&req) if err != nil { @@ -102,13 +96,14 @@ func (h *nameserversHandler) createNameserverGroup(w http.ResponseWriter, r *htt // updateNameserverGroup handles update to a nameserver group identified by a given ID func (h *nameserversHandler) updateNameserverGroup(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId + nsGroupID := mux.Vars(r)["nsgroupId"] if len(nsGroupID) == 0 { util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid nameserver group ID"), w) @@ -153,13 +148,14 @@ func (h *nameserversHandler) updateNameserverGroup(w http.ResponseWriter, r *htt // deleteNameserverGroup handles nameserver group deletion request func (h *nameserversHandler) deleteNameserverGroup(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId + nsGroupID := mux.Vars(r)["nsgroupId"] if len(nsGroupID) == 0 { util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid nameserver group ID"), w) @@ -177,14 +173,14 @@ func (h *nameserversHandler) deleteNameserverGroup(w http.ResponseWriter, r *htt // getNameserverGroup handles a nameserver group Get request identified by ID func (h *nameserversHandler) getNameserverGroup(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { - log.WithContext(r.Context()).Error(err) - http.Redirect(w, r, "/", http.StatusInternalServerError) + util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId + nsGroupID := mux.Vars(r)["nsgroupId"] if len(nsGroupID) == 0 { util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid nameserver group ID"), w) diff --git a/management/server/http/handlers/dns/nameservers_handler_test.go b/management/server/http/handlers/dns/nameservers_handler_test.go index c6561e4d8..45283bc37 100644 --- a/management/server/http/handlers/dns/nameservers_handler_test.go +++ b/management/server/http/handlers/dns/nameservers_handler_test.go @@ -18,7 +18,8 @@ import ( "github.com/gorilla/mux" - "github.com/netbirdio/netbird/management/server/jwtclaims" + nbcontext "github.com/netbirdio/netbird/management/server/context" + "github.com/netbirdio/netbird/management/server/mock_server" ) @@ -81,19 +82,7 @@ func initNameserversTestData() *nameserversHandler { } return status.Errorf(status.NotFound, "nameserver group with ID %s was not found", nsGroupToSave.ID) }, - GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) { - return claims.AccountId, claims.UserId, nil - }, }, - claimsExtractor: jwtclaims.NewClaimsExtractor( - jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims { - return jwtclaims.AuthorizationClaims{ - UserId: "test_user", - Domain: "hotmail.com", - AccountId: testNSGroupAccountID, - } - }), - ), } } @@ -204,6 +193,11 @@ func TestNameserversHandlers(t *testing.T) { t.Run(tc.name, func(t *testing.T) { recorder := httptest.NewRecorder() req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody) + req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{ + UserId: "test_user", + AccountId: testNSGroupAccountID, + Domain: "hotmail.com", + }) router := mux.NewRouter() router.HandleFunc("/api/dns/nameservers/{nsgroupId}", p.getNameserverGroup).Methods("GET") diff --git a/management/server/http/handlers/events/events_handler.go b/management/server/http/handlers/events/events_handler.go index 62da59535..0fb2295a8 100644 --- a/management/server/http/handlers/events/events_handler.go +++ b/management/server/http/handlers/events/events_handler.go @@ -10,44 +10,37 @@ import ( "github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server/activity" + nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/configs" "github.com/netbirdio/netbird/management/server/http/util" - "github.com/netbirdio/netbird/management/server/jwtclaims" ) // handler HTTP handler type handler struct { - accountManager server.AccountManager - claimsExtractor *jwtclaims.ClaimsExtractor + accountManager server.AccountManager } -func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) { - eventsHandler := newHandler(accountManager, authCfg) +func AddEndpoints(accountManager server.AccountManager, router *mux.Router) { + eventsHandler := newHandler(accountManager) router.HandleFunc("/events", eventsHandler.getAllEvents).Methods("GET", "OPTIONS") } // newHandler creates a new events handler -func newHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *handler { - return &handler{ - accountManager: accountManager, - claimsExtractor: jwtclaims.NewClaimsExtractor( - jwtclaims.WithAudience(authCfg.Audience), - jwtclaims.WithUserIDClaim(authCfg.UserIDClaim), - ), - } +func newHandler(accountManager server.AccountManager) *handler { + return &handler{accountManager: accountManager} } // getAllEvents list of the given account func (h *handler) getAllEvents(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { log.WithContext(r.Context()).Error(err) http.Redirect(w, r, "/", http.StatusInternalServerError) return } + accountID, userID := userAuth.AccountId, userAuth.UserId + accountEvents, err := h.accountManager.GetEvents(r.Context(), accountID, userID) if err != nil { util.WriteError(r.Context(), err, w) diff --git a/management/server/http/handlers/events/events_handler_test.go b/management/server/http/handlers/events/events_handler_test.go index 17478aba3..3a643fe90 100644 --- a/management/server/http/handlers/events/events_handler_test.go +++ b/management/server/http/handlers/events/events_handler_test.go @@ -13,9 +13,10 @@ import ( "github.com/gorilla/mux" "github.com/stretchr/testify/assert" + nbcontext "github.com/netbirdio/netbird/management/server/context" + "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/mock_server" "github.com/netbirdio/netbird/management/server/types" ) @@ -29,22 +30,10 @@ func initEventsTestData(account string, events ...*activity.Event) *handler { } return []*activity.Event{}, nil }, - GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) { - return claims.AccountId, claims.UserId, nil - }, - GetUsersFromAccountFunc: func(_ context.Context, accountID, userID string) ([]*types.UserInfo, error) { - return make([]*types.UserInfo, 0), nil + GetUsersFromAccountFunc: func(_ context.Context, accountID, userID string) (map[string]*types.UserInfo, error) { + return make(map[string]*types.UserInfo), nil }, }, - claimsExtractor: jwtclaims.NewClaimsExtractor( - jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims { - return jwtclaims.AuthorizationClaims{ - UserId: "test_user", - Domain: "hotmail.com", - AccountId: "test_account", - } - }), - ), } } @@ -199,6 +188,11 @@ func TestEvents_GetEvents(t *testing.T) { t.Run(tc.name, func(t *testing.T) { recorder := httptest.NewRecorder() req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody) + req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{ + UserId: "test_user", + Domain: "hotmail.com", + AccountId: "test_account", + }) router := mux.NewRouter() router.HandleFunc("/api/events/", handler.getAllEvents).Methods("GET") diff --git a/management/server/http/handlers/groups/groups_handler.go b/management/server/http/handlers/groups/groups_handler.go index 0ecea7ec2..040c08b87 100644 --- a/management/server/http/handlers/groups/groups_handler.go +++ b/management/server/http/handlers/groups/groups_handler.go @@ -7,25 +7,23 @@ import ( "github.com/gorilla/mux" log "github.com/sirupsen/logrus" - "github.com/netbirdio/netbird/management/server/http/configs" + nbcontext "github.com/netbirdio/netbird/management/server/context" nbpeer "github.com/netbirdio/netbird/management/server/peer" - "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/http/util" - "github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/management/server/types" ) // handler is a handler that returns groups of the account type handler struct { - accountManager server.AccountManager - claimsExtractor *jwtclaims.ClaimsExtractor + accountManager server.AccountManager } -func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) { - groupsHandler := newHandler(accountManager, authCfg) +func AddEndpoints(accountManager server.AccountManager, router *mux.Router) { + groupsHandler := newHandler(accountManager) router.HandleFunc("/groups", groupsHandler.getAllGroups).Methods("GET", "OPTIONS") router.HandleFunc("/groups", groupsHandler.createGroup).Methods("POST", "OPTIONS") router.HandleFunc("/groups/{groupId}", groupsHandler.updateGroup).Methods("PUT", "OPTIONS") @@ -34,25 +32,21 @@ func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, } // newHandler creates a new groups handler -func newHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *handler { +func newHandler(accountManager server.AccountManager) *handler { return &handler{ accountManager: accountManager, - claimsExtractor: jwtclaims.NewClaimsExtractor( - jwtclaims.WithAudience(authCfg.Audience), - jwtclaims.WithUserIDClaim(authCfg.UserIDClaim), - ), } } // getAllGroups list for the account func (h *handler) getAllGroups(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { log.WithContext(r.Context()).Error(err) http.Redirect(w, r, "/", http.StatusInternalServerError) return } + accountID, userID := userAuth.AccountId, userAuth.UserId groups, err := h.accountManager.GetAllGroups(r.Context(), accountID, userID) if err != nil { @@ -76,13 +70,14 @@ func (h *handler) getAllGroups(w http.ResponseWriter, r *http.Request) { // updateGroup handles update to a group identified by a given ID func (h *handler) updateGroup(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId + vars := mux.Vars(r) groupID, ok := vars["groupId"] if !ok { @@ -165,13 +160,14 @@ func (h *handler) updateGroup(w http.ResponseWriter, r *http.Request) { // createGroup handles group creation request func (h *handler) createGroup(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId + var req api.PostApiGroupsJSONRequestBody err = json.NewDecoder(r.Body).Decode(&req) if err != nil { @@ -224,13 +220,14 @@ func (h *handler) createGroup(w http.ResponseWriter, r *http.Request) { // deleteGroup handles group deletion request func (h *handler) deleteGroup(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId + groupID := mux.Vars(r)["groupId"] if len(groupID) == 0 { util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid group ID"), w) @@ -239,8 +236,9 @@ func (h *handler) deleteGroup(w http.ResponseWriter, r *http.Request) { err = h.accountManager.DeleteGroup(r.Context(), accountID, userID, groupID) if err != nil { - _, ok := err.(*server.GroupLinkError) - if ok { + wrappedErr, ok := err.(interface{ Unwrap() []error }) + if ok && len(wrappedErr.Unwrap()) > 0 { + err = wrappedErr.Unwrap()[0] util.WriteErrorResponse(err.Error(), http.StatusBadRequest, w) return } @@ -253,12 +251,13 @@ func (h *handler) deleteGroup(w http.ResponseWriter, r *http.Request) { // getGroup returns a group func (h *handler) getGroup(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + + accountID, userID := userAuth.AccountId, userAuth.UserId groupID := mux.Vars(r)["groupId"] if len(groupID) == 0 { util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid group ID"), w) diff --git a/management/server/http/handlers/groups/groups_handler_test.go b/management/server/http/handlers/groups/groups_handler_test.go index 49805ca9b..c4b9e46ab 100644 --- a/management/server/http/handlers/groups/groups_handler_test.go +++ b/management/server/http/handlers/groups/groups_handler_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "net" @@ -13,13 +14,13 @@ import ( "testing" "github.com/gorilla/mux" - "github.com/magiconair/properties/assert" + "github.com/stretchr/testify/assert" "golang.org/x/exp/maps" "github.com/netbirdio/netbird/management/server" + nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/http/util" - "github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/mock_server" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/status" @@ -58,9 +59,6 @@ func initGroupTestData(initGroups ...*types.Group) *handler { return group, nil }, - GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) { - return claims.AccountId, claims.UserId, nil - }, GetGroupByNameFunc: func(ctx context.Context, groupName, _ string) (*types.Group, error) { if groupName == "All" { return &types.Group{ID: "id-all", Name: "All", Issued: types.GroupIssuedAPI}, nil @@ -73,10 +71,12 @@ func initGroupTestData(initGroups ...*types.Group) *handler { }, DeleteGroupFunc: func(_ context.Context, accountID, userId, groupID string) error { if groupID == "linked-grp" { - return &server.GroupLinkError{ + err := &server.GroupLinkError{ Resource: "something", Name: "linked-grp", } + var allErrors error + return errors.Join(allErrors, err) } if groupID == "invalid-grp" { return fmt.Errorf("internal error") @@ -84,15 +84,6 @@ func initGroupTestData(initGroups ...*types.Group) *handler { return nil }, }, - claimsExtractor: jwtclaims.NewClaimsExtractor( - jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims { - return jwtclaims.AuthorizationClaims{ - UserId: "test_user", - Domain: "hotmail.com", - AccountId: "test_id", - } - }), - ), } } @@ -131,6 +122,11 @@ func TestGetGroup(t *testing.T) { t.Run(tc.name, func(t *testing.T) { recorder := httptest.NewRecorder() req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody) + req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{ + UserId: "test_user", + Domain: "hotmail.com", + AccountId: "test_id", + }) router := mux.NewRouter() router.HandleFunc("/api/groups/{groupId}", p.getGroup).Methods("GET") @@ -252,6 +248,11 @@ func TestWriteGroup(t *testing.T) { t.Run(tc.name, func(t *testing.T) { recorder := httptest.NewRecorder() req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody) + req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{ + UserId: "test_user", + Domain: "hotmail.com", + AccountId: "test_id", + }) router := mux.NewRouter() router.HandleFunc("/api/groups", p.createGroup).Methods("POST") @@ -329,7 +330,11 @@ func TestDeleteGroup(t *testing.T) { t.Run(tc.name, func(t *testing.T) { recorder := httptest.NewRecorder() req := httptest.NewRequest(tc.requestType, tc.requestPath, nil) - + req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{ + UserId: "test_user", + Domain: "hotmail.com", + AccountId: "test_id", + }) router := mux.NewRouter() router.HandleFunc("/api/groups/{groupId}", p.deleteGroup).Methods("DELETE") router.ServeHTTP(recorder, req) diff --git a/management/server/http/handlers/networks/handler.go b/management/server/http/handlers/networks/handler.go index 316b93611..bb6b97267 100644 --- a/management/server/http/handlers/networks/handler.go +++ b/management/server/http/handlers/networks/handler.go @@ -7,13 +7,13 @@ import ( "net/http" "github.com/gorilla/mux" + log "github.com/sirupsen/logrus" s "github.com/netbirdio/netbird/management/server" + nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/groups" "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/configs" "github.com/netbirdio/netbird/management/server/http/util" - "github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/networks" "github.com/netbirdio/netbird/management/server/networks/resources" "github.com/netbirdio/netbird/management/server/networks/routers" @@ -30,16 +30,14 @@ type handler struct { routerManager routers.Manager accountManager s.AccountManager - groupsManager groups.Manager - extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) - claimsExtractor *jwtclaims.ClaimsExtractor + groupsManager groups.Manager } -func AddEndpoints(networksManager networks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager groups.Manager, accountManager s.AccountManager, extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error), authCfg configs.AuthCfg, router *mux.Router) { - addRouterEndpoints(routerManager, extractFromToken, authCfg, router) - addResourceEndpoints(resourceManager, groupsManager, extractFromToken, authCfg, router) +func AddEndpoints(networksManager networks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager groups.Manager, accountManager s.AccountManager, router *mux.Router) { + addRouterEndpoints(routerManager, router) + addResourceEndpoints(resourceManager, groupsManager, router) - networksHandler := newHandler(networksManager, resourceManager, routerManager, groupsManager, accountManager, extractFromToken, authCfg) + networksHandler := newHandler(networksManager, resourceManager, routerManager, groupsManager, accountManager) router.HandleFunc("/networks", networksHandler.getAllNetworks).Methods("GET", "OPTIONS") router.HandleFunc("/networks", networksHandler.createNetwork).Methods("POST", "OPTIONS") router.HandleFunc("/networks/{networkId}", networksHandler.getNetwork).Methods("GET", "OPTIONS") @@ -47,29 +45,25 @@ func AddEndpoints(networksManager networks.Manager, resourceManager resources.Ma router.HandleFunc("/networks/{networkId}", networksHandler.deleteNetwork).Methods("DELETE", "OPTIONS") } -func newHandler(networksManager networks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager groups.Manager, accountManager s.AccountManager, extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error), authCfg configs.AuthCfg) *handler { +func newHandler(networksManager networks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager groups.Manager, accountManager s.AccountManager) *handler { return &handler{ - networksManager: networksManager, - resourceManager: resourceManager, - routerManager: routerManager, - groupsManager: groupsManager, - accountManager: accountManager, - extractFromToken: extractFromToken, - claimsExtractor: jwtclaims.NewClaimsExtractor( - jwtclaims.WithAudience(authCfg.Audience), - jwtclaims.WithUserIDClaim(authCfg.UserIDClaim), - ), + networksManager: networksManager, + resourceManager: resourceManager, + routerManager: routerManager, + groupsManager: groupsManager, + accountManager: accountManager, } } func (h *handler) getAllNetworks(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.extractFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId + networks, err := h.networksManager.GetAllNetworks(r.Context(), accountID, userID) if err != nil { util.WriteError(r.Context(), err, w) @@ -104,12 +98,12 @@ func (h *handler) getAllNetworks(w http.ResponseWriter, r *http.Request) { } func (h *handler) createNetwork(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.extractFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId var req api.NetworkRequest err = json.NewDecoder(r.Body).Decode(&req) @@ -140,12 +134,12 @@ func (h *handler) createNetwork(w http.ResponseWriter, r *http.Request) { } func (h *handler) getNetwork(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.extractFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId vars := mux.Vars(r) networkID := vars["networkId"] @@ -178,13 +172,13 @@ func (h *handler) getNetwork(w http.ResponseWriter, r *http.Request) { } func (h *handler) updateNetwork(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.extractFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId vars := mux.Vars(r) networkID := vars["networkId"] if len(networkID) == 0 { @@ -228,13 +222,13 @@ func (h *handler) updateNetwork(w http.ResponseWriter, r *http.Request) { } func (h *handler) deleteNetwork(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.extractFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId vars := mux.Vars(r) networkID := vars["networkId"] if len(networkID) == 0 { @@ -281,7 +275,12 @@ func (h *handler) collectIDsInNetwork(ctx context.Context, accountID, userID, ne } if len(router.PeerGroups) > 0 { for _, groupID := range router.PeerGroups { - peerCounter += len(groups[groupID].Peers) + group, ok := groups[groupID] + if !ok { + log.WithContext(ctx).Warnf("group %s not found", groupID) + continue + } + peerCounter += len(group.Peers) } } } diff --git a/management/server/http/handlers/networks/resources_handler.go b/management/server/http/handlers/networks/resources_handler.go index f2dc8e3b8..fba7026e8 100644 --- a/management/server/http/handlers/networks/resources_handler.go +++ b/management/server/http/handlers/networks/resources_handler.go @@ -1,30 +1,26 @@ package networks import ( - "context" "encoding/json" "net/http" "github.com/gorilla/mux" + nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/groups" "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/configs" "github.com/netbirdio/netbird/management/server/http/util" - "github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/networks/resources" "github.com/netbirdio/netbird/management/server/networks/resources/types" ) type resourceHandler struct { - resourceManager resources.Manager - groupsManager groups.Manager - extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) - claimsExtractor *jwtclaims.ClaimsExtractor + resourceManager resources.Manager + groupsManager groups.Manager } -func addResourceEndpoints(resourcesManager resources.Manager, groupsManager groups.Manager, extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error), authCfg configs.AuthCfg, router *mux.Router) { - resourceHandler := newResourceHandler(resourcesManager, groupsManager, extractFromToken, authCfg) +func addResourceEndpoints(resourcesManager resources.Manager, groupsManager groups.Manager, router *mux.Router) { + resourceHandler := newResourceHandler(resourcesManager, groupsManager) router.HandleFunc("/networks/resources", resourceHandler.getAllResourcesInAccount).Methods("GET", "OPTIONS") router.HandleFunc("/networks/{networkId}/resources", resourceHandler.getAllResourcesInNetwork).Methods("GET", "OPTIONS") router.HandleFunc("/networks/{networkId}/resources", resourceHandler.createResource).Methods("POST", "OPTIONS") @@ -33,26 +29,21 @@ func addResourceEndpoints(resourcesManager resources.Manager, groupsManager grou router.HandleFunc("/networks/{networkId}/resources/{resourceId}", resourceHandler.deleteResource).Methods("DELETE", "OPTIONS") } -func newResourceHandler(resourceManager resources.Manager, groupsManager groups.Manager, extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error), authCfg configs.AuthCfg) *resourceHandler { +func newResourceHandler(resourceManager resources.Manager, groupsManager groups.Manager) *resourceHandler { return &resourceHandler{ - resourceManager: resourceManager, - groupsManager: groupsManager, - extractFromToken: extractFromToken, - claimsExtractor: jwtclaims.NewClaimsExtractor( - jwtclaims.WithAudience(authCfg.Audience), - jwtclaims.WithUserIDClaim(authCfg.UserIDClaim), - ), + resourceManager: resourceManager, + groupsManager: groupsManager, } } func (h *resourceHandler) getAllResourcesInNetwork(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.extractFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId networkID := mux.Vars(r)["networkId"] resources, err := h.resourceManager.GetAllResourcesInNetwork(r.Context(), accountID, userID, networkID) if err != nil { @@ -76,13 +67,14 @@ func (h *resourceHandler) getAllResourcesInNetwork(w http.ResponseWriter, r *htt util.WriteJSONObject(r.Context(), w, resourcesResponse) } func (h *resourceHandler) getAllResourcesInAccount(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.extractFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId + resources, err := h.resourceManager.GetAllResourcesInAccount(r.Context(), accountID, userID) if err != nil { util.WriteError(r.Context(), err, w) @@ -106,13 +98,14 @@ func (h *resourceHandler) getAllResourcesInAccount(w http.ResponseWriter, r *htt } func (h *resourceHandler) createResource(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.extractFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId + var req api.NetworkResourceRequest err = json.NewDecoder(r.Body).Decode(&req) if err != nil { @@ -144,13 +137,13 @@ func (h *resourceHandler) createResource(w http.ResponseWriter, r *http.Request) } func (h *resourceHandler) getResource(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.extractFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId networkID := mux.Vars(r)["networkId"] resourceID := mux.Vars(r)["resourceId"] resource, err := h.resourceManager.GetResource(r.Context(), accountID, userID, networkID, resourceID) @@ -171,13 +164,13 @@ func (h *resourceHandler) getResource(w http.ResponseWriter, r *http.Request) { } func (h *resourceHandler) updateResource(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.extractFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId var req api.NetworkResourceRequest err = json.NewDecoder(r.Body).Decode(&req) if err != nil { @@ -209,12 +202,12 @@ func (h *resourceHandler) updateResource(w http.ResponseWriter, r *http.Request) } func (h *resourceHandler) deleteResource(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.extractFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId networkID := mux.Vars(r)["networkId"] resourceID := mux.Vars(r)["resourceId"] diff --git a/management/server/http/handlers/networks/routers_handler.go b/management/server/http/handlers/networks/routers_handler.go index 7ca95d902..f98da4966 100644 --- a/management/server/http/handlers/networks/routers_handler.go +++ b/management/server/http/handlers/networks/routers_handler.go @@ -1,28 +1,24 @@ package networks import ( - "context" "encoding/json" "net/http" "github.com/gorilla/mux" + nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/configs" "github.com/netbirdio/netbird/management/server/http/util" - "github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/networks/routers" "github.com/netbirdio/netbird/management/server/networks/routers/types" ) type routersHandler struct { - routersManager routers.Manager - extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) - claimsExtractor *jwtclaims.ClaimsExtractor + routersManager routers.Manager } -func addRouterEndpoints(routersManager routers.Manager, extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error), authCfg configs.AuthCfg, router *mux.Router) { - routersHandler := newRoutersHandler(routersManager, extractFromToken, authCfg) +func addRouterEndpoints(routersManager routers.Manager, router *mux.Router) { + routersHandler := newRoutersHandler(routersManager) router.HandleFunc("/networks/{networkId}/routers", routersHandler.getAllRouters).Methods("GET", "OPTIONS") router.HandleFunc("/networks/{networkId}/routers", routersHandler.createRouter).Methods("POST", "OPTIONS") router.HandleFunc("/networks/{networkId}/routers/{routerId}", routersHandler.getRouter).Methods("GET", "OPTIONS") @@ -30,25 +26,21 @@ func addRouterEndpoints(routersManager routers.Manager, extractFromToken func(ct router.HandleFunc("/networks/{networkId}/routers/{routerId}", routersHandler.deleteRouter).Methods("DELETE", "OPTIONS") } -func newRoutersHandler(routersManager routers.Manager, extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error), authCfg configs.AuthCfg) *routersHandler { +func newRoutersHandler(routersManager routers.Manager) *routersHandler { return &routersHandler{ - routersManager: routersManager, - extractFromToken: extractFromToken, - claimsExtractor: jwtclaims.NewClaimsExtractor( - jwtclaims.WithAudience(authCfg.Audience), - jwtclaims.WithUserIDClaim(authCfg.UserIDClaim), - ), + routersManager: routersManager, } } func (h *routersHandler) getAllRouters(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.extractFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId + networkID := mux.Vars(r)["networkId"] routers, err := h.routersManager.GetAllRoutersInNetwork(r.Context(), accountID, userID, networkID) if err != nil { @@ -65,13 +57,14 @@ func (h *routersHandler) getAllRouters(w http.ResponseWriter, r *http.Request) { } func (h *routersHandler) createRouter(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.extractFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId + networkID := mux.Vars(r)["networkId"] var req api.NetworkRouterRequest err = json.NewDecoder(r.Body).Decode(&req) @@ -96,13 +89,14 @@ func (h *routersHandler) createRouter(w http.ResponseWriter, r *http.Request) { } func (h *routersHandler) getRouter(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.extractFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId + routerID := mux.Vars(r)["routerId"] networkID := mux.Vars(r)["networkId"] router, err := h.routersManager.GetRouter(r.Context(), accountID, userID, networkID, routerID) @@ -115,13 +109,14 @@ func (h *routersHandler) getRouter(w http.ResponseWriter, r *http.Request) { } func (h *routersHandler) updateRouter(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.extractFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId + var req api.NetworkRouterRequest err = json.NewDecoder(r.Body).Decode(&req) if err != nil { @@ -146,13 +141,13 @@ func (h *routersHandler) updateRouter(w http.ResponseWriter, r *http.Request) { } func (h *routersHandler) deleteRouter(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.extractFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId routerID := mux.Vars(r)["routerId"] networkID := mux.Vars(r)["networkId"] err = h.routersManager.DeleteRouter(r.Context(), accountID, userID, networkID, routerID) diff --git a/management/server/http/handlers/peers/peers_handler.go b/management/server/http/handlers/peers/peers_handler.go index cdd8026f2..709ba64d0 100644 --- a/management/server/http/handlers/peers/peers_handler.go +++ b/management/server/http/handlers/peers/peers_handler.go @@ -10,11 +10,10 @@ import ( log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/management/server" + nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/groups" "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/configs" "github.com/netbirdio/netbird/management/server/http/util" - "github.com/netbirdio/netbird/management/server/jwtclaims" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/types" @@ -22,12 +21,11 @@ import ( // Handler is a handler that returns peers of the account type Handler struct { - accountManager server.AccountManager - claimsExtractor *jwtclaims.ClaimsExtractor + accountManager server.AccountManager } -func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) { - peersHandler := NewHandler(accountManager, authCfg) +func AddEndpoints(accountManager server.AccountManager, router *mux.Router) { + peersHandler := NewHandler(accountManager) router.HandleFunc("/peers", peersHandler.GetAllPeers).Methods("GET", "OPTIONS") router.HandleFunc("/peers/{peerId}", peersHandler.HandlePeer). Methods("GET", "PUT", "DELETE", "OPTIONS") @@ -35,13 +33,9 @@ func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, } // NewHandler creates a new peers Handler -func NewHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *Handler { +func NewHandler(accountManager server.AccountManager) *Handler { return &Handler{ accountManager: accountManager, - claimsExtractor: jwtclaims.NewClaimsExtractor( - jwtclaims.WithAudience(authCfg.Audience), - jwtclaims.WithUserIDClaim(authCfg.UserIDClaim), - ), } } @@ -149,12 +143,13 @@ func (h *Handler) deletePeer(ctx context.Context, accountID, userID string, peer // HandlePeer handles all peer requests for GET, PUT and DELETE operations func (h *Handler) HandlePeer(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + + accountID, userID := userAuth.AccountId, userAuth.UserId vars := mux.Vars(r) peerID := vars["peerId"] if len(peerID) == 0 { @@ -179,13 +174,14 @@ func (h *Handler) HandlePeer(w http.ResponseWriter, r *http.Request) { // GetAllPeers returns a list of all peers associated with a provided account func (h *Handler) GetAllPeers(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId + peers, err := h.accountManager.GetPeers(r.Context(), accountID, userID) if err != nil { util.WriteError(r.Context(), err, w) @@ -230,13 +226,14 @@ func (h *Handler) setApprovalRequiredFlag(respBody []*api.PeerBatch, approvedPee // GetAccessiblePeers returns a list of all peers that the specified peer can connect to within the network. func (h *Handler) GetAccessiblePeers(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId + vars := mux.Vars(r) peerID := vars["peerId"] if len(peerID) == 0 { @@ -338,6 +335,7 @@ func toSinglePeerResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dnsD UserId: peer.UserID, UiVersion: peer.Meta.UIVersion, DnsLabel: fqdn(peer, dnsDomain), + ExtraDnsLabels: fqdnList(peer.ExtraDNSLabels, dnsDomain), LoginExpirationEnabled: peer.LoginExpirationEnabled, LastLogin: peer.GetLastLogin(), LoginExpired: peer.Status.LoginExpired, @@ -372,6 +370,7 @@ func toPeerListItemResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dn UserId: peer.UserID, UiVersion: peer.Meta.UIVersion, DnsLabel: fqdn(peer, dnsDomain), + ExtraDnsLabels: fqdnList(peer.ExtraDNSLabels, dnsDomain), LoginExpirationEnabled: peer.LoginExpirationEnabled, LastLogin: peer.GetLastLogin(), LoginExpired: peer.Status.LoginExpired, @@ -392,3 +391,11 @@ func fqdn(peer *nbpeer.Peer, dnsDomain string) string { return fqdn } } +func fqdnList(extraLabels []string, dnsDomain string) []string { + fqdnList := make([]string, 0, len(extraLabels)) + for _, label := range extraLabels { + fqdn := fmt.Sprintf("%s.%s", label, dnsDomain) + fqdnList = append(fqdnList, fqdn) + } + return fqdnList +} diff --git a/management/server/http/handlers/peers/peers_handler_test.go b/management/server/http/handlers/peers/peers_handler_test.go index 16065a677..8e07567c7 100644 --- a/management/server/http/handlers/peers/peers_handler_test.go +++ b/management/server/http/handlers/peers/peers_handler_test.go @@ -15,8 +15,8 @@ import ( "github.com/gorilla/mux" "golang.org/x/exp/maps" + nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/jwtclaims" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/types" @@ -25,16 +25,13 @@ import ( "github.com/netbirdio/netbird/management/server/mock_server" ) -type ctxKey string - const ( testPeerID = "test_peer" noUpdateChannelTestPeerID = "no-update-channel" - adminUser = "admin_user" - regularUser = "regular_user" - serviceUser = "service_user" - userIDKey ctxKey = "user_id" + adminUser = "admin_user" + regularUser = "regular_user" + serviceUser = "service_user" ) func initTestMetaData(peers ...*nbpeer.Peer) *Handler { @@ -146,8 +143,8 @@ func initTestMetaData(peers ...*nbpeer.Peer) *Handler { GetDNSDomainFunc: func() string { return "netbird.selfhosted" }, - GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) { - return claims.AccountId, claims.UserId, nil + GetAccountFunc: func(ctx context.Context, accountID string) (*types.Account, error) { + return account, nil }, GetAccountFunc: func(ctx context.Context, accountID string) (*types.Account, error) { return account, nil @@ -167,16 +164,6 @@ func initTestMetaData(peers ...*nbpeer.Peer) *Handler { return ok }, }, - claimsExtractor: jwtclaims.NewClaimsExtractor( - jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims { - userID := r.Context().Value(userIDKey).(string) - return jwtclaims.AuthorizationClaims{ - UserId: userID, - Domain: "hotmail.com", - AccountId: "test_id", - } - }), - ), } } @@ -267,8 +254,11 @@ func TestGetPeers(t *testing.T) { recorder := httptest.NewRecorder() req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody) - ctx := context.WithValue(context.Background(), userIDKey, "admin_user") - req = req.WithContext(ctx) + req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{ + UserId: "admin_user", + Domain: "hotmail.com", + AccountId: "test_id", + }) router := mux.NewRouter() router.HandleFunc("/api/peers/", p.GetAllPeers).Methods("GET") @@ -412,8 +402,11 @@ func TestGetAccessiblePeers(t *testing.T) { recorder := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/peers/%s/accessible-peers", tc.peerID), nil) - ctx := context.WithValue(context.Background(), userIDKey, tc.callerUserID) - req = req.WithContext(ctx) + req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{ + UserId: tc.callerUserID, + Domain: "hotmail.com", + AccountId: "test_id", + }) router := mux.NewRouter() router.HandleFunc("/api/peers/{peerId}/accessible-peers", p.GetAccessiblePeers).Methods("GET") diff --git a/management/server/http/handlers/policies/geolocation_handler_test.go b/management/server/http/handlers/policies/geolocation_handler_test.go index fc5839baa..fbdc324d6 100644 --- a/management/server/http/handlers/policies/geolocation_handler_test.go +++ b/management/server/http/handlers/policies/geolocation_handler_test.go @@ -13,9 +13,9 @@ import ( "github.com/gorilla/mux" "github.com/stretchr/testify/assert" + nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/geolocation" "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/mock_server" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/util" @@ -43,23 +43,11 @@ func initGeolocationTestData(t *testing.T) *geolocationsHandler { return &geolocationsHandler{ accountManager: &mock_server.MockAccountManager{ - GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) { - return claims.AccountId, claims.UserId, nil - }, GetUserByIDFunc: func(ctx context.Context, id string) (*types.User, error) { return types.NewAdminUser(id), nil }, }, geolocationManager: geo, - claimsExtractor: jwtclaims.NewClaimsExtractor( - jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims { - return jwtclaims.AuthorizationClaims{ - UserId: "test_user", - Domain: "hotmail.com", - AccountId: "test_id", - } - }), - ), } } @@ -112,6 +100,11 @@ func TestGetCitiesByCountry(t *testing.T) { t.Run(tc.name, func(t *testing.T) { recorder := httptest.NewRecorder() req := httptest.NewRequest(tc.requestType, tc.requestPath, nil) + req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{ + UserId: "test_user", + Domain: "hotmail.com", + AccountId: "test_id", + }) router := mux.NewRouter() router.HandleFunc("/api/locations/countries/{country}/cities", geolocationHandler.getCitiesByCountry).Methods("GET") @@ -200,6 +193,11 @@ func TestGetAllCountries(t *testing.T) { t.Run(tc.name, func(t *testing.T) { recorder := httptest.NewRecorder() req := httptest.NewRequest(tc.requestType, tc.requestPath, nil) + req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{ + UserId: "test_user", + Domain: "hotmail.com", + AccountId: "test_id", + }) router := mux.NewRouter() router.HandleFunc("/api/locations/countries", geolocationHandler.getAllCountries).Methods("GET") diff --git a/management/server/http/handlers/policies/geolocations_handler.go b/management/server/http/handlers/policies/geolocations_handler.go index 161d97402..c4868f879 100644 --- a/management/server/http/handlers/policies/geolocations_handler.go +++ b/management/server/http/handlers/policies/geolocations_handler.go @@ -7,11 +7,10 @@ import ( "github.com/gorilla/mux" "github.com/netbirdio/netbird/management/server" + nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/geolocation" "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/configs" "github.com/netbirdio/netbird/management/server/http/util" - "github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/status" ) @@ -23,24 +22,19 @@ var ( type geolocationsHandler struct { accountManager server.AccountManager geolocationManager geolocation.Geolocation - claimsExtractor *jwtclaims.ClaimsExtractor } -func addLocationsEndpoint(accountManager server.AccountManager, locationManager geolocation.Geolocation, authCfg configs.AuthCfg, router *mux.Router) { - locationHandler := newGeolocationsHandlerHandler(accountManager, locationManager, authCfg) +func addLocationsEndpoint(accountManager server.AccountManager, locationManager geolocation.Geolocation, router *mux.Router) { + locationHandler := newGeolocationsHandlerHandler(accountManager, locationManager) router.HandleFunc("/locations/countries", locationHandler.getAllCountries).Methods("GET", "OPTIONS") router.HandleFunc("/locations/countries/{country}/cities", locationHandler.getCitiesByCountry).Methods("GET", "OPTIONS") } // newGeolocationsHandlerHandler creates a new Geolocations handler -func newGeolocationsHandlerHandler(accountManager server.AccountManager, geolocationManager geolocation.Geolocation, authCfg configs.AuthCfg) *geolocationsHandler { +func newGeolocationsHandlerHandler(accountManager server.AccountManager, geolocationManager geolocation.Geolocation) *geolocationsHandler { return &geolocationsHandler{ accountManager: accountManager, geolocationManager: geolocationManager, - claimsExtractor: jwtclaims.NewClaimsExtractor( - jwtclaims.WithAudience(authCfg.Audience), - jwtclaims.WithUserIDClaim(authCfg.UserIDClaim), - ), } } @@ -104,12 +98,13 @@ func (l *geolocationsHandler) getCitiesByCountry(w http.ResponseWriter, r *http. } func (l *geolocationsHandler) authenticateUser(r *http.Request) error { - claims := l.claimsExtractor.FromRequestContext(r) - _, userID, err := l.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { return err } + _, userID := userAuth.AccountId, userAuth.UserId + user, err := l.accountManager.GetUserByID(r.Context(), userID) if err != nil { return err diff --git a/management/server/http/handlers/policies/policies_handler.go b/management/server/http/handlers/policies/policies_handler.go index a748e73b8..63fc8a03b 100644 --- a/management/server/http/handlers/policies/policies_handler.go +++ b/management/server/http/handlers/policies/policies_handler.go @@ -8,51 +8,46 @@ import ( "github.com/gorilla/mux" "github.com/netbirdio/netbird/management/server" + nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/geolocation" "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/configs" "github.com/netbirdio/netbird/management/server/http/util" - "github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/types" ) // handler is a handler that returns policy of the account type handler struct { - accountManager server.AccountManager - claimsExtractor *jwtclaims.ClaimsExtractor + accountManager server.AccountManager } -func AddEndpoints(accountManager server.AccountManager, locationManager geolocation.Geolocation, authCfg configs.AuthCfg, router *mux.Router) { - policiesHandler := newHandler(accountManager, authCfg) +func AddEndpoints(accountManager server.AccountManager, locationManager geolocation.Geolocation, router *mux.Router) { + policiesHandler := newHandler(accountManager) router.HandleFunc("/policies", policiesHandler.getAllPolicies).Methods("GET", "OPTIONS") router.HandleFunc("/policies", policiesHandler.createPolicy).Methods("POST", "OPTIONS") router.HandleFunc("/policies/{policyId}", policiesHandler.updatePolicy).Methods("PUT", "OPTIONS") router.HandleFunc("/policies/{policyId}", policiesHandler.getPolicy).Methods("GET", "OPTIONS") router.HandleFunc("/policies/{policyId}", policiesHandler.deletePolicy).Methods("DELETE", "OPTIONS") - addPostureCheckEndpoint(accountManager, locationManager, authCfg, router) + addPostureCheckEndpoint(accountManager, locationManager, router) } // newHandler creates a new policies handler -func newHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *handler { +func newHandler(accountManager server.AccountManager) *handler { return &handler{ accountManager: accountManager, - claimsExtractor: jwtclaims.NewClaimsExtractor( - jwtclaims.WithAudience(authCfg.Audience), - jwtclaims.WithUserIDClaim(authCfg.UserIDClaim), - ), } } // getAllPolicies list for the account func (h *handler) getAllPolicies(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId + listPolicies, err := h.accountManager.ListPolicies(r.Context(), accountID, userID) if err != nil { util.WriteError(r.Context(), err, w) @@ -80,13 +75,14 @@ func (h *handler) getAllPolicies(w http.ResponseWriter, r *http.Request) { // updatePolicy handles update to a policy identified by a given ID func (h *handler) updatePolicy(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId + vars := mux.Vars(r) policyID := vars["policyId"] if len(policyID) == 0 { @@ -105,13 +101,14 @@ func (h *handler) updatePolicy(w http.ResponseWriter, r *http.Request) { // createPolicy handles policy creation request func (h *handler) createPolicy(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId + h.savePolicy(w, r, accountID, userID, "") } @@ -306,13 +303,13 @@ func (h *handler) savePolicy(w http.ResponseWriter, r *http.Request, accountID s // deletePolicy handles policy deletion request func (h *handler) deletePolicy(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId vars := mux.Vars(r) policyID := vars["policyId"] if len(policyID) == 0 { @@ -330,13 +327,14 @@ func (h *handler) deletePolicy(w http.ResponseWriter, r *http.Request) { // getPolicy handles a group Get request identified by ID func (h *handler) getPolicy(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId + vars := mux.Vars(r) policyID := vars["policyId"] if len(policyID) == 0 { diff --git a/management/server/http/handlers/policies/policies_handler_test.go b/management/server/http/handlers/policies/policies_handler_test.go index 3e1be187c..6450295eb 100644 --- a/management/server/http/handlers/policies/policies_handler_test.go +++ b/management/server/http/handlers/policies/policies_handler_test.go @@ -10,17 +10,14 @@ import ( "strings" "testing" + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" + + nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/mock_server" "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/types" - - "github.com/gorilla/mux" - - "github.com/netbirdio/netbird/management/server/jwtclaims" - - "github.com/magiconair/properties/assert" - - "github.com/netbirdio/netbird/management/server/mock_server" ) func initPoliciesTestData(policies ...*types.Policy) *handler { @@ -47,9 +44,6 @@ func initPoliciesTestData(policies ...*types.Policy) *handler { GetAllGroupsFunc: func(ctx context.Context, accountID, userID string) ([]*types.Group, error) { return []*types.Group{{ID: "F"}, {ID: "G"}}, nil }, - GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) { - return claims.AccountId, claims.UserId, nil - }, GetAccountByIDFunc: func(ctx context.Context, accountID string, userID string) (*types.Account, error) { user := types.NewAdminUser(userID) return &types.Account{ @@ -68,15 +62,6 @@ func initPoliciesTestData(policies ...*types.Policy) *handler { }, nil }, }, - claimsExtractor: jwtclaims.NewClaimsExtractor( - jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims { - return jwtclaims.AuthorizationClaims{ - UserId: "test_user", - Domain: "hotmail.com", - AccountId: "test_id", - } - }), - ), } } @@ -118,6 +103,11 @@ func TestPoliciesGetPolicy(t *testing.T) { t.Run(tc.name, func(t *testing.T) { recorder := httptest.NewRecorder() req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody) + req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{ + UserId: "test_user", + Domain: "hotmail.com", + AccountId: "test_id", + }) router := mux.NewRouter() router.HandleFunc("/api/policies/{policyId}", p.getPolicy).Methods("GET") @@ -277,6 +267,11 @@ func TestPoliciesWritePolicy(t *testing.T) { t.Run(tc.name, func(t *testing.T) { recorder := httptest.NewRecorder() req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody) + req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{ + UserId: "test_user", + Domain: "hotmail.com", + AccountId: "test_id", + }) router := mux.NewRouter() router.HandleFunc("/api/policies", p.createPolicy).Methods("POST") diff --git a/management/server/http/handlers/policies/posture_checks_handler.go b/management/server/http/handlers/policies/posture_checks_handler.go index ce0d4878c..e6e58da58 100644 --- a/management/server/http/handlers/policies/posture_checks_handler.go +++ b/management/server/http/handlers/policies/posture_checks_handler.go @@ -7,11 +7,10 @@ import ( "github.com/gorilla/mux" "github.com/netbirdio/netbird/management/server" + nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/geolocation" "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/configs" "github.com/netbirdio/netbird/management/server/http/util" - "github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/status" ) @@ -20,40 +19,35 @@ import ( type postureChecksHandler struct { accountManager server.AccountManager geolocationManager geolocation.Geolocation - claimsExtractor *jwtclaims.ClaimsExtractor } -func addPostureCheckEndpoint(accountManager server.AccountManager, locationManager geolocation.Geolocation, authCfg configs.AuthCfg, router *mux.Router) { - postureCheckHandler := newPostureChecksHandler(accountManager, locationManager, authCfg) +func addPostureCheckEndpoint(accountManager server.AccountManager, locationManager geolocation.Geolocation, router *mux.Router) { + postureCheckHandler := newPostureChecksHandler(accountManager, locationManager) router.HandleFunc("/posture-checks", postureCheckHandler.getAllPostureChecks).Methods("GET", "OPTIONS") router.HandleFunc("/posture-checks", postureCheckHandler.createPostureCheck).Methods("POST", "OPTIONS") router.HandleFunc("/posture-checks/{postureCheckId}", postureCheckHandler.updatePostureCheck).Methods("PUT", "OPTIONS") router.HandleFunc("/posture-checks/{postureCheckId}", postureCheckHandler.getPostureCheck).Methods("GET", "OPTIONS") router.HandleFunc("/posture-checks/{postureCheckId}", postureCheckHandler.deletePostureCheck).Methods("DELETE", "OPTIONS") - addLocationsEndpoint(accountManager, locationManager, authCfg, router) + addLocationsEndpoint(accountManager, locationManager, router) } // newPostureChecksHandler creates a new PostureChecks handler -func newPostureChecksHandler(accountManager server.AccountManager, geolocationManager geolocation.Geolocation, authCfg configs.AuthCfg) *postureChecksHandler { +func newPostureChecksHandler(accountManager server.AccountManager, geolocationManager geolocation.Geolocation) *postureChecksHandler { return &postureChecksHandler{ accountManager: accountManager, geolocationManager: geolocationManager, - claimsExtractor: jwtclaims.NewClaimsExtractor( - jwtclaims.WithAudience(authCfg.Audience), - jwtclaims.WithUserIDClaim(authCfg.UserIDClaim), - ), } } // getAllPostureChecks list for the account func (p *postureChecksHandler) getAllPostureChecks(w http.ResponseWriter, r *http.Request) { - claims := p.claimsExtractor.FromRequestContext(r) - accountID, userID, err := p.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId listPostureChecks, err := p.accountManager.ListPostureChecks(r.Context(), accountID, userID) if err != nil { util.WriteError(r.Context(), err, w) @@ -70,13 +64,14 @@ func (p *postureChecksHandler) getAllPostureChecks(w http.ResponseWriter, r *htt // updatePostureCheck handles update to a posture check identified by a given ID func (p *postureChecksHandler) updatePostureCheck(w http.ResponseWriter, r *http.Request) { - claims := p.claimsExtractor.FromRequestContext(r) - accountID, userID, err := p.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId + vars := mux.Vars(r) postureChecksID := vars["postureCheckId"] if len(postureChecksID) == 0 { @@ -95,25 +90,26 @@ func (p *postureChecksHandler) updatePostureCheck(w http.ResponseWriter, r *http // createPostureCheck handles posture check creation request func (p *postureChecksHandler) createPostureCheck(w http.ResponseWriter, r *http.Request) { - claims := p.claimsExtractor.FromRequestContext(r) - accountID, userID, err := p.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId + p.savePostureChecks(w, r, accountID, userID, "") } // getPostureCheck handles a posture check Get request identified by ID func (p *postureChecksHandler) getPostureCheck(w http.ResponseWriter, r *http.Request) { - claims := p.claimsExtractor.FromRequestContext(r) - accountID, userID, err := p.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId vars := mux.Vars(r) postureChecksID := vars["postureCheckId"] if len(postureChecksID) == 0 { @@ -132,13 +128,13 @@ func (p *postureChecksHandler) getPostureCheck(w http.ResponseWriter, r *http.Re // deletePostureCheck handles posture check deletion request func (p *postureChecksHandler) deletePostureCheck(w http.ResponseWriter, r *http.Request) { - claims := p.claimsExtractor.FromRequestContext(r) - accountID, userID, err := p.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId vars := mux.Vars(r) postureChecksID := vars["postureCheckId"] if len(postureChecksID) == 0 { diff --git a/management/server/http/handlers/policies/posture_checks_handler_test.go b/management/server/http/handlers/policies/posture_checks_handler_test.go index 237687fd4..e3844caa2 100644 --- a/management/server/http/handlers/policies/posture_checks_handler_test.go +++ b/management/server/http/handlers/policies/posture_checks_handler_test.go @@ -14,9 +14,9 @@ import ( "github.com/gorilla/mux" "github.com/stretchr/testify/assert" + nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/geolocation" "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/mock_server" "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/status" @@ -66,20 +66,8 @@ func initPostureChecksTestData(postureChecks ...*posture.Checks) *postureChecksH } return accountPostureChecks, nil }, - GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) { - return claims.AccountId, claims.UserId, nil - }, }, geolocationManager: &geolocation.Mock{}, - claimsExtractor: jwtclaims.NewClaimsExtractor( - jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims { - return jwtclaims.AuthorizationClaims{ - UserId: "test_user", - Domain: "hotmail.com", - AccountId: "test_id", - } - }), - ), } } @@ -187,6 +175,11 @@ func TestGetPostureCheck(t *testing.T) { t.Run(tc.name, func(t *testing.T) { recorder := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/api/posture-checks/"+tc.id, tc.requestBody) + req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{ + UserId: "test_user", + Domain: "hotmail.com", + AccountId: "test_id", + }) router := mux.NewRouter() router.HandleFunc("/api/posture-checks/{postureCheckId}", p.getPostureCheck).Methods("GET") @@ -835,6 +828,11 @@ func TestPostureCheckUpdate(t *testing.T) { t.Run(tc.name, func(t *testing.T) { recorder := httptest.NewRecorder() req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody) + req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{ + UserId: "test_user", + Domain: "hotmail.com", + AccountId: "test_id", + }) defaultHandler := *p if tc.setupHandlerFunc != nil { diff --git a/management/server/http/handlers/routes/routes_handler.go b/management/server/http/handlers/routes/routes_handler.go index a29ba4562..0f0d24780 100644 --- a/management/server/http/handlers/routes/routes_handler.go +++ b/management/server/http/handlers/routes/routes_handler.go @@ -2,36 +2,30 @@ package routes import ( "encoding/json" - "fmt" "net/http" "net/netip" - "regexp" - "strings" "unicode/utf8" "github.com/gorilla/mux" "github.com/netbirdio/netbird/management/domain" "github.com/netbirdio/netbird/management/server" + nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/configs" "github.com/netbirdio/netbird/management/server/http/util" - "github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/route" ) -const maxDomains = 32 const failedToConvertRoute = "failed to convert route to response: %v" // handler is the routes handler of the account type handler struct { - accountManager server.AccountManager - claimsExtractor *jwtclaims.ClaimsExtractor + accountManager server.AccountManager } -func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) { - routesHandler := newHandler(accountManager, authCfg) +func AddEndpoints(accountManager server.AccountManager, router *mux.Router) { + routesHandler := newHandler(accountManager) router.HandleFunc("/routes", routesHandler.getAllRoutes).Methods("GET", "OPTIONS") router.HandleFunc("/routes", routesHandler.createRoute).Methods("POST", "OPTIONS") router.HandleFunc("/routes/{routeId}", routesHandler.updateRoute).Methods("PUT", "OPTIONS") @@ -40,25 +34,22 @@ func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, } // newHandler returns a new instance of routes handler -func newHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *handler { +func newHandler(accountManager server.AccountManager) *handler { return &handler{ accountManager: accountManager, - claimsExtractor: jwtclaims.NewClaimsExtractor( - jwtclaims.WithAudience(authCfg.Audience), - jwtclaims.WithUserIDClaim(authCfg.UserIDClaim), - ), } } // getAllRoutes returns the list of routes for the account func (h *handler) getAllRoutes(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId + routes, err := h.accountManager.ListRoutes(r.Context(), accountID, userID) if err != nil { util.WriteError(r.Context(), err, w) @@ -79,13 +70,14 @@ func (h *handler) getAllRoutes(w http.ResponseWriter, r *http.Request) { // createRoute handles route creation request func (h *handler) createRoute(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId + var req api.PostApiRoutesJSONRequestBody err = json.NewDecoder(r.Body).Decode(&req) if err != nil { @@ -102,7 +94,7 @@ func (h *handler) createRoute(w http.ResponseWriter, r *http.Request) { var networkType route.NetworkType var newPrefix netip.Prefix if req.Domains != nil { - d, err := validateDomains(*req.Domains) + d, err := domain.ValidateDomains(*req.Domains) if err != nil { util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid domains: %v", err), w) return @@ -176,13 +168,13 @@ func (h *handler) validateRoute(req api.PostApiRoutesJSONRequestBody) error { // updateRoute handles update to a route identified by a given ID func (h *handler) updateRoute(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId vars := mux.Vars(r) routeID := vars["routeId"] if len(routeID) == 0 { @@ -225,7 +217,7 @@ func (h *handler) updateRoute(w http.ResponseWriter, r *http.Request) { } if req.Domains != nil { - d, err := validateDomains(*req.Domains) + d, err := domain.ValidateDomains(*req.Domains) if err != nil { util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid domains: %v", err), w) return @@ -269,13 +261,13 @@ func (h *handler) updateRoute(w http.ResponseWriter, r *http.Request) { // deleteRoute handles route deletion request func (h *handler) deleteRoute(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId routeID := mux.Vars(r)["routeId"] if len(routeID) == 0 { util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid route ID"), w) @@ -293,13 +285,14 @@ func (h *handler) deleteRoute(w http.ResponseWriter, r *http.Request) { // getRoute handles a route Get request identified by ID func (h *handler) getRoute(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId + routeID := mux.Vars(r)["routeId"] if len(routeID) == 0 { util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid route ID"), w) @@ -350,34 +343,3 @@ func toRouteResponse(serverRoute *route.Route) (*api.Route, error) { } return route, nil } - -// validateDomains checks if each domain in the list is valid and returns a punycode-encoded DomainList. -func validateDomains(domains []string) (domain.List, error) { - if len(domains) == 0 { - return nil, fmt.Errorf("domains list is empty") - } - if len(domains) > maxDomains { - return nil, fmt.Errorf("domains list exceeds maximum allowed domains: %d", maxDomains) - } - - domainRegex := regexp.MustCompile(`^(?:\*\.)?(?:(?:xn--)?[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?\.)*(?:xn--)?[a-zA-Z0-9](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?$`) - - var domainList domain.List - - for _, d := range domains { - d := strings.ToLower(d) - - // handles length and idna conversion - punycode, err := domain.FromString(d) - if err != nil { - return domainList, fmt.Errorf("failed to convert domain to punycode: %s: %v", d, err) - } - - if !domainRegex.MatchString(string(punycode)) { - return domainList, fmt.Errorf("invalid domain format: %s", d) - } - - domainList = append(domainList, punycode) - } - return domainList, nil -} diff --git a/management/server/http/handlers/routes/routes_handler_test.go b/management/server/http/handlers/routes/routes_handler_test.go index 45c465587..ad1f8912d 100644 --- a/management/server/http/handlers/routes/routes_handler_test.go +++ b/management/server/http/handlers/routes/routes_handler_test.go @@ -11,21 +11,17 @@ import ( "net/netip" "testing" - "github.com/netbirdio/netbird/management/server/util" + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/netbirdio/netbird/management/server/http/api" - nbpeer "github.com/netbirdio/netbird/management/server/peer" - "github.com/netbirdio/netbird/management/server/status" - "github.com/netbirdio/netbird/management/server/types" - "github.com/netbirdio/netbird/route" - - "github.com/gorilla/mux" - "github.com/magiconair/properties/assert" - "github.com/netbirdio/netbird/management/domain" - "github.com/netbirdio/netbird/management/server/jwtclaims" + nbcontext "github.com/netbirdio/netbird/management/server/context" + "github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/mock_server" + "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/management/server/util" + "github.com/netbirdio/netbird/route" ) const ( @@ -62,32 +58,6 @@ var baseExistingRoute = &route.Route{ Groups: []string{existingGroupID}, } -var testingAccount = &types.Account{ - Id: testAccountID, - Domain: "hotmail.com", - Peers: map[string]*nbpeer.Peer{ - existingPeerID: { - Key: existingPeerKey, - IP: netip.MustParseAddr(existingPeerIP1).AsSlice(), - ID: existingPeerID, - Meta: nbpeer.PeerSystemMeta{ - GoOS: "linux", - }, - }, - nonLinuxExistingPeerID: { - Key: nonLinuxExistingPeerID, - IP: netip.MustParseAddr(existingPeerIP2).AsSlice(), - ID: nonLinuxExistingPeerID, - Meta: nbpeer.PeerSystemMeta{ - GoOS: "darwin", - }, - }, - }, - Users: map[string]*types.User{ - "test_user": types.NewAdminUser("test_user"), - }, -} - func initRoutesTestData() *handler { return &handler{ accountManager: &mock_server.MockAccountManager{ @@ -152,20 +122,7 @@ func initRoutesTestData() *handler { } return nil }, - GetAccountIDFromTokenFunc: func(_ context.Context, _ jwtclaims.AuthorizationClaims) (string, string, error) { - // return testingAccount, testingAccount.Users["test_user"], nil - return testingAccount.Id, testingAccount.Users["test_user"].Id, nil - }, }, - claimsExtractor: jwtclaims.NewClaimsExtractor( - jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims { - return jwtclaims.AuthorizationClaims{ - UserId: "test_user", - Domain: "hotmail.com", - AccountId: testAccountID, - } - }), - ), } } @@ -528,6 +485,11 @@ func TestRoutesHandlers(t *testing.T) { t.Run(tc.name, func(t *testing.T) { recorder := httptest.NewRecorder() req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody) + req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{ + UserId: "test_user", + Domain: "hotmail.com", + AccountId: testAccountID, + }) router := mux.NewRouter() router.HandleFunc("/api/routes/{routeId}", p.getRoute).Methods("GET") @@ -563,96 +525,6 @@ func TestRoutesHandlers(t *testing.T) { } } -func TestValidateDomains(t *testing.T) { - tests := []struct { - name string - domains []string - expected domain.List - wantErr bool - }{ - { - name: "Empty list", - domains: nil, - expected: nil, - wantErr: true, - }, - { - name: "Valid ASCII domain", - domains: []string{"sub.ex-ample.com"}, - expected: domain.List{"sub.ex-ample.com"}, - wantErr: false, - }, - { - name: "Valid Unicode domain", - domains: []string{"münchen.de"}, - expected: domain.List{"xn--mnchen-3ya.de"}, - wantErr: false, - }, - { - name: "Valid Unicode, all labels", - domains: []string{"中国.中国.中国"}, - expected: domain.List{"xn--fiqs8s.xn--fiqs8s.xn--fiqs8s"}, - wantErr: false, - }, - { - name: "With underscores", - domains: []string{"_jabber._tcp.gmail.com"}, - expected: domain.List{"_jabber._tcp.gmail.com"}, - wantErr: false, - }, - { - name: "Invalid domain format", - domains: []string{"-example.com"}, - expected: nil, - wantErr: true, - }, - { - name: "Invalid domain format 2", - domains: []string{"example.com-"}, - expected: nil, - wantErr: true, - }, - { - name: "Multiple domains valid and invalid", - domains: []string{"google.com", "invalid,nbdomain.com", "münchen.de"}, - expected: domain.List{"google.com"}, - wantErr: true, - }, - { - name: "Valid wildcard domain", - domains: []string{"*.example.com"}, - expected: domain.List{"*.example.com"}, - wantErr: false, - }, - { - name: "Wildcard with dot domain", - domains: []string{".*.example.com"}, - expected: nil, - wantErr: true, - }, - { - name: "Wildcard with dot domain", - domains: []string{".*.example.com"}, - expected: nil, - wantErr: true, - }, - { - name: "Invalid wildcard domain", - domains: []string{"a.*.example.com"}, - expected: nil, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := validateDomains(tt.domains) - assert.Equal(t, tt.wantErr, err != nil) - assert.Equal(t, got, tt.expected) - }) - } -} - func toApiRoute(t *testing.T, r *route.Route) *api.Route { t.Helper() diff --git a/management/server/http/handlers/setup_keys/setupkeys_handler.go b/management/server/http/handlers/setup_keys/setupkeys_handler.go index 67e296901..8095f43b0 100644 --- a/management/server/http/handlers/setup_keys/setupkeys_handler.go +++ b/management/server/http/handlers/setup_keys/setupkeys_handler.go @@ -3,28 +3,27 @@ package setup_keys import ( "context" "encoding/json" + "net/http" "time" "github.com/gorilla/mux" "github.com/netbirdio/netbird/management/server" + nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/configs" "github.com/netbirdio/netbird/management/server/http/util" - "github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/types" ) // handler is a handler that returns a list of setup keys of the account type handler struct { - accountManager server.AccountManager - claimsExtractor *jwtclaims.ClaimsExtractor + accountManager server.AccountManager } -func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) { - keysHandler := newHandler(accountManager, authCfg) +func AddEndpoints(accountManager server.AccountManager, router *mux.Router) { + keysHandler := newHandler(accountManager) router.HandleFunc("/setup-keys", keysHandler.getAllSetupKeys).Methods("GET", "OPTIONS") router.HandleFunc("/setup-keys", keysHandler.createSetupKey).Methods("POST", "OPTIONS") router.HandleFunc("/setup-keys/{keyId}", keysHandler.getSetupKey).Methods("GET", "OPTIONS") @@ -33,25 +32,21 @@ func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, } // newHandler creates a new setup key handler -func newHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *handler { +func newHandler(accountManager server.AccountManager) *handler { return &handler{ accountManager: accountManager, - claimsExtractor: jwtclaims.NewClaimsExtractor( - jwtclaims.WithAudience(authCfg.Audience), - jwtclaims.WithUserIDClaim(authCfg.UserIDClaim), - ), } } // createSetupKey is a POST requests that creates a new SetupKey func (h *handler) createSetupKey(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId req := &api.PostApiSetupKeysJSONRequestBody{} err = json.NewDecoder(r.Body).Decode(&req) if err != nil { @@ -86,8 +81,13 @@ func (h *handler) createSetupKey(w http.ResponseWriter, r *http.Request) { ephemeral = *req.Ephemeral } + var allowExtraDNSLabels bool + if req.AllowExtraDnsLabels != nil { + allowExtraDNSLabels = *req.AllowExtraDnsLabels + } + setupKey, err := h.accountManager.CreateSetupKey(r.Context(), accountID, req.Name, types.SetupKeyType(req.Type), expiresIn, - req.AutoGroups, req.UsageLimit, userID, ephemeral) + req.AutoGroups, req.UsageLimit, userID, ephemeral, allowExtraDNSLabels) if err != nil { util.WriteError(r.Context(), err, w) return @@ -102,12 +102,12 @@ func (h *handler) createSetupKey(w http.ResponseWriter, r *http.Request) { // getSetupKey is a GET request to get a SetupKey by ID func (h *handler) getSetupKey(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId vars := mux.Vars(r) keyID := vars["keyId"] @@ -127,13 +127,13 @@ func (h *handler) getSetupKey(w http.ResponseWriter, r *http.Request) { // updateSetupKey is a PUT request to update server.SetupKey func (h *handler) updateSetupKey(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId vars := mux.Vars(r) keyID := vars["keyId"] if len(keyID) == 0 { @@ -168,13 +168,13 @@ func (h *handler) updateSetupKey(w http.ResponseWriter, r *http.Request) { // getAllSetupKeys is a GET request that returns a list of SetupKey func (h *handler) getAllSetupKeys(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId setupKeys, err := h.accountManager.ListSetupKeys(r.Context(), accountID, userID) if err != nil { util.WriteError(r.Context(), err, w) @@ -190,13 +190,13 @@ func (h *handler) getAllSetupKeys(w http.ResponseWriter, r *http.Request) { } func (h *handler) deleteSetupKey(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId vars := mux.Vars(r) keyID := vars["keyId"] if len(keyID) == 0 { @@ -237,19 +237,20 @@ func ToResponseBody(key *types.SetupKey) *api.SetupKey { } return &api.SetupKey{ - Id: key.Id, - Key: key.KeySecret, - Name: key.Name, - Expires: key.GetExpiresAt(), - Type: string(key.Type), - Valid: key.IsValid(), - Revoked: key.Revoked, - UsedTimes: key.UsedTimes, - LastUsed: key.GetLastUsed(), - State: state, - AutoGroups: key.AutoGroups, - UpdatedAt: key.UpdatedAt, - UsageLimit: key.UsageLimit, - Ephemeral: key.Ephemeral, + Id: key.Id, + Key: key.KeySecret, + Name: key.Name, + Expires: key.GetExpiresAt(), + Type: string(key.Type), + Valid: key.IsValid(), + Revoked: key.Revoked, + UsedTimes: key.UsedTimes, + LastUsed: key.GetLastUsed(), + State: state, + AutoGroups: key.AutoGroups, + UpdatedAt: key.UpdatedAt, + UsageLimit: key.UsageLimit, + Ephemeral: key.Ephemeral, + AllowExtraDnsLabels: key.AllowExtraDNSLabels, } } diff --git a/management/server/http/handlers/setup_keys/setupkeys_handler_test.go b/management/server/http/handlers/setup_keys/setupkeys_handler_test.go index f56227c10..e9135469f 100644 --- a/management/server/http/handlers/setup_keys/setupkeys_handler_test.go +++ b/management/server/http/handlers/setup_keys/setupkeys_handler_test.go @@ -14,8 +14,8 @@ import ( "github.com/gorilla/mux" "github.com/stretchr/testify/assert" + nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/mock_server" "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/types" @@ -28,20 +28,16 @@ const ( notFoundSetupKeyID = "notFoundSetupKeyID" ) -func initSetupKeysTestMetaData(defaultKey *types.SetupKey, newKey *types.SetupKey, updatedSetupKey *types.SetupKey, - user *types.User, -) *handler { +func initSetupKeysTestMetaData(defaultKey *types.SetupKey, newKey *types.SetupKey, updatedSetupKey *types.SetupKey) *handler { return &handler{ accountManager: &mock_server.MockAccountManager{ - GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) { - return claims.AccountId, claims.UserId, nil - }, CreateSetupKeyFunc: func(_ context.Context, _ string, keyName string, typ types.SetupKeyType, _ time.Duration, _ []string, - _ int, _ string, ephemeral bool, + _ int, _ string, ephemeral bool, allowExtraDNSLabels bool, ) (*types.SetupKey, error) { if keyName == newKey.Name || typ != newKey.Type { nk := newKey.Copy() nk.Ephemeral = ephemeral + nk.AllowExtraDNSLabels = allowExtraDNSLabels return nk, nil } return nil, fmt.Errorf("failed creating setup key") @@ -75,15 +71,6 @@ func initSetupKeysTestMetaData(defaultKey *types.SetupKey, newKey *types.SetupKe return status.Errorf(status.NotFound, "key %s not found", keyID) }, }, - claimsExtractor: jwtclaims.NewClaimsExtractor( - jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims { - return jwtclaims.AuthorizationClaims{ - UserId: user.Id, - Domain: "hotmail.com", - AccountId: "testAccountId", - } - }), - ), } } @@ -94,7 +81,7 @@ func TestSetupKeysHandlers(t *testing.T) { adminUser := types.NewAdminUser("test_user") newSetupKey, plainKey := types.GenerateSetupKey(newSetupKeyName, types.SetupKeyReusable, 0, []string{"group-1"}, - types.SetupKeyUnlimitedUsage, true) + types.SetupKeyUnlimitedUsage, true, false) newSetupKey.Key = plainKey updatedDefaultSetupKey := defaultSetupKey.Copy() updatedDefaultSetupKey.AutoGroups = []string{"group-1"} @@ -170,12 +157,17 @@ func TestSetupKeysHandlers(t *testing.T) { }, } - handler := initSetupKeysTestMetaData(defaultSetupKey, newSetupKey, updatedDefaultSetupKey, adminUser) + handler := initSetupKeysTestMetaData(defaultSetupKey, newSetupKey, updatedDefaultSetupKey) for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { recorder := httptest.NewRecorder() req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody) + req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{ + UserId: adminUser.Id, + Domain: "hotmail.com", + AccountId: "testAccountId", + }) router := mux.NewRouter() router.HandleFunc("/api/setup-keys", handler.getAllSetupKeys).Methods("GET", "OPTIONS") diff --git a/management/server/http/handlers/users/pat_handler.go b/management/server/http/handlers/users/pat_handler.go index 7b93d2ae1..84fbef93e 100644 --- a/management/server/http/handlers/users/pat_handler.go +++ b/management/server/http/handlers/users/pat_handler.go @@ -7,22 +7,20 @@ import ( "github.com/gorilla/mux" "github.com/netbirdio/netbird/management/server" + nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/configs" "github.com/netbirdio/netbird/management/server/http/util" - "github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/types" ) // patHandler is the nameserver group handler of the account type patHandler struct { - accountManager server.AccountManager - claimsExtractor *jwtclaims.ClaimsExtractor + accountManager server.AccountManager } -func addUsersTokensEndpoint(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) { - tokenHandler := newPATsHandler(accountManager, authCfg) +func addUsersTokensEndpoint(accountManager server.AccountManager, router *mux.Router) { + tokenHandler := newPATsHandler(accountManager) router.HandleFunc("/users/{userId}/tokens", tokenHandler.getAllTokens).Methods("GET", "OPTIONS") router.HandleFunc("/users/{userId}/tokens", tokenHandler.createToken).Methods("POST", "OPTIONS") router.HandleFunc("/users/{userId}/tokens/{tokenId}", tokenHandler.getToken).Methods("GET", "OPTIONS") @@ -30,25 +28,21 @@ func addUsersTokensEndpoint(accountManager server.AccountManager, authCfg config } // newPATsHandler creates a new patHandler HTTP handler -func newPATsHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *patHandler { +func newPATsHandler(accountManager server.AccountManager) *patHandler { return &patHandler{ accountManager: accountManager, - claimsExtractor: jwtclaims.NewClaimsExtractor( - jwtclaims.WithAudience(authCfg.Audience), - jwtclaims.WithUserIDClaim(authCfg.UserIDClaim), - ), } } // getAllTokens is HTTP GET handler that returns a list of all personal access tokens for the given user func (h *patHandler) getAllTokens(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId vars := mux.Vars(r) targetUserID := vars["userId"] if len(userID) == 0 { @@ -72,13 +66,13 @@ func (h *patHandler) getAllTokens(w http.ResponseWriter, r *http.Request) { // getToken is HTTP GET handler that returns a personal access token for the given user func (h *patHandler) getToken(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId vars := mux.Vars(r) targetUserID := vars["userId"] if len(targetUserID) == 0 { @@ -103,13 +97,13 @@ func (h *patHandler) getToken(w http.ResponseWriter, r *http.Request) { // createToken is HTTP POST handler that creates a personal access token for the given user func (h *patHandler) createToken(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId vars := mux.Vars(r) targetUserID := vars["userId"] if len(targetUserID) == 0 { @@ -135,13 +129,13 @@ func (h *patHandler) createToken(w http.ResponseWriter, r *http.Request) { // deleteToken is HTTP DELETE handler that deletes a personal access token for the given user func (h *patHandler) deleteToken(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId vars := mux.Vars(r) targetUserID := vars["userId"] if len(targetUserID) == 0 { diff --git a/management/server/http/handlers/users/pat_handler_test.go b/management/server/http/handlers/users/pat_handler_test.go index 9388067a4..6593de64a 100644 --- a/management/server/http/handlers/users/pat_handler_test.go +++ b/management/server/http/handlers/users/pat_handler_test.go @@ -12,11 +12,12 @@ import ( "github.com/google/go-cmp/cmp" "github.com/gorilla/mux" - "github.com/netbirdio/netbird/management/server/util" "github.com/stretchr/testify/assert" + "github.com/netbirdio/netbird/management/server/util" + + nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/mock_server" "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/types" @@ -77,10 +78,6 @@ func initPATTestData() *patHandler { PersonalAccessToken: types.PersonalAccessToken{}, }, nil }, - - GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) { - return claims.AccountId, claims.UserId, nil - }, DeletePATFunc: func(_ context.Context, accountID string, initiatorUserID string, targetUserID string, tokenID string) error { if accountID != existingAccountID { return status.Errorf(status.NotFound, "account with ID %s not found", accountID) @@ -115,15 +112,6 @@ func initPATTestData() *patHandler { return []*types.PersonalAccessToken{testAccount.Users[existingUserID].PATs[existingTokenID], testAccount.Users[existingUserID].PATs["token2"]}, nil }, }, - claimsExtractor: jwtclaims.NewClaimsExtractor( - jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims { - return jwtclaims.AuthorizationClaims{ - UserId: existingUserID, - Domain: testDomain, - AccountId: existingAccountID, - } - }), - ), } } @@ -185,6 +173,11 @@ func TestTokenHandlers(t *testing.T) { t.Run(tc.name, func(t *testing.T) { recorder := httptest.NewRecorder() req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody) + req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{ + UserId: existingUserID, + Domain: testDomain, + AccountId: existingAccountID, + }) router := mux.NewRouter() router.HandleFunc("/api/users/{userId}/tokens", p.getAllTokens).Methods("GET") diff --git a/management/server/http/handlers/users/users_handler.go b/management/server/http/handlers/users/users_handler.go index 7380dd97e..3869f21f0 100644 --- a/management/server/http/handlers/users/users_handler.go +++ b/management/server/http/handlers/users/users_handler.go @@ -9,39 +9,33 @@ import ( log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/configs" "github.com/netbirdio/netbird/management/server/http/util" "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server" - "github.com/netbirdio/netbird/management/server/jwtclaims" + nbcontext "github.com/netbirdio/netbird/management/server/context" ) // handler is a handler that returns users of the account type handler struct { - accountManager server.AccountManager - claimsExtractor *jwtclaims.ClaimsExtractor + accountManager server.AccountManager } -func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) { - userHandler := newHandler(accountManager, authCfg) +func AddEndpoints(accountManager server.AccountManager, router *mux.Router) { + userHandler := newHandler(accountManager) router.HandleFunc("/users", userHandler.getAllUsers).Methods("GET", "OPTIONS") router.HandleFunc("/users/{userId}", userHandler.updateUser).Methods("PUT", "OPTIONS") router.HandleFunc("/users/{userId}", userHandler.deleteUser).Methods("DELETE", "OPTIONS") router.HandleFunc("/users", userHandler.createUser).Methods("POST", "OPTIONS") router.HandleFunc("/users/{userId}/invite", userHandler.inviteUser).Methods("POST", "OPTIONS") - addUsersTokensEndpoint(accountManager, authCfg, router) + addUsersTokensEndpoint(accountManager, router) } // newHandler creates a new UsersHandler HTTP handler -func newHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *handler { +func newHandler(accountManager server.AccountManager) *handler { return &handler{ accountManager: accountManager, - claimsExtractor: jwtclaims.NewClaimsExtractor( - jwtclaims.WithAudience(authCfg.Audience), - jwtclaims.WithUserIDClaim(authCfg.UserIDClaim), - ), } } @@ -52,13 +46,13 @@ func (h *handler) updateUser(w http.ResponseWriter, r *http.Request) { return } - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId vars := mux.Vars(r) targetUserID := vars["userId"] if len(targetUserID) == 0 { @@ -103,7 +97,7 @@ func (h *handler) updateUser(w http.ResponseWriter, r *http.Request) { util.WriteError(r.Context(), err, w) return } - util.WriteJSONObject(r.Context(), w, toUserResponse(newUser, claims.UserId)) + util.WriteJSONObject(r.Context(), w, toUserResponse(newUser, userID)) } // deleteUser is a DELETE request to delete a user @@ -113,13 +107,13 @@ func (h *handler) deleteUser(w http.ResponseWriter, r *http.Request) { return } - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId vars := mux.Vars(r) targetUserID := vars["userId"] if len(targetUserID) == 0 { @@ -143,12 +137,12 @@ func (h *handler) createUser(w http.ResponseWriter, r *http.Request) { return } - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId req := &api.PostApiUsersJSONRequestBody{} err = json.NewDecoder(r.Body).Decode(&req) @@ -184,7 +178,7 @@ func (h *handler) createUser(w http.ResponseWriter, r *http.Request) { util.WriteError(r.Context(), err, w) return } - util.WriteJSONObject(r.Context(), w, toUserResponse(newUser, claims.UserId)) + util.WriteJSONObject(r.Context(), w, toUserResponse(newUser, userID)) } // getAllUsers returns a list of users of the account this user belongs to. @@ -195,13 +189,13 @@ func (h *handler) getAllUsers(w http.ResponseWriter, r *http.Request) { return } - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId data, err := h.accountManager.GetUsersFromAccount(r.Context(), accountID, userID) if err != nil { util.WriteError(r.Context(), err, w) @@ -216,7 +210,7 @@ func (h *handler) getAllUsers(w http.ResponseWriter, r *http.Request) { continue } if serviceUser == "" { - users = append(users, toUserResponse(d, claims.UserId)) + users = append(users, toUserResponse(d, userID)) continue } @@ -227,7 +221,7 @@ func (h *handler) getAllUsers(w http.ResponseWriter, r *http.Request) { return } if includeServiceUser == d.IsServiceUser { - users = append(users, toUserResponse(d, claims.UserId)) + users = append(users, toUserResponse(d, userID)) } } @@ -242,12 +236,12 @@ func (h *handler) inviteUser(w http.ResponseWriter, r *http.Request) { return } - claims := h.claimsExtractor.FromRequestContext(r) - accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } + accountID, userID := userAuth.AccountId, userAuth.UserId vars := mux.Vars(r) targetUserID := vars["userId"] diff --git a/management/server/http/handlers/users/users_handler_test.go b/management/server/http/handlers/users/users_handler_test.go index 90081830a..a6a904a4c 100644 --- a/management/server/http/handlers/users/users_handler_test.go +++ b/management/server/http/handlers/users/users_handler_test.go @@ -13,8 +13,8 @@ import ( "github.com/gorilla/mux" "github.com/stretchr/testify/assert" + nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/mock_server" "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/types" @@ -52,7 +52,7 @@ var usersTestAccount = &types.Account{ Issued: types.UserIssuedAPI, }, nonDeletableServiceUserID: { - Id: serviceUserID, + Id: nonDeletableServiceUserID, Role: "admin", IsServiceUser: true, NonDeletable: true, @@ -64,16 +64,13 @@ var usersTestAccount = &types.Account{ func initUsersTestData() *handler { return &handler{ accountManager: &mock_server.MockAccountManager{ - GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) { - return usersTestAccount.Id, claims.UserId, nil - }, GetUserByIDFunc: func(ctx context.Context, id string) (*types.User, error) { return usersTestAccount.Users[id], nil }, - GetUsersFromAccountFunc: func(_ context.Context, accountID, userID string) ([]*types.UserInfo, error) { - users := make([]*types.UserInfo, 0) + GetUsersFromAccountFunc: func(_ context.Context, accountID, userID string) (map[string]*types.UserInfo, error) { + usersInfos := make(map[string]*types.UserInfo) for _, v := range usersTestAccount.Users { - users = append(users, &types.UserInfo{ + usersInfos[v.Id] = &types.UserInfo{ ID: v.Id, Role: string(v.Role), Name: "", @@ -81,9 +78,9 @@ func initUsersTestData() *handler { IsServiceUser: v.IsServiceUser, NonDeletable: v.NonDeletable, Issued: v.Issued, - }) + } } - return users, nil + return usersInfos, nil }, CreateUserFunc: func(_ context.Context, accountID, userID string, key *types.UserInfo) (*types.UserInfo, error) { if userID != existingUserID { @@ -127,15 +124,6 @@ func initUsersTestData() *handler { return nil }, }, - claimsExtractor: jwtclaims.NewClaimsExtractor( - jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims { - return jwtclaims.AuthorizationClaims{ - UserId: existingUserID, - Domain: testDomain, - AccountId: existingAccountID, - } - }), - ), } } @@ -158,6 +146,11 @@ func TestGetUsers(t *testing.T) { t.Run(tc.name, func(t *testing.T) { recorder := httptest.NewRecorder() req := httptest.NewRequest(tc.requestType, tc.requestPath, nil) + req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{ + UserId: existingUserID, + Domain: testDomain, + AccountId: existingAccountID, + }) userHandler.getAllUsers(recorder, req) @@ -263,6 +256,11 @@ func TestUpdateUser(t *testing.T) { t.Run(tc.name, func(t *testing.T) { recorder := httptest.NewRecorder() req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody) + req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{ + UserId: existingUserID, + Domain: testDomain, + AccountId: existingAccountID, + }) router := mux.NewRouter() router.HandleFunc("/api/users/{userId}", userHandler.updateUser).Methods("PUT") @@ -355,6 +353,11 @@ func TestCreateUser(t *testing.T) { t.Run(tc.name, func(t *testing.T) { req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody) rr := httptest.NewRecorder() + req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{ + UserId: existingUserID, + Domain: testDomain, + AccountId: existingAccountID, + }) userHandler.createUser(rr, req) @@ -399,6 +402,12 @@ func TestInviteUser(t *testing.T) { t.Run(tc.name, func(t *testing.T) { req := httptest.NewRequest(tc.requestType, tc.requestPath, nil) req = mux.SetURLVars(req, tc.requestVars) + req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{ + UserId: existingUserID, + Domain: testDomain, + AccountId: existingAccountID, + }) + rr := httptest.NewRecorder() userHandler.inviteUser(rr, req) @@ -452,6 +461,12 @@ func TestDeleteUser(t *testing.T) { t.Run(tc.name, func(t *testing.T) { req := httptest.NewRequest(tc.requestType, tc.requestPath, nil) req = mux.SetURLVars(req, tc.requestVars) + req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{ + UserId: existingUserID, + Domain: testDomain, + AccountId: existingAccountID, + }) + rr := httptest.NewRecorder() userHandler.deleteUser(rr, req) diff --git a/management/server/http/middleware/access_control.go b/management/server/http/middleware/access_control.go index c5bdf5fe7..4ed90f47b 100644 --- a/management/server/http/middleware/access_control.go +++ b/management/server/http/middleware/access_control.go @@ -7,30 +7,24 @@ import ( log "github.com/sirupsen/logrus" + nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/http/middleware/bypass" "github.com/netbirdio/netbird/management/server/http/util" "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/types" - - "github.com/netbirdio/netbird/management/server/jwtclaims" ) // GetUser function defines a function to fetch user from Account by jwtclaims.AuthorizationClaims -type GetUser func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (*types.User, error) +type GetUser func(ctx context.Context, userAuth nbcontext.UserAuth) (*types.User, error) // AccessControl middleware to restrict to make POST/PUT/DELETE requests by admin only type AccessControl struct { - claimsExtract jwtclaims.ClaimsExtractor - getUser GetUser + getUser GetUser } // NewAccessControl instance constructor -func NewAccessControl(audience, userIDClaim string, getUser GetUser) *AccessControl { +func NewAccessControl(getUser GetUser) *AccessControl { return &AccessControl{ - claimsExtract: *jwtclaims.NewClaimsExtractor( - jwtclaims.WithAudience(audience), - jwtclaims.WithUserIDClaim(userIDClaim), - ), getUser: getUser, } } @@ -45,12 +39,16 @@ func (a *AccessControl) Handler(h http.Handler) http.Handler { return } - claims := a.claimsExtract.FromRequestContext(r) - - user, err := a.getUser(r.Context(), claims) + userAuth, err := nbcontext.GetUserAuthFromRequest(r) if err != nil { - log.WithContext(r.Context()).Errorf("failed to get user from claims: %s", err) - util.WriteError(r.Context(), status.Errorf(status.Unauthorized, "invalid JWT"), w) + log.WithContext(r.Context()).Errorf("failed to get user auth from request: %s", err) + util.WriteError(r.Context(), status.Errorf(status.Unauthorized, "invalid user auth"), w) + } + + user, err := a.getUser(r.Context(), userAuth) + if err != nil { + log.WithContext(r.Context()).Errorf("failed to get user: %s", err) + util.WriteError(r.Context(), status.Errorf(status.Unauthorized, "invalid user auth"), w) return } diff --git a/management/server/http/middleware/auth_middleware.go b/management/server/http/middleware/auth_middleware.go index dcf73259a..a8e6790a9 100644 --- a/management/server/http/middleware/auth_middleware.go +++ b/management/server/http/middleware/auth_middleware.go @@ -8,67 +8,41 @@ import ( "strings" "time" - "github.com/golang-jwt/jwt" log "github.com/sirupsen/logrus" - nbContext "github.com/netbirdio/netbird/management/server/context" + "github.com/netbirdio/netbird/management/server/auth" + nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/http/middleware/bypass" "github.com/netbirdio/netbird/management/server/http/util" - "github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/status" - "github.com/netbirdio/netbird/management/server/types" ) -// GetAccountInfoFromPATFunc function -type GetAccountInfoFromPATFunc func(ctx context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error) - -// ValidateAndParseTokenFunc function -type ValidateAndParseTokenFunc func(ctx context.Context, token string) (*jwt.Token, error) - -// MarkPATUsedFunc function -type MarkPATUsedFunc func(ctx context.Context, token string) error - -// CheckUserAccessByJWTGroupsFunc function -type CheckUserAccessByJWTGroupsFunc func(ctx context.Context, claims jwtclaims.AuthorizationClaims) error +type EnsureAccountFunc func(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error) +type SyncUserJWTGroupsFunc func(ctx context.Context, userAuth nbcontext.UserAuth) error // AuthMiddleware middleware to verify personal access tokens (PAT) and JWT tokens type AuthMiddleware struct { - getAccountInfoFromPAT GetAccountInfoFromPATFunc - validateAndParseToken ValidateAndParseTokenFunc - markPATUsed MarkPATUsedFunc - checkUserAccessByJWTGroups CheckUserAccessByJWTGroupsFunc - claimsExtractor *jwtclaims.ClaimsExtractor - audience string - userIDClaim string + authManager auth.Manager + ensureAccount EnsureAccountFunc + syncUserJWTGroups SyncUserJWTGroupsFunc } -const ( - userProperty = "user" -) - // NewAuthMiddleware instance constructor -func NewAuthMiddleware(getAccountInfoFromPAT GetAccountInfoFromPATFunc, validateAndParseToken ValidateAndParseTokenFunc, - markPATUsed MarkPATUsedFunc, checkUserAccessByJWTGroups CheckUserAccessByJWTGroupsFunc, claimsExtractor *jwtclaims.ClaimsExtractor, - audience string, userIdClaim string) *AuthMiddleware { - if userIdClaim == "" { - userIdClaim = jwtclaims.UserIDClaim - } - +func NewAuthMiddleware( + authManager auth.Manager, + ensureAccount EnsureAccountFunc, + syncUserJWTGroups SyncUserJWTGroupsFunc, +) *AuthMiddleware { return &AuthMiddleware{ - getAccountInfoFromPAT: getAccountInfoFromPAT, - validateAndParseToken: validateAndParseToken, - markPATUsed: markPATUsed, - checkUserAccessByJWTGroups: checkUserAccessByJWTGroups, - claimsExtractor: claimsExtractor, - audience: audience, - userIDClaim: userIdClaim, + authManager: authManager, + ensureAccount: ensureAccount, + syncUserJWTGroups: syncUserJWTGroups, } } // Handler method of the middleware which authenticates a user either by JWT claims or by PAT func (m *AuthMiddleware) Handler(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if bypass.ShouldBypass(r.URL.Path, h, w, r) { return } @@ -84,108 +58,111 @@ func (m *AuthMiddleware) Handler(h http.Handler) http.Handler { switch authType { case "bearer": - err := m.checkJWTFromRequest(w, r, auth) + request, err := m.checkJWTFromRequest(r, auth) if err != nil { - log.WithContext(r.Context()).Errorf("Error when validating JWT claims: %s", err.Error()) + log.WithContext(r.Context()).Errorf("Error when validating JWT: %s", err.Error()) util.WriteError(r.Context(), status.Errorf(status.Unauthorized, "token invalid"), w) return } + + h.ServeHTTP(w, request) case "token": - err := m.checkPATFromRequest(w, r, auth) + request, err := m.checkPATFromRequest(r, auth) if err != nil { - log.WithContext(r.Context()).Debugf("Error when validating PAT claims: %s", err.Error()) + log.WithContext(r.Context()).Debugf("Error when validating PAT: %s", err.Error()) util.WriteError(r.Context(), status.Errorf(status.Unauthorized, "token invalid"), w) return } + h.ServeHTTP(w, request) default: util.WriteError(r.Context(), status.Errorf(status.Unauthorized, "no valid authentication provided"), w) return } - claims := m.claimsExtractor.FromRequestContext(r) - //nolint - ctx := context.WithValue(r.Context(), nbContext.UserIDKey, claims.UserId) - //nolint - ctx = context.WithValue(ctx, nbContext.AccountIDKey, claims.AccountId) - h.ServeHTTP(w, r.WithContext(ctx)) }) } // CheckJWTFromRequest checks if the JWT is valid -func (m *AuthMiddleware) checkJWTFromRequest(w http.ResponseWriter, r *http.Request, auth []string) error { +func (m *AuthMiddleware) checkJWTFromRequest(r *http.Request, auth []string) (*http.Request, error) { token, err := getTokenFromJWTRequest(auth) // If an error occurs, call the error handler and return an error if err != nil { - return fmt.Errorf("Error extracting token: %w", err) + return r, fmt.Errorf("error extracting token: %w", err) } - validatedToken, err := m.validateAndParseToken(r.Context(), token) + ctx := r.Context() + + userAuth, validatedToken, err := m.authManager.ValidateAndParseToken(ctx, token) if err != nil { - return err + return r, err } - if validatedToken == nil { - return nil + if impersonate, ok := r.URL.Query()["account"]; ok && len(impersonate) == 1 { + userAuth.AccountId = impersonate[0] + userAuth.IsChild = ok } - if err := m.verifyUserAccess(r.Context(), validatedToken); err != nil { - return err + // we need to call this method because if user is new, we will automatically add it to existing or create a new account + accountId, _, err := m.ensureAccount(ctx, userAuth) + if err != nil { + return r, err } - // If we get here, everything worked and we can set the - // user property in context. - newRequest := r.WithContext(context.WithValue(r.Context(), userProperty, validatedToken)) //nolint - // Update the current request with the new context information. - *r = *newRequest - return nil -} + if userAuth.AccountId != accountId { + log.WithContext(ctx).Debugf("Auth middleware sets accountId from ensure, before %s, now %s", userAuth.AccountId, accountId) + userAuth.AccountId = accountId + } -// verifyUserAccess checks if a user, based on a validated JWT token, -// is allowed access, particularly in cases where the admin enabled JWT -// group propagation and designated certain groups with access permissions. -func (m *AuthMiddleware) verifyUserAccess(ctx context.Context, validatedToken *jwt.Token) error { - authClaims := m.claimsExtractor.FromToken(validatedToken) - return m.checkUserAccessByJWTGroups(ctx, authClaims) + userAuth, err = m.authManager.EnsureUserAccessByJWTGroups(ctx, userAuth, validatedToken) + if err != nil { + return r, err + } + + err = m.syncUserJWTGroups(ctx, userAuth) + if err != nil { + log.WithContext(ctx).Errorf("HTTP server failed to sync user JWT groups: %s", err) + } + + return nbcontext.SetUserAuthInRequest(r, userAuth), nil } // CheckPATFromRequest checks if the PAT is valid -func (m *AuthMiddleware) checkPATFromRequest(w http.ResponseWriter, r *http.Request, auth []string) error { +func (m *AuthMiddleware) checkPATFromRequest(r *http.Request, auth []string) (*http.Request, error) { token, err := getTokenFromPATRequest(auth) if err != nil { - return fmt.Errorf("error extracting token: %w", err) + return r, fmt.Errorf("error extracting token: %w", err) } - user, pat, accDomain, accCategory, err := m.getAccountInfoFromPAT(r.Context(), token) + ctx := r.Context() + user, pat, accDomain, accCategory, err := m.authManager.GetPATInfo(ctx, token) if err != nil { - return fmt.Errorf("invalid Token: %w", err) + return r, fmt.Errorf("invalid Token: %w", err) } if time.Now().After(pat.GetExpirationDate()) { - return fmt.Errorf("token expired") + return r, fmt.Errorf("token expired") } - err = m.markPATUsed(r.Context(), pat.ID) + err = m.authManager.MarkPATUsed(ctx, pat.ID) if err != nil { - return err + return r, err } - claimMaps := jwt.MapClaims{} - claimMaps[m.userIDClaim] = user.Id - claimMaps[m.audience+jwtclaims.AccountIDSuffix] = user.AccountID - claimMaps[m.audience+jwtclaims.DomainIDSuffix] = accDomain - claimMaps[m.audience+jwtclaims.DomainCategorySuffix] = accCategory - claimMaps[jwtclaims.IsToken] = true - jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claimMaps) - newRequest := r.WithContext(context.WithValue(r.Context(), jwtclaims.TokenUserProperty, jwtToken)) //nolint - // Update the current request with the new context information. - *r = *newRequest - return nil + userAuth := nbcontext.UserAuth{ + UserId: user.Id, + AccountId: user.AccountID, + Domain: accDomain, + DomainCategory: accCategory, + IsPAT: true, + } + + return nbcontext.SetUserAuthInRequest(r, userAuth), nil } // getTokenFromJWTRequest is a "TokenExtractor" that takes auth header parts and extracts // the JWT token from the Authorization header. func getTokenFromJWTRequest(authHeaderParts []string) (string, error) { if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" { - return "", errors.New("Authorization header format must be Bearer {token}") + return "", errors.New("authorization header format must be Bearer {token}") } return authHeaderParts[1], nil @@ -195,7 +172,7 @@ func getTokenFromJWTRequest(authHeaderParts []string) (string, error) { // the PAT token from the Authorization header. func getTokenFromPATRequest(authHeaderParts []string) (string, error) { if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "token" { - return "", errors.New("Authorization header format must be Token {token}") + return "", errors.New("authorization header format must be Token {token}") } return authHeaderParts[1], nil diff --git a/management/server/http/middleware/auth_middleware_test.go b/management/server/http/middleware/auth_middleware_test.go index c1686ed44..3dc7d51cb 100644 --- a/management/server/http/middleware/auth_middleware_test.go +++ b/management/server/http/middleware/auth_middleware_test.go @@ -9,10 +9,14 @@ import ( "time" "github.com/golang-jwt/jwt" + "github.com/stretchr/testify/assert" + + "github.com/netbirdio/netbird/management/server/auth" + nbjwt "github.com/netbirdio/netbird/management/server/auth/jwt" + nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/util" "github.com/netbirdio/netbird/management/server/http/middleware/bypass" - "github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/types" ) @@ -58,17 +62,23 @@ func mockGetAccountInfoFromPAT(_ context.Context, token string) (user *types.Use return nil, nil, "", "", fmt.Errorf("PAT invalid") } -func mockValidateAndParseToken(_ context.Context, token string) (*jwt.Token, error) { +func mockValidateAndParseToken(_ context.Context, token string) (nbcontext.UserAuth, *jwt.Token, error) { if token == JWT { - return &jwt.Token{ - Claims: jwt.MapClaims{ - userIDClaim: userID, - audience + jwtclaims.AccountIDSuffix: accountID, + return nbcontext.UserAuth{ + UserId: userID, + AccountId: accountID, + Domain: testAccount.Domain, + DomainCategory: testAccount.DomainCategory, }, - Valid: true, - }, nil + &jwt.Token{ + Claims: jwt.MapClaims{ + userIDClaim: userID, + audience + nbjwt.AccountIDSuffix: accountID, + }, + Valid: true, + }, nil } - return nil, fmt.Errorf("JWT invalid") + return nbcontext.UserAuth{}, nil, fmt.Errorf("JWT invalid") } func mockMarkPATUsed(_ context.Context, token string) error { @@ -78,16 +88,20 @@ func mockMarkPATUsed(_ context.Context, token string) error { return fmt.Errorf("Should never get reached") } -func mockCheckUserAccessByJWTGroups(_ context.Context, claims jwtclaims.AuthorizationClaims) error { - if testAccount.Id != claims.AccountId { - return fmt.Errorf("account with id %s does not exist", claims.AccountId) +func mockEnsureUserAccessByJWTGroups(_ context.Context, userAuth nbcontext.UserAuth, token *jwt.Token) (nbcontext.UserAuth, error) { + if userAuth.IsChild || userAuth.IsPAT { + return userAuth, nil } - if _, ok := testAccount.Users[claims.UserId]; !ok { - return fmt.Errorf("user with id %s does not exist", claims.UserId) + if testAccount.Id != userAuth.AccountId { + return userAuth, fmt.Errorf("account with id %s does not exist", userAuth.AccountId) } - return nil + if _, ok := testAccount.Users[userAuth.UserId]; !ok { + return userAuth, fmt.Errorf("user with id %s does not exist", userAuth.UserId) + } + + return userAuth, nil } func TestAuthMiddleware_Handler(t *testing.T) { @@ -158,22 +172,24 @@ func TestAuthMiddleware_Handler(t *testing.T) { } nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // do nothing + }) - claimsExtractor := jwtclaims.NewClaimsExtractor( - jwtclaims.WithAudience(audience), - jwtclaims.WithUserIDClaim(userIDClaim), - ) + mockAuth := &auth.MockManager{ + ValidateAndParseTokenFunc: mockValidateAndParseToken, + EnsureUserAccessByJWTGroupsFunc: mockEnsureUserAccessByJWTGroups, + MarkPATUsedFunc: mockMarkPATUsed, + GetPATInfoFunc: mockGetAccountInfoFromPAT, + } authMiddleware := NewAuthMiddleware( - mockGetAccountInfoFromPAT, - mockValidateAndParseToken, - mockMarkPATUsed, - mockCheckUserAccessByJWTGroups, - claimsExtractor, - audience, - userIDClaim, + mockAuth, + func(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error) { + return userAuth.AccountId, userAuth.UserId, nil + }, + func(ctx context.Context, userAuth nbcontext.UserAuth) error { + return nil + }, ) handlerToTest := authMiddleware.Handler(nextHandler) @@ -195,9 +211,115 @@ func TestAuthMiddleware_Handler(t *testing.T) { result := rec.Result() defer result.Body.Close() + if result.StatusCode != tc.expectedStatusCode { t.Errorf("expected status code %d, got %d", tc.expectedStatusCode, result.StatusCode) } }) } } + +func TestAuthMiddleware_Handler_Child(t *testing.T) { + tt := []struct { + name string + path string + authHeader string + expectedUserAuth *nbcontext.UserAuth // nil expects 401 response status + }{ + { + name: "Valid PAT Token", + path: "/test", + authHeader: "Token " + PAT, + expectedUserAuth: &nbcontext.UserAuth{ + AccountId: accountID, + UserId: userID, + Domain: testAccount.Domain, + DomainCategory: testAccount.DomainCategory, + IsPAT: true, + }, + }, + { + name: "Valid PAT Token ignores child", + path: "/test?account=xyz", + authHeader: "Token " + PAT, + expectedUserAuth: &nbcontext.UserAuth{ + AccountId: accountID, + UserId: userID, + Domain: testAccount.Domain, + DomainCategory: testAccount.DomainCategory, + IsPAT: true, + }, + }, + { + name: "Valid JWT Token", + path: "/test", + authHeader: "Bearer " + JWT, + expectedUserAuth: &nbcontext.UserAuth{ + AccountId: accountID, + UserId: userID, + Domain: testAccount.Domain, + DomainCategory: testAccount.DomainCategory, + }, + }, + + { + name: "Valid JWT Token with child", + path: "/test?account=xyz", + authHeader: "Bearer " + JWT, + expectedUserAuth: &nbcontext.UserAuth{ + AccountId: "xyz", + UserId: userID, + Domain: testAccount.Domain, + DomainCategory: testAccount.DomainCategory, + IsChild: true, + }, + }, + } + + mockAuth := &auth.MockManager{ + ValidateAndParseTokenFunc: mockValidateAndParseToken, + EnsureUserAccessByJWTGroupsFunc: mockEnsureUserAccessByJWTGroups, + MarkPATUsedFunc: mockMarkPATUsed, + GetPATInfoFunc: mockGetAccountInfoFromPAT, + } + + authMiddleware := NewAuthMiddleware( + mockAuth, + func(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error) { + return userAuth.AccountId, userAuth.UserId, nil + }, + func(ctx context.Context, userAuth nbcontext.UserAuth) error { + return nil + }, + ) + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + handlerToTest := authMiddleware.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + userAuth, err := nbcontext.GetUserAuthFromRequest(r) + if tc.expectedUserAuth != nil { + assert.NoError(t, err) + assert.Equal(t, *tc.expectedUserAuth, userAuth) + } else { + assert.Error(t, err) + assert.Empty(t, userAuth) + } + })) + + req := httptest.NewRequest("GET", "http://testing"+tc.path, nil) + req.Header.Set("Authorization", tc.authHeader) + rec := httptest.NewRecorder() + + handlerToTest.ServeHTTP(rec, req) + + result := rec.Result() + defer result.Body.Close() + + if tc.expectedUserAuth != nil { + assert.Equal(t, 200, result.StatusCode) + } else { + assert.Equal(t, 401, result.StatusCode) + } + }) + } +} diff --git a/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go b/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go index 2eb50e4b4..e2c2c1d85 100644 --- a/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go +++ b/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go @@ -77,13 +77,13 @@ func BenchmarkUpdatePeer(b *testing.B) { func BenchmarkGetOnePeer(b *testing.B) { var expectedMetrics = map[string]testing_tools.PerformanceMetrics{ - "Peers - XS": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 40, MinMsPerOpCICD: 30, MaxMsPerOpCICD: 70}, - "Peers - S": {MinMsPerOpLocal: 1, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 30}, - "Peers - M": {MinMsPerOpLocal: 9, MaxMsPerOpLocal: 18, MinMsPerOpCICD: 15, MaxMsPerOpCICD: 50}, - "Peers - L": {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 90, MinMsPerOpCICD: 50, MaxMsPerOpCICD: 130}, - "Groups - L": {MinMsPerOpLocal: 80, MaxMsPerOpLocal: 130, MinMsPerOpCICD: 30, MaxMsPerOpCICD: 200}, - "Users - L": {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 90, MinMsPerOpCICD: 50, MaxMsPerOpCICD: 130}, - "Setup Keys - L": {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 90, MinMsPerOpCICD: 50, MaxMsPerOpCICD: 130}, + "Peers - XS": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 40, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 70}, + "Peers - S": {MinMsPerOpLocal: 1, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 70}, + "Peers - M": {MinMsPerOpLocal: 9, MaxMsPerOpLocal: 18, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 70}, + "Peers - L": {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 90, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 200}, + "Groups - L": {MinMsPerOpLocal: 80, MaxMsPerOpLocal: 130, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 200}, + "Users - L": {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 90, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 200}, + "Setup Keys - L": {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 90, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 200}, "Peers - XL": {MinMsPerOpLocal: 200, MaxMsPerOpLocal: 400, MinMsPerOpCICD: 200, MaxMsPerOpCICD: 750}, } @@ -111,9 +111,9 @@ func BenchmarkGetOnePeer(b *testing.B) { func BenchmarkGetAllPeers(b *testing.B) { var expectedMetrics = map[string]testing_tools.PerformanceMetrics{ - "Peers - XS": {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 70, MinMsPerOpCICD: 50, MaxMsPerOpCICD: 150}, - "Peers - S": {MinMsPerOpLocal: 2, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 30}, - "Peers - M": {MinMsPerOpLocal: 20, MaxMsPerOpLocal: 50, MinMsPerOpCICD: 20, MaxMsPerOpCICD: 70}, + "Peers - XS": {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 70, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 100}, + "Peers - S": {MinMsPerOpLocal: 2, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 100}, + "Peers - M": {MinMsPerOpLocal: 20, MaxMsPerOpLocal: 50, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 100}, "Peers - L": {MinMsPerOpLocal: 110, MaxMsPerOpLocal: 150, MinMsPerOpCICD: 100, MaxMsPerOpCICD: 300}, "Groups - L": {MinMsPerOpLocal: 150, MaxMsPerOpLocal: 200, MinMsPerOpCICD: 130, MaxMsPerOpCICD: 500}, "Users - L": {MinMsPerOpLocal: 100, MaxMsPerOpLocal: 170, MinMsPerOpCICD: 100, MaxMsPerOpCICD: 400}, @@ -145,14 +145,14 @@ func BenchmarkGetAllPeers(b *testing.B) { func BenchmarkDeletePeer(b *testing.B) { var expectedMetrics = map[string]testing_tools.PerformanceMetrics{ - "Peers - XS": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, - "Peers - S": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, - "Peers - M": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, - "Peers - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, - "Groups - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, - "Users - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, - "Setup Keys - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, - "Peers - XL": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, + "Peers - XS": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 18}, + "Peers - S": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 18}, + "Peers - M": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 18}, + "Peers - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 18}, + "Groups - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 18}, + "Users - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 18}, + "Setup Keys - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 18}, + "Peers - XL": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 18}, } log.SetOutput(io.Discard) diff --git a/management/server/http/testing/benchmarks/users_handler_benchmark_test.go b/management/server/http/testing/benchmarks/users_handler_benchmark_test.go index e9bc63f9b..b7deab334 100644 --- a/management/server/http/testing/benchmarks/users_handler_benchmark_test.go +++ b/management/server/http/testing/benchmarks/users_handler_benchmark_test.go @@ -37,24 +37,23 @@ func BenchmarkUpdateUser(b *testing.B) { var expectedMetrics = map[string]testing_tools.PerformanceMetrics{ "Users - XS": {MinMsPerOpLocal: 100, MaxMsPerOpLocal: 160, MinMsPerOpCICD: 100, MaxMsPerOpCICD: 310}, "Users - S": {MinMsPerOpLocal: 0.3, MaxMsPerOpLocal: 3, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 15}, - "Users - M": {MinMsPerOpLocal: 1, MaxMsPerOpLocal: 3, MinMsPerOpCICD: 3, MaxMsPerOpCICD: 15}, - "Users - L": {MinMsPerOpLocal: 0.3, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 20}, + "Users - M": {MinMsPerOpLocal: 1, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 3, MaxMsPerOpCICD: 20}, + "Users - L": {MinMsPerOpLocal: 5, MaxMsPerOpLocal: 20, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 50}, "Peers - L": {MinMsPerOpLocal: 80, MaxMsPerOpLocal: 150, MinMsPerOpCICD: 80, MaxMsPerOpCICD: 310}, - "Groups - L": {MinMsPerOpLocal: 10, MaxMsPerOpLocal: 30, MinMsPerOpCICD: 20, MaxMsPerOpCICD: 60}, - "Setup Keys - L": {MinMsPerOpLocal: 0.3, MaxMsPerOpLocal: 3, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, - "Users - XL": {MinMsPerOpLocal: 5, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 10, MaxMsPerOpCICD: 30}, + "Groups - L": {MinMsPerOpLocal: 10, MaxMsPerOpLocal: 50, MinMsPerOpCICD: 20, MaxMsPerOpCICD: 120}, + "Setup Keys - L": {MinMsPerOpLocal: 5, MaxMsPerOpLocal: 20, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 50}, + "Users - XL": {MinMsPerOpLocal: 30, MaxMsPerOpLocal: 100, MinMsPerOpCICD: 60, MaxMsPerOpCICD: 280}, } log.SetOutput(io.Discard) defer log.SetOutput(os.Stderr) - recorder := httptest.NewRecorder() - for name, bc := range benchCasesUsers { b.Run(name, func(b *testing.B) { apiHandler, am, _ := testing_tools.BuildApiBlackBoxWithDBState(b, "../testdata/users.sql", nil, false) testing_tools.PopulateTestData(b, am.(*server.DefaultAccountManager), bc.Peers, bc.Groups, bc.Users, bc.SetupKeys) + recorder := httptest.NewRecorder() b.ResetTimer() start := time.Now() for i := 0; i < b.N; i++ { @@ -97,13 +96,12 @@ func BenchmarkGetOneUser(b *testing.B) { log.SetOutput(io.Discard) defer log.SetOutput(os.Stderr) - recorder := httptest.NewRecorder() - for name, bc := range benchCasesUsers { b.Run(name, func(b *testing.B) { apiHandler, am, _ := testing_tools.BuildApiBlackBoxWithDBState(b, "../testdata/users.sql", nil, false) testing_tools.PopulateTestData(b, am.(*server.DefaultAccountManager), bc.Peers, bc.Groups, bc.Users, bc.SetupKeys) + recorder := httptest.NewRecorder() b.ResetTimer() start := time.Now() for i := 0; i < b.N; i++ { @@ -118,26 +116,25 @@ func BenchmarkGetOneUser(b *testing.B) { func BenchmarkGetAllUsers(b *testing.B) { var expectedMetrics = map[string]testing_tools.PerformanceMetrics{ - "Users - XS": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 10}, - "Users - S": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 10}, - "Users - M": {MinMsPerOpLocal: 3, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 15}, - "Users - L": {MinMsPerOpLocal: 10, MaxMsPerOpLocal: 20, MinMsPerOpCICD: 20, MaxMsPerOpCICD: 50}, - "Peers - L": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 25, MinMsPerOpCICD: 20, MaxMsPerOpCICD: 55}, - "Groups - L": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 25, MinMsPerOpCICD: 25, MaxMsPerOpCICD: 55}, - "Setup Keys - L": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 25, MinMsPerOpCICD: 25, MaxMsPerOpCICD: 55}, - "Users - XL": {MinMsPerOpLocal: 80, MaxMsPerOpLocal: 120, MinMsPerOpCICD: 100, MaxMsPerOpCICD: 300}, + "Users - XS": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 75}, + "Users - S": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 75}, + "Users - M": {MinMsPerOpLocal: 3, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 75}, + "Users - L": {MinMsPerOpLocal: 10, MaxMsPerOpLocal: 20, MinMsPerOpCICD: 10, MaxMsPerOpCICD: 100}, + "Peers - L": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 25, MinMsPerOpCICD: 10, MaxMsPerOpCICD: 100}, + "Groups - L": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 25, MinMsPerOpCICD: 10, MaxMsPerOpCICD: 100}, + "Setup Keys - L": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 25, MinMsPerOpCICD: 10, MaxMsPerOpCICD: 100}, + "Users - XL": {MinMsPerOpLocal: 80, MaxMsPerOpLocal: 120, MinMsPerOpCICD: 50, MaxMsPerOpCICD: 300}, } log.SetOutput(io.Discard) defer log.SetOutput(os.Stderr) - recorder := httptest.NewRecorder() - for name, bc := range benchCasesUsers { b.Run(name, func(b *testing.B) { apiHandler, am, _ := testing_tools.BuildApiBlackBoxWithDBState(b, "../testdata/users.sql", nil, false) testing_tools.PopulateTestData(b, am.(*server.DefaultAccountManager), bc.Peers, bc.Groups, bc.Users, bc.SetupKeys) + recorder := httptest.NewRecorder() b.ResetTimer() start := time.Now() for i := 0; i < b.N; i++ { @@ -152,26 +149,25 @@ func BenchmarkGetAllUsers(b *testing.B) { func BenchmarkDeleteUsers(b *testing.B) { var expectedMetrics = map[string]testing_tools.PerformanceMetrics{ - "Users - XS": {MinMsPerOpLocal: 3, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 25}, - "Users - S": {MinMsPerOpLocal: 2, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 20}, - "Users - M": {MinMsPerOpLocal: 2, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 20}, - "Users - L": {MinMsPerOpLocal: 2, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 20}, - "Peers - L": {MinMsPerOpLocal: 3, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 25}, - "Groups - L": {MinMsPerOpLocal: 2, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 20}, - "Setup Keys - L": {MinMsPerOpLocal: 3, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 20}, - "Users - XL": {MinMsPerOpLocal: 3, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 20}, + "Users - XS": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50}, + "Users - S": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50}, + "Users - M": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50}, + "Users - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50}, + "Peers - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50}, + "Groups - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50}, + "Setup Keys - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50}, + "Users - XL": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50}, } log.SetOutput(io.Discard) defer log.SetOutput(os.Stderr) - recorder := httptest.NewRecorder() - for name, bc := range benchCasesUsers { b.Run(name, func(b *testing.B) { apiHandler, am, _ := testing_tools.BuildApiBlackBoxWithDBState(b, "../testdata/users.sql", nil, false) testing_tools.PopulateTestData(b, am.(*server.DefaultAccountManager), bc.Peers, bc.Groups, 1000, bc.SetupKeys) + recorder := httptest.NewRecorder() b.ResetTimer() start := time.Now() for i := 0; i < b.N; i++ { diff --git a/management/server/http/testing/testing_tools/tools.go b/management/server/http/testing/testing_tools/tools.go index 006d5679c..e534dac46 100644 --- a/management/server/http/testing/testing_tools/tools.go +++ b/management/server/http/testing/testing_tools/tools.go @@ -3,6 +3,7 @@ package testing_tools import ( "bytes" "context" + "errors" "fmt" "io" "net" @@ -13,17 +14,17 @@ import ( "testing" "time" - "github.com/netbirdio/netbird/management/server/util" + "github.com/golang-jwt/jwt" "github.com/stretchr/testify/assert" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server/activity" + "github.com/netbirdio/netbird/management/server/auth" + nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/geolocation" "github.com/netbirdio/netbird/management/server/groups" nbhttp "github.com/netbirdio/netbird/management/server/http" - "github.com/netbirdio/netbird/management/server/http/configs" - "github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/networks" "github.com/netbirdio/netbird/management/server/networks/resources" "github.com/netbirdio/netbird/management/server/networks/routers" @@ -32,6 +33,7 @@ import ( "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/management/server/types" + "github.com/netbirdio/netbird/management/server/util" ) const ( @@ -115,11 +117,20 @@ func BuildApiBlackBoxWithDBState(t TB, sqlFile string, expectedPeerUpdate *serve t.Fatalf("Failed to create manager: %v", err) } + // @note this is required so that PAT's validate from store, but JWT's are mocked + authManager := auth.NewManager(store, "", "", "", "", []string{}, false) + authManagerMock := &auth.MockManager{ + ValidateAndParseTokenFunc: mockValidateAndParseToken, + EnsureUserAccessByJWTGroupsFunc: authManager.EnsureUserAccessByJWTGroups, + MarkPATUsedFunc: authManager.MarkPATUsed, + GetPATInfoFunc: authManager.GetPATInfo, + } + networksManagerMock := networks.NewManagerMock() resourcesManagerMock := resources.NewManagerMock() routersManagerMock := routers.NewManagerMock() groupsManagerMock := groups.NewManagerMock() - apiHandler, err := nbhttp.NewAPIHandler(context.Background(), am, networksManagerMock, resourcesManagerMock, routersManagerMock, groupsManagerMock, geoMock, &jwtclaims.JwtValidatorMock{}, metrics, configs.AuthCfg{}, validatorMock) + apiHandler, err := nbhttp.NewAPIHandler(context.Background(), am, networksManagerMock, resourcesManagerMock, routersManagerMock, groupsManagerMock, geoMock, authManagerMock, metrics, &server.Config{}, validatorMock) if err != nil { t.Fatalf("Failed to create API handler: %v", err) } @@ -309,3 +320,25 @@ func EvaluateBenchmarkResults(b *testing.B, name string, duration time.Duration, b.Fatalf("Benchmark %s failed: too slow (%.2f ms/op, maximum %.2f ms/op)", name, msPerOp, maxExpected) } } + +func mockValidateAndParseToken(_ context.Context, token string) (nbcontext.UserAuth, *jwt.Token, error) { + userAuth := nbcontext.UserAuth{} + + switch token { + case "testUserId", "testAdminId", "testOwnerId", "testServiceUserId", "testServiceAdminId", "blockedUserId": + userAuth.UserId = token + userAuth.AccountId = "testAccountId" + userAuth.Domain = "test.com" + userAuth.DomainCategory = "private" + case "otherUserId": + userAuth.UserId = "otherUserId" + userAuth.AccountId = "otherAccountId" + userAuth.Domain = "other.com" + userAuth.DomainCategory = "private" + case "invalidToken": + return userAuth, nil, errors.New("invalid token") + } + + jwtToken := jwt.New(jwt.SigningMethodHS256) + return userAuth, jwtToken, nil +} diff --git a/management/server/idp/idp.go b/management/server/idp/idp.go index 419220942..0f1ff0f1f 100644 --- a/management/server/idp/idp.go +++ b/management/server/idp/idp.go @@ -149,6 +149,7 @@ func NewManager(ctx context.Context, config Config, appMetrics telemetry.AppMetr GrantType: config.ClientConfig.GrantType, TokenEndpoint: config.ClientConfig.TokenEndpoint, ManagementEndpoint: config.ExtraConfig["ManagementEndpoint"], + PAT: config.ExtraConfig["PAT"], } } diff --git a/management/server/idp/zitadel.go b/management/server/idp/zitadel.go index 9d7626844..343357927 100644 --- a/management/server/idp/zitadel.go +++ b/management/server/idp/zitadel.go @@ -34,6 +34,7 @@ type ZitadelClientConfig struct { GrantType string TokenEndpoint string ManagementEndpoint string + PAT string } // ZitadelCredentials zitadel authentication information. @@ -135,6 +136,28 @@ func readZitadelError(body io.ReadCloser) error { return errors.New(strings.Join(errsOut, " ")) } +// verifyJWTConfig ensures necessary values are set in the ZitadelClientConfig for JWTs to be generated. +func verifyJWTConfig(config ZitadelClientConfig) error { + + if config.ClientID == "" { + return fmt.Errorf("zitadel IdP configuration is incomplete, clientID is missing") + } + + if config.ClientSecret == "" { + return fmt.Errorf("zitadel IdP configuration is incomplete, ClientSecret is missing") + } + + if config.TokenEndpoint == "" { + return fmt.Errorf("zitadel IdP configuration is incomplete, TokenEndpoint is missing") + } + + if config.GrantType == "" { + return fmt.Errorf("zitadel IdP configuration is incomplete, GrantType is missing") + } + + return nil +} + // NewZitadelManager creates a new instance of the ZitadelManager. func NewZitadelManager(config ZitadelClientConfig, appMetrics telemetry.AppMetrics) (*ZitadelManager, error) { httpTransport := http.DefaultTransport.(*http.Transport).Clone() @@ -146,26 +169,18 @@ func NewZitadelManager(config ZitadelClientConfig, appMetrics telemetry.AppMetri } helper := JsonParser{} - if config.ClientID == "" { - return nil, fmt.Errorf("zitadel IdP configuration is incomplete, clientID is missing") - } - - if config.ClientSecret == "" { - return nil, fmt.Errorf("zitadel IdP configuration is incomplete, ClientSecret is missing") - } - - if config.TokenEndpoint == "" { - return nil, fmt.Errorf("zitadel IdP configuration is incomplete, TokenEndpoint is missing") + hasPAT := config.PAT != "" + if !hasPAT { + jwtErr := verifyJWTConfig(config) + if jwtErr != nil { + return nil, jwtErr + } } if config.ManagementEndpoint == "" { return nil, fmt.Errorf("zitadel IdP configuration is incomplete, ManagementEndpoint is missing") } - if config.GrantType == "" { - return nil, fmt.Errorf("zitadel IdP configuration is incomplete, GrantType is missing") - } - credentials := &ZitadelCredentials{ clientConfig: config, httpClient: httpClient, @@ -254,6 +269,20 @@ func (zc *ZitadelCredentials) parseRequestJWTResponse(rawBody io.ReadCloser) (JW return jwtToken, nil } +// generatePATToken creates a functional JWTToken instance which will pass the +// PAT to the API directly and skip requesting a token. +func (zc *ZitadelCredentials) generatePATToken() (JWTToken, error) { + tok := JWTToken{ + AccessToken: zc.clientConfig.PAT, + Scope: "openid", + ExpiresIn: 9999, + TokenType: "PAT", + } + tok.expiresInTime = time.Now().Add(time.Duration(tok.ExpiresIn) * time.Second) + zc.jwtToken = tok + return tok, nil +} + // Authenticate retrieves access token to use the Zitadel Management API. func (zc *ZitadelCredentials) Authenticate(ctx context.Context) (JWTToken, error) { zc.mux.Lock() @@ -269,6 +298,10 @@ func (zc *ZitadelCredentials) Authenticate(ctx context.Context) (JWTToken, error return zc.jwtToken, nil } + if zc.clientConfig.PAT != "" { + return zc.generatePATToken() + } + resp, err := zc.requestJWTToken(ctx) if err != nil { return zc.jwtToken, err diff --git a/management/server/integrated_validator.go b/management/server/integrated_validator.go index dcb9400f6..b9827f457 100644 --- a/management/server/integrated_validator.go +++ b/management/server/integrated_validator.go @@ -89,17 +89,17 @@ func (am *DefaultAccountManager) GetValidatedPeers(ctx context.Context, accountI } peers, err = transaction.GetAccountPeers(ctx, store.LockingStrengthShare, accountID) - if err != nil { - return err - } - - settings, err = transaction.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) return err }) if err != nil { return nil, err } + settings, err = am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return nil, err + } + return am.integratedPeerValidator.GetValidatedPeers(accountID, groups, peers, settings.Extra) } diff --git a/management/server/jwtclaims/claims.go b/management/server/jwtclaims/claims.go deleted file mode 100644 index 2527acbe3..000000000 --- a/management/server/jwtclaims/claims.go +++ /dev/null @@ -1,19 +0,0 @@ -package jwtclaims - -import ( - "time" - - "github.com/golang-jwt/jwt" -) - -// AuthorizationClaims stores authorization information from JWTs -type AuthorizationClaims struct { - UserId string - AccountId string - Domain string - DomainCategory string - LastLogin time.Time - Invited bool - - Raw jwt.MapClaims -} diff --git a/management/server/jwtclaims/extractor_test.go b/management/server/jwtclaims/extractor_test.go deleted file mode 100644 index eccd7c9e7..000000000 --- a/management/server/jwtclaims/extractor_test.go +++ /dev/null @@ -1,227 +0,0 @@ -package jwtclaims - -import ( - "context" - "net/http" - "testing" - "time" - - "github.com/golang-jwt/jwt" - "github.com/stretchr/testify/require" -) - -func newTestRequestWithJWT(t *testing.T, claims AuthorizationClaims, audience string) *http.Request { - t.Helper() - const layout = "2006-01-02T15:04:05.999Z" - - claimMaps := jwt.MapClaims{} - if claims.UserId != "" { - claimMaps[UserIDClaim] = claims.UserId - } - if claims.AccountId != "" { - claimMaps[audience+AccountIDSuffix] = claims.AccountId - } - if claims.Domain != "" { - claimMaps[audience+DomainIDSuffix] = claims.Domain - } - if claims.DomainCategory != "" { - claimMaps[audience+DomainCategorySuffix] = claims.DomainCategory - } - if claims.LastLogin != (time.Time{}) { - claimMaps[audience+LastLoginSuffix] = claims.LastLogin.Format(layout) - } - - if claims.Invited { - claimMaps[audience+Invited] = true - } - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claimMaps) - r, err := http.NewRequest(http.MethodGet, "http://localhost", nil) - require.NoError(t, err, "creating testing request failed") - testRequest := r.WithContext(context.WithValue(r.Context(), TokenUserProperty, token)) // nolint - - return testRequest -} - -func TestExtractClaimsFromRequestContext(t *testing.T) { - type test struct { - name string - inputAuthorizationClaims AuthorizationClaims - inputAudiance string - testingFunc require.ComparisonAssertionFunc - expectedMSG string - } - - const layout = "2006-01-02T15:04:05.999Z" - lastLogin, _ := time.Parse(layout, "2023-08-17T09:30:40.465Z") - - testCase1 := test{ - name: "All Claim Fields", - inputAudiance: "https://login/", - inputAuthorizationClaims: AuthorizationClaims{ - UserId: "test", - Domain: "test.com", - AccountId: "testAcc", - LastLogin: lastLogin, - DomainCategory: "public", - Invited: true, - Raw: jwt.MapClaims{ - "https://login/wt_account_domain": "test.com", - "https://login/wt_account_domain_category": "public", - "https://login/wt_account_id": "testAcc", - "https://login/nb_last_login": lastLogin.Format(layout), - "sub": "test", - "https://login/" + Invited: true, - }, - }, - testingFunc: require.EqualValues, - expectedMSG: "extracted claims should match input claims", - } - - testCase2 := test{ - name: "Domain Is Empty", - inputAudiance: "https://login/", - inputAuthorizationClaims: AuthorizationClaims{ - UserId: "test", - AccountId: "testAcc", - Raw: jwt.MapClaims{ - "https://login/wt_account_id": "testAcc", - "sub": "test", - }, - }, - testingFunc: require.EqualValues, - expectedMSG: "extracted claims should match input claims", - } - - testCase3 := test{ - name: "Account ID Is Empty", - inputAudiance: "https://login/", - inputAuthorizationClaims: AuthorizationClaims{ - UserId: "test", - Domain: "test.com", - Raw: jwt.MapClaims{ - "https://login/wt_account_domain": "test.com", - "sub": "test", - }, - }, - testingFunc: require.EqualValues, - expectedMSG: "extracted claims should match input claims", - } - - testCase4 := test{ - name: "Category Is Empty", - inputAudiance: "https://login/", - inputAuthorizationClaims: AuthorizationClaims{ - UserId: "test", - Domain: "test.com", - AccountId: "testAcc", - Raw: jwt.MapClaims{ - "https://login/wt_account_domain": "test.com", - "https://login/wt_account_id": "testAcc", - "sub": "test", - }, - }, - testingFunc: require.EqualValues, - expectedMSG: "extracted claims should match input claims", - } - - testCase5 := test{ - name: "Only User ID Is set", - inputAudiance: "https://login/", - inputAuthorizationClaims: AuthorizationClaims{ - UserId: "test", - Raw: jwt.MapClaims{ - "sub": "test", - }, - }, - testingFunc: require.EqualValues, - expectedMSG: "extracted claims should match input claims", - } - - for _, testCase := range []test{testCase1, testCase2, testCase3, testCase4, testCase5} { - t.Run(testCase.name, func(t *testing.T) { - request := newTestRequestWithJWT(t, testCase.inputAuthorizationClaims, testCase.inputAudiance) - - extractor := NewClaimsExtractor(WithAudience(testCase.inputAudiance)) - extractedClaims := extractor.FromRequestContext(request) - - testCase.testingFunc(t, testCase.inputAuthorizationClaims, extractedClaims, testCase.expectedMSG) - }) - } -} - -func TestExtractClaimsSetOptions(t *testing.T) { - t.Helper() - type test struct { - name string - extractor *ClaimsExtractor - check func(t *testing.T, c test) - } - - testCase1 := test{ - name: "No custom options", - extractor: NewClaimsExtractor(), - check: func(t *testing.T, c test) { - t.Helper() - if c.extractor.authAudience != "" { - t.Error("audience should be empty") - return - } - if c.extractor.userIDClaim != UserIDClaim { - t.Errorf("user id claim should be default, expected %s, got %s", UserIDClaim, c.extractor.userIDClaim) - return - } - if c.extractor.FromRequestContext == nil { - t.Error("from request context should not be nil") - return - } - }, - } - - testCase2 := test{ - name: "Custom audience", - extractor: NewClaimsExtractor(WithAudience("https://login/")), - check: func(t *testing.T, c test) { - t.Helper() - if c.extractor.authAudience != "https://login/" { - t.Errorf("audience expected %s, got %s", "https://login/", c.extractor.authAudience) - return - } - }, - } - - testCase3 := test{ - name: "Custom user id claim", - extractor: NewClaimsExtractor(WithUserIDClaim("customUserId")), - check: func(t *testing.T, c test) { - t.Helper() - if c.extractor.userIDClaim != "customUserId" { - t.Errorf("user id claim expected %s, got %s", "customUserId", c.extractor.userIDClaim) - return - } - }, - } - - testCase4 := test{ - name: "Custom extractor from request context", - extractor: NewClaimsExtractor( - WithFromRequestContext(func(r *http.Request) AuthorizationClaims { - return AuthorizationClaims{ - UserId: "testCustomRequest", - } - })), - check: func(t *testing.T, c test) { - t.Helper() - claims := c.extractor.FromRequestContext(&http.Request{}) - if claims.UserId != "testCustomRequest" { - t.Errorf("user id claim expected %s, got %s", "testCustomRequest", claims.UserId) - return - } - }, - } - - for _, testCase := range []test{testCase1, testCase2, testCase3, testCase4} { - t.Run(testCase.name, func(t *testing.T) { - testCase.check(t, testCase) - }) - } -} diff --git a/management/server/jwtclaims/jwtValidator.go b/management/server/jwtclaims/jwtValidator.go deleted file mode 100644 index 79e59e76f..000000000 --- a/management/server/jwtclaims/jwtValidator.go +++ /dev/null @@ -1,349 +0,0 @@ -package jwtclaims - -import ( - "context" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rsa" - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "math/big" - "net/http" - "strconv" - "strings" - "sync" - "time" - - "github.com/golang-jwt/jwt" - log "github.com/sirupsen/logrus" -) - -// Options is a struct for specifying configuration options for the middleware. -type Options struct { - // The function that will return the Key to validate the JWT. - // It can be either a shared secret or a public key. - // Default value: nil - ValidationKeyGetter jwt.Keyfunc - // The name of the property in the request where the user information - // from the JWT will be stored. - // Default value: "user" - UserProperty string - // The function that will be called when there's an error validating the token - // Default value: - CredentialsOptional bool - // A function that extracts the token from the request - // Default: FromAuthHeader (i.e., from Authorization header as bearer token) - Debug bool - // When set, all requests with the OPTIONS method will use authentication - // Default: false - EnableAuthOnOptions bool -} - -// Jwks is a collection of JSONWebKey obtained from Config.HttpServerConfig.AuthKeysLocation -type Jwks struct { - Keys []JSONWebKey `json:"keys"` - expiresInTime time.Time -} - -// The supported elliptic curves types -const ( - // p256 represents a cryptographic elliptical curve type. - p256 = "P-256" - - // p384 represents a cryptographic elliptical curve type. - p384 = "P-384" - - // p521 represents a cryptographic elliptical curve type. - p521 = "P-521" -) - -// JSONWebKey is a representation of a Jason Web Key -type JSONWebKey struct { - Kty string `json:"kty"` - Kid string `json:"kid"` - Use string `json:"use"` - N string `json:"n"` - E string `json:"e"` - Crv string `json:"crv"` - X string `json:"x"` - Y string `json:"y"` - X5c []string `json:"x5c"` -} - -type JWTValidator interface { - ValidateAndParse(ctx context.Context, token string) (*jwt.Token, error) -} - -// jwtValidatorImpl struct to handle token validation and parsing -type jwtValidatorImpl struct { - options Options -} - -var keyNotFound = errors.New("unable to find appropriate key") - -// NewJWTValidator constructor -func NewJWTValidator(ctx context.Context, issuer string, audienceList []string, keysLocation string, idpSignkeyRefreshEnabled bool) (JWTValidator, error) { - keys, err := getPemKeys(ctx, keysLocation) - if err != nil { - return nil, err - } - - var lock sync.Mutex - options := Options{ - ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) { - // Verify 'aud' claim - var checkAud bool - for _, audience := range audienceList { - checkAud = token.Claims.(jwt.MapClaims).VerifyAudience(audience, false) - if checkAud { - break - } - } - if !checkAud { - return token, errors.New("invalid audience") - } - // Verify 'issuer' claim - checkIss := token.Claims.(jwt.MapClaims).VerifyIssuer(issuer, false) - if !checkIss { - return token, errors.New("invalid issuer") - } - - // If keys are rotated, verify the keys prior to token validation - if idpSignkeyRefreshEnabled { - // If the keys are invalid, retrieve new ones - if !keys.stillValid() { - lock.Lock() - defer lock.Unlock() - - refreshedKeys, err := getPemKeys(ctx, keysLocation) - if err != nil { - log.WithContext(ctx).Debugf("cannot get JSONWebKey: %v, falling back to old keys", err) - refreshedKeys = keys - } - - log.WithContext(ctx).Debugf("keys refreshed, new UTC expiration time: %s", refreshedKeys.expiresInTime.UTC()) - - keys = refreshedKeys - } - } - - publicKey, err := getPublicKey(ctx, token, keys) - if err == nil { - return publicKey, nil - } - - msg := fmt.Sprintf("getPublicKey error: %s", err) - if errors.Is(err, keyNotFound) && !idpSignkeyRefreshEnabled { - msg = fmt.Sprintf("getPublicKey error: %s. You can enable key refresh by setting HttpServerConfig.IdpSignKeyRefreshEnabled to true in your management.json file and restart the service", err) - } - - log.WithContext(ctx).Error(msg) - - return nil, err - }, - EnableAuthOnOptions: false, - } - - if options.UserProperty == "" { - options.UserProperty = "user" - } - - return &jwtValidatorImpl{ - options: options, - }, nil -} - -// ValidateAndParse validates the token and returns the parsed token -func (m *jwtValidatorImpl) ValidateAndParse(ctx context.Context, token string) (*jwt.Token, error) { - // If the token is empty... - if token == "" { - // Check if it was required - if m.options.CredentialsOptional { - log.WithContext(ctx).Debugf("no credentials found (CredentialsOptional=true)") - // No error, just no token (and that is ok given that CredentialsOptional is true) - return nil, nil //nolint:nilnil - } - - // If we get here, the required token is missing - errorMsg := "required authorization token not found" - log.WithContext(ctx).Debugf(" Error: No credentials found (CredentialsOptional=false)") - return nil, errors.New(errorMsg) - } - - // Now parse the token - parsedToken, err := jwt.Parse(token, m.options.ValidationKeyGetter) - - // Check if there was an error in parsing... - if err != nil { - log.WithContext(ctx).Errorf("error parsing token: %v", err) - return nil, fmt.Errorf("error parsing token: %w", err) - } - - // Check if the parsed token is valid... - if !parsedToken.Valid { - errorMsg := "token is invalid" - log.WithContext(ctx).Debug(errorMsg) - return nil, errors.New(errorMsg) - } - - return parsedToken, nil -} - -// stillValid returns true if the JSONWebKey still valid and have enough time to be used -func (jwks *Jwks) stillValid() bool { - return !jwks.expiresInTime.IsZero() && time.Now().Add(5*time.Second).Before(jwks.expiresInTime) -} - -func getPemKeys(ctx context.Context, keysLocation string) (*Jwks, error) { - resp, err := http.Get(keysLocation) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - jwks := &Jwks{} - err = json.NewDecoder(resp.Body).Decode(jwks) - if err != nil { - return jwks, err - } - - cacheControlHeader := resp.Header.Get("Cache-Control") - expiresIn := getMaxAgeFromCacheHeader(ctx, cacheControlHeader) - jwks.expiresInTime = time.Now().Add(time.Duration(expiresIn) * time.Second) - - return jwks, err -} - -func getPublicKey(ctx context.Context, token *jwt.Token, jwks *Jwks) (interface{}, error) { - // todo as we load the jkws when the server is starting, we should build a JKS map with the pem cert at the boot time - - for k := range jwks.Keys { - if token.Header["kid"] != jwks.Keys[k].Kid { - continue - } - - if len(jwks.Keys[k].X5c) != 0 { - cert := "-----BEGIN CERTIFICATE-----\n" + jwks.Keys[k].X5c[0] + "\n-----END CERTIFICATE-----" - return jwt.ParseRSAPublicKeyFromPEM([]byte(cert)) - } - - if jwks.Keys[k].Kty == "RSA" { - log.WithContext(ctx).Debugf("generating PublicKey from RSA JWK") - return getPublicKeyFromRSA(jwks.Keys[k]) - } - if jwks.Keys[k].Kty == "EC" { - log.WithContext(ctx).Debugf("generating PublicKey from ECDSA JWK") - return getPublicKeyFromECDSA(jwks.Keys[k]) - } - - log.WithContext(ctx).Debugf("Key Type: %s not yet supported, please raise ticket!", jwks.Keys[k].Kty) - } - - return nil, keyNotFound -} - -func getPublicKeyFromECDSA(jwk JSONWebKey) (publicKey *ecdsa.PublicKey, err error) { - - if jwk.X == "" || jwk.Y == "" || jwk.Crv == "" { - return nil, fmt.Errorf("ecdsa key incomplete") - } - - var xCoordinate []byte - if xCoordinate, err = base64.RawURLEncoding.DecodeString(jwk.X); err != nil { - return nil, err - } - - var yCoordinate []byte - if yCoordinate, err = base64.RawURLEncoding.DecodeString(jwk.Y); err != nil { - return nil, err - } - - publicKey = &ecdsa.PublicKey{} - - var curve elliptic.Curve - switch jwk.Crv { - case p256: - curve = elliptic.P256() - case p384: - curve = elliptic.P384() - case p521: - curve = elliptic.P521() - } - - publicKey.Curve = curve - publicKey.X = big.NewInt(0).SetBytes(xCoordinate) - publicKey.Y = big.NewInt(0).SetBytes(yCoordinate) - - return publicKey, nil -} - -func getPublicKeyFromRSA(jwk JSONWebKey) (*rsa.PublicKey, error) { - - decodedE, err := base64.RawURLEncoding.DecodeString(jwk.E) - if err != nil { - return nil, err - } - decodedN, err := base64.RawURLEncoding.DecodeString(jwk.N) - if err != nil { - return nil, err - } - - var n, e big.Int - e.SetBytes(decodedE) - n.SetBytes(decodedN) - - return &rsa.PublicKey{ - E: int(e.Int64()), - N: &n, - }, nil -} - -// getMaxAgeFromCacheHeader extracts max-age directive from the Cache-Control header -func getMaxAgeFromCacheHeader(ctx context.Context, cacheControl string) int { - // Split into individual directives - directives := strings.Split(cacheControl, ",") - - for _, directive := range directives { - directive = strings.TrimSpace(directive) - if strings.HasPrefix(directive, "max-age=") { - // Extract the max-age value - maxAgeStr := strings.TrimPrefix(directive, "max-age=") - maxAge, err := strconv.Atoi(maxAgeStr) - if err != nil { - log.WithContext(ctx).Debugf("error parsing max-age: %v", err) - return 0 - } - - return maxAge - } - } - - return 0 -} - -type JwtValidatorMock struct{} - -func (j *JwtValidatorMock) ValidateAndParse(ctx context.Context, token string) (*jwt.Token, error) { - claimMaps := jwt.MapClaims{} - - switch token { - case "testUserId", "testAdminId", "testOwnerId", "testServiceUserId", "testServiceAdminId", "blockedUserId": - claimMaps[UserIDClaim] = token - claimMaps[AccountIDSuffix] = "testAccountId" - claimMaps[DomainIDSuffix] = "test.com" - claimMaps[DomainCategorySuffix] = "private" - case "otherUserId": - claimMaps[UserIDClaim] = "otherUserId" - claimMaps[AccountIDSuffix] = "otherAccountId" - claimMaps[DomainIDSuffix] = "other.com" - claimMaps[DomainCategorySuffix] = "private" - case "invalidToken": - return nil, errors.New("invalid token") - } - - jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claimMaps) - return jwtToken, nil -} - diff --git a/management/server/management_proto_test.go b/management/server/management_proto_test.go index 0df2462f4..4d0630f0f 100644 --- a/management/server/management_proto_test.go +++ b/management/server/management_proto_test.go @@ -94,7 +94,7 @@ func Test_SyncProtocol(t *testing.T) { mgmtServer, _, mgmtAddr, cleanup, err := startManagementForTest(t, "testdata/store_with_expired_peers.sql", &Config{ Stuns: []*Host{{ Proto: "udp", - URI: "stun:stun.wiretrustee.com:3468", + URI: "stun:stun.netbird.io:3468", }}, TURNConfig: &TURNConfig{ TimeBasedCredentials: false, @@ -102,12 +102,12 @@ func Test_SyncProtocol(t *testing.T) { Secret: "whatever", Turns: []*Host{{ Proto: "udp", - URI: "turn:stun.wiretrustee.com:3468", + URI: "turn:stun.netbird.io:3468", }}, }, Signal: &Host{ Proto: "http", - URI: "signal.wiretrustee.com:10000", + URI: "signal.netbird.io:10000", }, Datadir: dir, HttpConfig: nil, @@ -173,64 +173,64 @@ func Test_SyncProtocol(t *testing.T) { return } - wiretrusteeConfig := syncResp.GetWiretrusteeConfig() - if wiretrusteeConfig == nil { - t.Fatal("expecting SyncResponse to have non-nil WiretrusteeConfig") + netbirdConfig := syncResp.GetNetbirdConfig() + if netbirdConfig == nil { + t.Fatal("expecting SyncResponse to have non-nil NetbirdConfig") } - if wiretrusteeConfig.GetSignal() == nil { - t.Fatal("expecting SyncResponse to have WiretrusteeConfig with non-nil Signal config") + if netbirdConfig.GetSignal() == nil { + t.Fatal("expecting SyncResponse to have NetbirdConfig with non-nil Signal config") } expectedSignalConfig := &mgmtProto.HostConfig{ - Uri: "signal.wiretrustee.com:10000", + Uri: "signal.netbird.io:10000", Protocol: mgmtProto.HostConfig_HTTP, } - if wiretrusteeConfig.GetSignal().GetUri() != expectedSignalConfig.GetUri() { - t.Fatalf("expecting SyncResponse to have WiretrusteeConfig with expected Signal URI: %v, actual: %v", + if netbirdConfig.GetSignal().GetUri() != expectedSignalConfig.GetUri() { + t.Fatalf("expecting SyncResponse to have NetbirdConfig with expected Signal URI: %v, actual: %v", expectedSignalConfig.GetUri(), - wiretrusteeConfig.GetSignal().GetUri()) + netbirdConfig.GetSignal().GetUri()) } - if wiretrusteeConfig.GetSignal().GetProtocol() != expectedSignalConfig.GetProtocol() { - t.Fatalf("expecting SyncResponse to have WiretrusteeConfig with expected Signal Protocol: %v, actual: %v", + if netbirdConfig.GetSignal().GetProtocol() != expectedSignalConfig.GetProtocol() { + t.Fatalf("expecting SyncResponse to have NetbirdConfig with expected Signal Protocol: %v, actual: %v", expectedSignalConfig.GetProtocol().String(), - wiretrusteeConfig.GetSignal().GetProtocol()) + netbirdConfig.GetSignal().GetProtocol()) } expectedStunsConfig := &mgmtProto.HostConfig{ - Uri: "stun:stun.wiretrustee.com:3468", + Uri: "stun:stun.netbird.io:3468", Protocol: mgmtProto.HostConfig_UDP, } - if wiretrusteeConfig.GetStuns()[0].GetUri() != expectedStunsConfig.GetUri() { - t.Fatalf("expecting SyncResponse to have WiretrusteeConfig with expected STUN URI: %v, actual: %v", + if netbirdConfig.GetStuns()[0].GetUri() != expectedStunsConfig.GetUri() { + t.Fatalf("expecting SyncResponse to have NetbirdConfig with expected STUN URI: %v, actual: %v", expectedStunsConfig.GetUri(), - wiretrusteeConfig.GetStuns()[0].GetUri()) + netbirdConfig.GetStuns()[0].GetUri()) } - if wiretrusteeConfig.GetStuns()[0].GetProtocol() != expectedStunsConfig.GetProtocol() { - t.Fatalf("expecting SyncResponse to have WiretrusteeConfig with expected STUN Protocol: %v, actual: %v", + if netbirdConfig.GetStuns()[0].GetProtocol() != expectedStunsConfig.GetProtocol() { + t.Fatalf("expecting SyncResponse to have NetbirdConfig with expected STUN Protocol: %v, actual: %v", expectedStunsConfig.GetProtocol(), - wiretrusteeConfig.GetStuns()[0].GetProtocol()) + netbirdConfig.GetStuns()[0].GetProtocol()) } expectedTRUNHost := &mgmtProto.HostConfig{ - Uri: "turn:stun.wiretrustee.com:3468", + Uri: "turn:stun.netbird.io:3468", Protocol: mgmtProto.HostConfig_UDP, } - if wiretrusteeConfig.GetTurns()[0].GetHostConfig().GetUri() != expectedTRUNHost.GetUri() { - t.Fatalf("expecting SyncResponse to have WiretrusteeConfig with expected TURN URI: %v, actual: %v", + if netbirdConfig.GetTurns()[0].GetHostConfig().GetUri() != expectedTRUNHost.GetUri() { + t.Fatalf("expecting SyncResponse to have NetbirdConfig with expected TURN URI: %v, actual: %v", expectedTRUNHost.GetUri(), - wiretrusteeConfig.GetTurns()[0].GetHostConfig().GetUri()) + netbirdConfig.GetTurns()[0].GetHostConfig().GetUri()) } - if wiretrusteeConfig.GetTurns()[0].GetHostConfig().GetProtocol() != expectedTRUNHost.GetProtocol() { - t.Fatalf("expecting SyncResponse to have WiretrusteeConfig with expected TURN Protocol: %v, actual: %v", + if netbirdConfig.GetTurns()[0].GetHostConfig().GetProtocol() != expectedTRUNHost.GetProtocol() { + t.Fatalf("expecting SyncResponse to have NetbirdConfig with expected TURN Protocol: %v, actual: %v", expectedTRUNHost.GetProtocol().String(), - wiretrusteeConfig.GetTurns()[0].GetHostConfig().GetProtocol()) + netbirdConfig.GetTurns()[0].GetHostConfig().GetProtocol()) } // ensure backward compatibility @@ -285,13 +285,13 @@ func loginPeerWithValidSetupKey(key wgtypes.Key, client mgmtProto.ManagementServ } meta := &mgmtProto.PeerSystemMeta{ - Hostname: key.PublicKey().String(), - GoOS: runtime.GOOS, - OS: runtime.GOOS, - Core: "core", - Platform: "platform", - Kernel: "kernel", - WiretrusteeVersion: "", + Hostname: key.PublicKey().String(), + GoOS: runtime.GOOS, + OS: runtime.GOOS, + Core: "core", + Platform: "platform", + Kernel: "kernel", + NetbirdVersion: "", } message, err := encryption.EncryptMessage(*serverKey, key, &mgmtProto.LoginRequest{SetupKey: TestValidSetupKey, Meta: meta}) if err != nil { @@ -440,7 +440,7 @@ func startManagementForTest(t *testing.T, testFile string, config *Config) (*grp secretsManager := NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay) ephemeralMgr := NewEphemeralManager(store, accountManager) - mgmtServer, err := NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, ephemeralMgr) + mgmtServer, err := NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, ephemeralMgr, nil) if err != nil { return nil, nil, "", cleanup, err } @@ -498,7 +498,7 @@ func testSyncStatusRace(t *testing.T) { mgmtServer, am, mgmtAddr, cleanup, err := startManagementForTest(t, "testdata/store_with_expired_peers.sql", &Config{ Stuns: []*Host{{ Proto: "udp", - URI: "stun:stun.wiretrustee.com:3468", + URI: "stun:stun.netbird.io:3468", }}, TURNConfig: &TURNConfig{ TimeBasedCredentials: false, @@ -506,12 +506,12 @@ func testSyncStatusRace(t *testing.T) { Secret: "whatever", Turns: []*Host{{ Proto: "udp", - URI: "turn:stun.wiretrustee.com:3468", + URI: "turn:stun.netbird.io:3468", }}, }, Signal: &Host{ Proto: "http", - URI: "signal.wiretrustee.com:10000", + URI: "signal.netbird.io:10000", }, Datadir: dir, HttpConfig: nil, @@ -670,7 +670,7 @@ func Test_LoginPerformance(t *testing.T) { mgmtServer, am, _, cleanup, err := startManagementForTest(t, "testdata/store_with_expired_peers.sql", &Config{ Stuns: []*Host{{ Proto: "udp", - URI: "stun:stun.wiretrustee.com:3468", + URI: "stun:stun.netbird.io:3468", }}, TURNConfig: &TURNConfig{ TimeBasedCredentials: false, @@ -678,12 +678,12 @@ func Test_LoginPerformance(t *testing.T) { Secret: "whatever", Turns: []*Host{{ Proto: "udp", - URI: "turn:stun.wiretrustee.com:3468", + URI: "turn:stun.netbird.io:3468", }}, }, Signal: &Host{ Proto: "http", - URI: "signal.wiretrustee.com:10000", + URI: "signal.netbird.io:10000", }, Datadir: dir, HttpConfig: nil, @@ -714,7 +714,7 @@ func Test_LoginPerformance(t *testing.T) { return } - setupKey, err := am.CreateSetupKey(context.Background(), account.Id, fmt.Sprintf("key-%d", j), types.SetupKeyReusable, time.Hour, nil, 0, fmt.Sprintf("user-%d", j), false) + setupKey, err := am.CreateSetupKey(context.Background(), account.Id, fmt.Sprintf("key-%d", j), types.SetupKeyReusable, time.Hour, nil, 0, fmt.Sprintf("user-%d", j), false, false) if err != nil { t.Logf("error creating setup key: %v", err) return @@ -730,13 +730,13 @@ func Test_LoginPerformance(t *testing.T) { } meta := &mgmtProto.PeerSystemMeta{ - Hostname: key.PublicKey().String(), - GoOS: runtime.GOOS, - OS: runtime.GOOS, - Core: "core", - Platform: "platform", - Kernel: "kernel", - WiretrusteeVersion: "", + Hostname: key.PublicKey().String(), + GoOS: runtime.GOOS, + OS: runtime.GOOS, + Core: "core", + Platform: "platform", + Kernel: "kernel", + NetbirdVersion: "", } peerLogin := PeerLogin{ diff --git a/management/server/management_suite_test.go b/management/server/management_suite_test.go deleted file mode 100644 index cc99624a0..000000000 --- a/management/server/management_suite_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package server_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "testing" -) - -func TestManagement(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Management Service Suite") -} diff --git a/management/server/management_test.go b/management/server/management_test.go index cfa2c138f..fd82d8037 100644 --- a/management/server/management_test.go +++ b/management/server/management_test.go @@ -6,13 +6,13 @@ import ( "net" "os" "runtime" - sync2 "sync" + "sync" + "testing" "time" pb "github.com/golang/protobuf/proto" //nolint - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" @@ -30,450 +30,110 @@ import ( const ( ValidSetupKey = "A2C8E62B-38F5-4553-B31E-DD66C696CEBB" - AccountKey = "bf1c8084-ba50-4ce7-9439-34653001fc3b" ) -var _ = Describe("Management service", func() { - var ( - addr string - s *grpc.Server - dataDir string - client mgmtProto.ManagementServiceClient - serverPubKey wgtypes.Key - conn *grpc.ClientConn - ) - - BeforeEach(func() { - level, _ := log.ParseLevel("Debug") - log.SetLevel(level) - var err error - dataDir, err = os.MkdirTemp("", "wiretrustee_mgmt_test_tmp_*") - Expect(err).NotTo(HaveOccurred()) - - var listener net.Listener - - config := &server.Config{} - _, err = util.ReadJson("testdata/management.json", config) - Expect(err).NotTo(HaveOccurred()) - config.Datadir = dataDir - - s, listener = startServer(config, dataDir, "testdata/store.sql") - addr = listener.Addr().String() - client, conn = createRawClient(addr) - - // s public key - resp, err := client.GetServerKey(context.TODO(), &mgmtProto.Empty{}) - Expect(err).NotTo(HaveOccurred()) - serverPubKey, err = wgtypes.ParseKey(resp.Key) - Expect(err).NotTo(HaveOccurred()) - }) - - AfterEach(func() { - s.Stop() - err := conn.Close() - Expect(err).NotTo(HaveOccurred()) - os.RemoveAll(dataDir) - }) - - Context("when calling IsHealthy endpoint", func() { - Specify("a non-error result is returned", func() { - healthy, err := client.IsHealthy(context.TODO(), &mgmtProto.Empty{}) - - Expect(err).NotTo(HaveOccurred()) - Expect(healthy).ToNot(BeNil()) - }) - }) - - Context("when calling Sync endpoint", func() { - Context("when there is a new peer registered", func() { - Specify("a proper configuration is returned", func() { - key, _ := wgtypes.GenerateKey() - loginPeerWithValidSetupKey(serverPubKey, key, client) - - syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}} - encryptedBytes, err := encryption.EncryptMessage(serverPubKey, key, syncReq) - Expect(err).NotTo(HaveOccurred()) - - sync, err := client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ - WgPubKey: key.PublicKey().String(), - Body: encryptedBytes, - }) - Expect(err).NotTo(HaveOccurred()) - - encryptedResponse := &mgmtProto.EncryptedMessage{} - err = sync.RecvMsg(encryptedResponse) - Expect(err).NotTo(HaveOccurred()) - - resp := &mgmtProto.SyncResponse{} - err = encryption.DecryptMessage(serverPubKey, key, encryptedResponse.Body, resp) - Expect(err).NotTo(HaveOccurred()) - - expectedSignalConfig := &mgmtProto.HostConfig{ - Uri: "signal.wiretrustee.com:10000", - Protocol: mgmtProto.HostConfig_HTTP, - } - expectedStunsConfig := &mgmtProto.HostConfig{ - Uri: "stun:stun.wiretrustee.com:3468", - Protocol: mgmtProto.HostConfig_UDP, - } - expectedTRUNHost := &mgmtProto.HostConfig{ - Uri: "turn:stun.wiretrustee.com:3468", - Protocol: mgmtProto.HostConfig_UDP, - } - - Expect(resp.WiretrusteeConfig.Signal).To(BeEquivalentTo(expectedSignalConfig)) - Expect(resp.WiretrusteeConfig.Stuns).To(ConsistOf(expectedStunsConfig)) - // TURN validation is special because credentials are dynamically generated - Expect(resp.WiretrusteeConfig.Turns).To(HaveLen(1)) - actualTURN := resp.WiretrusteeConfig.Turns[0] - Expect(len(actualTURN.User) > 0).To(BeTrue()) - Expect(actualTURN.HostConfig).To(BeEquivalentTo(expectedTRUNHost)) - Expect(len(resp.NetworkMap.OfflinePeers) == 0).To(BeTrue()) - }) - }) - - Context("when there are 3 peers registered under one account", func() { - Specify("a list containing other 2 peers is returned", func() { - key, _ := wgtypes.GenerateKey() - key1, _ := wgtypes.GenerateKey() - key2, _ := wgtypes.GenerateKey() - loginPeerWithValidSetupKey(serverPubKey, key, client) - loginPeerWithValidSetupKey(serverPubKey, key1, client) - loginPeerWithValidSetupKey(serverPubKey, key2, client) - - messageBytes, err := pb.Marshal(&mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}}) - Expect(err).NotTo(HaveOccurred()) - encryptedBytes, err := encryption.Encrypt(messageBytes, serverPubKey, key) - Expect(err).NotTo(HaveOccurred()) - - sync, err := client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ - WgPubKey: key.PublicKey().String(), - Body: encryptedBytes, - }) - Expect(err).NotTo(HaveOccurred()) - - encryptedResponse := &mgmtProto.EncryptedMessage{} - err = sync.RecvMsg(encryptedResponse) - Expect(err).NotTo(HaveOccurred()) - decryptedBytes, err := encryption.Decrypt(encryptedResponse.Body, serverPubKey, key) - Expect(err).NotTo(HaveOccurred()) - - resp := &mgmtProto.SyncResponse{} - err = pb.Unmarshal(decryptedBytes, resp) - Expect(err).NotTo(HaveOccurred()) - - Expect(resp.GetRemotePeers()).To(HaveLen(2)) - peers := []string{resp.GetRemotePeers()[0].WgPubKey, resp.GetRemotePeers()[1].WgPubKey} - Expect(peers).To(ContainElements(key1.PublicKey().String(), key2.PublicKey().String())) - }) - }) - - Context("when there is a new peer registered", func() { - Specify("an update is returned", func() { - // register only a single peer - key, _ := wgtypes.GenerateKey() - loginPeerWithValidSetupKey(serverPubKey, key, client) - - messageBytes, err := pb.Marshal(&mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}}) - Expect(err).NotTo(HaveOccurred()) - encryptedBytes, err := encryption.Encrypt(messageBytes, serverPubKey, key) - Expect(err).NotTo(HaveOccurred()) - - sync, err := client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ - WgPubKey: key.PublicKey().String(), - Body: encryptedBytes, - }) - Expect(err).NotTo(HaveOccurred()) - - // after the initial sync call we have 0 peer updates - encryptedResponse := &mgmtProto.EncryptedMessage{} - err = sync.RecvMsg(encryptedResponse) - Expect(err).NotTo(HaveOccurred()) - decryptedBytes, err := encryption.Decrypt(encryptedResponse.Body, serverPubKey, key) - Expect(err).NotTo(HaveOccurred()) - resp := &mgmtProto.SyncResponse{} - err = pb.Unmarshal(decryptedBytes, resp) - Expect(resp.GetRemotePeers()).To(HaveLen(0)) - - wg := sync2.WaitGroup{} - wg.Add(1) - - // continue listening on updates for a peer - go func() { - err = sync.RecvMsg(encryptedResponse) - - decryptedBytes, err = encryption.Decrypt(encryptedResponse.Body, serverPubKey, key) - Expect(err).NotTo(HaveOccurred()) - resp = &mgmtProto.SyncResponse{} - err = pb.Unmarshal(decryptedBytes, resp) - wg.Done() - }() - - // register a new peer - key1, _ := wgtypes.GenerateKey() - loginPeerWithValidSetupKey(serverPubKey, key1, client) - - wg.Wait() - - Expect(err).NotTo(HaveOccurred()) - Expect(resp.GetRemotePeers()).To(HaveLen(1)) - Expect(resp.GetRemotePeers()[0].WgPubKey).To(BeEquivalentTo(key1.PublicKey().String())) - }) - }) - }) - - Context("when calling GetServerKey endpoint", func() { - Specify("a public Wireguard key of the service is returned", func() { - resp, err := client.GetServerKey(context.TODO(), &mgmtProto.Empty{}) - - Expect(err).NotTo(HaveOccurred()) - Expect(resp).ToNot(BeNil()) - Expect(resp.Key).ToNot(BeNil()) - Expect(resp.ExpiresAt).ToNot(BeNil()) - - // check if the key is a valid Wireguard key - key, err := wgtypes.ParseKey(resp.Key) - Expect(err).NotTo(HaveOccurred()) - Expect(key).ToNot(BeNil()) - }) - }) - - Context("when calling Login endpoint", func() { - Context("with an invalid setup key", func() { - Specify("an error is returned", func() { - key, _ := wgtypes.GenerateKey() - message, err := encryption.EncryptMessage(serverPubKey, key, &mgmtProto.LoginRequest{SetupKey: "invalid setup key", - Meta: &mgmtProto.PeerSystemMeta{}}) - Expect(err).NotTo(HaveOccurred()) - - resp, err := client.Login(context.TODO(), &mgmtProto.EncryptedMessage{ - WgPubKey: key.PublicKey().String(), - Body: message, - }) - - Expect(err).To(HaveOccurred()) - Expect(resp).To(BeNil()) - }) - }) - - Context("with a valid setup key", func() { - It("a non error result is returned", func() { - key, _ := wgtypes.GenerateKey() - resp := loginPeerWithValidSetupKey(serverPubKey, key, client) - - Expect(resp).ToNot(BeNil()) - }) - }) - - Context("with a registered peer", func() { - It("a non error result is returned", func() { - key, _ := wgtypes.GenerateKey() - regResp := loginPeerWithValidSetupKey(serverPubKey, key, client) - Expect(regResp).NotTo(BeNil()) - - // just login without registration - message, err := encryption.EncryptMessage(serverPubKey, key, &mgmtProto.LoginRequest{Meta: &mgmtProto.PeerSystemMeta{}}) - Expect(err).NotTo(HaveOccurred()) - loginResp, err := client.Login(context.TODO(), &mgmtProto.EncryptedMessage{ - WgPubKey: key.PublicKey().String(), - Body: message, - }) - - Expect(err).NotTo(HaveOccurred()) - - decryptedResp := &mgmtProto.LoginResponse{} - err = encryption.DecryptMessage(serverPubKey, key, loginResp.Body, decryptedResp) - Expect(err).NotTo(HaveOccurred()) - - expectedSignalConfig := &mgmtProto.HostConfig{ - Uri: "signal.wiretrustee.com:10000", - Protocol: mgmtProto.HostConfig_HTTP, - } - expectedStunsConfig := &mgmtProto.HostConfig{ - Uri: "stun:stun.wiretrustee.com:3468", - Protocol: mgmtProto.HostConfig_UDP, - } - expectedTurnsConfig := &mgmtProto.ProtectedHostConfig{ - HostConfig: &mgmtProto.HostConfig{ - Uri: "turn:stun.wiretrustee.com:3468", - Protocol: mgmtProto.HostConfig_UDP, - }, - User: "some_user", - Password: "some_password", - } - - Expect(decryptedResp.GetWiretrusteeConfig().Signal).To(BeEquivalentTo(expectedSignalConfig)) - Expect(decryptedResp.GetWiretrusteeConfig().Stuns).To(ConsistOf(expectedStunsConfig)) - Expect(decryptedResp.GetWiretrusteeConfig().Turns).To(ConsistOf(expectedTurnsConfig)) - }) - }) - }) - - Context("when there are 10 peers registered under one account", func() { - Context("when there are 10 more peers registered under the same account", func() { - Specify("all of the 10 peers will get updates of 10 newly registered peers", func() { - initialPeers := 10 - additionalPeers := 10 - - var peers []wgtypes.Key - for i := 0; i < initialPeers; i++ { - key, _ := wgtypes.GenerateKey() - loginPeerWithValidSetupKey(serverPubKey, key, client) - peers = append(peers, key) - } - - wg := sync2.WaitGroup{} - wg.Add(initialPeers + initialPeers*additionalPeers) - - var clients []mgmtProto.ManagementService_SyncClient - for _, peer := range peers { - messageBytes, err := pb.Marshal(&mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}}) - Expect(err).NotTo(HaveOccurred()) - encryptedBytes, err := encryption.Encrypt(messageBytes, serverPubKey, peer) - Expect(err).NotTo(HaveOccurred()) - - // open stream - sync, err := client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ - WgPubKey: peer.PublicKey().String(), - Body: encryptedBytes, - }) - Expect(err).NotTo(HaveOccurred()) - clients = append(clients, sync) - - // receive stream - peer := peer - go func() { - for { - encryptedResponse := &mgmtProto.EncryptedMessage{} - err = sync.RecvMsg(encryptedResponse) - if err != nil { - break - } - decryptedBytes, err := encryption.Decrypt(encryptedResponse.Body, serverPubKey, peer) - Expect(err).NotTo(HaveOccurred()) - - resp := &mgmtProto.SyncResponse{} - err = pb.Unmarshal(decryptedBytes, resp) - Expect(err).NotTo(HaveOccurred()) - if len(resp.GetRemotePeers()) > 0 { - // only consider peer updates - wg.Done() - } - } - }() - } - - time.Sleep(1 * time.Second) - for i := 0; i < additionalPeers; i++ { - key, _ := wgtypes.GenerateKey() - loginPeerWithValidSetupKey(serverPubKey, key, client) - r := rand.New(rand.NewSource(time.Now().UnixNano())) - n := r.Intn(200) - time.Sleep(time.Duration(n) * time.Millisecond) - } - - wg.Wait() - - for _, syncClient := range clients { - err := syncClient.CloseSend() - Expect(err).NotTo(HaveOccurred()) - } - }) - }) - }) - - Context("when there are peers registered under one account concurrently", func() { - Specify("then there are no duplicate IPs", func() { - initialPeers := 30 - - ipChannel := make(chan string, 20) - for i := 0; i < initialPeers; i++ { - go func() { - defer GinkgoRecover() - key, _ := wgtypes.GenerateKey() - loginPeerWithValidSetupKey(serverPubKey, key, client) - syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}} - encryptedBytes, err := encryption.EncryptMessage(serverPubKey, key, syncReq) - Expect(err).NotTo(HaveOccurred()) - - // open stream - sync, err := client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ - WgPubKey: key.PublicKey().String(), - Body: encryptedBytes, - }) - Expect(err).NotTo(HaveOccurred()) - encryptedResponse := &mgmtProto.EncryptedMessage{} - err = sync.RecvMsg(encryptedResponse) - Expect(err).NotTo(HaveOccurred()) - - resp := &mgmtProto.SyncResponse{} - err = encryption.DecryptMessage(serverPubKey, key, encryptedResponse.Body, resp) - Expect(err).NotTo(HaveOccurred()) - - ipChannel <- resp.GetPeerConfig().Address - }() - } - - ips := make(map[string]struct{}) - for ip := range ipChannel { - if _, ok := ips[ip]; ok { - Fail("found duplicate IP: " + ip) - } - ips[ip] = struct{}{} - if len(ips) == initialPeers { - break - } - } - close(ipChannel) - }) - }) - - Context("after login two peers", func() { - Specify("then they receive the same network", func() { - key, _ := wgtypes.GenerateKey() - firstLogin := loginPeerWithValidSetupKey(serverPubKey, key, client) - key, _ = wgtypes.GenerateKey() - secondLogin := loginPeerWithValidSetupKey(serverPubKey, key, client) - - _, firstLoginNetwork, err := net.ParseCIDR(firstLogin.GetPeerConfig().GetAddress()) - Expect(err).NotTo(HaveOccurred()) - _, secondLoginNetwork, err := net.ParseCIDR(secondLogin.GetPeerConfig().GetAddress()) - Expect(err).NotTo(HaveOccurred()) - - Expect(secondLoginNetwork.String()).To(BeEquivalentTo(firstLoginNetwork.String())) - }) - }) -}) - -func loginPeerWithValidSetupKey(serverPubKey wgtypes.Key, key wgtypes.Key, client mgmtProto.ManagementServiceClient) *mgmtProto.LoginResponse { - defer GinkgoRecover() - - meta := &mgmtProto.PeerSystemMeta{ - Hostname: key.PublicKey().String(), - GoOS: runtime.GOOS, - OS: runtime.GOOS, - Core: "core", - Platform: "platform", - Kernel: "kernel", - WiretrusteeVersion: "", +type testSuite struct { + t *testing.T + addr string + grpcServer *grpc.Server + dataDir string + client mgmtProto.ManagementServiceClient + serverPubKey wgtypes.Key + conn *grpc.ClientConn +} + +func setupTest(t *testing.T) *testSuite { + t.Helper() + level, _ := log.ParseLevel("Debug") + log.SetLevel(level) + + ts := &testSuite{t: t} + + var err error + ts.dataDir, err = os.MkdirTemp("", "netbird_mgmt_test_tmp_*") + if err != nil { + t.Fatalf("failed to create temp directory: %v", err) + } + + config := &server.Config{} + _, err = util.ReadJson("testdata/management.json", config) + if err != nil { + t.Fatalf("failed to read management.json: %v", err) + } + config.Datadir = ts.dataDir + + var listener net.Listener + ts.grpcServer, listener = startServer(t, config, ts.dataDir, "testdata/store.sql") + ts.addr = listener.Addr().String() + + ts.client, ts.conn = createRawClient(t, ts.addr) + + resp, err := ts.client.GetServerKey(context.TODO(), &mgmtProto.Empty{}) + if err != nil { + t.Fatalf("failed to get server key: %v", err) + } + + serverKey, err := wgtypes.ParseKey(resp.Key) + if err != nil { + t.Fatalf("failed to parse server key: %v", err) + } + ts.serverPubKey = serverKey + + return ts +} + +func tearDownTest(t *testing.T, ts *testSuite) { + t.Helper() + ts.grpcServer.Stop() + if err := ts.conn.Close(); err != nil { + t.Fatalf("failed to close client connection: %v", err) + } + time.Sleep(100 * time.Millisecond) + if err := os.RemoveAll(ts.dataDir); err != nil { + t.Fatalf("failed to remove data directory %s: %v", ts.dataDir, err) + } +} + +func loginPeerWithValidSetupKey( + t *testing.T, + serverPubKey wgtypes.Key, + key wgtypes.Key, + client mgmtProto.ManagementServiceClient, +) *mgmtProto.LoginResponse { + t.Helper() + meta := &mgmtProto.PeerSystemMeta{ + Hostname: key.PublicKey().String(), + GoOS: runtime.GOOS, + OS: runtime.GOOS, + Core: "core", + Platform: "platform", + Kernel: "kernel", + NetbirdVersion: "", + } + msgToEncrypt := &mgmtProto.LoginRequest{SetupKey: ValidSetupKey, Meta: meta} + message, err := encryption.EncryptMessage(serverPubKey, key, msgToEncrypt) + if err != nil { + t.Fatalf("failed to encrypt login request: %v", err) } - message, err := encryption.EncryptMessage(serverPubKey, key, &mgmtProto.LoginRequest{SetupKey: ValidSetupKey, Meta: meta}) - Expect(err).NotTo(HaveOccurred()) resp, err := client.Login(context.TODO(), &mgmtProto.EncryptedMessage{ WgPubKey: key.PublicKey().String(), Body: message, }) - - Expect(err).NotTo(HaveOccurred()) + if err != nil { + t.Fatalf("login request failed: %v", err) + } loginResp := &mgmtProto.LoginResponse{} err = encryption.DecryptMessage(serverPubKey, key, resp.Body, loginResp) - Expect(err).NotTo(HaveOccurred()) + if err != nil { + t.Fatalf("failed to decrypt login response: %v", err) + } return loginResp } -func createRawClient(addr string) (mgmtProto.ManagementServiceClient, *grpc.ClientConn) { +func createRawClient(t *testing.T, addr string) (mgmtProto.ManagementServiceClient, *grpc.ClientConn) { + t.Helper() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() @@ -484,17 +144,27 @@ func createRawClient(addr string) (mgmtProto.ManagementServiceClient, *grpc.Clie Time: 10 * time.Second, Timeout: 2 * time.Second, })) - Expect(err).NotTo(HaveOccurred()) + if err != nil { + t.Fatalf("failed to dial gRPC server: %v", err) + } return mgmtProto.NewManagementServiceClient(conn), conn } -func startServer(config *server.Config, dataDir string, testFile string) (*grpc.Server, net.Listener) { +func startServer( + t *testing.T, + config *server.Config, + dataDir string, + testFile string, +) (*grpc.Server, net.Listener) { + t.Helper() lis, err := net.Listen("tcp", ":0") - Expect(err).NotTo(HaveOccurred()) + if err != nil { + t.Fatalf("failed to listen on a random port: %v", err) + } s := grpc.NewServer() - store, _, err := store.NewTestStoreFromSQL(context.Background(), testFile, dataDir) + str, _, err := store.NewTestStoreFromSQL(context.Background(), testFile, dataDir) if err != nil { log.Fatalf("failed creating a store: %s: %v", config.Datadir, err) } @@ -504,23 +174,530 @@ func startServer(config *server.Config, dataDir string, testFile string) (*grpc. metrics, err := telemetry.NewDefaultAppMetrics(context.Background()) if err != nil { - log.Fatalf("failed creating metrics: %v", err) + t.Fatalf("failed creating metrics: %v", err) } - accountManager, err := server.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, server.MocIntegratedValidator{}, metrics) + accountManager, err := server.BuildManager( + context.Background(), + str, + peersUpdateManager, + nil, + "", + "netbird.selfhosted", + eventStore, + nil, + false, + server.MocIntegratedValidator{}, + metrics, + ) if err != nil { - log.Fatalf("failed creating a manager: %v", err) + t.Fatalf("failed creating an account manager: %v", err) } secretsManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay) - mgmtServer, err := server.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil) - Expect(err).NotTo(HaveOccurred()) + mgmtServer, err := server.NewServer( + context.Background(), + config, + accountManager, + settings.NewManager(str), + peersUpdateManager, + secretsManager, + nil, + nil, + nil, + ) + if err != nil { + t.Fatalf("failed creating management server: %v", err) + } + mgmtProto.RegisterManagementServiceServer(s, mgmtServer) + go func() { if err := s.Serve(lis); err != nil { - Expect(err).NotTo(HaveOccurred()) + t.Errorf("failed to serve gRPC: %v", err) + return } }() return s, lis } + +func TestIsHealthy(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + healthy, err := ts.client.IsHealthy(context.TODO(), &mgmtProto.Empty{}) + if err != nil { + t.Fatalf("IsHealthy call returned an error: %v", err) + } + if healthy == nil { + t.Fatal("IsHealthy returned a nil response") + } +} + +func TestSyncNewPeerConfiguration(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + peerKey, _ := wgtypes.GenerateKey() + loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey, ts.client) + + syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}} + encryptedBytes, err := encryption.EncryptMessage(ts.serverPubKey, peerKey, syncReq) + if err != nil { + t.Fatalf("failed to encrypt sync request: %v", err) + } + + syncStream, err := ts.client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ + WgPubKey: peerKey.PublicKey().String(), + Body: encryptedBytes, + }) + if err != nil { + t.Fatalf("failed to call Sync: %v", err) + } + + encryptedResponse := &mgmtProto.EncryptedMessage{} + err = syncStream.RecvMsg(encryptedResponse) + if err != nil { + t.Fatalf("failed to receive sync response message: %v", err) + } + + resp := &mgmtProto.SyncResponse{} + err = encryption.DecryptMessage(ts.serverPubKey, peerKey, encryptedResponse.Body, resp) + if err != nil { + t.Fatalf("failed to decrypt sync response: %v", err) + } + + expectedSignalConfig := &mgmtProto.HostConfig{ + Uri: "signal.netbird.io:10000", + Protocol: mgmtProto.HostConfig_HTTP, + } + expectedStunsConfig := &mgmtProto.HostConfig{ + Uri: "stun:stun.netbird.io:3468", + Protocol: mgmtProto.HostConfig_UDP, + } + expectedTRUNHost := &mgmtProto.HostConfig{ + Uri: "turn:stun.netbird.io:3468", + Protocol: mgmtProto.HostConfig_UDP, + } + + assert.NotNil(t, resp.NetbirdConfig) + assert.Equal(t, resp.NetbirdConfig.Signal, expectedSignalConfig) + assert.Contains(t, resp.NetbirdConfig.Stuns, expectedStunsConfig) + assert.Equal(t, len(resp.NetbirdConfig.Turns), 1) + actualTURN := resp.NetbirdConfig.Turns[0] + assert.Greater(t, len(actualTURN.User), 0) + assert.Equal(t, actualTURN.HostConfig, expectedTRUNHost) + assert.Equal(t, len(resp.NetworkMap.OfflinePeers), 0) +} + +func TestSyncThreePeers(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + peerKey, _ := wgtypes.GenerateKey() + peerKey1, _ := wgtypes.GenerateKey() + peerKey2, _ := wgtypes.GenerateKey() + + loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey, ts.client) + loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey1, ts.client) + loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey2, ts.client) + + syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}} + syncBytes, err := pb.Marshal(syncReq) + if err != nil { + t.Fatalf("failed to marshal sync request: %v", err) + } + encryptedBytes, err := encryption.Encrypt(syncBytes, ts.serverPubKey, peerKey) + if err != nil { + t.Fatalf("failed to encrypt sync request: %v", err) + } + + syncStream, err := ts.client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ + WgPubKey: peerKey.PublicKey().String(), + Body: encryptedBytes, + }) + if err != nil { + t.Fatalf("failed to call Sync: %v", err) + } + + encryptedResponse := &mgmtProto.EncryptedMessage{} + err = syncStream.RecvMsg(encryptedResponse) + if err != nil { + t.Fatalf("failed to receive sync response: %v", err) + } + + decryptedBytes, err := encryption.Decrypt(encryptedResponse.Body, ts.serverPubKey, peerKey) + if err != nil { + t.Fatalf("failed to decrypt sync response: %v", err) + } + + resp := &mgmtProto.SyncResponse{} + err = pb.Unmarshal(decryptedBytes, resp) + if err != nil { + t.Fatalf("failed to unmarshal sync response: %v", err) + } + + if len(resp.GetRemotePeers()) != 2 { + t.Fatalf("expected 2 remote peers, got %d", len(resp.GetRemotePeers())) + } + + var found1, found2 bool + for _, rp := range resp.GetRemotePeers() { + if rp.WgPubKey == peerKey1.PublicKey().String() { + found1 = true + } else if rp.WgPubKey == peerKey2.PublicKey().String() { + found2 = true + } + } + if !found1 || !found2 { + t.Fatalf("did not find the expected peer keys %s, %s among %v", + peerKey1.PublicKey().String(), + peerKey2.PublicKey().String(), + resp.GetRemotePeers()) + } +} + +func TestSyncNewPeerUpdate(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + peerKey, _ := wgtypes.GenerateKey() + loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey, ts.client) + + syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}} + syncBytes, err := pb.Marshal(syncReq) + if err != nil { + t.Fatalf("failed to marshal sync request: %v", err) + } + + encryptedBytes, err := encryption.Encrypt(syncBytes, ts.serverPubKey, peerKey) + if err != nil { + t.Fatalf("failed to encrypt sync request: %v", err) + } + + syncStream, err := ts.client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ + WgPubKey: peerKey.PublicKey().String(), + Body: encryptedBytes, + }) + if err != nil { + t.Fatalf("failed to call Sync: %v", err) + } + + encryptedResponse := &mgmtProto.EncryptedMessage{} + err = syncStream.RecvMsg(encryptedResponse) + if err != nil { + t.Fatalf("failed to receive first sync response: %v", err) + } + + decryptedBytes, err := encryption.Decrypt(encryptedResponse.Body, ts.serverPubKey, peerKey) + if err != nil { + t.Fatalf("failed to decrypt first sync response: %v", err) + } + + resp := &mgmtProto.SyncResponse{} + if err := pb.Unmarshal(decryptedBytes, resp); err != nil { + t.Fatalf("failed to unmarshal first sync response: %v", err) + } + + if len(resp.GetRemotePeers()) != 0 { + t.Fatalf("expected 0 remote peers at first sync, got %d", len(resp.GetRemotePeers())) + } + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + encryptedResponse := &mgmtProto.EncryptedMessage{} + err = syncStream.RecvMsg(encryptedResponse) + if err != nil { + t.Errorf("failed to receive second sync response: %v", err) + return + } + + decryptedBytes, err := encryption.Decrypt(encryptedResponse.Body, ts.serverPubKey, peerKey) + if err != nil { + t.Errorf("failed to decrypt second sync response: %v", err) + return + } + err = pb.Unmarshal(decryptedBytes, resp) + if err != nil { + t.Errorf("failed to unmarshal second sync response: %v", err) + return + } + }() + + newPeerKey, _ := wgtypes.GenerateKey() + loginPeerWithValidSetupKey(t, ts.serverPubKey, newPeerKey, ts.client) + + wg.Wait() + + if len(resp.GetRemotePeers()) != 1 { + t.Fatalf("expected exactly 1 remote peer update, got %d", len(resp.GetRemotePeers())) + } + if resp.GetRemotePeers()[0].WgPubKey != newPeerKey.PublicKey().String() { + t.Fatalf("expected new peer key %s, got %s", + newPeerKey.PublicKey().String(), + resp.GetRemotePeers()[0].WgPubKey) + } +} + +func TestGetServerKey(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + resp, err := ts.client.GetServerKey(context.TODO(), &mgmtProto.Empty{}) + if err != nil { + t.Fatalf("GetServerKey returned error: %v", err) + } + if resp == nil { + t.Fatal("GetServerKey returned nil response") + } + if resp.Key == "" { + t.Fatal("GetServerKey returned empty key") + } + if resp.ExpiresAt.AsTime().IsZero() { + t.Fatal("GetServerKey returned 0 for ExpiresAt") + } + + _, err = wgtypes.ParseKey(resp.Key) + if err != nil { + t.Fatalf("GetServerKey returned an invalid WG key: %v", err) + } +} + +func TestLoginInvalidSetupKey(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + peerKey, _ := wgtypes.GenerateKey() + request := &mgmtProto.LoginRequest{ + SetupKey: "invalid setup key", + Meta: &mgmtProto.PeerSystemMeta{}, + } + encryptedMsg, err := encryption.EncryptMessage(ts.serverPubKey, peerKey, request) + if err != nil { + t.Fatalf("failed to encrypt login request: %v", err) + } + + resp, err := ts.client.Login(context.TODO(), &mgmtProto.EncryptedMessage{ + WgPubKey: peerKey.PublicKey().String(), + Body: encryptedMsg, + }) + if err == nil { + t.Fatal("expected error for invalid setup key but got nil") + } + if resp != nil { + t.Fatalf("expected nil response for invalid setup key but got: %+v", resp) + } +} + +func TestLoginValidSetupKey(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + peerKey, _ := wgtypes.GenerateKey() + resp := loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey, ts.client) + if resp == nil { + t.Fatal("loginPeerWithValidSetupKey returned nil, expected a valid response") + } +} + +func TestLoginRegisteredPeer(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + peerKey, _ := wgtypes.GenerateKey() + regResp := loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey, ts.client) + if regResp == nil { + t.Fatal("registration with valid setup key failed") + } + + loginReq := &mgmtProto.LoginRequest{Meta: &mgmtProto.PeerSystemMeta{}} + encryptedLogin, err := encryption.EncryptMessage(ts.serverPubKey, peerKey, loginReq) + if err != nil { + t.Fatalf("failed to encrypt login request: %v", err) + } + loginRespEnc, err := ts.client.Login(context.TODO(), &mgmtProto.EncryptedMessage{ + WgPubKey: peerKey.PublicKey().String(), + Body: encryptedLogin, + }) + if err != nil { + t.Fatalf("login call returned an error: %v", err) + } + + loginResp := &mgmtProto.LoginResponse{} + err = encryption.DecryptMessage(ts.serverPubKey, peerKey, loginRespEnc.Body, loginResp) + if err != nil { + t.Fatalf("failed to decrypt login response: %v", err) + } + + expectedSignalConfig := &mgmtProto.HostConfig{ + Uri: "signal.netbird.io:10000", + Protocol: mgmtProto.HostConfig_HTTP, + } + expectedStunsConfig := &mgmtProto.HostConfig{ + Uri: "stun:stun.netbird.io:3468", + Protocol: mgmtProto.HostConfig_UDP, + } + expectedTurnsConfig := &mgmtProto.ProtectedHostConfig{ + HostConfig: &mgmtProto.HostConfig{ + Uri: "turn:stun.netbird.io:3468", + Protocol: mgmtProto.HostConfig_UDP, + }, + User: "some_user", + Password: "some_password", + } + + assert.NotNil(t, loginResp.GetNetbirdConfig()) + assert.Equal(t, loginResp.GetNetbirdConfig().Signal, expectedSignalConfig) + assert.Contains(t, loginResp.GetNetbirdConfig().Stuns, expectedStunsConfig) + assert.Contains(t, loginResp.GetNetbirdConfig().Turns, expectedTurnsConfig) +} + +func TestSync10PeersGetUpdates(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + initialPeers := 10 + additionalPeers := 10 + + var peers []wgtypes.Key + for i := 0; i < initialPeers; i++ { + key, _ := wgtypes.GenerateKey() + loginPeerWithValidSetupKey(t, ts.serverPubKey, key, ts.client) + peers = append(peers, key) + } + + var wg sync.WaitGroup + wg.Add(initialPeers + initialPeers*additionalPeers) + + var syncClients []mgmtProto.ManagementService_SyncClient + for _, pk := range peers { + syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}} + msgBytes, err := pb.Marshal(syncReq) + if err != nil { + t.Fatalf("failed to marshal SyncRequest: %v", err) + } + encBytes, err := encryption.Encrypt(msgBytes, ts.serverPubKey, pk) + if err != nil { + t.Fatalf("failed to encrypt SyncRequest: %v", err) + } + + s, err := ts.client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ + WgPubKey: pk.PublicKey().String(), + Body: encBytes, + }) + if err != nil { + t.Fatalf("failed to call Sync for peer: %v", err) + } + syncClients = append(syncClients, s) + + go func(pk wgtypes.Key, syncStream mgmtProto.ManagementService_SyncClient) { + for { + encMsg := &mgmtProto.EncryptedMessage{} + err := syncStream.RecvMsg(encMsg) + if err != nil { + return + } + decryptedBytes, decErr := encryption.Decrypt(encMsg.Body, ts.serverPubKey, pk) + if decErr != nil { + t.Errorf("failed to decrypt SyncResponse for peer %s: %v", pk.PublicKey().String(), decErr) + return + } + resp := &mgmtProto.SyncResponse{} + umErr := pb.Unmarshal(decryptedBytes, resp) + if umErr != nil { + t.Errorf("failed to unmarshal SyncResponse for peer %s: %v", pk.PublicKey().String(), umErr) + return + } + // We only count if there's a new peer update + if len(resp.GetRemotePeers()) > 0 { + wg.Done() + } + } + }(pk, s) + } + + time.Sleep(500 * time.Millisecond) + for i := 0; i < additionalPeers; i++ { + key, _ := wgtypes.GenerateKey() + loginPeerWithValidSetupKey(t, ts.serverPubKey, key, ts.client) + r := rand.New(rand.NewSource(time.Now().UnixNano())) + n := r.Intn(200) + time.Sleep(time.Duration(n) * time.Millisecond) + } + + wg.Wait() + + for _, sc := range syncClients { + err := sc.CloseSend() + if err != nil { + t.Fatalf("failed to close sync client: %v", err) + } + } +} + +func TestConcurrentPeersNoDuplicateIPs(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + initialPeers := 30 + ipChan := make(chan string, initialPeers) + + var wg sync.WaitGroup + wg.Add(initialPeers) + + for i := 0; i < initialPeers; i++ { + go func() { + defer wg.Done() + key, _ := wgtypes.GenerateKey() + loginPeerWithValidSetupKey(t, ts.serverPubKey, key, ts.client) + + syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}} + encryptedBytes, err := encryption.EncryptMessage(ts.serverPubKey, key, syncReq) + if err != nil { + t.Errorf("failed to encrypt sync request: %v", err) + return + } + + s, err := ts.client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ + WgPubKey: key.PublicKey().String(), + Body: encryptedBytes, + }) + if err != nil { + t.Errorf("failed to call Sync: %v", err) + return + } + + encResp := &mgmtProto.EncryptedMessage{} + if err = s.RecvMsg(encResp); err != nil { + t.Errorf("failed to receive sync response: %v", err) + return + } + + resp := &mgmtProto.SyncResponse{} + if err = encryption.DecryptMessage(ts.serverPubKey, key, encResp.Body, resp); err != nil { + t.Errorf("failed to decrypt sync response: %v", err) + return + } + ipChan <- resp.GetPeerConfig().Address + }() + } + + wg.Wait() + close(ipChan) + + ipMap := make(map[string]bool) + for ip := range ipChan { + if ipMap[ip] { + t.Fatalf("found duplicate IP: %s", ip) + } + ipMap[ip] = true + } + + // Ensure we collected all peers + if len(ipMap) != initialPeers { + t.Fatalf("expected %d unique IPs, got %d", initialPeers, len(ipMap)) + } +} diff --git a/management/server/migration/migration.go b/management/server/migration/migration.go index 8986d77b5..d7abbad47 100644 --- a/management/server/migration/migration.go +++ b/management/server/migration/migration.go @@ -330,10 +330,7 @@ func MigrateNewField[T any](ctx context.Context, db *gorm.DB, columnName string, } var rows []map[string]any - if err := tx.Table(tableName). - Select("id", columnName). - Where(columnName + " IS NULL OR " + columnName + " = ''"). - Find(&rows).Error; err != nil { + if err := tx.Table(tableName).Select("id", columnName).Where(columnName + " IS NULL").Find(&rows).Error; err != nil { return fmt.Errorf("failed to find rows with empty %s: %w", columnName, err) } diff --git a/management/server/migration/migration_test.go b/management/server/migration/migration_test.go index a645ae325..e907d6853 100644 --- a/management/server/migration/migration_test.go +++ b/management/server/migration/migration_test.go @@ -21,9 +21,7 @@ import ( func setupDatabase(t *testing.T) *gorm.DB { t.Helper() - db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{ - PrepareStmt: true, - }) + db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) require.NoError(t, err, "Failed to open database") return db diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index bcb7f0642..67c23b95d 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -13,23 +13,25 @@ import ( "github.com/netbirdio/netbird/management/domain" "github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server/activity" + nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/idp" - "github.com/netbirdio/netbird/management/server/jwtclaims" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/route" ) +var _ server.AccountManager = (*MockAccountManager)(nil) + type MockAccountManager struct { GetOrCreateAccountByUserFunc func(ctx context.Context, userId, domain string) (*types.Account, error) GetAccountFunc func(ctx context.Context, accountID string) (*types.Account, error) CreateSetupKeyFunc func(ctx context.Context, accountId string, keyName string, keyType types.SetupKeyType, - expiresIn time.Duration, autoGroups []string, usageLimit int, userID string, ephemeral bool) (*types.SetupKey, error) + expiresIn time.Duration, autoGroups []string, usageLimit int, userID string, ephemeral bool, allowExtraDNSLabels bool) (*types.SetupKey, error) GetSetupKeyFunc func(ctx context.Context, accountID, userID, keyID string) (*types.SetupKey, error) AccountExistsFunc func(ctx context.Context, accountID string) (bool, error) GetAccountIDByUserIdFunc func(ctx context.Context, userId, domain string) (string, error) - GetUserFunc func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (*types.User, error) + GetUserFromUserAuthFunc func(ctx context.Context, userAuth nbcontext.UserAuth) (*types.User, error) ListUsersFunc func(ctx context.Context, accountID string) ([]*types.User, error) GetPeersFunc func(ctx context.Context, accountID, userID string) ([]*nbpeer.Peer, error) MarkPeerConnectedFunc func(ctx context.Context, peerKey string, connected bool, realIP net.IP) error @@ -53,9 +55,7 @@ type MockAccountManager struct { SavePolicyFunc func(ctx context.Context, accountID, userID string, policy *types.Policy) (*types.Policy, error) DeletePolicyFunc func(ctx context.Context, accountID, policyID, userID string) error ListPoliciesFunc func(ctx context.Context, accountID, userID string) ([]*types.Policy, error) - GetUsersFromAccountFunc func(ctx context.Context, accountID, userID string) ([]*types.UserInfo, error) - GetAccountInfoFromPATFunc func(ctx context.Context, token string) (*types.User, *types.PersonalAccessToken, string, string, error) - MarkPATUsedFunc func(ctx context.Context, pat string) error + GetUsersFromAccountFunc func(ctx context.Context, accountID, userID string) (map[string]*types.UserInfo, error) UpdatePeerMetaFunc func(ctx context.Context, peerID string, meta nbpeer.PeerSystemMeta) error UpdatePeerFunc func(ctx context.Context, accountID, userID string, peer *nbpeer.Peer) (*nbpeer.Peer, error) CreateRouteFunc func(ctx context.Context, accountID string, prefix netip.Prefix, networkType route.NetworkType, domains domain.List, peer string, peerGroups []string, description string, netID route.NetID, masquerade bool, metric int, groups, accessControlGroupIDs []string, enabled bool, userID string, keepRoute bool) (*route.Route, error) @@ -69,7 +69,7 @@ type MockAccountManager struct { SaveOrAddUserFunc func(ctx context.Context, accountID, userID string, user *types.User, addIfNotExists bool) (*types.UserInfo, error) SaveOrAddUsersFunc func(ctx context.Context, accountID, initiatorUserID string, update []*types.User, addIfNotExists bool) ([]*types.UserInfo, error) DeleteUserFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserID string) error - DeleteRegularUsersFunc func(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string) error + DeleteRegularUsersFunc func(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string, userInfos map[string]*types.UserInfo) error CreatePATFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserId string, tokenName string, expiresIn int) (*types.PersonalAccessTokenGenerated, error) DeletePATFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserId string, tokenID string) error GetPATFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserId string, tokenID string) (*types.PersonalAccessToken, error) @@ -80,8 +80,7 @@ type MockAccountManager struct { DeleteNameServerGroupFunc func(ctx context.Context, accountID, nsGroupID, userID string) error ListNameServerGroupsFunc func(ctx context.Context, accountID string, userID string) ([]*nbdns.NameServerGroup, error) CreateUserFunc func(ctx context.Context, accountID, userID string, key *types.UserInfo) (*types.UserInfo, error) - GetAccountIDFromTokenFunc func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) - CheckUserAccessByJWTGroupsFunc func(ctx context.Context, claims jwtclaims.AuthorizationClaims) error + GetAccountIDFromUserAuthFunc func(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error) DeleteAccountFunc func(ctx context.Context, accountID, userID string) error GetDNSDomainFunc func() string StoreEventFunc func(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any) @@ -110,6 +109,7 @@ type MockAccountManager struct { GetUserByIDFunc func(ctx context.Context, id string) (*types.User, error) GetAccountSettingsFunc func(ctx context.Context, accountID string, userID string) (*types.Settings, error) DeleteSetupKeyFunc func(ctx context.Context, accountID, userID, keyID string) error + BuildUserInfosForAccountFunc func(ctx context.Context, accountID, initiatorUserID string, accountUsers []*types.User) (map[string]*types.UserInfo, error) } func (am *MockAccountManager) UpdateAccountPeers(ctx context.Context, accountID string) { @@ -165,7 +165,7 @@ func (am *MockAccountManager) GetAllGroups(ctx context.Context, accountID, userI } // GetUsersFromAccount mock implementation of GetUsersFromAccount from server.AccountManager interface -func (am *MockAccountManager) GetUsersFromAccount(ctx context.Context, accountID string, userID string) ([]*types.UserInfo, error) { +func (am *MockAccountManager) GetUsersFromAccount(ctx context.Context, accountID string, userID string) (map[string]*types.UserInfo, error) { if am.GetUsersFromAccountFunc != nil { return am.GetUsersFromAccountFunc(ctx, accountID, userID) } @@ -204,9 +204,10 @@ func (am *MockAccountManager) CreateSetupKey( usageLimit int, userID string, ephemeral bool, + allowExtraDNSLabels bool, ) (*types.SetupKey, error) { if am.CreateSetupKeyFunc != nil { - return am.CreateSetupKeyFunc(ctx, accountID, keyName, keyType, expiresIn, autoGroups, usageLimit, userID, ephemeral) + return am.CreateSetupKeyFunc(ctx, accountID, keyName, keyType, expiresIn, autoGroups, usageLimit, userID, ephemeral, allowExtraDNSLabels) } return nil, status.Errorf(codes.Unimplemented, "method CreateSetupKey is not implemented") } @@ -238,14 +239,6 @@ func (am *MockAccountManager) MarkPeerConnected(ctx context.Context, peerKey str return status.Errorf(codes.Unimplemented, "method MarkPeerConnected is not implemented") } -// GetAccountInfoFromPAT mock implementation of GetAccountInfoFromPAT from server.AccountManager interface -func (am *MockAccountManager) GetAccountInfoFromPAT(ctx context.Context, pat string) (*types.User, *types.PersonalAccessToken, string, string, error) { - if am.GetAccountInfoFromPATFunc != nil { - return am.GetAccountInfoFromPATFunc(ctx, pat) - } - return nil, nil, "", "", status.Errorf(codes.Unimplemented, "method GetAccountInfoFromPAT is not implemented") -} - // DeleteAccount mock implementation of DeleteAccount from server.AccountManager interface func (am *MockAccountManager) DeleteAccount(ctx context.Context, accountID, userID string) error { if am.DeleteAccountFunc != nil { @@ -254,14 +247,6 @@ func (am *MockAccountManager) DeleteAccount(ctx context.Context, accountID, user return status.Errorf(codes.Unimplemented, "method DeleteAccount is not implemented") } -// MarkPATUsed mock implementation of MarkPATUsed from server.AccountManager interface -func (am *MockAccountManager) MarkPATUsed(ctx context.Context, pat string) error { - if am.MarkPATUsedFunc != nil { - return am.MarkPATUsedFunc(ctx, pat) - } - return status.Errorf(codes.Unimplemented, "method MarkPATUsed is not implemented") -} - // CreatePAT mock implementation of GetPAT from server.AccountManager interface func (am *MockAccountManager) CreatePAT(ctx context.Context, accountID string, initiatorUserID string, targetUserID string, name string, expiresIn int) (*types.PersonalAccessTokenGenerated, error) { if am.CreatePATFunc != nil { @@ -428,11 +413,11 @@ func (am *MockAccountManager) UpdatePeerMeta(ctx context.Context, peerID string, } // GetUser mock implementation of GetUser from server.AccountManager interface -func (am *MockAccountManager) GetUser(ctx context.Context, claims jwtclaims.AuthorizationClaims) (*types.User, error) { - if am.GetUserFunc != nil { - return am.GetUserFunc(ctx, claims) +func (am *MockAccountManager) GetUserFromUserAuth(ctx context.Context, userAuth nbcontext.UserAuth) (*types.User, error) { + if am.GetUserFromUserAuthFunc != nil { + return am.GetUserFromUserAuthFunc(ctx, userAuth) } - return nil, status.Errorf(codes.Unimplemented, "method GetUser is not implemented") + return nil, status.Errorf(codes.Unimplemented, "method GetUserFromUserAuth is not implemented") } func (am *MockAccountManager) ListUsers(ctx context.Context, accountID string) ([]*types.User, error) { @@ -550,9 +535,9 @@ func (am *MockAccountManager) DeleteUser(ctx context.Context, accountID string, } // DeleteRegularUsers mocks DeleteRegularUsers of the AccountManager interface -func (am *MockAccountManager) DeleteRegularUsers(ctx context.Context, accountID string, initiatorUserID string, targetUserIDs []string) error { +func (am *MockAccountManager) DeleteRegularUsers(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string, userInfos map[string]*types.UserInfo) error { if am.DeleteRegularUsersFunc != nil { - return am.DeleteRegularUsersFunc(ctx, accountID, initiatorUserID, targetUserIDs) + return am.DeleteRegularUsersFunc(ctx, accountID, initiatorUserID, targetUserIDs, userInfos) } return status.Errorf(codes.Unimplemented, "method DeleteRegularUsers is not implemented") } @@ -612,19 +597,11 @@ func (am *MockAccountManager) CreateUser(ctx context.Context, accountID, userID return nil, status.Errorf(codes.Unimplemented, "method CreateUser is not implemented") } -// GetAccountIDFromToken mocks GetAccountIDFromToken of the AccountManager interface -func (am *MockAccountManager) GetAccountIDFromToken(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) { - if am.GetAccountIDFromTokenFunc != nil { - return am.GetAccountIDFromTokenFunc(ctx, claims) +func (am *MockAccountManager) GetAccountIDFromUserAuth(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error) { + if am.GetAccountIDFromUserAuthFunc != nil { + return am.GetAccountIDFromUserAuthFunc(ctx, userAuth) } - return "", "", status.Errorf(codes.Unimplemented, "method GetAccountIDFromToken is not implemented") -} - -func (am *MockAccountManager) CheckUserAccessByJWTGroups(ctx context.Context, claims jwtclaims.AuthorizationClaims) error { - if am.CheckUserAccessByJWTGroupsFunc != nil { - return am.CheckUserAccessByJWTGroupsFunc(ctx, claims) - } - return status.Errorf(codes.Unimplemented, "method CheckUserAccessByJWTGroups is not implemented") + return "", "", status.Errorf(codes.Unimplemented, "method GetAccountIDFromUserAuth is not implemented") } // GetPeers mocks GetPeers of the AccountManager interface @@ -849,3 +826,15 @@ func (am *MockAccountManager) GetPeerGroups(ctx context.Context, accountID, peer } return nil, status.Errorf(codes.Unimplemented, "method GetPeerGroups is not implemented") } + +// BuildUserInfosForAccount mocks BuildUserInfosForAccount of the AccountManager interface +func (am *MockAccountManager) BuildUserInfosForAccount(ctx context.Context, accountID, initiatorUserID string, accountUsers []*types.User) (map[string]*types.UserInfo, error) { + if am.BuildUserInfosForAccountFunc != nil { + return am.BuildUserInfosForAccountFunc(ctx, accountID, initiatorUserID, accountUsers) + } + return nil, status.Errorf(codes.Unimplemented, "method BuildUserInfosForAccount is not implemented") +} + +func (am *MockAccountManager) SyncUserJWTGroups(ctx context.Context, userAuth nbcontext.UserAuth) error { + return status.Errorf(codes.Unimplemented, "method SyncUserJWTGroups is not implemented") +} diff --git a/management/server/nameserver_test.go b/management/server/nameserver_test.go index 0743db513..497d9af4f 100644 --- a/management/server/nameserver_test.go +++ b/management/server/nameserver_test.go @@ -379,12 +379,12 @@ func TestCreateNameServerGroup(t *testing.T) { t.Run(testCase.name, func(t *testing.T) { am, err := createNSManager(t) if err != nil { - t.Error("failed to create account manager") + t.Fatalf("failed to create account manager: %s", err) } account, err := initTestNSAccount(t, am) if err != nil { - t.Error("failed to init testing account") + t.Fatalf("failed to init testing account: %s", err) } outNSGroup, err := am.CreateNameServerGroup( @@ -607,12 +607,12 @@ func TestSaveNameServerGroup(t *testing.T) { t.Run(testCase.name, func(t *testing.T) { am, err := createNSManager(t) if err != nil { - t.Error("failed to create account manager") + t.Fatalf("failed to create account manager: %s", err) } account, err := initTestNSAccount(t, am) if err != nil { - t.Error("failed to init testing account") + t.Fatalf("failed to init testing account: %s", err) } account.NameServerGroups[testCase.existingNSGroup.ID] = testCase.existingNSGroup @@ -706,7 +706,7 @@ func TestDeleteNameServerGroup(t *testing.T) { account, err := initTestNSAccount(t, am) if err != nil { - t.Error("failed to init testing account") + t.Fatalf("failed to init testing account: %s", err) } account.NameServerGroups[testingNSGroup.ID] = testingNSGroup @@ -741,7 +741,7 @@ func TestGetNameServerGroup(t *testing.T) { account, err := initTestNSAccount(t, am) if err != nil { - t.Error("failed to init testing account") + t.Fatalf("failed to init testing account: %s", err) } foundGroup, err := am.GetNameServerGroup(context.Background(), account.Id, testUserID, existingNSGroupID) @@ -761,6 +761,7 @@ func TestGetNameServerGroup(t *testing.T) { func createNSManager(t *testing.T) (*DefaultAccountManager, error) { t.Helper() + store, err := createNSStore(t) if err != nil { return nil, err diff --git a/management/server/networks/resources/manager.go b/management/server/networks/resources/manager.go index 725d15496..5b542d886 100644 --- a/management/server/networks/resources/manager.go +++ b/management/server/networks/resources/manager.go @@ -113,7 +113,7 @@ func (m *managerImpl) CreateResource(ctx context.Context, userID string, resourc err = m.store.ExecuteInTransaction(ctx, func(transaction store.Store) error { _, err = transaction.GetNetworkResourceByName(ctx, store.LockingStrengthShare, resource.AccountID, resource.Name) if err == nil { - return errors.New("resource already exists") + return status.Errorf(status.InvalidArgument, "resource with name %s already exists", resource.Name) } network, err := transaction.GetNetworkByID(ctx, store.LockingStrengthUpdate, resource.AccountID, resource.NetworkID) @@ -223,7 +223,7 @@ func (m *managerImpl) UpdateResource(ctx context.Context, userID string, resourc oldResource, err := transaction.GetNetworkResourceByName(ctx, store.LockingStrengthShare, resource.AccountID, resource.Name) if err == nil && oldResource.ID != resource.ID { - return errors.New("new resource name already exists") + return status.Errorf(status.InvalidArgument, "new resource name already exists") } oldResource, err = transaction.GetNetworkResourceByID(ctx, store.LockingStrengthShare, resource.AccountID, resource.ID) diff --git a/management/server/peer.go b/management/server/peer.go index f48ba2d15..c9b0fcfee 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -11,11 +11,13 @@ import ( "sync" "time" - "github.com/netbirdio/netbird/management/server/geolocation" "github.com/rs/xid" log "github.com/sirupsen/logrus" "golang.org/x/exp/maps" + "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/management/server/geolocation" + "github.com/netbirdio/netbird/management/server/idp" "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/store" @@ -52,6 +54,9 @@ type PeerLogin struct { SetupKey string // ConnectionIP is the real IP of the peer ConnectionIP net.IP + + // ExtraDNSLabels is a list of extra DNS labels that the peer wants to use + ExtraDNSLabels []string } // GetPeers returns a list of peers under the given account filtering out peers that do not belong to a user if @@ -119,6 +124,11 @@ func (am *DefaultAccountManager) GetPeers(ctx context.Context, accountID, userID // MarkPeerConnected marks peer as connected (true) or disconnected (false) func (am *DefaultAccountManager) MarkPeerConnected(ctx context.Context, peerPubKey string, connected bool, realIP net.IP, accountID string) error { + start := time.Now() + defer func() { + log.WithContext(ctx).Debugf("MarkPeerConnected: took %v", time.Since(start)) + }() + var peer *nbpeer.Peer var settings *types.Settings var expired bool @@ -130,11 +140,6 @@ func (am *DefaultAccountManager) MarkPeerConnected(ctx context.Context, peerPubK return err } - settings, err = transaction.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) - if err != nil { - return err - } - expired, err = updatePeerStatusAndLocation(ctx, am.geo, transaction, peer, connected, realIP, accountID) return err }) @@ -143,6 +148,11 @@ func (am *DefaultAccountManager) MarkPeerConnected(ctx context.Context, peerPubK } if peer.AddedWithSSOLogin() { + settings, err = am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return err + } + if peer.LoginExpirationEnabled && settings.PeerLoginExpirationEnabled { am.checkAndSchedulePeerLoginExpiration(ctx, accountID) } @@ -374,6 +384,19 @@ func (am *DefaultAccountManager) DeletePeer(ctx context.Context, accountID, peer return err } + groups, err := transaction.GetPeerGroups(ctx, store.LockingStrengthUpdate, accountID, peerID) + if err != nil { + return fmt.Errorf("failed to get peer groups: %w", err) + } + + for _, group := range groups { + group.RemovePeer(peerID) + err = transaction.SaveGroup(ctx, store.LockingStrengthUpdate, group) + if err != nil { + return fmt.Errorf("failed to save group: %w", err) + } + } + eventsToStore, err = deletePeers(ctx, am, transaction, accountID, userID, []*nbpeer.Peer{peer}) return err }) @@ -483,6 +506,7 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s var setupKeyName string var ephemeral bool var groupsToAdd []string + var allowExtraDNSLabels bool if addedByUser { user, err := transaction.GetUserByUserID(ctx, store.LockingStrengthUpdate, userID) if err != nil { @@ -508,6 +532,11 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s ephemeral = sk.Ephemeral setupKeyID = sk.Id setupKeyName = sk.Name + allowExtraDNSLabels = sk.AllowExtraDNSLabels + + if !sk.AllowExtraDNSLabels && len(peer.ExtraDNSLabels) > 0 { + return status.Errorf(status.PreconditionFailed, "couldn't add peer: setup key doesn't allow extra DNS labels") + } } if (strings.ToLower(peer.Meta.Hostname) == "iphone" || strings.ToLower(peer.Meta.Hostname) == "ipad") && userID != "" { @@ -548,6 +577,8 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s Ephemeral: ephemeral, Location: peer.Location, InactivityExpirationEnabled: addedByUser, + ExtraDNSLabels: peer.ExtraDNSLabels, + AllowExtraDNSLabels: allowExtraDNSLabels, } opEvent.TargetID = newPeer.ID opEvent.Meta = newPeer.EventMeta(am.GetDNSDomain()) @@ -658,6 +689,11 @@ func getFreeIP(ctx context.Context, transaction store.Store, accountID string) ( // SyncPeer checks whether peer is eligible for receiving NetworkMap (authenticated) and returns its NetworkMap if eligible func (am *DefaultAccountManager) SyncPeer(ctx context.Context, sync PeerSync, accountID string) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) { + start := time.Now() + defer func() { + log.WithContext(ctx).Debugf("SyncPeer: took %v", time.Since(start)) + }() + var peer *nbpeer.Peer var peerNotValid bool var isStatusChanged bool @@ -665,6 +701,11 @@ func (am *DefaultAccountManager) SyncPeer(ctx context.Context, sync PeerSync, ac var err error var postureChecks []*posture.Checks + settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return nil, nil, nil, err + } + err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { peer, err = transaction.GetPeerByPeerPubKey(ctx, store.LockingStrengthUpdate, sync.WireGuardPubKey) if err != nil { @@ -682,11 +723,6 @@ func (am *DefaultAccountManager) SyncPeer(ctx context.Context, sync PeerSync, ac } } - settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) - if err != nil { - return err - } - if peerLoginExpired(ctx, peer, settings) { return status.NewPeerLoginExpiredError() } @@ -779,16 +815,17 @@ func (am *DefaultAccountManager) LoginPeer(ctx context.Context, login PeerLogin) var isPeerUpdated bool var postureChecks []*posture.Checks + settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return nil, nil, nil, err + } + err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { peer, err = transaction.GetPeerByPeerPubKey(ctx, store.LockingStrengthUpdate, login.WireGuardPubKey) if err != nil { return err } - settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) - if err != nil { - return err - } // this flag prevents unnecessary calls to the persistent store. shouldStorePeer := false @@ -835,6 +872,20 @@ func (am *DefaultAccountManager) LoginPeer(ctx context.Context, login PeerLogin) shouldStorePeer = true } + if !peer.AllowExtraDNSLabels && len(login.ExtraDNSLabels) > 0 { + return status.Errorf(status.PreconditionFailed, "couldn't login peer: setup key doesn't allow extra DNS labels") + } + + extraLabels, err := domain.ValidateDomainsStrSlice(login.ExtraDNSLabels) + if err != nil { + return status.Errorf(status.InvalidArgument, "invalid extra DNS labels: %v", err) + } + + if !slices.Equal(peer.ExtraDNSLabels, extraLabels) { + peer.ExtraDNSLabels = extraLabels + shouldStorePeer = true + } + if shouldStorePeer { if err = transaction.SavePeer(ctx, store.LockingStrengthUpdate, accountID, peer); err != nil { return err @@ -947,6 +998,11 @@ func (am *DefaultAccountManager) checkIFPeerNeedsLoginWithoutLock(ctx context.Co } func (am *DefaultAccountManager) getValidatedPeerWithMap(ctx context.Context, isRequiresApproval bool, accountID string, peer *nbpeer.Peer) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) { + start := time.Now() + defer func() { + log.WithContext(ctx).Debugf("getValidatedPeerWithMap: took %s", time.Since(start)) + }() + if isRequiresApproval { network, err := am.Store.GetAccountNetwork(ctx, store.LockingStrengthShare, accountID) if err != nil { @@ -959,7 +1015,7 @@ func (am *DefaultAccountManager) getValidatedPeerWithMap(ctx context.Context, is return peer, emptyMap, nil, nil } - account, err := am.Store.GetAccount(ctx, accountID) + account, err := am.requestBuffer.GetAccountWithBackpressure(ctx, accountID) if err != nil { return nil, nil, nil, err } @@ -1100,11 +1156,6 @@ func (am *DefaultAccountManager) UpdateAccountPeers(ctx context.Context, account } start := time.Now() - defer func() { - if am.metrics != nil { - am.metrics.AccountManagerMetrics().CountUpdateAccountPeersDuration(time.Since(start)) - } - }() approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra) if err != nil { @@ -1145,6 +1196,9 @@ func (am *DefaultAccountManager) UpdateAccountPeers(ctx context.Context, account } wg.Wait() + if am.metrics != nil { + am.metrics.AccountManagerMetrics().CountUpdateAccountPeersDuration(time.Since(start)) + } } // UpdateAccountPeer updates a single peer that belongs to an account. diff --git a/management/server/peer/peer.go b/management/server/peer/peer.go index 199c7c89d..afda55d17 100644 --- a/management/server/peer/peer.go +++ b/management/server/peer/peer.go @@ -49,6 +49,11 @@ type Peer struct { Ephemeral bool `gorm:"index"` // Geo location based on connection IP Location Location `gorm:"embedded;embeddedPrefix:location_"` + + // ExtraDNSLabels is a list of additional DNS labels that can be used to resolve the peer + ExtraDNSLabels []string `gorm:"serializer:json"` + // AllowExtraDNSLabels indicates whether the peer allows extra DNS labels to be used for resolving the peer + AllowExtraDNSLabels bool } type PeerStatus struct { //nolint:revive @@ -202,6 +207,8 @@ func (p *Peer) Copy() *Peer { Ephemeral: p.Ephemeral, Location: p.Location, InactivityExpirationEnabled: p.InactivityExpirationEnabled, + ExtraDNSLabels: slices.Clone(p.ExtraDNSLabels), + AllowExtraDNSLabels: p.AllowExtraDNSLabels, } } diff --git a/management/server/peer_test.go b/management/server/peer_test.go index 0ecd635ba..9deb8e456 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -168,7 +168,7 @@ func TestAccountManager_GetNetworkMap(t *testing.T) { t.Fatal(err) } - setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userId, false) + setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userId, false, false) if err != nil { t.Fatal("error creating setup key") return @@ -417,7 +417,7 @@ func TestAccountManager_GetPeerNetwork(t *testing.T) { t.Fatal(err) } - setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userId, false) + setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userId, false, false) if err != nil { t.Fatal("error creating setup key") return @@ -489,7 +489,7 @@ func TestDefaultAccountManager_GetPeer(t *testing.T) { } // two peers one added by a regular user and one with a setup key - setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, adminUser, false) + setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, adminUser, false, false) if err != nil { t.Fatal("error creating setup key") return @@ -1099,13 +1099,13 @@ func TestToSyncResponse(t *testing.T) { assert.Equal(t, "192.168.1.1/24", response.PeerConfig.Address) assert.Equal(t, "peer1.example.com", response.PeerConfig.Fqdn) assert.Equal(t, true, response.PeerConfig.SshConfig.SshEnabled) - // assert wiretrustee config - assert.Equal(t, "signal.uri", response.WiretrusteeConfig.Signal.Uri) - assert.Equal(t, proto.HostConfig_HTTPS, response.WiretrusteeConfig.Signal.GetProtocol()) - assert.Equal(t, "stun.uri", response.WiretrusteeConfig.Stuns[0].Uri) - assert.Equal(t, "turn.uri", response.WiretrusteeConfig.Turns[0].HostConfig.GetUri()) - assert.Equal(t, "turn-user", response.WiretrusteeConfig.Turns[0].User) - assert.Equal(t, "turn-pass", response.WiretrusteeConfig.Turns[0].Password) + // assert netbird config + assert.Equal(t, "signal.uri", response.NetbirdConfig.Signal.Uri) + assert.Equal(t, proto.HostConfig_HTTPS, response.NetbirdConfig.Signal.GetProtocol()) + assert.Equal(t, "stun.uri", response.NetbirdConfig.Stuns[0].Uri) + assert.Equal(t, "turn.uri", response.NetbirdConfig.Turns[0].HostConfig.GetUri()) + assert.Equal(t, "turn-user", response.NetbirdConfig.Turns[0].User) + assert.Equal(t, "turn-pass", response.NetbirdConfig.Turns[0].Password) // assert RemotePeers assert.Equal(t, 1, len(response.RemotePeers)) assert.Equal(t, "192.168.1.2/32", response.RemotePeers[0].AllowedIps[0]) @@ -1729,3 +1729,52 @@ func TestPeerAccountPeersUpdate(t *testing.T) { } }) } + +func Test_DeletePeer(t *testing.T) { + manager, err := createManager(t) + if err != nil { + t.Fatal(err) + return + } + + // account with an admin and a regular user + accountID := "test_account" + adminUser := "account_creator" + account := newAccountWithId(context.Background(), accountID, adminUser, "") + account.Peers = map[string]*nbpeer.Peer{ + "peer1": { + ID: "peer1", + AccountID: accountID, + }, + "peer2": { + ID: "peer2", + AccountID: accountID, + }, + } + account.Groups = map[string]*types.Group{ + "group1": { + ID: "group1", + Name: "Group1", + Peers: []string{"peer1", "peer2"}, + }, + } + + err = manager.Store.SaveAccount(context.Background(), account) + if err != nil { + t.Fatal(err) + return + } + + err = manager.DeletePeer(context.Background(), accountID, "peer1", adminUser) + if err != nil { + t.Fatalf("DeletePeer failed: %v", err) + } + + _, err = manager.GetPeer(context.Background(), accountID, "peer1", adminUser) + assert.Error(t, err) + + group, err := manager.GetGroup(context.Background(), accountID, "group1", adminUser) + assert.NoError(t, err) + assert.NotContains(t, group.Peers, "peer1") + +} diff --git a/management/server/route_test.go b/management/server/route_test.go index 1c5c56f60..40e0f41b0 100644 --- a/management/server/route_test.go +++ b/management/server/route_test.go @@ -13,12 +13,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/management/server/activity" resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" networkTypes "github.com/netbirdio/netbird/management/server/networks/types" - - "github.com/netbirdio/netbird/management/domain" - "github.com/netbirdio/netbird/management/server/activity" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/telemetry" diff --git a/management/server/setupkey.go b/management/server/setupkey.go index f2f1aad45..b0bdad4e5 100644 --- a/management/server/setupkey.go +++ b/management/server/setupkey.go @@ -52,7 +52,7 @@ type SetupKeyUpdateOperation struct { // CreateSetupKey generates a new setup key with a given name, type, list of groups IDs to auto-assign to peers registered with this key, // and adds it to the specified account. A list of autoGroups IDs can be empty. func (am *DefaultAccountManager) CreateSetupKey(ctx context.Context, accountID string, keyName string, keyType types.SetupKeyType, - expiresIn time.Duration, autoGroups []string, usageLimit int, userID string, ephemeral bool) (*types.SetupKey, error) { + expiresIn time.Duration, autoGroups []string, usageLimit int, userID string, ephemeral bool, allowExtraDNSLabels bool) (*types.SetupKey, error) { unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) defer unlock() @@ -78,7 +78,7 @@ func (am *DefaultAccountManager) CreateSetupKey(ctx context.Context, accountID s return status.Errorf(status.InvalidArgument, "invalid auto groups: %v", err) } - setupKey, plainKey = types.GenerateSetupKey(keyName, keyType, expiresIn, autoGroups, usageLimit, ephemeral) + setupKey, plainKey = types.GenerateSetupKey(keyName, keyType, expiresIn, autoGroups, usageLimit, ephemeral, allowExtraDNSLabels) setupKey.AccountID = accountID events := am.prepareSetupKeyEvents(ctx, transaction, accountID, userID, autoGroups, nil, setupKey) diff --git a/management/server/setupkey_test.go b/management/server/setupkey_test.go index e225ec54b..6e1e1cf7d 100644 --- a/management/server/setupkey_test.go +++ b/management/server/setupkey_test.go @@ -50,7 +50,7 @@ func TestDefaultAccountManager_SaveSetupKey(t *testing.T) { keyName := "my-test-key" key, err := manager.CreateSetupKey(context.Background(), account.Id, keyName, types.SetupKeyReusable, expiresIn, []string{}, - types.SetupKeyUnlimitedUsage, userID, false) + types.SetupKeyUnlimitedUsage, userID, false, false) if err != nil { t.Fatal(err) } @@ -168,7 +168,7 @@ func TestDefaultAccountManager_CreateSetupKey(t *testing.T) { for _, tCase := range []testCase{testCase1, testCase2, testCase3} { t.Run(tCase.name, func(t *testing.T) { key, err := manager.CreateSetupKey(context.Background(), account.Id, tCase.expectedKeyName, types.SetupKeyReusable, expiresIn, - tCase.expectedGroups, types.SetupKeyUnlimitedUsage, userID, false) + tCase.expectedGroups, types.SetupKeyUnlimitedUsage, userID, false, false) if tCase.expectedFailure { if err == nil { @@ -210,7 +210,7 @@ func TestGetSetupKeys(t *testing.T) { t.Fatal(err) } - plainKey, err := manager.CreateSetupKey(context.Background(), account.Id, "key1", types.SetupKeyReusable, time.Hour, nil, types.SetupKeyUnlimitedUsage, userID, false) + plainKey, err := manager.CreateSetupKey(context.Background(), account.Id, "key1", types.SetupKeyReusable, time.Hour, nil, types.SetupKeyUnlimitedUsage, userID, false, false) if err != nil { t.Fatal(err) } @@ -275,7 +275,7 @@ func TestGenerateSetupKey(t *testing.T) { expectedUpdatedAt := time.Now().UTC() var expectedAutoGroups []string - key, plain := types.GenerateSetupKey(expectedName, types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false) + key, plain := types.GenerateSetupKey(expectedName, types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false, false) assertKey(t, key, expectedName, expectedRevoke, expectedType, expectedUsedTimes, expectedCreatedAt, expectedExpiresAt, strconv.Itoa(int(types.Hash(plain))), expectedUpdatedAt, expectedAutoGroups, true) @@ -283,33 +283,33 @@ func TestGenerateSetupKey(t *testing.T) { } func TestSetupKey_IsValid(t *testing.T) { - validKey, _ := types.GenerateSetupKey("valid key", types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false) + validKey, _ := types.GenerateSetupKey("valid key", types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false, false) if !validKey.IsValid() { t.Errorf("expected key to be valid, got invalid %v", validKey) } // expired - expiredKey, _ := types.GenerateSetupKey("invalid key", types.SetupKeyOneOff, -time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false) + expiredKey, _ := types.GenerateSetupKey("invalid key", types.SetupKeyOneOff, -time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false, false) if expiredKey.IsValid() { t.Errorf("expected key to be invalid due to expiration, got valid %v", expiredKey) } // revoked - revokedKey, _ := types.GenerateSetupKey("invalid key", types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false) + revokedKey, _ := types.GenerateSetupKey("invalid key", types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false, false) revokedKey.Revoked = true if revokedKey.IsValid() { t.Errorf("expected revoked key to be invalid, got valid %v", revokedKey) } // overused - overUsedKey, _ := types.GenerateSetupKey("invalid key", types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false) + overUsedKey, _ := types.GenerateSetupKey("invalid key", types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false, false) overUsedKey.UsedTimes = 1 if overUsedKey.IsValid() { t.Errorf("expected overused key to be invalid, got valid %v", overUsedKey) } // overused - reusableKey, _ := types.GenerateSetupKey("valid key", types.SetupKeyReusable, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false) + reusableKey, _ := types.GenerateSetupKey("valid key", types.SetupKeyReusable, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false, false) reusableKey.UsedTimes = 99 if !reusableKey.IsValid() { t.Errorf("expected reusable key to be valid when used many times, got valid %v", reusableKey) @@ -388,7 +388,7 @@ func isValidBase64SHA256(encodedKey string) bool { func TestSetupKey_Copy(t *testing.T) { - key, _ := types.GenerateSetupKey("key name", types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false) + key, _ := types.GenerateSetupKey("key name", types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false, false) keyCopy := key.Copy() assertKey(t, keyCopy, key.Name, key.Revoked, string(key.Type), key.UsedTimes, key.CreatedAt, key.GetExpiresAt(), key.Id, @@ -436,7 +436,7 @@ func TestSetupKeyAccountPeersUpdate(t *testing.T) { close(done) }() - setupKey, err = manager.CreateSetupKey(context.Background(), account.Id, "key1", types.SetupKeyReusable, time.Hour, nil, 999, userID, false) + setupKey, err = manager.CreateSetupKey(context.Background(), account.Id, "key1", types.SetupKeyReusable, time.Hour, nil, 999, userID, false, false) assert.NoError(t, err) select { @@ -477,7 +477,7 @@ func TestDefaultAccountManager_CreateSetupKey_ShouldNotAllowToUpdateRevokedKey(t t.Fatal(err) } - key, err := manager.CreateSetupKey(context.Background(), account.Id, "testName", types.SetupKeyReusable, time.Hour, nil, types.SetupKeyUnlimitedUsage, userID, false) + key, err := manager.CreateSetupKey(context.Background(), account.Id, "testName", types.SetupKeyReusable, time.Hour, nil, types.SetupKeyUnlimitedUsage, userID, false, false) assert.NoError(t, err) // revoke the key diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index 7f0b3979e..43ad80023 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -15,7 +15,6 @@ import ( "sync" "time" - "github.com/netbirdio/netbird/management/server/util" log "github.com/sirupsen/logrus" "gorm.io/driver/mysql" "gorm.io/driver/postgres" @@ -34,6 +33,7 @@ import ( "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/management/server/types" + "github.com/netbirdio/netbird/management/server/util" "github.com/netbirdio/netbird/route" ) @@ -420,7 +420,7 @@ func (s *SqlStore) SaveUsers(ctx context.Context, lockStrength LockingStrength, return nil } - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Save(&users) + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}, clause.OnConflict{UpdateAll: true}).Create(&users) if result.Error != nil { log.WithContext(ctx).Errorf("failed to save users to store: %s", result.Error) return status.Errorf(status.Internal, "failed to save users to store") @@ -444,7 +444,7 @@ func (s *SqlStore) SaveGroups(ctx context.Context, lockStrength LockingStrength, return nil } - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Save(&groups) + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}, clause.OnConflict{UpdateAll: true}).Create(&groups) if result.Error != nil { return status.Errorf(status.Internal, "failed to save groups to store: %v", result.Error) } @@ -615,6 +615,16 @@ func (s *SqlStore) GetResourceGroups(ctx context.Context, lockStrength LockingSt return groups, nil } +func (s *SqlStore) GetAccountsCounter(ctx context.Context) (int64, error) { + var count int64 + result := s.db.Model(&types.Account{}).Count(&count) + if result.Error != nil { + return 0, fmt.Errorf("failed to get all accounts counter: %w", result.Error) + } + + return count, nil +} + func (s *SqlStore) GetAllAccounts(ctx context.Context) (all []*types.Account) { var accounts []types.Account result := s.db.Find(&accounts) @@ -1001,7 +1011,6 @@ func getGormConfig() *gorm.Config { return &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), CreateBatchSize: 400, - PrepareStmt: true, } } @@ -1036,6 +1045,13 @@ func NewSqliteStoreFromFileStore(ctx context.Context, fileStore *FileStore, data } for _, account := range fileStore.GetAllAccounts(ctx) { + _, err = account.GetGroupAll() + if err != nil { + if err := account.AddAllGroup(); err != nil { + return nil, err + } + } + err := store.SaveAccount(ctx, account) if err != nil { return nil, err diff --git a/management/server/store/sql_store_test.go b/management/server/store/sql_store_test.go index 7efc68745..46ef0eaa3 100644 --- a/management/server/store/sql_store_test.go +++ b/management/server/store/sql_store_test.go @@ -10,17 +10,19 @@ import ( "net/netip" "os" "runtime" + "sync" "testing" "time" "github.com/google/uuid" - "github.com/netbirdio/netbird/management/server/util" "github.com/rs/xid" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + nbdns "github.com/netbirdio/netbird/dns" + "github.com/netbirdio/netbird/management/server/util" resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" networkTypes "github.com/netbirdio/netbird/management/server/networks/types" @@ -35,40 +37,44 @@ import ( nbroute "github.com/netbirdio/netbird/route" ) -func TestSqlite_NewStore(t *testing.T) { +func runTestForAllEngines(t *testing.T, testDataFile string, f func(t *testing.T, store Store)) { + t.Helper() + for _, engine := range supportedEngines { + if os.Getenv("NETBIRD_STORE_ENGINE") != "" && os.Getenv("NETBIRD_STORE_ENGINE") != string(engine) { + continue + } + t.Setenv("NETBIRD_STORE_ENGINE", string(engine)) + store, cleanUp, err := NewTestStoreFromSQL(context.Background(), testDataFile, t.TempDir()) + t.Cleanup(cleanUp) + assert.NoError(t, err) + t.Run(string(engine), func(t *testing.T) { + f(t, store) + }) + os.Unsetenv("NETBIRD_STORE_ENGINE") + } +} + +func Test_NewStore(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) - - if len(store.GetAllAccounts(context.Background())) != 0 { - t.Errorf("expected to create a new empty Accounts map when creating a new FileStore") - } + runTestForAllEngines(t, "", func(t *testing.T, store Store) { + if store == nil { + t.Errorf("expected to create a new Store") + } + if len(store.GetAllAccounts(context.Background())) != 0 { + t.Errorf("expected to create a new empty Accounts map when creating a new FileStore") + } + }) } -func TestSqlite_SaveAccount_Large(t *testing.T) { +func Test_SaveAccount_Large(t *testing.T) { if (os.Getenv("CI") == "true" && runtime.GOOS == "darwin") || runtime.GOOS == "windows" { t.Skip("skip CI tests on darwin and windows") } - t.Run("SQLite", func(t *testing.T) { - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) - runLargeTest(t, store) - }) - - // create store outside to have a better time counter for the test - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) - t.Run("PostgreSQL", func(t *testing.T) { + runTestForAllEngines(t, "", func(t *testing.T, store Store) { runLargeTest(t, store) }) } @@ -213,77 +219,74 @@ func randomIPv4() net.IP { return net.IP(b) } -func TestSqlite_SaveAccount(t *testing.T) { +func Test_SaveAccount(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) + runTestForAllEngines(t, "", func(t *testing.T, store Store) { + account := newAccountWithId(context.Background(), "account_id", "testuser", "") + setupKey, _ := types.GenerateDefaultSetupKey() + account.SetupKeys[setupKey.Key] = setupKey + account.Peers["testpeer"] = &nbpeer.Peer{ + Key: "peerkey", + IP: net.IP{127, 0, 0, 1}, + Meta: nbpeer.PeerSystemMeta{}, + Name: "peer name", + Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, + } - account := newAccountWithId(context.Background(), "account_id", "testuser", "") - setupKey, _ := types.GenerateDefaultSetupKey() - account.SetupKeys[setupKey.Key] = setupKey - account.Peers["testpeer"] = &nbpeer.Peer{ - Key: "peerkey", - IP: net.IP{127, 0, 0, 1}, - Meta: nbpeer.PeerSystemMeta{}, - Name: "peer name", - Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, - } + err := store.SaveAccount(context.Background(), account) + require.NoError(t, err) - err = store.SaveAccount(context.Background(), account) - require.NoError(t, err) + account2 := newAccountWithId(context.Background(), "account_id2", "testuser2", "") + setupKey, _ = types.GenerateDefaultSetupKey() + account2.SetupKeys[setupKey.Key] = setupKey + account2.Peers["testpeer2"] = &nbpeer.Peer{ + Key: "peerkey2", + IP: net.IP{127, 0, 0, 2}, + Meta: nbpeer.PeerSystemMeta{}, + Name: "peer name 2", + Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, + } - account2 := newAccountWithId(context.Background(), "account_id2", "testuser2", "") - setupKey, _ = types.GenerateDefaultSetupKey() - account2.SetupKeys[setupKey.Key] = setupKey - account2.Peers["testpeer2"] = &nbpeer.Peer{ - Key: "peerkey2", - IP: net.IP{127, 0, 0, 2}, - Meta: nbpeer.PeerSystemMeta{}, - Name: "peer name 2", - Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, - } + err = store.SaveAccount(context.Background(), account2) + require.NoError(t, err) - err = store.SaveAccount(context.Background(), account2) - require.NoError(t, err) + if len(store.GetAllAccounts(context.Background())) != 2 { + t.Errorf("expecting 2 Accounts to be stored after SaveAccount()") + } - if len(store.GetAllAccounts(context.Background())) != 2 { - t.Errorf("expecting 2 Accounts to be stored after SaveAccount()") - } + a, err := store.GetAccount(context.Background(), account.Id) + if a == nil { + t.Errorf("expecting Account to be stored after SaveAccount(): %v", err) + } - a, err := store.GetAccount(context.Background(), account.Id) - if a == nil { - t.Errorf("expecting Account to be stored after SaveAccount(): %v", err) - } + if a != nil && len(a.Policies) != 1 { + t.Errorf("expecting Account to have one policy stored after SaveAccount(), got %d", len(a.Policies)) + } - if a != nil && len(a.Policies) != 1 { - t.Errorf("expecting Account to have one policy stored after SaveAccount(), got %d", len(a.Policies)) - } + if a != nil && len(a.Policies[0].Rules) != 1 { + t.Errorf("expecting Account to have one policy rule stored after SaveAccount(), got %d", len(a.Policies[0].Rules)) + return + } - if a != nil && len(a.Policies[0].Rules) != 1 { - t.Errorf("expecting Account to have one policy rule stored after SaveAccount(), got %d", len(a.Policies[0].Rules)) - return - } + if a, err := store.GetAccountByPeerPubKey(context.Background(), "peerkey"); a == nil { + t.Errorf("expecting PeerKeyID2AccountID index updated after SaveAccount(): %v", err) + } - if a, err := store.GetAccountByPeerPubKey(context.Background(), "peerkey"); a == nil { - t.Errorf("expecting PeerKeyID2AccountID index updated after SaveAccount(): %v", err) - } + if a, err := store.GetAccountByUser(context.Background(), "testuser"); a == nil { + t.Errorf("expecting UserID2AccountID index updated after SaveAccount(): %v", err) + } - if a, err := store.GetAccountByUser(context.Background(), "testuser"); a == nil { - t.Errorf("expecting UserID2AccountID index updated after SaveAccount(): %v", err) - } + if a, err := store.GetAccountByPeerID(context.Background(), "testpeer"); a == nil { + t.Errorf("expecting PeerID2AccountID index updated after SaveAccount(): %v", err) + } - if a, err := store.GetAccountByPeerID(context.Background(), "testpeer"); a == nil { - t.Errorf("expecting PeerID2AccountID index updated after SaveAccount(): %v", err) - } - - if a, err := store.GetAccountBySetupKey(context.Background(), setupKey.Key); a == nil { - t.Errorf("expecting SetupKeyID2AccountID index updated after SaveAccount(): %v", err) - } + if a, err := store.GetAccountBySetupKey(context.Background(), setupKey.Key); a == nil { + t.Errorf("expecting SetupKeyID2AccountID index updated after SaveAccount(): %v", err) + } + }) } func TestSqlite_DeleteAccount(t *testing.T) { @@ -400,27 +403,24 @@ func TestSqlite_DeleteAccount(t *testing.T) { } } -func TestSqlite_GetAccount(t *testing.T) { +func Test_GetAccount(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) + runTestForAllEngines(t, "../testdata/store.sql", func(t *testing.T, store Store) { + id := "bf1c8084-ba50-4ce7-9439-34653001fc3b" - id := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + account, err := store.GetAccount(context.Background(), id) + require.NoError(t, err) + require.Equal(t, id, account.Id, "account id should match") - account, err := store.GetAccount(context.Background(), id) - require.NoError(t, err) - require.Equal(t, id, account.Id, "account id should match") - - _, err = store.GetAccount(context.Background(), "non-existing-account") - assert.Error(t, err) - parsedErr, ok := status.FromError(err) - require.True(t, ok) - require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") + _, err = store.GetAccount(context.Background(), "non-existing-account") + assert.Error(t, err) + parsedErr, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") + }) } func TestSqlStore_SavePeer(t *testing.T) { @@ -578,51 +578,45 @@ func TestSqlStore_SavePeerLocation(t *testing.T) { require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") } -func TestSqlite_TestGetAccountByPrivateDomain(t *testing.T) { +func Test_TestGetAccountByPrivateDomain(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) + runTestForAllEngines(t, "../testdata/store.sql", func(t *testing.T, store Store) { + existingDomain := "test.com" - existingDomain := "test.com" + account, err := store.GetAccountByPrivateDomain(context.Background(), existingDomain) + require.NoError(t, err, "should found account") + require.Equal(t, existingDomain, account.Domain, "domains should match") - account, err := store.GetAccountByPrivateDomain(context.Background(), existingDomain) - require.NoError(t, err, "should found account") - require.Equal(t, existingDomain, account.Domain, "domains should match") - - _, err = store.GetAccountByPrivateDomain(context.Background(), "missing-domain.com") - require.Error(t, err, "should return error on domain lookup") - parsedErr, ok := status.FromError(err) - require.True(t, ok) - require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") + _, err = store.GetAccountByPrivateDomain(context.Background(), "missing-domain.com") + require.Error(t, err, "should return error on domain lookup") + parsedErr, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") + }) } -func TestSqlite_GetTokenIDByHashedToken(t *testing.T) { +func Test_GetTokenIDByHashedToken(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) + runTestForAllEngines(t, "../testdata/store.sql", func(t *testing.T, store Store) { + hashed := "SoMeHaShEdToKeN" + id := "9dj38s35-63fb-11ec-90d6-0242ac120003" - hashed := "SoMeHaShEdToKeN" - id := "9dj38s35-63fb-11ec-90d6-0242ac120003" + token, err := store.GetTokenIDByHashedToken(context.Background(), hashed) + require.NoError(t, err) + require.Equal(t, id, token) - token, err := store.GetTokenIDByHashedToken(context.Background(), hashed) - require.NoError(t, err) - require.Equal(t, id, token) - - _, err = store.GetTokenIDByHashedToken(context.Background(), "non-existing-hash") - require.Error(t, err) - parsedErr, ok := status.FromError(err) - require.True(t, ok) - require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") + _, err = store.GetTokenIDByHashedToken(context.Background(), "non-existing-hash") + require.Error(t, err) + parsedErr, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") + }) } func TestMigrate(t *testing.T) { @@ -1329,6 +1323,14 @@ func TestSqlStore_SaveGroups(t *testing.T) { } err = store.SaveGroups(context.Background(), LockingStrengthUpdate, groups) require.NoError(t, err) + + groups[1].Peers = []string{} + err = store.SaveGroups(context.Background(), LockingStrengthUpdate, groups) + require.NoError(t, err) + + group, err := store.GetGroupByID(context.Background(), LockingStrengthShare, accountID, groups[1].ID) + require.NoError(t, err) + require.Equal(t, groups[1], group) } func TestSqlStore_DeleteGroup(t *testing.T) { @@ -2043,52 +2045,12 @@ func newAccountWithId(ctx context.Context, accountID, userID, domain string) *ty }, } - if err := addAllGroup(acc); err != nil { + if err := acc.AddAllGroup(); err != nil { log.WithContext(ctx).Errorf("error adding all group to account %s: %v", acc.Id, err) } return acc } -// addAllGroup to account object if it doesn't exist -func addAllGroup(account *types.Account) error { - if len(account.Groups) == 0 { - allGroup := &types.Group{ - ID: xid.New().String(), - Name: "All", - Issued: types.GroupIssuedAPI, - } - for _, peer := range account.Peers { - allGroup.Peers = append(allGroup.Peers, peer.ID) - } - account.Groups = map[string]*types.Group{allGroup.ID: allGroup} - - id := xid.New().String() - - defaultPolicy := &types.Policy{ - ID: id, - Name: types.DefaultRuleName, - Description: types.DefaultRuleDescription, - Enabled: true, - Rules: []*types.PolicyRule{ - { - ID: id, - Name: types.DefaultRuleName, - Description: types.DefaultRuleDescription, - Enabled: true, - Sources: []string{allGroup.ID}, - Destinations: []string{allGroup.ID}, - Bidirectional: true, - Protocol: types.PolicyRuleProtocolALL, - Action: types.PolicyTrafficActionAccept, - }, - }, - } - - account.Policies = []*types.Policy{defaultPolicy} - } - return nil -} - func TestSqlStore_GetAccountNetworks(t *testing.T) { store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) t.Cleanup(cleanup) @@ -2478,6 +2440,465 @@ func TestSqlStore_AddAndRemoveResourceFromGroup(t *testing.T) { require.NotContains(t, group.Resources, *res) } +func TestSqlStore_DatabaseBlocking(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store_with_expired_peers.sql", t.TempDir()) + t.Cleanup(cleanup) + if err != nil { + t.Fatal(err) + } + + concurrentReads := 40 + + testRunSuccessful := false + wgSuccess := sync.WaitGroup{} + wgSuccess.Add(concurrentReads) + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + start := make(chan struct{}) + + for i := 0; i < concurrentReads/2; i++ { + go func() { + t.Logf("Entered routine 1-%d", i) + + <-start + err := store.ExecuteInTransaction(context.Background(), func(tx Store) error { + _, err := tx.GetAccountIDByPeerID(context.Background(), LockingStrengthShare, "cfvprsrlo1hqoo49ohog") + return err + }) + if err != nil { + t.Errorf("Failed, got error: %v", err) + return + } + + t.Log("Got User from routine 1") + wgSuccess.Done() + }() + } + + for i := 0; i < concurrentReads/2; i++ { + go func() { + t.Logf("Entered routine 2-%d", i) + + <-start + _, err := store.GetAccountIDByPeerID(context.Background(), LockingStrengthShare, "cfvprsrlo1hqoo49ohog") + if err != nil { + t.Errorf("Failed, got error: %v", err) + return + } + + t.Log("Got User from routine 2") + wgSuccess.Done() + }() + } + + time.Sleep(200 * time.Millisecond) + close(start) + t.Log("Started routines") + + go func() { + wgSuccess.Wait() + testRunSuccessful = true + }() + + <-ctx.Done() + if !testRunSuccessful { + t.Fatalf("Test failed") + } + + t.Logf("Test completed") +} + +func TestSqlStore_GetAccountCreatedBy(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + tests := []struct { + name string + accountID string + expectError bool + createdBy string + }{ + { + name: "existing account ID", + accountID: "bf1c8084-ba50-4ce7-9439-34653001fc3b", + expectError: false, + createdBy: "edafee4e-63fb-11ec-90d6-0242ac120003", + }, + { + name: "non-existing account ID", + accountID: "nonexistent", + expectError: true, + }, + { + name: "empty account ID", + accountID: "", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + createdBy, err := store.GetAccountCreatedBy(context.Background(), LockingStrengthShare, tt.accountID) + if tt.expectError { + require.Error(t, err) + sErr, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, sErr.Type(), status.NotFound) + require.Empty(t, createdBy) + } else { + require.NoError(t, err) + require.NotNil(t, createdBy) + require.Equal(t, tt.createdBy, createdBy) + } + }) + } + +} + +func TestSqlStore_GetUserByUserID(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + tests := []struct { + name string + userID string + expectError bool + }{ + { + name: "retrieve existing user", + userID: "edafee4e-63fb-11ec-90d6-0242ac120003", + expectError: false, + }, + { + name: "retrieve non-existing user", + userID: "non-existing", + expectError: true, + }, + { + name: "retrieve with empty user ID", + userID: "", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + user, err := store.GetUserByUserID(context.Background(), LockingStrengthShare, tt.userID) + if tt.expectError { + require.Error(t, err) + sErr, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, sErr.Type(), status.NotFound) + require.Nil(t, user) + } else { + require.NoError(t, err) + require.NotNil(t, user) + require.Equal(t, tt.userID, user.Id) + } + }) + } +} + +func TestSqlStore_GetUserByPATID(t *testing.T) { + store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) + t.Cleanup(cleanUp) + assert.NoError(t, err) + + id := "9dj38s35-63fb-11ec-90d6-0242ac120003" + + user, err := store.GetUserByPATID(context.Background(), LockingStrengthShare, id) + require.NoError(t, err) + require.Equal(t, "f4f6d672-63fb-11ec-90d6-0242ac120003", user.Id) +} + +func TestSqlStore_SaveUser(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + + user := &types.User{ + Id: "user-id", + AccountID: accountID, + Role: types.UserRoleAdmin, + IsServiceUser: false, + AutoGroups: []string{"groupA", "groupB"}, + Blocked: false, + LastLogin: util.ToPtr(time.Now().UTC()), + CreatedAt: time.Now().UTC().Add(-time.Hour), + Issued: types.UserIssuedIntegration, + } + err = store.SaveUser(context.Background(), LockingStrengthUpdate, user) + require.NoError(t, err) + + saveUser, err := store.GetUserByUserID(context.Background(), LockingStrengthShare, user.Id) + require.NoError(t, err) + require.Equal(t, user.Id, saveUser.Id) + require.Equal(t, user.AccountID, saveUser.AccountID) + require.Equal(t, user.Role, saveUser.Role) + require.Equal(t, user.AutoGroups, saveUser.AutoGroups) + require.WithinDurationf(t, user.GetLastLogin(), saveUser.LastLogin.UTC(), time.Millisecond, "LastLogin should be equal") + require.WithinDurationf(t, user.CreatedAt, saveUser.CreatedAt.UTC(), time.Millisecond, "CreatedAt should be equal") + require.Equal(t, user.Issued, saveUser.Issued) + require.Equal(t, user.Blocked, saveUser.Blocked) + require.Equal(t, user.IsServiceUser, saveUser.IsServiceUser) +} + +func TestSqlStore_SaveUsers(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + + accountUsers, err := store.GetAccountUsers(context.Background(), LockingStrengthShare, accountID) + require.NoError(t, err) + require.Len(t, accountUsers, 2) + + users := []*types.User{ + { + Id: "user-1", + AccountID: accountID, + Issued: "api", + AutoGroups: []string{"groupA", "groupB"}, + }, + { + Id: "user-2", + AccountID: accountID, + Issued: "integration", + AutoGroups: []string{"groupA"}, + }, + } + err = store.SaveUsers(context.Background(), LockingStrengthUpdate, users) + require.NoError(t, err) + + accountUsers, err = store.GetAccountUsers(context.Background(), LockingStrengthShare, accountID) + require.NoError(t, err) + require.Len(t, accountUsers, 4) + + users[1].AutoGroups = []string{"groupA", "groupC"} + err = store.SaveUsers(context.Background(), LockingStrengthUpdate, users) + require.NoError(t, err) + + user, err := store.GetUserByUserID(context.Background(), LockingStrengthShare, users[1].Id) + require.NoError(t, err) + require.Equal(t, users[1].AutoGroups, user.AutoGroups) +} + +func TestSqlStore_DeleteUser(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + userID := "f4f6d672-63fb-11ec-90d6-0242ac120003" + + err = store.DeleteUser(context.Background(), LockingStrengthUpdate, accountID, userID) + require.NoError(t, err) + + user, err := store.GetUserByUserID(context.Background(), LockingStrengthShare, userID) + require.Error(t, err) + require.Nil(t, user) + + userPATs, err := store.GetUserPATs(context.Background(), LockingStrengthShare, userID) + require.NoError(t, err) + require.Len(t, userPATs, 0) +} + +func TestSqlStore_GetPATByID(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + userID := "f4f6d672-63fb-11ec-90d6-0242ac120003" + + tests := []struct { + name string + patID string + expectError bool + }{ + { + name: "retrieve existing PAT", + patID: "9dj38s35-63fb-11ec-90d6-0242ac120003", + expectError: false, + }, + { + name: "retrieve non-existing PAT", + patID: "non-existing", + expectError: true, + }, + { + name: "retrieve with empty PAT ID", + patID: "", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pat, err := store.GetPATByID(context.Background(), LockingStrengthShare, userID, tt.patID) + if tt.expectError { + require.Error(t, err) + sErr, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, sErr.Type(), status.NotFound) + require.Nil(t, pat) + } else { + require.NoError(t, err) + require.NotNil(t, pat) + require.Equal(t, tt.patID, pat.ID) + } + }) + } +} + +func TestSqlStore_GetUserPATs(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + userPATs, err := store.GetUserPATs(context.Background(), LockingStrengthShare, "f4f6d672-63fb-11ec-90d6-0242ac120003") + require.NoError(t, err) + require.Len(t, userPATs, 1) +} + +func TestSqlStore_GetPATByHashedToken(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + pat, err := store.GetPATByHashedToken(context.Background(), LockingStrengthShare, "SoMeHaShEdToKeN") + require.NoError(t, err) + require.Equal(t, "9dj38s35-63fb-11ec-90d6-0242ac120003", pat.ID) +} + +func TestSqlStore_MarkPATUsed(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + userID := "f4f6d672-63fb-11ec-90d6-0242ac120003" + patID := "9dj38s35-63fb-11ec-90d6-0242ac120003" + + err = store.MarkPATUsed(context.Background(), LockingStrengthUpdate, patID) + require.NoError(t, err) + + pat, err := store.GetPATByID(context.Background(), LockingStrengthShare, userID, patID) + require.NoError(t, err) + now := time.Now().UTC() + require.WithinRange(t, pat.LastUsed.UTC(), now.Add(-15*time.Second), now, "LastUsed should be within 1 second of now") +} + +func TestSqlStore_SavePAT(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + userID := "edafee4e-63fb-11ec-90d6-0242ac120003" + + pat := &types.PersonalAccessToken{ + ID: "pat-id", + UserID: userID, + Name: "token", + HashedToken: "SoMeHaShEdToKeN", + ExpirationDate: util.ToPtr(time.Now().UTC().Add(12 * time.Hour)), + CreatedBy: userID, + CreatedAt: time.Now().UTC().Add(time.Hour), + LastUsed: util.ToPtr(time.Now().UTC().Add(-15 * time.Minute)), + } + err = store.SavePAT(context.Background(), LockingStrengthUpdate, pat) + require.NoError(t, err) + + savePAT, err := store.GetPATByID(context.Background(), LockingStrengthShare, userID, pat.ID) + require.NoError(t, err) + require.Equal(t, pat.ID, savePAT.ID) + require.Equal(t, pat.UserID, savePAT.UserID) + require.Equal(t, pat.HashedToken, savePAT.HashedToken) + require.Equal(t, pat.CreatedBy, savePAT.CreatedBy) + require.WithinDurationf(t, pat.GetExpirationDate(), savePAT.ExpirationDate.UTC(), time.Millisecond, "ExpirationDate should be equal") + require.WithinDurationf(t, pat.CreatedAt, savePAT.CreatedAt.UTC(), time.Millisecond, "CreatedAt should be equal") + require.WithinDurationf(t, pat.GetLastUsed(), savePAT.LastUsed.UTC(), time.Millisecond, "LastUsed should be equal") +} + +func TestSqlStore_DeletePAT(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + userID := "f4f6d672-63fb-11ec-90d6-0242ac120003" + patID := "9dj38s35-63fb-11ec-90d6-0242ac120003" + + err = store.DeletePAT(context.Background(), LockingStrengthUpdate, userID, patID) + require.NoError(t, err) + + pat, err := store.GetPATByID(context.Background(), LockingStrengthShare, userID, patID) + require.Error(t, err) + require.Nil(t, pat) +} + +func TestSqlStore_SaveUsers_LargeBatch(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + + accountUsers, err := store.GetAccountUsers(context.Background(), LockingStrengthShare, accountID) + require.NoError(t, err) + require.Len(t, accountUsers, 2) + + usersToSave := make([]*types.User, 0) + + for i := 1; i <= 8000; i++ { + usersToSave = append(usersToSave, &types.User{ + Id: fmt.Sprintf("user-%d", i), + AccountID: accountID, + Role: types.UserRoleUser, + }) + } + + err = store.SaveUsers(context.Background(), LockingStrengthUpdate, usersToSave) + require.NoError(t, err) + + accountUsers, err = store.GetAccountUsers(context.Background(), LockingStrengthShare, accountID) + require.NoError(t, err) + require.Equal(t, 8002, len(accountUsers)) +} + +func TestSqlStore_SaveGroups_LargeBatch(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + + accountGroups, err := store.GetAccountGroups(context.Background(), LockingStrengthShare, accountID) + require.NoError(t, err) + require.Len(t, accountGroups, 3) + + groupsToSave := make([]*types.Group, 0) + + for i := 1; i <= 8000; i++ { + groupsToSave = append(groupsToSave, &types.Group{ + ID: fmt.Sprintf("%d", i), + AccountID: accountID, + Name: fmt.Sprintf("group-%d", i), + }) + } + + err = store.SaveGroups(context.Background(), LockingStrengthUpdate, groupsToSave) + require.NoError(t, err) + + accountGroups, err = store.GetAccountGroups(context.Background(), LockingStrengthShare, accountID) + require.NoError(t, err) + require.Equal(t, 8003, len(accountGroups)) +} + func TestSqlStore_AddPeerToGroup(t *testing.T) { store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store_policy_migrate.sql", t.TempDir()) t.Cleanup(cleanup) @@ -2804,329 +3225,6 @@ func TestSqlStore_DeletePeer(t *testing.T) { require.Nil(t, peer) } -func TestSqlStore_GetAccountCreatedBy(t *testing.T) { - store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) - t.Cleanup(cleanup) - require.NoError(t, err) - - tests := []struct { - name string - accountID string - expectError bool - createdBy string - }{ - { - name: "existing account ID", - accountID: "bf1c8084-ba50-4ce7-9439-34653001fc3b", - expectError: false, - createdBy: "edafee4e-63fb-11ec-90d6-0242ac120003", - }, - { - name: "non-existing account ID", - accountID: "nonexistent", - expectError: true, - }, - { - name: "empty account ID", - accountID: "", - expectError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - createdBy, err := store.GetAccountCreatedBy(context.Background(), LockingStrengthShare, tt.accountID) - if tt.expectError { - require.Error(t, err) - sErr, ok := status.FromError(err) - require.True(t, ok) - require.Equal(t, sErr.Type(), status.NotFound) - require.Empty(t, createdBy) - } else { - require.NoError(t, err) - require.NotNil(t, createdBy) - require.Equal(t, tt.createdBy, createdBy) - } - }) - } - -} - -func TestSqlStore_GetUserByUserID(t *testing.T) { - store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) - t.Cleanup(cleanup) - require.NoError(t, err) - - tests := []struct { - name string - userID string - expectError bool - }{ - { - name: "retrieve existing user", - userID: "edafee4e-63fb-11ec-90d6-0242ac120003", - expectError: false, - }, - { - name: "retrieve non-existing user", - userID: "non-existing", - expectError: true, - }, - { - name: "retrieve with empty user ID", - userID: "", - expectError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - user, err := store.GetUserByUserID(context.Background(), LockingStrengthShare, tt.userID) - if tt.expectError { - require.Error(t, err) - sErr, ok := status.FromError(err) - require.True(t, ok) - require.Equal(t, sErr.Type(), status.NotFound) - require.Nil(t, user) - } else { - require.NoError(t, err) - require.NotNil(t, user) - require.Equal(t, tt.userID, user.Id) - } - }) - } -} - -func TestSqlStore_GetUserByPATID(t *testing.T) { - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) - - id := "9dj38s35-63fb-11ec-90d6-0242ac120003" - - user, err := store.GetUserByPATID(context.Background(), LockingStrengthShare, id) - require.NoError(t, err) - require.Equal(t, "f4f6d672-63fb-11ec-90d6-0242ac120003", user.Id) -} - -func TestSqlStore_SaveUser(t *testing.T) { - store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) - t.Cleanup(cleanup) - require.NoError(t, err) - - accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" - - user := &types.User{ - Id: "user-id", - AccountID: accountID, - Role: types.UserRoleAdmin, - IsServiceUser: false, - AutoGroups: []string{"groupA", "groupB"}, - Blocked: false, - LastLogin: util.ToPtr(time.Now().UTC()), - CreatedAt: time.Now().UTC().Add(-time.Hour), - Issued: types.UserIssuedIntegration, - } - err = store.SaveUser(context.Background(), LockingStrengthUpdate, user) - require.NoError(t, err) - - saveUser, err := store.GetUserByUserID(context.Background(), LockingStrengthShare, user.Id) - require.NoError(t, err) - require.Equal(t, user.Id, saveUser.Id) - require.Equal(t, user.AccountID, saveUser.AccountID) - require.Equal(t, user.Role, saveUser.Role) - require.Equal(t, user.AutoGroups, saveUser.AutoGroups) - require.WithinDurationf(t, user.GetLastLogin(), saveUser.LastLogin.UTC(), time.Millisecond, "LastLogin should be equal") - require.WithinDurationf(t, user.CreatedAt, saveUser.CreatedAt.UTC(), time.Millisecond, "CreatedAt should be equal") - require.Equal(t, user.Issued, saveUser.Issued) - require.Equal(t, user.Blocked, saveUser.Blocked) - require.Equal(t, user.IsServiceUser, saveUser.IsServiceUser) -} - -func TestSqlStore_SaveUsers(t *testing.T) { - store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) - t.Cleanup(cleanup) - require.NoError(t, err) - - accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" - - accountUsers, err := store.GetAccountUsers(context.Background(), LockingStrengthShare, accountID) - require.NoError(t, err) - require.Len(t, accountUsers, 2) - - users := []*types.User{ - { - Id: "user-1", - AccountID: accountID, - Issued: "api", - AutoGroups: []string{"groupA", "groupB"}, - }, - { - Id: "user-2", - AccountID: accountID, - Issued: "integration", - AutoGroups: []string{"groupA"}, - }, - } - err = store.SaveUsers(context.Background(), LockingStrengthUpdate, users) - require.NoError(t, err) - - accountUsers, err = store.GetAccountUsers(context.Background(), LockingStrengthShare, accountID) - require.NoError(t, err) - require.Len(t, accountUsers, 4) -} - -func TestSqlStore_DeleteUser(t *testing.T) { - store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) - t.Cleanup(cleanup) - require.NoError(t, err) - - accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" - userID := "f4f6d672-63fb-11ec-90d6-0242ac120003" - - err = store.DeleteUser(context.Background(), LockingStrengthUpdate, accountID, userID) - require.NoError(t, err) - - user, err := store.GetUserByUserID(context.Background(), LockingStrengthShare, userID) - require.Error(t, err) - require.Nil(t, user) - - userPATs, err := store.GetUserPATs(context.Background(), LockingStrengthShare, userID) - require.NoError(t, err) - require.Len(t, userPATs, 0) -} - -func TestSqlStore_GetPATByID(t *testing.T) { - store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) - t.Cleanup(cleanup) - require.NoError(t, err) - - userID := "f4f6d672-63fb-11ec-90d6-0242ac120003" - - tests := []struct { - name string - patID string - expectError bool - }{ - { - name: "retrieve existing PAT", - patID: "9dj38s35-63fb-11ec-90d6-0242ac120003", - expectError: false, - }, - { - name: "retrieve non-existing PAT", - patID: "non-existing", - expectError: true, - }, - { - name: "retrieve with empty PAT ID", - patID: "", - expectError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - pat, err := store.GetPATByID(context.Background(), LockingStrengthShare, userID, tt.patID) - if tt.expectError { - require.Error(t, err) - sErr, ok := status.FromError(err) - require.True(t, ok) - require.Equal(t, sErr.Type(), status.NotFound) - require.Nil(t, pat) - } else { - require.NoError(t, err) - require.NotNil(t, pat) - require.Equal(t, tt.patID, pat.ID) - } - }) - } -} - -func TestSqlStore_GetUserPATs(t *testing.T) { - store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) - t.Cleanup(cleanup) - require.NoError(t, err) - - userPATs, err := store.GetUserPATs(context.Background(), LockingStrengthShare, "f4f6d672-63fb-11ec-90d6-0242ac120003") - require.NoError(t, err) - require.Len(t, userPATs, 1) -} - -func TestSqlStore_GetPATByHashedToken(t *testing.T) { - store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) - t.Cleanup(cleanup) - require.NoError(t, err) - - pat, err := store.GetPATByHashedToken(context.Background(), LockingStrengthShare, "SoMeHaShEdToKeN") - require.NoError(t, err) - require.Equal(t, "9dj38s35-63fb-11ec-90d6-0242ac120003", pat.ID) -} - -func TestSqlStore_MarkPATUsed(t *testing.T) { - store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) - t.Cleanup(cleanup) - require.NoError(t, err) - - userID := "f4f6d672-63fb-11ec-90d6-0242ac120003" - patID := "9dj38s35-63fb-11ec-90d6-0242ac120003" - - err = store.MarkPATUsed(context.Background(), LockingStrengthUpdate, patID) - require.NoError(t, err) - - pat, err := store.GetPATByID(context.Background(), LockingStrengthShare, userID, patID) - require.NoError(t, err) - now := time.Now().UTC() - require.WithinRange(t, pat.LastUsed.UTC(), now.Add(-15*time.Second), now, "LastUsed should be within 1 second of now") -} - -func TestSqlStore_SavePAT(t *testing.T) { - store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) - t.Cleanup(cleanup) - require.NoError(t, err) - - userID := "edafee4e-63fb-11ec-90d6-0242ac120003" - - pat := &types.PersonalAccessToken{ - ID: "pat-id", - UserID: userID, - Name: "token", - HashedToken: "SoMeHaShEdToKeN", - ExpirationDate: util.ToPtr(time.Now().UTC().Add(12 * time.Hour)), - CreatedBy: userID, - CreatedAt: time.Now().UTC().Add(time.Hour), - LastUsed: util.ToPtr(time.Now().UTC().Add(-15 * time.Minute)), - } - err = store.SavePAT(context.Background(), LockingStrengthUpdate, pat) - require.NoError(t, err) - - savePAT, err := store.GetPATByID(context.Background(), LockingStrengthShare, userID, pat.ID) - require.NoError(t, err) - require.Equal(t, pat.ID, savePAT.ID) - require.Equal(t, pat.UserID, savePAT.UserID) - require.Equal(t, pat.HashedToken, savePAT.HashedToken) - require.Equal(t, pat.CreatedBy, savePAT.CreatedBy) - require.WithinDurationf(t, pat.GetExpirationDate(), savePAT.ExpirationDate.UTC(), time.Millisecond, "ExpirationDate should be equal") - require.WithinDurationf(t, pat.CreatedAt, savePAT.CreatedAt.UTC(), time.Millisecond, "CreatedAt should be equal") - require.WithinDurationf(t, pat.GetLastUsed(), savePAT.LastUsed.UTC(), time.Millisecond, "LastUsed should be equal") -} - -func TestSqlStore_DeletePAT(t *testing.T) { - store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) - t.Cleanup(cleanup) - require.NoError(t, err) - - userID := "f4f6d672-63fb-11ec-90d6-0242ac120003" - patID := "9dj38s35-63fb-11ec-90d6-0242ac120003" - - err = store.DeletePAT(context.Background(), LockingStrengthUpdate, userID, patID) - require.NoError(t, err) - - pat, err := store.GetPATByID(context.Background(), LockingStrengthShare, userID, patID) - require.Error(t, err) - require.Nil(t, pat) -} - func TestSqlStore_GetAccountRoutes(t *testing.T) { store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) t.Cleanup(cleanup) diff --git a/management/server/store/store.go b/management/server/store/store.go index e05fe57c8..0acb74123 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -9,11 +9,16 @@ import ( "os" "path" "path/filepath" + "regexp" "runtime" + "slices" "strings" "time" + "github.com/google/uuid" log "github.com/sirupsen/logrus" + "gorm.io/driver/mysql" + "gorm.io/driver/postgres" "gorm.io/driver/sqlite" "gorm.io/gorm" @@ -43,6 +48,7 @@ const ( ) type Store interface { + GetAccountsCounter(ctx context.Context) (int64, error) GetAllAccounts(ctx context.Context) []*types.Account GetAccount(ctx context.Context, accountID string) (*types.Account, error) AccountExists(ctx context.Context, lockStrength LockingStrength, id string) (bool, error) @@ -195,6 +201,8 @@ const ( mysqlDsnEnv = "NETBIRD_STORE_ENGINE_MYSQL_DSN" ) +var supportedEngines = []Engine{SqliteStoreEngine, PostgresStoreEngine, MysqlStoreEngine} + func getStoreEngineFromEnv() Engine { // NETBIRD_STORE_ENGINE supposed to be used in tests. Otherwise, rely on the config file. kind, ok := os.LookupEnv("NETBIRD_STORE_ENGINE") @@ -203,7 +211,7 @@ func getStoreEngineFromEnv() Engine { } value := Engine(strings.ToLower(kind)) - if value == SqliteStoreEngine || value == PostgresStoreEngine || value == MysqlStoreEngine { + if slices.Contains(supportedEngines, value) { return value } @@ -347,55 +355,158 @@ func NewTestStoreFromSQL(ctx context.Context, filename string, dataDir string) ( return nil, nil, fmt.Errorf("failed to create test store: %v", err) } + err = addAllGroupToAccount(ctx, store) + if err != nil { + return nil, nil, fmt.Errorf("failed to add all group to account: %v", err) + } + return getSqlStoreEngine(ctx, store, kind) } -func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind Engine) (Store, func(), error) { - if kind == PostgresStoreEngine { - cleanUp, err := testutil.CreatePostgresTestContainer() +func addAllGroupToAccount(ctx context.Context, store Store) error { + allAccounts := store.GetAllAccounts(ctx) + for _, account := range allAccounts { + shouldSave := false + + _, err := account.GetGroupAll() if err != nil { - return nil, nil, err + if err := account.AddAllGroup(); err != nil { + return err + } + shouldSave = true } - dsn, ok := os.LookupEnv(postgresDsnEnv) - if !ok { - return nil, nil, fmt.Errorf("%s is not set", postgresDsnEnv) + if shouldSave { + err = store.SaveAccount(ctx, account) + if err != nil { + return err + } } - - store, err = NewPostgresqlStoreFromSqlStore(ctx, store, dsn, nil) - if err != nil { - return nil, nil, err - } - - return store, cleanUp, nil } + return nil +} - if kind == MysqlStoreEngine { - cleanUp, err := testutil.CreateMysqlTestContainer() - if err != nil { - return nil, nil, err +func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind Engine) (Store, func(), error) { + var cleanup func() + var err error + switch kind { + case PostgresStoreEngine: + store, cleanup, err = newReusedPostgresStore(ctx, store, kind) + case MysqlStoreEngine: + store, cleanup, err = newReusedMysqlStore(ctx, store, kind) + default: + cleanup = func() { + // sqlite doesn't need to be cleaned up } - - dsn, ok := os.LookupEnv(mysqlDsnEnv) - if !ok { - return nil, nil, fmt.Errorf("%s is not set", mysqlDsnEnv) - } - - store, err = NewMysqlStoreFromSqlStore(ctx, store, dsn, nil) - if err != nil { - return nil, nil, err - } - - return store, cleanUp, nil + } + if err != nil { + return nil, cleanup, fmt.Errorf("failed to create test store: %v", err) } closeConnection := func() { + cleanup() store.Close(ctx) } return store, closeConnection, nil } +func newReusedPostgresStore(ctx context.Context, store *SqlStore, kind Engine) (*SqlStore, func(), error) { + if envDsn, ok := os.LookupEnv(postgresDsnEnv); !ok || envDsn == "" { + var err error + _, err = testutil.CreatePostgresTestContainer() + if err != nil { + return nil, nil, err + } + } + + dsn, ok := os.LookupEnv(postgresDsnEnv) + if !ok { + return nil, nil, fmt.Errorf("%s is not set", postgresDsnEnv) + } + + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + return nil, nil, fmt.Errorf("failed to open postgres connection: %v", err) + } + + dsn, cleanup, err := createRandomDB(dsn, db, kind) + if err != nil { + return nil, cleanup, err + } + + store, err = NewPostgresqlStoreFromSqlStore(ctx, store, dsn, nil) + if err != nil { + return nil, cleanup, err + } + + return store, cleanup, nil +} + +func newReusedMysqlStore(ctx context.Context, store *SqlStore, kind Engine) (*SqlStore, func(), error) { + if envDsn, ok := os.LookupEnv(mysqlDsnEnv); !ok || envDsn == "" { + var err error + _, err = testutil.CreateMysqlTestContainer() + if err != nil { + return nil, nil, err + } + } + + dsn, ok := os.LookupEnv(mysqlDsnEnv) + if !ok { + return nil, nil, fmt.Errorf("%s is not set", mysqlDsnEnv) + } + + db, err := gorm.Open(mysql.Open(dsn+"?charset=utf8&parseTime=True&loc=Local"), &gorm.Config{}) + if err != nil { + return nil, nil, fmt.Errorf("failed to open mysql connection: %v", err) + } + + dsn, cleanup, err := createRandomDB(dsn, db, kind) + if err != nil { + return nil, cleanup, err + } + + store, err = NewMysqlStoreFromSqlStore(ctx, store, dsn, nil) + if err != nil { + return nil, nil, err + } + + return store, cleanup, nil +} + +func createRandomDB(dsn string, db *gorm.DB, engine Engine) (string, func(), error) { + dbName := fmt.Sprintf("test_db_%s", strings.ReplaceAll(uuid.New().String(), "-", "_")) + + if err := db.Exec(fmt.Sprintf("CREATE DATABASE %s", dbName)).Error; err != nil { + return "", nil, fmt.Errorf("failed to create database: %v", err) + } + + var err error + cleanup := func() { + switch engine { + case PostgresStoreEngine: + err = db.Exec(fmt.Sprintf("DROP DATABASE %s WITH (FORCE)", dbName)).Error + case MysqlStoreEngine: + // err = killMySQLConnections(dsn, dbName) + err = db.Exec(fmt.Sprintf("DROP DATABASE %s", dbName)).Error + } + if err != nil { + log.Errorf("failed to drop database %s: %v", dbName, err) + panic(err) + } + sqlDB, _ := db.DB() + _ = sqlDB.Close() + } + + return replaceDBName(dsn, dbName), cleanup, nil +} + +func replaceDBName(dsn, newDBName string) string { + re := regexp.MustCompile(`(?P
    [:/@])(?P[^/?]+)(?P\?|$)`)
    +	return re.ReplaceAllString(dsn, `${pre}`+newDBName+`${post}`)
    +}
    +
     func loadSQL(db *gorm.DB, filepath string) error {
     	sqlContent, err := os.ReadFile(filepath)
     	if err != nil {
    diff --git a/management/server/telemetry/accountmanager_metrics.go b/management/server/telemetry/accountmanager_metrics.go
    index 4a5a31e2d..3b1e078eb 100644
    --- a/management/server/telemetry/accountmanager_metrics.go
    +++ b/management/server/telemetry/accountmanager_metrics.go
    @@ -22,7 +22,8 @@ func NewAccountManagerMetrics(ctx context.Context, meter metric.Meter) (*Account
     		metric.WithUnit("milliseconds"),
     		metric.WithExplicitBucketBoundaries(
     			0.5, 1, 2.5, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000,
    -		))
    +		),
    +		metric.WithDescription("Duration of triggering the account peers update and preparing the required data for the network map being sent to the clients"))
     	if err != nil {
     		return nil, err
     	}
    @@ -31,7 +32,8 @@ func NewAccountManagerMetrics(ctx context.Context, meter metric.Meter) (*Account
     		metric.WithUnit("milliseconds"),
     		metric.WithExplicitBucketBoundaries(
     			0.1, 0.5, 1, 2.5, 5, 10, 25, 50, 100, 250, 500, 1000,
    -		))
    +		),
    +		metric.WithDescription("Duration of calculating the peer network map that is sent to the clients"))
     	if err != nil {
     		return nil, err
     	}
    @@ -40,12 +42,15 @@ func NewAccountManagerMetrics(ctx context.Context, meter metric.Meter) (*Account
     		metric.WithUnit("objects"),
     		metric.WithExplicitBucketBoundaries(
     			50, 100, 200, 500, 1000, 2500, 5000, 10000,
    -		))
    +		),
    +		metric.WithDescription("Number of objects in the network map like peers, routes, firewall rules, etc. that are sent to the clients"))
     	if err != nil {
     		return nil, err
     	}
     
    -	peerMetaUpdateCount, err := meter.Int64Counter("management.account.peer.meta.update.counter", metric.WithUnit("1"))
    +	peerMetaUpdateCount, err := meter.Int64Counter("management.account.peer.meta.update.counter",
    +		metric.WithUnit("1"),
    +		metric.WithDescription("Number of updates with new meta data from the peers"))
     	if err != nil {
     		return nil, err
     	}
    diff --git a/management/server/telemetry/grpc_metrics.go b/management/server/telemetry/grpc_metrics.go
    index acbe1281c..ac6ff2ea8 100644
    --- a/management/server/telemetry/grpc_metrics.go
    +++ b/management/server/telemetry/grpc_metrics.go
    @@ -22,32 +22,50 @@ type GRPCMetrics struct {
     
     // NewGRPCMetrics creates new GRPCMetrics struct and registers common metrics of the gRPC server
     func NewGRPCMetrics(ctx context.Context, meter metric.Meter) (*GRPCMetrics, error) {
    -	syncRequestsCounter, err := meter.Int64Counter("management.grpc.sync.request.counter", metric.WithUnit("1"))
    +	syncRequestsCounter, err := meter.Int64Counter("management.grpc.sync.request.counter",
    +		metric.WithUnit("1"),
    +		metric.WithDescription("Number of sync gRPC requests from the peers to establish a connection and receive network map updates (update channel)"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
    -	loginRequestsCounter, err := meter.Int64Counter("management.grpc.login.request.counter", metric.WithUnit("1"))
    +	loginRequestsCounter, err := meter.Int64Counter("management.grpc.login.request.counter",
    +		metric.WithUnit("1"),
    +		metric.WithDescription("Number of login gRPC requests from the peers to authenticate and receive initial configuration and relay credentials"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
    -	getKeyRequestsCounter, err := meter.Int64Counter("management.grpc.key.request.counter", metric.WithUnit("1"))
    +	getKeyRequestsCounter, err := meter.Int64Counter("management.grpc.key.request.counter",
    +		metric.WithUnit("1"),
    +		metric.WithDescription("Number of key gRPC requests from the peers to get the server's public WireGuard key"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
    -	activeStreamsGauge, err := meter.Int64ObservableGauge("management.grpc.connected.streams", metric.WithUnit("1"))
    +	activeStreamsGauge, err := meter.Int64ObservableGauge("management.grpc.connected.streams",
    +		metric.WithUnit("1"),
    +		metric.WithDescription("Number of active peer streams connected to the gRPC server"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
    -	syncRequestDuration, err := meter.Int64Histogram("management.grpc.sync.request.duration.ms", metric.WithUnit("milliseconds"))
    +	syncRequestDuration, err := meter.Int64Histogram("management.grpc.sync.request.duration.ms",
    +		metric.WithUnit("milliseconds"),
    +		metric.WithDescription("Duration of the sync gRPC requests from the peers to establish a connection and receive network map updates (update channel)"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
    -	loginRequestDuration, err := meter.Int64Histogram("management.grpc.login.request.duration.ms", metric.WithUnit("milliseconds"))
    +	loginRequestDuration, err := meter.Int64Histogram("management.grpc.login.request.duration.ms",
    +		metric.WithUnit("milliseconds"),
    +		metric.WithDescription("Duration of the login gRPC requests from the peers to authenticate and receive initial configuration and relay credentials"),
    +	)
     	if err != nil {
     		return nil, err
     	}
    @@ -57,7 +75,7 @@ func NewGRPCMetrics(ctx context.Context, meter metric.Meter) (*GRPCMetrics, erro
     	// TODO(yury): This needs custom bucketing as we are interested in the values from 0 to server.channelBufferSize (100)
     	channelQueue, err := meter.Int64Histogram(
     		"management.grpc.updatechannel.queue",
    -		metric.WithDescription("Number of update messages in the channel queue"),
    +		metric.WithDescription("Number of update messages piling up in the update channel queue"),
     		metric.WithUnit("length"),
     	)
     	if err != nil {
    diff --git a/management/server/telemetry/http_api_metrics.go b/management/server/telemetry/http_api_metrics.go
    index 357f019c7..5ef9e6d02 100644
    --- a/management/server/telemetry/http_api_metrics.go
    +++ b/management/server/telemetry/http_api_metrics.go
    @@ -74,37 +74,58 @@ type HTTPMiddleware struct {
     
     // NewMetricsMiddleware creates a new HTTPMiddleware
     func NewMetricsMiddleware(ctx context.Context, meter metric.Meter) (*HTTPMiddleware, error) {
    -	httpRequestCounter, err := meter.Int64Counter(httpRequestCounterPrefix, metric.WithUnit("1"))
    +	httpRequestCounter, err := meter.Int64Counter(httpRequestCounterPrefix,
    +		metric.WithUnit("1"),
    +		metric.WithDescription("Number of incoming HTTP requests by endpoint and method"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
    -	httpResponseCounter, err := meter.Int64Counter(httpResponseCounterPrefix, metric.WithUnit("1"))
    +	httpResponseCounter, err := meter.Int64Counter(httpResponseCounterPrefix,
    +		metric.WithUnit("1"),
    +		metric.WithDescription("Number of outgoing HTTP responses by endpoint, method and returned status code"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
    -	totalHTTPRequestsCounter, err := meter.Int64Counter(fmt.Sprintf("%s.total", httpRequestCounterPrefix), metric.WithUnit("1"))
    +	totalHTTPRequestsCounter, err := meter.Int64Counter(fmt.Sprintf("%s.total", httpRequestCounterPrefix),
    +		metric.WithUnit("1"),
    +		metric.WithDescription("Number of incoming HTTP requests"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
    -	totalHTTPResponseCounter, err := meter.Int64Counter(fmt.Sprintf("%s.total", httpResponseCounterPrefix), metric.WithUnit("1"))
    +	totalHTTPResponseCounter, err := meter.Int64Counter(fmt.Sprintf("%s.total", httpResponseCounterPrefix),
    +		metric.WithUnit("1"),
    +		metric.WithDescription("Number of outgoing HTTP responses"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
    -	totalHTTPResponseCodeCounter, err := meter.Int64Counter(fmt.Sprintf("%s.code.total", httpResponseCounterPrefix), metric.WithUnit("1"))
    +	totalHTTPResponseCodeCounter, err := meter.Int64Counter(fmt.Sprintf("%s.code.total", httpResponseCounterPrefix),
    +		metric.WithUnit("1"),
    +		metric.WithDescription("Number of outgoing HTTP responses by status code"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
    -	httpRequestDuration, err := meter.Int64Histogram(httpRequestDurationPrefix, metric.WithUnit("milliseconds"))
    +	httpRequestDuration, err := meter.Int64Histogram(httpRequestDurationPrefix,
    +		metric.WithUnit("milliseconds"),
    +		metric.WithDescription("Duration of incoming HTTP requests by endpoint and method"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
    -	totalHTTPRequestDuration, err := meter.Int64Histogram(fmt.Sprintf("%s.total", httpRequestDurationPrefix), metric.WithUnit("milliseconds"))
    +	totalHTTPRequestDuration, err := meter.Int64Histogram(fmt.Sprintf("%s.total", httpRequestDurationPrefix),
    +		metric.WithUnit("milliseconds"),
    +		metric.WithDescription("Duration of incoming HTTP requests"),
    +	)
     	if err != nil {
     		return nil, err
     	}
    diff --git a/management/server/telemetry/idp_metrics.go b/management/server/telemetry/idp_metrics.go
    index 0bcd5d432..5337c91c2 100644
    --- a/management/server/telemetry/idp_metrics.go
    +++ b/management/server/telemetry/idp_metrics.go
    @@ -23,43 +23,73 @@ type IDPMetrics struct {
     
     // NewIDPMetrics creates new IDPMetrics struct and registers common
     func NewIDPMetrics(ctx context.Context, meter metric.Meter) (*IDPMetrics, error) {
    -	metaUpdateCounter, err := meter.Int64Counter("management.idp.update.user.meta.counter", metric.WithUnit("1"))
    +	metaUpdateCounter, err := meter.Int64Counter("management.idp.update.user.meta.counter",
    +		metric.WithUnit("1"),
    +		metric.WithDescription("Number of updates of user metadata sent to the configured identity provider"),
    +	)
     	if err != nil {
     		return nil, err
     	}
    -	getUserByEmailCounter, err := meter.Int64Counter("management.idp.get.user.by.email.counter", metric.WithUnit("1"))
    +	getUserByEmailCounter, err := meter.Int64Counter("management.idp.get.user.by.email.counter",
    +		metric.WithUnit("1"),
    +		metric.WithDescription("Number of requests to get a user by email from the configured identity provider"),
    +	)
     	if err != nil {
     		return nil, err
     	}
    -	getAllAccountsCounter, err := meter.Int64Counter("management.idp.get.accounts.counter", metric.WithUnit("1"))
    +	getAllAccountsCounter, err := meter.Int64Counter("management.idp.get.accounts.counter",
    +		metric.WithUnit("1"),
    +		metric.WithDescription("Number of requests to get all accounts from the configured identity provider"),
    +	)
     	if err != nil {
     		return nil, err
     	}
    -	createUserCounter, err := meter.Int64Counter("management.idp.create.user.counter", metric.WithUnit("1"))
    +	createUserCounter, err := meter.Int64Counter("management.idp.create.user.counter",
    +		metric.WithUnit("1"),
    +		metric.WithDescription("Number of requests to create a new user in the configured identity provider"),
    +	)
     	if err != nil {
     		return nil, err
     	}
    -	deleteUserCounter, err := meter.Int64Counter("management.idp.delete.user.counter", metric.WithUnit("1"))
    +	deleteUserCounter, err := meter.Int64Counter("management.idp.delete.user.counter",
    +		metric.WithUnit("1"),
    +		metric.WithDescription("Number of requests to delete a user from the configured identity provider"),
    +	)
     	if err != nil {
     		return nil, err
     	}
    -	getAccountCounter, err := meter.Int64Counter("management.idp.get.account.counter", metric.WithUnit("1"))
    +	getAccountCounter, err := meter.Int64Counter("management.idp.get.account.counter",
    +		metric.WithUnit("1"),
    +		metric.WithDescription("Number of requests to get all users in an account from the configured identity provider"),
    +	)
     	if err != nil {
     		return nil, err
     	}
    -	getUserByIDCounter, err := meter.Int64Counter("management.idp.get.user.by.id.counter", metric.WithUnit("1"))
    +	getUserByIDCounter, err := meter.Int64Counter("management.idp.get.user.by.id.counter",
    +		metric.WithUnit("1"),
    +		metric.WithDescription("Number of requests to get a user by ID from the configured identity provider"),
    +	)
     	if err != nil {
     		return nil, err
     	}
    -	authenticateRequestCounter, err := meter.Int64Counter("management.idp.authenticate.request.counter", metric.WithUnit("1"))
    +	authenticateRequestCounter, err := meter.Int64Counter("management.idp.authenticate.request.counter",
    +		metric.WithUnit("1"),
    +		metric.WithDescription("Number of requests to authenticate the server with the configured identity provider"),
    +	)
     	if err != nil {
     		return nil, err
     	}
    -	requestErrorCounter, err := meter.Int64Counter("management.idp.request.error.counter", metric.WithUnit("1"))
    +	requestErrorCounter, err := meter.Int64Counter("management.idp.request.error.counter",
    +		metric.WithUnit("1"),
    +		metric.WithDescription("Number of errors that happened when doing http request to the configured identity provider"),
    +	)
     	if err != nil {
     		return nil, err
     	}
    -	requestStatusErrorCounter, err := meter.Int64Counter("management.idp.request.status.error.counter", metric.WithUnit("1"))
    +	requestStatusErrorCounter, err := meter.Int64Counter("management.idp.request.status.error.counter",
    +		metric.WithUnit("1"),
    +		metric.WithDescription("Number of responses that came from the configured identity provider with non success status code"),
    +	)
     	if err != nil {
     		return nil, err
     	}
    diff --git a/management/server/telemetry/store_metrics.go b/management/server/telemetry/store_metrics.go
    index bb3745b5a..f035ce847 100644
    --- a/management/server/telemetry/store_metrics.go
    +++ b/management/server/telemetry/store_metrics.go
    @@ -20,28 +20,41 @@ type StoreMetrics struct {
     // NewStoreMetrics creates an instance of StoreMetrics
     func NewStoreMetrics(ctx context.Context, meter metric.Meter) (*StoreMetrics, error) {
     	globalLockAcquisitionDurationMicro, err := meter.Int64Histogram("management.store.global.lock.acquisition.duration.micro",
    -		metric.WithUnit("microseconds"))
    +		metric.WithUnit("microseconds"),
    +		metric.WithDescription("Duration of how long it takes to acquire the global lock in the store to block all other requests to the store"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
    -	globalLockAcquisitionDurationMs, err := meter.Int64Histogram("management.store.global.lock.acquisition.duration.ms")
    +	globalLockAcquisitionDurationMs, err := meter.Int64Histogram("management.store.global.lock.acquisition.duration.ms",
    +		metric.WithUnit("milliseconds"),
    +		metric.WithDescription("Duration of how long a process holds the acquired global lock in the store"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
     	persistenceDurationMicro, err := meter.Int64Histogram("management.store.persistence.duration.micro",
    -		metric.WithUnit("microseconds"))
    +		metric.WithUnit("microseconds"),
    +		metric.WithDescription("Duration of how long it takes to save or delete an account in the store"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
    -	persistenceDurationMs, err := meter.Int64Histogram("management.store.persistence.duration.ms")
    +	persistenceDurationMs, err := meter.Int64Histogram("management.store.persistence.duration.ms",
    +		metric.WithUnit("milliseconds"),
    +		metric.WithDescription("Duration of how long it takes to save or delete an account in the store"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
    -	transactionDurationMs, err := meter.Int64Histogram("management.store.transaction.duration.ms")
    +	transactionDurationMs, err := meter.Int64Histogram("management.store.transaction.duration.ms",
    +		metric.WithUnit("milliseconds"),
    +		metric.WithDescription("Duration of how long it takes to execute a transaction in the store"),
    +	)
     	if err != nil {
     		return nil, err
     	}
    diff --git a/management/server/telemetry/updatechannel_metrics.go b/management/server/telemetry/updatechannel_metrics.go
    index 2582006e5..584b9ec20 100644
    --- a/management/server/telemetry/updatechannel_metrics.go
    +++ b/management/server/telemetry/updatechannel_metrics.go
    @@ -23,42 +23,68 @@ type UpdateChannelMetrics struct {
     
     // NewUpdateChannelMetrics creates an instance of UpdateChannel
     func NewUpdateChannelMetrics(ctx context.Context, meter metric.Meter) (*UpdateChannelMetrics, error) {
    -	createChannelDurationMicro, err := meter.Int64Histogram("management.updatechannel.create.duration.micro")
    +	createChannelDurationMicro, err := meter.Int64Histogram("management.updatechannel.create.duration.micro",
    +		metric.WithUnit("microseconds"),
    +		metric.WithDescription("Duration of how long it takes to create a new peer update channel"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
    -	closeChannelDurationMicro, err := meter.Int64Histogram("management.updatechannel.close.one.duration.micro")
    +	closeChannelDurationMicro, err := meter.Int64Histogram("management.updatechannel.close.one.duration.micro",
    +		metric.WithUnit("microseconds"),
    +		metric.WithDescription("Duration of how long it takes to close a peer update channel"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
    -	closeChannelsDurationMicro, err := meter.Int64Histogram("management.updatechannel.close.multiple.duration.micro")
    +	closeChannelsDurationMicro, err := meter.Int64Histogram("management.updatechannel.close.multiple.duration.micro",
    +		metric.WithUnit("microseconds"),
    +		metric.WithDescription("Duration of how long it takes to close a set of peer update channels"),
    +	)
    +
     	if err != nil {
     		return nil, err
     	}
     
    -	closeChannels, err := meter.Int64Histogram("management.updatechannel.close.multiple.channels")
    +	closeChannels, err := meter.Int64Histogram("management.updatechannel.close.multiple.channels",
    +		metric.WithUnit("1"),
    +		metric.WithDescription("Number of peer update channels that have been closed"),
    +	)
    +
     	if err != nil {
     		return nil, err
     	}
     
    -	sendUpdateDurationMicro, err := meter.Int64Histogram("management.updatechannel.send.duration.micro")
    +	sendUpdateDurationMicro, err := meter.Int64Histogram("management.updatechannel.send.duration.micro",
    +		metric.WithUnit("microseconds"),
    +		metric.WithDescription("Duration of how long it takes to send an network map update to a peer"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
    -	getAllConnectedPeersDurationMicro, err := meter.Int64Histogram("management.updatechannel.get.all.duration.micro")
    +	getAllConnectedPeersDurationMicro, err := meter.Int64Histogram("management.updatechannel.get.all.duration.micro",
    +		metric.WithUnit("microseconds"),
    +		metric.WithDescription("Duration of how long it takes to get all connected peers"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
    -	getAllConnectedPeers, err := meter.Int64Histogram("management.updatechannel.get.all.peers")
    +	getAllConnectedPeers, err := meter.Int64Histogram("management.updatechannel.get.all.peers",
    +		metric.WithUnit("1"),
    +		metric.WithDescription("Number of connected peers"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
    -	hasChannelDurationMicro, err := meter.Int64Histogram("management.updatechannel.haschannel.duration.micro")
    +	hasChannelDurationMicro, err := meter.Int64Histogram("management.updatechannel.haschannel.duration.micro",
    +		metric.WithUnit("microseconds"),
    +		metric.WithDescription("Duration of how long it takes to check if a peer has a channel"),
    +	)
     	if err != nil {
     		return nil, err
     	}
    diff --git a/management/server/testdata/management.json b/management/server/testdata/management.json
    index d29491118..f797a7d2b 100644
    --- a/management/server/testdata/management.json
    +++ b/management/server/testdata/management.json
    @@ -2,7 +2,7 @@
         "Stuns": [
             {
                 "Proto": "udp",
    -            "URI": "stun:stun.wiretrustee.com:3468",
    +            "URI": "stun:stun.netbird.io:3468",
                 "Username": "",
                 "Password": null
             }
    @@ -11,7 +11,7 @@
             "Turns": [
                 {
                     "Proto": "udp",
    -                "URI": "turn:stun.wiretrustee.com:3468",
    +                "URI": "turn:stun.netbird.io:3468",
                     "Username": "some_user",
                     "Password": "some_password"
                 }
    @@ -22,7 +22,7 @@
         },
         "Signal": {
             "Proto": "http",
    -        "URI": "signal.wiretrustee.com:10000",
    +        "URI": "signal.netbird.io:10000",
             "Username": "",
             "Password": null
         },
    @@ -44,4 +44,4 @@
                 "GrantType": "client_credentials"
             }
         }
    -}
    \ No newline at end of file
    +}
    diff --git a/management/server/testdata/store.sql b/management/server/testdata/store.sql
    index 84524127f..41b8fa2f7 100644
    --- a/management/server/testdata/store.sql
    +++ b/management/server/testdata/store.sql
    @@ -19,6 +19,7 @@ CREATE INDEX `idx_accounts_domain` ON `accounts`(`domain`);
     CREATE INDEX `idx_setup_keys_account_id` ON `setup_keys`(`account_id`);
     CREATE INDEX `idx_peers_key` ON `peers`(`key`);
     CREATE INDEX `idx_peers_account_id` ON `peers`(`account_id`);
    +CREATE INDEX `idx_peers_account_id_ip` ON `peers`(`account_id`,`ip`);
     CREATE INDEX `idx_users_account_id` ON `users`(`account_id`);
     CREATE INDEX `idx_personal_access_tokens_user_id` ON `personal_access_tokens`(`user_id`);
     CREATE INDEX `idx_groups_account_id` ON `groups`(`account_id`);
    @@ -39,8 +40,11 @@ CREATE INDEX `idx_networks_account_id` ON `networks`(`account_id`);
     INSERT INTO accounts VALUES('bf1c8084-ba50-4ce7-9439-34653001fc3b','edafee4e-63fb-11ec-90d6-0242ac120003','2024-10-02 16:03:06.778746+02:00','test.com','private',1,'af1c8024-ha40-4ce2-9418-34653101fc3c','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',0,86400000000000,0,0,0,'',NULL,NULL,NULL);
     INSERT INTO "groups" VALUES('cs1tnh0hhcjnqoiuebeg','bf1c8084-ba50-4ce7-9439-34653001fc3b','All','api','[]',0,'');
     INSERT INTO setup_keys VALUES('','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBB','Default key','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,NULL,'["cs1tnh0hhcjnqoiuebeg"]',0,0);
    +INSERT INTO users VALUES('a23efe53-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','owner',0,0,'','[]',0,NULL,'2024-10-02 16:03:06.779156+02:00','api',0,'');
     INSERT INTO users VALUES('edafee4e-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','admin',0,0,'','[]',0,NULL,'2024-10-02 16:03:06.779156+02:00','api',0,'');
     INSERT INTO users VALUES('f4f6d672-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','user',0,0,'','[]',0,NULL,'2024-10-02 16:03:06.779156+02:00','api',0,'');
    +-- Unhashed PAT is "nbp_apTmlmUXHSC4PKmHwtIZNaGr8eqcVI2gMURp"
    +INSERT INTO personal_access_tokens VALUES('9dj38s35-63fb-11ec-90d6-0242ac120004','a23efe53-63fb-11ec-90d6-0242ac120003','','smJvzexPcQ3NRezrVDUmF++0XqvFvXzx8Rsn2y9r1z0=','5023-02-27 00:00:00+00:00','user','2023-01-01 00:00:00+00:00','2023-02-01 00:00:00+00:00');
     INSERT INTO personal_access_tokens VALUES('9dj38s35-63fb-11ec-90d6-0242ac120003','f4f6d672-63fb-11ec-90d6-0242ac120003','','SoMeHaShEdToKeN','2023-02-27 00:00:00+00:00','user','2023-01-01 00:00:00+00:00','2023-02-01 00:00:00+00:00');
     INSERT INTO installations VALUES(1,'');
     INSERT INTO policies VALUES('cs1tnh0hhcjnqoiuebf0','bf1c8084-ba50-4ce7-9439-34653001fc3b','Default','This is a default rule that allows connections between all the resources',1,'[]');
    @@ -48,3 +52,4 @@ INSERT INTO policy_rules VALUES('cs387mkv2d4bgq41b6n0','cs1tnh0hhcjnqoiuebf0','D
     INSERT INTO network_routers VALUES('ctc20ji7qv9ck2sebc80','ct286bi7qv930dsrrug0','bf1c8084-ba50-4ce7-9439-34653001fc3b','cs1tnh0hhcjnqoiuebeg',NULL,0,0);
     INSERT INTO network_resources VALUES ('ctc4nci7qv9061u6ilfg','ct286bi7qv930dsrrug0','bf1c8084-ba50-4ce7-9439-34653001fc3b','Host','192.168.1.1');
     INSERT INTO networks VALUES('ct286bi7qv930dsrrug0','bf1c8084-ba50-4ce7-9439-34653001fc3b','Test Network','Test Network');
    +INSERT INTO peers VALUES('ct286bi7qv930dsrrug0','bf1c8084-ba50-4ce7-9439-34653001fc3b','','','192.168.0.0','','','','','','','','','','','','','','','','','test','test','2023-01-01 00:00:00+00:00',0,0,0,'a23efe53-63fb-11ec-90d6-0242ac120003','',0,0,'2023-01-01 00:00:00+00:00','2023-01-01 00:00:00+00:00',0,'','','',0);
    diff --git a/management/server/testdata/storev1.sql b/management/server/testdata/storev1.sql
    index cda333d4f..8b09ec2be 100644
    --- a/management/server/testdata/storev1.sql
    +++ b/management/server/testdata/storev1.sql
    @@ -36,4 +36,3 @@ INSERT INTO peers VALUES('xlx9/9D8+ibnRiIIB8nHGMxGOzxV17r8ShPHgi4aYSM=','auth0|6
     INSERT INTO peers VALUES('6kjbmVq1hmucVzvBXo5OucY5OYv+jSsB1jUTLq291Dw=','google-oauth2|103201118415301331038','6kjbmVq1hmucVzvBXo5OucY5OYv+jSsB1jUTLq291Dw=','5AFB60DB-61F2-4251-8E11-494847EE88E9','"100.64.0.2"','braginini','linux','Linux','21.04','x86_64','Ubuntu','','','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'braginini','braginini','2021-12-24 16:12:05.994305438+01:00',0,0,0,'','',0,0,NULL,'2024-10-02 17:00:54.228182+02:00',0,'""','','',0);
     INSERT INTO peers VALUES('Ok+5QMdt/UjoktNOvicGYj+IX2g98p+0N2PJ3vJ45RI=','google-oauth2|103201118415301331038','Ok+5QMdt/UjoktNOvicGYj+IX2g98p+0N2PJ3vJ45RI=','A72E4DC2-00DE-4542-8A24-62945438104E','"100.64.0.1"','braginini','linux','Linux','21.04','x86_64','Ubuntu','','','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'braginini','braginini-1','2021-12-24 16:11:27.015739803+01:00',0,0,0,'','',0,0,NULL,'2024-10-02 17:00:54.228182+02:00',1,'""','','',0);
     INSERT INTO installations VALUES(1,'');
    -
    diff --git a/management/server/testutil/store.go b/management/server/testutil/store.go
    index 16438cab8..8672efa7f 100644
    --- a/management/server/testutil/store.go
    +++ b/management/server/testutil/store.go
    @@ -22,7 +22,7 @@ func CreateMysqlTestContainer() (func(), error) {
     	myContainer, err := mysql.RunContainer(ctx,
     		testcontainers.WithImage("mlsmaycon/warmed-mysql:8"),
     		mysql.WithDatabase("testing"),
    -		mysql.WithUsername("testing"),
    +		mysql.WithUsername("root"),
     		mysql.WithPassword("testing"),
     		testcontainers.WithWaitStrategy(
     			wait.ForLog("/usr/sbin/mysqld: ready for connections").
    @@ -34,6 +34,7 @@ func CreateMysqlTestContainer() (func(), error) {
     	}
     
     	cleanup := func() {
    +		os.Unsetenv("NETBIRD_STORE_ENGINE_MYSQL_DSN")
     		timeoutCtx, cancelFunc := context.WithTimeout(ctx, 1*time.Second)
     		defer cancelFunc()
     		if err = myContainer.Terminate(timeoutCtx); err != nil {
    @@ -68,6 +69,7 @@ func CreatePostgresTestContainer() (func(), error) {
     	}
     
     	cleanup := func() {
    +		os.Unsetenv("NETBIRD_STORE_ENGINE_POSTGRES_DSN")
     		timeoutCtx, cancelFunc := context.WithTimeout(ctx, 1*time.Second)
     		defer cancelFunc()
     		if err = pgContainer.Terminate(timeoutCtx); err != nil {
    diff --git a/management/server/token_mgr.go b/management/server/token_mgr.go
    index fd67fa3e3..ec8aae47e 100644
    --- a/management/server/token_mgr.go
    +++ b/management/server/token_mgr.go
    @@ -199,7 +199,7 @@ func (m *TimeBasedAuthSecretsManager) pushNewTURNAndRelayTokens(ctx context.Cont
     	}
     
     	update := &proto.SyncResponse{
    -		WiretrusteeConfig: &proto.WiretrusteeConfig{
    +		NetbirdConfig: &proto.NetbirdConfig{
     			Turns: turns,
     		},
     	}
    @@ -208,7 +208,7 @@ func (m *TimeBasedAuthSecretsManager) pushNewTURNAndRelayTokens(ctx context.Cont
     	if m.relayCfg != nil {
     		token, err := m.GenerateRelayToken()
     		if err == nil {
    -			update.WiretrusteeConfig.Relay = &proto.RelayConfig{
    +			update.NetbirdConfig.Relay = &proto.RelayConfig{
     				Urls:           m.relayCfg.Addresses,
     				TokenPayload:   token.Payload,
     				TokenSignature: token.Signature,
    @@ -228,7 +228,7 @@ func (m *TimeBasedAuthSecretsManager) pushNewRelayTokens(ctx context.Context, pe
     	}
     
     	update := &proto.SyncResponse{
    -		WiretrusteeConfig: &proto.WiretrusteeConfig{
    +		NetbirdConfig: &proto.NetbirdConfig{
     			Relay: &proto.RelayConfig{
     				Urls:           m.relayCfg.Addresses,
     				TokenPayload:   string(relayToken.Payload),
    diff --git a/management/server/token_mgr_test.go b/management/server/token_mgr_test.go
    index 2aafb9f68..f2b056d8f 100644
    --- a/management/server/token_mgr_test.go
    +++ b/management/server/token_mgr_test.go
    @@ -18,7 +18,7 @@ import (
     
     var TurnTestHost = &Host{
     	Proto:    UDP,
    -	URI:      "turn:turn.wiretrustee.com:77777",
    +	URI:      "turn:turn.netbird.io:77777",
     	Username: "username",
     	Password: "",
     }
    @@ -124,7 +124,7 @@ loop:
     	var firstRelayUpdate, secondRelayUpdate *proto.RelayConfig
     
     	for _, update := range updates {
    -		if turns := update.Update.GetWiretrusteeConfig().GetTurns(); len(turns) > 0 {
    +		if turns := update.Update.GetNetbirdConfig().GetTurns(); len(turns) > 0 {
     			turnUpdates++
     			if turnUpdates == 1 {
     				firstTurnUpdate = turns[0]
    @@ -132,9 +132,9 @@ loop:
     				secondTurnUpdate = turns[0]
     			}
     		}
    -		if relay := update.Update.GetWiretrusteeConfig().GetRelay(); relay != nil {
    +		if relay := update.Update.GetNetbirdConfig().GetRelay(); relay != nil {
     			// avoid updating on turn updates since they also send relay credentials
    -			if update.Update.GetWiretrusteeConfig().GetTurns() == nil {
    +			if update.Update.GetNetbirdConfig().GetTurns() == nil {
     				relayUpdates++
     				if relayUpdates == 1 {
     					firstRelayUpdate = relay
    diff --git a/management/server/types/account.go b/management/server/types/account.go
    index f74d38cb6..c890a7730 100644
    --- a/management/server/types/account.go
    +++ b/management/server/types/account.go
    @@ -12,6 +12,7 @@ import (
     
     	"github.com/hashicorp/go-multierror"
     	"github.com/miekg/dns"
    +	"github.com/rs/xid"
     	log "github.com/sirupsen/logrus"
     
     	nbdns "github.com/netbirdio/netbird/dns"
    @@ -289,14 +290,14 @@ func (a *Account) GetPeerNetworkMap(
     	}
     
     	if metrics != nil {
    -		objectCount := int64(len(peersToConnect) + len(expiredPeers) + len(routesUpdate) + len(firewallRules))
    +		objectCount := int64(len(peersToConnectIncludingRouters) + len(expiredPeers) + len(routesUpdate) + len(networkResourcesRoutes) + len(firewallRules) + +len(networkResourcesFirewallRules) + len(routesFirewallRules))
     		metrics.CountNetworkMapObjects(objectCount)
     		metrics.CountGetPeerNetworkMapDuration(time.Since(start))
     
     		if objectCount > 5000 {
     			log.WithContext(ctx).Tracef("account: %s has a total resource count of %d objects, "+
    -				"peers to connect: %d, expired peers: %d, routes: %d, firewall rules: %d",
    -				a.Id, objectCount, len(peersToConnect), len(expiredPeers), len(routesUpdate), len(firewallRules))
    +				"peers to connect: %d, expired peers: %d, routes: %d, firewall rules: %d, network resources routes: %d, network resources firewall rules: %d, routes firewall rules: %d",
    +				a.Id, objectCount, len(peersToConnectIncludingRouters), len(expiredPeers), len(routesUpdate), len(firewallRules), len(networkResourcesRoutes), len(networkResourcesFirewallRules), len(routesFirewallRules))
     		}
     	}
     
    @@ -459,8 +460,23 @@ func (a *Account) GetPeersCustomZone(ctx context.Context, dnsDomain string) nbdn
     			TTL:   defaultTTL,
     			RData: peer.IP.String(),
     		})
    -
     		sb.Reset()
    +
    +		for _, extraLabel := range peer.ExtraDNSLabels {
    +			sb.Grow(len(extraLabel) + len(domainSuffix))
    +			sb.WriteString(extraLabel)
    +			sb.WriteString(domainSuffix)
    +
    +			customZone.Records = append(customZone.Records, nbdns.SimpleRecord{
    +				Name:  sb.String(),
    +				Type:  int(dns.TypeA),
    +				Class: nbdns.DefaultClass,
    +				TTL:   defaultTTL,
    +				RData: peer.IP.String(),
    +			})
    +			sb.Reset()
    +		}
    +
     	}
     
     	go func() {
    @@ -1510,3 +1526,43 @@ func getPoliciesSourcePeers(policies []*Policy, groups map[string]*Group) map[st
     
     	return sourcePeers
     }
    +
    +// AddAllGroup to account object if it doesn't exist
    +func (a *Account) AddAllGroup() error {
    +	if len(a.Groups) == 0 {
    +		allGroup := &Group{
    +			ID:     xid.New().String(),
    +			Name:   "All",
    +			Issued: GroupIssuedAPI,
    +		}
    +		for _, peer := range a.Peers {
    +			allGroup.Peers = append(allGroup.Peers, peer.ID)
    +		}
    +		a.Groups = map[string]*Group{allGroup.ID: allGroup}
    +
    +		id := xid.New().String()
    +
    +		defaultPolicy := &Policy{
    +			ID:          id,
    +			Name:        DefaultRuleName,
    +			Description: DefaultRuleDescription,
    +			Enabled:     true,
    +			Rules: []*PolicyRule{
    +				{
    +					ID:            id,
    +					Name:          DefaultRuleName,
    +					Description:   DefaultRuleDescription,
    +					Enabled:       true,
    +					Sources:       []string{allGroup.ID},
    +					Destinations:  []string{allGroup.ID},
    +					Bidirectional: true,
    +					Protocol:      PolicyRuleProtocolALL,
    +					Action:        PolicyTrafficActionAccept,
    +				},
    +			},
    +		}
    +
    +		a.Policies = []*Policy{defaultPolicy}
    +	}
    +	return nil
    +}
    diff --git a/management/server/types/policyrule.go b/management/server/types/policyrule.go
    index bd9a99292..721621a4b 100644
    --- a/management/server/types/policyrule.go
    +++ b/management/server/types/policyrule.go
    @@ -66,18 +66,20 @@ type PolicyRule struct {
     // Copy returns a copy of a policy rule
     func (pm *PolicyRule) Copy() *PolicyRule {
     	rule := &PolicyRule{
    -		ID:            pm.ID,
    -		PolicyID:      pm.PolicyID,
    -		Name:          pm.Name,
    -		Description:   pm.Description,
    -		Enabled:       pm.Enabled,
    -		Action:        pm.Action,
    -		Destinations:  make([]string, len(pm.Destinations)),
    -		Sources:       make([]string, len(pm.Sources)),
    -		Bidirectional: pm.Bidirectional,
    -		Protocol:      pm.Protocol,
    -		Ports:         make([]string, len(pm.Ports)),
    -		PortRanges:    make([]RulePortRange, len(pm.PortRanges)),
    +		ID:                  pm.ID,
    +		PolicyID:            pm.PolicyID,
    +		Name:                pm.Name,
    +		Description:         pm.Description,
    +		Enabled:             pm.Enabled,
    +		Action:              pm.Action,
    +		Destinations:        make([]string, len(pm.Destinations)),
    +		DestinationResource: pm.DestinationResource,
    +		Sources:             make([]string, len(pm.Sources)),
    +		SourceResource:      pm.SourceResource,
    +		Bidirectional:       pm.Bidirectional,
    +		Protocol:            pm.Protocol,
    +		Ports:               make([]string, len(pm.Ports)),
    +		PortRanges:          make([]RulePortRange, len(pm.PortRanges)),
     	}
     	copy(rule.Destinations, pm.Destinations)
     	copy(rule.Sources, pm.Sources)
    diff --git a/management/server/types/setupkey.go b/management/server/types/setupkey.go
    index 2cd835289..ab8e46bea 100644
    --- a/management/server/types/setupkey.go
    +++ b/management/server/types/setupkey.go
    @@ -10,6 +10,7 @@ import (
     	"unicode/utf8"
     
     	"github.com/google/uuid"
    +
     	"github.com/netbirdio/netbird/management/server/util"
     )
     
    @@ -54,6 +55,8 @@ type SetupKey struct {
     	UsageLimit int
     	// Ephemeral indicate if the peers will be ephemeral or not
     	Ephemeral bool
    +	// AllowExtraDNSLabels indicates if the key allows extra DNS labels
    +	AllowExtraDNSLabels bool
     }
     
     // Copy copies SetupKey to a new object
    @@ -64,21 +67,22 @@ func (key *SetupKey) Copy() *SetupKey {
     		key.UpdatedAt = key.CreatedAt
     	}
     	return &SetupKey{
    -		Id:         key.Id,
    -		AccountID:  key.AccountID,
    -		Key:        key.Key,
    -		KeySecret:  key.KeySecret,
    -		Name:       key.Name,
    -		Type:       key.Type,
    -		CreatedAt:  key.CreatedAt,
    -		ExpiresAt:  key.ExpiresAt,
    -		UpdatedAt:  key.UpdatedAt,
    -		Revoked:    key.Revoked,
    -		UsedTimes:  key.UsedTimes,
    -		LastUsed:   key.LastUsed,
    -		AutoGroups: autoGroups,
    -		UsageLimit: key.UsageLimit,
    -		Ephemeral:  key.Ephemeral,
    +		Id:                  key.Id,
    +		AccountID:           key.AccountID,
    +		Key:                 key.Key,
    +		KeySecret:           key.KeySecret,
    +		Name:                key.Name,
    +		Type:                key.Type,
    +		CreatedAt:           key.CreatedAt,
    +		ExpiresAt:           key.ExpiresAt,
    +		UpdatedAt:           key.UpdatedAt,
    +		Revoked:             key.Revoked,
    +		UsedTimes:           key.UsedTimes,
    +		LastUsed:            key.LastUsed,
    +		AutoGroups:          autoGroups,
    +		UsageLimit:          key.UsageLimit,
    +		Ephemeral:           key.Ephemeral,
    +		AllowExtraDNSLabels: key.AllowExtraDNSLabels,
     	}
     }
     
    @@ -150,7 +154,7 @@ func (key *SetupKey) IsOverUsed() bool {
     
     // GenerateSetupKey generates a new setup key
     func GenerateSetupKey(name string, t SetupKeyType, validFor time.Duration, autoGroups []string,
    -	usageLimit int, ephemeral bool) (*SetupKey, string) {
    +	usageLimit int, ephemeral bool, allowExtraDNSLabels bool) (*SetupKey, string) {
     	key := strings.ToUpper(uuid.New().String())
     	limit := usageLimit
     	if t == SetupKeyOneOff {
    @@ -166,26 +170,27 @@ func GenerateSetupKey(name string, t SetupKeyType, validFor time.Duration, autoG
     	encodedHashedKey := b64.StdEncoding.EncodeToString(hashedKey[:])
     
     	return &SetupKey{
    -		Id:         strconv.Itoa(int(Hash(key))),
    -		Key:        encodedHashedKey,
    -		KeySecret:  HiddenKey(key, 4),
    -		Name:       name,
    -		Type:       t,
    -		CreatedAt:  time.Now().UTC(),
    -		ExpiresAt:  expiresAt,
    -		UpdatedAt:  time.Now().UTC(),
    -		Revoked:    false,
    -		UsedTimes:  0,
    -		AutoGroups: autoGroups,
    -		UsageLimit: limit,
    -		Ephemeral:  ephemeral,
    +		Id:                  strconv.Itoa(int(Hash(key))),
    +		Key:                 encodedHashedKey,
    +		KeySecret:           HiddenKey(key, 4),
    +		Name:                name,
    +		Type:                t,
    +		CreatedAt:           time.Now().UTC(),
    +		ExpiresAt:           expiresAt,
    +		UpdatedAt:           time.Now().UTC(),
    +		Revoked:             false,
    +		UsedTimes:           0,
    +		AutoGroups:          autoGroups,
    +		UsageLimit:          limit,
    +		Ephemeral:           ephemeral,
    +		AllowExtraDNSLabels: allowExtraDNSLabels,
     	}, key
     }
     
     // GenerateDefaultSetupKey generates a default reusable setup key with an unlimited usage and 30 days expiration
     func GenerateDefaultSetupKey() (*SetupKey, string) {
     	return GenerateSetupKey(DefaultSetupKeyName, SetupKeyReusable, DefaultSetupKeyDuration, []string{},
    -		SetupKeyUnlimitedUsage, false)
    +		SetupKeyUnlimitedUsage, false, false)
     }
     
     func Hash(s string) uint32 {
    diff --git a/management/server/types/user.go b/management/server/types/user.go
    index 348fbfb22..5f7a4f2cb 100644
    --- a/management/server/types/user.go
    +++ b/management/server/types/user.go
    @@ -80,7 +80,7 @@ type User struct {
     	// AutoGroups is a list of Group IDs to auto-assign to peers registered by this user
     	AutoGroups []string                        `gorm:"serializer:json"`
     	PATs       map[string]*PersonalAccessToken `gorm:"-"`
    -	PATsG      []PersonalAccessToken           `json:"-" gorm:"foreignKey:UserID;references:id"`
    +	PATsG      []PersonalAccessToken           `json:"-" gorm:"foreignKey:UserID;references:id;constraint:OnDelete:CASCADE;"`
     	// Blocked indicates whether the user is blocked. Blocked users can't use the system.
     	Blocked bool
     	// LastLogin is the last time the user logged in to IdP
    diff --git a/management/server/user.go b/management/server/user.go
    index 7a3e04a1d..381879ae6 100644
    --- a/management/server/user.go
    +++ b/management/server/user.go
    @@ -8,16 +8,16 @@ import (
     	"time"
     
     	"github.com/google/uuid"
    +	log "github.com/sirupsen/logrus"
    +
     	"github.com/netbirdio/netbird/management/server/activity"
     	nbContext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/idp"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	nbpeer "github.com/netbirdio/netbird/management/server/peer"
     	"github.com/netbirdio/netbird/management/server/status"
     	"github.com/netbirdio/netbird/management/server/store"
     	"github.com/netbirdio/netbird/management/server/types"
     	"github.com/netbirdio/netbird/management/server/util"
    -	log "github.com/sirupsen/logrus"
     )
     
     // createServiceUser creates a new service user under the given account.
    @@ -143,7 +143,7 @@ func (am *DefaultAccountManager) inviteNewUser(ctx context.Context, accountID, u
     // createNewIdpUser validates the invite and creates a new user in the IdP
     func (am *DefaultAccountManager) createNewIdpUser(ctx context.Context, accountID string, inviterID string, invite *types.UserInfo) (*idp.UserData, error) {
     	// inviterUser is the one who is inviting the new user
    -	inviterUser, err := am.lookupUserInCache(ctx, am.Store, inviterID, accountID)
    +	inviterUser, err := am.lookupUserInCache(ctx, inviterID, accountID)
     	if err != nil {
     		return nil, status.Errorf(status.NotFound, "inviter user with ID %s doesn't exist in IdP", inviterID)
     	}
    @@ -174,31 +174,26 @@ func (am *DefaultAccountManager) GetUserByID(ctx context.Context, id string) (*t
     	return am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, id)
     }
     
    -// GetUser looks up a user by provided authorization claims.
    -// It will also create an account if didn't exist for this user before.
    -func (am *DefaultAccountManager) GetUser(ctx context.Context, claims jwtclaims.AuthorizationClaims) (*types.User, error) {
    -	accountID, userID, err := am.GetAccountIDFromToken(ctx, claims)
    -	if err != nil {
    -		return nil, fmt.Errorf("failed to get account with token claims %v", err)
    -	}
    -
    -	user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID)
    +// GetUser looks up a user by provided nbContext.UserAuths.
    +// Expects account to have been created already.
    +func (am *DefaultAccountManager) GetUserFromUserAuth(ctx context.Context, userAuth nbContext.UserAuth) (*types.User, error) {
    +	user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userAuth.UserId)
     	if err != nil {
     		return nil, err
     	}
     
     	// this code should be outside of the am.GetAccountIDFromToken(claims) because this method is called also by the gRPC
     	// server when user authenticates a device. And we need to separate the Dashboard login event from the Device login event.
    -	newLogin := user.LastDashboardLoginChanged(claims.LastLogin)
    +	newLogin := user.LastDashboardLoginChanged(userAuth.LastLogin)
     
    -	err = am.Store.SaveUserLastLogin(ctx, accountID, userID, claims.LastLogin)
    +	err = am.Store.SaveUserLastLogin(ctx, userAuth.AccountId, userAuth.UserId, userAuth.LastLogin)
     	if err != nil {
     		log.WithContext(ctx).Errorf("failed saving user last login: %v", err)
     	}
     
     	if newLogin {
    -		meta := map[string]any{"timestamp": claims.LastLogin}
    -		am.StoreEvent(ctx, claims.UserId, claims.UserId, accountID, activity.DashboardLogin, meta)
    +		meta := map[string]any{"timestamp": userAuth.LastLogin}
    +		am.StoreEvent(ctx, userAuth.UserId, userAuth.UserId, userAuth.AccountId, activity.DashboardLogin, meta)
     	}
     
     	return user, nil
    @@ -207,8 +202,6 @@ func (am *DefaultAccountManager) GetUser(ctx context.Context, claims jwtclaims.A
     // ListUsers returns lists of all users under the account.
     // It doesn't populate user information such as email or name.
     func (am *DefaultAccountManager) ListUsers(ctx context.Context, accountID string) ([]*types.User, error) {
    -	unlock := am.Store.AcquireWriteLockByUID(ctx, accountID)
    -	defer unlock()
     	return am.Store.GetAccountUsers(ctx, store.LockingStrengthShare, accountID)
     }
     
    @@ -266,7 +259,12 @@ func (am *DefaultAccountManager) DeleteUser(ctx context.Context, accountID, init
     		return am.deleteServiceUser(ctx, accountID, initiatorUserID, targetUser)
     	}
     
    -	updateAccountPeers, err := am.deleteRegularUser(ctx, accountID, initiatorUserID, targetUserID)
    +	userInfo, err := am.getUserInfo(ctx, targetUser, accountID)
    +	if err != nil {
    +		return err
    +	}
    +
    +	updateAccountPeers, err := am.deleteRegularUser(ctx, accountID, initiatorUserID, userInfo)
     	if err != nil {
     		return err
     	}
    @@ -297,7 +295,7 @@ func (am *DefaultAccountManager) InviteUser(ctx context.Context, accountID strin
     	}
     
     	// check if the user is already registered with this ID
    -	user, err := am.lookupUserInCache(ctx, am.Store, targetUserID, accountID)
    +	user, err := am.lookupUserInCache(ctx, targetUserID, accountID)
     	if err != nil {
     		return err
     	}
    @@ -495,7 +493,6 @@ func (am *DefaultAccountManager) SaveOrAddUsers(ctx context.Context, accountID,
     	var peersToExpire []*nbpeer.Peer
     	var addUserEvents []func()
     	var usersToSave = make([]*types.User, 0, len(updates))
    -	var updatedUsersInfo = make([]*types.UserInfo, 0, len(updates))
     
     	groups, err := am.Store.GetAccountGroups(ctx, store.LockingStrengthShare, accountID)
     	if err != nil {
    @@ -526,20 +523,28 @@ func (am *DefaultAccountManager) SaveOrAddUsers(ctx context.Context, accountID,
     			if userHadPeers {
     				updateAccountPeers = true
     			}
    -
    -			updatedUserInfo, err := am.getUserInfo(ctx, transaction, updatedUser, accountID)
    -			if err != nil {
    -				return fmt.Errorf("failed to get user info: %w", err)
    -			}
    -			updatedUsersInfo = append(updatedUsersInfo, updatedUserInfo)
     		}
    -
     		return transaction.SaveUsers(ctx, store.LockingStrengthUpdate, usersToSave)
     	})
     	if err != nil {
     		return nil, err
     	}
     
    +	var updatedUsersInfo = make([]*types.UserInfo, 0, len(updates))
    +
    +	userInfos, err := am.GetUsersFromAccount(ctx, accountID, initiatorUserID)
    +	if err != nil {
    +		return nil, err
    +	}
    +
    +	for _, updatedUser := range usersToSave {
    +		updatedUserInfo, ok := userInfos[updatedUser.Id]
    +		if !ok || updatedUserInfo == nil {
    +			return nil, fmt.Errorf("failed to get user: %s updated user info", updatedUser.Id)
    +		}
    +		updatedUsersInfo = append(updatedUsersInfo, updatedUserInfo)
    +	}
    +
     	for _, addUserEvent := range addUserEvents {
     		addUserEvent()
     	}
    @@ -682,14 +687,14 @@ func handleOwnerRoleTransfer(ctx context.Context, transaction store.Store, initi
     // getUserInfo retrieves the UserInfo for a given User and Account.
     // If the AccountManager has a non-nil idpManager and the User is not a service user,
     // it will attempt to look up the UserData from the cache.
    -func (am *DefaultAccountManager) getUserInfo(ctx context.Context, transaction store.Store, user *types.User, accountID string) (*types.UserInfo, error) {
    -	settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
    +func (am *DefaultAccountManager) getUserInfo(ctx context.Context, user *types.User, accountID string) (*types.UserInfo, error) {
    +	settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
     	if err != nil {
     		return nil, err
     	}
     
     	if !isNil(am.idpManager) && !user.IsServiceUser {
    -		userData, err := am.lookupUserInCache(ctx, transaction, user.Id, accountID)
    +		userData, err := am.lookupUserInCache(ctx, user.Id, accountID)
     		if err != nil {
     			return nil, err
     		}
    @@ -774,22 +779,34 @@ func (am *DefaultAccountManager) GetOrCreateAccountByUser(ctx context.Context, u
     
     // GetUsersFromAccount performs a batched request for users from IDP by account ID apply filter on what data to return
     // based on provided user role.
    -func (am *DefaultAccountManager) GetUsersFromAccount(ctx context.Context, accountID, userID string) ([]*types.UserInfo, error) {
    +func (am *DefaultAccountManager) GetUsersFromAccount(ctx context.Context, accountID, initiatorUserID string) (map[string]*types.UserInfo, error) {
     	accountUsers, err := am.Store.GetAccountUsers(ctx, store.LockingStrengthShare, accountID)
     	if err != nil {
     		return nil, err
     	}
     
    -	user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID)
    +	initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID)
     	if err != nil {
     		return nil, err
     	}
     
    -	if user.AccountID != accountID {
    +	if initiatorUser.AccountID != accountID {
     		return nil, status.NewUserNotPartOfAccountError()
     	}
     
    -	queriedUsers := make([]*idp.UserData, 0)
    +	return am.BuildUserInfosForAccount(ctx, accountID, initiatorUserID, accountUsers)
    +}
    +
    +// BuildUserInfosForAccount builds user info for the given account.
    +func (am *DefaultAccountManager) BuildUserInfosForAccount(ctx context.Context, accountID, initiatorUserID string, accountUsers []*types.User) (map[string]*types.UserInfo, error) {
    +	var queriedUsers []*idp.UserData
    +	var err error
    +
    +	initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID)
    +	if err != nil {
    +		return nil, err
    +	}
    +
     	if !isNil(am.idpManager) {
     		users := make(map[string]userLoggedInOnce, len(accountUsers))
     		usersFromIntegration := make([]*idp.UserData, 0)
    @@ -818,31 +835,33 @@ func (am *DefaultAccountManager) GetUsersFromAccount(ctx context.Context, accoun
     		queriedUsers = append(queriedUsers, usersFromIntegration...)
     	}
     
    -	userInfos := make([]*types.UserInfo, 0)
    -
     	settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
     	if err != nil {
     		return nil, err
     	}
     
    +	userInfosMap := make(map[string]*types.UserInfo)
    +
     	// in case of self-hosted, or IDP doesn't return anything, we will return the locally stored userInfo
     	if len(queriedUsers) == 0 {
     		for _, accountUser := range accountUsers {
    -			if user.IsRegularUser() && user.Id != accountUser.Id {
    +			if initiatorUser.IsRegularUser() && initiatorUser.Id != accountUser.Id {
     				// if user is not an admin then show only current user and do not show other users
     				continue
     			}
    +
     			info, err := accountUser.ToUserInfo(nil, settings)
     			if err != nil {
     				return nil, err
     			}
    -			userInfos = append(userInfos, info)
    +			userInfosMap[accountUser.Id] = info
     		}
    -		return userInfos, nil
    +
    +		return userInfosMap, nil
     	}
     
     	for _, localUser := range accountUsers {
    -		if user.IsRegularUser() && user.Id != localUser.Id {
    +		if initiatorUser.IsRegularUser() && initiatorUser.Id != localUser.Id {
     			// if user is not an admin then show only current user and do not show other users
     			continue
     		}
    @@ -879,10 +898,10 @@ func (am *DefaultAccountManager) GetUsersFromAccount(ctx context.Context, accoun
     				Permissions:   types.UserPermissions{DashboardView: dashboardViewPermissions},
     			}
     		}
    -		userInfos = append(userInfos, info)
    +		userInfosMap[info.ID] = info
     	}
     
    -	return userInfos, nil
    +	return userInfosMap, nil
     }
     
     // expireAndUpdatePeers expires all peers of the given user and updates them in the account
    @@ -936,27 +955,13 @@ func (am *DefaultAccountManager) deleteUserFromIDP(ctx context.Context, targetUs
     	return nil
     }
     
    -func (am *DefaultAccountManager) getEmailAndNameOfTargetUser(ctx context.Context, accountId, initiatorId, targetId string) (string, string, error) {
    -	userInfos, err := am.GetUsersFromAccount(ctx, accountId, initiatorId)
    -	if err != nil {
    -		return "", "", err
    -	}
    -	for _, ui := range userInfos {
    -		if ui.ID == targetId {
    -			return ui.Email, ui.Name, nil
    -		}
    -	}
    -
    -	return "", "", fmt.Errorf("user info not found for user: %s", targetId)
    -}
    -
     // DeleteRegularUsers deletes regular users from an account.
     // Note: This function does not acquire the global lock.
     // It is the caller's responsibility to ensure proper locking is in place before invoking this method.
     //
     // If an error occurs while deleting the user, the function skips it and continues deleting other users.
     // Errors are collected and returned at the end.
    -func (am *DefaultAccountManager) DeleteRegularUsers(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string) error {
    +func (am *DefaultAccountManager) DeleteRegularUsers(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string, userInfos map[string]*types.UserInfo) error {
     	initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID)
     	if err != nil {
     		return err
    @@ -992,7 +997,13 @@ func (am *DefaultAccountManager) DeleteRegularUsers(ctx context.Context, account
     			continue
     		}
     
    -		userHadPeers, err := am.deleteRegularUser(ctx, accountID, initiatorUserID, targetUserID)
    +		userInfo, ok := userInfos[targetUserID]
    +		if !ok || userInfo == nil {
    +			allErrors = errors.Join(allErrors, fmt.Errorf("user info not found for user: %s", targetUserID))
    +			continue
    +		}
    +
    +		userHadPeers, err := am.deleteRegularUser(ctx, accountID, initiatorUserID, userInfo)
     		if err != nil {
     			allErrors = errors.Join(allErrors, err)
     			continue
    @@ -1011,53 +1022,48 @@ func (am *DefaultAccountManager) DeleteRegularUsers(ctx context.Context, account
     }
     
     // deleteRegularUser deletes a specified user and their related peers from the account.
    -func (am *DefaultAccountManager) deleteRegularUser(ctx context.Context, accountID, initiatorUserID, targetUserID string) (bool, error) {
    -	tuEmail, tuName, err := am.getEmailAndNameOfTargetUser(ctx, accountID, initiatorUserID, targetUserID)
    -	if err != nil {
    -		log.WithContext(ctx).Errorf("failed to resolve email address: %s", err)
    -		return false, err
    -	}
    -
    +func (am *DefaultAccountManager) deleteRegularUser(ctx context.Context, accountID, initiatorUserID string, targetUserInfo *types.UserInfo) (bool, error) {
     	if !isNil(am.idpManager) {
     		// Delete if the user already exists in the IdP. Necessary in cases where a user account
     		// was created where a user account was provisioned but the user did not sign in
    -		_, err = am.idpManager.GetUserDataByID(ctx, targetUserID, idp.AppMetadata{WTAccountID: accountID})
    +		_, err := am.idpManager.GetUserDataByID(ctx, targetUserInfo.ID, idp.AppMetadata{WTAccountID: accountID})
     		if err == nil {
    -			err = am.deleteUserFromIDP(ctx, targetUserID, accountID)
    +			err = am.deleteUserFromIDP(ctx, targetUserInfo.ID, accountID)
     			if err != nil {
    -				log.WithContext(ctx).Debugf("failed to delete user from IDP: %s", targetUserID)
    +				log.WithContext(ctx).Debugf("failed to delete user from IDP: %s", targetUserInfo.ID)
     				return false, err
     			}
     		} else {
    -			log.WithContext(ctx).Debugf("skipped deleting user %s from IDP, error: %v", targetUserID, err)
    +			log.WithContext(ctx).Debugf("skipped deleting user %s from IDP, error: %v", targetUserInfo.ID, err)
     		}
     	}
     
     	var addPeerRemovedEvents []func()
     	var updateAccountPeers bool
     	var targetUser *types.User
    +	var err error
     
     	err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
    -		targetUser, err = transaction.GetUserByUserID(ctx, store.LockingStrengthShare, targetUserID)
    +		targetUser, err = transaction.GetUserByUserID(ctx, store.LockingStrengthShare, targetUserInfo.ID)
     		if err != nil {
     			return fmt.Errorf("failed to get user to delete: %w", err)
     		}
     
    -		userPeers, err := transaction.GetUserPeers(ctx, store.LockingStrengthShare, accountID, targetUserID)
    +		userPeers, err := transaction.GetUserPeers(ctx, store.LockingStrengthShare, accountID, targetUserInfo.ID)
     		if err != nil {
     			return fmt.Errorf("failed to get user peers: %w", err)
     		}
     
     		if len(userPeers) > 0 {
     			updateAccountPeers = true
    -			addPeerRemovedEvents, err = deletePeers(ctx, am, transaction, accountID, targetUserID, userPeers)
    +			addPeerRemovedEvents, err = deletePeers(ctx, am, transaction, accountID, targetUserInfo.ID, userPeers)
     			if err != nil {
     				return fmt.Errorf("failed to delete user peers: %w", err)
     			}
     		}
     
    -		if err = transaction.DeleteUser(ctx, store.LockingStrengthUpdate, accountID, targetUserID); err != nil {
    -			return fmt.Errorf("failed to delete user: %s %w", targetUserID, err)
    +		if err = transaction.DeleteUser(ctx, store.LockingStrengthUpdate, accountID, targetUserInfo.ID); err != nil {
    +			return fmt.Errorf("failed to delete user: %s %w", targetUserInfo.ID, err)
     		}
     
     		return nil
    @@ -1069,7 +1075,7 @@ func (am *DefaultAccountManager) deleteRegularUser(ctx context.Context, accountI
     	for _, addPeerRemovedEvent := range addPeerRemovedEvents {
     		addPeerRemovedEvent()
     	}
    -	meta := map[string]any{"name": tuName, "email": tuEmail, "created_at": targetUser.CreatedAt}
    +	meta := map[string]any{"name": targetUserInfo.Name, "email": targetUserInfo.Email, "created_at": targetUser.CreatedAt}
     	am.StoreEvent(ctx, initiatorUserID, targetUser.Id, accountID, activity.UserDeleted, meta)
     
     	return updateAccountPeers, nil
    diff --git a/management/server/user_test.go b/management/server/user_test.go
    index 5c4b1e2cb..a180a761a 100644
    --- a/management/server/user_test.go
    +++ b/management/server/user_test.go
    @@ -10,7 +10,10 @@ import (
     	"github.com/eko/gocache/v3/cache"
     	cacheStore "github.com/eko/gocache/v3/store"
     	"github.com/google/go-cmp/cmp"
    +
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/util"
    +	"golang.org/x/exp/maps"
     
     	nbpeer "github.com/netbirdio/netbird/management/server/peer"
     	"github.com/netbirdio/netbird/management/server/store"
    @@ -24,7 +27,6 @@ import (
     	"github.com/netbirdio/netbird/management/server/activity"
     	"github.com/netbirdio/netbird/management/server/idp"
     	"github.com/netbirdio/netbird/management/server/integration_reference"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     )
     
     const (
    @@ -867,7 +869,10 @@ func TestUser_DeleteUser_RegularUsers(t *testing.T) {
     
     	for _, tc := range testCases {
     		t.Run(tc.name, func(t *testing.T) {
    -			err = am.DeleteRegularUsers(context.Background(), mockAccountID, mockUserID, tc.userIDs)
    +			userInfos, err := am.BuildUserInfosForAccount(context.Background(), mockAccountID, mockUserID, maps.Values(account.Users))
    +			assert.NoError(t, err)
    +
    +			err = am.DeleteRegularUsers(context.Background(), mockAccountID, mockUserID, tc.userIDs, userInfos)
     			if len(tc.expectedReasons) > 0 {
     				assert.Error(t, err)
     				var foundExpectedErrors int
    @@ -921,11 +926,12 @@ func TestDefaultAccountManager_GetUser(t *testing.T) {
     		eventStore: &activity.InMemoryEventStore{},
     	}
     
    -	claims := jwtclaims.AuthorizationClaims{
    -		UserId: mockUserID,
    +	claims := nbcontext.UserAuth{
    +		UserId:    mockUserID,
    +		AccountId: mockAccountID,
     	}
     
    -	user, err := am.GetUser(context.Background(), claims)
    +	user, err := am.GetUserFromUserAuth(context.Background(), claims)
     	if err != nil {
     		t.Fatalf("Error when checking user role: %s", err)
     	}
    diff --git a/relay/client/client.go b/relay/client/client.go
    index db5252f50..9e7e54393 100644
    --- a/relay/client/client.go
    +++ b/relay/client/client.go
    @@ -10,6 +10,8 @@ import (
     	log "github.com/sirupsen/logrus"
     
     	auth "github.com/netbirdio/netbird/relay/auth/hmac"
    +	"github.com/netbirdio/netbird/relay/client/dialer"
    +	"github.com/netbirdio/netbird/relay/client/dialer/quic"
     	"github.com/netbirdio/netbird/relay/client/dialer/ws"
     	"github.com/netbirdio/netbird/relay/healthcheck"
     	"github.com/netbirdio/netbird/relay/messages"
    @@ -95,8 +97,6 @@ func (cc *connContainer) writeMsg(msg Msg) {
     		msg.Free()
     	default:
     		msg.Free()
    -		cc.log.Infof("message queue is full")
    -		// todo consider to close the connection
     	}
     }
     
    @@ -141,7 +141,6 @@ type Client struct {
     	muInstanceURL    sync.Mutex
     
     	onDisconnectListener func(string)
    -	onConnectedListener  func()
     	listenerMutex        sync.Mutex
     }
     
    @@ -179,8 +178,7 @@ func (c *Client) Connect() error {
     		return nil
     	}
     
    -	err := c.connect()
    -	if err != nil {
    +	if err := c.connect(); err != nil {
     		return err
     	}
     
    @@ -191,7 +189,6 @@ func (c *Client) Connect() error {
     
     	c.wgReadLoop.Add(1)
     	go c.readLoop(c.relayConn)
    -	go c.notifyConnected()
     
     	return nil
     }
    @@ -239,12 +236,6 @@ func (c *Client) SetOnDisconnectListener(fn func(string)) {
     	c.onDisconnectListener = fn
     }
     
    -func (c *Client) SetOnConnectedListener(fn func()) {
    -	c.listenerMutex.Lock()
    -	defer c.listenerMutex.Unlock()
    -	c.onConnectedListener = fn
    -}
    -
     // HasConns returns true if there are connections.
     func (c *Client) HasConns() bool {
     	c.mu.Lock()
    @@ -264,14 +255,14 @@ func (c *Client) Close() error {
     }
     
     func (c *Client) connect() error {
    -	conn, err := ws.Dial(c.connectionURL)
    +	rd := dialer.NewRaceDial(c.log, c.connectionURL, quic.Dialer{}, ws.Dialer{})
    +	conn, err := rd.Dial()
     	if err != nil {
     		return err
     	}
     	c.relayConn = conn
     
    -	err = c.handShake()
    -	if err != nil {
    +	if err = c.handShake(); err != nil {
     		cErr := conn.Close()
     		if cErr != nil {
     			c.log.Errorf("failed to close connection: %s", cErr)
    @@ -306,7 +297,7 @@ func (c *Client) handShake() error {
     		return fmt.Errorf("validate version: %w", err)
     	}
     
    -	msgType, err := messages.DetermineServerMessageType(buf[messages.SizeOfVersionByte:n])
    +	msgType, err := messages.DetermineServerMessageType(buf[:n])
     	if err != nil {
     		c.log.Errorf("failed to determine message type: %s", err)
     		return err
    @@ -317,7 +308,7 @@ func (c *Client) handShake() error {
     		return fmt.Errorf("unexpected message type")
     	}
     
    -	addr, err := messages.UnmarshalAuthResponse(buf[messages.SizeOfProtoHeader:n])
    +	addr, err := messages.UnmarshalAuthResponse(buf[:n])
     	if err != nil {
     		return err
     	}
    @@ -345,27 +336,30 @@ func (c *Client) readLoop(relayConn net.Conn) {
     			c.log.Infof("start to Relay read loop exit")
     			c.mu.Lock()
     			if c.serviceIsRunning && !internallyStoppedFlag.isSet() {
    -				c.log.Debugf("failed to read message from relay server: %s", errExit)
    +				c.log.Errorf("failed to read message from relay server: %s", errExit)
     			}
     			c.mu.Unlock()
    +			c.bufPool.Put(bufPtr)
     			break
     		}
     
    -		_, err := messages.ValidateVersion(buf[:n])
    +		buf = buf[:n]
    +
    +		_, err := messages.ValidateVersion(buf)
     		if err != nil {
     			c.log.Errorf("failed to validate protocol version: %s", err)
     			c.bufPool.Put(bufPtr)
     			continue
     		}
     
    -		msgType, err := messages.DetermineServerMessageType(buf[messages.SizeOfVersionByte:n])
    +		msgType, err := messages.DetermineServerMessageType(buf)
     		if err != nil {
     			c.log.Errorf("failed to determine message type: %s", err)
     			c.bufPool.Put(bufPtr)
     			continue
     		}
     
    -		if !c.handleMsg(msgType, buf[messages.SizeOfProtoHeader:n], bufPtr, hc, internallyStoppedFlag) {
    +		if !c.handleMsg(msgType, buf, bufPtr, hc, internallyStoppedFlag) {
     			break
     		}
     	}
    @@ -557,16 +551,6 @@ func (c *Client) notifyDisconnected() {
     	go c.onDisconnectListener(c.connectionURL)
     }
     
    -func (c *Client) notifyConnected() {
    -	c.listenerMutex.Lock()
    -	defer c.listenerMutex.Unlock()
    -
    -	if c.onConnectedListener == nil {
    -		return
    -	}
    -	go c.onConnectedListener()
    -}
    -
     func (c *Client) writeCloseMsg() {
     	msg := messages.MarshalCloseMsg()
     	_, err := c.relayConn.Write(msg)
    diff --git a/relay/client/dialer/net/err.go b/relay/client/dialer/net/err.go
    new file mode 100644
    index 000000000..fee844963
    --- /dev/null
    +++ b/relay/client/dialer/net/err.go
    @@ -0,0 +1,7 @@
    +package net
    +
    +import "errors"
    +
    +var (
    +	ErrClosedByServer = errors.New("closed by server")
    +)
    diff --git a/relay/client/dialer/quic/conn.go b/relay/client/dialer/quic/conn.go
    new file mode 100644
    index 000000000..d64633c8c
    --- /dev/null
    +++ b/relay/client/dialer/quic/conn.go
    @@ -0,0 +1,97 @@
    +package quic
    +
    +import (
    +	"context"
    +	"errors"
    +	"fmt"
    +	"net"
    +	"time"
    +
    +	"github.com/quic-go/quic-go"
    +	log "github.com/sirupsen/logrus"
    +
    +	netErr "github.com/netbirdio/netbird/relay/client/dialer/net"
    +)
    +
    +const (
    +	Network = "quic"
    +)
    +
    +type Addr struct {
    +	addr string
    +}
    +
    +func (a Addr) Network() string {
    +	return Network
    +}
    +
    +func (a Addr) String() string {
    +	return a.addr
    +}
    +
    +type Conn struct {
    +	session quic.Connection
    +	ctx     context.Context
    +}
    +
    +func NewConn(session quic.Connection) net.Conn {
    +	return &Conn{
    +		session: session,
    +		ctx:     context.Background(),
    +	}
    +}
    +
    +func (c *Conn) Read(b []byte) (n int, err error) {
    +	dgram, err := c.session.ReceiveDatagram(c.ctx)
    +	if err != nil {
    +		return 0, c.remoteCloseErrHandling(err)
    +	}
    +
    +	n = copy(b, dgram)
    +	return n, nil
    +}
    +
    +func (c *Conn) Write(b []byte) (int, error) {
    +	err := c.session.SendDatagram(b)
    +	if err != nil {
    +		err = c.remoteCloseErrHandling(err)
    +		log.Errorf("failed to write to QUIC stream: %v", err)
    +		return 0, err
    +	}
    +	return len(b), nil
    +}
    +
    +func (c *Conn) RemoteAddr() net.Addr {
    +	return c.session.RemoteAddr()
    +}
    +
    +func (c *Conn) LocalAddr() net.Addr {
    +	if c.session != nil {
    +		return c.session.LocalAddr()
    +	}
    +	return Addr{addr: "unknown"}
    +}
    +
    +func (c *Conn) SetReadDeadline(t time.Time) error {
    +	return fmt.Errorf("SetReadDeadline is not implemented")
    +}
    +
    +func (c *Conn) SetWriteDeadline(t time.Time) error {
    +	return fmt.Errorf("SetWriteDeadline is not implemented")
    +}
    +
    +func (c *Conn) SetDeadline(t time.Time) error {
    +	return nil
    +}
    +
    +func (c *Conn) Close() error {
    +	return c.session.CloseWithError(0, "normal closure")
    +}
    +
    +func (c *Conn) remoteCloseErrHandling(err error) error {
    +	var appErr *quic.ApplicationError
    +	if errors.As(err, &appErr) && appErr.ErrorCode == 0x0 {
    +		return netErr.ErrClosedByServer
    +	}
    +	return err
    +}
    diff --git a/relay/client/dialer/quic/quic.go b/relay/client/dialer/quic/quic.go
    new file mode 100644
    index 000000000..7fd486f87
    --- /dev/null
    +++ b/relay/client/dialer/quic/quic.go
    @@ -0,0 +1,72 @@
    +package quic
    +
    +import (
    +	"context"
    +	"errors"
    +	"fmt"
    +	"net"
    +	"strings"
    +	"time"
    +
    +	"github.com/quic-go/quic-go"
    +	log "github.com/sirupsen/logrus"
    +
    +	quictls "github.com/netbirdio/netbird/relay/tls"
    +	nbnet "github.com/netbirdio/netbird/util/net"
    +)
    +
    +type Dialer struct {
    +}
    +
    +func (d Dialer) Protocol() string {
    +	return Network
    +}
    +
    +func (d Dialer) Dial(ctx context.Context, address string) (net.Conn, error) {
    +	quicURL, err := prepareURL(address)
    +	if err != nil {
    +		return nil, err
    +	}
    +
    +	quicConfig := &quic.Config{
    +		KeepAlivePeriod:   30 * time.Second,
    +		MaxIdleTimeout:    4 * time.Minute,
    +		EnableDatagrams:   true,
    +		InitialPacketSize: 1452,
    +	}
    +
    +	udpConn, err := nbnet.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
    +	if err != nil {
    +		log.Errorf("failed to listen on UDP: %s", err)
    +		return nil, err
    +	}
    +
    +	udpAddr, err := net.ResolveUDPAddr("udp", quicURL)
    +	if err != nil {
    +		log.Errorf("failed to resolve UDP address: %s", err)
    +		return nil, err
    +	}
    +
    +	session, err := quic.Dial(ctx, udpConn, udpAddr, quictls.ClientQUICTLSConfig(), quicConfig)
    +	if err != nil {
    +		if errors.Is(err, context.Canceled) {
    +			return nil, err
    +		}
    +		log.Errorf("failed to dial to Relay server via QUIC '%s': %s", quicURL, err)
    +		return nil, err
    +	}
    +
    +	conn := NewConn(session)
    +	return conn, nil
    +}
    +
    +func prepareURL(address string) (string, error) {
    +	if !strings.HasPrefix(address, "rel://") && !strings.HasPrefix(address, "rels://") {
    +		return "", fmt.Errorf("unsupported scheme: %s", address)
    +	}
    +
    +	if strings.HasPrefix(address, "rels://") {
    +		return address[7:], nil
    +	}
    +	return address[6:], nil
    +}
    diff --git a/relay/client/dialer/race_dialer.go b/relay/client/dialer/race_dialer.go
    new file mode 100644
    index 000000000..11dba5799
    --- /dev/null
    +++ b/relay/client/dialer/race_dialer.go
    @@ -0,0 +1,96 @@
    +package dialer
    +
    +import (
    +	"context"
    +	"errors"
    +	"net"
    +	"time"
    +
    +	log "github.com/sirupsen/logrus"
    +)
    +
    +var (
    +	connectionTimeout = 30 * time.Second
    +)
    +
    +type DialeFn interface {
    +	Dial(ctx context.Context, address string) (net.Conn, error)
    +	Protocol() string
    +}
    +
    +type dialResult struct {
    +	Conn     net.Conn
    +	Protocol string
    +	Err      error
    +}
    +
    +type RaceDial struct {
    +	log       *log.Entry
    +	serverURL string
    +	dialerFns []DialeFn
    +}
    +
    +func NewRaceDial(log *log.Entry, serverURL string, dialerFns ...DialeFn) *RaceDial {
    +	return &RaceDial{
    +		log:       log,
    +		serverURL: serverURL,
    +		dialerFns: dialerFns,
    +	}
    +}
    +
    +func (r *RaceDial) Dial() (net.Conn, error) {
    +	connChan := make(chan dialResult, len(r.dialerFns))
    +	winnerConn := make(chan net.Conn, 1)
    +	abortCtx, abort := context.WithCancel(context.Background())
    +	defer abort()
    +
    +	for _, dfn := range r.dialerFns {
    +		go r.dial(dfn, abortCtx, connChan)
    +	}
    +
    +	go r.processResults(connChan, winnerConn, abort)
    +
    +	conn, ok := <-winnerConn
    +	if !ok {
    +		return nil, errors.New("failed to dial to Relay server on any protocol")
    +	}
    +	return conn, nil
    +}
    +
    +func (r *RaceDial) dial(dfn DialeFn, abortCtx context.Context, connChan chan dialResult) {
    +	ctx, cancel := context.WithTimeout(abortCtx, connectionTimeout)
    +	defer cancel()
    +
    +	r.log.Infof("dialing Relay server via %s", dfn.Protocol())
    +	conn, err := dfn.Dial(ctx, r.serverURL)
    +	connChan <- dialResult{Conn: conn, Protocol: dfn.Protocol(), Err: err}
    +}
    +
    +func (r *RaceDial) processResults(connChan chan dialResult, winnerConn chan net.Conn, abort context.CancelFunc) {
    +	var hasWinner bool
    +	for i := 0; i < len(r.dialerFns); i++ {
    +		dr := <-connChan
    +		if dr.Err != nil {
    +			if errors.Is(dr.Err, context.Canceled) {
    +				r.log.Infof("connection attempt aborted via: %s", dr.Protocol)
    +			} else {
    +				r.log.Errorf("failed to dial via %s: %s", dr.Protocol, dr.Err)
    +			}
    +			continue
    +		}
    +
    +		if hasWinner {
    +			if cerr := dr.Conn.Close(); cerr != nil {
    +				r.log.Warnf("failed to close connection via %s: %s", dr.Protocol, cerr)
    +			}
    +			continue
    +		}
    +
    +		r.log.Infof("successfully dialed via: %s", dr.Protocol)
    +
    +		abort()
    +		hasWinner = true
    +		winnerConn <- dr.Conn
    +	}
    +	close(winnerConn)
    +}
    diff --git a/relay/client/dialer/race_dialer_test.go b/relay/client/dialer/race_dialer_test.go
    new file mode 100644
    index 000000000..989abb0a6
    --- /dev/null
    +++ b/relay/client/dialer/race_dialer_test.go
    @@ -0,0 +1,252 @@
    +package dialer
    +
    +import (
    +	"context"
    +	"errors"
    +	"net"
    +	"testing"
    +	"time"
    +
    +	"github.com/sirupsen/logrus"
    +)
    +
    +type MockAddr struct {
    +	network string
    +}
    +
    +func (m *MockAddr) Network() string {
    +	return m.network
    +}
    +
    +func (m *MockAddr) String() string {
    +	return "1.2.3.4"
    +}
    +
    +// MockDialer is a mock implementation of DialeFn
    +type MockDialer struct {
    +	dialFunc    func(ctx context.Context, address string) (net.Conn, error)
    +	protocolStr string
    +}
    +
    +func (m *MockDialer) Dial(ctx context.Context, address string) (net.Conn, error) {
    +	return m.dialFunc(ctx, address)
    +}
    +
    +func (m *MockDialer) Protocol() string {
    +	return m.protocolStr
    +}
    +
    +// MockConn implements net.Conn for testing
    +type MockConn struct {
    +	remoteAddr net.Addr
    +}
    +
    +func (m *MockConn) Read(b []byte) (n int, err error) {
    +	return 0, nil
    +}
    +
    +func (m *MockConn) Write(b []byte) (n int, err error) {
    +	return 0, nil
    +}
    +
    +func (m *MockConn) Close() error {
    +	return nil
    +}
    +
    +func (m *MockConn) LocalAddr() net.Addr {
    +	return nil
    +}
    +
    +func (m *MockConn) RemoteAddr() net.Addr {
    +	return m.remoteAddr
    +}
    +
    +func (m *MockConn) SetDeadline(t time.Time) error {
    +	return nil
    +}
    +
    +func (m *MockConn) SetReadDeadline(t time.Time) error {
    +	return nil
    +}
    +
    +func (m *MockConn) SetWriteDeadline(t time.Time) error {
    +	return nil
    +}
    +
    +func TestRaceDialEmptyDialers(t *testing.T) {
    +	logger := logrus.NewEntry(logrus.New())
    +	serverURL := "test.server.com"
    +
    +	rd := NewRaceDial(logger, serverURL)
    +	conn, err := rd.Dial()
    +	if err == nil {
    +		t.Errorf("Expected an error with empty dialers, got nil")
    +	}
    +	if conn != nil {
    +		t.Errorf("Expected nil connection with empty dialers, got %v", conn)
    +	}
    +}
    +
    +func TestRaceDialSingleSuccessfulDialer(t *testing.T) {
    +	logger := logrus.NewEntry(logrus.New())
    +	serverURL := "test.server.com"
    +	proto := "test-protocol"
    +
    +	mockConn := &MockConn{
    +		remoteAddr: &MockAddr{network: proto},
    +	}
    +
    +	mockDialer := &MockDialer{
    +		dialFunc: func(ctx context.Context, address string) (net.Conn, error) {
    +			return mockConn, nil
    +		},
    +		protocolStr: proto,
    +	}
    +
    +	rd := NewRaceDial(logger, serverURL, mockDialer)
    +	conn, err := rd.Dial()
    +	if err != nil {
    +		t.Errorf("Expected no error, got %v", err)
    +	}
    +	if conn == nil {
    +		t.Errorf("Expected non-nil connection")
    +	}
    +}
    +
    +func TestRaceDialMultipleDialersWithOneSuccess(t *testing.T) {
    +	logger := logrus.NewEntry(logrus.New())
    +	serverURL := "test.server.com"
    +	proto2 := "protocol2"
    +
    +	mockConn2 := &MockConn{
    +		remoteAddr: &MockAddr{network: proto2},
    +	}
    +
    +	mockDialer1 := &MockDialer{
    +		dialFunc: func(ctx context.Context, address string) (net.Conn, error) {
    +			return nil, errors.New("first dialer failed")
    +		},
    +		protocolStr: "proto1",
    +	}
    +
    +	mockDialer2 := &MockDialer{
    +		dialFunc: func(ctx context.Context, address string) (net.Conn, error) {
    +			return mockConn2, nil
    +		},
    +		protocolStr: "proto2",
    +	}
    +
    +	rd := NewRaceDial(logger, serverURL, mockDialer1, mockDialer2)
    +	conn, err := rd.Dial()
    +	if err != nil {
    +		t.Errorf("Expected no error, got %v", err)
    +	}
    +	if conn.RemoteAddr().Network() != proto2 {
    +		t.Errorf("Expected connection with protocol %s, got %s", proto2, conn.RemoteAddr().Network())
    +	}
    +}
    +
    +func TestRaceDialTimeout(t *testing.T) {
    +	logger := logrus.NewEntry(logrus.New())
    +	serverURL := "test.server.com"
    +
    +	connectionTimeout = 3 * time.Second
    +	mockDialer := &MockDialer{
    +		dialFunc: func(ctx context.Context, address string) (net.Conn, error) {
    +			<-ctx.Done()
    +			return nil, ctx.Err()
    +		},
    +		protocolStr: "proto1",
    +	}
    +
    +	rd := NewRaceDial(logger, serverURL, mockDialer)
    +	conn, err := rd.Dial()
    +	if err == nil {
    +		t.Errorf("Expected an error, got nil")
    +	}
    +	if conn != nil {
    +		t.Errorf("Expected nil connection, got %v", conn)
    +	}
    +}
    +
    +func TestRaceDialAllDialersFail(t *testing.T) {
    +	logger := logrus.NewEntry(logrus.New())
    +	serverURL := "test.server.com"
    +
    +	mockDialer1 := &MockDialer{
    +		dialFunc: func(ctx context.Context, address string) (net.Conn, error) {
    +			return nil, errors.New("first dialer failed")
    +		},
    +		protocolStr: "protocol1",
    +	}
    +
    +	mockDialer2 := &MockDialer{
    +		dialFunc: func(ctx context.Context, address string) (net.Conn, error) {
    +			return nil, errors.New("second dialer failed")
    +		},
    +		protocolStr: "protocol2",
    +	}
    +
    +	rd := NewRaceDial(logger, serverURL, mockDialer1, mockDialer2)
    +	conn, err := rd.Dial()
    +	if err == nil {
    +		t.Errorf("Expected an error, got nil")
    +	}
    +	if conn != nil {
    +		t.Errorf("Expected nil connection, got %v", conn)
    +	}
    +}
    +
    +func TestRaceDialFirstSuccessfulDialerWins(t *testing.T) {
    +	logger := logrus.NewEntry(logrus.New())
    +	serverURL := "test.server.com"
    +	proto1 := "protocol1"
    +	proto2 := "protocol2"
    +
    +	mockConn1 := &MockConn{
    +		remoteAddr: &MockAddr{network: proto1},
    +	}
    +
    +	mockConn2 := &MockConn{
    +		remoteAddr: &MockAddr{network: proto2},
    +	}
    +
    +	mockDialer1 := &MockDialer{
    +		dialFunc: func(ctx context.Context, address string) (net.Conn, error) {
    +			time.Sleep(1 * time.Second)
    +			return mockConn1, nil
    +		},
    +		protocolStr: proto1,
    +	}
    +
    +	mock2err := make(chan error)
    +	mockDialer2 := &MockDialer{
    +		dialFunc: func(ctx context.Context, address string) (net.Conn, error) {
    +			<-ctx.Done()
    +			mock2err <- ctx.Err()
    +			return mockConn2, ctx.Err()
    +		},
    +		protocolStr: proto2,
    +	}
    +
    +	rd := NewRaceDial(logger, serverURL, mockDialer1, mockDialer2)
    +	conn, err := rd.Dial()
    +	if err != nil {
    +		t.Errorf("Expected no error, got %v", err)
    +	}
    +	if conn == nil {
    +		t.Errorf("Expected non-nil connection")
    +	}
    +	if conn != mockConn1 {
    +		t.Errorf("Expected first connection, got %v", conn)
    +	}
    +
    +	select {
    +	case <-time.After(3 * time.Second):
    +		t.Errorf("Timed out waiting for second dialer to finish")
    +	case err := <-mock2err:
    +		if !errors.Is(err, context.Canceled) {
    +			t.Errorf("Expected context.Canceled error, got %v", err)
    +		}
    +	}
    +}
    diff --git a/relay/client/dialer/ws/addr.go b/relay/client/dialer/ws/addr.go
    index 43f5dd6af..11158cfbd 100644
    --- a/relay/client/dialer/ws/addr.go
    +++ b/relay/client/dialer/ws/addr.go
    @@ -1,11 +1,15 @@
     package ws
     
    +const (
    +	Network = "ws"
    +)
    +
     type WebsocketAddr struct {
     	addr string
     }
     
     func (a WebsocketAddr) Network() string {
    -	return "websocket"
    +	return Network
     }
     
     func (a WebsocketAddr) String() string {
    diff --git a/relay/client/dialer/ws/conn.go b/relay/client/dialer/ws/conn.go
    index e7f771b8d..0086b702b 100644
    --- a/relay/client/dialer/ws/conn.go
    +++ b/relay/client/dialer/ws/conn.go
    @@ -6,7 +6,7 @@ import (
     	"net"
     	"time"
     
    -	"nhooyr.io/websocket"
    +	"github.com/coder/websocket"
     )
     
     type Conn struct {
    @@ -26,6 +26,7 @@ func NewConn(wsConn *websocket.Conn, serverAddress string) net.Conn {
     func (c *Conn) Read(b []byte) (n int, err error) {
     	t, ioReader, err := c.Conn.Reader(c.ctx)
     	if err != nil {
    +		// todo use ErrClosedByServer
     		return 0, err
     	}
     
    diff --git a/relay/client/dialer/ws/ws.go b/relay/client/dialer/ws/ws.go
    index d9388aafd..cb525865b 100644
    --- a/relay/client/dialer/ws/ws.go
    +++ b/relay/client/dialer/ws/ws.go
    @@ -2,20 +2,31 @@ package ws
     
     import (
     	"context"
    +	"crypto/tls"
    +	"crypto/x509"
    +	"errors"
     	"fmt"
     	"net"
     	"net/http"
     	"net/url"
     	"strings"
     
    +	"github.com/coder/websocket"
     	log "github.com/sirupsen/logrus"
    -	"nhooyr.io/websocket"
     
     	"github.com/netbirdio/netbird/relay/server/listener/ws"
    +	"github.com/netbirdio/netbird/util/embeddedroots"
     	nbnet "github.com/netbirdio/netbird/util/net"
     )
     
    -func Dial(address string) (net.Conn, error) {
    +type Dialer struct {
    +}
    +
    +func (d Dialer) Protocol() string {
    +	return "WS"
    +}
    +
    +func (d Dialer) Dial(ctx context.Context, address string) (net.Conn, error) {
     	wsURL, err := prepareURL(address)
     	if err != nil {
     		return nil, err
    @@ -31,8 +42,11 @@ func Dial(address string) (net.Conn, error) {
     	}
     	parsedURL.Path = ws.URLPath
     
    -	wsConn, resp, err := websocket.Dial(context.Background(), parsedURL.String(), opts)
    +	wsConn, resp, err := websocket.Dial(ctx, parsedURL.String(), opts)
     	if err != nil {
    +		if errors.Is(err, context.Canceled) {
    +			return nil, err
    +		}
     		log.Errorf("failed to dial to Relay server '%s': %s", wsURL, err)
     		return nil, err
     	}
    @@ -55,10 +69,19 @@ func prepareURL(address string) (string, error) {
     func httpClientNbDialer() *http.Client {
     	customDialer := nbnet.NewDialer()
     
    +	certPool, err := x509.SystemCertPool()
    +	if err != nil || certPool == nil {
    +		log.Debugf("System cert pool not available; falling back to embedded cert, error: %v", err)
    +		certPool = embeddedroots.Get()
    +	}
    +
     	customTransport := &http.Transport{
     		DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
     			return customDialer.DialContext(ctx, network, addr)
     		},
    +		TLSClientConfig: &tls.Config{
    +			RootCAs: certPool,
    +		},
     	}
     
     	return &http.Client{
    diff --git a/relay/client/guard.go b/relay/client/guard.go
    index b971363a8..554330ea3 100644
    --- a/relay/client/guard.go
    +++ b/relay/client/guard.go
    @@ -14,8 +14,9 @@ var (
     
     // Guard manage the reconnection tries to the Relay server in case of disconnection event.
     type Guard struct {
    -	// OnNewRelayClient is a channel that is used to notify the relay client about a new relay client instance.
    +	// OnNewRelayClient is a channel that is used to notify the relay manager about a new relay client instance.
     	OnNewRelayClient chan *Client
    +	OnReconnected    chan struct{}
     	serverPicker     *ServerPicker
     }
     
    @@ -23,6 +24,7 @@ type Guard struct {
     func NewGuard(sp *ServerPicker) *Guard {
     	g := &Guard{
     		OnNewRelayClient: make(chan *Client, 1),
    +		OnReconnected:    make(chan struct{}, 1),
     		serverPicker:     sp,
     	}
     	return g
    @@ -39,14 +41,13 @@ func NewGuard(sp *ServerPicker) *Guard {
     // - relayClient: The relay client instance that was disconnected.
     // todo prevent multiple reconnection instances. In the current usage it should not happen, but it is better to prevent
     func (g *Guard) StartReconnectTrys(ctx context.Context, relayClient *Client) {
    -	if relayClient == nil {
    -		goto RETRY
    -	}
    -	if g.isServerURLStillValid(relayClient) && g.quickReconnect(ctx, relayClient) {
    +	// try to reconnect to the same server
    +	if ok := g.tryToQuickReconnect(ctx, relayClient); ok {
    +		g.notifyReconnected()
     		return
     	}
     
    -RETRY:
    +	// start a ticker to pick a new server
     	ticker := exponentTicker(ctx)
     	defer ticker.Stop()
     
    @@ -64,6 +65,28 @@ RETRY:
     	}
     }
     
    +func (g *Guard) tryToQuickReconnect(parentCtx context.Context, rc *Client) bool {
    +	if rc == nil {
    +		return false
    +	}
    +
    +	if !g.isServerURLStillValid(rc) {
    +		return false
    +	}
    +
    +	if cancelled := waiteBeforeRetry(parentCtx); !cancelled {
    +		return false
    +	}
    +
    +	log.Infof("try to reconnect to Relay server: %s", rc.connectionURL)
    +
    +	if err := rc.Connect(); err != nil {
    +		log.Errorf("failed to reconnect to relay server: %s", err)
    +		return false
    +	}
    +	return true
    +}
    +
     func (g *Guard) retry(ctx context.Context) error {
     	log.Infof("try to pick up a new Relay server")
     	relayClient, err := g.serverPicker.PickServer(ctx)
    @@ -78,23 +101,6 @@ func (g *Guard) retry(ctx context.Context) error {
     	return nil
     }
     
    -func (g *Guard) quickReconnect(parentCtx context.Context, rc *Client) bool {
    -	ctx, cancel := context.WithTimeout(parentCtx, 1500*time.Millisecond)
    -	defer cancel()
    -	<-ctx.Done()
    -
    -	if parentCtx.Err() != nil {
    -		return false
    -	}
    -	log.Infof("try to reconnect to Relay server: %s", rc.connectionURL)
    -
    -	if err := rc.Connect(); err != nil {
    -		log.Errorf("failed to reconnect to relay server: %s", err)
    -		return false
    -	}
    -	return true
    -}
    -
     func (g *Guard) drainRelayClientChan() {
     	select {
     	case <-g.OnNewRelayClient:
    @@ -111,6 +117,13 @@ func (g *Guard) isServerURLStillValid(rc *Client) bool {
     	return false
     }
     
    +func (g *Guard) notifyReconnected() {
    +	select {
    +	case g.OnReconnected <- struct{}{}:
    +	default:
    +	}
    +}
    +
     func exponentTicker(ctx context.Context) *backoff.Ticker {
     	bo := backoff.WithContext(&backoff.ExponentialBackOff{
     		InitialInterval: 2 * time.Second,
    @@ -121,3 +134,15 @@ func exponentTicker(ctx context.Context) *backoff.Ticker {
     
     	return backoff.NewTicker(bo)
     }
    +
    +func waiteBeforeRetry(ctx context.Context) bool {
    +	timer := time.NewTimer(1500 * time.Millisecond)
    +	defer timer.Stop()
    +
    +	select {
    +	case <-timer.C:
    +		return true
    +	case <-ctx.Done():
    +		return false
    +	}
    +}
    diff --git a/relay/client/manager.go b/relay/client/manager.go
    index d847bb879..26b113050 100644
    --- a/relay/client/manager.go
    +++ b/relay/client/manager.go
    @@ -165,6 +165,9 @@ func (m *Manager) Ready() bool {
     }
     
     func (m *Manager) SetOnReconnectedListener(f func()) {
    +	m.listenerLock.Lock()
    +	defer m.listenerLock.Unlock()
    +
     	m.onReconnectedListenerFn = f
     }
     
    @@ -284,6 +287,9 @@ func (m *Manager) openConnVia(serverAddress, peerKey string) (net.Conn, error) {
     }
     
     func (m *Manager) onServerConnected() {
    +	m.listenerLock.Lock()
    +	defer m.listenerLock.Unlock()
    +
     	if m.onReconnectedListenerFn == nil {
     		return
     	}
    @@ -304,8 +310,11 @@ func (m *Manager) onServerDisconnected(serverAddress string) {
     func (m *Manager) listenGuardEvent(ctx context.Context) {
     	for {
     		select {
    +		case <-m.reconnectGuard.OnReconnected:
    +			m.onServerConnected()
     		case rc := <-m.reconnectGuard.OnNewRelayClient:
     			m.storeClient(rc)
    +			m.onServerConnected()
     		case <-ctx.Done():
     			return
     		}
    @@ -317,7 +326,6 @@ func (m *Manager) storeClient(client *Client) {
     	defer m.relayClientMu.Unlock()
     
     	m.relayClient = client
    -	m.relayClient.SetOnConnectedListener(m.onServerConnected)
     	m.relayClient.SetOnDisconnectListener(m.onServerDisconnected)
     }
     
    diff --git a/relay/messages/message.go b/relay/messages/message.go
    index 39ca0aa90..7794c57bc 100644
    --- a/relay/messages/message.go
    +++ b/relay/messages/message.go
    @@ -23,20 +23,26 @@ const (
     	MsgTypeAuth                  = 6
     	MsgTypeAuthResponse          = 7
     
    -	SizeOfVersionByte = 1
    -	SizeOfMsgType     = 1
    +	// base size of the message
    +	sizeOfVersionByte = 1
    +	sizeOfMsgType     = 1
    +	sizeOfProtoHeader = sizeOfVersionByte + sizeOfMsgType
     
    -	SizeOfProtoHeader = SizeOfVersionByte + SizeOfMsgType
    -
    -	sizeOfMagicByte = 4
    -
    -	headerSizeTransport = IDSize
    +	// auth message
    +	sizeOfMagicByte     = 4
    +	headerSizeAuth      = sizeOfMagicByte + IDSize
    +	offsetMagicByte     = sizeOfProtoHeader
    +	offsetAuthPeerID    = sizeOfProtoHeader + sizeOfMagicByte
    +	headerTotalSizeAuth = sizeOfProtoHeader + headerSizeAuth
     
    +	// hello message
     	headerSizeHello     = sizeOfMagicByte + IDSize
     	headerSizeHelloResp = 0
     
    -	headerSizeAuth     = sizeOfMagicByte + IDSize
    -	headerSizeAuthResp = 0
    +	// transport
    +	headerSizeTransport      = IDSize
    +	offsetTransportID        = sizeOfProtoHeader
    +	headerTotalSizeTransport = sizeOfProtoHeader + headerSizeTransport
     )
     
     var (
    @@ -73,7 +79,7 @@ func (m MsgType) String() string {
     
     // ValidateVersion checks if the given version is supported by the protocol
     func ValidateVersion(msg []byte) (int, error) {
    -	if len(msg) < SizeOfVersionByte {
    +	if len(msg) < sizeOfProtoHeader {
     		return 0, ErrInvalidMessageLength
     	}
     	version := int(msg[0])
    @@ -85,11 +91,11 @@ func ValidateVersion(msg []byte) (int, error) {
     
     // DetermineClientMessageType determines the message type from the first the message
     func DetermineClientMessageType(msg []byte) (MsgType, error) {
    -	if len(msg) < SizeOfMsgType {
    +	if len(msg) < sizeOfProtoHeader {
     		return 0, ErrInvalidMessageLength
     	}
     
    -	msgType := MsgType(msg[0])
    +	msgType := MsgType(msg[1])
     	switch msgType {
     	case
     		MsgTypeHello,
    @@ -105,11 +111,11 @@ func DetermineClientMessageType(msg []byte) (MsgType, error) {
     
     // DetermineServerMessageType determines the message type from the first the message
     func DetermineServerMessageType(msg []byte) (MsgType, error) {
    -	if len(msg) < SizeOfMsgType {
    +	if len(msg) < sizeOfProtoHeader {
     		return 0, ErrInvalidMessageLength
     	}
     
    -	msgType := MsgType(msg[0])
    +	msgType := MsgType(msg[1])
     	switch msgType {
     	case
     		MsgTypeHelloResponse,
    @@ -134,12 +140,12 @@ func MarshalHelloMsg(peerID []byte, additions []byte) ([]byte, error) {
     		return nil, fmt.Errorf("invalid peerID length: %d", len(peerID))
     	}
     
    -	msg := make([]byte, SizeOfProtoHeader+sizeOfMagicByte, SizeOfProtoHeader+headerSizeHello+len(additions))
    +	msg := make([]byte, sizeOfProtoHeader+sizeOfMagicByte, sizeOfProtoHeader+headerSizeHello+len(additions))
     
     	msg[0] = byte(CurrentProtocolVersion)
     	msg[1] = byte(MsgTypeHello)
     
    -	copy(msg[SizeOfProtoHeader:SizeOfProtoHeader+sizeOfMagicByte], magicHeader)
    +	copy(msg[sizeOfProtoHeader:sizeOfProtoHeader+sizeOfMagicByte], magicHeader)
     
     	msg = append(msg, peerID...)
     	msg = append(msg, additions...)
    @@ -151,14 +157,14 @@ func MarshalHelloMsg(peerID []byte, additions []byte) ([]byte, error) {
     // UnmarshalHelloMsg extracts peerID and the additional data from the hello message. The Additional data is used to
     // authenticate the client with the server.
     func UnmarshalHelloMsg(msg []byte) ([]byte, []byte, error) {
    -	if len(msg) < headerSizeHello {
    +	if len(msg) < sizeOfProtoHeader+headerSizeHello {
     		return nil, nil, ErrInvalidMessageLength
     	}
    -	if !bytes.Equal(msg[:sizeOfMagicByte], magicHeader) {
    +	if !bytes.Equal(msg[sizeOfProtoHeader:sizeOfProtoHeader+sizeOfMagicByte], magicHeader) {
     		return nil, nil, errors.New("invalid magic header")
     	}
     
    -	return msg[sizeOfMagicByte:headerSizeHello], msg[headerSizeHello:], nil
    +	return msg[sizeOfProtoHeader+sizeOfMagicByte : sizeOfProtoHeader+headerSizeHello], msg[headerSizeHello:], nil
     }
     
     // Deprecated: Use MarshalAuthResponse instead.
    @@ -167,7 +173,7 @@ func UnmarshalHelloMsg(msg []byte) ([]byte, []byte, error) {
     // instance URL. This URL will be used by choose the common Relay server in case if the peers are in different Relay
     // servers.
     func MarshalHelloResponse(additionalData []byte) ([]byte, error) {
    -	msg := make([]byte, SizeOfProtoHeader, SizeOfProtoHeader+headerSizeHelloResp+len(additionalData))
    +	msg := make([]byte, sizeOfProtoHeader, sizeOfProtoHeader+headerSizeHelloResp+len(additionalData))
     
     	msg[0] = byte(CurrentProtocolVersion)
     	msg[1] = byte(MsgTypeHelloResponse)
    @@ -180,7 +186,7 @@ func MarshalHelloResponse(additionalData []byte) ([]byte, error) {
     // Deprecated: Use UnmarshalAuthResponse instead.
     // UnmarshalHelloResponse extracts the additional data from the hello response message.
     func UnmarshalHelloResponse(msg []byte) ([]byte, error) {
    -	if len(msg) < headerSizeHelloResp {
    +	if len(msg) < sizeOfProtoHeader+headerSizeHelloResp {
     		return nil, ErrInvalidMessageLength
     	}
     	return msg, nil
    @@ -196,12 +202,12 @@ func MarshalAuthMsg(peerID []byte, authPayload []byte) ([]byte, error) {
     		return nil, fmt.Errorf("invalid peerID length: %d", len(peerID))
     	}
     
    -	msg := make([]byte, SizeOfProtoHeader+sizeOfMagicByte, SizeOfProtoHeader+headerSizeAuth+len(authPayload))
    +	msg := make([]byte, sizeOfProtoHeader+sizeOfMagicByte, headerTotalSizeAuth+len(authPayload))
     
     	msg[0] = byte(CurrentProtocolVersion)
     	msg[1] = byte(MsgTypeAuth)
     
    -	copy(msg[SizeOfProtoHeader:SizeOfProtoHeader+sizeOfMagicByte], magicHeader)
    +	copy(msg[sizeOfProtoHeader:], magicHeader)
     
     	msg = append(msg, peerID...)
     	msg = append(msg, authPayload...)
    @@ -211,14 +217,14 @@ func MarshalAuthMsg(peerID []byte, authPayload []byte) ([]byte, error) {
     
     // UnmarshalAuthMsg extracts peerID and the auth payload from the message
     func UnmarshalAuthMsg(msg []byte) ([]byte, []byte, error) {
    -	if len(msg) < headerSizeAuth {
    +	if len(msg) < headerTotalSizeAuth {
     		return nil, nil, ErrInvalidMessageLength
     	}
    -	if !bytes.Equal(msg[:sizeOfMagicByte], magicHeader) {
    +	if !bytes.Equal(msg[offsetMagicByte:offsetMagicByte+sizeOfMagicByte], magicHeader) {
     		return nil, nil, errors.New("invalid magic header")
     	}
     
    -	return msg[sizeOfMagicByte:headerSizeAuth], msg[headerSizeAuth:], nil
    +	return msg[offsetAuthPeerID:headerTotalSizeAuth], msg[headerTotalSizeAuth:], nil
     }
     
     // MarshalAuthResponse creates a response message to the auth.
    @@ -227,7 +233,7 @@ func UnmarshalAuthMsg(msg []byte) ([]byte, []byte, error) {
     // servers.
     func MarshalAuthResponse(address string) ([]byte, error) {
     	ab := []byte(address)
    -	msg := make([]byte, SizeOfProtoHeader, SizeOfProtoHeader+headerSizeAuthResp+len(ab))
    +	msg := make([]byte, sizeOfProtoHeader, sizeOfProtoHeader+len(ab))
     
     	msg[0] = byte(CurrentProtocolVersion)
     	msg[1] = byte(MsgTypeAuthResponse)
    @@ -243,39 +249,34 @@ func MarshalAuthResponse(address string) ([]byte, error) {
     
     // UnmarshalAuthResponse it is a confirmation message to auth success
     func UnmarshalAuthResponse(msg []byte) (string, error) {
    -	if len(msg) < headerSizeAuthResp+1 {
    +	if len(msg) < sizeOfProtoHeader+1 {
     		return "", ErrInvalidMessageLength
     	}
    -	return string(msg), nil
    +	return string(msg[sizeOfProtoHeader:]), nil
     }
     
     // MarshalCloseMsg creates a close message.
     // The close message is used to close the connection gracefully between the client and the server. The server and the
     // client can send this message. After receiving this message, the server or client will close the connection.
     func MarshalCloseMsg() []byte {
    -	msg := make([]byte, SizeOfProtoHeader)
    -
    -	msg[0] = byte(CurrentProtocolVersion)
    -	msg[1] = byte(MsgTypeClose)
    -
    -	return msg
    +	return []byte{
    +		byte(CurrentProtocolVersion),
    +		byte(MsgTypeClose),
    +	}
     }
     
     // MarshalTransportMsg creates a transport message.
     // The transport message is used to exchange data between peers. The message contains the data to be exchanged and the
     // destination peer hashed ID.
    -func MarshalTransportMsg(peerID []byte, payload []byte) ([]byte, error) {
    +func MarshalTransportMsg(peerID, payload []byte) ([]byte, error) {
     	if len(peerID) != IDSize {
     		return nil, fmt.Errorf("invalid peerID length: %d", len(peerID))
     	}
     
    -	msg := make([]byte, SizeOfProtoHeader+headerSizeTransport, SizeOfProtoHeader+headerSizeTransport+len(payload))
    -
    +	msg := make([]byte, headerTotalSizeTransport, headerTotalSizeTransport+len(payload))
     	msg[0] = byte(CurrentProtocolVersion)
     	msg[1] = byte(MsgTypeTransport)
    -
    -	copy(msg[SizeOfProtoHeader:], peerID)
    -
    +	copy(msg[sizeOfProtoHeader:], peerID)
     	msg = append(msg, payload...)
     
     	return msg, nil
    @@ -283,29 +284,29 @@ func MarshalTransportMsg(peerID []byte, payload []byte) ([]byte, error) {
     
     // UnmarshalTransportMsg extracts the peerID and the payload from the transport message.
     func UnmarshalTransportMsg(buf []byte) ([]byte, []byte, error) {
    -	if len(buf) < headerSizeTransport {
    +	if len(buf) < headerTotalSizeTransport {
     		return nil, nil, ErrInvalidMessageLength
     	}
     
    -	return buf[:headerSizeTransport], buf[headerSizeTransport:], nil
    +	return buf[offsetTransportID:headerTotalSizeTransport], buf[headerTotalSizeTransport:], nil
     }
     
     // UnmarshalTransportID extracts the peerID from the transport message.
     func UnmarshalTransportID(buf []byte) ([]byte, error) {
    -	if len(buf) < headerSizeTransport {
    +	if len(buf) < headerTotalSizeTransport {
     		return nil, ErrInvalidMessageLength
     	}
    -	return buf[:headerSizeTransport], nil
    +	return buf[offsetTransportID:headerTotalSizeTransport], nil
     }
     
     // UpdateTransportMsg updates the peerID in the transport message.
     // With this function the server can reuse the given byte slice to update the peerID in the transport message. So do
     // need to allocate a new byte slice.
     func UpdateTransportMsg(msg []byte, peerID []byte) error {
    -	if len(msg) < len(peerID) {
    +	if len(msg) < offsetTransportID+len(peerID) {
     		return ErrInvalidMessageLength
     	}
    -	copy(msg, peerID)
    +	copy(msg[offsetTransportID:], peerID)
     	return nil
     }
     
    diff --git a/relay/messages/message_test.go b/relay/messages/message_test.go
    index 6e917da71..19bede07b 100644
    --- a/relay/messages/message_test.go
    +++ b/relay/messages/message_test.go
    @@ -6,12 +6,21 @@ import (
     
     func TestMarshalHelloMsg(t *testing.T) {
     	peerID := []byte("abdFAaBcawquEiCMzAabYosuUaGLtSNhKxz+")
    -	bHello, err := MarshalHelloMsg(peerID, nil)
    +	msg, err := MarshalHelloMsg(peerID, nil)
     	if err != nil {
     		t.Fatalf("error: %v", err)
     	}
     
    -	receivedPeerID, _, err := UnmarshalHelloMsg(bHello[SizeOfProtoHeader:])
    +	msgType, err := DetermineClientMessageType(msg)
    +	if err != nil {
    +		t.Fatalf("error: %v", err)
    +	}
    +
    +	if msgType != MsgTypeHello {
    +		t.Errorf("expected %d, got %d", MsgTypeHello, msgType)
    +	}
    +
    +	receivedPeerID, _, err := UnmarshalHelloMsg(msg)
     	if err != nil {
     		t.Fatalf("error: %v", err)
     	}
    @@ -22,12 +31,21 @@ func TestMarshalHelloMsg(t *testing.T) {
     
     func TestMarshalAuthMsg(t *testing.T) {
     	peerID := []byte("abdFAaBcawquEiCMzAabYosuUaGLtSNhKxz+")
    -	bHello, err := MarshalAuthMsg(peerID, []byte{})
    +	msg, err := MarshalAuthMsg(peerID, []byte{})
     	if err != nil {
     		t.Fatalf("error: %v", err)
     	}
     
    -	receivedPeerID, _, err := UnmarshalAuthMsg(bHello[SizeOfProtoHeader:])
    +	msgType, err := DetermineClientMessageType(msg)
    +	if err != nil {
    +		t.Fatalf("error: %v", err)
    +	}
    +
    +	if msgType != MsgTypeAuth {
    +		t.Errorf("expected %d, got %d", MsgTypeAuth, msgType)
    +	}
    +
    +	receivedPeerID, _, err := UnmarshalAuthMsg(msg)
     	if err != nil {
     		t.Fatalf("error: %v", err)
     	}
    @@ -36,6 +54,31 @@ func TestMarshalAuthMsg(t *testing.T) {
     	}
     }
     
    +func TestMarshalAuthResponse(t *testing.T) {
    +	address := "myaddress"
    +	msg, err := MarshalAuthResponse(address)
    +	if err != nil {
    +		t.Fatalf("error: %v", err)
    +	}
    +
    +	msgType, err := DetermineServerMessageType(msg)
    +	if err != nil {
    +		t.Fatalf("error: %v", err)
    +	}
    +
    +	if msgType != MsgTypeAuthResponse {
    +		t.Errorf("expected %d, got %d", MsgTypeAuthResponse, msgType)
    +	}
    +
    +	respAddr, err := UnmarshalAuthResponse(msg)
    +	if err != nil {
    +		t.Fatalf("error: %v", err)
    +	}
    +	if respAddr != address {
    +		t.Errorf("expected %s, got %s", address, respAddr)
    +	}
    +}
    +
     func TestMarshalTransportMsg(t *testing.T) {
     	peerID := []byte("abdFAaBcawquEiCMzAabYosuUaGLtSNhKxz+")
     	payload := []byte("payload")
    @@ -44,7 +87,25 @@ func TestMarshalTransportMsg(t *testing.T) {
     		t.Fatalf("error: %v", err)
     	}
     
    -	id, respPayload, err := UnmarshalTransportMsg(msg[SizeOfProtoHeader:])
    +	msgType, err := DetermineClientMessageType(msg)
    +	if err != nil {
    +		t.Fatalf("error: %v", err)
    +	}
    +
    +	if msgType != MsgTypeTransport {
    +		t.Errorf("expected %d, got %d", MsgTypeTransport, msgType)
    +	}
    +
    +	uPeerID, err := UnmarshalTransportID(msg)
    +	if err != nil {
    +		t.Fatalf("failed to unmarshal transport id: %v", err)
    +	}
    +
    +	if string(uPeerID) != string(peerID) {
    +		t.Errorf("expected %s, got %s", peerID, uPeerID)
    +	}
    +
    +	id, respPayload, err := UnmarshalTransportMsg(msg)
     	if err != nil {
     		t.Fatalf("error: %v", err)
     	}
    @@ -57,3 +118,21 @@ func TestMarshalTransportMsg(t *testing.T) {
     		t.Errorf("expected %s, got %s", payload, respPayload)
     	}
     }
    +
    +func TestMarshalHealthcheck(t *testing.T) {
    +	msg := MarshalHealthcheck()
    +
    +	_, err := ValidateVersion(msg)
    +	if err != nil {
    +		t.Fatalf("error: %v", err)
    +	}
    +
    +	msgType, err := DetermineServerMessageType(msg)
    +	if err != nil {
    +		t.Fatalf("error: %v", err)
    +	}
    +
    +	if msgType != MsgTypeHealthCheck {
    +		t.Errorf("expected %d, got %d", MsgTypeHealthCheck, msgType)
    +	}
    +}
    diff --git a/relay/metrics/realy.go b/relay/metrics/realy.go
    index 4dc98a0e0..2e90940e6 100644
    --- a/relay/metrics/realy.go
    +++ b/relay/metrics/realy.go
    @@ -29,37 +29,53 @@ type Metrics struct {
     }
     
     func NewMetrics(ctx context.Context, meter metric.Meter) (*Metrics, error) {
    -	bytesSent, err := meter.Int64Counter("relay_transfer_sent_bytes_total")
    +	bytesSent, err := meter.Int64Counter("relay_transfer_sent_bytes_total",
    +		metric.WithDescription("Total number of bytes sent to peers"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
    -	bytesRecv, err := meter.Int64Counter("relay_transfer_received_bytes_total")
    +	bytesRecv, err := meter.Int64Counter("relay_transfer_received_bytes_total",
    +		metric.WithDescription("Total number of bytes received from peers"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
    -	peers, err := meter.Int64UpDownCounter("relay_peers")
    +	peers, err := meter.Int64UpDownCounter("relay_peers",
    +		metric.WithDescription("Number of connected peers"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
    -	peersActive, err := meter.Int64ObservableGauge("relay_peers_active")
    +	peersActive, err := meter.Int64ObservableGauge("relay_peers_active",
    +		metric.WithDescription("Number of active connected peers"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
    -	peersIdle, err := meter.Int64ObservableGauge("relay_peers_idle")
    +	peersIdle, err := meter.Int64ObservableGauge("relay_peers_idle",
    +		metric.WithDescription("Number of idle connected peers"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
    -	authTime, err := meter.Float64Histogram("relay_peer_authentication_time_milliseconds", metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...))
    +	authTime, err := meter.Float64Histogram("relay_peer_authentication_time_milliseconds",
    +		metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...),
    +		metric.WithDescription("Time taken to authenticate a peer"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
    -	peerStoreTime, err := meter.Float64Histogram("relay_peer_store_time_milliseconds", metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...))
    +	peerStoreTime, err := meter.Float64Histogram("relay_peer_store_time_milliseconds",
    +		metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...),
    +		metric.WithDescription("Time taken to store a new peer connection"),
    +	)
     	if err != nil {
     		return nil, err
     	}
    diff --git a/relay/server/handshake.go b/relay/server/handshake.go
    index 0257300f8..babd6f955 100644
    --- a/relay/server/handshake.go
    +++ b/relay/server/handshake.go
    @@ -68,12 +68,14 @@ func (h *handshake) handshakeReceive() ([]byte, error) {
     		return nil, fmt.Errorf("read from %s: %w", h.conn.RemoteAddr(), err)
     	}
     
    -	_, err = messages.ValidateVersion(buf[:n])
    +	buf = buf[:n]
    +
    +	_, err = messages.ValidateVersion(buf)
     	if err != nil {
     		return nil, fmt.Errorf("validate version from %s: %w", h.conn.RemoteAddr(), err)
     	}
     
    -	msgType, err := messages.DetermineClientMessageType(buf[messages.SizeOfVersionByte:n])
    +	msgType, err := messages.DetermineClientMessageType(buf)
     	if err != nil {
     		return nil, fmt.Errorf("determine message type from %s: %w", h.conn.RemoteAddr(), err)
     	}
    @@ -85,10 +87,10 @@ func (h *handshake) handshakeReceive() ([]byte, error) {
     	switch msgType {
     	//nolint:staticcheck
     	case messages.MsgTypeHello:
    -		bytePeerID, peerID, err = h.handleHelloMsg(buf[messages.SizeOfProtoHeader:n])
    +		bytePeerID, peerID, err = h.handleHelloMsg(buf)
     	case messages.MsgTypeAuth:
     		h.handshakeMethodAuth = true
    -		bytePeerID, peerID, err = h.handleAuthMsg(buf[messages.SizeOfProtoHeader:n])
    +		bytePeerID, peerID, err = h.handleAuthMsg(buf)
     	default:
     		return nil, fmt.Errorf("invalid message type %d from %s", msgType, h.conn.RemoteAddr())
     	}
    diff --git a/relay/server/listener/quic/conn.go b/relay/server/listener/quic/conn.go
    new file mode 100644
    index 000000000..909ec1cc6
    --- /dev/null
    +++ b/relay/server/listener/quic/conn.go
    @@ -0,0 +1,101 @@
    +package quic
    +
    +import (
    +	"context"
    +	"errors"
    +	"fmt"
    +	"net"
    +	"sync"
    +	"time"
    +
    +	"github.com/quic-go/quic-go"
    +)
    +
    +type Conn struct {
    +	session   quic.Connection
    +	closed    bool
    +	closedMu  sync.Mutex
    +	ctx       context.Context
    +	ctxCancel context.CancelFunc
    +}
    +
    +func NewConn(session quic.Connection) *Conn {
    +	ctx, cancel := context.WithCancel(context.Background())
    +	return &Conn{
    +		session:   session,
    +		ctx:       ctx,
    +		ctxCancel: cancel,
    +	}
    +}
    +
    +func (c *Conn) Read(b []byte) (n int, err error) {
    +	dgram, err := c.session.ReceiveDatagram(c.ctx)
    +	if err != nil {
    +		return 0, c.remoteCloseErrHandling(err)
    +	}
    +	// Copy data to b, ensuring we don’t exceed the size of b
    +	n = copy(b, dgram)
    +	return n, nil
    +}
    +
    +func (c *Conn) Write(b []byte) (int, error) {
    +	if err := c.session.SendDatagram(b); err != nil {
    +		return 0, c.remoteCloseErrHandling(err)
    +	}
    +	return len(b), nil
    +}
    +
    +func (c *Conn) LocalAddr() net.Addr {
    +	return c.session.LocalAddr()
    +}
    +
    +func (c *Conn) RemoteAddr() net.Addr {
    +	return c.session.RemoteAddr()
    +}
    +
    +func (c *Conn) SetReadDeadline(t time.Time) error {
    +	return nil
    +}
    +
    +func (c *Conn) SetWriteDeadline(t time.Time) error {
    +	return fmt.Errorf("SetWriteDeadline is not implemented")
    +}
    +
    +func (c *Conn) SetDeadline(t time.Time) error {
    +	return fmt.Errorf("SetDeadline is not implemented")
    +}
    +
    +func (c *Conn) Close() error {
    +	c.closedMu.Lock()
    +	if c.closed {
    +		c.closedMu.Unlock()
    +		return nil
    +	}
    +	c.closed = true
    +	c.closedMu.Unlock()
    +
    +	c.ctxCancel() // Cancel the context
    +
    +	sessionErr := c.session.CloseWithError(0, "normal closure")
    +	return sessionErr
    +}
    +
    +func (c *Conn) isClosed() bool {
    +	c.closedMu.Lock()
    +	defer c.closedMu.Unlock()
    +	return c.closed
    +}
    +
    +func (c *Conn) remoteCloseErrHandling(err error) error {
    +	if c.isClosed() {
    +		return net.ErrClosed
    +	}
    +
    +	// Check if the connection was closed remotely
    +	var appErr *quic.ApplicationError
    +	if errors.As(err, &appErr) && appErr.ErrorCode == 0x0 {
    +		return net.ErrClosed
    +	}
    +
    +	return err
    +}
    diff --git a/relay/server/listener/quic/listener.go b/relay/server/listener/quic/listener.go
    new file mode 100644
    index 000000000..17a5e8ab6
    --- /dev/null
    +++ b/relay/server/listener/quic/listener.go
    @@ -0,0 +1,67 @@
    +package quic
    +
    +import (
    +	"context"
    +	"crypto/tls"
    +	"errors"
    +	"fmt"
    +	"net"
    +
    +	"github.com/quic-go/quic-go"
    +	log "github.com/sirupsen/logrus"
    +)
    +
    +type Listener struct {
    +	// Address is the address to listen on
    +	Address string
    +	// TLSConfig is the TLS configuration for the server
    +	TLSConfig *tls.Config
    +
    +	listener *quic.Listener
    +	acceptFn func(conn net.Conn)
    +}
    +
    +func (l *Listener) Listen(acceptFn func(conn net.Conn)) error {
    +	l.acceptFn = acceptFn
    +
    +	quicCfg := &quic.Config{
    +		EnableDatagrams:   true,
    +		InitialPacketSize: 1452,
    +	}
    +	listener, err := quic.ListenAddr(l.Address, l.TLSConfig, quicCfg)
    +	if err != nil {
    +		return fmt.Errorf("failed to create QUIC listener: %v", err)
    +	}
    +
    +	l.listener = listener
    +	log.Infof("QUIC server listening on address: %s", l.Address)
    +
    +	for {
    +		session, err := listener.Accept(context.Background())
    +		if err != nil {
    +			if errors.Is(err, quic.ErrServerClosed) {
    +				return nil
    +			}
    +
    +			log.Errorf("Failed to accept QUIC session: %v", err)
    +			continue
    +		}
    +
    +		log.Infof("QUIC client connected from: %s", session.RemoteAddr())
    +		conn := NewConn(session)
    +		l.acceptFn(conn)
    +	}
    +}
    +
    +func (l *Listener) Shutdown(ctx context.Context) error {
    +	if l.listener == nil {
    +		return nil
    +	}
    +
    +	log.Infof("stopping QUIC listener")
    +	if err := l.listener.Close(); err != nil {
    +		return fmt.Errorf("listener shutdown failed: %v", err)
    +	}
    +	log.Infof("QUIC listener stopped")
    +	return nil
    +}
    diff --git a/relay/server/listener/ws/conn.go b/relay/server/listener/ws/conn.go
    index 12e721fdb..3ec08945b 100644
    --- a/relay/server/listener/ws/conn.go
    +++ b/relay/server/listener/ws/conn.go
    @@ -8,8 +8,8 @@ import (
     	"sync"
     	"time"
     
    +	"github.com/coder/websocket"
     	log "github.com/sirupsen/logrus"
    -	"nhooyr.io/websocket"
     )
     
     const (
    diff --git a/relay/server/listener/ws/listener.go b/relay/server/listener/ws/listener.go
    index 5c62c0826..3a95951ee 100644
    --- a/relay/server/listener/ws/listener.go
    +++ b/relay/server/listener/ws/listener.go
    @@ -8,8 +8,8 @@ import (
     	"net"
     	"net/http"
     
    +	"github.com/coder/websocket"
     	log "github.com/sirupsen/logrus"
    -	"nhooyr.io/websocket"
     )
     
     // URLPath is the path for the websocket connection.
    @@ -88,6 +88,8 @@ func (l *Listener) onAccept(w http.ResponseWriter, r *http.Request) {
     		return
     	}
     
    +	log.Infof("WS client connected from: %s", rAddr)
    +
     	conn := NewConn(wsConn, lAddr, rAddr)
     	l.acceptFn(conn)
     }
    diff --git a/relay/server/peer.go b/relay/server/peer.go
    index f65fb786a..aa9790f63 100644
    --- a/relay/server/peer.go
    +++ b/relay/server/peer.go
    @@ -84,7 +84,7 @@ func (p *Peer) Work() {
     			return
     		}
     
    -		msgType, err := messages.DetermineClientMessageType(msg[messages.SizeOfVersionByte:])
    +		msgType, err := messages.DetermineClientMessageType(msg)
     		if err != nil {
     			p.log.Errorf("failed to determine message type: %s", err)
     			return
    @@ -191,7 +191,7 @@ func (p *Peer) handleHealthcheckEvents(ctx context.Context, hc *healthcheck.Send
     }
     
     func (p *Peer) handleTransportMsg(msg []byte) {
    -	peerID, err := messages.UnmarshalTransportID(msg[messages.SizeOfProtoHeader:])
    +	peerID, err := messages.UnmarshalTransportID(msg)
     	if err != nil {
     		p.log.Errorf("failed to unmarshal transport message: %s", err)
     		return
    @@ -204,7 +204,7 @@ func (p *Peer) handleTransportMsg(msg []byte) {
     		return
     	}
     
    -	err = messages.UpdateTransportMsg(msg[messages.SizeOfProtoHeader:], p.idB)
    +	err = messages.UpdateTransportMsg(msg, p.idB)
     	if err != nil {
     		p.log.Errorf("failed to update transport message: %s", err)
     		return
    diff --git a/relay/server/relay.go b/relay/server/relay.go
    index 6cd8506ae..a5e77bc61 100644
    --- a/relay/server/relay.go
    +++ b/relay/server/relay.go
    @@ -150,6 +150,8 @@ func (r *Relay) Accept(conn net.Conn) {
     func (r *Relay) Shutdown(ctx context.Context) {
     	log.Infof("close connection with all peers")
     	r.closeMu.Lock()
    +	defer r.closeMu.Unlock()
    +
     	wg := sync.WaitGroup{}
     	peers := r.store.Peers()
     	for _, peer := range peers {
    @@ -161,7 +163,7 @@ func (r *Relay) Shutdown(ctx context.Context) {
     	}
     	wg.Wait()
     	r.metricsCancel()
    -	r.closeMu.Unlock()
    +	r.closed = true
     }
     
     // InstanceURL returns the instance URL of the relay server
    diff --git a/relay/server/server.go b/relay/server/server.go
    index 0036e2390..10aabcace 100644
    --- a/relay/server/server.go
    +++ b/relay/server/server.go
    @@ -3,13 +3,18 @@ package server
     import (
     	"context"
     	"crypto/tls"
    +	"sync"
     
    +	"github.com/hashicorp/go-multierror"
     	log "github.com/sirupsen/logrus"
     	"go.opentelemetry.io/otel/metric"
     
    +	nberrors "github.com/netbirdio/netbird/client/errors"
     	"github.com/netbirdio/netbird/relay/auth"
     	"github.com/netbirdio/netbird/relay/server/listener"
    +	"github.com/netbirdio/netbird/relay/server/listener/quic"
     	"github.com/netbirdio/netbird/relay/server/listener/ws"
    +	quictls "github.com/netbirdio/netbird/relay/tls"
     )
     
     // ListenerConfig is the configuration for the listener.
    @@ -24,8 +29,8 @@ type ListenerConfig struct {
     // It is the gate between the WebSocket listener and the Relay server logic.
     // In a new HTTP connection, the server will accept the connection and pass it to the Relay server via the Accept method.
     type Server struct {
    -	relay      *Relay
    -	wSListener listener.Listener
    +	relay     *Relay
    +	listeners []listener.Listener
     }
     
     // NewServer creates a new relay server instance.
    @@ -39,35 +44,63 @@ func NewServer(meter metric.Meter, exposedAddress string, tlsSupport bool, authV
     		return nil, err
     	}
     	return &Server{
    -		relay: relay,
    +		relay:     relay,
    +		listeners: make([]listener.Listener, 0, 2),
     	}, nil
     }
     
     // Listen starts the relay server.
     func (r *Server) Listen(cfg ListenerConfig) error {
    -	r.wSListener = &ws.Listener{
    +	wSListener := &ws.Listener{
     		Address:   cfg.Address,
     		TLSConfig: cfg.TLSConfig,
     	}
    +	r.listeners = append(r.listeners, wSListener)
     
    -	wslErr := r.wSListener.Listen(r.relay.Accept)
    -	if wslErr != nil {
    -		log.Errorf("failed to bind ws server: %s", wslErr)
    +	tlsConfigQUIC, err := quictls.ServerQUICTLSConfig(cfg.TLSConfig)
    +	if err != nil {
    +		log.Warnf("Not starting QUIC listener: %v", err)
    +	} else {
    +		quicListener := &quic.Listener{
    +			Address:   cfg.Address,
    +			TLSConfig: tlsConfigQUIC,
    +		}
    +
    +		r.listeners = append(r.listeners, quicListener)
     	}
     
    -	return wslErr
    +	errChan := make(chan error, len(r.listeners))
    +	wg := sync.WaitGroup{}
    +	for _, l := range r.listeners {
    +		wg.Add(1)
    +		go func(listener listener.Listener) {
    +			defer wg.Done()
    +			errChan <- listener.Listen(r.relay.Accept)
    +		}(l)
    +	}
    +
    +	wg.Wait()
    +	close(errChan)
    +	var multiErr *multierror.Error
    +	for err := range errChan {
    +		multiErr = multierror.Append(multiErr, err)
    +	}
    +
    +	return nberrors.FormatErrorOrNil(multiErr)
     }
     
     // Shutdown stops the relay server. If there are active connections, they will be closed gracefully. In case of a context,
     // the connections will be forcefully closed.
    -func (r *Server) Shutdown(ctx context.Context) (err error) {
    -	// stop service new connections
    -	if r.wSListener != nil {
    -		err = r.wSListener.Shutdown(ctx)
    -	}
    -
    +func (r *Server) Shutdown(ctx context.Context) error {
     	r.relay.Shutdown(ctx)
    -	return
    +
    +	var multiErr *multierror.Error
    +	for _, l := range r.listeners {
    +		if err := l.Shutdown(ctx); err != nil {
    +			multiErr = multierror.Append(multiErr, err)
    +		}
    +	}
    +	return nberrors.FormatErrorOrNil(multiErr)
     }
     
     // InstanceURL returns the instance URL of the relay server.
    diff --git a/relay/tls/alpn.go b/relay/tls/alpn.go
    new file mode 100644
    index 000000000..29497d401
    --- /dev/null
    +++ b/relay/tls/alpn.go
    @@ -0,0 +1,3 @@
    +package tls
    +
    +const nbalpn = "nb-quic"
    diff --git a/relay/tls/client_dev.go b/relay/tls/client_dev.go
    new file mode 100644
    index 000000000..52e5535c5
    --- /dev/null
    +++ b/relay/tls/client_dev.go
    @@ -0,0 +1,26 @@
    +//go:build devcert
    +
    +package tls
    +
    +import (
    +	"crypto/tls"
    +	"crypto/x509"
    +
    +	log "github.com/sirupsen/logrus"
    +
    +	"github.com/netbirdio/netbird/util/embeddedroots"
    +)
    +
    +func ClientQUICTLSConfig() *tls.Config {
    +	certPool, err := x509.SystemCertPool()
    +	if err != nil || certPool == nil {
    +		log.Debugf("System cert pool not available; falling back to embedded cert, error: %v", err)
    +		certPool = embeddedroots.Get()
    +	}
    +
    +	return &tls.Config{
    +		InsecureSkipVerify: true,             // Debug mode allows insecure connections
    +		NextProtos:         []string{nbalpn}, // Ensure this matches the server's ALPN
    +		RootCAs:            certPool,
    +	}
    +}
    diff --git a/relay/tls/client_prod.go b/relay/tls/client_prod.go
    new file mode 100644
    index 000000000..62e218bc3
    --- /dev/null
    +++ b/relay/tls/client_prod.go
    @@ -0,0 +1,25 @@
    +//go:build !devcert
    +
    +package tls
    +
    +import (
    +	"crypto/tls"
    +	"crypto/x509"
    +
    +	log "github.com/sirupsen/logrus"
    +
    +	"github.com/netbirdio/netbird/util/embeddedroots"
    +)
    +
    +func ClientQUICTLSConfig() *tls.Config {
    +	certPool, err := x509.SystemCertPool()
    +	if err != nil || certPool == nil {
    +		log.Debugf("System cert pool not available; falling back to embedded cert, error: %v", err)
    +		certPool = embeddedroots.Get()
    +	}
    +
    +	return &tls.Config{
    +		NextProtos: []string{nbalpn},
    +		RootCAs:    certPool,
    +	}
    +}
    diff --git a/relay/tls/doc.go b/relay/tls/doc.go
    new file mode 100644
    index 000000000..38b807f84
    --- /dev/null
    +++ b/relay/tls/doc.go
    @@ -0,0 +1,36 @@
    +// Package tls provides utilities for configuring and managing Transport Layer
    +// Security (TLS) in server and client environments, with a focus on QUIC
    +// protocol support and testing configurations.
    +//
    +// The package includes functions for cloning and customizing TLS
    +// configurations as well as generating self-signed certificates for
    +// development and testing purposes.
    +//
    +// Key Features:
    +//
    +//   - `ServerQUICTLSConfig`: Creates a server-side TLS configuration tailored
    +//     for QUIC protocol with specified or default settings. QUIC requires a
    +//     specific TLS configuration with proper ALPN (Application-Layer Protocol
    +//     Negotiation) support, making the TLS settings crucial for establishing
    +//     secure connections.
    +//
    +//   - `ClientQUICTLSConfig`: Provides a client-side TLS configuration suitable
    +//     for QUIC protocol. The configuration differs between development
    +//     (insecure testing) and production (strict verification).
    +//
    +//   - `generateTestTLSConfig`: Generates a self-signed TLS configuration for
    +//     use in local development and testing scenarios.
    +//
    +// Usage:
    +//
    +// This package provides separate implementations for development and production
    +// environments. The development implementation (guarded by `//go:build devcert`)
    +// supports testing configurations with self-signed certificates and insecure
    +// client connections. The production implementation (guarded by `//go:build
    +// !devcert`) ensures that valid and secure TLS configurations are supplied
    +// and used.
    +//
    +// The QUIC protocol is highly reliant on properly configured TLS settings,
    +// and this package ensures that configurations meet the requirements for
    +// secure and efficient QUIC communication.
    +package tls
    diff --git a/relay/tls/server_dev.go b/relay/tls/server_dev.go
    new file mode 100644
    index 000000000..1a01658fc
    --- /dev/null
    +++ b/relay/tls/server_dev.go
    @@ -0,0 +1,79 @@
    +//go:build devcert
    +
    +package tls
    +
    +import (
    +	"crypto/rand"
    +	"crypto/rsa"
    +	"crypto/tls"
    +	"crypto/x509"
    +	"crypto/x509/pkix"
    +	"encoding/pem"
    +	"math/big"
    +	"net"
    +	"time"
    +
    +	log "github.com/sirupsen/logrus"
    +)
    +
    +func ServerQUICTLSConfig(originTLSCfg *tls.Config) (*tls.Config, error) {
    +	if originTLSCfg == nil {
    +		log.Warnf("QUIC server will use self signed certificate for testing!")
    +		return generateTestTLSConfig()
    +	}
    +
    +	cfg := originTLSCfg.Clone()
    +	cfg.NextProtos = []string{nbalpn}
    +	return cfg, nil
    +}
    +
    +// GenerateTestTLSConfig creates a self-signed certificate for testing
    +func generateTestTLSConfig() (*tls.Config, error) {
    +	log.Infof("generating test TLS config")
    +	privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
    +	if err != nil {
    +		return nil, err
    +	}
    +
    +	template := x509.Certificate{
    +		SerialNumber: big.NewInt(1),
    +		Subject: pkix.Name{
    +			Organization: []string{"Test Organization"},
    +		},
    +		NotBefore: time.Now(),
    +		NotAfter:  time.Now().Add(time.Hour * 24 * 180), // Valid for 180 days
    +		KeyUsage:  x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
    +		ExtKeyUsage: []x509.ExtKeyUsage{
    +			x509.ExtKeyUsageServerAuth,
    +		},
    +		BasicConstraintsValid: true,
    +		DNSNames:              []string{"localhost"},
    +		IPAddresses:           []net.IP{net.ParseIP("127.0.0.1")},
    +	}
    +
    +	// Create certificate
    +	certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
    +	if err != nil {
    +		return nil, err
    +	}
    +
    +	certPEM := pem.EncodeToMemory(&pem.Block{
    +		Type:  "CERTIFICATE",
    +		Bytes: certDER,
    +	})
    +
    +	privateKeyPEM := pem.EncodeToMemory(&pem.Block{
    +		Type:  "RSA PRIVATE KEY",
    +		Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
    +	})
    +
    +	tlsCert, err := tls.X509KeyPair(certPEM, privateKeyPEM)
    +	if err != nil {
    +		return nil, err
    +	}
    +
    +	return &tls.Config{
    +		Certificates: []tls.Certificate{tlsCert},
    +		NextProtos:   []string{nbalpn},
    +	}, nil
    +}
    diff --git a/relay/tls/server_prod.go b/relay/tls/server_prod.go
    new file mode 100644
    index 000000000..9d1c47d88
    --- /dev/null
    +++ b/relay/tls/server_prod.go
    @@ -0,0 +1,17 @@
    +//go:build !devcert
    +
    +package tls
    +
    +import (
    +	"crypto/tls"
    +	"fmt"
    +)
    +
    +func ServerQUICTLSConfig(originTLSCfg *tls.Config) (*tls.Config, error) {
    +	if originTLSCfg == nil {
    +		return nil, fmt.Errorf("valid TLS config is required for QUIC listener")
    +	}
    +	cfg := originTLSCfg.Clone()
    +	cfg.NextProtos = []string{nbalpn}
    +	return cfg, nil
    +}
    diff --git a/release_files/darwin-ui-installer.sh b/release_files/darwin-ui-installer.sh
    index 5179f02d6..de331730f 100644
    --- a/release_files/darwin-ui-installer.sh
    +++ b/release_files/darwin-ui-installer.sh
    @@ -2,13 +2,13 @@
     
     export PATH=$PATH:/usr/local/bin:/opt/homebrew/bin
     
    -# check if wiretrustee is installed
    -WT_BIN=$(which wiretrustee)
    -if [ -n "$WT_BIN" ]
    +# check if netbird is installed
    +NB_BIN=$(which netbird)
    +if [ -n "$NB_BIN" ]
     then
    -  echo "Stopping and uninstalling Wiretrustee daemon"
    -  wiretrustee service stop || true
    -  wiretrustee service uninstall || true
    +  echo "Stopping and uninstalling Netbird daemon"
    +  netbird service stop || true
    +  netbird service uninstall || true
     fi
     
     # check if netbird is installed
    diff --git a/release_files/install.sh b/release_files/install.sh
    index bb917c39a..459645c58 100755
    --- a/release_files/install.sh
    +++ b/release_files/install.sh
    @@ -263,16 +263,16 @@ install_netbird() {
             add_aur_repo
         ;;
         brew)
    -        # Remove Wiretrustee if it had been installed using Homebrew before
    -        if brew ls --versions wiretrustee >/dev/null 2>&1; then
    -            echo "Removing existing wiretrustee client"
    +        # Remove Netbird if it had been installed using Homebrew before
    +        if brew ls --versions netbird >/dev/null 2>&1; then
    +            echo "Removing existing netbird client"
     
                 # Stop and uninstall daemon service:
    -            wiretrustee service stop
    -            wiretrustee service uninstall
    +            netbird service stop
    +            netbird service uninstall
     
                 # Unlik the app
    -            brew unlink wiretrustee
    +            brew unlink netbird
             fi
     
             brew install netbirdio/tap/netbird
    diff --git a/release_files/ui-post-install.sh b/release_files/ui-post-install.sh
    new file mode 100644
    index 000000000..f6e8ddf92
    --- /dev/null
    +++ b/release_files/ui-post-install.sh
    @@ -0,0 +1,10 @@
    +#!/bin/sh
    +
    +# Check if netbird-ui is running
    +if pgrep -x -f /usr/bin/netbird-ui >/dev/null 2>&1;
    +then
    +  runner=$(ps --no-headers -o '%U' -p $(pgrep -x -f /usr/bin/netbird-ui) | sed 's/^[ \t]*//;s/[ \t]*$//')
    +  # Only re-run if it was already running
    +  pkill -x -f /usr/bin/netbird-ui >/dev/null 2>&1
    +  su -l - "$runner" -c 'nohup /usr/bin/netbird-ui > /dev/null 2>&1 &'
    +fi
    diff --git a/signal/README.md b/signal/README.md
    index 9e3207cfa..0033eaf90 100644
    --- a/signal/README.md
    +++ b/signal/README.md
    @@ -60,7 +60,7 @@ subdomain sub.mydomain.com).
     
     ```bash
     # create a volume
    -docker volume create wiretrustee-signal
    +docker volume create netbird-signal
     # run the docker container
     docker run -d --name netbird-signal \
     -p 10000:10000  \
    diff --git a/signal/cmd/run.go b/signal/cmd/run.go
    index 1bb2f1d0c..3a671a848 100644
    --- a/signal/cmd/run.go
    +++ b/signal/cmd/run.go
    @@ -8,6 +8,8 @@ import (
     	"fmt"
     	"net"
     	"net/http"
    +	// nolint:gosec
    +	_ "net/http/pprof"
     	"strings"
     	"time"
     
    @@ -82,6 +84,8 @@ var (
     		RunE: func(cmd *cobra.Command, args []string) error {
     			flag.Parse()
     
    +			startPprof()
    +
     			opts, certManager, err := getTLSConfigurations()
     			if err != nil {
     				return err
    @@ -170,6 +174,15 @@ var (
     	}
     )
     
    +func startPprof() {
    +	go func() {
    +		log.Debugf("Starting pprof server on 127.0.0.1:6060")
    +		if err := http.ListenAndServe("127.0.0.1:6060", nil); err != nil {
    +			log.Fatalf("pprof server failed: %v", err)
    +		}
    +	}()
    +}
    +
     func getTLSConfigurations() ([]grpc.ServerOption, *autocert.Manager, error) {
     	var (
     		err         error
    diff --git a/signal/metrics/app.go b/signal/metrics/app.go
    index f8be88be7..e3b1c67cd 100644
    --- a/signal/metrics/app.go
    +++ b/signal/metrics/app.go
    @@ -20,59 +20,91 @@ type AppMetrics struct {
     	MessagesForwarded      metric.Int64Counter
     	MessageForwardFailures metric.Int64Counter
     	MessageForwardLatency  metric.Float64Histogram
    +
    +	MessageSize metric.Int64Histogram
     }
     
     func NewAppMetrics(meter metric.Meter) (*AppMetrics, error) {
    -	activePeers, err := meter.Int64UpDownCounter("active_peers")
    +	activePeers, err := meter.Int64UpDownCounter("active_peers",
    +		metric.WithDescription("Number of active connected peers"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
     	peerConnectionDuration, err := meter.Int64Histogram("peer_connection_duration_seconds",
    -		metric.WithExplicitBucketBoundaries(getPeerConnectionDurationBucketBoundaries()...))
    +		metric.WithExplicitBucketBoundaries(getPeerConnectionDurationBucketBoundaries()...),
    +		metric.WithDescription("Duration of how long a peer was connected"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
    -	registrations, err := meter.Int64Counter("registrations_total")
    +	registrations, err := meter.Int64Counter("registrations_total",
    +		metric.WithDescription("Total number of peer registrations"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
    -	deregistrations, err := meter.Int64Counter("deregistrations_total")
    +	deregistrations, err := meter.Int64Counter("deregistrations_total",
    +		metric.WithDescription("Total number of peer deregistrations"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
    -	registrationFailures, err := meter.Int64Counter("registration_failures_total")
    +	registrationFailures, err := meter.Int64Counter("registration_failures_total",
    +		metric.WithDescription("Total number of peer registration failures"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
     	registrationDelay, err := meter.Float64Histogram("registration_delay_milliseconds",
    -		metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...))
    +		metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...),
    +		metric.WithDescription("Duration of how long it takes to register a peer"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
     	getRegistrationDelay, err := meter.Float64Histogram("get_registration_delay_milliseconds",
    -		metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...))
    +		metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...),
    +		metric.WithDescription("Duration of how long it takes to load a connection from the registry"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
    -	messagesForwarded, err := meter.Int64Counter("messages_forwarded_total")
    +	messagesForwarded, err := meter.Int64Counter("messages_forwarded_total",
    +		metric.WithDescription("Total number of messages forwarded to peers"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
    -	messageForwardFailures, err := meter.Int64Counter("message_forward_failures_total")
    +	messageForwardFailures, err := meter.Int64Counter("message_forward_failures_total",
    +		metric.WithDescription("Total number of message forwarding failures"),
    +	)
     	if err != nil {
     		return nil, err
     	}
     
     	messageForwardLatency, err := meter.Float64Histogram("message_forward_latency_milliseconds",
    -		metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...))
    +		metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...),
    +		metric.WithDescription("Duration of how long it takes to forward a message to a peer"),
    +	)
    +	if err != nil {
    +		return nil, err
    +	}
    +
    +	messageSize, err := meter.Int64Histogram(
    +		"message.size.bytes",
    +		metric.WithUnit("bytes"),
    +		metric.WithExplicitBucketBoundaries(getMessageSizeBucketBoundaries()...),
    +		metric.WithDescription("Records the size of each message sent"),
    +	)
     	if err != nil {
     		return nil, err
     	}
    @@ -92,9 +124,26 @@ func NewAppMetrics(meter metric.Meter) (*AppMetrics, error) {
     		MessagesForwarded:      messagesForwarded,
     		MessageForwardFailures: messageForwardFailures,
     		MessageForwardLatency:  messageForwardLatency,
    +
    +		MessageSize: messageSize,
     	}, nil
     }
     
    +func getMessageSizeBucketBoundaries() []float64 {
    +	return []float64{
    +		100,
    +		250,
    +		500,
    +		1000,
    +		5000,
    +		10000,
    +		50000,
    +		100000,
    +		500000,
    +		1000000,
    +	}
    +}
    +
     func getStandardBucketBoundaries() []float64 {
     	return []float64{
     		0.1,
    diff --git a/signal/server/signal.go b/signal/server/signal.go
    index 305fd052b..3cae7e860 100644
    --- a/signal/server/signal.go
    +++ b/signal/server/signal.go
    @@ -13,6 +13,7 @@ import (
     	"google.golang.org/grpc/codes"
     	"google.golang.org/grpc/metadata"
     	"google.golang.org/grpc/status"
    +	gproto "google.golang.org/protobuf/proto"
     
     	"github.com/netbirdio/netbird/signal/metrics"
     	"github.com/netbirdio/netbird/signal/peer"
    @@ -52,13 +53,13 @@ func NewServer(ctx context.Context, meter metric.Meter) (*Server, error) {
     		return nil, fmt.Errorf("creating app metrics: %v", err)
     	}
     
    -	dispatcher, err := dispatcher.NewDispatcher(ctx, meter)
    +	d, err := dispatcher.NewDispatcher(ctx, meter)
     	if err != nil {
     		return nil, fmt.Errorf("creating dispatcher: %v", err)
     	}
     
     	s := &Server{
    -		dispatcher: dispatcher,
    +		dispatcher: d,
     		registry:   peer.NewRegistry(appMetrics),
     		metrics:    appMetrics,
     	}
    @@ -75,7 +76,7 @@ func (s *Server) Send(ctx context.Context, msg *proto.EncryptedMessage) (*proto.
     		return &proto.EncryptedMessage{}, nil
     	}
     
    -	return s.dispatcher.SendMessage(context.Background(), msg)
    +	return s.dispatcher.SendMessage(ctx, msg)
     }
     
     // ConnectStream connects to the exchange stream
    @@ -98,76 +99,83 @@ func (s *Server) ConnectStream(stream proto.SignalExchange_ConnectStreamServer)
     	log.Debugf("peer connected [%s] [streamID %d] ", p.Id, p.StreamID)
     
     	for {
    -		// read incoming messages
    -		msg, err := stream.Recv()
    -		if err == io.EOF {
    -			break
    -		} else if err != nil {
    -			return err
    -		}
    +		select {
    +		case <-stream.Context().Done():
    +			log.Debugf("stream closed for peer [%s] [streamID %d] due to context cancellation", p.Id, p.StreamID)
    +			return stream.Context().Err()
    +		default:
    +			// read incoming messages
    +			msg, err := stream.Recv()
    +			if err == io.EOF {
    +				break
    +			} else if err != nil {
    +				return err
    +			}
     
    -		log.Debugf("Received a response from peer [%s] to peer [%s]", msg.Key, msg.RemoteKey)
    +			log.Debugf("Received a response from peer [%s] to peer [%s]", msg.Key, msg.RemoteKey)
     
    -		_, err = s.dispatcher.SendMessage(stream.Context(), msg)
    -		if err != nil {
    -			log.Debugf("error while sending message from peer [%s] to peer [%s] %v", msg.Key, msg.RemoteKey, err)
    +			_, err = s.dispatcher.SendMessage(stream.Context(), msg)
    +			if err != nil {
    +				log.Debugf("error while sending message from peer [%s] to peer [%s] %v", msg.Key, msg.RemoteKey, err)
    +			}
     		}
     	}
    -
    -	<-stream.Context().Done()
    -	return stream.Context().Err()
     }
     
     func (s *Server) RegisterPeer(stream proto.SignalExchange_ConnectStreamServer) (*peer.Peer, error) {
     	log.Debugf("registering new peer")
    -	if meta, hasMeta := metadata.FromIncomingContext(stream.Context()); hasMeta {
    -		if id, found := meta[proto.HeaderId]; found {
    -			p := peer.NewPeer(id[0], stream)
    -
    -			s.registry.Register(p)
    -			s.dispatcher.ListenForMessages(stream.Context(), p.Id, s.forwardMessageToPeer)
    -
    -			return p, nil
    -		} else {
    -			s.metrics.RegistrationFailures.Add(stream.Context(), 1, metric.WithAttributes(attribute.String(labelError, labelErrorMissingId)))
    -			return nil, status.Errorf(codes.FailedPrecondition, "missing connection header: "+proto.HeaderId)
    -		}
    -	} else {
    +	meta, hasMeta := metadata.FromIncomingContext(stream.Context())
    +	if !hasMeta {
     		s.metrics.RegistrationFailures.Add(stream.Context(), 1, metric.WithAttributes(attribute.String(labelError, labelErrorMissingMeta)))
     		return nil, status.Errorf(codes.FailedPrecondition, "missing connection stream meta")
     	}
    +
    +	id, found := meta[proto.HeaderId]
    +	if !found {
    +		s.metrics.RegistrationFailures.Add(stream.Context(), 1, metric.WithAttributes(attribute.String(labelError, labelErrorMissingId)))
    +		return nil, status.Errorf(codes.FailedPrecondition, "missing connection header: %s", proto.HeaderId)
    +	}
    +
    +	p := peer.NewPeer(id[0], stream)
    +	s.registry.Register(p)
    +	s.dispatcher.ListenForMessages(stream.Context(), p.Id, s.forwardMessageToPeer)
    +	return p, nil
     }
     
     func (s *Server) DeregisterPeer(p *peer.Peer) {
     	log.Debugf("peer disconnected [%s] [streamID %d] ", p.Id, p.StreamID)
     	s.registry.Deregister(p)
    -
     	s.metrics.PeerConnectionDuration.Record(p.Stream.Context(), int64(time.Since(p.RegisteredAt).Seconds()))
     }
     
     func (s *Server) forwardMessageToPeer(ctx context.Context, msg *proto.EncryptedMessage) {
     	log.Debugf("forwarding a new message from peer [%s] to peer [%s]", msg.Key, msg.RemoteKey)
    -
     	getRegistrationStart := time.Now()
     
     	// lookup the target peer where the message is going to
    -	if dstPeer, found := s.registry.Get(msg.RemoteKey); found {
    -		s.metrics.GetRegistrationDelay.Record(ctx, float64(time.Since(getRegistrationStart).Nanoseconds())/1e6, metric.WithAttributes(attribute.String(labelType, labelTypeStream), attribute.String(labelRegistrationStatus, labelRegistrationFound)))
    -		start := time.Now()
    -		// forward the message to the target peer
    -		if err := dstPeer.Stream.Send(msg); err != nil {
    -			log.Warnf("error while forwarding message from peer [%s] to peer [%s] %v", msg.Key, msg.RemoteKey, err)
    -			// todo respond to the sender?
    -			s.metrics.MessageForwardFailures.Add(ctx, 1, metric.WithAttributes(attribute.String(labelType, labelTypeError)))
    -		} else {
    -			// in milliseconds
    -			s.metrics.MessageForwardLatency.Record(ctx, float64(time.Since(start).Nanoseconds())/1e6, metric.WithAttributes(attribute.String(labelType, labelTypeStream)))
    -			s.metrics.MessagesForwarded.Add(ctx, 1)
    -		}
    -	} else {
    +	dstPeer, found := s.registry.Get(msg.RemoteKey)
    +
    +	if !found {
     		s.metrics.GetRegistrationDelay.Record(ctx, float64(time.Since(getRegistrationStart).Nanoseconds())/1e6, metric.WithAttributes(attribute.String(labelType, labelTypeStream), attribute.String(labelRegistrationStatus, labelRegistrationNotFound)))
     		s.metrics.MessageForwardFailures.Add(ctx, 1, metric.WithAttributes(attribute.String(labelType, labelTypeNotConnected)))
     		log.Debugf("message from peer [%s] can't be forwarded to peer [%s] because destination peer is not connected", msg.Key, msg.RemoteKey)
     		// todo respond to the sender?
    +		return
     	}
    +
    +	s.metrics.GetRegistrationDelay.Record(ctx, float64(time.Since(getRegistrationStart).Nanoseconds())/1e6, metric.WithAttributes(attribute.String(labelType, labelTypeStream), attribute.String(labelRegistrationStatus, labelRegistrationFound)))
    +	start := time.Now()
    +
    +	// forward the message to the target peer
    +	if err := dstPeer.Stream.Send(msg); err != nil {
    +		log.Warnf("error while forwarding message from peer [%s] to peer [%s] %v", msg.Key, msg.RemoteKey, err)
    +		// todo respond to the sender?
    +		s.metrics.MessageForwardFailures.Add(ctx, 1, metric.WithAttributes(attribute.String(labelType, labelTypeError)))
    +		return
    +	}
    +
    +	// in milliseconds
    +	s.metrics.MessageForwardLatency.Record(ctx, float64(time.Since(start).Nanoseconds())/1e6, metric.WithAttributes(attribute.String(labelType, labelTypeStream)))
    +	s.metrics.MessagesForwarded.Add(ctx, 1)
    +	s.metrics.MessageSize.Record(ctx, int64(gproto.Size(msg)), metric.WithAttributes(attribute.String(labelType, labelTypeMessage)))
     }
    diff --git a/util/embeddedroots/embeddedroots.go b/util/embeddedroots/embeddedroots.go
    new file mode 100644
    index 000000000..d205f5b69
    --- /dev/null
    +++ b/util/embeddedroots/embeddedroots.go
    @@ -0,0 +1,42 @@
    +package embeddedroots
    +
    +import (
    +	"crypto/x509"
    +	_ "embed"
    +	"sync"
    +)
    +
    +func Get() *x509.CertPool {
    +	rootsVar.load()
    +	return rootsVar.p
    +}
    +
    +type roots struct {
    +	once sync.Once
    +	p    *x509.CertPool
    +}
    +
    +var rootsVar roots
    +
    +func (r *roots) load() {
    +	r.once.Do(func() {
    +		p := x509.NewCertPool()
    +		p.AppendCertsFromPEM([]byte(isrgRootX1RootPEM))
    +		p.AppendCertsFromPEM([]byte(isrgRootX2RootPEM))
    +		r.p = p
    +	})
    +}
    +
    +// Subject: O = Internet Security Research Group, CN = ISRG Root X1
    +// Key type: RSA 4096
    +// Validity: until 2030-06-04 (generated 2015-06-04)
    +//
    +//go:embed isrg-root-x1.pem
    +var isrgRootX1RootPEM string
    +
    +// Subject: O = Internet Security Research Group, CN = ISRG Root X2
    +// Key type: ECDSA P-384
    +// Validity: until 2035-09-04 (generated 2020-09-04)
    +//
    +//go:embed isrg-root-x2.pem
    +var isrgRootX2RootPEM string
    diff --git a/util/embeddedroots/isrg-root-x1.pem b/util/embeddedroots/isrg-root-x1.pem
    new file mode 100644
    index 000000000..57d4a3766
    --- /dev/null
    +++ b/util/embeddedroots/isrg-root-x1.pem
    @@ -0,0 +1,31 @@
    +-----BEGIN CERTIFICATE-----
    +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
    +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
    +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
    +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
    +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
    +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
    +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
    +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
    +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
    +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
    +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
    +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
    +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
    +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
    +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
    +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
    +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
    +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
    +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
    +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
    +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
    +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
    +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
    +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
    +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
    +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
    +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
    +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
    +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
    +-----END CERTIFICATE-----
    \ No newline at end of file
    diff --git a/util/embeddedroots/isrg-root-x2.pem b/util/embeddedroots/isrg-root-x2.pem
    new file mode 100644
    index 000000000..7d903edc9
    --- /dev/null
    +++ b/util/embeddedroots/isrg-root-x2.pem
    @@ -0,0 +1,14 @@
    +-----BEGIN CERTIFICATE-----
    +MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw
    +CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg
    +R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00
    +MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT
    +ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw
    +EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW
    ++1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9
    +ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T
    +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI
    +zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW
    +tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1
    +/q4AaOeMSQ+2b1tbFfLn
    +-----END CERTIFICATE-----
    diff --git a/util/grpc/dialer.go b/util/grpc/dialer.go
    index 4fbffe342..f6d6d2f04 100644
    --- a/util/grpc/dialer.go
    +++ b/util/grpc/dialer.go
    @@ -3,14 +3,16 @@ package grpc
     import (
     	"context"
     	"crypto/tls"
    +	"crypto/x509"
     	"fmt"
    -	"google.golang.org/grpc/codes"
    -	"google.golang.org/grpc/status"
     	"net"
     	"os/user"
     	"runtime"
     	"time"
     
    +	"google.golang.org/grpc/codes"
    +	"google.golang.org/grpc/status"
    +
     	"github.com/cenkalti/backoff/v4"
     	log "github.com/sirupsen/logrus"
     	"google.golang.org/grpc"
    @@ -18,6 +20,7 @@ import (
     	"google.golang.org/grpc/credentials/insecure"
     	"google.golang.org/grpc/keepalive"
     
    +	"github.com/netbirdio/netbird/util/embeddedroots"
     	nbnet "github.com/netbirdio/netbird/util/net"
     )
     
    @@ -37,7 +40,6 @@ func WithCustomDialer() grpc.DialOption {
     			}
     		}
     
    -		log.Debug("Using nbnet.NewDialer()")
     		conn, err := nbnet.NewDialer().DialContext(ctx, "tcp", addr)
     		if err != nil {
     			log.Errorf("Failed to dial: %s", err)
    @@ -57,9 +59,16 @@ func Backoff(ctx context.Context) backoff.BackOff {
     
     func CreateConnection(addr string, tlsEnabled bool) (*grpc.ClientConn, error) {
     	transportOption := grpc.WithTransportCredentials(insecure.NewCredentials())
    -
     	if tlsEnabled {
    -		transportOption = grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{}))
    +		certPool, err := x509.SystemCertPool()
    +		if err != nil || certPool == nil {
    +			log.Debugf("System cert pool not available; falling back to embedded cert, error: %v", err)
    +			certPool = embeddedroots.Get()
    +		}
    +
    +		transportOption = grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
    +			RootCAs: certPool,
    +		}))
     	}
     
     	connCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    diff --git a/util/net/env.go b/util/net/env.go
    index 099da39b7..32425665d 100644
    --- a/util/net/env.go
    +++ b/util/net/env.go
    @@ -2,6 +2,7 @@ package net
     
     import (
     	"os"
    +	"strconv"
     
     	log "github.com/sirupsen/logrus"
     
    @@ -10,20 +11,24 @@ import (
     
     const (
     	envDisableCustomRouting = "NB_DISABLE_CUSTOM_ROUTING"
    -	envSkipSocketMark       = "NB_SKIP_SOCKET_MARK"
     )
     
    +// CustomRoutingDisabled returns true if custom routing is disabled.
    +// This will fall back to the operation mode before the exit node functionality was implemented.
    +// In particular exclusion routes won't be set up and all dialers and listeners will use net.Dial and net.Listen, respectively.
     func CustomRoutingDisabled() bool {
     	if netstack.IsEnabled() {
     		return true
     	}
    -	return os.Getenv(envDisableCustomRouting) == "true"
    -}
     
    -func SkipSocketMark() bool {
    -	if skipSocketMark := os.Getenv(envSkipSocketMark); skipSocketMark == "true" {
    -		log.Infof("%s is set to true, skipping SO_MARK", envSkipSocketMark)
    -		return true
    +	var customRoutingDisabled bool
    +	if val := os.Getenv(envDisableCustomRouting); val != "" {
    +		var err error
    +		customRoutingDisabled, err = strconv.ParseBool(val)
    +		if err != nil {
    +			log.Warnf("failed to parse %s: %v", envDisableCustomRouting, err)
    +		}
     	}
    -	return false
    +
    +	return customRoutingDisabled
     }
    diff --git a/util/net/env_generic.go b/util/net/env_generic.go
    new file mode 100644
    index 000000000..6d142a838
    --- /dev/null
    +++ b/util/net/env_generic.go
    @@ -0,0 +1,12 @@
    +//go:build !linux || android
    +
    +package net
    +
    +func Init() {
    +	// nothing to do on non-linux
    +}
    +
    +func AdvancedRouting() bool {
    +	// non-linux currently doesn't support advanced routing
    +	return false
    +}
    diff --git a/util/net/env_linux.go b/util/net/env_linux.go
    new file mode 100644
    index 000000000..124bf64de
    --- /dev/null
    +++ b/util/net/env_linux.go
    @@ -0,0 +1,119 @@
    +//go:build linux && !android
    +
    +package net
    +
    +import (
    +	"errors"
    +	"os"
    +	"strconv"
    +	"syscall"
    +	"time"
    +
    +	log "github.com/sirupsen/logrus"
    +	"github.com/vishvananda/netlink"
    +
    +	"github.com/netbirdio/netbird/client/iface/netstack"
    +)
    +
    +const (
    +	// these have the same effect, skip socket env supported for backward compatibility
    +	envSkipSocketMark   = "NB_SKIP_SOCKET_MARK"
    +	envUseLegacyRouting = "NB_USE_LEGACY_ROUTING"
    +)
    +
    +var advancedRoutingSupported bool
    +
    +func Init() {
    +	advancedRoutingSupported = checkAdvancedRoutingSupport()
    +}
    +
    +func AdvancedRouting() bool {
    +	return advancedRoutingSupported
    +}
    +
    +func checkAdvancedRoutingSupport() bool {
    +	var err error
    +
    +	var legacyRouting bool
    +	if val := os.Getenv(envUseLegacyRouting); val != "" {
    +		legacyRouting, err = strconv.ParseBool(val)
    +		if err != nil {
    +			log.Warnf("failed to parse %s: %v", envUseLegacyRouting, err)
    +		}
    +	}
    +
    +	var skipSocketMark bool
    +	if val := os.Getenv(envSkipSocketMark); val != "" {
    +		skipSocketMark, err = strconv.ParseBool(val)
    +		if err != nil {
    +			log.Warnf("failed to parse %s: %v", envSkipSocketMark, err)
    +		}
    +	}
    +
    +	// requested to disable advanced routing
    +	if legacyRouting || skipSocketMark ||
    +		// envCustomRoutingDisabled disables the custom dialers.
    +		// There is no point in using advanced routing without those, as they set up fwmarks on the sockets.
    +		CustomRoutingDisabled() ||
    +		// netstack mode doesn't need routing at all
    +		netstack.IsEnabled() {
    +
    +		log.Info("advanced routing has been requested to be disabled")
    +		return false
    +	}
    +
    +	if !CheckFwmarkSupport() || !CheckRuleOperationsSupport() {
    +		log.Warn("system doesn't support required routing features, falling back to legacy routing")
    +		return false
    +	}
    +
    +	log.Info("system supports advanced routing")
    +
    +	return true
    +}
    +
    +func CheckFwmarkSupport() bool {
    +	// temporarily enable advanced routing to check fwmarks are supported
    +	old := advancedRoutingSupported
    +	advancedRoutingSupported = true
    +	defer func() {
    +		advancedRoutingSupported = old
    +	}()
    +
    +	dialer := NewDialer()
    +	dialer.Timeout = 100 * time.Millisecond
    +
    +	conn, err := dialer.Dial("udp", "127.0.0.1:9")
    +	if err != nil {
    +		log.Warnf("failed to dial with fwmark: %v", err)
    +		return false
    +	}
    +	if err := conn.Close(); err != nil {
    +		log.Warnf("failed to close connection: %v", err)
    +
    +	}
    +
    +	return true
    +}
    +
    +func CheckRuleOperationsSupport() bool {
    +	rule := netlink.NewRule()
    +	// low precedence, semi-random
    +	rule.Priority = 32321
    +	rule.Table = syscall.RT_TABLE_MAIN
    +	rule.Family = netlink.FAMILY_V4
    +
    +	if err := netlink.RuleAdd(rule); err != nil {
    +		if errors.Is(err, syscall.EOPNOTSUPP) {
    +			log.Warn("IP rule operations are not supported")
    +			return false
    +		}
    +		log.Warnf("failed to test rule support: %v", err)
    +		return false
    +	}
    +
    +	if err := netlink.RuleDel(rule); err != nil {
    +		log.Warnf("failed to delete test rule: %v", err)
    +	}
    +	return true
    +}
    diff --git a/util/net/net.go b/util/net/net.go
    index 403aa87e7..7b43b952f 100644
    --- a/util/net/net.go
    +++ b/util/net/net.go
    @@ -1,6 +1,7 @@
     package net
     
     import (
    +	"math/big"
     	"net"
     
     	"github.com/google/uuid"
    @@ -26,3 +27,22 @@ type RemoveHookFunc func(connID ConnectionID) error
     func GenerateConnID() ConnectionID {
     	return ConnectionID(uuid.NewString())
     }
    +
    +func GetLastIPFromNetwork(network *net.IPNet, fromEnd int) net.IP {
    +	// Calculate the last IP in the CIDR range
    +	var endIP net.IP
    +	for i := 0; i < len(network.IP); i++ {
    +		endIP = append(endIP, network.IP[i]|^network.Mask[i])
    +	}
    +
    +	// convert to big.Int
    +	endInt := big.NewInt(0)
    +	endInt.SetBytes(endIP)
    +
    +	// subtract fromEnd from the last ip
    +	fromEndBig := big.NewInt(int64(fromEnd))
    +	resultInt := big.NewInt(0)
    +	resultInt.Sub(endInt, fromEndBig)
    +
    +	return resultInt.Bytes()
    +}
    diff --git a/util/net/net_linux.go b/util/net/net_linux.go
    index fc486ebd4..eae483a26 100644
    --- a/util/net/net_linux.go
    +++ b/util/net/net_linux.go
    @@ -5,13 +5,11 @@ package net
     import (
     	"fmt"
     	"syscall"
    -
    -	log "github.com/sirupsen/logrus"
     )
     
     // SetSocketMark sets the SO_MARK option on the given socket connection
     func SetSocketMark(conn syscall.Conn) error {
    -	if isSocketMarkDisabled() {
    +	if !AdvancedRouting() {
     		return nil
     	}
     
    @@ -25,7 +23,7 @@ func SetSocketMark(conn syscall.Conn) error {
     
     // SetSocketOpt sets the SO_MARK option on the given file descriptor
     func SetSocketOpt(fd int) error {
    -	if isSocketMarkDisabled() {
    +	if !AdvancedRouting() {
     		return nil
     	}
     
    @@ -36,7 +34,7 @@ func setRawSocketMark(conn syscall.RawConn) error {
     	var setErr error
     
     	err := conn.Control(func(fd uintptr) {
    -		if isSocketMarkDisabled() {
    +		if !AdvancedRouting() {
     			return
     		}
     		setErr = setSocketOptInt(int(fd))
    @@ -55,15 +53,3 @@ func setRawSocketMark(conn syscall.RawConn) error {
     func setSocketOptInt(fd int) error {
     	return syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_MARK, NetbirdFwmark)
     }
    -
    -func isSocketMarkDisabled() bool {
    -	if CustomRoutingDisabled() {
    -		log.Infof("Custom routing is disabled, skipping SO_MARK")
    -		return true
    -	}
    -
    -	if SkipSocketMark() {
    -		return true
    -	}
    -	return false
    -}