mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-08 16:51:29 +02:00
Compare commits
3 Commits
anonymize-
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91be8dfe14 | ||
|
|
98f8a24108 | ||
|
|
b2ae98f0fd |
8
.github/workflows/agent-network-e2e.yml
vendored
8
.github/workflows/agent-network-e2e.yml
vendored
@@ -24,25 +24,25 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
|
||||
# Container-driver builder so the harness can build the combined/proxy/
|
||||
# client images from source with a local layer cache.
|
||||
- name: Set up Buildx
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
|
||||
# Persist the Docker layer cache across runs. This caches the base, apt,
|
||||
# and go-mod-download layers; the Go compile still re-runs, as BuildKit
|
||||
# mount caches cannot be exported to the GitHub cache.
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-anet-e2e-buildx-${{ hashFiles('go.sum', 'combined/Dockerfile.multistage', 'proxy/Dockerfile.multistage', 'e2e/harness/Dockerfile.client') }}
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -59,12 +59,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
cache: true
|
||||
|
||||
10
.github/workflows/frontend-ui.yml
vendored
10
.github/workflows/frontend-ui.yml
vendored
@@ -32,17 +32,17 @@ jobs:
|
||||
working-directory: client/ui/frontend
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- name: Set up pnpm
|
||||
uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3.0.0
|
||||
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
with:
|
||||
version: 11
|
||||
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
# are not checked in (see client/ui/frontend/bindings/). Without them,
|
||||
# typecheck/build fail on missing module imports.
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
cache: false
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Cache pnpm store
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: ${{ steps.pnpm-store.outputs.path }}
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('client/ui/frontend/pnpm-lock.yaml') }}
|
||||
|
||||
2
.github/workflows/git-town.yml
vendored
2
.github/workflows/git-town.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: git-town/action@3d8b878379abb1ee393fb49865a28b4a6c2cd3b0 # v1.2.1
|
||||
|
||||
6
.github/workflows/golang-test-darwin.yml
vendored
6
.github/workflows/golang-test-darwin.yml
vendored
@@ -16,18 +16,18 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
cache: false
|
||||
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: macos-gotest-${{ hashFiles('**/go.sum') }}
|
||||
|
||||
4
.github/workflows/golang-test-freebsd.yml
vendored
4
.github/workflows/golang-test-freebsd.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
id: test
|
||||
env:
|
||||
GO_VERSION: ${{ steps.goversion.outputs.version }}
|
||||
uses: vmactions/freebsd-vm@b84ab5559b5a1bb4b8ee2737d2506a16e1737636 # v1.4.8
|
||||
uses: vmactions/freebsd-vm@83b151f58c6047089f4c80eb5ba2039d158ce093 # v1.5.3
|
||||
with:
|
||||
usesh: true
|
||||
copyback: false
|
||||
|
||||
68
.github/workflows/golang-test-linux.yml
vendored
68
.github/workflows/golang-test-linux.yml
vendored
@@ -18,11 +18,11 @@ jobs:
|
||||
management: ${{ steps.filter.outputs.management }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
- 'management/**'
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
cache: false
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
||||
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
id: cache
|
||||
with:
|
||||
path: |
|
||||
@@ -119,12 +119,12 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
cache: false
|
||||
@@ -135,7 +135,7 @@ jobs:
|
||||
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
||||
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
${{ env.cache }}
|
||||
@@ -182,12 +182,12 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
cache: false
|
||||
@@ -199,7 +199,7 @@ jobs:
|
||||
echo "modcache_dir=$(go env GOMODCACHE)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
id: cache-restore
|
||||
with:
|
||||
path: |
|
||||
@@ -253,12 +253,12 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
cache: false
|
||||
@@ -273,7 +273,7 @@ jobs:
|
||||
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
||||
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
${{ env.cache }}
|
||||
@@ -313,12 +313,12 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
cache: false
|
||||
@@ -332,7 +332,7 @@ jobs:
|
||||
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
||||
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
${{ env.cache }}
|
||||
@@ -370,12 +370,12 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
cache: false
|
||||
@@ -390,7 +390,7 @@ jobs:
|
||||
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
||||
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
${{ env.cache }}
|
||||
@@ -431,12 +431,12 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
cache: false
|
||||
@@ -447,7 +447,7 @@ jobs:
|
||||
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
||||
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
${{ env.cache }}
|
||||
@@ -464,7 +464,7 @@ jobs:
|
||||
|
||||
- name: Login to Docker hub
|
||||
if: github.event.pull_request && github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name == '' || github.repository == github.event.pull_request.head.repo.full_name || !github.head_ref
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
@@ -536,12 +536,12 @@ jobs:
|
||||
prom/prometheus
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
cache: false
|
||||
@@ -552,7 +552,7 @@ jobs:
|
||||
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
||||
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
${{ env.cache }}
|
||||
@@ -569,7 +569,7 @@ jobs:
|
||||
|
||||
- name: Login to Docker hub
|
||||
if: github.event.pull_request && github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name == '' || github.repository == github.event.pull_request.head.repo.full_name || !github.head_ref
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
@@ -631,12 +631,12 @@ jobs:
|
||||
prom/prometheus
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
cache: false
|
||||
@@ -647,7 +647,7 @@ jobs:
|
||||
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
||||
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
${{ env.cache }}
|
||||
@@ -664,7 +664,7 @@ jobs:
|
||||
|
||||
- name: Login to Docker hub
|
||||
if: github.event.pull_request && github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name == '' || github.repository == github.event.pull_request.head.repo.full_name || !github.head_ref
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
@@ -701,12 +701,12 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
cache: false
|
||||
@@ -717,7 +717,7 @@ jobs:
|
||||
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
||||
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
${{ env.cache }}
|
||||
|
||||
6
.github/workflows/golang-test-windows.yml
vendored
6
.github/workflows/golang-test-windows.yml
vendored
@@ -18,12 +18,12 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
id: go
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
echo "modcache=$(go env GOMODCACHE)" >> $env:GITHUB_ENV
|
||||
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
${{ env.cache }}
|
||||
|
||||
8
.github/workflows/golangci-lint.yml
vendored
8
.github/workflows/golangci-lint.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: codespell
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for duplicate constants
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
run: |
|
||||
! awk '/const \(/,/)/{print $0}' management/server/activity/codes.go | grep -o '= [0-9]*' | sort | uniq -d | grep .
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
cache: false
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
mkdir -p client/ui/frontend/dist
|
||||
touch client/ui/frontend/dist/.embed-placeholder
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee #v9.2.1
|
||||
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a #v9.3.0
|
||||
with:
|
||||
version: latest
|
||||
skip-cache: true
|
||||
|
||||
2
.github/workflows/install-script-test.yml
vendored
2
.github/workflows/install-script-test.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
12
.github/workflows/mobile-build-validation.yml
vendored
12
.github/workflows/mobile-build-validation.yml
vendored
@@ -16,11 +16,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- name: Setup Android SDK
|
||||
@@ -28,13 +28,13 @@ jobs:
|
||||
with:
|
||||
cmdline-tools-version: 8512546
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961
|
||||
with:
|
||||
java-version: "11"
|
||||
distribution: "adopt"
|
||||
- name: NDK Cache
|
||||
id: ndk-cache
|
||||
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: /usr/local/lib/android/sdk/ndk
|
||||
key: ndk-cache-23.1.7779620
|
||||
@@ -54,11 +54,11 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- name: install gomobile
|
||||
|
||||
181
.github/workflows/release.yml
vendored
181
.github/workflows/release.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
if: steps.check_diff.outputs.diff_exists == 'true'
|
||||
env:
|
||||
GO_VERSION: ${{ steps.goversion.outputs.version }}
|
||||
uses: vmactions/freebsd-vm@b84ab5559b5a1bb4b8ee2737d2506a16e1737636 # v1.4.8
|
||||
uses: vmactions/freebsd-vm@83b151f58c6047089f4c80eb5ba2039d158ce093 # v1.5.3
|
||||
with:
|
||||
usesh: true
|
||||
copyback: false
|
||||
@@ -135,7 +135,7 @@ jobs:
|
||||
ghcr_images: ${{ steps.tag_and_push_images.outputs.images_markdown }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0 # It is required for GoReleaser to work properly
|
||||
persist-credentials: false
|
||||
@@ -166,12 +166,12 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
cache: false
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
@@ -186,18 +186,18 @@ jobs:
|
||||
- name: check git status
|
||||
run: git --no-pager diff --exit-code
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 #v4.1.0
|
||||
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 #v4.2.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 #v4.1.0
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c #v4.2.0
|
||||
- name: Login to Docker hub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
- name: Log in to the GitHub container registry
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -221,7 +221,7 @@ jobs:
|
||||
run: goversioninfo -arm -64 -icon client/ui/build/windows/icon.ico -manifest client/manifest.xml -product-name ${{ env.PRODUCT_NAME }} -copyright "${{ env.COPYRIGHT }}" -ver-major ${{ steps.semver_parser.outputs.major }} -ver-minor ${{ steps.semver_parser.outputs.minor }} -ver-patch ${{ steps.semver_parser.outputs.patch }} -ver-build 0 -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/resources_windows_arm64.syso
|
||||
- name: Run GoReleaser
|
||||
id: goreleaser
|
||||
uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2
|
||||
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
|
||||
with:
|
||||
version: ${{ env.GORELEASER_VER }}
|
||||
args: release --clean ${{ env.flags }}
|
||||
@@ -350,7 +350,7 @@ jobs:
|
||||
release_ui_artifact_url: ${{ steps.upload_release_ui.outputs.artifact-url }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0 # It is required for GoReleaser to work properly
|
||||
persist-credentials: false
|
||||
@@ -377,12 +377,12 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
cache: false
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
@@ -398,12 +398,12 @@ jobs:
|
||||
run: git --no-pager diff --exit-code
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Set up pnpm
|
||||
uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3.0.0
|
||||
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
with:
|
||||
version: 11
|
||||
|
||||
@@ -439,7 +439,7 @@ jobs:
|
||||
run: goversioninfo -arm -64 -icon client/ui/build/windows/icon.ico -manifest client/ui/build/windows/wails.exe.manifest -product-name ${{ env.PRODUCT_NAME }}-"UI" -copyright "${{ env.COPYRIGHT }}" -ver-major ${{ steps.semver_parser.outputs.major }} -ver-minor ${{ steps.semver_parser.outputs.minor }} -ver-patch ${{ steps.semver_parser.outputs.patch }} -ver-build 0 -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/ui/resources_windows_arm64.syso
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2
|
||||
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
|
||||
with:
|
||||
version: ${{ env.GORELEASER_VER }}
|
||||
args: release --config .goreleaser_ui.yaml --clean ${{ env.flags }}
|
||||
@@ -475,6 +475,132 @@ jobs:
|
||||
path: dist/
|
||||
retention-days: 3
|
||||
|
||||
release_ui_gtk3:
|
||||
# Legacy GTK3/WebKit2GTK 4.1 UI build for distros without WebKitGTK 6.0
|
||||
# (Ubuntu 22.04, Debian 12, RHEL 9, Fedora <=39). Runs on ubuntu-22.04 so
|
||||
# the binary links against the oldest supported glibc.
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
release_ui_gtk3_artifact_url: ${{ steps.upload_release_ui_gtk3.outputs.artifact-url }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0 # It is required for GoReleaser to work properly
|
||||
persist-credentials: false
|
||||
|
||||
- name: Parse semver string
|
||||
id: semver_parser
|
||||
uses: netbirdio/shared-actions/actions/parse-semver@be5df6047383da2236e02243cceb857d8567c27e # v0.0.2
|
||||
|
||||
- name: Set snapshot flag
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: |
|
||||
echo "flags=--snapshot" >> $GITHUB_ENV
|
||||
|
||||
- name: Set build vars
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: |
|
||||
if [[ "x-${{ steps.semver_parser.outputs.prerelease }}" == "x-" && "x-${{ github.repository }}" == "x-netbirdio/netbird" ]]; then
|
||||
echo "x-${{ github.repository }}"
|
||||
echo "x-${{ steps.semver_parser.outputs.prerelease }}"
|
||||
echo "SKIP_PUBLISH=false" >> $GITHUB_ENV
|
||||
else
|
||||
echo "x-${{ github.repository }}"
|
||||
echo "x-${{ steps.semver_parser.outputs.prerelease }}"
|
||||
fi
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
cache: false
|
||||
- name: Cache Go modules
|
||||
# Restore-only from the release_ui cache written by trusted runs; the
|
||||
# module cache is identical (same go.sum) and stale build-cache
|
||||
# entries just miss.
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
~/.cache/go-build
|
||||
key: ${{ runner.os }}-ui-go-releaser-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-ui-go-releaser-
|
||||
|
||||
- name: Install modules
|
||||
run: go mod tidy
|
||||
|
||||
- name: check git status
|
||||
run: git --no-pager diff --exit-code
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Set up pnpm
|
||||
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
with:
|
||||
version: 11
|
||||
|
||||
- name: Install dependencies
|
||||
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libwebkit2gtk-4.1-dev
|
||||
|
||||
- name: Decode GPG signing key
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
env:
|
||||
GPG_RPM_PRIVATE_KEY: ${{ secrets.GPG_RPM_PRIVATE_KEY }}
|
||||
run: |
|
||||
echo "$GPG_RPM_PRIVATE_KEY" | base64 -d > /tmp/gpg-rpm-signing-key.asc
|
||||
echo "GPG_RPM_KEY_FILE=/tmp/gpg-rpm-signing-key.asc" >> $GITHUB_ENV
|
||||
|
||||
- name: Install wails3 CLI
|
||||
# Version derived from go.mod so the binding generator always matches
|
||||
# the wails runtime the binary links against.
|
||||
# -tags gtk3: the CLI links the wails runtime's cgo packages, and the
|
||||
# default tags request gtk4/webkitgtk-6.0 pkg-config entries that do
|
||||
# not exist on ubuntu-22.04.
|
||||
run: |
|
||||
WAILS_VERSION=$(go list -m -f '{{.Version}}' github.com/wailsapp/wails/v3)
|
||||
go install -tags gtk3 github.com/wailsapp/wails/v3/cmd/wails3@$WAILS_VERSION
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
|
||||
with:
|
||||
version: ${{ env.GORELEASER_VER }}
|
||||
args: release --config .goreleaser_ui_gtk3.yaml --clean ${{ env.flags }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
UPLOAD_DEBIAN_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
||||
UPLOAD_YUM_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
||||
GPG_RPM_KEY_FILE: ${{ env.GPG_RPM_KEY_FILE }}
|
||||
NFPM_NETBIRD_UI_RPM_GTK3_PASSPHRASE: ${{ secrets.GPG_RPM_PASSPHRASE }}
|
||||
SKIP_PUBLISH: ${{ env.SKIP_PUBLISH }}
|
||||
- name: Verify RPM signatures
|
||||
run: |
|
||||
docker run --rm -v $(pwd)/dist:/dist fedora:41 bash -c '
|
||||
dnf install -y -q rpm-sign curl >/dev/null 2>&1
|
||||
curl -sSL https://pkgs.netbird.io/yum/repodata/repomd.xml.key -o /tmp/rpm-pub.key
|
||||
rpm --import /tmp/rpm-pub.key
|
||||
echo "=== Verifying RPM signatures ==="
|
||||
for rpm_file in /dist/*.rpm; do
|
||||
[ -f "$rpm_file" ] || continue
|
||||
echo "--- $(basename $rpm_file) ---"
|
||||
rpm -K "$rpm_file"
|
||||
done
|
||||
'
|
||||
- name: Clean up GPG key
|
||||
if: always()
|
||||
run: rm -f /tmp/gpg-rpm-signing-key.asc
|
||||
- name: upload non tags for debug purposes
|
||||
id: upload_release_ui_gtk3
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
||||
with:
|
||||
name: release-ui-gtk3
|
||||
path: dist/
|
||||
retention-days: 3
|
||||
|
||||
release_ui_darwin:
|
||||
runs-on: macos-latest
|
||||
outputs:
|
||||
@@ -483,17 +609,17 @@ jobs:
|
||||
- if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: echo "flags=--snapshot" >> $GITHUB_ENV
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0 # It is required for GoReleaser to work properly
|
||||
persist-credentials: false
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
cache: false
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
@@ -506,11 +632,11 @@ jobs:
|
||||
- name: check git status
|
||||
run: git --no-pager diff --exit-code
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '22'
|
||||
- name: Set up pnpm
|
||||
uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3.0.0
|
||||
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
with:
|
||||
version: 11
|
||||
- name: Install wails3 CLI
|
||||
@@ -521,7 +647,7 @@ jobs:
|
||||
go install github.com/wailsapp/wails/v3/cmd/wails3@$WAILS_VERSION
|
||||
- name: Run GoReleaser
|
||||
id: goreleaser
|
||||
uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2
|
||||
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
|
||||
with:
|
||||
version: ${{ env.GORELEASER_VER }}
|
||||
args: release --config .goreleaser_ui_darwin.yaml --clean ${{ env.flags }}
|
||||
@@ -555,7 +681,7 @@ jobs:
|
||||
downloadPath: '${{ github.workspace }}\temp'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -629,7 +755,7 @@ jobs:
|
||||
run: 7z x -o"${{ github.workspace }}/NSIS_Plugins" "${{ github.workspace }}/ShellExecAsUser_amd64-Unicode.7z"
|
||||
|
||||
- name: Set up Go for wails3 CLI
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
cache: false
|
||||
@@ -688,7 +814,7 @@ jobs:
|
||||
comment_release_artifacts:
|
||||
name: Comment release artifacts
|
||||
runs-on: ubuntu-latest
|
||||
needs: [release, release_ui, release_ui_darwin]
|
||||
needs: [release, release_ui, release_ui_gtk3, release_ui_darwin]
|
||||
if: ${{ always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -700,12 +826,14 @@ jobs:
|
||||
env:
|
||||
RELEASE_RESULT: ${{ needs.release.result }}
|
||||
RELEASE_UI_RESULT: ${{ needs.release_ui.result }}
|
||||
RELEASE_UI_GTK3_RESULT: ${{ needs.release_ui_gtk3.result }}
|
||||
RELEASE_UI_DARWIN_RESULT: ${{ needs.release_ui_darwin.result }}
|
||||
RELEASE_ARTIFACT_URL: ${{ needs.release.outputs.release_artifact_url }}
|
||||
LINUX_PACKAGES_ARTIFACT_URL: ${{ needs.release.outputs.linux_packages_artifact_url }}
|
||||
WINDOWS_PACKAGES_ARTIFACT_URL: ${{ needs.release.outputs.windows_packages_artifact_url }}
|
||||
MACOS_PACKAGES_ARTIFACT_URL: ${{ needs.release.outputs.macos_packages_artifact_url }}
|
||||
RELEASE_UI_ARTIFACT_URL: ${{ needs.release_ui.outputs.release_ui_artifact_url }}
|
||||
RELEASE_UI_GTK3_ARTIFACT_URL: ${{ needs.release_ui_gtk3.outputs.release_ui_gtk3_artifact_url }}
|
||||
RELEASE_UI_DARWIN_ARTIFACT_URL: ${{ needs.release_ui_darwin.outputs.release_ui_darwin_artifact_url }}
|
||||
GHCR_IMAGES_MARKDOWN: ${{ needs.release.outputs.ghcr_images }}
|
||||
with:
|
||||
@@ -728,6 +856,7 @@ jobs:
|
||||
['Windows packages', process.env.WINDOWS_PACKAGES_ARTIFACT_URL, process.env.RELEASE_RESULT],
|
||||
['macOS packages', process.env.MACOS_PACKAGES_ARTIFACT_URL, process.env.RELEASE_RESULT],
|
||||
['UI artifacts', process.env.RELEASE_UI_ARTIFACT_URL, process.env.RELEASE_UI_RESULT],
|
||||
['UI GTK3 artifacts', process.env.RELEASE_UI_GTK3_ARTIFACT_URL, process.env.RELEASE_UI_GTK3_RESULT],
|
||||
['UI macOS artifacts', process.env.RELEASE_UI_DARWIN_ARTIFACT_URL, process.env.RELEASE_UI_DARWIN_RESULT],
|
||||
];
|
||||
|
||||
@@ -784,7 +913,7 @@ jobs:
|
||||
|
||||
trigger_signer:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [release, release_ui, release_ui_darwin, test_windows_installer]
|
||||
needs: [release, release_ui, release_ui_gtk3, release_ui_darwin, test_windows_installer]
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
steps:
|
||||
- name: Trigger binaries sign pipelines
|
||||
|
||||
@@ -68,17 +68,17 @@ jobs:
|
||||
run: sudo apt-get install -y curl
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
@@ -253,7 +253,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
10
.github/workflows/wasm-build-validation.yml
vendored
10
.github/workflows/wasm-build-validation.yml
vendored
@@ -19,17 +19,17 @@ jobs:
|
||||
GOARCH: wasm
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- name: Install dependencies
|
||||
run: sudo apt update && sudo apt install -y -q libgtk-4-dev libwebkitgtk-6.0-dev libsoup-3.0-dev libgl1-mesa-dev xorg-dev libpcap-dev
|
||||
- name: Install golangci-lint
|
||||
uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee #v9.2.1
|
||||
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a #v9.3.0
|
||||
with:
|
||||
version: latest
|
||||
install-mode: binary
|
||||
@@ -44,11 +44,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- name: Build Wasm client
|
||||
|
||||
131
.goreleaser_ui_gtk3.yaml
Normal file
131
.goreleaser_ui_gtk3.yaml
Normal file
@@ -0,0 +1,131 @@
|
||||
version: 2
|
||||
env:
|
||||
- SKIP_PUBLISH={{ if index .Env "SKIP_PUBLISH" }}{{ .Env.SKIP_PUBLISH }}{{ else }}true{{ end }}
|
||||
project_name: netbird-ui
|
||||
|
||||
before:
|
||||
hooks:
|
||||
# Bindings are gitignored; regenerate before the frontend build so
|
||||
# the @wailsio/runtime Vite plugin can resolve them (vite refuses to
|
||||
# build without them).
|
||||
# -f '-tags gtk3': the generator type-checks client/ui, whose cgo imports
|
||||
# would otherwise resolve gtk4/webkitgtk-6.0 pkg-config entries that do
|
||||
# not exist on ubuntu-22.04.
|
||||
- sh -c 'cd client/ui && wails3 generate bindings -clean=true -ts -f "-tags gtk3"'
|
||||
- sh -c 'cd client/ui/frontend && pnpm install --frozen-lockfile && pnpm build'
|
||||
|
||||
builds:
|
||||
# Legacy GTK3 / WebKit2GTK 4.1 build for distros without WebKitGTK 6.0
|
||||
# (Ubuntu 22.04, Debian 12, RHEL 9, Fedora <=39). The gtk3 tag flips the
|
||||
# Wails Linux backend to the GTK3 stack and swaps our GTK4-only XEmbed
|
||||
# tray host for the pure-Go stub (client/ui/xembed_host_gtk3_linux.go).
|
||||
# Must be built on the oldest supported glibc (ubuntu-22.04 runner).
|
||||
- id: netbird-ui-gtk3
|
||||
dir: client/ui
|
||||
binary: netbird-ui
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
ldflags:
|
||||
- -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
tags:
|
||||
- production
|
||||
- gtk3
|
||||
|
||||
archives:
|
||||
- id: linux-gtk3-arch
|
||||
name_template: "{{ .ProjectName }}-linux-gtk3_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
||||
builds:
|
||||
- netbird-ui-gtk3
|
||||
|
||||
nfpms:
|
||||
# Same package_name as the GTK4 packages -- the two are mutually-exclusive
|
||||
# alternatives served from separate repo paths (see uploads below); a given
|
||||
# distro points at exactly one of them. The file names must still differ:
|
||||
# the Debian pool is shared storage keyed by file name, so a default-named
|
||||
# gtk3 .deb would overwrite the stable one.
|
||||
- maintainer: Netbird <dev@netbird.io>
|
||||
description: Netbird client UI.
|
||||
homepage: https://netbird.io/
|
||||
license: BSD-3-Clause
|
||||
vendor: NetBird
|
||||
id: netbird_ui_deb_gtk3
|
||||
package_name: netbird-ui
|
||||
file_name_template: "{{ .PackageName }}-gtk3_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
||||
builds:
|
||||
- netbird-ui-gtk3
|
||||
formats:
|
||||
- deb
|
||||
scripts:
|
||||
postinstall: "release_files/ui-post-install.sh"
|
||||
contents:
|
||||
- src: client/ui/build/linux/netbird.desktop
|
||||
dst: /usr/share/applications/org.wails.netbird.desktop
|
||||
- src: client/ui/build/appicon.png
|
||||
dst: /usr/share/pixmaps/netbird.png
|
||||
dependencies:
|
||||
- netbird (>= 0.75.0)
|
||||
- libgtk-3-0
|
||||
- libwebkit2gtk-4.1-0
|
||||
|
||||
- maintainer: Netbird <dev@netbird.io>
|
||||
description: Netbird client UI.
|
||||
homepage: https://netbird.io/
|
||||
license: BSD-3-Clause
|
||||
vendor: NetBird
|
||||
id: netbird_ui_rpm_gtk3
|
||||
package_name: netbird-ui
|
||||
file_name_template: "{{ .PackageName }}-gtk3_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
||||
builds:
|
||||
- netbird-ui-gtk3
|
||||
formats:
|
||||
- rpm
|
||||
scripts:
|
||||
postinstall: "release_files/ui-post-install.sh"
|
||||
contents:
|
||||
- src: client/ui/build/linux/netbird.desktop
|
||||
dst: /usr/share/applications/org.wails.netbird.desktop
|
||||
- src: client/ui/build/appicon.png
|
||||
dst: /usr/share/pixmaps/netbird.png
|
||||
dependencies:
|
||||
- netbird >= 0.75.0
|
||||
- (gtk3 or libgtk-3-0)
|
||||
- (webkit2gtk4.1 or libwebkit2gtk-4_1-0)
|
||||
|
||||
rpm:
|
||||
signature:
|
||||
key_file: '{{ if index .Env "GPG_RPM_KEY_FILE" }}{{ .Env.GPG_RPM_KEY_FILE }}{{ end }}'
|
||||
|
||||
# The GTK4 UI job shares project_name, so the default checksum file name would
|
||||
# collide with it on the shared GitHub release.
|
||||
checksum:
|
||||
name_template: "{{ .ProjectName }}_gtk3_checksums.txt"
|
||||
|
||||
changelog:
|
||||
disable: true
|
||||
|
||||
uploads:
|
||||
# The gtk3 packages reuse the netbird-ui package name, so they live in
|
||||
# dedicated repo paths (deb distribution `gtk3`, yum path `yum-gtk3`) that
|
||||
# legacy distros point their repo config at.
|
||||
- name: debian-gtk3
|
||||
skip: "{{ .Env.SKIP_PUBLISH }}"
|
||||
ids:
|
||||
- netbird_ui_deb_gtk3
|
||||
mode: archive
|
||||
target: https://pkgs.wiretrustee.com/debian/pool/{{ .ArtifactName }};deb.distribution=gtk3;deb.component=main;deb.architecture={{ if .Arm }}armhf{{ else }}{{ .Arch }}{{ end }};deb.package=
|
||||
username: dev@wiretrustee.com
|
||||
method: PUT
|
||||
|
||||
- name: yum-gtk3
|
||||
skip: "{{ .Env.SKIP_PUBLISH }}"
|
||||
ids:
|
||||
- netbird_ui_rpm_gtk3
|
||||
mode: archive
|
||||
target: https://pkgs.wiretrustee.com/yum-gtk3/{{ .Arch }}{{ if .Arm }}{{ .Arm }}{{ end }}
|
||||
username: dev@wiretrustee.com
|
||||
method: PUT
|
||||
317
AGENTS.md
317
AGENTS.md
@@ -14,20 +14,22 @@ in this file, not duplicated there.
|
||||
|
||||
## Contents
|
||||
|
||||
- [NetBird Agent Guidelines](#netbird-agent-guidelines)
|
||||
- [Contents](#contents)
|
||||
- [STOP and ask the user before](#stop-and-ask-the-user-before)
|
||||
- [Quick reference](#quick-reference)
|
||||
- [Structure](#structure)
|
||||
- [Where to look](#where-to-look)
|
||||
- [Repo-wide principles](#repo-wide-principles)
|
||||
- [Error handling](#error-handling)
|
||||
- [Comments](#comments)
|
||||
- [Testing](#testing)
|
||||
- [Pitfalls](#pitfalls)
|
||||
- [Commits, PRs, releases](#commits-prs-releases)
|
||||
- [After you push: CI and review bots](#after-you-push-ci-and-review-bots)
|
||||
- [Discussion and support](#discussion-and-support)
|
||||
- [STOP and ask the user before](#stop-and-ask-the-user-before)
|
||||
- [Quick reference](#quick-reference)
|
||||
- [Structure](#structure)
|
||||
- [Where to look](#where-to-look)
|
||||
- [Security](#security)
|
||||
- [Agent conventions](#agent-conventions)
|
||||
- [Repo-wide principles](#repo-wide-principles)
|
||||
- [Type safety](#type-safety)
|
||||
- [Concurrency and lifecycle](#concurrency-and-lifecycle)
|
||||
- [Error handling](#error-handling)
|
||||
- [Comments](#comments)
|
||||
- [Testing](#testing)
|
||||
- [Pitfalls](#pitfalls)
|
||||
- [Commits, PRs, releases](#commits-prs-releases)
|
||||
- [After you push: CI and review bots](#after-you-push-ci-and-review-bots)
|
||||
- [Discussion and support](#discussion-and-support)
|
||||
|
||||
## STOP and ask the user before
|
||||
|
||||
@@ -157,11 +159,125 @@ netbird/
|
||||
| LLM routing / Agent Network | `proxy/internal/llm/`, `agent-network/` |
|
||||
| End-to-end tests | `e2e/` |
|
||||
|
||||
## Security
|
||||
|
||||
### Never fail open
|
||||
|
||||
When a security check — access control, an IP restriction, an auth decision —
|
||||
hits an error such as an unparseable value, an unavailable lookup, or a state it
|
||||
does not recognize, it must **deny**. Never skip the check or allow the request
|
||||
through because the check itself failed, and make the `default` and unknown cases
|
||||
of a security-related `switch` deny rather than fall through.
|
||||
|
||||
### Daemon RPC input is untrusted
|
||||
|
||||
The agent runs as root (LocalSystem on Windows), so a daemon RPC crosses a
|
||||
privilege boundary: treat every field as untrusted input rather than as something
|
||||
the UI or CLI validated on the way in.
|
||||
|
||||
When you add or change an RPC, ask what the handler does with caller input while
|
||||
running as root. If the answer touches a filesystem path, a URL or host, or a
|
||||
privileged state change, it needs a gate **in the handler** — a check in the client
|
||||
that normally calls it is not a check at all.
|
||||
|
||||
- **A caller-supplied path the daemon opens.** Never `os.Open` it as root.
|
||||
Constrain it, then open it *as the caller* with `ipcauth.OpenOwnedFile`, which
|
||||
opens `O_NOFOLLOW`, requires a regular file, and refuses a file the caller does
|
||||
not own — so a symlink or hardlink aimed at a root-only file is rejected.
|
||||
- **A caller-supplied URL or host the daemon fetches.** Restrict the scheme and
|
||||
allow only known hosts for unprivileged callers. Prefer a lexical host
|
||||
allowlist plus TLS verification over "resolve the host, then reject private
|
||||
IPs": the resolve-then-trust pattern has a DNS-rebinding race (public IP at
|
||||
check time, attacker IP at connect time), while a name allowlist has no IP
|
||||
check to race. Never accept `http://` where `https://` is expected.
|
||||
- **A privileged state change** (SSH root login, management URL, deregistration)
|
||||
gates on the caller identity from `ipcauth.CallerIdentity(ctx)`.
|
||||
|
||||
Caller identity comes from the kernel — `SO_PEERCRED`, `LOCAL_PEERCRED`, or the
|
||||
named-pipe client token — and never from an RPC field. When
|
||||
`ipcauth.CallerIdentity` reports that it could not determine an identity, **deny**;
|
||||
do not fall back to treating the caller as the transport peer.
|
||||
|
||||
## Agent conventions
|
||||
|
||||
### Three networking modes
|
||||
|
||||
Where packets actually flow depends on the mode the agent is running in. The
|
||||
three are not interchangeable, so establish which one a change applies to — and
|
||||
what it should do in the other two — before you write it.
|
||||
|
||||
- **kernel mode** (Linux only): in-kernel WireGuard®. The kernel handles both
|
||||
peer-to-peer and routed traffic, and ACLs are iptables or nftables rules. The
|
||||
client programs kernel facilities but never sees the traffic itself.
|
||||
- **userspace mode** (wireguard-go with a TUN): wireguard-go runs in-process. The
|
||||
kernel handles peer-to-peer traffic once it leaves the TUN, while routed traffic
|
||||
— exit nodes and network routes — goes through the userspace forwarder, which
|
||||
terminates the connection and re-establishes it over OS sockets. Used on
|
||||
platforms without kernel WireGuard® or when the user opts out.
|
||||
- **netstack mode**: wireguard-go in-process with no TUN and no kernel
|
||||
networking. The forwarder does all routing by stitching userspace sockets, and
|
||||
listeners such as the embedded SSH and DNS servers bind on a gVisor netstack.
|
||||
Used where the process cannot create a TUN device, such as the embedded client
|
||||
(`client/embed/`) and the WASM build.
|
||||
|
||||
### The overlay interface is not "WireGuard"
|
||||
|
||||
Do not put "WireGuard" in identifiers or comments unless the code is genuinely
|
||||
coupled to WireGuard® specifically — a wireguard-go call, a handshake field, a
|
||||
kernel WireGuard® netlink attribute. For the interface, the host, peers, or
|
||||
traffic in general, say "the NetBird interface", "the interface", or "the overlay".
|
||||
Most firewall, routing, and DNS code is transport-agnostic, so a WireGuard®
|
||||
reference there is simply inaccurate and rots as the transports change.
|
||||
|
||||
### IPv6 is a soft feature
|
||||
|
||||
The IPv6 overlay is opt-in dual-stack, and capability can change at runtime. Treat
|
||||
it as soft rather than a requirement:
|
||||
|
||||
- Gate local v6 paths on the interface accessor (`wgIface.Address().HasIPv6()`),
|
||||
not on raw state fields, and skip the v6 path when the host has no v6 rather
|
||||
than returning an error.
|
||||
- Treat an empty or unparseable peer v6 address as "no v6 for that peer" and skip
|
||||
it, keeping the v4 path working.
|
||||
- Never let a missing v6 break v4. Fail-closed is for security checks; a
|
||||
capability mismatch skips the v6 work and carries on.
|
||||
|
||||
### Environment variables
|
||||
|
||||
Name the variable in a constant and parse booleans with `strconv.ParseBool` rather
|
||||
than comparing strings inline, so an unexpected value is logged instead of
|
||||
silently meaning false:
|
||||
|
||||
```go
|
||||
const EnvDisableFeature = "NB_DISABLE_FEATURE"
|
||||
|
||||
func isDisabledByEnv() bool {
|
||||
val := os.Getenv(EnvDisableFeature)
|
||||
if val == "" {
|
||||
return false
|
||||
}
|
||||
disabled, err := strconv.ParseBool(val)
|
||||
if err != nil {
|
||||
log.Warnf("failed to parse %s: %v", EnvDisableFeature, err)
|
||||
return false
|
||||
}
|
||||
return disabled
|
||||
}
|
||||
```
|
||||
|
||||
### Validating against protocol specs
|
||||
|
||||
When a change depends on what a protocol actually mandates, read the specification
|
||||
text from the [IETF datatracker](https://datatracker.ietf.org/) rather than a
|
||||
summary, and check that you have the current RFC — the widely cited one for a
|
||||
protocol is often superseded. Cite the section, not just the document, so a
|
||||
reviewer can jump straight to the rule.
|
||||
|
||||
## Repo-wide principles
|
||||
|
||||
1. **Run `go fmt` on every modified Go file.** Formatting is not optional.
|
||||
2. **Zero unaddressed diagnostics.** Fix IDE and linter warnings on code you
|
||||
touch, and delete imports, helpers, and parameters your refactor orphaned.
|
||||
2. **Zero unaddressed linter warnings.** Fix what `golangci-lint` reports on code
|
||||
you touch, and delete imports, helpers, and parameters your refactor orphaned.
|
||||
Exception: unused parameters in shared code may be consumed by builds outside
|
||||
this repository — do not remove them, ask instead.
|
||||
3. **Function comments are mandatory for exported functions**, written as full
|
||||
@@ -175,9 +291,12 @@ netbird/
|
||||
7. **Avoid LLM-slop tells:** em dashes, hedging narration, restating the diff in
|
||||
prose, trailing summaries. Defaults, not absolute bans. Applies to code,
|
||||
comments, commit messages, and PR descriptions alike.
|
||||
8. **Concurrency: do a two-pass race analysis after every change** that adds
|
||||
shared state. Guard maps and slices with a mutex, keep critical sections
|
||||
short, and run `go test -race` on the touched packages.
|
||||
8. **Concurrency: do a two-pass race analysis after every change** that touches
|
||||
shared state, including reads of existing maps and slices. Guard them with a
|
||||
mutex (or an atomic or channel where that fits better), keep critical
|
||||
sections short, and run `go test -race` on the touched packages. See
|
||||
[Concurrency and lifecycle](#concurrency-and-lifecycle) for the failure modes
|
||||
to check for.
|
||||
9. **Cross-platform builds must keep working.** The agent targets Linux, macOS,
|
||||
Windows, FreeBSD, Android, and iOS. When you add a platform-specific file,
|
||||
add the counterpart or a build-tagged fallback for the others.
|
||||
@@ -185,6 +304,93 @@ netbird/
|
||||
11. **Never log secrets** — private keys, setup keys, tokens, PAT values — and
|
||||
keep peer IPs and hostnames out of logs above debug level.
|
||||
|
||||
## Type safety
|
||||
|
||||
**No bare primitives for domain concepts.** A `string` parameter for an account
|
||||
ID next to a `string` parameter for a peer ID is two bugs waiting to happen,
|
||||
because the compiler cannot catch the swap. Declare the type once and use it
|
||||
throughout, converting only at the boundaries where data enters or leaves —
|
||||
protobuf, gRPC, HTTP, an external library.
|
||||
|
||||
```go
|
||||
type ServiceID string
|
||||
type AccountID string
|
||||
|
||||
// Internal: typed all the way through
|
||||
func (r *Router) RemoveRoute(host SNIHost, svcID ServiceID) { ... }
|
||||
|
||||
// Proto boundary: convert once, on the way in and on the way out
|
||||
svcID := ServiceID(mapping.GetId())
|
||||
req.ServiceId = string(svcID)
|
||||
```
|
||||
|
||||
- **IP addresses are `netip.Addr`**, not `string` and not `net.IP`. Parse at the
|
||||
boundary and pass the typed value inward.
|
||||
- **Always `Unmap()`** after parsing an address, after converting from `net.IP`,
|
||||
and after extracting one from `RemoteAddr()`. This normalizes a v4-mapped v6
|
||||
address (`::ffff:10.1.2.3`) to plain v4 so IPv4 rules match it. A stored or
|
||||
compared mapped address silently fails to match those rules.
|
||||
- **Ports are `uint16`** internally; use `int` only where a library forces it and
|
||||
convert immediately.
|
||||
- **Enums are a typed string with constants**, so the valid set is discoverable
|
||||
and a typo fails to compile.
|
||||
- **Map keys follow the same rule**, and must be a real type (`type ServiceID
|
||||
string`) rather than an alias (`type serviceID = string`) — an alias silently
|
||||
accepts bare strings.
|
||||
|
||||
## Concurrency and lifecycle
|
||||
|
||||
Beyond the mutex hygiene in the principles above, check for these failure
|
||||
modes.
|
||||
|
||||
- **Never read a struct field inside a goroutine** when another goroutine may nil
|
||||
or reassign it. Pass the value as a parameter, or capture it into a local before
|
||||
launching. This matters most when `Stop()` nils a field without waiting for the
|
||||
goroutine to finish.
|
||||
|
||||
```go
|
||||
go func(ifaceName string) { // good: passed in, cannot be nilled underneath
|
||||
m.Start(ctx, ifaceName)
|
||||
}(iface.Name())
|
||||
```
|
||||
|
||||
- **Never wait on a channel while holding a lock the sender needs.** Copy what you
|
||||
need out from under the lock, release it, then wait.
|
||||
|
||||
```go
|
||||
func (m *Manager) Stop() {
|
||||
m.mu.Lock()
|
||||
cancel, done := m.cancel, m.done
|
||||
m.mu.Unlock()
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
<-done
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **`Stop`/`Close` must be idempotent** — guard on an already-stopped flag or a
|
||||
nil cancel — and must release the state they guarded. Clear maps and caches;
|
||||
a cancelled goroutine holding a live map still pins that memory. Note that a
|
||||
nil map only panics on writes; reads and iteration behave like an empty map,
|
||||
so where post-close use must be rejected, check the stopped flag explicitly.
|
||||
- **Publish coupled state only after every fallible step succeeds.** When several
|
||||
fields form an invariant, build them into locals and assign them to the receiver
|
||||
at the end. Assigning as you go leaves the object half-initialized when a later
|
||||
step fails, so a readiness predicate reports ready while a coupled field is nil.
|
||||
If an earlier step already had an external side effect — a created chain, an
|
||||
opened handle, an inserted rule — roll it back before returning the error.
|
||||
- **Clean up what you own on constructor error paths.** Once a constructor has
|
||||
started something, every later error path must undo it: cancel a goroutine and
|
||||
wait for it to exit, stop a ticker, close a watcher. The object is never
|
||||
returned, so its `Close` will never run.
|
||||
- **A failed `Start` must undo everything it started.** When a component brings up
|
||||
several subsystems in sequence — connection manager, watchers, routing, DNS,
|
||||
flow, persisted state — a failure partway through has to tear down the ones
|
||||
already running, not just close the handle the error came from. Put the
|
||||
already-started guard *before* that teardown path, so a rejected second `Start`
|
||||
cannot dismantle the one that is running.
|
||||
|
||||
## Error handling
|
||||
|
||||
Use single-assignment form when the error is only needed inside the `if`:
|
||||
@@ -248,6 +454,45 @@ Log the errors you choose not to act on:
|
||||
- Close errors may be ignored for read-only operations; log them at debug for
|
||||
writes.
|
||||
|
||||
**Do not log and return the same error.** It gets reported twice, from two places,
|
||||
and the second reader cannot tell whether it happened once or twice. Return it and
|
||||
let the caller decide. The exception is an API handler that has already written a
|
||||
response. Internal helpers return errors rather than logging and swallowing them.
|
||||
|
||||
**Never return a typed nil as an error.** A nil `*MyError` stored in an `error`
|
||||
interface is not nil, so `err != nil` is true and callers take the failure path on
|
||||
success. Return the error only where it is actually set:
|
||||
|
||||
```go
|
||||
if _, err := conn.Write(buf); err != nil { // good
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
```
|
||||
|
||||
**Accumulate with `multierror` when an operation should continue past individual
|
||||
failures** — teardown, cleanup, or setup where partial success is acceptable.
|
||||
`client/errors.FormatErrorOrNil` returns nil for an empty accumulator, so callers
|
||||
still see a plain nil on full success:
|
||||
|
||||
```go
|
||||
func (m *Manager) Cleanup() error {
|
||||
var merr *multierror.Error
|
||||
for _, r := range m.resources {
|
||||
if err := r.Close(); err != nil {
|
||||
merr = multierror.Append(merr, fmt.Errorf("close %s: %w", r.Name, err))
|
||||
}
|
||||
}
|
||||
return nberrors.FormatErrorOrNil(merr)
|
||||
}
|
||||
```
|
||||
|
||||
| Scenario | Approach | Why |
|
||||
| --------------------- | --------------------- | ----------------------------------------- |
|
||||
| Cleanup / teardown | Accumulate | Clean up as much as possible |
|
||||
| Setup with rollback | Abort on first error | Partial state is invalid; undo what stuck |
|
||||
| Setup with partial OK | Accumulate | Degraded operation is still useful |
|
||||
|
||||
## Comments
|
||||
|
||||
Comment the **why**, never the **what**. Default to no comment, and add one only
|
||||
@@ -269,10 +514,14 @@ checksum = updateChecksum(checksum, oldPort, newPort)
|
||||
|
||||
### Length budget
|
||||
|
||||
- **90 characters per line.** Wrap the comment, do not run past it.
|
||||
- **250 characters per comment**, roughly three wrapped lines. Doc comments on
|
||||
exported identifiers may exceed it when the API genuinely needs the
|
||||
explanation; inline comments inside a function body may not.
|
||||
Neither of these is linter-enforced, so they are conventions the surrounding code
|
||||
mostly follows rather than hard limits:
|
||||
|
||||
- **Around 90 characters per line.** Wrap the comment rather than running well past
|
||||
it.
|
||||
- **Roughly 250 characters per comment**, about three wrapped lines. Doc comments
|
||||
on exported identifiers may exceed it when the API genuinely needs the
|
||||
explanation; inline comments inside a function body rarely should.
|
||||
|
||||
The budget is a smell detector, not a rule to game. Do not compress a needed
|
||||
explanation into cryptic shorthand to fit — if a block of code needs more than
|
||||
@@ -329,6 +578,19 @@ up, and the 250-character budget does not apply to them.
|
||||
otherwise.
|
||||
- **Message guidance:** optional for `NoError`/`Error`; always give context for
|
||||
comparison, boolean, and collection assertions.
|
||||
- **Reproduce a bug before fixing it.** Write the test, watch it fail *for the
|
||||
reason you expect* — a test that fails for an unrelated reason proves nothing —
|
||||
then apply the fix and confirm it passes. Add the thin surrounding cases while
|
||||
you are there.
|
||||
- **Use `t.Setenv`** rather than `os.Setenv` so the previous value is restored on
|
||||
cleanup. To test the unset case, call `t.Setenv` first to register the restore,
|
||||
then `os.Unsetenv`.
|
||||
- **Prefer `t.Cleanup` over `defer`** in any test with parallel subtests: the
|
||||
parent function returns, running its `defer`s, while parallel subtests are
|
||||
still suspended. Sequential subtests finish inside `t.Run`, so `defer` is safe
|
||||
there, but `t.Cleanup` works in both cases.
|
||||
- **Explanatory comments in tests are welcome.** Describe the scenario being set
|
||||
up; the comment budget below does not apply to them.
|
||||
|
||||
```go
|
||||
server, err := StartTestServer()
|
||||
@@ -380,7 +642,8 @@ assert.Equal(t, expectedResult, result, "Result should match expected")
|
||||
than replacing it with your own summary: describe the change, link the issue,
|
||||
tick the checklist honestly (including "ran locally" and "single purpose"),
|
||||
and complete the documentation section. Do not tick a box you have not
|
||||
verified, and do not delete rows that do not apply.
|
||||
verified, and do not delete rows that do not apply — the docs gate in CI reads
|
||||
that section and fails when it is missing.
|
||||
|
||||
- **Keep the PR description short.** Under 1000 words on top of the template's
|
||||
own text, and usually far less — a few paragraphs. Reviewers read the diff;
|
||||
@@ -439,6 +702,12 @@ assert.Equal(t, expectedResult, result, "Result should match expected")
|
||||
on their own. Propose that split to the user rather than opening one large PR
|
||||
and hoping.
|
||||
|
||||
Prefer GitHub's stacked pull requests for such a sequence, rather than
|
||||
hand-managing base branches: open each PR against the branch below it instead of
|
||||
`main`, so every PR's diff shows only its own change. Merging a layer retargets
|
||||
the PRs above it, and branch protections and required checks on the base branch
|
||||
still apply to each one.
|
||||
|
||||
- **User-facing changes need a docs PR** in
|
||||
[netbirdio/docs](https://github.com/netbirdio/docs), linked from the PR
|
||||
description.
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
nbAnonymize "github.com/netbirdio/netbird/client/anonymize"
|
||||
"github.com/netbirdio/netbird/client/iface/device"
|
||||
"github.com/netbirdio/netbird/client/internal"
|
||||
"github.com/netbirdio/netbird/client/internal/debug"
|
||||
@@ -279,10 +278,8 @@ func (c *Client) GetTunSettings() (*TunSettings, error) {
|
||||
}
|
||||
|
||||
// DebugBundle generates a debug bundle, uploads it, and returns the upload key.
|
||||
// It works both with and without a running engine. anonymizeLevel is "default"
|
||||
// or "strict"; strict also anonymizes internal IP ranges, peer names, and
|
||||
// WireGuard public keys, and implies anonymize.
|
||||
func (c *Client) DebugBundle(platformFiles PlatformFiles, anonymize bool, anonymizeLevel string) (string, error) {
|
||||
// It works both with and without a running engine.
|
||||
func (c *Client) DebugBundle(platformFiles PlatformFiles, anonymize bool) (string, error) {
|
||||
cfg, cacheDir, cc := c.stateSnapshot()
|
||||
|
||||
// If the engine hasn't been started, load config from disk
|
||||
@@ -301,7 +298,6 @@ func (c *Client) DebugBundle(platformFiles PlatformFiles, anonymize bool, anonym
|
||||
InternalConfig: cfg,
|
||||
StatusRecorder: c.recorder,
|
||||
TempDir: cacheDir,
|
||||
StatePath: platformFiles.StateFilePath(),
|
||||
}
|
||||
|
||||
if cc != nil {
|
||||
@@ -325,7 +321,6 @@ func (c *Client) DebugBundle(platformFiles PlatformFiles, anonymize bool, anonym
|
||||
deps,
|
||||
debug.BundleConfig{
|
||||
Anonymize: anonymize,
|
||||
AnonymizeLevel: nbAnonymize.ParseLevel(anonymizeLevel),
|
||||
IncludeSystemInfo: true,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -2,7 +2,6 @@ package anonymize
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
@@ -16,81 +15,13 @@ import (
|
||||
|
||||
const anonTLD = ".domain"
|
||||
|
||||
// Level selects how much the anonymizer redacts. Levels are ordered: a higher
|
||||
// level redacts strictly more. On the wire (protos, flags) levels travel as
|
||||
// their string form.
|
||||
type Level int
|
||||
|
||||
const (
|
||||
// LevelDefault anonymizes public IP addresses, IPv6 ULA, domains, and MAC
|
||||
// addresses. Internal IPv4 ranges (RFC 1918, CGNAT, link-local) are
|
||||
// preserved so support can reason about the real topology.
|
||||
LevelDefault Level = iota
|
||||
// LevelStrict additionally anonymizes internal IP ranges, peer names, and
|
||||
// WireGuard public keys.
|
||||
LevelStrict
|
||||
)
|
||||
|
||||
// ParseLevel maps s to a Level. Empty means LevelDefault; anything
|
||||
// unrecognized maps to LevelStrict so an unknown request never yields less
|
||||
// anonymization than intended.
|
||||
func ParseLevel(s string) Level {
|
||||
switch strings.ToLower(s) {
|
||||
case "", LevelDefault.String():
|
||||
return LevelDefault
|
||||
default:
|
||||
return LevelStrict
|
||||
}
|
||||
}
|
||||
|
||||
// String returns the wire form of the level: "default" or "strict".
|
||||
func (l Level) String() string {
|
||||
if l >= LevelStrict {
|
||||
return "strict"
|
||||
}
|
||||
return "default"
|
||||
}
|
||||
|
||||
// protectedDomains are NetBird-operated suffixes that stay recognizable in an
|
||||
// anonymized bundle. At LevelStrict the labels in front of them (the peer
|
||||
// name) are still replaced, except under netbird.io, which only hosts
|
||||
// NetBird infrastructure (api, signal, flow), never peer names.
|
||||
var protectedDomains = []string{"netbird.io", "netbird.selfhosted", "netbird.cloud", "netbird.stage"}
|
||||
|
||||
const infraDomain = "netbird.io"
|
||||
|
||||
var (
|
||||
macColonRegex = regexp.MustCompile(`\b[0-9a-fA-F]{2}(?::[0-9a-fA-F]{2}){5}\b`)
|
||||
macDashRegex = regexp.MustCompile(`\b[0-9a-fA-F]{2}(?:-[0-9a-fA-F]{2}){5}\b`)
|
||||
wgKeyRegex = regexp.MustCompile(`\b[A-Za-z0-9+/]{43}=`)
|
||||
)
|
||||
|
||||
type Anonymizer struct {
|
||||
ipAnonymizer map[netip.Addr]netip.Addr
|
||||
domainAnonymizer map[string]string
|
||||
// domainOrder caches the keys of domainAnonymizer sorted longest-first
|
||||
// for AnonymizeString; it is rebuilt when the map gains entries.
|
||||
domainOrder []string
|
||||
labelAnonymizer map[string]string
|
||||
labelAnonymized map[string]struct{}
|
||||
labelCounter uint32
|
||||
macAnonymizer map[string]string
|
||||
macCounter uint32
|
||||
wgKeyAnonymizer map[string]string
|
||||
wgKeyAnonymized map[string]struct{}
|
||||
currentAnonIPv4 netip.Addr
|
||||
currentAnonIPv6 netip.Addr
|
||||
startAnonIPv4 netip.Addr
|
||||
startAnonIPv6 netip.Addr
|
||||
|
||||
// LevelStrict also anonymizes internal ranges (RFC 1918, CGNAT,
|
||||
// link-local), replacing them from the dedicated internal pools below so
|
||||
// a reader can still tell an internal address from a public one.
|
||||
level Level
|
||||
currentAnonInternalIPv4 netip.Addr
|
||||
currentAnonInternalIPv6 netip.Addr
|
||||
startAnonInternalIPv4 netip.Addr
|
||||
startAnonInternalIPv6 netip.Addr
|
||||
currentAnonIPv4 netip.Addr
|
||||
currentAnonIPv6 netip.Addr
|
||||
startAnonIPv4 netip.Addr
|
||||
startAnonIPv6 netip.Addr
|
||||
|
||||
domainKeyRegex *regexp.Regexp
|
||||
}
|
||||
@@ -101,50 +32,25 @@ func DefaultAddresses() (netip.Addr, netip.Addr) {
|
||||
return netip.AddrFrom4([4]byte{198, 51, 100, 0}), netip.MustParseAddr("2001:db8:ffff::")
|
||||
}
|
||||
|
||||
// InternalAddresses returns the pool starts used in strict mode for internal
|
||||
// ranges. Both are reserved ranges that cannot collide with real addressing:
|
||||
// 198.18.0.0 (RFC 2544 benchmarking), 2001:db8:1:: (RFC 3849 documentation).
|
||||
func InternalAddresses() (netip.Addr, netip.Addr) {
|
||||
return netip.AddrFrom4([4]byte{198, 18, 0, 0}), netip.MustParseAddr("2001:db8:1::")
|
||||
}
|
||||
|
||||
func NewAnonymizer(startIPv4, startIPv6 netip.Addr) *Anonymizer {
|
||||
internalIPv4, internalIPv6 := InternalAddresses()
|
||||
return &Anonymizer{
|
||||
ipAnonymizer: map[netip.Addr]netip.Addr{},
|
||||
domainAnonymizer: map[string]string{},
|
||||
labelAnonymizer: map[string]string{},
|
||||
labelAnonymized: map[string]struct{}{},
|
||||
macAnonymizer: map[string]string{},
|
||||
wgKeyAnonymizer: map[string]string{},
|
||||
wgKeyAnonymized: map[string]struct{}{},
|
||||
currentAnonIPv4: startIPv4,
|
||||
currentAnonIPv6: startIPv6,
|
||||
startAnonIPv4: startIPv4,
|
||||
startAnonIPv6: startIPv6,
|
||||
|
||||
level: LevelDefault,
|
||||
currentAnonInternalIPv4: internalIPv4,
|
||||
currentAnonInternalIPv6: internalIPv6,
|
||||
startAnonInternalIPv4: internalIPv4,
|
||||
startAnonInternalIPv6: internalIPv6,
|
||||
|
||||
domainKeyRegex: regexp.MustCompile(`\bdomain=([^\s,:"]+)`),
|
||||
}
|
||||
}
|
||||
|
||||
// SetLevel selects the anonymization level. The zero value of a new
|
||||
// Anonymizer is LevelDefault.
|
||||
func (a *Anonymizer) SetLevel(level Level) {
|
||||
a.level = level
|
||||
}
|
||||
|
||||
func (a *Anonymizer) AnonymizeIP(ip netip.Addr) netip.Addr {
|
||||
// Normalize 4-in-6 addresses so ::ffff:192.168.1.1 classifies and maps
|
||||
// like 192.168.1.1.
|
||||
ip = ip.Unmap()
|
||||
|
||||
if ip.IsLoopback() ||
|
||||
ip.IsLinkLocalUnicast() ||
|
||||
ip.IsLinkLocalMulticast() ||
|
||||
ip.IsInterfaceLocalMulticast() ||
|
||||
(ip.Is4() && ip.IsPrivate()) ||
|
||||
ip.IsUnspecified() ||
|
||||
ip.IsMulticast() ||
|
||||
isWellKnown(ip) ||
|
||||
@@ -153,100 +59,18 @@ func (a *Anonymizer) AnonymizeIP(ip netip.Addr) netip.Addr {
|
||||
return ip
|
||||
}
|
||||
|
||||
if isInternal(ip) && a.level < LevelStrict {
|
||||
return ip
|
||||
}
|
||||
|
||||
if _, ok := a.ipAnonymizer[ip]; !ok {
|
||||
a.ipAnonymizer[ip] = a.nextAnonIP(ip)
|
||||
if ip.Is4() {
|
||||
a.ipAnonymizer[ip] = a.currentAnonIPv4
|
||||
a.currentAnonIPv4 = a.currentAnonIPv4.Next()
|
||||
} else {
|
||||
a.ipAnonymizer[ip] = a.currentAnonIPv6
|
||||
a.currentAnonIPv6 = a.currentAnonIPv6.Next()
|
||||
}
|
||||
}
|
||||
return a.ipAnonymizer[ip]
|
||||
}
|
||||
|
||||
func (a *Anonymizer) nextAnonIP(ip netip.Addr) netip.Addr {
|
||||
// At the strict level, internal addresses (including IPv6 ULA, matched
|
||||
// by IsPrivate) come from the internal pools so they remain recognizable
|
||||
// as internal without disclosing the real values.
|
||||
if a.level >= LevelStrict && (isInternal(ip) || ip.IsPrivate()) {
|
||||
if ip.Is4() {
|
||||
anon := a.currentAnonInternalIPv4
|
||||
a.currentAnonInternalIPv4 = a.currentAnonInternalIPv4.Next()
|
||||
return anon
|
||||
}
|
||||
anon := a.currentAnonInternalIPv6
|
||||
a.currentAnonInternalIPv6 = a.currentAnonInternalIPv6.Next()
|
||||
return anon
|
||||
}
|
||||
|
||||
if ip.Is4() {
|
||||
anon := a.currentAnonIPv4
|
||||
a.currentAnonIPv4 = a.currentAnonIPv4.Next()
|
||||
return anon
|
||||
}
|
||||
anon := a.currentAnonIPv6
|
||||
a.currentAnonIPv6 = a.currentAnonIPv6.Next()
|
||||
return anon
|
||||
}
|
||||
|
||||
// AnonymizeMAC replaces a MAC address with a consistent placeholder from the
|
||||
// locally administered range starting at 02:00:00:00:00:01, at every
|
||||
// anonymization level. Broadcast, multicast, all-zero, and already assigned
|
||||
// placeholder addresses are preserved. The colon and dash spellings of the
|
||||
// same address share one placeholder; the output keeps the input's separator.
|
||||
func (a *Anonymizer) AnonymizeMAC(mac string) string {
|
||||
hw, err := net.ParseMAC(mac)
|
||||
if err != nil || len(hw) != 6 {
|
||||
return mac
|
||||
}
|
||||
|
||||
if isWellKnownMAC(hw) || a.isAnonymizedMAC(hw) {
|
||||
return mac
|
||||
}
|
||||
|
||||
key := hw.String()
|
||||
anon, ok := a.macAnonymizer[key]
|
||||
if !ok {
|
||||
a.macCounter++
|
||||
anon = fmt.Sprintf("02:00:00:%02x:%02x:%02x", byte(a.macCounter>>16), byte(a.macCounter>>8), byte(a.macCounter))
|
||||
a.macAnonymizer[key] = anon
|
||||
}
|
||||
|
||||
if strings.Contains(mac, "-") {
|
||||
anon = strings.ReplaceAll(anon, ":", "-")
|
||||
}
|
||||
return anon
|
||||
}
|
||||
|
||||
// isAnonymizedMAC reports whether hw is a placeholder this anonymizer already
|
||||
// handed out, so a second pass over anonymized output leaves it unchanged.
|
||||
func (a *Anonymizer) isAnonymizedMAC(hw net.HardwareAddr) bool {
|
||||
if hw[0] != 0x02 || hw[1] != 0 || hw[2] != 0 {
|
||||
return false
|
||||
}
|
||||
value := uint32(hw[3])<<16 | uint32(hw[4])<<8 | uint32(hw[5])
|
||||
return value <= a.macCounter
|
||||
}
|
||||
|
||||
// AnonymizeWGKey replaces a WireGuard public key with a consistent random
|
||||
// placeholder of the same shape. Keys are only anonymized at LevelStrict;
|
||||
// placeholders already handed out pass through unchanged.
|
||||
func (a *Anonymizer) AnonymizeWGKey(key string) string {
|
||||
if a.level < LevelStrict || !looksLikeWGKey(key) {
|
||||
return key
|
||||
}
|
||||
if _, ok := a.wgKeyAnonymized[key]; ok {
|
||||
return key
|
||||
}
|
||||
|
||||
anon, ok := a.wgKeyAnonymizer[key]
|
||||
if !ok {
|
||||
anon = generateAnonymousKey()
|
||||
a.wgKeyAnonymizer[key] = anon
|
||||
a.wgKeyAnonymized[anon] = struct{}{}
|
||||
}
|
||||
return anon
|
||||
}
|
||||
|
||||
func (a *Anonymizer) AnonymizeUDPAddr(addr net.UDPAddr) net.UDPAddr {
|
||||
// Convert IP to netip.Addr
|
||||
ip, ok := netip.AddrFromSlice(addr.IP)
|
||||
@@ -265,12 +89,12 @@ func (a *Anonymizer) AnonymizeUDPAddr(addr net.UDPAddr) net.UDPAddr {
|
||||
|
||||
// isInAnonymizedRange checks if an IP is within the range of already assigned anonymized IPs
|
||||
func (a *Anonymizer) isInAnonymizedRange(ip netip.Addr) bool {
|
||||
if ip.Is4() {
|
||||
return inPoolRange(ip, a.startAnonIPv4, a.currentAnonIPv4) ||
|
||||
inPoolRange(ip, a.startAnonInternalIPv4, a.currentAnonInternalIPv4)
|
||||
if ip.Is4() && ip.Compare(a.startAnonIPv4) >= 0 && ip.Compare(a.currentAnonIPv4) <= 0 {
|
||||
return true
|
||||
} else if !ip.Is4() && ip.Compare(a.startAnonIPv6) >= 0 && ip.Compare(a.currentAnonIPv6) <= 0 {
|
||||
return true
|
||||
}
|
||||
return inPoolRange(ip, a.startAnonIPv6, a.currentAnonIPv6) ||
|
||||
inPoolRange(ip, a.startAnonInternalIPv6, a.currentAnonInternalIPv6)
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *Anonymizer) AnonymizeIPString(ip string) string {
|
||||
@@ -294,17 +118,14 @@ func (a *Anonymizer) AnonymizeDomain(domain string) string {
|
||||
baseDomain = domain[:len(domain)-1]
|
||||
}
|
||||
|
||||
if strings.HasSuffix(baseDomain, anonTLD) {
|
||||
if strings.HasSuffix(baseDomain, "netbird.io") ||
|
||||
strings.HasSuffix(baseDomain, "netbird.selfhosted") ||
|
||||
strings.HasSuffix(baseDomain, "netbird.cloud") ||
|
||||
strings.HasSuffix(baseDomain, "netbird.stage") ||
|
||||
strings.HasSuffix(baseDomain, anonTLD) {
|
||||
return domain
|
||||
}
|
||||
|
||||
if suffix := protectedSuffix(baseDomain); suffix != "" {
|
||||
if a.level < LevelStrict || baseDomain == suffix || suffix == infraDomain {
|
||||
return domain
|
||||
}
|
||||
return withTrailingDot(a.anonymizePeerName(baseDomain, suffix), hasDot)
|
||||
}
|
||||
|
||||
parts := strings.Split(baseDomain, ".")
|
||||
if len(parts) < 2 {
|
||||
return domain
|
||||
@@ -320,53 +141,12 @@ func (a *Anonymizer) AnonymizeDomain(domain string) string {
|
||||
}
|
||||
|
||||
result := strings.Replace(baseDomain, baseForLookup, anonymized, 1)
|
||||
if a.level >= LevelStrict && len(parts) > 2 {
|
||||
prefix := strings.TrimSuffix(baseDomain, "."+baseForLookup)
|
||||
result = a.anonymizeLabels(prefix, "host") + "." + anonymized
|
||||
// The full mapping feeds AnonymizeString so seeded FQDNs are caught
|
||||
// in log lines as a whole, labels included.
|
||||
a.domainAnonymizer[baseDomain] = result
|
||||
}
|
||||
return withTrailingDot(result, hasDot)
|
||||
}
|
||||
|
||||
// anonymizePeerName replaces the labels in front of a protected suffix with
|
||||
// numbered peer placeholders, keeping the suffix, and records the full
|
||||
// mapping for string replacement in logs. The numbering keeps a peer
|
||||
// recognizable across the whole bundle without disclosing its name.
|
||||
func (a *Anonymizer) anonymizePeerName(baseDomain, suffix string) string {
|
||||
prefix := strings.TrimSuffix(baseDomain, "."+suffix)
|
||||
result := a.anonymizeLabels(prefix, "peer") + "." + suffix
|
||||
if result != baseDomain {
|
||||
a.domainAnonymizer[baseDomain] = result
|
||||
if hasDot {
|
||||
result += "."
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// anonymizeLabels replaces each dot-separated label with a consistent
|
||||
// numbered placeholder ("<placeholder>-<n>"). Wildcard labels and
|
||||
// placeholders already handed out pass through unchanged.
|
||||
func (a *Anonymizer) anonymizeLabels(prefix, placeholder string) string {
|
||||
labels := strings.Split(prefix, ".")
|
||||
for i, label := range labels {
|
||||
if label == "*" {
|
||||
continue
|
||||
}
|
||||
if _, ok := a.labelAnonymized[label]; ok {
|
||||
continue
|
||||
}
|
||||
anon, ok := a.labelAnonymizer[label]
|
||||
if !ok {
|
||||
a.labelCounter++
|
||||
anon = fmt.Sprintf("%s-%d", placeholder, a.labelCounter)
|
||||
a.labelAnonymizer[label] = anon
|
||||
a.labelAnonymized[anon] = struct{}{}
|
||||
}
|
||||
labels[i] = anon
|
||||
}
|
||||
return strings.Join(labels, ".")
|
||||
}
|
||||
|
||||
func (a *Anonymizer) AnonymizeURI(uri string) string {
|
||||
u, err := url.Parse(uri)
|
||||
if err != nil {
|
||||
@@ -401,70 +181,16 @@ func (a *Anonymizer) AnonymizeString(str string) string {
|
||||
str = ipv4Regex.ReplaceAllStringFunc(str, a.AnonymizeIPString)
|
||||
str = ipv6Regex.ReplaceAllStringFunc(str, a.AnonymizeIPString)
|
||||
|
||||
for _, domain := range a.sortedDomains() {
|
||||
str = strings.ReplaceAll(str, domain, a.domainAnonymizer[domain])
|
||||
for domain, anonDomain := range a.domainAnonymizer {
|
||||
str = strings.ReplaceAll(str, domain, anonDomain)
|
||||
}
|
||||
|
||||
str = a.AnonymizeSchemeURI(str)
|
||||
str = a.AnonymizeDNSLogLine(str)
|
||||
|
||||
// MAC handling runs after the IP passes so preserved IPv6 addresses are
|
||||
// already out of the way; the separator guard skips matches embedded in a
|
||||
// longer colon- or dash-separated sequence (such as an IPv6 tail).
|
||||
str = a.anonymizeMACsInString(str, macColonRegex, ':')
|
||||
str = a.anonymizeMACsInString(str, macDashRegex, '-')
|
||||
|
||||
if a.level >= LevelStrict {
|
||||
str = wgKeyRegex.ReplaceAllStringFunc(str, a.AnonymizeWGKey)
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
// sortedDomains returns the domain mappings longest-first, so a full-FQDN
|
||||
// mapping (strict level) is applied before the base-domain mapping it
|
||||
// contains. The order is rebuilt only when domainAnonymizer has grown.
|
||||
func (a *Anonymizer) sortedDomains() []string {
|
||||
if len(a.domainOrder) == len(a.domainAnonymizer) {
|
||||
return a.domainOrder
|
||||
}
|
||||
|
||||
a.domainOrder = a.domainOrder[:0]
|
||||
for domain := range a.domainAnonymizer {
|
||||
a.domainOrder = append(a.domainOrder, domain)
|
||||
}
|
||||
slices.SortFunc(a.domainOrder, func(x, y string) int {
|
||||
if d := len(y) - len(x); d != 0 {
|
||||
return d
|
||||
}
|
||||
return strings.Compare(x, y)
|
||||
})
|
||||
return a.domainOrder
|
||||
}
|
||||
|
||||
// anonymizeMACsInString replaces MAC addresses matched by re, skipping
|
||||
// matches that directly adjoin another sep so a six-group run inside a longer
|
||||
// separated sequence is left alone.
|
||||
func (a *Anonymizer) anonymizeMACsInString(str string, re *regexp.Regexp, sep byte) string {
|
||||
matches := re.FindAllStringIndex(str, -1)
|
||||
if len(matches) == 0 {
|
||||
return str
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
last := 0
|
||||
for _, m := range matches {
|
||||
if (m[0] > 0 && str[m[0]-1] == sep) || (m[1] < len(str) && str[m[1]] == sep) {
|
||||
continue
|
||||
}
|
||||
b.WriteString(str[last:m[0]])
|
||||
b.WriteString(a.AnonymizeMAC(str[m[0]:m[1]]))
|
||||
last = m[1]
|
||||
}
|
||||
b.WriteString(str[last:])
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// AnonymizeSchemeURI finds and anonymizes URIs with ws, wss, rel, rels, stun, stuns, turn, and turns schemes.
|
||||
func (a *Anonymizer) AnonymizeSchemeURI(text string) string {
|
||||
re := regexp.MustCompile(`(?i)\b(wss?://|rels?://|stuns?:|turns?:|https?://)\S+\b`)
|
||||
@@ -513,79 +239,10 @@ func isWellKnown(addr netip.Addr) bool {
|
||||
"128.0.0.0", "8000::", // 2nd split subnet for default routes
|
||||
}
|
||||
|
||||
return slices.Contains(wellKnown, addr.String())
|
||||
}
|
||||
|
||||
// isInternal reports whether ip identifies a host only within the local
|
||||
// network: IPv4 private (RFC 1918), CGNAT (RFC 6598), and link-local (v4 and
|
||||
// v6). These are preserved at the default level so support can reason about
|
||||
// the real topology, and replaced from the internal pools at the strict
|
||||
// level. IPv6 ULA is deliberately not internal: its random global ID uniquely
|
||||
// fingerprints the network, so it is anonymized at every level.
|
||||
func isInternal(ip netip.Addr) bool {
|
||||
return (ip.Is4() && ip.IsPrivate()) ||
|
||||
ip.IsLinkLocalUnicast() ||
|
||||
isCGNAT(ip)
|
||||
}
|
||||
|
||||
func inPoolRange(ip, start, current netip.Addr) bool {
|
||||
return ip.Compare(start) >= 0 && ip.Compare(current) <= 0
|
||||
}
|
||||
|
||||
// isWellKnownMAC reports whether hw carries no stable host identity: all-zero
|
||||
// or a group address (broadcast and multicast).
|
||||
func isWellKnownMAC(hw net.HardwareAddr) bool {
|
||||
if hw[0]&1 == 1 {
|
||||
if slices.Contains(wellKnown, addr.String()) {
|
||||
return true
|
||||
}
|
||||
for _, b := range hw {
|
||||
if b != 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// looksLikeWGKey reports whether s has the shape of a WireGuard key:
|
||||
// 44 base64 characters decoding to 32 bytes.
|
||||
func looksLikeWGKey(s string) bool {
|
||||
if len(s) != 44 || s[43] != '=' {
|
||||
return false
|
||||
}
|
||||
decoded, err := base64.StdEncoding.DecodeString(s)
|
||||
return err == nil && len(decoded) == 32
|
||||
}
|
||||
|
||||
func generateAnonymousKey() string {
|
||||
buf := make([]byte, 32)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
return strings.Repeat("A", 43) + "="
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(buf)
|
||||
}
|
||||
|
||||
// protectedSuffix returns the protected NetBird suffix baseDomain ends with,
|
||||
// or empty. The match is label-anchored so an unrelated domain that merely
|
||||
// ends in the same characters is not preserved.
|
||||
func protectedSuffix(baseDomain string) string {
|
||||
for _, d := range protectedDomains {
|
||||
if baseDomain == d || strings.HasSuffix(baseDomain, "."+d) {
|
||||
return d
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func withTrailingDot(domain string, hasDot bool) string {
|
||||
if hasDot {
|
||||
return domain + "."
|
||||
}
|
||||
return domain
|
||||
}
|
||||
|
||||
// isCGNAT reports whether addr is in 100.64.0.0/10 (RFC 6598), the range
|
||||
// NetBird assigns overlay peer addresses from.
|
||||
func isCGNAT(addr netip.Addr) bool {
|
||||
cgnatRangeStart := netip.AddrFrom4([4]byte{100, 64, 0, 0})
|
||||
cgnatRange := netip.PrefixFrom(cgnatRangeStart, 10)
|
||||
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
package anonymize_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"net/netip"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -47,301 +44,6 @@ func TestAnonymizeIP(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLevel(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expect anonymize.Level
|
||||
}{
|
||||
{"", anonymize.LevelDefault},
|
||||
{"default", anonymize.LevelDefault},
|
||||
{"DEFAULT", anonymize.LevelDefault},
|
||||
{"strict", anonymize.LevelStrict},
|
||||
{"STRICT", anonymize.LevelStrict},
|
||||
// Unknown values must never yield less anonymization than requested.
|
||||
{"garbage", anonymize.LevelStrict},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run("input="+tc.input, func(t *testing.T) {
|
||||
assert.Equal(t, tc.expect, anonymize.ParseLevel(tc.input), "parsed level should match")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnonymizeIP_DefaultLevelInternalRanges(t *testing.T) {
|
||||
anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ip string
|
||||
expect string
|
||||
}{
|
||||
{"RFC1918 10/8", "10.1.2.3", "10.1.2.3"},
|
||||
{"RFC1918 172.16/12", "172.16.5.5", "172.16.5.5"},
|
||||
{"RFC1918 192.168/16", "192.168.1.1", "192.168.1.1"},
|
||||
{"CGNAT", "100.64.0.5", "100.64.0.5"},
|
||||
{"IPv4 link-local", "169.254.1.1", "169.254.1.1"},
|
||||
{"IPv6 link-local", "fe80::1", "fe80::1"},
|
||||
// ULA is anonymized even at the default level: its random global ID
|
||||
// uniquely fingerprints the network, unlike shared RFC 1918 space.
|
||||
{"IPv6 ULA", "fd12:3456:789a::1", "2001:db8:ffff::"},
|
||||
// 4-in-6 addresses classify like their unmapped IPv4 form.
|
||||
{"4-in-6 RFC1918", "::ffff:192.168.1.1", "192.168.1.1"},
|
||||
{"4-in-6 CGNAT", "::ffff:100.64.0.5", "100.64.0.5"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := anonymizer.AnonymizeIP(netip.MustParseAddr(tc.ip))
|
||||
assert.Equal(t, tc.expect, result.String(), "default level should preserve internal ranges except ULA")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnonymizeIP_StrictLevel(t *testing.T) {
|
||||
anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
|
||||
anonymizer.SetLevel(anonymize.LevelStrict)
|
||||
|
||||
// Order matters: internal pool addresses are assigned sequentially.
|
||||
tests := []struct {
|
||||
name string
|
||||
ip string
|
||||
expect string
|
||||
}{
|
||||
{"RFC1918 192.168/16", "192.168.1.1", "198.18.0.0"},
|
||||
{"Second RFC1918", "192.168.1.2", "198.18.0.1"},
|
||||
{"Repeated RFC1918", "192.168.1.1", "198.18.0.0"},
|
||||
{"RFC1918 10/8", "10.1.2.3", "198.18.0.2"},
|
||||
{"RFC1918 172.16/12", "172.16.5.5", "198.18.0.3"},
|
||||
{"CGNAT", "100.64.0.5", "198.18.0.4"},
|
||||
{"IPv4 link-local", "169.254.1.1", "198.18.0.5"},
|
||||
{"Public IPv4 uses public pool", "1.2.3.4", "198.51.100.0"},
|
||||
{"IPv6 link-local", "fe80::1", "2001:db8:1::"},
|
||||
{"IPv6 ULA", "fd12:3456:789a::1", "2001:db8:1::1"},
|
||||
{"Public IPv6 uses public pool", "2607:f8b0:4005:805::200e", "2001:db8:ffff::"},
|
||||
{"Loopback IPv4", "127.0.0.1", "127.0.0.1"},
|
||||
{"Loopback IPv6", "::1", "::1"},
|
||||
{"Unspecified", "0.0.0.0", "0.0.0.0"},
|
||||
{"Multicast", "224.0.0.251", "224.0.0.251"},
|
||||
{"Well known resolver", "8.8.8.8", "8.8.8.8"},
|
||||
{"Well known split marker", "128.0.0.0", "128.0.0.0"},
|
||||
{"In internal pool range", "198.18.0.3", "198.18.0.3"},
|
||||
{"In public pool range", "198.51.100.0", "198.51.100.0"},
|
||||
{"4-in-6 repeated RFC1918", "::ffff:192.168.1.1", "198.18.0.0"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := anonymizer.AnonymizeIP(netip.MustParseAddr(tc.ip))
|
||||
assert.Equal(t, tc.expect, result.String(), "strict level should replace internal ranges from the internal pools")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnonymizeString_StrictInternalIPs(t *testing.T) {
|
||||
anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
|
||||
anonymizer.SetLevel(anonymize.LevelStrict)
|
||||
|
||||
input := "route 10.20.30.0/24 via 192.168.1.1 dev eth0 src 100.64.0.7"
|
||||
firstPass := anonymizer.AnonymizeString(input)
|
||||
secondPass := anonymizer.AnonymizeString(firstPass)
|
||||
|
||||
assert.NotContains(t, firstPass, "10.20.30.0", "private network address should be anonymized")
|
||||
assert.NotContains(t, firstPass, "192.168.1.1", "private gateway should be anonymized")
|
||||
assert.NotContains(t, firstPass, "100.64.0.7", "CGNAT address should be anonymized")
|
||||
assert.Contains(t, firstPass, "/24", "prefix length should be preserved")
|
||||
assert.Equal(t, firstPass, secondPass, "second pass should not further anonymize the string")
|
||||
}
|
||||
|
||||
func TestAnonymizeMAC(t *testing.T) {
|
||||
anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
|
||||
|
||||
first := anonymizer.AnonymizeMAC("aa:bb:cc:dd:ee:0f")
|
||||
assert.Equal(t, "02:00:00:00:00:01", first, "first MAC should get the first placeholder")
|
||||
assert.Equal(t, first, anonymizer.AnonymizeMAC("aa:bb:cc:dd:ee:0f"), "repeated MAC should map to the same placeholder")
|
||||
assert.Equal(t, first, anonymizer.AnonymizeMAC("AA:BB:CC:DD:EE:0F"), "case should not affect the mapping")
|
||||
assert.Equal(t, "02-00-00-00-00-01", anonymizer.AnonymizeMAC("AA-BB-CC-DD-EE-0F"), "dash form should keep its separator but share the mapping")
|
||||
|
||||
second := anonymizer.AnonymizeMAC("10:22:33:44:55:66")
|
||||
assert.Equal(t, "02:00:00:00:00:02", second, "second distinct MAC should get the next placeholder")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
mac string
|
||||
}{
|
||||
{"Broadcast", "ff:ff:ff:ff:ff:ff"},
|
||||
{"IPv4 multicast", "01:00:5e:00:00:fb"},
|
||||
{"IPv6 multicast", "33:33:00:00:00:01"},
|
||||
{"All zero", "00:00:00:00:00:00"},
|
||||
{"Assigned placeholder", "02:00:00:00:00:01"},
|
||||
{"Invalid", "not-a-mac"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.mac, anonymizer.AnonymizeMAC(tc.mac), "should be preserved")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnonymizeString_MACAddresses(t *testing.T) {
|
||||
anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
name: "nftables ether rule",
|
||||
input: "ether saddr aa:bb:cc:dd:ee:ff drop",
|
||||
expect: "ether saddr 02:00:00:00:00:01 drop",
|
||||
},
|
||||
{
|
||||
name: "Windows dash form",
|
||||
input: "Physical Address : AA-BB-CC-DD-EE-FF",
|
||||
expect: "Physical Address : 02-00-00-00-00-01",
|
||||
},
|
||||
{
|
||||
name: "IPv6 address tail is not treated as MAC",
|
||||
input: "addr fe80:0:11:22:33:44:55:66 scope link",
|
||||
expect: "addr fe80:0:11:22:33:44:55:66 scope link",
|
||||
},
|
||||
{
|
||||
name: "broadcast MAC preserved",
|
||||
input: "dst ff:ff:ff:ff:ff:ff type ARP",
|
||||
expect: "dst ff:ff:ff:ff:ff:ff type ARP",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := anonymizer.AnonymizeString(tc.input)
|
||||
assert.Equal(t, tc.expect, result, "MAC addresses should be anonymized at every level")
|
||||
assert.Equal(t, result, anonymizer.AnonymizeString(result), "second pass should not change the result")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnonymizeWGKey(t *testing.T) {
|
||||
key := base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{0x42}, 32))
|
||||
|
||||
t.Run("default level preserves keys", func(t *testing.T) {
|
||||
anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
|
||||
assert.Equal(t, key, anonymizer.AnonymizeWGKey(key), "default level should not touch WireGuard keys")
|
||||
})
|
||||
|
||||
t.Run("strict level replaces keys", func(t *testing.T) {
|
||||
anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
|
||||
anonymizer.SetLevel(anonymize.LevelStrict)
|
||||
|
||||
anon := anonymizer.AnonymizeWGKey(key)
|
||||
assert.NotEqual(t, key, anon, "strict level should replace the key")
|
||||
assert.Regexp(t, `^[A-Za-z0-9+/]{43}=$`, anon, "placeholder should keep the WireGuard key shape")
|
||||
assert.Equal(t, anon, anonymizer.AnonymizeWGKey(key), "repeated key should map to the same placeholder")
|
||||
assert.Equal(t, anon, anonymizer.AnonymizeWGKey(anon), "an assigned placeholder should pass through unchanged")
|
||||
|
||||
assert.Equal(t, "not-a-key", anonymizer.AnonymizeWGKey("not-a-key"), "non-key values should be preserved")
|
||||
})
|
||||
}
|
||||
|
||||
func TestAnonymizeString_WGKeys(t *testing.T) {
|
||||
key := base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{0x42}, 32))
|
||||
input := "peer " + key + " handshake completed"
|
||||
|
||||
t.Run("default level preserves keys", func(t *testing.T) {
|
||||
anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
|
||||
assert.Equal(t, input, anonymizer.AnonymizeString(input), "default level should not touch WireGuard keys in strings")
|
||||
})
|
||||
|
||||
t.Run("strict level replaces keys", func(t *testing.T) {
|
||||
anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
|
||||
anonymizer.SetLevel(anonymize.LevelStrict)
|
||||
|
||||
firstPass := anonymizer.AnonymizeString(input)
|
||||
assert.NotContains(t, firstPass, key, "the key should not survive strict anonymization")
|
||||
assert.Equal(t, anonymizer.AnonymizeWGKey(key), extractKey(t, firstPass), "string replacement should be consistent with AnonymizeWGKey")
|
||||
assert.Equal(t, firstPass, anonymizer.AnonymizeString(firstPass), "second pass should not change the result")
|
||||
})
|
||||
}
|
||||
|
||||
func extractKey(t *testing.T, logLine string) string {
|
||||
t.Helper()
|
||||
fields := strings.Fields(logLine)
|
||||
require.Len(t, fields, 4, "log line should keep its structure")
|
||||
return fields[1]
|
||||
}
|
||||
|
||||
func TestAnonymizeDomain_StrictLevel(t *testing.T) {
|
||||
anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
|
||||
anonymizer.SetLevel(anonymize.LevelStrict)
|
||||
|
||||
t.Run("netbird peer name", func(t *testing.T) {
|
||||
result := anonymizer.AnonymizeDomain("my-laptop.netbird.cloud")
|
||||
assert.Regexp(t, `^peer-\d+\.netbird\.cloud$`, result, "peer name should be anonymized, suffix kept")
|
||||
assert.NotContains(t, result, "my-laptop", "the peer name should not survive")
|
||||
assert.Equal(t, result, anonymizer.AnonymizeDomain("my-laptop.netbird.cloud"), "repeated domain should map consistently")
|
||||
assert.Equal(t, result, anonymizer.AnonymizeDomain(result), "an anonymized domain should pass through unchanged")
|
||||
})
|
||||
|
||||
t.Run("bare netbird domain", func(t *testing.T) {
|
||||
assert.Equal(t, "netbird.cloud", anonymizer.AnonymizeDomain("netbird.cloud"), "the bare protected suffix should be preserved")
|
||||
})
|
||||
|
||||
t.Run("netbird infrastructure preserved", func(t *testing.T) {
|
||||
assert.Equal(t, "api.netbird.io", anonymizer.AnonymizeDomain("api.netbird.io"),
|
||||
"netbird.io hosts infrastructure, not peer names, and should stay readable")
|
||||
})
|
||||
|
||||
t.Run("leading labels of other domains", func(t *testing.T) {
|
||||
result := anonymizer.AnonymizeDomain("host1.corp.example.com")
|
||||
assert.Regexp(t, `^host-\d+\.host-\d+\.anon-[a-zA-Z0-9]+\.domain$`, result, "every label should be anonymized")
|
||||
for _, label := range []string{"host1", "corp", "example"} {
|
||||
assert.NotContains(t, result, label, "no original label should survive")
|
||||
}
|
||||
assert.Equal(t, result, anonymizer.AnonymizeDomain("host1.corp.example.com"), "repeated domain should map consistently")
|
||||
})
|
||||
|
||||
t.Run("same label maps consistently across domains", func(t *testing.T) {
|
||||
first := anonymizer.AnonymizeDomain("shared.one.com")
|
||||
second := anonymizer.AnonymizeDomain("shared.two.com")
|
||||
assert.Equal(t, strings.Split(first, ".")[0], strings.Split(second, ".")[0], "the shared host label should get one placeholder")
|
||||
})
|
||||
|
||||
t.Run("wildcard label preserved", func(t *testing.T) {
|
||||
result := anonymizer.AnonymizeDomain("*.example.com")
|
||||
assert.Regexp(t, `^\*\.anon-[a-zA-Z0-9]+\.domain$`, result, "the wildcard label should stay a wildcard")
|
||||
})
|
||||
}
|
||||
|
||||
func TestAnonymizeDomain_DefaultLevelKeepsPeerNames(t *testing.T) {
|
||||
anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
|
||||
|
||||
assert.Equal(t, "my-laptop.netbird.cloud", anonymizer.AnonymizeDomain("my-laptop.netbird.cloud"),
|
||||
"default level should preserve netbird FQDNs including the peer name")
|
||||
assert.Regexp(t, `^sub\.anon-[a-zA-Z0-9]+\.domain$`, anonymizer.AnonymizeDomain("sub.example.com"),
|
||||
"default level should keep subdomain labels")
|
||||
}
|
||||
|
||||
func TestAnonymizeString_StrictPeerNames(t *testing.T) {
|
||||
anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
|
||||
anonymizer.SetLevel(anonymize.LevelStrict)
|
||||
|
||||
// Seed like the bundle generator does from the status: base first, then
|
||||
// the full FQDN, so replacement must prefer the longer mapping.
|
||||
anonBase := anonymizer.AnonymizeDomain("example.com")
|
||||
anonPeer := anonymizer.AnonymizeDomain("peer1.netbird.cloud")
|
||||
anonHost := anonymizer.AnonymizeDomain("host1.example.com")
|
||||
|
||||
logLine := "connected to peer1.netbird.cloud via host1.example.com endpoint"
|
||||
firstPass := anonymizer.AnonymizeString(logLine)
|
||||
assert.NotContains(t, firstPass, "peer1", "the peer name should not survive in logs")
|
||||
assert.NotContains(t, firstPass, "host1", "the host label should not survive in logs")
|
||||
assert.Contains(t, firstPass, anonPeer, "the seeded peer mapping should be applied")
|
||||
assert.Contains(t, firstPass, anonHost, "the seeded host mapping should be applied, not just the base mapping")
|
||||
assert.NotContains(t, firstPass, "host1."+anonBase, "the base mapping must not preempt the longer FQDN mapping")
|
||||
assert.Equal(t, firstPass, anonymizer.AnonymizeString(firstPass), "second pass should not change the result")
|
||||
}
|
||||
|
||||
func TestAnonymizeDNSLogLine(t *testing.T) {
|
||||
anonymizer := anonymize.NewAnonymizer(netip.Addr{}, netip.Addr{})
|
||||
tests := []struct {
|
||||
|
||||
@@ -27,8 +27,8 @@ import (
|
||||
const errCloseConnection = "Failed to close connection: %v"
|
||||
|
||||
var (
|
||||
logFileCount uint32
|
||||
systemInfoFlag bool
|
||||
logFileCount uint32
|
||||
systemInfoFlag bool
|
||||
uploadBundleFlag bool
|
||||
uploadBundleURLFlag string
|
||||
uploadBundleInsecureFlag bool
|
||||
@@ -156,11 +156,6 @@ func debugConfigDump(cmd *cobra.Command, _ []string) error {
|
||||
// request. Returns an error if the RPC fails or if the daemon reports
|
||||
// an upload failure reason.
|
||||
func debugBundle(cmd *cobra.Command, _ []string) error {
|
||||
anonymizeEnabled, anonymizeLevel, err := effectiveAnonymize()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conn, err := getClient(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -173,11 +168,10 @@ func debugBundle(cmd *cobra.Command, _ []string) error {
|
||||
|
||||
client := proto.NewDaemonServiceClient(conn)
|
||||
request := &proto.DebugBundleRequest{
|
||||
Anonymize: anonymizeEnabled,
|
||||
AnonymizeLevel: anonymizeLevel.String(),
|
||||
SystemInfo: systemInfoFlag,
|
||||
LogFileCount: logFileCount,
|
||||
CliVersion: version.NetbirdVersion(),
|
||||
Anonymize: anonymizeFlag,
|
||||
SystemInfo: systemInfoFlag,
|
||||
LogFileCount: logFileCount,
|
||||
CliVersion: version.NetbirdVersion(),
|
||||
}
|
||||
if uploadBundleFlag {
|
||||
request.UploadURL = uploadBundleURLFlag
|
||||
@@ -235,11 +229,6 @@ func runForDuration(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("invalid duration format: %v", err)
|
||||
}
|
||||
|
||||
anonymizeEnabled, anonymizeLevel, err := effectiveAnonymize()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conn, err := getClient(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -379,11 +368,10 @@ func runForDuration(cmd *cobra.Command, args []string) error {
|
||||
cmd.Println("Creating debug bundle...")
|
||||
|
||||
request := &proto.DebugBundleRequest{
|
||||
Anonymize: anonymizeEnabled,
|
||||
AnonymizeLevel: anonymizeLevel.String(),
|
||||
SystemInfo: systemInfoFlag,
|
||||
LogFileCount: logFileCount,
|
||||
CliVersion: version.NetbirdVersion(),
|
||||
Anonymize: anonymizeFlag,
|
||||
SystemInfo: systemInfoFlag,
|
||||
LogFileCount: logFileCount,
|
||||
CliVersion: version.NetbirdVersion(),
|
||||
}
|
||||
if uploadBundleFlag {
|
||||
request.UploadURL = uploadBundleURLFlag
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"github.com/spf13/pflag"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/netbirdio/netbird/client/anonymize"
|
||||
daddr "github.com/netbirdio/netbird/client/internal/daemonaddr"
|
||||
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
||||
)
|
||||
@@ -70,7 +69,6 @@ var (
|
||||
autoConnectDisabled bool
|
||||
extraIFaceBlackList []string
|
||||
anonymizeFlag bool
|
||||
anonymizeLevelFlag string
|
||||
dnsRouteInterval time.Duration
|
||||
// lazyConnEnabled is the parse target for the deprecated --enable-lazy-connection
|
||||
// flag. The flag is inert; the value is no longer read (use NB_LAZY_CONN instead).
|
||||
@@ -158,8 +156,7 @@ func init() {
|
||||
rootCmd.MarkFlagsMutuallyExclusive("setup-key", "setup-key-file")
|
||||
rootCmd.PersistentFlags().StringVar(&preSharedKey, preSharedKeyFlag, "", "Sets WireGuard PreSharedKey property. If set, then only peers that have the same key can communicate.")
|
||||
rootCmd.PersistentFlags().StringVarP(&hostName, "hostname", "n", "", "Sets a custom hostname for the device")
|
||||
rootCmd.PersistentFlags().BoolVarP(&anonymizeFlag, "anonymize", "A", false, "anonymize public IP addresses, MAC addresses, and non-netbird.io domains in logs and status output; private, CGNAT, and link-local IP ranges are kept (see --anonymize-level strict)")
|
||||
rootCmd.PersistentFlags().StringVar(&anonymizeLevelFlag, "anonymize-level", "", "anonymization level: \"default\" or \"strict\"; strict also anonymizes private, CGNAT, and link-local IP ranges, peer names, and WireGuard public keys. Setting this flag implies --anonymize")
|
||||
rootCmd.PersistentFlags().BoolVarP(&anonymizeFlag, "anonymize", "A", false, "anonymize IP addresses and non-netbird.io domains in logs and status output")
|
||||
rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", profilemanager.DefaultConfigPath, "Overrides the default profile file location")
|
||||
|
||||
rootCmd.AddCommand(upCmd)
|
||||
@@ -296,19 +293,6 @@ var CLIBackOffSettings = &backoff.ExponentialBackOff{
|
||||
Clock: backoff.SystemClock,
|
||||
}
|
||||
|
||||
// effectiveAnonymize resolves the --anonymize and --anonymize-level flags:
|
||||
// setting a level implies anonymization, and an invalid level is rejected.
|
||||
func effectiveAnonymize() (bool, anonymize.Level, error) {
|
||||
if anonymizeLevelFlag == "" {
|
||||
return anonymizeFlag, anonymize.LevelDefault, nil
|
||||
}
|
||||
level := anonymize.ParseLevel(anonymizeLevelFlag)
|
||||
if !strings.EqualFold(anonymizeLevelFlag, level.String()) {
|
||||
return false, anonymize.LevelDefault, fmt.Errorf("invalid anonymize level %q: use %q or %q", anonymizeLevelFlag, anonymize.LevelDefault.String(), anonymize.LevelStrict.String())
|
||||
}
|
||||
return true, level, nil
|
||||
}
|
||||
|
||||
func getSetupKey() (string, error) {
|
||||
if setupKeyPath != "" && setupKey == "" {
|
||||
return getSetupKeyFromFile(setupKeyPath)
|
||||
|
||||
@@ -121,14 +121,8 @@ func statusFunc(cmd *cobra.Command, args []string) error {
|
||||
sessionExpiresAt = ts.AsTime().UTC()
|
||||
}
|
||||
|
||||
anonymizeEnabled, anonymizeLevel, err := effectiveAnonymize()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var outputInformationHolder = nbstatus.ConvertToStatusOutputOverview(resp.GetFullStatus(), nbstatus.ConvertOptions{
|
||||
Anonymize: anonymizeEnabled,
|
||||
AnonymizeLevel: anonymizeLevel,
|
||||
Anonymize: anonymizeFlag,
|
||||
DaemonVersion: resp.GetDaemonVersion(),
|
||||
DaemonStatus: nbstatus.ParseDaemonStatus(status),
|
||||
StatusFilter: statusFilter,
|
||||
|
||||
@@ -34,8 +34,9 @@ import (
|
||||
"github.com/netbirdio/netbird/shared/netiputil"
|
||||
)
|
||||
|
||||
const readmeContent = `This debug bundle contains the following files.
|
||||
If anonymization is enabled (--anonymize / --anonymize-level), the files are anonymized to protect sensitive information.
|
||||
const readmeContent = `Netbird debug bundle
|
||||
This debug bundle contains the following files.
|
||||
If the --anonymize flag is set, the files are anonymized to protect sensitive information.
|
||||
|
||||
status.txt: Anonymized status information of the NetBird client.
|
||||
client.log: Most recent, anonymized client log file of the NetBird client.
|
||||
@@ -69,34 +70,21 @@ capture.pcap: Packet capture in pcap format. Only present when capture was runni
|
||||
|
||||
|
||||
Anonymization Process
|
||||
The files in this bundle have been anonymized to protect sensitive information. The level applied to this bundle is recorded at the top of this file. Here's how the anonymization was applied:
|
||||
The files in this bundle have been anonymized to protect sensitive information. Here's how the anonymization was applied:
|
||||
|
||||
IP Addresses
|
||||
|
||||
Default level:
|
||||
- Public IPv4 addresses are replaced with addresses starting from 198.51.100.0
|
||||
- Public IPv6 addresses are replaced with addresses starting from 2001:db8:ffff::
|
||||
- IPv6 unique local addresses (fc00::/7) are anonymized as well: their random global ID uniquely identifies the network.
|
||||
- IP addresses from internal IPv4 ranges and well-known addresses are not anonymized (e.g. 8.8.8.8, 100.64.0.0/10, addresses starting with 192.168., 172.16., 10., 169.254., fe80::).
|
||||
|
||||
Strict level (--anonymize-level strict), in addition to the default level:
|
||||
- Private (RFC 1918), CGNAT (100.64.0.0/10), and link-local (169.254.0.0/16, fe80::/10) addresses are anonymized too.
|
||||
- Internal IPv4 addresses are replaced with addresses starting from 198.18.0.0 and internal IPv6 addresses with addresses starting from 2001:db8:1::, so internal addresses remain distinguishable from public ones.
|
||||
- Addresses are mapped in order of first appearance: subnet structure, allocation scheme, and gateway conventions are not preserved. Prefix lengths of networks are preserved.
|
||||
- Peer names in front of NetBird domains are replaced with numbered placeholders (e.g. peer-1.netbird.cloud), and subdomain labels of other domains with host-N placeholders.
|
||||
- WireGuard public keys are replaced with consistent placeholder keys.
|
||||
IPv4 addresses are replaced with addresses starting from 198.51.100.0
|
||||
IPv6 addresses are replaced with addresses starting from 100::
|
||||
|
||||
IP addresses from non public ranges and well known addresses are not anonymized (e.g. 8.8.8.8, 100.64.0.0/10, addresses starting with 192.168., 172.16., 10., etc.).
|
||||
Reoccuring IP addresses are replaced with the same anonymized address.
|
||||
|
||||
Note: The anonymized IP addresses in the status file do not match those in the log and routes files. However, the anonymized IP addresses are consistent within the status file and across the routes and log files.
|
||||
|
||||
MAC Addresses
|
||||
MAC addresses are replaced at every anonymization level with consistent placeholders counting up from 02:00:00:00:00:01. Broadcast, multicast, and all-zero addresses are kept. At the default level a preserved IPv6 link-local address may still embed a MAC address (EUI-64); the strict level anonymizes those addresses.
|
||||
|
||||
Domains
|
||||
All domain names (except for the netbird domains) are replaced with randomly generated strings ending in ".domain". Anonymized domains are consistent across all files in the bundle.
|
||||
Reoccuring domain names are replaced with the same anonymized domain.
|
||||
At the strict level, the peer name labels in front of netbird domains are anonymized as well.
|
||||
|
||||
Sync Response
|
||||
The network_map.json file contains the following anonymized information:
|
||||
@@ -293,7 +281,6 @@ type BundleGenerator struct {
|
||||
cliVersion string
|
||||
|
||||
anonymize bool
|
||||
anonymizeLevel anonymize.Level
|
||||
includeSystemInfo bool
|
||||
logFileCount uint32
|
||||
|
||||
@@ -301,10 +288,7 @@ type BundleGenerator struct {
|
||||
}
|
||||
|
||||
type BundleConfig struct {
|
||||
Anonymize bool
|
||||
// AnonymizeLevel selects how much the anonymizer redacts.
|
||||
// anonymize.LevelStrict implies Anonymize.
|
||||
AnonymizeLevel anonymize.Level
|
||||
Anonymize bool
|
||||
IncludeSystemInfo bool
|
||||
LogFileCount uint32
|
||||
}
|
||||
@@ -343,11 +327,8 @@ func NewBundleGenerator(deps GeneratorDependencies, cfg BundleConfig) *BundleGen
|
||||
uiLogOpener = openLogFile
|
||||
}
|
||||
|
||||
anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
|
||||
anonymizer.SetLevel(cfg.AnonymizeLevel)
|
||||
|
||||
return &BundleGenerator{
|
||||
anonymizer: anonymizer,
|
||||
anonymizer: anonymize.NewAnonymizer(anonymize.DefaultAddresses()),
|
||||
|
||||
internalConfig: deps.InternalConfig,
|
||||
statusRecorder: deps.StatusRecorder,
|
||||
@@ -364,8 +345,7 @@ func NewBundleGenerator(deps GeneratorDependencies, cfg BundleConfig) *BundleGen
|
||||
daemonVersion: deps.DaemonVersion,
|
||||
cliVersion: deps.CliVersion,
|
||||
|
||||
anonymize: cfg.Anonymize || cfg.AnonymizeLevel >= anonymize.LevelStrict,
|
||||
anonymizeLevel: cfg.AnonymizeLevel,
|
||||
anonymize: cfg.Anonymize,
|
||||
includeSystemInfo: cfg.IncludeSystemInfo,
|
||||
logFileCount: logFileCount,
|
||||
}
|
||||
@@ -505,13 +485,7 @@ func (g *BundleGenerator) addSystemInfo() {
|
||||
}
|
||||
|
||||
func (g *BundleGenerator) addReadme() error {
|
||||
level := "none (anonymization disabled)"
|
||||
if g.anonymize {
|
||||
level = g.anonymizeLevel.String()
|
||||
}
|
||||
header := fmt.Sprintf("Netbird debug bundle\nAnonymization level applied to this bundle: %s\n", level)
|
||||
|
||||
readmeReader := strings.NewReader(header + readmeContent)
|
||||
readmeReader := strings.NewReader(readmeContent)
|
||||
if err := g.addFileToZip(readmeReader, "README.txt"); err != nil {
|
||||
return fmt.Errorf("add README file to zip: %w", err)
|
||||
}
|
||||
@@ -533,10 +507,9 @@ func (g *BundleGenerator) addStatus() error {
|
||||
fullStatus := g.statusRecorder.GetFullStatus()
|
||||
protoFullStatus := nbstatus.ToProtoFullStatus(fullStatus)
|
||||
overview := nbstatus.ConvertToStatusOutputOverview(protoFullStatus, nbstatus.ConvertOptions{
|
||||
Anonymize: g.anonymize,
|
||||
AnonymizeLevel: g.anonymizeLevel,
|
||||
ProfileName: profName,
|
||||
DaemonVersion: g.daemonVersion,
|
||||
Anonymize: g.anonymize,
|
||||
ProfileName: profName,
|
||||
DaemonVersion: g.daemonVersion,
|
||||
})
|
||||
overview.CliVersion = g.cliVersion
|
||||
statusOutput := overview.FullDetailSummary()
|
||||
@@ -689,7 +662,7 @@ func (g *BundleGenerator) addCommonConfigFields(configContent *strings.Builder)
|
||||
configContent.WriteString("NetBird Client Configuration:\n\n")
|
||||
|
||||
if key, err := wgtypes.ParseKey(g.internalConfig.PrivateKey); err == nil {
|
||||
configContent.WriteString(fmt.Sprintf("PublicKey: %s\n", g.anonymizer.AnonymizeWGKey(key.PublicKey().String())))
|
||||
configContent.WriteString(fmt.Sprintf("PublicKey: %s\n", key.PublicKey().String()))
|
||||
}
|
||||
configContent.WriteString(fmt.Sprintf("WgIface: %s\n", g.internalConfig.WgIface))
|
||||
configContent.WriteString(fmt.Sprintf("WgPort: %d\n", g.internalConfig.WgPort))
|
||||
@@ -979,11 +952,6 @@ func (g *BundleGenerator) addUpdateLogs() error {
|
||||
}
|
||||
|
||||
baseName := filepath.Base(logFile)
|
||||
data, err = g.anonymizeBytes(data)
|
||||
if err != nil {
|
||||
log.Warnf("skipping update log file %s: %v", baseName, err)
|
||||
continue
|
||||
}
|
||||
if err := g.addFileToZip(bytes.NewReader(data), filepath.Join("update-logs", baseName)); err != nil {
|
||||
return fmt.Errorf("add update log file %s to zip: %w", baseName, err)
|
||||
}
|
||||
@@ -1011,13 +979,6 @@ func (g *BundleGenerator) addCorruptedStateFiles() error {
|
||||
}
|
||||
|
||||
fileName := filepath.Base(match)
|
||||
// Corrupted state files usually fail structured JSON anonymization,
|
||||
// so run them through the string anonymizer instead.
|
||||
data, err = g.anonymizeBytes(data)
|
||||
if err != nil {
|
||||
log.Warnf("skipping corrupted state file %s: %v", fileName, err)
|
||||
continue
|
||||
}
|
||||
if err := g.addFileToZip(bytes.NewReader(data), "corrupted_states/"+fileName); err != nil {
|
||||
log.Warnf("Failed to add corrupted state file %s to zip: %v", fileName, err)
|
||||
continue
|
||||
@@ -1029,27 +990,6 @@ func (g *BundleGenerator) addCorruptedStateFiles() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// anonymizeBytes runs raw file content through the string anonymizer line by
|
||||
// line when anonymization is enabled. It errors instead of returning partial
|
||||
// content, so a caller never adds an unanonymized fallback to the bundle.
|
||||
func (g *BundleGenerator) anonymizeBytes(data []byte) ([]byte, error) {
|
||||
if !g.anonymize {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
scanner := bufio.NewScanner(bytes.NewReader(data))
|
||||
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
|
||||
for scanner.Scan() {
|
||||
buf.WriteString(g.anonymizer.AnonymizeString(scanner.Text()))
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("anonymize content: %w", err)
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (g *BundleGenerator) addMetrics() error {
|
||||
if g.clientMetrics == nil {
|
||||
log.Debugf("skipping metrics in debug bundle: no metrics collector")
|
||||
@@ -1522,7 +1462,6 @@ func anonymizeRemotePeer(peer *mgmProto.RemotePeerConfig, anonymizer *anonymize.
|
||||
}
|
||||
|
||||
peer.Fqdn = anonymizer.AnonymizeDomain(peer.Fqdn)
|
||||
peer.WgPubKey = anonymizer.AnonymizeWGKey(peer.WgPubKey)
|
||||
|
||||
anonymizeSSHConfig(peer.SshConfig)
|
||||
}
|
||||
|
||||
@@ -35,14 +35,14 @@ func (g *BundleGenerator) toWGShowFormat(s *configurer.Stats) string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(fmt.Sprintf("interface: %s\n", s.DeviceName))
|
||||
sb.WriteString(fmt.Sprintf(" public key: %s\n", g.anonymizer.AnonymizeWGKey(s.PublicKey)))
|
||||
sb.WriteString(fmt.Sprintf(" public key: %s\n", s.PublicKey))
|
||||
sb.WriteString(fmt.Sprintf(" listen port: %d\n", s.ListenPort))
|
||||
if s.FWMark != 0 {
|
||||
sb.WriteString(fmt.Sprintf(" fwmark: %#x\n", s.FWMark))
|
||||
}
|
||||
|
||||
for _, peer := range s.Peers {
|
||||
sb.WriteString(fmt.Sprintf("\npeer: %s\n", g.anonymizer.AnonymizeWGKey(peer.PublicKey)))
|
||||
sb.WriteString(fmt.Sprintf("\npeer: %s\n", peer.PublicKey))
|
||||
if peer.Endpoint.IP != nil {
|
||||
if g.anonymize {
|
||||
anonEndpoint := g.anonymizer.AnonymizeUDPAddr(peer.Endpoint)
|
||||
@@ -54,11 +54,7 @@ func (g *BundleGenerator) toWGShowFormat(s *configurer.Stats) string {
|
||||
if len(peer.AllowedIPs) > 0 {
|
||||
var ipStrings []string
|
||||
for _, ipnet := range peer.AllowedIPs {
|
||||
ipStr := ipnet.String()
|
||||
if g.anonymize {
|
||||
ipStr = g.anonymizer.AnonymizeIPString(ipStr)
|
||||
}
|
||||
ipStrings = append(ipStrings, ipStr)
|
||||
ipStrings = append(ipStrings, ipnet.String())
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf(" allowed ips: %s\n", strings.Join(ipStrings, ", ")))
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"golang.zx2c4.com/wireguard/tun/netstack"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
|
||||
"github.com/netbirdio/netbird/client/anonymize"
|
||||
nberrors "github.com/netbirdio/netbird/client/errors"
|
||||
"github.com/netbirdio/netbird/client/firewall"
|
||||
"github.com/netbirdio/netbird/client/firewall/firewalld"
|
||||
@@ -1386,7 +1385,6 @@ func (e *Engine) handleBundle(params *mgmProto.BundleParameters) (*mgmProto.JobR
|
||||
|
||||
bundleJobParams := debug.BundleConfig{
|
||||
Anonymize: params.Anonymize,
|
||||
AnonymizeLevel: anonymize.ParseLevel(params.AnonymizeLevel),
|
||||
IncludeSystemInfo: true,
|
||||
LogFileCount: uint32(params.LogFileCount),
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
nbAnonymize "github.com/netbirdio/netbird/client/anonymize"
|
||||
"github.com/netbirdio/netbird/client/internal"
|
||||
"github.com/netbirdio/netbird/client/internal/auth"
|
||||
"github.com/netbirdio/netbird/client/internal/debug"
|
||||
@@ -201,10 +200,8 @@ func (c *Client) Stop() {
|
||||
// DebugBundle generates a debug bundle, uploads it and returns the upload key.
|
||||
// It works with or without a running engine: when the engine is up it reuses
|
||||
// the live config, sync response and client metrics; otherwise it loads the
|
||||
// config from disk (or the preloaded tvOS config). anonymizeLevel is "default"
|
||||
// or "strict"; strict also anonymizes internal IP ranges, peer names, and
|
||||
// WireGuard public keys, and implies anonymize.
|
||||
func (c *Client) DebugBundle(anonymize bool, anonymizeLevel string) (string, error) {
|
||||
// config from disk (or the preloaded tvOS config).
|
||||
func (c *Client) DebugBundle(anonymize bool) (string, error) {
|
||||
cfg, cc := c.stateSnapshot()
|
||||
|
||||
// If the engine hasn't been started, load config so we can reach management.
|
||||
@@ -254,7 +251,6 @@ func (c *Client) DebugBundle(anonymize bool, anonymizeLevel string) (string, err
|
||||
deps,
|
||||
debug.BundleConfig{
|
||||
Anonymize: anonymize,
|
||||
AnonymizeLevel: nbAnonymize.ParseLevel(anonymizeLevel),
|
||||
IncludeSystemInfo: true,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -2781,11 +2781,6 @@ type DebugBundleRequest struct {
|
||||
// untrusted TLS certificate. Restricted to privileged callers; for
|
||||
// self-hosted upload servers.
|
||||
UploadInsecure bool `protobuf:"varint,7,opt,name=uploadInsecure,proto3" json:"uploadInsecure,omitempty"`
|
||||
// anonymizeLevel selects how much the anonymizer redacts: "default"
|
||||
// (or empty) keeps internal IP ranges, "strict" also anonymizes them.
|
||||
// Unknown values are treated as "strict". Only meaningful with anonymize;
|
||||
// "strict" implies it.
|
||||
AnonymizeLevel string `protobuf:"bytes,8,opt,name=anonymizeLevel,proto3" json:"anonymizeLevel,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -2862,13 +2857,6 @@ func (x *DebugBundleRequest) GetUploadInsecure() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *DebugBundleRequest) GetAnonymizeLevel() string {
|
||||
if x != nil {
|
||||
return x.AnonymizeLevel
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type DebugBundleResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"`
|
||||
@@ -7265,7 +7253,7 @@ const file_daemon_proto_rawDesc = "" +
|
||||
"\x12translatedHostname\x18\x04 \x01(\tR\x12translatedHostname\x128\n" +
|
||||
"\x0etranslatedPort\x18\x05 \x01(\v2\x10.daemon.PortInfoR\x0etranslatedPort\"G\n" +
|
||||
"\x17ForwardingRulesResponse\x12,\n" +
|
||||
"\x05rules\x18\x01 \x03(\v2\x16.daemon.ForwardingRuleR\x05rules\"\x84\x02\n" +
|
||||
"\x05rules\x18\x01 \x03(\v2\x16.daemon.ForwardingRuleR\x05rules\"\xdc\x01\n" +
|
||||
"\x12DebugBundleRequest\x12\x1c\n" +
|
||||
"\tanonymize\x18\x01 \x01(\bR\tanonymize\x12\x1e\n" +
|
||||
"\n" +
|
||||
@@ -7276,8 +7264,7 @@ const file_daemon_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"cliVersion\x18\x06 \x01(\tR\n" +
|
||||
"cliVersion\x12&\n" +
|
||||
"\x0euploadInsecure\x18\a \x01(\bR\x0euploadInsecure\x12&\n" +
|
||||
"\x0eanonymizeLevel\x18\b \x01(\tR\x0eanonymizeLevel\"}\n" +
|
||||
"\x0euploadInsecure\x18\a \x01(\bR\x0euploadInsecure\"}\n" +
|
||||
"\x13DebugBundleResponse\x12\x12\n" +
|
||||
"\x04path\x18\x01 \x01(\tR\x04path\x12 \n" +
|
||||
"\vuploadedKey\x18\x02 \x01(\tR\vuploadedKey\x120\n" +
|
||||
|
||||
@@ -540,11 +540,6 @@ message DebugBundleRequest {
|
||||
// untrusted TLS certificate. Restricted to privileged callers; for
|
||||
// self-hosted upload servers.
|
||||
bool uploadInsecure = 7;
|
||||
// anonymizeLevel selects how much the anonymizer redacts: "default"
|
||||
// (or empty) keeps internal IP ranges, "strict" also anonymizes them.
|
||||
// Unknown values are treated as "strict". Only meaningful with anonymize;
|
||||
// "strict" implies it.
|
||||
string anonymizeLevel = 8;
|
||||
}
|
||||
|
||||
message DebugBundleResponse {
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"google.golang.org/grpc/codes"
|
||||
gstatus "google.golang.org/grpc/status"
|
||||
|
||||
"github.com/netbirdio/netbird/client/anonymize"
|
||||
"github.com/netbirdio/netbird/client/internal/debug"
|
||||
"github.com/netbirdio/netbird/client/internal/ipcauth"
|
||||
"github.com/netbirdio/netbird/client/proto"
|
||||
@@ -123,7 +122,6 @@ func (s *Server) generateDebugBundle(req *proto.DebugBundleRequest, uiOpener deb
|
||||
},
|
||||
debug.BundleConfig{
|
||||
Anonymize: req.GetAnonymize(),
|
||||
AnonymizeLevel: anonymize.ParseLevel(req.GetAnonymizeLevel()),
|
||||
IncludeSystemInfo: req.GetSystemInfo(),
|
||||
LogFileCount: req.GetLogFileCount(),
|
||||
},
|
||||
|
||||
@@ -46,10 +46,7 @@ func ParseDaemonStatus(s string) DaemonStatus {
|
||||
|
||||
// ConvertOptions holds parameters for ConvertToStatusOutputOverview.
|
||||
type ConvertOptions struct {
|
||||
Anonymize bool
|
||||
// AnonymizeLevel selects how much the anonymizer redacts. Only
|
||||
// meaningful when Anonymize is set.
|
||||
AnonymizeLevel anonymize.Level
|
||||
Anonymize bool
|
||||
DaemonVersion string
|
||||
DaemonStatus DaemonStatus
|
||||
StatusFilter string
|
||||
@@ -220,7 +217,6 @@ func ConvertToStatusOutputOverview(pbFullStatus *proto.FullStatus, opts ConvertO
|
||||
|
||||
if opts.Anonymize {
|
||||
anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
|
||||
anonymizer.SetLevel(opts.AnonymizeLevel)
|
||||
anonymizeOverview(anonymizer, &overview)
|
||||
}
|
||||
|
||||
@@ -980,7 +976,6 @@ func timeAgo(t time.Time) string {
|
||||
|
||||
func anonymizePeerDetail(a *anonymize.Anonymizer, peer *PeerStateDetailOutput) {
|
||||
peer.FQDN = a.AnonymizeDomain(peer.FQDN)
|
||||
peer.PubKey = a.AnonymizeWGKey(peer.PubKey)
|
||||
if localIP, port, err := net.SplitHostPort(peer.IceCandidateEndpoint.Local); err == nil {
|
||||
peer.IceCandidateEndpoint.Local = fmt.Sprintf("%s:%s", a.AnonymizeIPString(localIP), port)
|
||||
}
|
||||
@@ -1012,7 +1007,6 @@ func anonymizeOverview(a *anonymize.Anonymizer, overview *OutputOverview) {
|
||||
overview.SignalState.URL = a.AnonymizeURI(overview.SignalState.URL)
|
||||
overview.SignalState.Error = a.AnonymizeString(overview.SignalState.Error)
|
||||
|
||||
overview.PubKey = a.AnonymizeWGKey(overview.PubKey)
|
||||
overview.IP = a.AnonymizeIPString(overview.IP)
|
||||
overview.IPv6 = a.AnonymizeIPString(overview.IPv6)
|
||||
for i, detail := range overview.Relays.Details {
|
||||
|
||||
@@ -71,12 +71,10 @@ type BundleOptions = {
|
||||
hasWindow: boolean;
|
||||
totalSec: number;
|
||||
uploadUrl: string;
|
||||
anonymizeLevel: AnonymizeLevel;
|
||||
anonymize: boolean;
|
||||
systemInfo: boolean;
|
||||
};
|
||||
|
||||
export type AnonymizeLevel = "none" | "default" | "strict";
|
||||
|
||||
const startCaptureBestEffort = async (totalSec: number, pcap: CaptureState) => {
|
||||
try {
|
||||
// Mirror the CLI's safety margin: window + 30s, server caps at 10m.
|
||||
@@ -189,10 +187,7 @@ const runBundleFlow = async (
|
||||
|
||||
if (opts.uploadUrl) setStage({ kind: "uploading" });
|
||||
const result = await DebugSvc.Bundle({
|
||||
anonymize: opts.anonymizeLevel !== "none",
|
||||
// The daemon only knows "default" and "strict"; "none" is expressed
|
||||
// through the anonymize flag being off.
|
||||
anonymizeLevel: opts.anonymizeLevel === "strict" ? "strict" : "default",
|
||||
anonymize: opts.anonymize,
|
||||
systemInfo: opts.systemInfo,
|
||||
uploadUrl: opts.uploadUrl,
|
||||
logFileCount,
|
||||
@@ -203,7 +198,7 @@ const runBundleFlow = async (
|
||||
};
|
||||
|
||||
const useDebugBundle = () => {
|
||||
const [anonymizeLevel, setAnonymizeLevel] = useState<AnonymizeLevel>("none");
|
||||
const [anonymize, setAnonymize] = useState(false);
|
||||
const [systemInfo, setSystemInfo] = useState(true);
|
||||
const [upload, setUpload] = useState(true);
|
||||
const [trace, setTrace] = useState(true);
|
||||
@@ -245,7 +240,7 @@ const useDebugBundle = () => {
|
||||
hasWindow: capture && totalSec > 0,
|
||||
totalSec,
|
||||
uploadUrl: upload ? NETBIRD_UPLOAD_URL : "",
|
||||
anonymizeLevel,
|
||||
anonymize,
|
||||
systemInfo,
|
||||
};
|
||||
|
||||
@@ -277,8 +272,8 @@ const useDebugBundle = () => {
|
||||
};
|
||||
|
||||
return {
|
||||
anonymizeLevel,
|
||||
setAnonymizeLevel,
|
||||
anonymize,
|
||||
setAnonymize,
|
||||
systemInfo,
|
||||
setSystemInfo,
|
||||
upload,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useId, type ReactNode } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { ChevronDown, CircleCheckBig, FolderOpen, Info, Loader2 } from "lucide-react";
|
||||
import { CircleCheckBig, FolderOpen, Loader2 } from "lucide-react";
|
||||
import { Browser } from "@wailsio/runtime";
|
||||
import { Debug as DebugSvc } from "@bindings/services";
|
||||
import type { DebugBundleResult } from "@bindings/services/models.js";
|
||||
@@ -8,22 +8,13 @@ import { Button } from "@/components/buttons/Button";
|
||||
import { DialogActions } from "@/components/dialog/DialogActions";
|
||||
import { DialogDescription } from "@/components/dialog/DialogDescription";
|
||||
import { DialogHeading } from "@/components/dialog/DialogHeading";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/DropdownMenu";
|
||||
import FancyToggleSwitch from "@/components/switches/FancyToggleSwitch";
|
||||
import HelpText from "@/components/typography/HelpText.tsx";
|
||||
import { Input } from "@/components/inputs/Input";
|
||||
import { Label } from "@/components/typography/Label";
|
||||
import { SquareIcon } from "@/components/SquareIcon";
|
||||
import { Tooltip } from "@/components/Tooltip";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { formatRemaining } from "@/lib/formatters";
|
||||
import type { AnonymizeLevel, DebugStage } from "@/contexts/DebugBundleContext";
|
||||
import type { DebugStage } from "@/contexts/DebugBundleContext";
|
||||
import { useDebugBundleContext } from "@/contexts/DebugBundleContext";
|
||||
import { SectionGroup, SettingsBottomBar } from "@/modules/settings/SettingsSection.tsx";
|
||||
|
||||
@@ -33,8 +24,8 @@ export function SettingsTroubleshooting() {
|
||||
const { t } = useTranslation();
|
||||
const durationId = useId();
|
||||
const {
|
||||
anonymizeLevel,
|
||||
setAnonymizeLevel,
|
||||
anonymize,
|
||||
setAnonymize,
|
||||
systemInfo,
|
||||
setSystemInfo,
|
||||
upload,
|
||||
@@ -64,71 +55,12 @@ export function SettingsTroubleshooting() {
|
||||
|
||||
return (
|
||||
<SectionGroup title={t("settings.troubleshooting.section.title")}>
|
||||
<div className={"flex items-center justify-between gap-6"}>
|
||||
<div className={"max-w-md flex-1"}>
|
||||
<Label as={"div"}>
|
||||
<span className={"inline-flex items-center gap-1.5"}>
|
||||
{t("settings.troubleshooting.anonymize.label")}
|
||||
<Tooltip
|
||||
content={
|
||||
<div className={"max-w-xs whitespace-normal leading-relaxed"}>
|
||||
{t("settings.troubleshooting.anonymize.info")}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Info
|
||||
size={14}
|
||||
aria-label={t("settings.troubleshooting.anonymize.label")}
|
||||
className={"shrink-0 cursor-default text-nb-gray-400"}
|
||||
/>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</Label>
|
||||
<HelpText margin={false}>
|
||||
{t("settings.troubleshooting.anonymize.help")}
|
||||
</HelpText>
|
||||
</div>
|
||||
<div className={"shrink-0"}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type={"button"}
|
||||
aria-label={t("settings.troubleshooting.anonymize.label")}
|
||||
className={cn(
|
||||
"inline-flex h-[40px] min-w-[160px] items-center justify-between gap-2 px-3",
|
||||
"rounded-md border bg-white dark:bg-nb-gray-900",
|
||||
"border-neutral-200 dark:border-nb-gray-700",
|
||||
"cursor-default text-xs font-semibold text-nb-gray-100 outline-none",
|
||||
"hover:border-nb-gray-600 data-[state=open]:border-nb-gray-600",
|
||||
)}
|
||||
>
|
||||
{t(`settings.troubleshooting.anonymize.${anonymizeLevel}`)}
|
||||
<ChevronDown
|
||||
size={16}
|
||||
aria-hidden={"true"}
|
||||
className={"shrink-0 text-nb-gray-200"}
|
||||
/>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align={"end"} className={"min-w-[160px]"}>
|
||||
<DropdownMenuRadioGroup
|
||||
value={anonymizeLevel}
|
||||
onValueChange={(v) => setAnonymizeLevel(v as AnonymizeLevel)}
|
||||
>
|
||||
<DropdownMenuRadioItem value={"none"}>
|
||||
{t("settings.troubleshooting.anonymize.none")}
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value={"default"}>
|
||||
{t("settings.troubleshooting.anonymize.default")}
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value={"strict"}>
|
||||
{t("settings.troubleshooting.anonymize.strict")}
|
||||
</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
<FancyToggleSwitch
|
||||
value={anonymize}
|
||||
onChange={setAnonymize}
|
||||
label={t("settings.troubleshooting.anonymize.label")}
|
||||
helpText={t("settings.troubleshooting.anonymize.help")}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={systemInfo}
|
||||
onChange={setSystemInfo}
|
||||
|
||||
@@ -1005,27 +1005,11 @@
|
||||
},
|
||||
"settings.troubleshooting.anonymize.label": {
|
||||
"message": "Anonymize Sensitive Information",
|
||||
"description": "Label for the anonymization level dropdown (None, Default, Strict)."
|
||||
"description": "Toggle label: anonymize sensitive information in the bundle."
|
||||
},
|
||||
"settings.troubleshooting.anonymize.help": {
|
||||
"message": "Hides IP addresses, domains, and other sensitive values.",
|
||||
"description": "Helper text under the anonymization dropdown. The level details live in the info tooltip."
|
||||
},
|
||||
"settings.troubleshooting.anonymize.info": {
|
||||
"message": "Default keeps internal IPv4 addresses and peer names readable for support. Strict additionally anonymizes private (RFC 1918), CGNAT, and link-local IP addresses, peer names, and WireGuard public keys. Recurring values map to the same placeholder, so peers stay distinguishable. Use Strict when sharing the bundle outside your organization.",
|
||||
"description": "Info tooltip explaining the anonymization levels. 'RFC 1918', 'CGNAT', 'link-local', and 'WireGuard' are technical terms — keep them."
|
||||
},
|
||||
"settings.troubleshooting.anonymize.none": {
|
||||
"message": "None",
|
||||
"description": "Dropdown option: no anonymization."
|
||||
},
|
||||
"settings.troubleshooting.anonymize.default": {
|
||||
"message": "Default",
|
||||
"description": "Dropdown option: default anonymization level."
|
||||
},
|
||||
"settings.troubleshooting.anonymize.strict": {
|
||||
"message": "Strict",
|
||||
"description": "Dropdown option: strict anonymization level."
|
||||
"message": "Hides public IP addresses and non-NetBird domains from logs.",
|
||||
"description": "Helper text for anonymizing logs (hides public IPs and non-NetBird domains)."
|
||||
},
|
||||
"settings.troubleshooting.systemInfo.label": {
|
||||
"message": "Include System Information",
|
||||
|
||||
@@ -49,5 +49,11 @@ func getCursorPosition(app *application.App) (application.Point, bool) {
|
||||
if app == nil || app.Screen == nil {
|
||||
return p, true
|
||||
}
|
||||
// The wails GTK3 backend caches screens from the active window; a tray app
|
||||
// has none at startup, so the cache is empty and PhysicalToDipPoint would
|
||||
// dereference a nil nearest screen. Raw pixels are correct there anyway.
|
||||
if app.Screen.ScreenNearestPhysicalPoint(p) == nil {
|
||||
return p, true
|
||||
}
|
||||
return app.Screen.PhysicalToDipPoint(p), true
|
||||
}
|
||||
|
||||
@@ -15,13 +15,10 @@ import (
|
||||
)
|
||||
|
||||
type DebugBundleParams struct {
|
||||
Anonymize bool `json:"anonymize"`
|
||||
// AnonymizeLevel is "default" or "strict"; strict also anonymizes
|
||||
// private IP ranges, peer names, and WireGuard public keys.
|
||||
AnonymizeLevel string `json:"anonymizeLevel"`
|
||||
SystemInfo bool `json:"systemInfo"`
|
||||
UploadURL string `json:"uploadUrl"`
|
||||
LogFileCount uint32 `json:"logFileCount"`
|
||||
Anonymize bool `json:"anonymize"`
|
||||
SystemInfo bool `json:"systemInfo"`
|
||||
UploadURL string `json:"uploadUrl"`
|
||||
LogFileCount uint32 `json:"logFileCount"`
|
||||
}
|
||||
|
||||
// DebugBundleResult: Path is set for local-only bundles, UploadedKey on upload
|
||||
@@ -51,12 +48,11 @@ func (s *Debug) Bundle(ctx context.Context, p DebugBundleParams) (DebugBundleRes
|
||||
return DebugBundleResult{}, err
|
||||
}
|
||||
resp, err := cli.DebugBundle(ctx, &proto.DebugBundleRequest{
|
||||
Anonymize: p.Anonymize,
|
||||
AnonymizeLevel: p.AnonymizeLevel,
|
||||
SystemInfo: p.SystemInfo,
|
||||
UploadURL: p.UploadURL,
|
||||
LogFileCount: p.LogFileCount,
|
||||
CliVersion: version.NetbirdVersion(),
|
||||
Anonymize: p.Anonymize,
|
||||
SystemInfo: p.SystemInfo,
|
||||
UploadURL: p.UploadURL,
|
||||
LogFileCount: p.LogFileCount,
|
||||
CliVersion: version.NetbirdVersion(),
|
||||
})
|
||||
if err != nil {
|
||||
return DebugBundleResult{}, err
|
||||
|
||||
40
client/ui/xembed_host_gtk3_linux.go
Normal file
40
client/ui/xembed_host_gtk3_linux.go
Normal file
@@ -0,0 +1,40 @@
|
||||
//go:build linux && gtk3 && !(linux && 386)
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
// The legacy GTK3 / WebKit2GTK 4.1 build (-tags gtk3) drops the in-process
|
||||
// XEmbed StatusNotifierWatcher entirely. The real implementation
|
||||
// (xembed_host_linux.go + xembed_tray_linux.c) links GTK4 and uses GTK4-only
|
||||
// popup-menu APIs that have no drop-in GTK3 equivalent, so rather than port the
|
||||
// C layer we stub the host out on gtk3 builds. The tray still works on every
|
||||
// desktop that ships its own StatusNotifierWatcher (KDE, GNOME+AppIndicator,
|
||||
// Cinnamon/xapp, XFCE, …); only the minimal-WM fallback (Fluxbox/OpenBox/i3/
|
||||
// dwm/vanilla GNOME) is unavailable on gtk3 packages. See LINUX-TRAY.md.
|
||||
|
||||
// xembedHost is a placeholder so the package compiles on gtk3 builds; the real
|
||||
// type (with X11/GTK4 state) lives in xembed_host_linux.go. It is never
|
||||
// instantiated here because xembedTrayAvailable always reports false.
|
||||
type xembedHost struct{}
|
||||
|
||||
// run satisfies the call in tray_watcher_linux.go; unreachable on gtk3 because
|
||||
// newXembedHost never returns a non-nil host.
|
||||
func (*xembedHost) run() {}
|
||||
|
||||
// xembedTrayAvailable always reports false on gtk3 builds, so the watcher probe
|
||||
// loop in startStatusNotifierWatcher exits immediately and newXembedHost is
|
||||
// never reached. recenter_linux.go's predicate becomes a harmless no-op too.
|
||||
func xembedTrayAvailable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// newXembedHost exists only to satisfy the reference in tray_watcher_linux.go;
|
||||
// it is unreachable because xembedTrayAvailable returns false on gtk3.
|
||||
func newXembedHost(conn *dbus.Conn, busName string, objPath dbus.ObjectPath) (*xembedHost, error) {
|
||||
return nil, errors.New("xembed host unsupported on gtk3 build")
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build linux && !(linux && 386)
|
||||
//go:build linux && !gtk3 && !(linux && 386)
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build linux && !gtk3 && !(linux && 386)
|
||||
|
||||
#include "xembed_tray_linux.h"
|
||||
|
||||
#include <X11/Xatom.h>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -110,10 +110,6 @@ message BundleParameters {
|
||||
int64 bundle_for_time = 2;
|
||||
int32 log_file_count = 3;
|
||||
bool anonymize = 4;
|
||||
// anonymize_level selects how much the anonymizer redacts: "default"
|
||||
// (or empty) keeps internal IP ranges, "strict" also anonymizes them.
|
||||
// Unknown values are treated as "strict".
|
||||
string anonymize_level = 5;
|
||||
}
|
||||
|
||||
message BundleResult {
|
||||
|
||||
Reference in New Issue
Block a user