diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml
index cf061f876..d585ba209 100644
--- a/.github/workflows/golang-test-linux.yml
+++ b/.github/workflows/golang-test-linux.yml
@@ -146,6 +146,65 @@ jobs:
- name: Test
run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} CI=true go test -tags devcert -exec 'sudo' -timeout 10m -p 1 $(go list ./... | grep -v -e /management -e /signal -e /relay)
+ test_client_on_docker:
+ name: "Client (Docker) / Unit"
+ needs: [build-cache]
+ runs-on: ubuntu-22.04
+ steps:
+ - name: Install Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: "1.23.x"
+ cache: false
+
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Get Go environment
+ id: go-env
+ run: |
+ echo "cache_dir=$(go env GOCACHE)" >> $GITHUB_OUTPUT
+ echo "modcache_dir=$(go env GOMODCACHE)" >> $GITHUB_OUTPUT
+
+ - name: Cache Go modules
+ uses: actions/cache/restore@v4
+ id: cache-restore
+ with:
+ path: |
+ ${{ steps.go-env.outputs.cache_dir }}
+ ${{ steps.go-env.outputs.modcache_dir }}
+ key: ${{ runner.os }}-gotest-cache-${{ hashFiles('**/go.sum') }}
+ restore-keys: |
+ ${{ runner.os }}-gotest-cache-
+
+ - name: Run tests in container
+ env:
+ HOST_GOCACHE: ${{ steps.go-env.outputs.cache_dir }}
+ HOST_GOMODCACHE: ${{ steps.go-env.outputs.modcache_dir }}
+ run: |
+ CONTAINER_GOCACHE="/root/.cache/go-build"
+ CONTAINER_GOMODCACHE="/go/pkg/mod"
+
+ docker run --rm \
+ --cap-add=NET_ADMIN \
+ --privileged \
+ -v $PWD:/app \
+ -w /app \
+ -v "${HOST_GOCACHE}:${CONTAINER_GOCACHE}" \
+ -v "${HOST_GOMODCACHE}:${CONTAINER_GOMODCACHE}" \
+ -e CGO_ENABLED=1 \
+ -e CI=true \
+ -e DOCKER_CI=true \
+ -e GOARCH=${GOARCH_TARGET} \
+ -e GOCACHE=${CONTAINER_GOCACHE} \
+ -e GOMODCACHE=${CONTAINER_GOMODCACHE} \
+ golang:1.23-alpine \
+ sh -c ' \
+ apk update; apk add --no-cache \
+ ca-certificates iptables ip6tables dbus dbus-dev libpcap-dev build-base; \
+ go test -buildvcs=false -tags devcert -v -timeout 10m -p 1 $(go list -buildvcs=false ./... | grep -v -e /management -e /signal -e /relay -e /client/ui -e /upload-server)
+ '
+
test_relay:
name: "Relay / Unit"
needs: [build-cache]
@@ -179,13 +238,6 @@ jobs:
restore-keys: |
${{ runner.os }}-gotest-cache-
- - name: Install dependencies
- run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev
-
- - name: Install 32-bit libpcap
- if: matrix.arch == '386'
- run: sudo dpkg --add-architecture i386 && sudo apt update && sudo apt-get install -y libpcap0.8-dev:i386
-
- name: Install modules
run: go mod tidy
@@ -232,13 +284,6 @@ jobs:
restore-keys: |
${{ runner.os }}-gotest-cache-
- - name: Install dependencies
- run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev
-
- - name: Install 32-bit libpcap
- if: matrix.arch == '386'
- run: sudo dpkg --add-architecture i386 && sudo apt update && sudo apt-get install -y libpcap0.8-dev:i386
-
- name: Install modules
run: go mod tidy
@@ -286,13 +331,6 @@ jobs:
restore-keys: |
${{ runner.os }}-gotest-cache-
- - name: Install dependencies
- run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev
-
- - name: Install 32-bit libpcap
- if: matrix.arch == '386'
- run: sudo dpkg --add-architecture i386 && sudo apt update && sudo apt-get install -y libpcap0.8-dev:i386
-
- name: Install modules
run: go mod tidy
@@ -314,6 +352,7 @@ jobs:
run: |
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
NETBIRD_STORE_ENGINE=${{ matrix.store }} \
+ CI=true \
go test -tags=devcert \
-exec "sudo --preserve-env=CI,NETBIRD_STORE_ENGINE" \
-timeout 20m ./management/...
@@ -353,13 +392,6 @@ jobs:
restore-keys: |
${{ runner.os }}-gotest-cache-
- - name: Install dependencies
- run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev
-
- - name: Install 32-bit libpcap
- if: matrix.arch == '386'
- run: sudo dpkg --add-architecture i386 && sudo apt update && sudo apt-get install -y libpcap0.8-dev:i386
-
- name: Install modules
run: go mod tidy
@@ -380,10 +412,11 @@ jobs:
- name: Test
run: |
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
- NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true \
+ NETBIRD_STORE_ENGINE=${{ matrix.store }} \
+ CI=true \
go test -tags devcert -run=^$ -bench=. \
-exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' \
- -timeout 20m ./...
+ -timeout 20m ./management/...
api_benchmark:
name: "Management / Benchmark (API)"
@@ -396,6 +429,33 @@ jobs:
store: [ 'sqlite', 'postgres' ]
runs-on: ubuntu-22.04
steps:
+ - name: Create Docker network
+ run: docker network create promnet
+
+ - name: Start Prometheus Pushgateway
+ run: docker run -d --name pushgateway --network promnet -p 9091:9091 prom/pushgateway
+
+ - name: Start Prometheus (for Pushgateway forwarding)
+ run: |
+ echo '
+ global:
+ scrape_interval: 15s
+ scrape_configs:
+ - job_name: "pushgateway"
+ static_configs:
+ - targets: ["pushgateway:9091"]
+ remote_write:
+ - url: ${{ secrets.GRAFANA_URL }}
+ basic_auth:
+ username: ${{ secrets.GRAFANA_USER }}
+ password: ${{ secrets.GRAFANA_API_KEY }}
+ ' > prometheus.yml
+
+ docker run -d --name prometheus --network promnet \
+ -v $PWD/prometheus.yml:/etc/prometheus/prometheus.yml \
+ -p 9090:9090 \
+ prom/prometheus
+
- name: Install Go
uses: actions/setup-go@v5
with:
@@ -420,13 +480,6 @@ jobs:
restore-keys: |
${{ runner.os }}-gotest-cache-
- - name: Install dependencies
- run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev
-
- - name: Install 32-bit libpcap
- if: matrix.arch == '386'
- run: sudo dpkg --add-architecture i386 && sudo apt update && sudo apt-get install -y libpcap0.8-dev:i386
-
- name: Install modules
run: go mod tidy
@@ -447,11 +500,13 @@ jobs:
- name: Test
run: |
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
- NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true \
+ NETBIRD_STORE_ENGINE=${{ matrix.store }} \
+ CI=true \
+ GIT_BRANCH=${{ github.ref_name }} \
go test -tags=benchmark \
-run=^$ \
-bench=. \
- -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' \
+ -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,GIT_BRANCH,GITHUB_RUN_ID' \
-timeout 20m ./management/...
api_integration_test:
@@ -489,13 +544,6 @@ jobs:
restore-keys: |
${{ runner.os }}-gotest-cache-
- - name: Install dependencies
- run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev
-
- - name: Install 32-bit libpcap
- if: matrix.arch == '386'
- run: sudo dpkg --add-architecture i386 && sudo apt update && sudo apt-get install -y libpcap0.8-dev:i386
-
- name: Install modules
run: go mod tidy
@@ -505,89 +553,8 @@ jobs:
- name: Test
run: |
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
- NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true \
+ NETBIRD_STORE_ENGINE=${{ matrix.store }} \
+ CI=true \
go test -tags=integration \
-exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' \
-timeout 20m ./management/...
-
- test_client_on_docker:
- name: "Client (Docker) / Unit"
- needs: [ build-cache ]
- runs-on: ubuntu-20.04
- steps:
- - name: Install Go
- uses: actions/setup-go@v5
- with:
- go-version: "1.23.x"
- cache: false
-
- - name: Checkout code
- uses: actions/checkout@v4
-
- - name: Get Go environment
- run: |
- echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
- echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
-
- - name: Cache Go modules
- uses: actions/cache/restore@v4
- with:
- path: |
- ${{ env.cache }}
- ${{ env.modcache }}
- key: ${{ runner.os }}-gotest-cache-${{ hashFiles('**/go.sum') }}
- restore-keys: |
- ${{ runner.os }}-gotest-cache-
-
- - name: Install dependencies
- run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev
-
- - name: Install modules
- run: go mod tidy
-
- - name: check git status
- run: git --no-pager diff --exit-code
-
- - name: Generate Shared Sock Test bin
- run: CGO_ENABLED=0 go test -c -o sharedsock-testing.bin ./sharedsock
-
- - name: Generate RouteManager Test bin
- run: CGO_ENABLED=0 go test -c -o routemanager-testing.bin ./client/internal/routemanager
-
- - name: Generate SystemOps Test bin
- run: CGO_ENABLED=1 go test -c -o systemops-testing.bin -tags netgo -ldflags '-w -extldflags "-static -ldbus-1 -lpcap"' ./client/internal/routemanager/systemops
-
- - name: Generate nftables Manager Test bin
- run: CGO_ENABLED=0 go test -c -o nftablesmanager-testing.bin ./client/firewall/nftables/...
-
- - name: Generate Engine Test bin
- run: CGO_ENABLED=1 go test -c -o engine-testing.bin ./client/internal
-
- - name: Generate Peer Test bin
- run: CGO_ENABLED=0 go test -c -o peer-testing.bin ./client/internal/peer/
-
- - run: chmod +x *testing.bin
-
- - name: Run Shared Sock tests in docker
- run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/sharedsock --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/sharedsock-testing.bin -test.timeout 5m -test.parallel 1
-
- - name: Run Iface tests in docker
- run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/netbird -v /tmp/cache:/tmp/cache -v /tmp/modcache:/tmp/modcache -w /netbird -e GOCACHE=/tmp/cache -e GOMODCACHE=/tmp/modcache -e CGO_ENABLED=0 golang:1.23-alpine go test -test.timeout 5m -test.parallel 1 ./client/iface/...
-
- - name: Run RouteManager tests in docker
- run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal/routemanager --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/routemanager-testing.bin -test.timeout 5m -test.parallel 1
-
- - name: Run SystemOps tests in docker
- run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal/routemanager/systemops --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/systemops-testing.bin -test.timeout 5m -test.parallel 1
-
- - name: Run nftables Manager tests in docker
- run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/firewall --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/nftablesmanager-testing.bin -test.timeout 5m -test.parallel 1
-
- - name: Run Engine tests in docker with file store
- run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal -e NETBIRD_STORE_ENGINE="jsonfile" --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/engine-testing.bin -test.timeout 5m -test.parallel 1
-
- - name: Run Engine tests in docker with sqlite store
- run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal -e NETBIRD_STORE_ENGINE="sqlite" --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/engine-testing.bin -test.timeout 5m -test.parallel 1
-
- - name: Run Peer tests in docker
- run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal/peer --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/peer-testing.bin -test.timeout 5m -test.parallel 1
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index d6479763e..112659d1c 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -96,6 +96,20 @@ builds:
- -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 }}"
+ - id: netbird-upload
+ dir: upload-server
+ env: [CGO_ENABLED=0]
+ binary: netbird-upload
+ goos:
+ - linux
+ goarch:
+ - amd64
+ - arm64
+ - arm
+ 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 }}"
+
universal_binaries:
- id: netbird
@@ -409,6 +423,52 @@ dockers:
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=dev@netbird.io"
+ - image_templates:
+ - netbirdio/upload:{{ .Version }}-amd64
+ ids:
+ - netbird-upload
+ goarch: amd64
+ use: buildx
+ dockerfile: upload-server/Dockerfile
+ build_flag_templates:
+ - "--platform=linux/amd64"
+ - "--label=org.opencontainers.image.created={{.Date}}"
+ - "--label=org.opencontainers.image.title={{.ProjectName}}"
+ - "--label=org.opencontainers.image.version={{.Version}}"
+ - "--label=org.opencontainers.image.revision={{.FullCommit}}"
+ - "--label=org.opencontainers.image.version={{.Version}}"
+ - "--label=maintainer=dev@netbird.io"
+ - image_templates:
+ - netbirdio/upload:{{ .Version }}-arm64v8
+ ids:
+ - netbird-upload
+ goarch: arm64
+ use: buildx
+ dockerfile: upload-server/Dockerfile
+ build_flag_templates:
+ - "--platform=linux/arm64"
+ - "--label=org.opencontainers.image.created={{.Date}}"
+ - "--label=org.opencontainers.image.title={{.ProjectName}}"
+ - "--label=org.opencontainers.image.version={{.Version}}"
+ - "--label=org.opencontainers.image.revision={{.FullCommit}}"
+ - "--label=org.opencontainers.image.version={{.Version}}"
+ - "--label=maintainer=dev@netbird.io"
+ - image_templates:
+ - netbirdio/upload:{{ .Version }}-arm
+ ids:
+ - netbird-upload
+ goarch: arm
+ goarm: 6
+ use: buildx
+ dockerfile: upload-server/Dockerfile
+ build_flag_templates:
+ - "--platform=linux/arm"
+ - "--label=org.opencontainers.image.created={{.Date}}"
+ - "--label=org.opencontainers.image.title={{.ProjectName}}"
+ - "--label=org.opencontainers.image.version={{.Version}}"
+ - "--label=org.opencontainers.image.revision={{.FullCommit}}"
+ - "--label=org.opencontainers.image.version={{.Version}}"
+ - "--label=maintainer=dev@netbird.io"
docker_manifests:
- name_template: netbirdio/netbird:{{ .Version }}
image_templates:
@@ -475,7 +535,17 @@ docker_manifests:
- netbirdio/management:{{ .Version }}-debug-arm64v8
- netbirdio/management:{{ .Version }}-debug-arm
- netbirdio/management:{{ .Version }}-debug-amd64
+ - name_template: netbirdio/upload:{{ .Version }}
+ image_templates:
+ - netbirdio/upload:{{ .Version }}-arm64v8
+ - netbirdio/upload:{{ .Version }}-arm
+ - netbirdio/upload:{{ .Version }}-amd64
+ - name_template: netbirdio/upload:latest
+ image_templates:
+ - netbirdio/upload:{{ .Version }}-arm64v8
+ - netbirdio/upload:{{ .Version }}-arm
+ - netbirdio/upload:{{ .Version }}-amd64
brews:
- ids:
- default
diff --git a/README.md b/README.md
index 4ab9db03b..e0f2df848 100644
--- a/README.md
+++ b/README.md
@@ -61,7 +61,7 @@
|----|----|----|----|----|
|
| - - \[x] [Admin Web UI](https://github.com/netbirdio/dashboard)
| - - \[x] [SSO & MFA support](https://docs.netbird.io/how-to/installation#running-net-bird-with-sso-login)
| - - \[x] [Public API](https://docs.netbird.io/api)
| |
| - - \[x] Peer-to-peer connections
| - - \[x] Auto peer discovery and configuration
| - - \[x] [Access control - groups & rules](https://docs.netbird.io/how-to/manage-network-access)
| - - \[x] [Setup keys for bulk network provisioning](https://docs.netbird.io/how-to/register-machines-using-setup-keys)
| - - \[x] Mac
|
-| - - \[x] Connection relay fallback
| - - \[x] [IdP integrations](https://docs.netbird.io/selfhosted/identity-providers)
| - - \[x] [Activity logging](https://docs.netbird.io/how-to/monitor-system-and-network-activity)
| - - \[x] [Self-hosting quickstart script](https://docs.netbird.io/selfhosted/selfhosted-quickstart)
| - - \[x] Windows
|
+| - - \[x] Connection relay fallback
| - - \[x] [IdP integrations](https://docs.netbird.io/selfhosted/identity-providers)
| - - \[x] [Activity logging](https://docs.netbird.io/how-to/audit-events-logging)
| - - \[x] [Self-hosting quickstart script](https://docs.netbird.io/selfhosted/selfhosted-quickstart)
| - - \[x] Windows
|
| - - \[x] [Routes to external networks](https://docs.netbird.io/how-to/routing-traffic-to-private-networks)
| - - \[x] [Private DNS](https://docs.netbird.io/how-to/manage-dns-in-your-network)
| - - \[x] [Device posture checks](https://docs.netbird.io/how-to/manage-posture-checks)
| - - \[x] IdP groups sync with JWT
| - - \[x] Android
|
| - - \[x] NAT traversal with BPF
| - - \[x] [Multiuser support](https://docs.netbird.io/how-to/add-users-to-your-network)
| - - \[x] Peer-to-peer encryption
|| - - \[x] iOS
|
||| - - \[x] [Quantum-resistance with Rosenpass](https://netbird.io/knowledge-hub/the-first-quantum-resistant-mesh-vpn)
|| - - \[x] OpenWRT
|
diff --git a/client/Dockerfile b/client/Dockerfile
index 35c1d04c2..16b2916c7 100644
--- a/client/Dockerfile
+++ b/client/Dockerfile
@@ -1,5 +1,6 @@
FROM alpine:3.21.3
-RUN apk add --no-cache ca-certificates iptables ip6tables
+# iproute2: busybox doesn't display ip rules properly
+RUN apk add --no-cache ca-certificates ip6tables iproute2 iptables
ENV NB_FOREGROUND_MODE=true
ENTRYPOINT [ "/usr/local/bin/netbird","up"]
-COPY netbird /usr/local/bin/netbird
\ No newline at end of file
+COPY netbird /usr/local/bin/netbird
diff --git a/client/cmd/debug.go b/client/cmd/debug.go
index c02f60aed..b4adee826 100644
--- a/client/cmd/debug.go
+++ b/client/cmd/debug.go
@@ -11,9 +11,12 @@ import (
"google.golang.org/grpc/status"
"github.com/netbirdio/netbird/client/internal"
+ "github.com/netbirdio/netbird/client/internal/debug"
+ "github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/client/server"
nbstatus "github.com/netbirdio/netbird/client/status"
+ mgmProto "github.com/netbirdio/netbird/management/proto"
)
const errCloseConnection = "Failed to close connection: %v"
@@ -84,16 +87,27 @@ func debugBundle(cmd *cobra.Command, _ []string) error {
}()
client := proto.NewDaemonServiceClient(conn)
- resp, err := client.DebugBundle(cmd.Context(), &proto.DebugBundleRequest{
+ request := &proto.DebugBundleRequest{
Anonymize: anonymizeFlag,
Status: getStatusOutput(cmd, anonymizeFlag),
SystemInfo: debugSystemInfoFlag,
- })
+ }
+ if debugUploadBundle {
+ request.UploadURL = debugUploadBundleURL
+ }
+ resp, err := client.DebugBundle(cmd.Context(), request)
if err != nil {
return fmt.Errorf("failed to bundle debug: %v", status.Convert(err).Message())
}
+ cmd.Printf("Local file:\n%s\n", resp.GetPath())
- cmd.Println(resp.GetPath())
+ if resp.GetUploadFailureReason() != "" {
+ return fmt.Errorf("upload failed: %s", resp.GetUploadFailureReason())
+ }
+
+ if debugUploadBundle {
+ cmd.Printf("Upload file key:\n%s\n", resp.GetUploadedKey())
+ }
return nil
}
@@ -208,12 +222,15 @@ func runForDuration(cmd *cobra.Command, args []string) error {
headerPreDown := fmt.Sprintf("----- Netbird pre-down - Timestamp: %s - Duration: %s", time.Now().Format(time.RFC3339), duration)
statusOutput = fmt.Sprintf("%s\n%s\n%s", statusOutput, headerPreDown, getStatusOutput(cmd, anonymizeFlag))
-
- resp, err := client.DebugBundle(cmd.Context(), &proto.DebugBundleRequest{
+ request := &proto.DebugBundleRequest{
Anonymize: anonymizeFlag,
Status: statusOutput,
SystemInfo: debugSystemInfoFlag,
- })
+ }
+ if debugUploadBundle {
+ request.UploadURL = debugUploadBundleURL
+ }
+ resp, err := client.DebugBundle(cmd.Context(), request)
if err != nil {
return fmt.Errorf("failed to bundle debug: %v", status.Convert(err).Message())
}
@@ -239,7 +256,15 @@ func runForDuration(cmd *cobra.Command, args []string) error {
cmd.Println("Log level restored to", initialLogLevel.GetLevel())
}
- cmd.Println(resp.GetPath())
+ cmd.Printf("Local file:\n%s\n", resp.GetPath())
+
+ if resp.GetUploadFailureReason() != "" {
+ return fmt.Errorf("upload failed: %s", resp.GetUploadFailureReason())
+ }
+
+ if debugUploadBundle {
+ cmd.Printf("Upload file key:\n%s\n", resp.GetUploadedKey())
+ }
return nil
}
@@ -326,3 +351,34 @@ func formatDuration(d time.Duration) string {
s := d / time.Second
return fmt.Sprintf("%02d:%02d:%02d", h, m, s)
}
+
+func generateDebugBundle(config *internal.Config, recorder *peer.Status, connectClient *internal.ConnectClient, logFilePath string) {
+ var networkMap *mgmProto.NetworkMap
+ var err error
+
+ if connectClient != nil {
+ networkMap, err = connectClient.GetLatestNetworkMap()
+ if err != nil {
+ log.Warnf("Failed to get latest network map: %v", err)
+ }
+ }
+
+ bundleGenerator := debug.NewBundleGenerator(
+ debug.GeneratorDependencies{
+ InternalConfig: config,
+ StatusRecorder: recorder,
+ NetworkMap: networkMap,
+ LogFile: logFilePath,
+ },
+ debug.BundleConfig{
+ IncludeSystemInfo: true,
+ },
+ )
+
+ path, err := bundleGenerator.Generate()
+ if err != nil {
+ log.Errorf("Failed to generate debug bundle: %v", err)
+ return
+ }
+ log.Infof("Generated debug bundle from SIGUSR1 at: %s", path)
+}
diff --git a/client/cmd/debug_unix.go b/client/cmd/debug_unix.go
new file mode 100644
index 000000000..45ace7e13
--- /dev/null
+++ b/client/cmd/debug_unix.go
@@ -0,0 +1,39 @@
+//go:build unix
+
+package cmd
+
+import (
+ "context"
+ "os"
+ "os/signal"
+ "syscall"
+
+ log "github.com/sirupsen/logrus"
+
+ "github.com/netbirdio/netbird/client/internal"
+ "github.com/netbirdio/netbird/client/internal/peer"
+)
+
+func SetupDebugHandler(
+ ctx context.Context,
+ config *internal.Config,
+ recorder *peer.Status,
+ connectClient *internal.ConnectClient,
+ logFilePath string,
+) {
+ usr1Ch := make(chan os.Signal, 1)
+
+ signal.Notify(usr1Ch, syscall.SIGUSR1)
+
+ go func() {
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case <-usr1Ch:
+ log.Info("Received SIGUSR1. Triggering debug bundle generation.")
+ go generateDebugBundle(config, recorder, connectClient, logFilePath)
+ }
+ }
+ }()
+}
diff --git a/client/cmd/debug_windows.go b/client/cmd/debug_windows.go
new file mode 100644
index 000000000..f57955fd4
--- /dev/null
+++ b/client/cmd/debug_windows.go
@@ -0,0 +1,126 @@
+package cmd
+
+import (
+ "context"
+ "errors"
+ "os"
+ "strconv"
+ "time"
+
+ log "github.com/sirupsen/logrus"
+ "golang.org/x/sys/windows"
+
+ "github.com/netbirdio/netbird/client/internal"
+ "github.com/netbirdio/netbird/client/internal/peer"
+)
+
+const (
+ envListenEvent = "NB_LISTEN_DEBUG_EVENT"
+ debugTriggerEventName = `Global\NetbirdDebugTriggerEvent`
+
+ waitTimeout = 5 * time.Second
+)
+
+// SetupDebugHandler sets up a Windows event to listen for a signal to generate a debug bundle.
+// Example usage with PowerShell:
+// $evt = [System.Threading.EventWaitHandle]::OpenExisting("Global\NetbirdDebugTriggerEvent")
+// $evt.Set()
+// $evt.Close()
+func SetupDebugHandler(
+ ctx context.Context,
+ config *internal.Config,
+ recorder *peer.Status,
+ connectClient *internal.ConnectClient,
+ logFilePath string,
+) {
+ env := os.Getenv(envListenEvent)
+ if env == "" {
+ return
+ }
+
+ listenEvent, err := strconv.ParseBool(env)
+ if err != nil {
+ log.Errorf("Failed to parse %s: %v", envListenEvent, err)
+ return
+ }
+ if !listenEvent {
+ return
+ }
+
+ eventNamePtr, err := windows.UTF16PtrFromString(debugTriggerEventName)
+ if err != nil {
+ log.Errorf("Failed to convert event name '%s' to UTF16: %v", debugTriggerEventName, err)
+ return
+ }
+
+ // TODO: restrict access by ACL
+ eventHandle, err := windows.CreateEvent(nil, 1, 0, eventNamePtr)
+ if err != nil {
+ if errors.Is(err, windows.ERROR_ALREADY_EXISTS) {
+ log.Warnf("Debug trigger event '%s' already exists. Attempting to open.", debugTriggerEventName)
+ // SYNCHRONIZE is needed for WaitForSingleObject, EVENT_MODIFY_STATE for ResetEvent.
+ eventHandle, err = windows.OpenEvent(windows.SYNCHRONIZE|windows.EVENT_MODIFY_STATE, false, eventNamePtr)
+ if err != nil {
+ log.Errorf("Failed to open existing debug trigger event '%s': %v", debugTriggerEventName, err)
+ return
+ }
+ log.Infof("Successfully opened existing debug trigger event '%s'.", debugTriggerEventName)
+ } else {
+ log.Errorf("Failed to create debug trigger event '%s': %v", debugTriggerEventName, err)
+ return
+ }
+ }
+
+ if eventHandle == windows.InvalidHandle {
+ log.Errorf("Obtained an invalid handle for debug trigger event '%s'", debugTriggerEventName)
+ return
+ }
+
+ log.Infof("Debug handler waiting for signal on event: %s", debugTriggerEventName)
+
+ go waitForEvent(ctx, config, recorder, connectClient, logFilePath, eventHandle)
+}
+
+func waitForEvent(
+ ctx context.Context,
+ config *internal.Config,
+ recorder *peer.Status,
+ connectClient *internal.ConnectClient,
+ logFilePath string,
+ eventHandle windows.Handle,
+) {
+ defer func() {
+ if err := windows.CloseHandle(eventHandle); err != nil {
+ log.Errorf("Failed to close debug event handle '%s': %v", debugTriggerEventName, err)
+ }
+ }()
+
+ for {
+ if ctx.Err() != nil {
+ return
+ }
+
+ status, err := windows.WaitForSingleObject(eventHandle, uint32(waitTimeout.Milliseconds()))
+
+ switch status {
+ case windows.WAIT_OBJECT_0:
+ log.Info("Received signal on debug event. Triggering debug bundle generation.")
+
+ // reset the event so it can be triggered again later (manual reset == 1)
+ if err := windows.ResetEvent(eventHandle); err != nil {
+ log.Errorf("Failed to reset debug event '%s': %v", debugTriggerEventName, err)
+ }
+
+ go generateDebugBundle(config, recorder, connectClient, logFilePath)
+ case uint32(windows.WAIT_TIMEOUT):
+
+ default:
+ log.Errorf("Unexpected status %d from WaitForSingleObject for debug event '%s': %v", status, debugTriggerEventName, err)
+ select {
+ case <-time.After(5 * time.Second):
+ case <-ctx.Done():
+ return
+ }
+ }
+ }
+}
diff --git a/client/cmd/login.go b/client/cmd/login.go
index c86d6c636..84906a7a4 100644
--- a/client/cmd/login.go
+++ b/client/cmd/login.go
@@ -55,6 +55,9 @@ var loginCmd = &cobra.Command{
return err
}
+ // update host's static platform and system information
+ system.UpdateStaticInfo()
+
ic := internal.ConfigInput{
ManagementURL: managementURL,
AdminURL: adminURL,
diff --git a/client/cmd/root.go b/client/cmd/root.go
index baf444b99..b4f067078 100644
--- a/client/cmd/root.go
+++ b/client/cmd/root.go
@@ -22,6 +22,7 @@ import (
"google.golang.org/grpc/credentials/insecure"
"github.com/netbirdio/netbird/client/internal"
+ "github.com/netbirdio/netbird/upload-server/types"
)
const (
@@ -39,6 +40,9 @@ const (
dnsRouteIntervalFlag = "dns-router-interval"
systemInfoFlag = "system-info"
blockLANAccessFlag = "block-lan-access"
+ uploadBundle = "upload-bundle"
+ uploadBundleURL = "upload-bundle-url"
+ defaultBundleURL = "https://upload.debug.netbird.io" + types.GetURLPath
)
var (
@@ -75,6 +79,8 @@ var (
debugSystemInfoFlag bool
dnsRouteInterval time.Duration
blockLANAccess bool
+ debugUploadBundle bool
+ debugUploadBundleURL string
rootCmd = &cobra.Command{
Use: "netbird",
@@ -181,6 +187,8 @@ func init() {
upCmd.PersistentFlags().BoolVar(&autoConnectDisabled, disableAutoConnectFlag, false, "Disables auto-connect feature. If enabled, then the client won't connect automatically when the service starts.")
debugCmd.PersistentFlags().BoolVarP(&debugSystemInfoFlag, systemInfoFlag, "S", true, "Adds system information to the debug bundle")
+ debugCmd.PersistentFlags().BoolVarP(&debugUploadBundle, uploadBundle, "U", false, fmt.Sprintf("Uploads the debug bundle to a server from URL defined by %s", uploadBundleURL))
+ debugCmd.PersistentFlags().StringVar(&debugUploadBundleURL, uploadBundleURL, defaultBundleURL, "Service URL to get an URL to upload the debug bundle")
}
// SetupCloseHandler handles SIGTERM signal and exits with success
diff --git a/client/cmd/service_controller.go b/client/cmd/service_controller.go
index 761c86628..5e3c63e57 100644
--- a/client/cmd/service_controller.go
+++ b/client/cmd/service_controller.go
@@ -16,12 +16,17 @@ import (
"github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/client/server"
+ "github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/util"
)
func (p *program) Start(svc service.Service) error {
// Start should not block. Do the actual work async.
log.Info("starting Netbird service") //nolint
+
+ // Collect static system and platform information
+ system.UpdateStaticInfo()
+
// in any case, even if configuration does not exists we run daemon to serve CLI gRPC API.
p.serv = grpc.NewServer()
@@ -115,6 +120,7 @@ var runCmd = &cobra.Command{
ctx, cancel := context.WithCancel(cmd.Context())
SetupCloseHandler(ctx, cancel)
+ SetupDebugHandler(ctx, nil, nil, nil, logFile)
s, err := newSVC(newProgram(ctx, cancel), newSVCConfig())
if err != nil {
diff --git a/client/cmd/testutil_test.go b/client/cmd/testutil_test.go
index 70abe4abe..258a8daff 100644
--- a/client/cmd/testutil_test.go
+++ b/client/cmd/testutil_test.go
@@ -98,6 +98,11 @@ func startManagement(t *testing.T, config *types.Config, testFile string) (*grpc
settingsMockManager := settings.NewMockManager(ctrl)
permissionsManagerMock := permissions.NewMockManager(ctrl)
+ settingsMockManager.EXPECT().
+ GetSettings(gomock.Any(), gomock.Any(), gomock.Any()).
+ Return(&types.Settings{}, nil).
+ AnyTimes()
+
accountManager, err := mgmt.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, iv, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManagerMock)
if err != nil {
t.Fatal(err)
diff --git a/client/cmd/up.go b/client/cmd/up.go
index 8b716a96d..bfe41628e 100644
--- a/client/cmd/up.go
+++ b/client/cmd/up.go
@@ -219,6 +219,8 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error {
r.GetFullStatus()
connectClient := internal.NewConnectClient(ctx, config, r)
+ SetupDebugHandler(ctx, config, r, connectClient, "")
+
return connectClient.Run(nil)
}
diff --git a/client/firewall/iptables/manager_linux.go b/client/firewall/iptables/manager_linux.go
index 652ab1b3e..b229688fc 100644
--- a/client/firewall/iptables/manager_linux.go
+++ b/client/firewall/iptables/manager_linux.go
@@ -113,17 +113,16 @@ func (m *Manager) AddPeerFiltering(
func (m *Manager) AddRouteFiltering(
id []byte,
sources []netip.Prefix,
- destination netip.Prefix,
+ destination firewall.Network,
proto firewall.Protocol,
- sPort *firewall.Port,
- dPort *firewall.Port,
+ sPort, dPort *firewall.Port,
action firewall.Action,
) (firewall.Rule, error) {
m.mutex.Lock()
defer m.mutex.Unlock()
- if !destination.Addr().Is4() {
- return nil, fmt.Errorf("unsupported IP version: %s", destination.Addr().String())
+ if destination.IsPrefix() && !destination.Prefix.Addr().Is4() {
+ return nil, fmt.Errorf("unsupported IP version: %s", destination.Prefix.Addr().String())
}
return m.router.AddRouteFiltering(id, sources, destination, proto, sPort, dPort, action)
@@ -243,6 +242,14 @@ func (m *Manager) DeleteDNATRule(rule firewall.Rule) error {
return m.router.DeleteDNATRule(rule)
}
+// UpdateSet updates the set with the given prefixes
+func (m *Manager) UpdateSet(set firewall.Set, prefixes []netip.Prefix) error {
+ m.mutex.Lock()
+ defer m.mutex.Unlock()
+
+ return m.router.UpdateSet(set, prefixes)
+}
+
func getConntrackEstablished() []string {
return []string{"-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT"}
}
diff --git a/client/firewall/iptables/router_linux.go b/client/firewall/iptables/router_linux.go
index 869b0b359..bb799b99b 100644
--- a/client/firewall/iptables/router_linux.go
+++ b/client/firewall/iptables/router_linux.go
@@ -57,18 +57,18 @@ type ruleInfo struct {
}
type routeFilteringRuleParams struct {
- Sources []netip.Prefix
- Destination netip.Prefix
+ Source firewall.Network
+ Destination firewall.Network
Proto firewall.Protocol
SPort *firewall.Port
DPort *firewall.Port
Direction firewall.RuleDirection
Action firewall.Action
- SetName string
}
type routeRules map[string][]string
+// the ipset library currently does not support comments, so we use the name only (string)
type ipsetCounter = refcounter.Counter[string, []netip.Prefix, struct{}]
type router struct {
@@ -129,7 +129,7 @@ func (r *router) init(stateManager *statemanager.Manager) error {
func (r *router) AddRouteFiltering(
id []byte,
sources []netip.Prefix,
- destination netip.Prefix,
+ destination firewall.Network,
proto firewall.Protocol,
sPort *firewall.Port,
dPort *firewall.Port,
@@ -140,27 +140,28 @@ func (r *router) AddRouteFiltering(
return ruleKey, nil
}
- var setName string
+ var source firewall.Network
if len(sources) > 1 {
- setName = firewall.GenerateSetName(sources)
- if _, err := r.ipsetCounter.Increment(setName, sources); err != nil {
- return nil, fmt.Errorf("create or get ipset: %w", err)
- }
+ source.Set = firewall.NewPrefixSet(sources)
+ } else if len(sources) > 0 {
+ source.Prefix = sources[0]
}
params := routeFilteringRuleParams{
- Sources: sources,
+ Source: source,
Destination: destination,
Proto: proto,
SPort: sPort,
DPort: dPort,
Action: action,
- SetName: setName,
}
- rule := genRouteFilteringRuleSpec(params)
+ rule, err := r.genRouteRuleSpec(params, sources)
+ if err != nil {
+ return nil, fmt.Errorf("generate route rule spec: %w", err)
+ }
+
// Insert DROP rules at the beginning, append ACCEPT rules at the end
- var err error
if action == firewall.ActionDrop {
// after the established rule
err = r.iptablesClient.Insert(tableFilter, chainRTFWDIN, 2, rule...)
@@ -183,17 +184,13 @@ func (r *router) DeleteRouteRule(rule firewall.Rule) error {
ruleKey := rule.ID()
if rule, exists := r.rules[ruleKey]; exists {
- setName := r.findSetNameInRule(rule)
-
if err := r.iptablesClient.Delete(tableFilter, chainRTFWDIN, rule...); err != nil {
return fmt.Errorf("delete route rule: %v", err)
}
delete(r.rules, ruleKey)
- if setName != "" {
- if _, err := r.ipsetCounter.Decrement(setName); err != nil {
- return fmt.Errorf("failed to remove ipset: %w", err)
- }
+ if err := r.decrementSetCounter(rule); err != nil {
+ return fmt.Errorf("decrement ipset counter: %w", err)
}
} else {
log.Debugf("route rule %s not found", ruleKey)
@@ -204,13 +201,26 @@ func (r *router) DeleteRouteRule(rule firewall.Rule) error {
return nil
}
-func (r *router) findSetNameInRule(rule []string) string {
- for i, arg := range rule {
- if arg == "-m" && i+3 < len(rule) && rule[i+1] == "set" && rule[i+2] == matchSet {
- return rule[i+3]
+func (r *router) decrementSetCounter(rule []string) error {
+ sets := r.findSets(rule)
+ var merr *multierror.Error
+ for _, setName := range sets {
+ if _, err := r.ipsetCounter.Decrement(setName); err != nil {
+ merr = multierror.Append(merr, fmt.Errorf("decrement counter: %w", err))
}
}
- return ""
+
+ return nberrors.FormatErrorOrNil(merr)
+}
+
+func (r *router) findSets(rule []string) []string {
+ var sets []string
+ for i, arg := range rule {
+ if arg == "-m" && i+3 < len(rule) && rule[i+1] == "set" && rule[i+2] == matchSet {
+ sets = append(sets, rule[i+3])
+ }
+ }
+ return sets
}
func (r *router) createIpSet(setName string, sources []netip.Prefix) error {
@@ -231,6 +241,8 @@ func (r *router) deleteIpSet(setName string) error {
if err := ipset.Destroy(setName); err != nil {
return fmt.Errorf("destroy set %s: %w", setName, err)
}
+
+ log.Debugf("Deleted unused ipset %s", setName)
return nil
}
@@ -270,12 +282,14 @@ func (r *router) RemoveNatRule(pair firewall.RouterPair) error {
log.Errorf("%v", err)
}
- if err := r.removeNatRule(pair); err != nil {
- return fmt.Errorf("remove nat rule: %w", err)
- }
+ if pair.Masquerade {
+ if err := r.removeNatRule(pair); err != nil {
+ return fmt.Errorf("remove nat rule: %w", err)
+ }
- if err := r.removeNatRule(firewall.GetInversePair(pair)); err != nil {
- return fmt.Errorf("remove inverse nat rule: %w", err)
+ if err := r.removeNatRule(firewall.GetInversePair(pair)); err != nil {
+ return fmt.Errorf("remove inverse nat rule: %w", err)
+ }
}
if err := r.removeLegacyRouteRule(pair); err != nil {
@@ -313,8 +327,10 @@ func (r *router) removeLegacyRouteRule(pair firewall.RouterPair) error {
return fmt.Errorf("remove legacy forwarding rule %s -> %s: %v", pair.Source, pair.Destination, err)
}
delete(r.rules, ruleKey)
- } else {
- log.Debugf("legacy forwarding rule %s not found", ruleKey)
+
+ if err := r.decrementSetCounter(rule); err != nil {
+ return fmt.Errorf("decrement ipset counter: %w", err)
+ }
}
return nil
@@ -599,12 +615,26 @@ func (r *router) addNatRule(pair firewall.RouterPair) error {
rule = append(rule,
"-m", "conntrack",
"--ctstate", "NEW",
- "-s", pair.Source.String(),
- "-d", pair.Destination.String(),
+ )
+ sourceExp, err := r.applyNetwork("-s", pair.Source, nil)
+ if err != nil {
+ return fmt.Errorf("apply network -s: %w", err)
+ }
+ destExp, err := r.applyNetwork("-d", pair.Destination, nil)
+ if err != nil {
+ return fmt.Errorf("apply network -d: %w", err)
+ }
+
+ rule = append(rule, sourceExp...)
+ rule = append(rule, destExp...)
+ rule = append(rule,
"-j", "MARK", "--set-mark", fmt.Sprintf("%#x", markValue),
)
- if err := r.iptablesClient.Append(tableMangle, chainRTPRE, rule...); err != nil {
+ // Ensure nat rules come first, so the mark can be overwritten.
+ // Currently overwritten by the dst-type LOCAL rules for redirected traffic.
+ if err := r.iptablesClient.Insert(tableMangle, chainRTPRE, 1, rule...); err != nil {
+ // TODO: rollback ipset counter
return fmt.Errorf("error while adding marking rule for %s: %v", pair.Destination, err)
}
@@ -622,6 +652,10 @@ func (r *router) removeNatRule(pair firewall.RouterPair) error {
return fmt.Errorf("error while removing marking rule for %s: %v", pair.Destination, err)
}
delete(r.rules, ruleKey)
+
+ if err := r.decrementSetCounter(rule); err != nil {
+ return fmt.Errorf("decrement ipset counter: %w", err)
+ }
} else {
log.Debugf("marking rule %s not found", ruleKey)
}
@@ -787,17 +821,21 @@ func (r *router) DeleteDNATRule(rule firewall.Rule) error {
return nberrors.FormatErrorOrNil(merr)
}
-func genRouteFilteringRuleSpec(params routeFilteringRuleParams) []string {
+func (r *router) genRouteRuleSpec(params routeFilteringRuleParams, sources []netip.Prefix) ([]string, error) {
var rule []string
- if params.SetName != "" {
- rule = append(rule, "-m", "set", matchSet, params.SetName, "src")
- } else if len(params.Sources) > 0 {
- source := params.Sources[0]
- rule = append(rule, "-s", source.String())
+ sourceExp, err := r.applyNetwork("-s", params.Source, sources)
+ if err != nil {
+ return nil, fmt.Errorf("apply network -s: %w", err)
+
+ }
+ destExp, err := r.applyNetwork("-d", params.Destination, nil)
+ if err != nil {
+ return nil, fmt.Errorf("apply network -d: %w", err)
}
- rule = append(rule, "-d", params.Destination.String())
+ rule = append(rule, sourceExp...)
+ rule = append(rule, destExp...)
if params.Proto != firewall.ProtocolALL {
rule = append(rule, "-p", strings.ToLower(string(params.Proto)))
@@ -807,7 +845,47 @@ func genRouteFilteringRuleSpec(params routeFilteringRuleParams) []string {
rule = append(rule, "-j", actionToStr(params.Action))
- return rule
+ return rule, nil
+}
+
+func (r *router) applyNetwork(flag string, network firewall.Network, prefixes []netip.Prefix) ([]string, error) {
+ direction := "src"
+ if flag == "-d" {
+ direction = "dst"
+ }
+
+ if network.IsSet() {
+ if _, err := r.ipsetCounter.Increment(network.Set.HashedName(), prefixes); err != nil {
+ return nil, fmt.Errorf("create or get ipset: %w", err)
+ }
+
+ return []string{"-m", "set", matchSet, network.Set.HashedName(), direction}, nil
+ }
+ if network.IsPrefix() {
+ return []string{flag, network.Prefix.String()}, nil
+ }
+
+ // nolint:nilnil
+ return nil, nil
+}
+
+func (r *router) UpdateSet(set firewall.Set, prefixes []netip.Prefix) error {
+ var merr *multierror.Error
+ for _, prefix := range prefixes {
+ // TODO: Implement IPv6 support
+ if prefix.Addr().Is6() {
+ log.Tracef("skipping IPv6 prefix %s: IPv6 support not yet implemented", prefix)
+ continue
+ }
+ if err := ipset.AddPrefix(set.HashedName(), prefix); err != nil {
+ merr = multierror.Append(merr, fmt.Errorf("increment ipset counter: %w", err))
+ }
+ }
+ if merr == nil {
+ log.Debugf("updated set %s with prefixes %v", set.HashedName(), prefixes)
+ }
+
+ return nberrors.FormatErrorOrNil(merr)
}
func applyPort(flag string, port *firewall.Port) []string {
diff --git a/client/firewall/iptables/router_linux_test.go b/client/firewall/iptables/router_linux_test.go
index dad77dee7..e9eeff863 100644
--- a/client/firewall/iptables/router_linux_test.go
+++ b/client/firewall/iptables/router_linux_test.go
@@ -60,8 +60,8 @@ func TestIptablesManager_RestoreOrCreateContainers(t *testing.T) {
pair := firewall.RouterPair{
ID: "abc",
- Source: netip.MustParsePrefix("100.100.100.1/32"),
- Destination: netip.MustParsePrefix("100.100.100.0/24"),
+ Source: firewall.Network{Prefix: netip.MustParsePrefix("100.100.100.1/32")},
+ Destination: firewall.Network{Prefix: netip.MustParsePrefix("100.100.100.0/24")},
Masquerade: true,
}
@@ -332,7 +332,7 @@ func TestRouter_AddRouteFiltering(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- ruleKey, err := r.AddRouteFiltering(nil, tt.sources, tt.destination, tt.proto, tt.sPort, tt.dPort, tt.action)
+ ruleKey, err := r.AddRouteFiltering(nil, tt.sources, firewall.Network{Prefix: tt.destination}, tt.proto, tt.sPort, tt.dPort, tt.action)
require.NoError(t, err, "AddRouteFiltering failed")
// Check if the rule is in the internal map
@@ -347,23 +347,29 @@ func TestRouter_AddRouteFiltering(t *testing.T) {
assert.NoError(t, err, "Failed to check rule existence")
assert.True(t, exists, "Rule not found in iptables")
+ var source firewall.Network
+ if len(tt.sources) > 1 {
+ source.Set = firewall.NewPrefixSet(tt.sources)
+ } else if len(tt.sources) > 0 {
+ source.Prefix = tt.sources[0]
+ }
// Verify rule content
params := routeFilteringRuleParams{
- Sources: tt.sources,
- Destination: tt.destination,
+ Source: source,
+ Destination: firewall.Network{Prefix: tt.destination},
Proto: tt.proto,
SPort: tt.sPort,
DPort: tt.dPort,
Action: tt.action,
- SetName: "",
}
- expectedRule := genRouteFilteringRuleSpec(params)
+ expectedRule, err := r.genRouteRuleSpec(params, nil)
+ require.NoError(t, err, "Failed to generate expected rule spec")
if tt.expectSet {
- setName := firewall.GenerateSetName(tt.sources)
- params.SetName = setName
- expectedRule = genRouteFilteringRuleSpec(params)
+ setName := firewall.NewPrefixSet(tt.sources).HashedName()
+ expectedRule, err = r.genRouteRuleSpec(params, nil)
+ require.NoError(t, err, "Failed to generate expected rule spec with set")
// Check if the set was created
_, exists := r.ipsetCounter.Get(setName)
@@ -378,3 +384,62 @@ func TestRouter_AddRouteFiltering(t *testing.T) {
})
}
}
+
+func TestFindSetNameInRule(t *testing.T) {
+ r := &router{}
+
+ testCases := []struct {
+ name string
+ rule []string
+ expected []string
+ }{
+ {
+ name: "Basic rule with two sets",
+ rule: []string{
+ "-A", "NETBIRD-RT-FWD-IN", "-p", "tcp", "-m", "set", "--match-set", "nb-2e5a2a05", "src",
+ "-m", "set", "--match-set", "nb-349ae051", "dst", "-m", "tcp", "--dport", "8080", "-j", "ACCEPT",
+ },
+ expected: []string{"nb-2e5a2a05", "nb-349ae051"},
+ },
+ {
+ name: "No sets",
+ rule: []string{"-A", "NETBIRD-RT-FWD-IN", "-p", "tcp", "-j", "ACCEPT"},
+ expected: []string{},
+ },
+ {
+ name: "Multiple sets with different positions",
+ rule: []string{
+ "-m", "set", "--match-set", "set1", "src", "-p", "tcp",
+ "-m", "set", "--match-set", "set-abc123", "dst", "-j", "ACCEPT",
+ },
+ expected: []string{"set1", "set-abc123"},
+ },
+ {
+ name: "Boundary case - sequence appears at end",
+ rule: []string{"-p", "tcp", "-m", "set", "--match-set", "final-set"},
+ expected: []string{"final-set"},
+ },
+ {
+ name: "Incomplete pattern - missing set name",
+ rule: []string{"-p", "tcp", "-m", "set", "--match-set"},
+ expected: []string{},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ result := r.findSets(tc.rule)
+
+ if len(result) != len(tc.expected) {
+ t.Errorf("Expected %d sets, got %d. Sets found: %v", len(tc.expected), len(result), result)
+ return
+ }
+
+ for i, set := range result {
+ if set != tc.expected[i] {
+ t.Errorf("Expected set %q at position %d, got %q", tc.expected[i], i, set)
+ }
+ }
+ })
+ }
+}
diff --git a/client/firewall/manager/firewall.go b/client/firewall/manager/firewall.go
index 1d71051ef..084d19423 100644
--- a/client/firewall/manager/firewall.go
+++ b/client/firewall/manager/firewall.go
@@ -1,13 +1,10 @@
package manager
import (
- "crypto/sha256"
- "encoding/hex"
"fmt"
"net"
"net/netip"
"sort"
- "strings"
log "github.com/sirupsen/logrus"
@@ -43,6 +40,18 @@ const (
// Action is the action to be taken on a rule
type Action int
+// String returns the string representation of the action
+func (a Action) String() string {
+ switch a {
+ case ActionAccept:
+ return "accept"
+ case ActionDrop:
+ return "drop"
+ default:
+ return "unknown"
+ }
+}
+
const (
// ActionAccept is the action to accept a packet
ActionAccept Action = iota
@@ -50,6 +59,33 @@ const (
ActionDrop
)
+// Network is a rule destination, either a set or a prefix
+type Network struct {
+ Set Set
+ Prefix netip.Prefix
+}
+
+// String returns the string representation of the destination
+func (d Network) String() string {
+ if d.Prefix.IsValid() {
+ return d.Prefix.String()
+ }
+ if d.IsSet() {
+ return d.Set.HashedName()
+ }
+ return ""
+}
+
+// IsSet returns true if the destination is a set
+func (d Network) IsSet() bool {
+ return d.Set != Set{}
+}
+
+// IsPrefix returns true if the destination is a valid prefix
+func (d Network) IsPrefix() bool {
+ return d.Prefix.IsValid()
+}
+
// Manager is the high level abstraction of a firewall manager
//
// It declares methods which handle actions required by the
@@ -83,10 +119,9 @@ type Manager interface {
AddRouteFiltering(
id []byte,
sources []netip.Prefix,
- destination netip.Prefix,
+ destination Network,
proto Protocol,
- sPort *Port,
- dPort *Port,
+ sPort, dPort *Port,
action Action,
) (Rule, error)
@@ -119,6 +154,9 @@ type Manager interface {
// DeleteDNATRule deletes a DNAT rule
DeleteDNATRule(Rule) error
+
+ // UpdateSet updates the set with the given prefixes
+ UpdateSet(hash Set, prefixes []netip.Prefix) error
}
func GenKey(format string, pair RouterPair) string {
@@ -153,22 +191,6 @@ func SetLegacyManagement(router LegacyManager, isLegacy bool) error {
return nil
}
-// GenerateSetName generates a unique name for an ipset based on the given sources.
-func GenerateSetName(sources []netip.Prefix) string {
- // sort for consistent naming
- SortPrefixes(sources)
-
- var sourcesStr strings.Builder
- for _, src := range sources {
- sourcesStr.WriteString(src.String())
- }
-
- hash := sha256.Sum256([]byte(sourcesStr.String()))
- shortHash := hex.EncodeToString(hash[:])[:8]
-
- return fmt.Sprintf("nb-%s", shortHash)
-}
-
// MergeIPRanges merges overlapping IP ranges and returns a slice of non-overlapping netip.Prefix
func MergeIPRanges(prefixes []netip.Prefix) []netip.Prefix {
if len(prefixes) == 0 {
diff --git a/client/firewall/manager/firewall_test.go b/client/firewall/manager/firewall_test.go
index 3f47d6679..180346906 100644
--- a/client/firewall/manager/firewall_test.go
+++ b/client/firewall/manager/firewall_test.go
@@ -20,8 +20,8 @@ func TestGenerateSetName(t *testing.T) {
netip.MustParsePrefix("192.168.1.0/24"),
}
- result1 := manager.GenerateSetName(prefixes1)
- result2 := manager.GenerateSetName(prefixes2)
+ result1 := manager.NewPrefixSet(prefixes1)
+ result2 := manager.NewPrefixSet(prefixes2)
if result1 != result2 {
t.Errorf("Different orders produced different hashes: %s != %s", result1, result2)
@@ -34,9 +34,9 @@ func TestGenerateSetName(t *testing.T) {
netip.MustParsePrefix("10.0.0.0/8"),
}
- result := manager.GenerateSetName(prefixes)
+ result := manager.NewPrefixSet(prefixes)
- matched, err := regexp.MatchString(`^nb-[0-9a-f]{8}$`, result)
+ matched, err := regexp.MatchString(`^nb-[0-9a-f]{8}$`, result.HashedName())
if err != nil {
t.Fatalf("Error matching regex: %v", err)
}
@@ -46,8 +46,8 @@ func TestGenerateSetName(t *testing.T) {
})
t.Run("Empty input produces consistent result", func(t *testing.T) {
- result1 := manager.GenerateSetName([]netip.Prefix{})
- result2 := manager.GenerateSetName([]netip.Prefix{})
+ result1 := manager.NewPrefixSet([]netip.Prefix{})
+ result2 := manager.NewPrefixSet([]netip.Prefix{})
if result1 != result2 {
t.Errorf("Empty input produced inconsistent results: %s != %s", result1, result2)
@@ -64,8 +64,8 @@ func TestGenerateSetName(t *testing.T) {
netip.MustParsePrefix("192.168.1.0/24"),
}
- result1 := manager.GenerateSetName(prefixes1)
- result2 := manager.GenerateSetName(prefixes2)
+ result1 := manager.NewPrefixSet(prefixes1)
+ result2 := manager.NewPrefixSet(prefixes2)
if result1 != result2 {
t.Errorf("Different orders of IPv4 and IPv6 produced different hashes: %s != %s", result1, result2)
diff --git a/client/firewall/manager/routerpair.go b/client/firewall/manager/routerpair.go
index 8c94b7dd4..079c051d9 100644
--- a/client/firewall/manager/routerpair.go
+++ b/client/firewall/manager/routerpair.go
@@ -1,15 +1,13 @@
package manager
import (
- "net/netip"
-
"github.com/netbirdio/netbird/route"
)
type RouterPair struct {
ID route.ID
- Source netip.Prefix
- Destination netip.Prefix
+ Source Network
+ Destination Network
Masquerade bool
Inverse bool
}
diff --git a/client/firewall/manager/set.go b/client/firewall/manager/set.go
new file mode 100644
index 000000000..4c88f6eac
--- /dev/null
+++ b/client/firewall/manager/set.go
@@ -0,0 +1,74 @@
+package manager
+
+import (
+ "crypto/sha256"
+ "encoding/hex"
+ "fmt"
+ "net/netip"
+ "slices"
+
+ log "github.com/sirupsen/logrus"
+
+ "github.com/netbirdio/netbird/management/domain"
+)
+
+type Set struct {
+ hash [4]byte
+ comment string
+}
+
+// String returns the string representation of the set: hashed name and comment
+func (h Set) String() string {
+ if h.comment == "" {
+ return h.HashedName()
+ }
+ return h.HashedName() + ": " + h.comment
+}
+
+// HashedName returns the string representation of the hash
+func (h Set) HashedName() string {
+ return fmt.Sprintf(
+ "nb-%s",
+ hex.EncodeToString(h.hash[:]),
+ )
+}
+
+// Comment returns the comment of the set
+func (h Set) Comment() string {
+ return h.comment
+}
+
+// NewPrefixSet generates a unique name for an ipset based on the given prefixes.
+func NewPrefixSet(prefixes []netip.Prefix) Set {
+ // sort for consistent naming
+ SortPrefixes(prefixes)
+
+ hash := sha256.New()
+ for _, src := range prefixes {
+ bytes, err := src.MarshalBinary()
+ if err != nil {
+ log.Warnf("failed to marshal prefix %s: %v", src, err)
+ }
+ hash.Write(bytes)
+ }
+ var set Set
+ copy(set.hash[:], hash.Sum(nil)[:4])
+
+ return set
+}
+
+// NewDomainSet generates a unique name for an ipset based on the given domains.
+func NewDomainSet(domains domain.List) Set {
+ slices.Sort(domains)
+
+ hash := sha256.New()
+ for _, d := range domains {
+ hash.Write([]byte(d.PunycodeString()))
+ }
+ set := Set{
+ comment: domains.SafeString(),
+ }
+ copy(set.hash[:], hash.Sum(nil)[:4])
+
+ return set
+}
diff --git a/client/firewall/nftables/manager_linux.go b/client/firewall/nftables/manager_linux.go
index a5809471c..e6b3a031b 100644
--- a/client/firewall/nftables/manager_linux.go
+++ b/client/firewall/nftables/manager_linux.go
@@ -135,17 +135,16 @@ func (m *Manager) AddPeerFiltering(
func (m *Manager) AddRouteFiltering(
id []byte,
sources []netip.Prefix,
- destination netip.Prefix,
+ destination firewall.Network,
proto firewall.Protocol,
- sPort *firewall.Port,
- dPort *firewall.Port,
+ sPort, dPort *firewall.Port,
action firewall.Action,
) (firewall.Rule, error) {
m.mutex.Lock()
defer m.mutex.Unlock()
- if !destination.Addr().Is4() {
- return nil, fmt.Errorf("unsupported IP version: %s", destination.Addr().String())
+ if destination.IsPrefix() && !destination.Prefix.Addr().Is4() {
+ return nil, fmt.Errorf("unsupported IP version: %s", destination.Prefix.Addr().String())
}
return m.router.AddRouteFiltering(id, sources, destination, proto, sPort, dPort, action)
@@ -242,7 +241,7 @@ func (m *Manager) SetLegacyManagement(isLegacy bool) error {
return firewall.SetLegacyManagement(m.router, isLegacy)
}
-// Reset firewall to the default state
+// Close closes the firewall manager
func (m *Manager) Close(stateManager *statemanager.Manager) error {
m.mutex.Lock()
defer m.mutex.Unlock()
@@ -359,6 +358,14 @@ func (m *Manager) DeleteDNATRule(rule firewall.Rule) error {
return m.router.DeleteDNATRule(rule)
}
+// UpdateSet updates the set with the given prefixes
+func (m *Manager) UpdateSet(set firewall.Set, prefixes []netip.Prefix) error {
+ m.mutex.Lock()
+ defer m.mutex.Unlock()
+
+ return m.router.UpdateSet(set, prefixes)
+}
+
func (m *Manager) createWorkTable() (*nftables.Table, error) {
tables, err := m.rConn.ListTablesOfFamily(nftables.TableFamilyIPv4)
if err != nil {
diff --git a/client/firewall/nftables/manager_linux_test.go b/client/firewall/nftables/manager_linux_test.go
index 373743a08..602a6b8dc 100644
--- a/client/firewall/nftables/manager_linux_test.go
+++ b/client/firewall/nftables/manager_linux_test.go
@@ -289,7 +289,7 @@ func TestNftablesManagerCompatibilityWithIptables(t *testing.T) {
_, err = manager.AddRouteFiltering(
nil,
[]netip.Prefix{netip.MustParsePrefix("192.168.2.0/24")},
- netip.MustParsePrefix("10.1.0.0/24"),
+ fw.Network{Prefix: netip.MustParsePrefix("10.1.0.0/24")},
fw.ProtocolTCP,
nil,
&fw.Port{Values: []uint16{443}},
@@ -298,8 +298,8 @@ func TestNftablesManagerCompatibilityWithIptables(t *testing.T) {
require.NoError(t, err, "failed to add route filtering rule")
pair := fw.RouterPair{
- Source: netip.MustParsePrefix("192.168.1.0/24"),
- Destination: netip.MustParsePrefix("10.0.0.0/24"),
+ Source: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
+ Destination: fw.Network{Prefix: netip.MustParsePrefix("10.0.0.0/24")},
Masquerade: true,
}
err = manager.AddNatRule(pair)
diff --git a/client/firewall/nftables/router_linux.go b/client/firewall/nftables/router_linux.go
index aff86dd90..0f6c5bdf6 100644
--- a/client/firewall/nftables/router_linux.go
+++ b/client/firewall/nftables/router_linux.go
@@ -10,7 +10,6 @@ import (
"strings"
"github.com/coreos/go-iptables/iptables"
- "github.com/davecgh/go-spew/spew"
"github.com/google/nftables"
"github.com/google/nftables/binaryutil"
"github.com/google/nftables/expr"
@@ -44,9 +43,14 @@ const (
const refreshRulesMapError = "refresh rules map: %w"
var (
- errFilterTableNotFound = fmt.Errorf("nftables: 'filter' table not found")
+ errFilterTableNotFound = fmt.Errorf("'filter' table not found")
)
+type setInput struct {
+ set firewall.Set
+ prefixes []netip.Prefix
+}
+
type router struct {
conn *nftables.Conn
workTable *nftables.Table
@@ -54,7 +58,7 @@ type router struct {
chains map[string]*nftables.Chain
// rules is useful to avoid duplicates and to get missing attributes that we don't have when adding new rules
rules map[string]*nftables.Rule
- ipsetCounter *refcounter.Counter[string, []netip.Prefix, *nftables.Set]
+ ipsetCounter *refcounter.Counter[string, setInput, *nftables.Set]
wgIface iFaceMapper
ipFwdState *ipfwdstate.IPForwardingState
@@ -163,7 +167,7 @@ func (r *router) removeNatPreroutingRules() error {
func (r *router) loadFilterTable() (*nftables.Table, error) {
tables, err := r.conn.ListTablesOfFamily(nftables.TableFamilyIPv4)
if err != nil {
- return nil, fmt.Errorf("nftables: unable to list tables: %v", err)
+ return nil, fmt.Errorf("unable to list tables: %v", err)
}
for _, table := range tables {
@@ -316,7 +320,7 @@ func (r *router) setupDataPlaneMark() error {
func (r *router) AddRouteFiltering(
id []byte,
sources []netip.Prefix,
- destination netip.Prefix,
+ destination firewall.Network,
proto firewall.Protocol,
sPort *firewall.Port,
dPort *firewall.Port,
@@ -331,23 +335,29 @@ func (r *router) AddRouteFiltering(
chain := r.chains[chainNameRoutingFw]
var exprs []expr.Any
+ var source firewall.Network
switch {
case len(sources) == 1 && sources[0].Bits() == 0:
// If it's 0.0.0.0/0, we don't need to add any source matching
case len(sources) == 1:
// If there's only one source, we can use it directly
- exprs = append(exprs, generateCIDRMatcherExpressions(true, sources[0])...)
+ source.Prefix = sources[0]
default:
- // If there are multiple sources, create or get an ipset
- var err error
- exprs, err = r.getIpSetExprs(sources, exprs)
- if err != nil {
- return nil, fmt.Errorf("get ipset expressions: %w", err)
- }
+ // If there are multiple sources, use a set
+ source.Set = firewall.NewPrefixSet(sources)
}
- // Handle destination
- exprs = append(exprs, generateCIDRMatcherExpressions(false, destination)...)
+ sourceExp, err := r.applyNetwork(source, sources, true)
+ if err != nil {
+ return nil, fmt.Errorf("apply source: %w", err)
+ }
+ exprs = append(exprs, sourceExp...)
+
+ destExp, err := r.applyNetwork(destination, nil, false)
+ if err != nil {
+ return nil, fmt.Errorf("apply destination: %w", err)
+ }
+ exprs = append(exprs, destExp...)
// Handle protocol
if proto != firewall.ProtocolALL {
@@ -391,39 +401,27 @@ func (r *router) AddRouteFiltering(
rule = r.conn.AddRule(rule)
}
- log.Tracef("Adding route rule %s", spew.Sdump(rule))
if err := r.conn.Flush(); err != nil {
return nil, fmt.Errorf(flushError, err)
}
r.rules[string(ruleKey)] = rule
- log.Debugf("nftables: added route rule: sources=%v, destination=%v, proto=%v, sPort=%v, dPort=%v, action=%v", sources, destination, proto, sPort, dPort, action)
+ log.Debugf("added route rule: sources=%v, destination=%v, proto=%v, sPort=%v, dPort=%v, action=%v", sources, destination, proto, sPort, dPort, action)
return ruleKey, nil
}
-func (r *router) getIpSetExprs(sources []netip.Prefix, exprs []expr.Any) ([]expr.Any, error) {
- setName := firewall.GenerateSetName(sources)
- ref, err := r.ipsetCounter.Increment(setName, sources)
+func (r *router) getIpSet(set firewall.Set, prefixes []netip.Prefix, isSource bool) ([]expr.Any, error) {
+ ref, err := r.ipsetCounter.Increment(set.HashedName(), setInput{
+ set: set,
+ prefixes: prefixes,
+ })
if err != nil {
- return nil, fmt.Errorf("create or get ipset for sources: %w", err)
+ return nil, fmt.Errorf("create or get ipset: %w", err)
}
- exprs = append(exprs,
- &expr.Payload{
- DestRegister: 1,
- Base: expr.PayloadBaseNetworkHeader,
- Offset: 12,
- Len: 4,
- },
- &expr.Lookup{
- SourceRegister: 1,
- SetName: ref.Out.Name,
- SetID: ref.Out.ID,
- },
- )
- return exprs, nil
+ return getIpSetExprs(ref, isSource)
}
func (r *router) DeleteRouteRule(rule firewall.Rule) error {
@@ -442,42 +440,54 @@ func (r *router) DeleteRouteRule(rule firewall.Rule) error {
return fmt.Errorf("route rule %s has no handle", ruleKey)
}
- setName := r.findSetNameInRule(nftRule)
-
if err := r.deleteNftRule(nftRule, ruleKey); err != nil {
return fmt.Errorf("delete: %w", err)
}
- if setName != "" {
- if _, err := r.ipsetCounter.Decrement(setName); err != nil {
- return fmt.Errorf("decrement ipset reference: %w", err)
- }
- }
-
if err := r.conn.Flush(); err != nil {
return fmt.Errorf(flushError, err)
}
+ if err := r.decrementSetCounter(nftRule); err != nil {
+ return fmt.Errorf("decrement set counter: %w", err)
+ }
+
return nil
}
-func (r *router) createIpSet(setName string, sources []netip.Prefix) (*nftables.Set, error) {
+func (r *router) createIpSet(setName string, input setInput) (*nftables.Set, error) {
// overlapping prefixes will result in an error, so we need to merge them
- sources = firewall.MergeIPRanges(sources)
+ prefixes := firewall.MergeIPRanges(input.prefixes)
- set := &nftables.Set{
- Name: setName,
- Table: r.workTable,
+ nfset := &nftables.Set{
+ Name: setName,
+ Comment: input.set.Comment(),
+ Table: r.workTable,
// required for prefixes
Interval: true,
KeyType: nftables.TypeIPAddr,
}
+ elements := convertPrefixesToSet(prefixes)
+ if err := r.conn.AddSet(nfset, elements); err != nil {
+ return nil, fmt.Errorf("error adding elements to set %s: %w", setName, err)
+ }
+
+ if err := r.conn.Flush(); err != nil {
+ return nil, fmt.Errorf("flush error: %w", err)
+ }
+
+ log.Printf("Created new ipset: %s with %d elements", setName, len(elements)/2)
+
+ return nfset, nil
+}
+
+func convertPrefixesToSet(prefixes []netip.Prefix) []nftables.SetElement {
var elements []nftables.SetElement
- for _, prefix := range sources {
+ for _, prefix := range prefixes {
// TODO: Implement IPv6 support
if prefix.Addr().Is6() {
- log.Printf("Skipping IPv6 prefix %s: IPv6 support not yet implemented", prefix)
+ log.Tracef("skipping IPv6 prefix %s: IPv6 support not yet implemented", prefix)
continue
}
@@ -493,18 +503,7 @@ func (r *router) createIpSet(setName string, sources []netip.Prefix) (*nftables.
nftables.SetElement{Key: lastIP.AsSlice(), IntervalEnd: true},
)
}
-
- if err := r.conn.AddSet(set, elements); err != nil {
- return nil, fmt.Errorf("error adding elements to set %s: %w", setName, err)
- }
-
- if err := r.conn.Flush(); err != nil {
- return nil, fmt.Errorf("flush error: %w", err)
- }
-
- log.Printf("Created new ipset: %s with %d elements", setName, len(elements)/2)
-
- return set, nil
+ return elements
}
// calculateLastIP determines the last IP in a given prefix.
@@ -528,8 +527,8 @@ func uint32ToBytes(ip uint32) [4]byte {
return b
}
-func (r *router) deleteIpSet(setName string, set *nftables.Set) error {
- r.conn.DelSet(set)
+func (r *router) deleteIpSet(setName string, nfset *nftables.Set) error {
+ r.conn.DelSet(nfset)
if err := r.conn.Flush(); err != nil {
return fmt.Errorf(flushError, err)
}
@@ -538,13 +537,27 @@ func (r *router) deleteIpSet(setName string, set *nftables.Set) error {
return nil
}
-func (r *router) findSetNameInRule(rule *nftables.Rule) string {
- for _, e := range rule.Exprs {
- if lookup, ok := e.(*expr.Lookup); ok {
- return lookup.SetName
+func (r *router) decrementSetCounter(rule *nftables.Rule) error {
+ sets := r.findSets(rule)
+
+ var merr *multierror.Error
+ for _, setName := range sets {
+ if _, err := r.ipsetCounter.Decrement(setName); err != nil {
+ merr = multierror.Append(merr, fmt.Errorf("decrement set counter: %w", err))
}
}
- return ""
+
+ return nberrors.FormatErrorOrNil(merr)
+}
+
+func (r *router) findSets(rule *nftables.Rule) []string {
+ var sets []string
+ for _, e := range rule.Exprs {
+ if lookup, ok := e.(*expr.Lookup); ok {
+ sets = append(sets, lookup.SetName)
+ }
+ }
+ return sets
}
func (r *router) deleteNftRule(rule *nftables.Rule, ruleKey string) error {
@@ -586,7 +599,8 @@ func (r *router) AddNatRule(pair firewall.RouterPair) error {
}
if err := r.conn.Flush(); err != nil {
- return fmt.Errorf("nftables: insert rules for %s: %v", pair.Destination, err)
+ // TODO: rollback ipset counter
+ return fmt.Errorf("insert rules for %s: %v", pair.Destination, err)
}
return nil
@@ -594,19 +608,22 @@ func (r *router) AddNatRule(pair firewall.RouterPair) error {
// addNatRule inserts a nftables rule to the conn client flush queue
func (r *router) addNatRule(pair firewall.RouterPair) error {
- sourceExp := generateCIDRMatcherExpressions(true, pair.Source)
- destExp := generateCIDRMatcherExpressions(false, pair.Destination)
+ sourceExp, err := r.applyNetwork(pair.Source, nil, true)
+ if err != nil {
+ return fmt.Errorf("apply source: %w", err)
+ }
+
+ destExp, err := r.applyNetwork(pair.Destination, nil, false)
+ if err != nil {
+ return fmt.Errorf("apply destination: %w", err)
+ }
op := expr.CmpOpEq
if pair.Inverse {
op = expr.CmpOpNeq
}
- // We only care about NEW connections to mark them and later identify them in the postrouting chain for masquerading.
- // Masquerading will take care of the conntrack state, which means we won't need to mark established connections.
- exprs := getCtNewExprs()
- exprs = append(exprs,
- // interface matching
+ exprs := []expr.Any{
&expr.Meta{
Key: expr.MetaKeyIIFNAME,
Register: 1,
@@ -616,7 +633,10 @@ func (r *router) addNatRule(pair firewall.RouterPair) error {
Register: 1,
Data: ifname(r.wgIface.Name()),
},
- )
+ }
+ // We only care about NEW connections to mark them and later identify them in the postrouting chain for masquerading.
+ // Masquerading will take care of the conntrack state, which means we won't need to mark established connections.
+ exprs = append(exprs, getCtNewExprs()...)
exprs = append(exprs, sourceExp...)
exprs = append(exprs, destExp...)
@@ -646,7 +666,9 @@ func (r *router) addNatRule(pair firewall.RouterPair) error {
}
}
- r.rules[ruleKey] = r.conn.AddRule(&nftables.Rule{
+ // Ensure nat rules come first, so the mark can be overwritten.
+ // Currently overwritten by the dst-type LOCAL rules for redirected traffic.
+ r.rules[ruleKey] = r.conn.InsertRule(&nftables.Rule{
Table: r.workTable,
Chain: r.chains[chainNameManglePrerouting],
Exprs: exprs,
@@ -729,8 +751,15 @@ func (r *router) addPostroutingRules() error {
// addLegacyRouteRule adds a legacy routing rule for mgmt servers pre route acls
func (r *router) addLegacyRouteRule(pair firewall.RouterPair) error {
- sourceExp := generateCIDRMatcherExpressions(true, pair.Source)
- destExp := generateCIDRMatcherExpressions(false, pair.Destination)
+ sourceExp, err := r.applyNetwork(pair.Source, nil, true)
+ if err != nil {
+ return fmt.Errorf("apply source: %w", err)
+ }
+
+ destExp, err := r.applyNetwork(pair.Destination, nil, false)
+ if err != nil {
+ return fmt.Errorf("apply destination: %w", err)
+ }
exprs := []expr.Any{
&expr.Counter{},
@@ -739,7 +768,8 @@ func (r *router) addLegacyRouteRule(pair firewall.RouterPair) error {
},
}
- expression := append(sourceExp, append(destExp, exprs...)...) // nolint:gocritic
+ exprs = append(exprs, sourceExp...)
+ exprs = append(exprs, destExp...)
ruleKey := firewall.GenKey(firewall.ForwardingFormat, pair)
@@ -752,7 +782,7 @@ func (r *router) addLegacyRouteRule(pair firewall.RouterPair) error {
r.rules[ruleKey] = r.conn.AddRule(&nftables.Rule{
Table: r.workTable,
Chain: r.chains[chainNameRoutingFw],
- Exprs: expression,
+ Exprs: exprs,
UserData: []byte(ruleKey),
})
return nil
@@ -767,11 +797,13 @@ func (r *router) removeLegacyRouteRule(pair firewall.RouterPair) error {
return fmt.Errorf("remove legacy forwarding rule %s -> %s: %v", pair.Source, pair.Destination, err)
}
- log.Debugf("nftables: removed legacy forwarding rule %s -> %s", pair.Source, pair.Destination)
+ log.Debugf("removed legacy forwarding rule %s -> %s", pair.Source, pair.Destination)
delete(r.rules, ruleKey)
- } else {
- log.Debugf("nftables: legacy forwarding rule %s not found", ruleKey)
+
+ if err := r.decrementSetCounter(rule); err != nil {
+ return fmt.Errorf("decrement set counter: %w", err)
+ }
}
return nil
@@ -982,12 +1014,14 @@ func (r *router) RemoveNatRule(pair firewall.RouterPair) error {
return fmt.Errorf(refreshRulesMapError, err)
}
- if err := r.removeNatRule(pair); err != nil {
- return fmt.Errorf("remove prerouting rule: %w", err)
- }
+ if pair.Masquerade {
+ if err := r.removeNatRule(pair); err != nil {
+ return fmt.Errorf("remove prerouting rule: %w", err)
+ }
- if err := r.removeNatRule(firewall.GetInversePair(pair)); err != nil {
- return fmt.Errorf("remove inverse prerouting rule: %w", err)
+ if err := r.removeNatRule(firewall.GetInversePair(pair)); err != nil {
+ return fmt.Errorf("remove inverse prerouting rule: %w", err)
+ }
}
if err := r.removeLegacyRouteRule(pair); err != nil {
@@ -995,10 +1029,10 @@ func (r *router) RemoveNatRule(pair firewall.RouterPair) error {
}
if err := r.conn.Flush(); err != nil {
- return fmt.Errorf("nftables: received error while applying rule removal for %s: %v", pair.Destination, err)
+ // TODO: rollback set counter
+ return fmt.Errorf("remove nat rules rule %s: %v", pair.Destination, err)
}
- log.Debugf("nftables: removed nat rules for %s", pair.Destination)
return nil
}
@@ -1006,16 +1040,19 @@ func (r *router) removeNatRule(pair firewall.RouterPair) error {
ruleKey := firewall.GenKey(firewall.PreroutingFormat, pair)
if rule, exists := r.rules[ruleKey]; exists {
- err := r.conn.DelRule(rule)
- if err != nil {
+ if err := r.conn.DelRule(rule); err != nil {
return fmt.Errorf("remove prerouting rule %s -> %s: %v", pair.Source, pair.Destination, err)
}
- log.Debugf("nftables: removed prerouting rule %s -> %s", pair.Source, pair.Destination)
+ log.Debugf("removed prerouting rule %s -> %s", pair.Source, pair.Destination)
delete(r.rules, ruleKey)
+
+ if err := r.decrementSetCounter(rule); err != nil {
+ return fmt.Errorf("decrement set counter: %w", err)
+ }
} else {
- log.Debugf("nftables: prerouting rule %s not found", ruleKey)
+ log.Debugf("prerouting rule %s not found", ruleKey)
}
return nil
@@ -1027,7 +1064,7 @@ func (r *router) refreshRulesMap() error {
for _, chain := range r.chains {
rules, err := r.conn.GetRules(chain.Table, chain)
if err != nil {
- return fmt.Errorf("nftables: unable to list rules: %v", err)
+ return fmt.Errorf(" unable to list rules: %v", err)
}
for _, rule := range rules {
if len(rule.UserData) > 0 {
@@ -1301,13 +1338,54 @@ func (r *router) DeleteDNATRule(rule firewall.Rule) error {
return nberrors.FormatErrorOrNil(merr)
}
-// generateCIDRMatcherExpressions generates nftables expressions that matches a CIDR
-func generateCIDRMatcherExpressions(source bool, prefix netip.Prefix) []expr.Any {
- var offset uint32
- if source {
- offset = 12 // src offset
- } else {
- offset = 16 // dst offset
+func (r *router) UpdateSet(set firewall.Set, prefixes []netip.Prefix) error {
+ nfset, err := r.conn.GetSetByName(r.workTable, set.HashedName())
+ if err != nil {
+ return fmt.Errorf("get set %s: %w", set.HashedName(), err)
+ }
+
+ elements := convertPrefixesToSet(prefixes)
+ if err := r.conn.SetAddElements(nfset, elements); err != nil {
+ return fmt.Errorf("add elements to set %s: %w", set.HashedName(), err)
+ }
+
+ if err := r.conn.Flush(); err != nil {
+ return fmt.Errorf(flushError, err)
+ }
+
+ log.Debugf("updated set %s with prefixes %v", set.HashedName(), prefixes)
+
+ return nil
+}
+
+// applyNetwork generates nftables expressions for networks (CIDR) or sets
+func (r *router) applyNetwork(
+ network firewall.Network,
+ setPrefixes []netip.Prefix,
+ isSource bool,
+) ([]expr.Any, error) {
+ if network.IsSet() {
+ exprs, err := r.getIpSet(network.Set, setPrefixes, isSource)
+ if err != nil {
+ return nil, fmt.Errorf("source: %w", err)
+ }
+ return exprs, nil
+ }
+
+ if network.IsPrefix() {
+ return applyPrefix(network.Prefix, isSource), nil
+ }
+
+ return nil, nil
+}
+
+// applyPrefix generates nftables expressions for a CIDR prefix
+func applyPrefix(prefix netip.Prefix, isSource bool) []expr.Any {
+ // dst offset
+ offset := uint32(16)
+ if isSource {
+ // src offset
+ offset = 12
}
ones := prefix.Bits()
@@ -1415,3 +1493,27 @@ func getCtNewExprs() []expr.Any {
},
}
}
+
+func getIpSetExprs(ref refcounter.Ref[*nftables.Set], isSource bool) ([]expr.Any, error) {
+
+ // dst offset
+ offset := uint32(16)
+ if isSource {
+ // src offset
+ offset = 12
+ }
+
+ return []expr.Any{
+ &expr.Payload{
+ DestRegister: 1,
+ Base: expr.PayloadBaseNetworkHeader,
+ Offset: offset,
+ Len: 4,
+ },
+ &expr.Lookup{
+ SourceRegister: 1,
+ SetName: ref.Out.Name,
+ SetID: ref.Out.ID,
+ },
+ }, nil
+}
diff --git a/client/firewall/nftables/router_linux_test.go b/client/firewall/nftables/router_linux_test.go
index 28baef4dd..4fdbf3505 100644
--- a/client/firewall/nftables/router_linux_test.go
+++ b/client/firewall/nftables/router_linux_test.go
@@ -88,8 +88,8 @@ func TestNftablesManager_AddNatRule(t *testing.T) {
}
// Build CIDR matching expressions
- sourceExp := generateCIDRMatcherExpressions(true, testCase.InputPair.Source)
- destExp := generateCIDRMatcherExpressions(false, testCase.InputPair.Destination)
+ sourceExp := applyPrefix(testCase.InputPair.Source.Prefix, true)
+ destExp := applyPrefix(testCase.InputPair.Destination.Prefix, false)
// Combine all expressions in the correct order
// nolint:gocritic
@@ -311,7 +311,7 @@ func TestRouter_AddRouteFiltering(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- ruleKey, err := r.AddRouteFiltering(nil, tt.sources, tt.destination, tt.proto, tt.sPort, tt.dPort, tt.action)
+ ruleKey, err := r.AddRouteFiltering(nil, tt.sources, firewall.Network{Prefix: tt.destination}, tt.proto, tt.sPort, tt.dPort, tt.action)
require.NoError(t, err, "AddRouteFiltering failed")
t.Cleanup(func() {
@@ -441,8 +441,8 @@ func TestNftablesCreateIpSet(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- setName := firewall.GenerateSetName(tt.sources)
- set, err := r.createIpSet(setName, tt.sources)
+ setName := firewall.NewPrefixSet(tt.sources).HashedName()
+ set, err := r.createIpSet(setName, setInput{prefixes: tt.sources})
if err != nil {
t.Logf("Failed to create IP set: %v", err)
printNftSets()
diff --git a/client/firewall/test/cases_linux.go b/client/firewall/test/cases_linux.go
index 267e93efd..59a370a97 100644
--- a/client/firewall/test/cases_linux.go
+++ b/client/firewall/test/cases_linux.go
@@ -15,8 +15,8 @@ var (
Name: "Insert Forwarding IPV4 Rule",
InputPair: firewall.RouterPair{
ID: "zxa",
- Source: netip.MustParsePrefix("100.100.100.1/32"),
- Destination: netip.MustParsePrefix("100.100.200.0/24"),
+ Source: firewall.Network{Prefix: netip.MustParsePrefix("100.100.100.1/32")},
+ Destination: firewall.Network{Prefix: netip.MustParsePrefix("100.100.200.0/24")},
Masquerade: false,
},
},
@@ -24,8 +24,8 @@ var (
Name: "Insert Forwarding And Nat IPV4 Rules",
InputPair: firewall.RouterPair{
ID: "zxa",
- Source: netip.MustParsePrefix("100.100.100.1/32"),
- Destination: netip.MustParsePrefix("100.100.200.0/24"),
+ Source: firewall.Network{Prefix: netip.MustParsePrefix("100.100.100.1/32")},
+ Destination: firewall.Network{Prefix: netip.MustParsePrefix("100.100.200.0/24")},
Masquerade: true,
},
},
@@ -40,8 +40,8 @@ var (
Name: "Remove Forwarding And Nat IPV4 Rules",
InputPair: firewall.RouterPair{
ID: "zxa",
- Source: netip.MustParsePrefix("100.100.100.1/32"),
- Destination: netip.MustParsePrefix("100.100.200.0/24"),
+ Source: firewall.Network{Prefix: netip.MustParsePrefix("100.100.100.1/32")},
+ Destination: firewall.Network{Prefix: netip.MustParsePrefix("100.100.200.0/24")},
Masquerade: true,
},
},
diff --git a/client/firewall/uspfilter/allow_netbird.go b/client/firewall/uspfilter/allow_netbird.go
index 5fe698aa9..ce04c82c7 100644
--- a/client/firewall/uspfilter/allow_netbird.go
+++ b/client/firewall/uspfilter/allow_netbird.go
@@ -12,7 +12,7 @@ import (
"github.com/netbirdio/netbird/client/internal/statemanager"
)
-// Reset firewall to the default state
+// Close cleans up the firewall manager by removing all rules and closing trackers
func (m *Manager) Close(stateManager *statemanager.Manager) error {
m.mutex.Lock()
defer m.mutex.Unlock()
diff --git a/client/firewall/uspfilter/allow_netbird_windows.go b/client/firewall/uspfilter/allow_netbird_windows.go
index f63792fec..f261c472f 100644
--- a/client/firewall/uspfilter/allow_netbird_windows.go
+++ b/client/firewall/uspfilter/allow_netbird_windows.go
@@ -10,7 +10,6 @@ import (
log "github.com/sirupsen/logrus"
- "github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack"
"github.com/netbirdio/netbird/client/internal/statemanager"
)
@@ -22,7 +21,7 @@ const (
firewallRuleName = "Netbird"
)
-// Reset firewall to the default state
+// Close cleans up the firewall manager by removing all rules and closing trackers
func (m *Manager) Close(*statemanager.Manager) error {
m.mutex.Lock()
defer m.mutex.Unlock()
@@ -32,17 +31,14 @@ func (m *Manager) Close(*statemanager.Manager) error {
if m.udpTracker != nil {
m.udpTracker.Close()
- m.udpTracker = conntrack.NewUDPTracker(conntrack.DefaultUDPTimeout, m.logger, m.flowLogger)
}
if m.icmpTracker != nil {
m.icmpTracker.Close()
- m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout, m.logger, m.flowLogger)
}
if m.tcpTracker != nil {
m.tcpTracker.Close()
- m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout, m.logger, m.flowLogger)
}
if fwder := m.forwarder.Load(); fwder != nil {
diff --git a/client/firewall/uspfilter/forwarder/forwarder.go b/client/firewall/uspfilter/forwarder/forwarder.go
index 0dff3acc7..2ae983f6e 100644
--- a/client/firewall/uspfilter/forwarder/forwarder.go
+++ b/client/firewall/uspfilter/forwarder/forwarder.go
@@ -4,7 +4,9 @@ import (
"context"
"fmt"
"net"
+ "net/netip"
"runtime"
+ "sync"
log "github.com/sirupsen/logrus"
"gvisor.dev/gvisor/pkg/buffer"
@@ -17,6 +19,7 @@ import (
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
"github.com/netbirdio/netbird/client/firewall/uspfilter/common"
+ "github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack"
nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log"
nftypes "github.com/netbirdio/netbird/client/internal/netflow/types"
)
@@ -29,8 +32,10 @@ const (
)
type Forwarder struct {
- logger *nblog.Logger
- flowLogger nftypes.FlowLogger
+ logger *nblog.Logger
+ flowLogger nftypes.FlowLogger
+ // ruleIdMap is used to store the rule ID for a given connection
+ ruleIdMap sync.Map
stack *stack.Stack
endpoint *endpoint
udpForwarder *udpForwarder
@@ -167,3 +172,35 @@ func (f *Forwarder) determineDialAddr(addr tcpip.Address) net.IP {
}
return addr.AsSlice()
}
+
+func (f *Forwarder) RegisterRuleID(srcIP, dstIP netip.Addr, srcPort, dstPort uint16, ruleID []byte) {
+ key := buildKey(srcIP, dstIP, srcPort, dstPort)
+ f.ruleIdMap.LoadOrStore(key, ruleID)
+}
+
+func (f *Forwarder) getRuleID(srcIP, dstIP netip.Addr, srcPort, dstPort uint16) ([]byte, bool) {
+
+ if value, ok := f.ruleIdMap.Load(buildKey(srcIP, dstIP, srcPort, dstPort)); ok {
+ return value.([]byte), true
+ } else if value, ok := f.ruleIdMap.Load(buildKey(dstIP, srcIP, dstPort, srcPort)); ok {
+ return value.([]byte), true
+ }
+
+ return nil, false
+}
+
+func (f *Forwarder) DeleteRuleID(srcIP, dstIP netip.Addr, srcPort, dstPort uint16) {
+ if _, ok := f.ruleIdMap.LoadAndDelete(buildKey(srcIP, dstIP, srcPort, dstPort)); ok {
+ return
+ }
+ f.ruleIdMap.LoadAndDelete(buildKey(dstIP, srcIP, dstPort, srcPort))
+}
+
+func buildKey(srcIP, dstIP netip.Addr, srcPort, dstPort uint16) conntrack.ConnKey {
+ return conntrack.ConnKey{
+ SrcIP: srcIP,
+ DstIP: dstIP,
+ SrcPort: srcPort,
+ DstPort: dstPort,
+ }
+}
diff --git a/client/firewall/uspfilter/forwarder/icmp.go b/client/firewall/uspfilter/forwarder/icmp.go
index a21ec2c87..08d77ed05 100644
--- a/client/firewall/uspfilter/forwarder/icmp.go
+++ b/client/firewall/uspfilter/forwarder/icmp.go
@@ -25,7 +25,7 @@ func (f *Forwarder) handleICMP(id stack.TransportEndpointID, pkt stack.PacketBuf
}
flowID := uuid.New()
- f.sendICMPEvent(nftypes.TypeStart, flowID, id, icmpType, icmpCode)
+ f.sendICMPEvent(nftypes.TypeStart, flowID, id, icmpType, icmpCode, 0, 0)
ctx, cancel := context.WithTimeout(f.ctx, 5*time.Second)
defer cancel()
@@ -34,14 +34,14 @@ func (f *Forwarder) handleICMP(id stack.TransportEndpointID, pkt stack.PacketBuf
// TODO: support non-root
conn, err := lc.ListenPacket(ctx, "ip4:icmp", "0.0.0.0")
if err != nil {
- f.logger.Error("Failed to create ICMP socket for %v: %v", epID(id), err)
+ f.logger.Error("forwarder: Failed to create ICMP socket for %v: %v", epID(id), err)
// This will make netstack reply on behalf of the original destination, that's ok for now
return false
}
defer func() {
if err := conn.Close(); err != nil {
- f.logger.Debug("Failed to close ICMP socket: %v", err)
+ f.logger.Debug("forwarder: Failed to close ICMP socket: %v", err)
}
}()
@@ -52,36 +52,37 @@ func (f *Forwarder) handleICMP(id stack.TransportEndpointID, pkt stack.PacketBuf
payload := fullPacket.AsSlice()
if _, err = conn.WriteTo(payload, dst); err != nil {
- f.logger.Error("Failed to write ICMP packet for %v: %v", epID(id), err)
+ f.logger.Error("forwarder: Failed to write ICMP packet for %v: %v", epID(id), err)
return true
}
- f.logger.Trace("Forwarded ICMP packet %v type %v code %v",
+ f.logger.Trace("forwarder: Forwarded ICMP packet %v type %v code %v",
epID(id), icmpHdr.Type(), icmpHdr.Code())
// For Echo Requests, send and handle response
if header.ICMPv4Type(icmpType) == header.ICMPv4Echo {
- f.handleEchoResponse(icmpHdr, conn, id)
- f.sendICMPEvent(nftypes.TypeEnd, flowID, id, icmpType, icmpCode)
+ rxBytes := pkt.Size()
+ txBytes := f.handleEchoResponse(icmpHdr, conn, id)
+ f.sendICMPEvent(nftypes.TypeEnd, flowID, id, icmpType, icmpCode, uint64(rxBytes), uint64(txBytes))
}
// For other ICMP types (Time Exceeded, Destination Unreachable, etc) do nothing
return true
}
-func (f *Forwarder) handleEchoResponse(icmpHdr header.ICMPv4, conn net.PacketConn, id stack.TransportEndpointID) {
+func (f *Forwarder) handleEchoResponse(icmpHdr header.ICMPv4, conn net.PacketConn, id stack.TransportEndpointID) int {
if err := conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil {
- f.logger.Error("Failed to set read deadline for ICMP response: %v", err)
- return
+ f.logger.Error("forwarder: Failed to set read deadline for ICMP response: %v", err)
+ return 0
}
response := make([]byte, f.endpoint.mtu)
n, _, err := conn.ReadFrom(response)
if err != nil {
if !isTimeout(err) {
- f.logger.Error("Failed to read ICMP response: %v", err)
+ f.logger.Error("forwarder: Failed to read ICMP response: %v", err)
}
- return
+ return 0
}
ipHdr := make([]byte, header.IPv4MinimumSize)
@@ -100,28 +101,54 @@ func (f *Forwarder) handleEchoResponse(icmpHdr header.ICMPv4, conn net.PacketCon
fullPacket = append(fullPacket, response[:n]...)
if err := f.InjectIncomingPacket(fullPacket); err != nil {
- f.logger.Error("Failed to inject ICMP response: %v", err)
+ f.logger.Error("forwarder: Failed to inject ICMP response: %v", err)
- return
+ return 0
}
- f.logger.Trace("Forwarded ICMP echo reply for %v type %v code %v",
+ f.logger.Trace("forwarder: Forwarded ICMP echo reply for %v type %v code %v",
epID(id), icmpHdr.Type(), icmpHdr.Code())
+
+ return len(fullPacket)
}
// sendICMPEvent stores flow events for ICMP packets
-func (f *Forwarder) sendICMPEvent(typ nftypes.Type, flowID uuid.UUID, id stack.TransportEndpointID, icmpType, icmpCode uint8) {
- f.flowLogger.StoreEvent(nftypes.EventFields{
+func (f *Forwarder) sendICMPEvent(typ nftypes.Type, flowID uuid.UUID, id stack.TransportEndpointID, icmpType, icmpCode uint8, rxBytes, txBytes uint64) {
+ var rxPackets, txPackets uint64
+ if rxBytes > 0 {
+ rxPackets = 1
+ }
+ if txBytes > 0 {
+ txPackets = 1
+ }
+
+ srcIp := netip.AddrFrom4(id.RemoteAddress.As4())
+ dstIp := netip.AddrFrom4(id.LocalAddress.As4())
+
+ fields := nftypes.EventFields{
FlowID: flowID,
Type: typ,
Direction: nftypes.Ingress,
Protocol: nftypes.ICMP,
// TODO: handle ipv6
- SourceIP: netip.AddrFrom4(id.RemoteAddress.As4()),
- DestIP: netip.AddrFrom4(id.LocalAddress.As4()),
+ SourceIP: srcIp,
+ DestIP: dstIp,
ICMPType: icmpType,
ICMPCode: icmpCode,
- // TODO: get packets/bytes
- })
+ RxBytes: rxBytes,
+ TxBytes: txBytes,
+ RxPackets: rxPackets,
+ TxPackets: txPackets,
+ }
+
+ if typ == nftypes.TypeStart {
+ if ruleId, ok := f.getRuleID(srcIp, dstIp, id.RemotePort, id.LocalPort); ok {
+ fields.RuleID = ruleId
+ }
+ } else {
+ f.DeleteRuleID(srcIp, dstIp, id.RemotePort, id.LocalPort)
+ }
+
+ f.flowLogger.StoreEvent(fields)
}
diff --git a/client/firewall/uspfilter/forwarder/tcp.go b/client/firewall/uspfilter/forwarder/tcp.go
index 71cd457ef..04b3ae233 100644
--- a/client/firewall/uspfilter/forwarder/tcp.go
+++ b/client/firewall/uspfilter/forwarder/tcp.go
@@ -6,8 +6,10 @@ import (
"io"
"net"
"net/netip"
+ "sync"
"github.com/google/uuid"
+
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
"gvisor.dev/gvisor/pkg/tcpip/stack"
@@ -23,11 +25,11 @@ func (f *Forwarder) handleTCP(r *tcp.ForwarderRequest) {
flowID := uuid.New()
- f.sendTCPEvent(nftypes.TypeStart, flowID, id, nil)
+ f.sendTCPEvent(nftypes.TypeStart, flowID, id, 0, 0, 0, 0)
var success bool
defer func() {
if !success {
- f.sendTCPEvent(nftypes.TypeEnd, flowID, id, nil)
+ f.sendTCPEvent(nftypes.TypeEnd, flowID, id, 0, 0, 0, 0)
}
}()
@@ -65,67 +67,97 @@ func (f *Forwarder) handleTCP(r *tcp.ForwarderRequest) {
}
func (f *Forwarder) proxyTCP(id stack.TransportEndpointID, inConn *gonet.TCPConn, outConn net.Conn, ep tcpip.Endpoint, flowID uuid.UUID) {
- defer func() {
- if err := inConn.Close(); err != nil {
- f.logger.Debug("forwarder: inConn close error: %v", err)
- }
- if err := outConn.Close(); err != nil {
- f.logger.Debug("forwarder: outConn close error: %v", err)
- }
- ep.Close()
- f.sendTCPEvent(nftypes.TypeEnd, flowID, id, ep)
- }()
-
- // Create context for managing the proxy goroutines
ctx, cancel := context.WithCancel(f.ctx)
defer cancel()
- errChan := make(chan error, 2)
-
go func() {
- _, err := io.Copy(outConn, inConn)
- errChan <- err
- }()
-
- go func() {
- _, err := io.Copy(inConn, outConn)
- errChan <- err
- }()
-
- select {
- case <-ctx.Done():
- f.logger.Trace("forwarder: tearing down TCP connection %v due to context done", epID(id))
- return
- case err := <-errChan:
- if err != nil && !isClosedError(err) {
- f.logger.Error("proxyTCP: copy error: %v", err)
+ <-ctx.Done()
+ // Close connections and endpoint.
+ if err := inConn.Close(); err != nil && !isClosedError(err) {
+ f.logger.Debug("forwarder: inConn close error: %v", err)
+ }
+ if err := outConn.Close(); err != nil && !isClosedError(err) {
+ f.logger.Debug("forwarder: outConn close error: %v", err)
+ }
+
+ ep.Close()
+ }()
+
+ var wg sync.WaitGroup
+ wg.Add(2)
+
+ var (
+ bytesFromInToOut int64 // bytes from client to server (tx for client)
+ bytesFromOutToIn int64 // bytes from server to client (rx for client)
+ errInToOut error
+ errOutToIn error
+ )
+
+ go func() {
+ bytesFromInToOut, errInToOut = io.Copy(outConn, inConn)
+ cancel()
+ wg.Done()
+ }()
+
+ go func() {
+
+ bytesFromOutToIn, errOutToIn = io.Copy(inConn, outConn)
+ cancel()
+ wg.Done()
+ }()
+
+ wg.Wait()
+
+ if errInToOut != nil {
+ if !isClosedError(errInToOut) {
+ f.logger.Error("proxyTCP: copy error (in -> out): %v", errInToOut)
}
- f.logger.Trace("forwarder: tearing down TCP connection %v", epID(id))
- return
}
+ if errOutToIn != nil {
+ if !isClosedError(errOutToIn) {
+ f.logger.Error("proxyTCP: copy error (out -> in): %v", errOutToIn)
+ }
+ }
+
+ var rxPackets, txPackets uint64
+ if tcpStats, ok := ep.Stats().(*tcp.Stats); ok {
+ // fields are flipped since this is the in conn
+ rxPackets = tcpStats.SegmentsSent.Value()
+ txPackets = tcpStats.SegmentsReceived.Value()
+ }
+
+ f.logger.Trace("forwarder: Removed TCP connection %s [in: %d Pkts/%d B, out: %d Pkts/%d B]", epID(id), rxPackets, bytesFromOutToIn, txPackets, bytesFromInToOut)
+
+ f.sendTCPEvent(nftypes.TypeEnd, flowID, id, uint64(bytesFromOutToIn), uint64(bytesFromInToOut), rxPackets, txPackets)
}
-func (f *Forwarder) sendTCPEvent(typ nftypes.Type, flowID uuid.UUID, id stack.TransportEndpointID, ep tcpip.Endpoint) {
+func (f *Forwarder) sendTCPEvent(typ nftypes.Type, flowID uuid.UUID, id stack.TransportEndpointID, rxBytes, txBytes, rxPackets, txPackets uint64) {
+ srcIp := netip.AddrFrom4(id.RemoteAddress.As4())
+ dstIp := netip.AddrFrom4(id.LocalAddress.As4())
+
fields := nftypes.EventFields{
FlowID: flowID,
Type: typ,
Direction: nftypes.Ingress,
Protocol: nftypes.TCP,
// TODO: handle ipv6
- SourceIP: netip.AddrFrom4(id.RemoteAddress.As4()),
- DestIP: netip.AddrFrom4(id.LocalAddress.As4()),
+ SourceIP: srcIp,
+ DestIP: dstIp,
SourcePort: id.RemotePort,
DestPort: id.LocalPort,
+ RxBytes: rxBytes,
+ TxBytes: txBytes,
+ RxPackets: rxPackets,
+ TxPackets: txPackets,
}
- if ep != nil {
- if tcpStats, ok := ep.Stats().(*tcp.Stats); ok {
- // fields are flipped since this is the in conn
- // TODO: get bytes
- fields.RxPackets = tcpStats.SegmentsSent.Value()
- fields.TxPackets = tcpStats.SegmentsReceived.Value()
+ if typ == nftypes.TypeStart {
+ if ruleId, ok := f.getRuleID(srcIp, dstIp, id.RemotePort, id.LocalPort); ok {
+ fields.RuleID = ruleId
}
+ } else {
+ f.DeleteRuleID(srcIp, dstIp, id.RemotePort, id.LocalPort)
}
f.flowLogger.StoreEvent(fields)
diff --git a/client/firewall/uspfilter/forwarder/udp.go b/client/firewall/uspfilter/forwarder/udp.go
index 7ce85e2b6..cb88aa59a 100644
--- a/client/firewall/uspfilter/forwarder/udp.go
+++ b/client/firewall/uspfilter/forwarder/udp.go
@@ -149,11 +149,11 @@ func (f *Forwarder) handleUDP(r *udp.ForwarderRequest) {
flowID := uuid.New()
- f.sendUDPEvent(nftypes.TypeStart, flowID, id, nil)
+ f.sendUDPEvent(nftypes.TypeStart, flowID, id, 0, 0, 0, 0)
var success bool
defer func() {
if !success {
- f.sendUDPEvent(nftypes.TypeEnd, flowID, id, nil)
+ f.sendUDPEvent(nftypes.TypeEnd, flowID, id, 0, 0, 0, 0)
}
}()
@@ -199,7 +199,6 @@ func (f *Forwarder) handleUDP(r *udp.ForwarderRequest) {
if err := outConn.Close(); err != nil {
f.logger.Debug("forwarder: UDP outConn close error for %v: %v", epID(id), err)
}
-
return
}
f.udpForwarder.conns[id] = pConn
@@ -212,68 +211,94 @@ func (f *Forwarder) handleUDP(r *udp.ForwarderRequest) {
}
func (f *Forwarder) proxyUDP(ctx context.Context, pConn *udpPacketConn, id stack.TransportEndpointID, ep tcpip.Endpoint) {
- defer func() {
+
+ ctx, cancel := context.WithCancel(f.ctx)
+ defer cancel()
+
+ go func() {
+ <-ctx.Done()
+
pConn.cancel()
- if err := pConn.conn.Close(); err != nil {
+ if err := pConn.conn.Close(); err != nil && !isClosedError(err) {
f.logger.Debug("forwarder: UDP inConn close error for %v: %v", epID(id), err)
}
- if err := pConn.outConn.Close(); err != nil {
+ if err := pConn.outConn.Close(); err != nil && !isClosedError(err) {
f.logger.Debug("forwarder: UDP outConn close error for %v: %v", epID(id), err)
}
ep.Close()
-
- f.udpForwarder.Lock()
- delete(f.udpForwarder.conns, id)
- f.udpForwarder.Unlock()
-
- f.sendUDPEvent(nftypes.TypeEnd, pConn.flowID, id, ep)
}()
- errChan := make(chan error, 2)
+ var wg sync.WaitGroup
+ wg.Add(2)
+ var txBytes, rxBytes int64
+ var outboundErr, inboundErr error
+
+ // outbound->inbound: copy from pConn.conn to pConn.outConn
go func() {
- errChan <- pConn.copy(ctx, pConn.conn, pConn.outConn, &f.udpForwarder.bufPool, "outbound->inbound")
+ defer wg.Done()
+ txBytes, outboundErr = pConn.copy(ctx, pConn.conn, pConn.outConn, &f.udpForwarder.bufPool, "outbound->inbound")
}()
+ // inbound->outbound: copy from pConn.outConn to pConn.conn
go func() {
- errChan <- pConn.copy(ctx, pConn.outConn, pConn.conn, &f.udpForwarder.bufPool, "inbound->outbound")
+ defer wg.Done()
+ rxBytes, inboundErr = pConn.copy(ctx, pConn.outConn, pConn.conn, &f.udpForwarder.bufPool, "inbound->outbound")
}()
- select {
- case <-ctx.Done():
- f.logger.Trace("forwarder: tearing down UDP connection %v due to context done", epID(id))
- return
- case err := <-errChan:
- if err != nil && !isClosedError(err) {
- f.logger.Error("proxyUDP: copy error: %v", err)
- }
- f.logger.Trace("forwarder: tearing down UDP connection %v", epID(id))
- return
+ wg.Wait()
+
+ if outboundErr != nil && !isClosedError(outboundErr) {
+ f.logger.Error("proxyUDP: copy error (outbound->inbound): %v", outboundErr)
}
+ if inboundErr != nil && !isClosedError(inboundErr) {
+ f.logger.Error("proxyUDP: copy error (inbound->outbound): %v", inboundErr)
+ }
+
+ var rxPackets, txPackets uint64
+ if udpStats, ok := ep.Stats().(*tcpip.TransportEndpointStats); ok {
+ // fields are flipped since this is the in conn
+ rxPackets = udpStats.PacketsSent.Value()
+ txPackets = udpStats.PacketsReceived.Value()
+ }
+
+ f.logger.Trace("forwarder: Removed UDP connection %s [in: %d Pkts/%d B, out: %d Pkts/%d B]", epID(id), rxPackets, rxBytes, txPackets, txBytes)
+
+ f.udpForwarder.Lock()
+ delete(f.udpForwarder.conns, id)
+ f.udpForwarder.Unlock()
+
+ f.sendUDPEvent(nftypes.TypeEnd, pConn.flowID, id, uint64(rxBytes), uint64(txBytes), rxPackets, txPackets)
}
// sendUDPEvent stores flow events for UDP connections
-func (f *Forwarder) sendUDPEvent(typ nftypes.Type, flowID uuid.UUID, id stack.TransportEndpointID, ep tcpip.Endpoint) {
+func (f *Forwarder) sendUDPEvent(typ nftypes.Type, flowID uuid.UUID, id stack.TransportEndpointID, rxBytes, txBytes, rxPackets, txPackets uint64) {
+ srcIp := netip.AddrFrom4(id.RemoteAddress.As4())
+ dstIp := netip.AddrFrom4(id.LocalAddress.As4())
+
fields := nftypes.EventFields{
FlowID: flowID,
Type: typ,
Direction: nftypes.Ingress,
Protocol: nftypes.UDP,
// TODO: handle ipv6
- SourceIP: netip.AddrFrom4(id.RemoteAddress.As4()),
- DestIP: netip.AddrFrom4(id.LocalAddress.As4()),
+ SourceIP: srcIp,
+ DestIP: dstIp,
SourcePort: id.RemotePort,
DestPort: id.LocalPort,
+ RxBytes: rxBytes,
+ TxBytes: txBytes,
+ RxPackets: rxPackets,
+ TxPackets: txPackets,
}
- if ep != nil {
- if tcpStats, ok := ep.Stats().(*tcpip.TransportEndpointStats); ok {
- // fields are flipped since this is the in conn
- // TODO: get bytes
- fields.RxPackets = tcpStats.PacketsSent.Value()
- fields.TxPackets = tcpStats.PacketsReceived.Value()
+ if typ == nftypes.TypeStart {
+ if ruleId, ok := f.getRuleID(srcIp, dstIp, id.RemotePort, id.LocalPort); ok {
+ fields.RuleID = ruleId
}
+ } else {
+ f.DeleteRuleID(srcIp, dstIp, id.RemotePort, id.LocalPort)
}
f.flowLogger.StoreEvent(fields)
@@ -288,18 +313,20 @@ func (c *udpPacketConn) getIdleDuration() time.Duration {
return time.Since(lastSeen)
}
-func (c *udpPacketConn) copy(ctx context.Context, dst net.Conn, src net.Conn, bufPool *sync.Pool, direction string) error {
+// copy reads from src and writes to dst.
+func (c *udpPacketConn) copy(ctx context.Context, dst net.Conn, src net.Conn, bufPool *sync.Pool, direction string) (int64, error) {
bufp := bufPool.Get().(*[]byte)
defer bufPool.Put(bufp)
buffer := *bufp
+ var totalBytes int64 = 0
for {
if ctx.Err() != nil {
- return ctx.Err()
+ return totalBytes, ctx.Err()
}
if err := src.SetDeadline(time.Now().Add(udpTimeout)); err != nil {
- return fmt.Errorf("set read deadline: %w", err)
+ return totalBytes, fmt.Errorf("set read deadline: %w", err)
}
n, err := src.Read(buffer)
@@ -307,14 +334,15 @@ func (c *udpPacketConn) copy(ctx context.Context, dst net.Conn, src net.Conn, bu
if isTimeout(err) {
continue
}
- return fmt.Errorf("read from %s: %w", direction, err)
+ return totalBytes, fmt.Errorf("read from %s: %w", direction, err)
}
- _, err = dst.Write(buffer[:n])
+ nWritten, err := dst.Write(buffer[:n])
if err != nil {
- return fmt.Errorf("write to %s: %w", direction, err)
+ return totalBytes, fmt.Errorf("write to %s: %w", direction, err)
}
+ totalBytes += int64(nWritten)
c.updateLastSeen()
}
}
diff --git a/client/firewall/uspfilter/rule.go b/client/firewall/uspfilter/rule.go
index a23d2011b..b765c72e9 100644
--- a/client/firewall/uspfilter/rule.go
+++ b/client/firewall/uspfilter/rule.go
@@ -29,14 +29,15 @@ func (r *PeerRule) ID() string {
}
type RouteRule struct {
- id string
- mgmtId []byte
- sources []netip.Prefix
- destination netip.Prefix
- proto firewall.Protocol
- srcPort *firewall.Port
- dstPort *firewall.Port
- action firewall.Action
+ id string
+ mgmtId []byte
+ sources []netip.Prefix
+ dstSet firewall.Set
+ destinations []netip.Prefix
+ proto firewall.Protocol
+ srcPort *firewall.Port
+ dstPort *firewall.Port
+ action firewall.Action
}
// ID returns the rule id
diff --git a/client/firewall/uspfilter/tracer_test.go b/client/firewall/uspfilter/tracer_test.go
index 48b0ec44d..bd87879a5 100644
--- a/client/firewall/uspfilter/tracer_test.go
+++ b/client/firewall/uspfilter/tracer_test.go
@@ -198,12 +198,12 @@ func TestTracePacket(t *testing.T) {
m.forwarder.Store(&forwarder.Forwarder{})
src := netip.PrefixFrom(netip.AddrFrom4([4]byte{1, 1, 1, 1}), 32)
- dst := netip.PrefixFrom(netip.AddrFrom4([4]byte{172, 17, 0, 2}), 32)
- _, err := m.AddRouteFiltering(nil, []netip.Prefix{src}, dst, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionAccept)
+ dst := netip.PrefixFrom(netip.AddrFrom4([4]byte{192, 168, 17, 2}), 32)
+ _, err := m.AddRouteFiltering(nil, []netip.Prefix{src}, fw.Network{Prefix: dst}, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionAccept)
require.NoError(t, err)
},
packetBuilder: func() *PacketBuilder {
- return createPacketBuilder("1.1.1.1", "172.17.0.2", "tcp", 12345, 80, fw.RuleDirectionIN)
+ return createPacketBuilder("1.1.1.1", "192.168.17.2", "tcp", 12345, 80, fw.RuleDirectionIN)
},
expectedStages: []PacketStage{
StageReceived,
@@ -222,12 +222,12 @@ func TestTracePacket(t *testing.T) {
m.nativeRouter.Store(false)
src := netip.PrefixFrom(netip.AddrFrom4([4]byte{1, 1, 1, 1}), 32)
- dst := netip.PrefixFrom(netip.AddrFrom4([4]byte{172, 17, 0, 2}), 32)
- _, err := m.AddRouteFiltering(nil, []netip.Prefix{src}, dst, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionDrop)
+ dst := netip.PrefixFrom(netip.AddrFrom4([4]byte{192, 168, 17, 2}), 32)
+ _, err := m.AddRouteFiltering(nil, []netip.Prefix{src}, fw.Network{Prefix: dst}, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionDrop)
require.NoError(t, err)
},
packetBuilder: func() *PacketBuilder {
- return createPacketBuilder("1.1.1.1", "172.17.0.2", "tcp", 12345, 80, fw.RuleDirectionIN)
+ return createPacketBuilder("1.1.1.1", "192.168.17.2", "tcp", 12345, 80, fw.RuleDirectionIN)
},
expectedStages: []PacketStage{
StageReceived,
@@ -245,7 +245,7 @@ func TestTracePacket(t *testing.T) {
m.nativeRouter.Store(true)
},
packetBuilder: func() *PacketBuilder {
- return createPacketBuilder("1.1.1.1", "172.17.0.2", "tcp", 12345, 80, fw.RuleDirectionIN)
+ return createPacketBuilder("1.1.1.1", "192.168.17.2", "tcp", 12345, 80, fw.RuleDirectionIN)
},
expectedStages: []PacketStage{
StageReceived,
@@ -263,7 +263,7 @@ func TestTracePacket(t *testing.T) {
m.routingEnabled.Store(false)
},
packetBuilder: func() *PacketBuilder {
- return createPacketBuilder("1.1.1.1", "172.17.0.2", "tcp", 12345, 80, fw.RuleDirectionIN)
+ return createPacketBuilder("1.1.1.1", "192.168.17.2", "tcp", 12345, 80, fw.RuleDirectionIN)
},
expectedStages: []PacketStage{
StageReceived,
@@ -425,8 +425,8 @@ func TestTracePacket(t *testing.T) {
require.True(t, m.localipmanager.IsLocalIP(netip.MustParseAddr("100.10.0.100")),
"100.10.0.100 should be recognized as a local IP")
- require.False(t, m.localipmanager.IsLocalIP(netip.MustParseAddr("172.17.0.2")),
- "172.17.0.2 should not be recognized as a local IP")
+ require.False(t, m.localipmanager.IsLocalIP(netip.MustParseAddr("192.168.17.2")),
+ "192.168.17.2 should not be recognized as a local IP")
pb := tc.packetBuilder()
diff --git a/client/firewall/uspfilter/uspfilter.go b/client/firewall/uspfilter/uspfilter.go
index 466c6a18b..11730dbb3 100644
--- a/client/firewall/uspfilter/uspfilter.go
+++ b/client/firewall/uspfilter/uspfilter.go
@@ -49,10 +49,10 @@ var errNatNotSupported = errors.New("nat not supported with userspace firewall")
// RuleSet is a set of rules grouped by a string key
type RuleSet map[string]PeerRule
-type RouteRules []RouteRule
+type RouteRules []*RouteRule
func (r RouteRules) Sort() {
- slices.SortStableFunc(r, func(a, b RouteRule) int {
+ slices.SortStableFunc(r, func(a, b *RouteRule) int {
// Deny rules come first
if a.action == firewall.ActionDrop && b.action != firewall.ActionDrop {
return -1
@@ -99,6 +99,8 @@ type Manager struct {
forwarder atomic.Pointer[forwarder.Forwarder]
logger *nblog.Logger
flowLogger nftypes.FlowLogger
+
+ blockRule firewall.Rule
}
// decoder for packages
@@ -201,41 +203,35 @@ func create(iface common.IFaceMapper, nativeFirewall firewall.Manager, disableSe
}
}
- if err := m.blockInvalidRouted(iface); err != nil {
- log.Errorf("failed to block invalid routed traffic: %v", err)
- }
-
if err := iface.SetFilter(m); err != nil {
return nil, fmt.Errorf("set filter: %w", err)
}
return m, nil
}
-func (m *Manager) blockInvalidRouted(iface common.IFaceMapper) error {
- if m.forwarder.Load() == nil {
- return nil
- }
+func (m *Manager) blockInvalidRouted(iface common.IFaceMapper) (firewall.Rule, error) {
wgPrefix, err := netip.ParsePrefix(iface.Address().Network.String())
if err != nil {
- return fmt.Errorf("parse wireguard network: %w", err)
+ return nil, fmt.Errorf("parse wireguard network: %w", err)
}
log.Debugf("blocking invalid routed traffic for %s", wgPrefix)
- if _, err := m.AddRouteFiltering(
+ rule, err := m.addRouteFiltering(
nil,
[]netip.Prefix{netip.PrefixFrom(netip.IPv4Unspecified(), 0)},
- wgPrefix,
+ firewall.Network{Prefix: wgPrefix},
firewall.ProtocolALL,
nil,
nil,
firewall.ActionDrop,
- ); err != nil {
- return fmt.Errorf("block wg nte : %w", err)
+ )
+ if err != nil {
+ return nil, fmt.Errorf("block wg nte : %w", err)
}
// TODO: Block networks that we're a client of
- return nil
+ return rule, nil
}
func (m *Manager) determineRouting() error {
@@ -413,10 +409,23 @@ func (m *Manager) AddPeerFiltering(
func (m *Manager) AddRouteFiltering(
id []byte,
sources []netip.Prefix,
- destination netip.Prefix,
+ destination firewall.Network,
proto firewall.Protocol,
- sPort *firewall.Port,
- dPort *firewall.Port,
+ sPort, dPort *firewall.Port,
+ action firewall.Action,
+) (firewall.Rule, error) {
+ m.mutex.Lock()
+ defer m.mutex.Unlock()
+
+ return m.addRouteFiltering(id, sources, destination, proto, sPort, dPort, action)
+}
+
+func (m *Manager) addRouteFiltering(
+ id []byte,
+ sources []netip.Prefix,
+ destination firewall.Network,
+ proto firewall.Protocol,
+ sPort, dPort *firewall.Port,
action firewall.Action,
) (firewall.Rule, error) {
if m.nativeRouter.Load() && m.nativeFirewall != nil {
@@ -426,34 +435,39 @@ func (m *Manager) AddRouteFiltering(
ruleID := uuid.New().String()
rule := RouteRule{
// TODO: consolidate these IDs
- id: ruleID,
- mgmtId: id,
- sources: sources,
- destination: destination,
- proto: proto,
- srcPort: sPort,
- dstPort: dPort,
- action: action,
+ id: ruleID,
+ mgmtId: id,
+ sources: sources,
+ dstSet: destination.Set,
+ proto: proto,
+ srcPort: sPort,
+ dstPort: dPort,
+ action: action,
+ }
+ if destination.IsPrefix() {
+ rule.destinations = []netip.Prefix{destination.Prefix}
}
- m.mutex.Lock()
- m.routeRules = append(m.routeRules, rule)
+ m.routeRules = append(m.routeRules, &rule)
m.routeRules.Sort()
- m.mutex.Unlock()
return &rule, nil
}
func (m *Manager) DeleteRouteRule(rule firewall.Rule) error {
+ m.mutex.Lock()
+ defer m.mutex.Unlock()
+
+ return m.deleteRouteRule(rule)
+}
+
+func (m *Manager) deleteRouteRule(rule firewall.Rule) error {
if m.nativeRouter.Load() && m.nativeFirewall != nil {
return m.nativeFirewall.DeleteRouteRule(rule)
}
- m.mutex.Lock()
- defer m.mutex.Unlock()
-
ruleID := rule.ID()
- idx := slices.IndexFunc(m.routeRules, func(r RouteRule) bool {
+ idx := slices.IndexFunc(m.routeRules, func(r *RouteRule) bool {
return r.id == ruleID
})
if idx < 0 {
@@ -509,6 +523,52 @@ func (m *Manager) DeleteDNATRule(rule firewall.Rule) error {
return m.nativeFirewall.DeleteDNATRule(rule)
}
+// UpdateSet updates the rule destinations associated with the given set
+// by merging the existing prefixes with the new ones, then deduplicating.
+func (m *Manager) UpdateSet(set firewall.Set, prefixes []netip.Prefix) error {
+ if m.nativeRouter.Load() && m.nativeFirewall != nil {
+ return m.nativeFirewall.UpdateSet(set, prefixes)
+ }
+
+ m.mutex.Lock()
+ defer m.mutex.Unlock()
+
+ var matches []*RouteRule
+ for _, rule := range m.routeRules {
+ if rule.dstSet == set {
+ matches = append(matches, rule)
+ }
+ }
+
+ if len(matches) == 0 {
+ return fmt.Errorf("no route rule found for set: %s", set)
+ }
+
+ destinations := matches[0].destinations
+ for _, prefix := range prefixes {
+ if prefix.Addr().Is4() {
+ destinations = append(destinations, prefix)
+ }
+ }
+
+ slices.SortFunc(destinations, func(a, b netip.Prefix) int {
+ cmp := a.Addr().Compare(b.Addr())
+ if cmp != 0 {
+ return cmp
+ }
+ return a.Bits() - b.Bits()
+ })
+
+ destinations = slices.Compact(destinations)
+
+ for _, rule := range matches {
+ rule.destinations = destinations
+ }
+ log.Debugf("updated set %s to prefixes %v", set.HashedName(), destinations)
+
+ return nil
+}
+
// DropOutgoing filter outgoing packets
func (m *Manager) DropOutgoing(packetData []byte, size int) bool {
return m.processOutgoingHooks(packetData, size)
@@ -764,7 +824,8 @@ func (m *Manager) handleRoutedTraffic(d *decoder, srcIP, dstIP netip.Addr, packe
proto, pnum := getProtocolFromPacket(d)
srcPort, dstPort := getPortsFromPacket(d)
- if ruleID, pass := m.routeACLsPass(srcIP, dstIP, proto, srcPort, dstPort); !pass {
+ ruleID, pass := m.routeACLsPass(srcIP, dstIP, proto, srcPort, dstPort)
+ if !pass {
m.logger.Trace("Dropping routed packet (ACL denied): rule_id=%s proto=%v src=%s:%d dst=%s:%d",
ruleID, pnum, srcIP, srcPort, dstIP, dstPort)
@@ -790,8 +851,11 @@ func (m *Manager) handleRoutedTraffic(d *decoder, srcIP, dstIP netip.Addr, packe
if fwd == nil {
m.logger.Trace("failed to forward routed packet (forwarder not initialized)")
} else {
+ fwd.RegisterRuleID(srcIP, dstIP, srcPort, dstPort, ruleID)
+
if err := fwd.InjectIncomingPacket(packetData); err != nil {
m.logger.Error("Failed to inject routed packet: %v", err)
+ fwd.DeleteRuleID(srcIP, dstIP, srcPort, dstPort)
}
}
@@ -988,8 +1052,15 @@ func (m *Manager) routeACLsPass(srcIP, dstIP netip.Addr, proto firewall.Protocol
return nil, false
}
-func (m *Manager) ruleMatches(rule RouteRule, srcAddr, dstAddr netip.Addr, proto firewall.Protocol, srcPort, dstPort uint16) bool {
- if !rule.destination.Contains(dstAddr) {
+func (m *Manager) ruleMatches(rule *RouteRule, srcAddr, dstAddr netip.Addr, proto firewall.Protocol, srcPort, dstPort uint16) bool {
+ destMatched := false
+ for _, dst := range rule.destinations {
+ if dst.Contains(dstAddr) {
+ destMatched = true
+ break
+ }
+ }
+ if !destMatched {
return false
}
@@ -1091,7 +1162,22 @@ func (m *Manager) EnableRouting() error {
m.mutex.Lock()
defer m.mutex.Unlock()
- return m.determineRouting()
+ if err := m.determineRouting(); err != nil {
+ return fmt.Errorf("determine routing: %w", err)
+ }
+
+ if m.forwarder.Load() == nil {
+ return nil
+ }
+
+ rule, err := m.blockInvalidRouted(m.wgIface)
+ if err != nil {
+ return fmt.Errorf("block invalid routed: %w", err)
+ }
+
+ m.blockRule = rule
+
+ return nil
}
func (m *Manager) DisableRouting() error {
@@ -1116,5 +1202,12 @@ func (m *Manager) DisableRouting() error {
log.Debug("forwarder stopped")
+ if m.blockRule != nil {
+ if err := m.deleteRouteRule(m.blockRule); err != nil {
+ return fmt.Errorf("delete block rule: %w", err)
+ }
+ m.blockRule = nil
+ }
+
return nil
}
diff --git a/client/firewall/uspfilter/uspfilter_filter_test.go b/client/firewall/uspfilter/uspfilter_filter_test.go
index ba97c2643..04a398d1f 100644
--- a/client/firewall/uspfilter/uspfilter_filter_test.go
+++ b/client/firewall/uspfilter/uspfilter_filter_test.go
@@ -15,6 +15,7 @@ import (
"github.com/netbirdio/netbird/client/iface/device"
"github.com/netbirdio/netbird/client/iface/mocks"
"github.com/netbirdio/netbird/client/iface/wgaddr"
+ "github.com/netbirdio/netbird/management/domain"
)
func TestPeerACLFiltering(t *testing.T) {
@@ -188,6 +189,281 @@ func TestPeerACLFiltering(t *testing.T) {
ruleAction: fw.ActionAccept,
shouldBeBlocked: true,
},
+ {
+ name: "Allow TCP traffic without port specification",
+ srcIP: "100.10.0.1",
+ dstIP: "100.10.0.100",
+ proto: fw.ProtocolTCP,
+ srcPort: 12345,
+ dstPort: 443,
+ ruleIP: "100.10.0.1",
+ ruleProto: fw.ProtocolTCP,
+ ruleAction: fw.ActionAccept,
+ shouldBeBlocked: false,
+ },
+ {
+ name: "Allow UDP traffic without port specification",
+ srcIP: "100.10.0.1",
+ dstIP: "100.10.0.100",
+ proto: fw.ProtocolUDP,
+ srcPort: 12345,
+ dstPort: 53,
+ ruleIP: "100.10.0.1",
+ ruleProto: fw.ProtocolUDP,
+ ruleAction: fw.ActionAccept,
+ shouldBeBlocked: false,
+ },
+ {
+ name: "TCP packet doesn't match UDP filter with same port",
+ srcIP: "100.10.0.1",
+ dstIP: "100.10.0.100",
+ proto: fw.ProtocolTCP,
+ srcPort: 12345,
+ dstPort: 443,
+ ruleIP: "100.10.0.1",
+ ruleProto: fw.ProtocolUDP,
+ ruleDstPort: &fw.Port{Values: []uint16{443}},
+ ruleAction: fw.ActionAccept,
+ shouldBeBlocked: true,
+ },
+ {
+ name: "UDP packet doesn't match TCP filter with same port",
+ srcIP: "100.10.0.1",
+ dstIP: "100.10.0.100",
+ proto: fw.ProtocolUDP,
+ srcPort: 12345,
+ dstPort: 443,
+ ruleIP: "100.10.0.1",
+ ruleProto: fw.ProtocolTCP,
+ ruleDstPort: &fw.Port{Values: []uint16{443}},
+ ruleAction: fw.ActionAccept,
+ shouldBeBlocked: true,
+ },
+ {
+ name: "ICMP packet doesn't match TCP filter",
+ srcIP: "100.10.0.1",
+ dstIP: "100.10.0.100",
+ proto: fw.ProtocolICMP,
+ ruleIP: "100.10.0.1",
+ ruleProto: fw.ProtocolTCP,
+ ruleAction: fw.ActionAccept,
+ shouldBeBlocked: true,
+ },
+ {
+ name: "ICMP packet doesn't match UDP filter",
+ srcIP: "100.10.0.1",
+ dstIP: "100.10.0.100",
+ proto: fw.ProtocolICMP,
+ ruleIP: "100.10.0.1",
+ ruleProto: fw.ProtocolUDP,
+ ruleAction: fw.ActionAccept,
+ shouldBeBlocked: true,
+ },
+ {
+ name: "Allow TCP traffic within port range",
+ srcIP: "100.10.0.1",
+ dstIP: "100.10.0.100",
+ proto: fw.ProtocolTCP,
+ srcPort: 12345,
+ dstPort: 8080,
+ ruleIP: "100.10.0.1",
+ ruleProto: fw.ProtocolTCP,
+ ruleDstPort: &fw.Port{IsRange: true, Values: []uint16{8000, 8100}},
+ ruleAction: fw.ActionAccept,
+ shouldBeBlocked: false,
+ },
+ {
+ name: "Block TCP traffic outside port range",
+ srcIP: "100.10.0.1",
+ dstIP: "100.10.0.100",
+ proto: fw.ProtocolTCP,
+ srcPort: 12345,
+ dstPort: 7999,
+ ruleIP: "100.10.0.1",
+ ruleProto: fw.ProtocolTCP,
+ ruleDstPort: &fw.Port{IsRange: true, Values: []uint16{8000, 8100}},
+ ruleAction: fw.ActionAccept,
+ shouldBeBlocked: true,
+ },
+ {
+ name: "Edge Case - Port at Range Boundary",
+ srcIP: "100.10.0.1",
+ dstIP: "100.10.0.100",
+ proto: fw.ProtocolTCP,
+ srcPort: 12345,
+ dstPort: 8100,
+ ruleIP: "100.10.0.1",
+ ruleProto: fw.ProtocolTCP,
+ ruleDstPort: &fw.Port{IsRange: true, Values: []uint16{8000, 8100}},
+ ruleAction: fw.ActionAccept,
+ shouldBeBlocked: false,
+ },
+ {
+ name: "UDP Port Range",
+ srcIP: "100.10.0.1",
+ dstIP: "100.10.0.100",
+ proto: fw.ProtocolUDP,
+ srcPort: 12345,
+ dstPort: 5060,
+ ruleIP: "100.10.0.1",
+ ruleProto: fw.ProtocolUDP,
+ ruleDstPort: &fw.Port{IsRange: true, Values: []uint16{5060, 5070}},
+ ruleAction: fw.ActionAccept,
+ shouldBeBlocked: false,
+ },
+ {
+ name: "Allow multiple destination ports",
+ srcIP: "100.10.0.1",
+ dstIP: "100.10.0.100",
+ proto: fw.ProtocolTCP,
+ srcPort: 12345,
+ dstPort: 8080,
+ ruleIP: "100.10.0.1",
+ ruleProto: fw.ProtocolTCP,
+ ruleDstPort: &fw.Port{Values: []uint16{80, 8080, 443}},
+ ruleAction: fw.ActionAccept,
+ shouldBeBlocked: false,
+ },
+ {
+ name: "Allow multiple source ports",
+ srcIP: "100.10.0.1",
+ dstIP: "100.10.0.100",
+ proto: fw.ProtocolTCP,
+ srcPort: 12345,
+ dstPort: 80,
+ ruleIP: "100.10.0.1",
+ ruleProto: fw.ProtocolTCP,
+ ruleSrcPort: &fw.Port{Values: []uint16{12345, 12346, 12347}},
+ ruleAction: fw.ActionAccept,
+ shouldBeBlocked: false,
+ },
+ // New drop test cases
+ {
+ name: "Drop TCP traffic from WG peer",
+ srcIP: "100.10.0.1",
+ dstIP: "100.10.0.100",
+ proto: fw.ProtocolTCP,
+ srcPort: 12345,
+ dstPort: 443,
+ ruleIP: "100.10.0.1",
+ ruleProto: fw.ProtocolTCP,
+ ruleDstPort: &fw.Port{Values: []uint16{443}},
+ ruleAction: fw.ActionDrop,
+ shouldBeBlocked: true,
+ },
+ {
+ name: "Drop UDP traffic from WG peer",
+ srcIP: "100.10.0.1",
+ dstIP: "100.10.0.100",
+ proto: fw.ProtocolUDP,
+ srcPort: 12345,
+ dstPort: 53,
+ ruleIP: "100.10.0.1",
+ ruleProto: fw.ProtocolUDP,
+ ruleDstPort: &fw.Port{Values: []uint16{53}},
+ ruleAction: fw.ActionDrop,
+ shouldBeBlocked: true,
+ },
+ {
+ name: "Drop ICMP traffic from WG peer",
+ srcIP: "100.10.0.1",
+ dstIP: "100.10.0.100",
+ proto: fw.ProtocolICMP,
+ ruleIP: "100.10.0.1",
+ ruleProto: fw.ProtocolICMP,
+ ruleAction: fw.ActionDrop,
+ shouldBeBlocked: true,
+ },
+ {
+ name: "Drop all traffic from WG peer",
+ srcIP: "100.10.0.1",
+ dstIP: "100.10.0.100",
+ proto: fw.ProtocolTCP,
+ srcPort: 12345,
+ dstPort: 443,
+ ruleIP: "100.10.0.1",
+ ruleProto: fw.ProtocolALL,
+ ruleAction: fw.ActionDrop,
+ shouldBeBlocked: true,
+ },
+ {
+ name: "Drop traffic from multiple source ports",
+ srcIP: "100.10.0.1",
+ dstIP: "100.10.0.100",
+ proto: fw.ProtocolTCP,
+ srcPort: 12345,
+ dstPort: 80,
+ ruleIP: "100.10.0.1",
+ ruleProto: fw.ProtocolTCP,
+ ruleSrcPort: &fw.Port{Values: []uint16{12345, 12346, 12347}},
+ ruleAction: fw.ActionDrop,
+ shouldBeBlocked: true,
+ },
+ {
+ name: "Drop multiple destination ports",
+ srcIP: "100.10.0.1",
+ dstIP: "100.10.0.100",
+ proto: fw.ProtocolTCP,
+ srcPort: 12345,
+ dstPort: 8080,
+ ruleIP: "100.10.0.1",
+ ruleProto: fw.ProtocolTCP,
+ ruleDstPort: &fw.Port{Values: []uint16{80, 8080, 443}},
+ ruleAction: fw.ActionDrop,
+ shouldBeBlocked: true,
+ },
+ {
+ name: "Drop TCP traffic within port range",
+ srcIP: "100.10.0.1",
+ dstIP: "100.10.0.100",
+ proto: fw.ProtocolTCP,
+ srcPort: 12345,
+ dstPort: 8080,
+ ruleIP: "100.10.0.1",
+ ruleProto: fw.ProtocolTCP,
+ ruleDstPort: &fw.Port{IsRange: true, Values: []uint16{8000, 8100}},
+ ruleAction: fw.ActionDrop,
+ shouldBeBlocked: true,
+ },
+ {
+ name: "Accept TCP traffic outside drop port range",
+ srcIP: "100.10.0.1",
+ dstIP: "100.10.0.100",
+ proto: fw.ProtocolTCP,
+ srcPort: 12345,
+ dstPort: 7999,
+ ruleIP: "100.10.0.1",
+ ruleProto: fw.ProtocolTCP,
+ ruleDstPort: &fw.Port{IsRange: true, Values: []uint16{8000, 8100}},
+ ruleAction: fw.ActionDrop,
+ shouldBeBlocked: false,
+ },
+ {
+ name: "Drop TCP traffic with source port range",
+ srcIP: "100.10.0.1",
+ dstIP: "100.10.0.100",
+ proto: fw.ProtocolTCP,
+ srcPort: 32100,
+ dstPort: 80,
+ ruleIP: "100.10.0.1",
+ ruleProto: fw.ProtocolTCP,
+ ruleSrcPort: &fw.Port{IsRange: true, Values: []uint16{32000, 33000}},
+ ruleAction: fw.ActionDrop,
+ shouldBeBlocked: true,
+ },
+ {
+ name: "Mixed rule - drop specific port but allow other ports",
+ srcIP: "100.10.0.1",
+ dstIP: "100.10.0.100",
+ proto: fw.ProtocolTCP,
+ srcPort: 12345,
+ dstPort: 443,
+ ruleIP: "100.10.0.1",
+ ruleProto: fw.ProtocolTCP,
+ ruleDstPort: &fw.Port{Values: []uint16{443}},
+ ruleAction: fw.ActionDrop,
+ shouldBeBlocked: true,
+ },
}
t.Run("Implicit DROP (no rules)", func(t *testing.T) {
@@ -198,6 +474,28 @@ func TestPeerACLFiltering(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
+
+ if tc.ruleAction == fw.ActionDrop {
+ // add general accept rule to test drop rule
+ // TODO: this only works because 0.0.0.0 is tested last, we need to implement order
+ rules, err := manager.AddPeerFiltering(
+ nil,
+ net.ParseIP("0.0.0.0"),
+ fw.ProtocolALL,
+ nil,
+ nil,
+ fw.ActionAccept,
+ "",
+ )
+ require.NoError(t, err)
+ require.NotEmpty(t, rules)
+ t.Cleanup(func() {
+ for _, rule := range rules {
+ require.NoError(t, manager.DeletePeerRule(rule))
+ }
+ })
+ }
+
rules, err := manager.AddPeerFiltering(
nil,
net.ParseIP(tc.ruleIP),
@@ -303,8 +601,8 @@ func setupRoutedManager(tb testing.TB, network string) *Manager {
}
manager, err := Create(ifaceMock, false, flowLogger)
- require.NoError(tb, manager.EnableRouting())
require.NoError(tb, err)
+ require.NoError(tb, manager.EnableRouting())
require.NotNil(tb, manager)
require.True(tb, manager.routingEnabled.Load())
require.False(tb, manager.nativeRouter.Load())
@@ -321,7 +619,7 @@ func TestRouteACLFiltering(t *testing.T) {
type rule struct {
sources []netip.Prefix
- dest netip.Prefix
+ dest fw.Network
proto fw.Protocol
srcPort *fw.Port
dstPort *fw.Port
@@ -347,7 +645,7 @@ func TestRouteACLFiltering(t *testing.T) {
dstPort: 443,
rule: rule{
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
- dest: netip.MustParsePrefix("192.168.1.0/24"),
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
proto: fw.ProtocolTCP,
dstPort: &fw.Port{Values: []uint16{443}},
action: fw.ActionAccept,
@@ -363,7 +661,7 @@ func TestRouteACLFiltering(t *testing.T) {
dstPort: 443,
rule: rule{
sources: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")},
- dest: netip.MustParsePrefix("192.168.1.0/24"),
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
proto: fw.ProtocolTCP,
dstPort: &fw.Port{Values: []uint16{443}},
action: fw.ActionAccept,
@@ -379,7 +677,7 @@ func TestRouteACLFiltering(t *testing.T) {
dstPort: 443,
rule: rule{
sources: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")},
- dest: netip.MustParsePrefix("0.0.0.0/0"),
+ dest: fw.Network{Prefix: netip.MustParsePrefix("0.0.0.0/0")},
proto: fw.ProtocolTCP,
dstPort: &fw.Port{Values: []uint16{443}},
action: fw.ActionAccept,
@@ -395,7 +693,7 @@ func TestRouteACLFiltering(t *testing.T) {
dstPort: 53,
rule: rule{
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
- dest: netip.MustParsePrefix("192.168.1.0/24"),
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
proto: fw.ProtocolUDP,
dstPort: &fw.Port{Values: []uint16{53}},
action: fw.ActionAccept,
@@ -409,7 +707,7 @@ func TestRouteACLFiltering(t *testing.T) {
proto: fw.ProtocolICMP,
rule: rule{
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
- dest: netip.MustParsePrefix("0.0.0.0/0"),
+ dest: fw.Network{Prefix: netip.MustParsePrefix("0.0.0.0/0")},
proto: fw.ProtocolICMP,
action: fw.ActionAccept,
},
@@ -424,7 +722,7 @@ func TestRouteACLFiltering(t *testing.T) {
dstPort: 80,
rule: rule{
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
- dest: netip.MustParsePrefix("192.168.1.0/24"),
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
proto: fw.ProtocolALL,
dstPort: &fw.Port{Values: []uint16{80}},
action: fw.ActionAccept,
@@ -440,7 +738,7 @@ func TestRouteACLFiltering(t *testing.T) {
dstPort: 8080,
rule: rule{
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
- dest: netip.MustParsePrefix("192.168.1.0/24"),
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
proto: fw.ProtocolTCP,
dstPort: &fw.Port{Values: []uint16{80}},
action: fw.ActionAccept,
@@ -456,7 +754,7 @@ func TestRouteACLFiltering(t *testing.T) {
dstPort: 80,
rule: rule{
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
- dest: netip.MustParsePrefix("192.168.1.0/24"),
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
proto: fw.ProtocolTCP,
dstPort: &fw.Port{Values: []uint16{80}},
action: fw.ActionAccept,
@@ -472,7 +770,7 @@ func TestRouteACLFiltering(t *testing.T) {
dstPort: 80,
rule: rule{
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
- dest: netip.MustParsePrefix("192.168.1.0/24"),
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
proto: fw.ProtocolTCP,
dstPort: &fw.Port{Values: []uint16{80}},
action: fw.ActionAccept,
@@ -488,7 +786,7 @@ func TestRouteACLFiltering(t *testing.T) {
dstPort: 80,
rule: rule{
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
- dest: netip.MustParsePrefix("192.168.1.0/24"),
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
proto: fw.ProtocolTCP,
srcPort: &fw.Port{Values: []uint16{12345}},
action: fw.ActionAccept,
@@ -507,7 +805,7 @@ func TestRouteACLFiltering(t *testing.T) {
netip.MustParsePrefix("100.10.0.0/16"),
netip.MustParsePrefix("172.16.0.0/16"),
},
- dest: netip.MustParsePrefix("192.168.1.0/24"),
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
proto: fw.ProtocolTCP,
dstPort: &fw.Port{Values: []uint16{80}},
action: fw.ActionAccept,
@@ -521,7 +819,7 @@ func TestRouteACLFiltering(t *testing.T) {
proto: fw.ProtocolICMP,
rule: rule{
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
- dest: netip.MustParsePrefix("192.168.1.0/24"),
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
proto: fw.ProtocolALL,
action: fw.ActionAccept,
},
@@ -536,33 +834,13 @@ func TestRouteACLFiltering(t *testing.T) {
dstPort: 80,
rule: rule{
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
- dest: netip.MustParsePrefix("192.168.1.0/24"),
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
proto: fw.ProtocolALL,
dstPort: &fw.Port{Values: []uint16{80}},
action: fw.ActionAccept,
},
shouldPass: true,
},
- {
- name: "Multiple source networks with mismatched protocol",
- srcIP: "172.16.0.1",
- dstIP: "192.168.1.100",
- // Should not match TCP rule
- proto: fw.ProtocolUDP,
- srcPort: 12345,
- dstPort: 80,
- rule: rule{
- sources: []netip.Prefix{
- netip.MustParsePrefix("100.10.0.0/16"),
- netip.MustParsePrefix("172.16.0.0/16"),
- },
- dest: netip.MustParsePrefix("192.168.1.0/24"),
- proto: fw.ProtocolTCP,
- dstPort: &fw.Port{Values: []uint16{80}},
- action: fw.ActionAccept,
- },
- shouldPass: false,
- },
{
name: "Allow multiple destination ports",
srcIP: "100.10.0.1",
@@ -572,7 +850,7 @@ func TestRouteACLFiltering(t *testing.T) {
dstPort: 8080,
rule: rule{
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
- dest: netip.MustParsePrefix("192.168.1.0/24"),
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
proto: fw.ProtocolTCP,
dstPort: &fw.Port{Values: []uint16{80, 8080, 443}},
action: fw.ActionAccept,
@@ -588,7 +866,7 @@ func TestRouteACLFiltering(t *testing.T) {
dstPort: 80,
rule: rule{
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
- dest: netip.MustParsePrefix("192.168.1.0/24"),
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
proto: fw.ProtocolTCP,
srcPort: &fw.Port{Values: []uint16{12345, 12346, 12347}},
action: fw.ActionAccept,
@@ -604,7 +882,7 @@ func TestRouteACLFiltering(t *testing.T) {
dstPort: 80,
rule: rule{
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
- dest: netip.MustParsePrefix("192.168.1.0/24"),
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
proto: fw.ProtocolALL,
srcPort: &fw.Port{Values: []uint16{12345}},
dstPort: &fw.Port{Values: []uint16{80}},
@@ -621,7 +899,7 @@ func TestRouteACLFiltering(t *testing.T) {
dstPort: 8080,
rule: rule{
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
- dest: netip.MustParsePrefix("192.168.1.0/24"),
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
proto: fw.ProtocolTCP,
dstPort: &fw.Port{
IsRange: true,
@@ -640,7 +918,7 @@ func TestRouteACLFiltering(t *testing.T) {
dstPort: 7999,
rule: rule{
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
- dest: netip.MustParsePrefix("192.168.1.0/24"),
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
proto: fw.ProtocolTCP,
dstPort: &fw.Port{
IsRange: true,
@@ -659,7 +937,7 @@ func TestRouteACLFiltering(t *testing.T) {
dstPort: 80,
rule: rule{
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
- dest: netip.MustParsePrefix("192.168.1.0/24"),
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
proto: fw.ProtocolTCP,
srcPort: &fw.Port{
IsRange: true,
@@ -678,7 +956,7 @@ func TestRouteACLFiltering(t *testing.T) {
dstPort: 443,
rule: rule{
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
- dest: netip.MustParsePrefix("192.168.1.0/24"),
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
proto: fw.ProtocolTCP,
srcPort: &fw.Port{
IsRange: true,
@@ -700,7 +978,7 @@ func TestRouteACLFiltering(t *testing.T) {
dstPort: 8100,
rule: rule{
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
- dest: netip.MustParsePrefix("192.168.1.0/24"),
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
proto: fw.ProtocolTCP,
dstPort: &fw.Port{
IsRange: true,
@@ -719,7 +997,7 @@ func TestRouteACLFiltering(t *testing.T) {
dstPort: 5060,
rule: rule{
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
- dest: netip.MustParsePrefix("192.168.1.0/24"),
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
proto: fw.ProtocolUDP,
dstPort: &fw.Port{
IsRange: true,
@@ -738,7 +1016,7 @@ func TestRouteACLFiltering(t *testing.T) {
dstPort: 8080,
rule: rule{
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
- dest: netip.MustParsePrefix("192.168.1.0/24"),
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
proto: fw.ProtocolALL,
dstPort: &fw.Port{
IsRange: true,
@@ -757,7 +1035,7 @@ func TestRouteACLFiltering(t *testing.T) {
dstPort: 443,
rule: rule{
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
- dest: netip.MustParsePrefix("192.168.1.0/24"),
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
proto: fw.ProtocolTCP,
dstPort: &fw.Port{Values: []uint16{443}},
action: fw.ActionDrop,
@@ -773,7 +1051,7 @@ func TestRouteACLFiltering(t *testing.T) {
dstPort: 80,
rule: rule{
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
- dest: netip.MustParsePrefix("192.168.1.0/24"),
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
proto: fw.ProtocolALL,
action: fw.ActionDrop,
},
@@ -791,17 +1069,158 @@ func TestRouteACLFiltering(t *testing.T) {
netip.MustParsePrefix("100.10.0.0/16"),
netip.MustParsePrefix("172.16.0.0/16"),
},
- dest: netip.MustParsePrefix("192.168.1.0/24"),
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
proto: fw.ProtocolTCP,
dstPort: &fw.Port{Values: []uint16{80}},
action: fw.ActionDrop,
},
shouldPass: false,
},
+
+ {
+ name: "Drop empty destination set",
+ srcIP: "172.16.0.1",
+ dstIP: "192.168.1.100",
+ proto: fw.ProtocolTCP,
+ srcPort: 12345,
+ dstPort: 80,
+ rule: rule{
+ sources: []netip.Prefix{
+ netip.MustParsePrefix("172.16.0.0/16"),
+ },
+ dest: fw.Network{Set: fw.Set{}},
+ proto: fw.ProtocolTCP,
+ dstPort: &fw.Port{Values: []uint16{80}},
+ action: fw.ActionAccept,
+ },
+ shouldPass: false,
+ },
+ {
+ name: "Accept TCP traffic outside drop port range",
+ srcIP: "100.10.0.1",
+ dstIP: "192.168.1.100",
+ proto: fw.ProtocolTCP,
+ srcPort: 12345,
+ dstPort: 7999,
+ rule: rule{
+ sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
+ proto: fw.ProtocolTCP,
+ dstPort: &fw.Port{IsRange: true, Values: []uint16{8000, 8100}},
+ action: fw.ActionDrop,
+ },
+ shouldPass: true,
+ },
+ {
+ name: "Allow TCP traffic without port specification",
+ srcIP: "100.10.0.1",
+ dstIP: "192.168.1.100",
+ proto: fw.ProtocolTCP,
+ srcPort: 12345,
+ dstPort: 443,
+ rule: rule{
+ sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
+ proto: fw.ProtocolTCP,
+ action: fw.ActionAccept,
+ },
+ shouldPass: true,
+ },
+ {
+ name: "Allow UDP traffic without port specification",
+ srcIP: "100.10.0.1",
+ dstIP: "192.168.1.100",
+ proto: fw.ProtocolUDP,
+ srcPort: 12345,
+ dstPort: 53,
+ rule: rule{
+ sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
+ proto: fw.ProtocolUDP,
+ action: fw.ActionAccept,
+ },
+ shouldPass: true,
+ },
+ {
+ name: "TCP packet doesn't match UDP filter with same port",
+ srcIP: "100.10.0.1",
+ dstIP: "192.168.1.100",
+ proto: fw.ProtocolTCP,
+ srcPort: 12345,
+ dstPort: 80,
+ rule: rule{
+ sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
+ proto: fw.ProtocolUDP,
+ dstPort: &fw.Port{Values: []uint16{80}},
+ action: fw.ActionAccept,
+ },
+ shouldPass: false,
+ },
+ {
+ name: "UDP packet doesn't match TCP filter with same port",
+ srcIP: "100.10.0.1",
+ dstIP: "192.168.1.100",
+ proto: fw.ProtocolUDP,
+ srcPort: 12345,
+ dstPort: 80,
+ rule: rule{
+ sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
+ proto: fw.ProtocolTCP,
+ dstPort: &fw.Port{Values: []uint16{80}},
+ action: fw.ActionAccept,
+ },
+ shouldPass: false,
+ },
+ {
+ name: "ICMP packet doesn't match TCP filter",
+ srcIP: "100.10.0.1",
+ dstIP: "192.168.1.100",
+ proto: fw.ProtocolICMP,
+ rule: rule{
+ sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
+ proto: fw.ProtocolTCP,
+ action: fw.ActionAccept,
+ },
+ shouldPass: false,
+ },
+ {
+ name: "ICMP packet doesn't match UDP filter",
+ srcIP: "100.10.0.1",
+ dstIP: "192.168.1.100",
+ proto: fw.ProtocolICMP,
+ rule: rule{
+ sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
+ proto: fw.ProtocolUDP,
+ action: fw.ActionAccept,
+ },
+ shouldPass: false,
+ },
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
+ if tc.rule.action == fw.ActionDrop {
+ // add general accept rule to test drop rule
+ rule, err := manager.AddRouteFiltering(
+ nil,
+ []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")},
+ fw.Network{Prefix: netip.MustParsePrefix("0.0.0.0/0")},
+ fw.ProtocolALL,
+ nil,
+ nil,
+ fw.ActionAccept,
+ )
+ require.NoError(t, err)
+ require.NotNil(t, rule)
+ t.Cleanup(func() {
+ require.NoError(t, manager.DeleteRouteRule(rule))
+ })
+ }
+
rule, err := manager.AddRouteFiltering(
nil,
tc.rule.sources,
@@ -836,7 +1255,7 @@ func TestRouteACLOrder(t *testing.T) {
name string
rules []struct {
sources []netip.Prefix
- dest netip.Prefix
+ dest fw.Network
proto fw.Protocol
srcPort *fw.Port
dstPort *fw.Port
@@ -857,7 +1276,7 @@ func TestRouteACLOrder(t *testing.T) {
name: "Drop rules take precedence over accept",
rules: []struct {
sources []netip.Prefix
- dest netip.Prefix
+ dest fw.Network
proto fw.Protocol
srcPort *fw.Port
dstPort *fw.Port
@@ -866,7 +1285,7 @@ func TestRouteACLOrder(t *testing.T) {
{
// Accept rule added first
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
- dest: netip.MustParsePrefix("192.168.1.0/24"),
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
proto: fw.ProtocolTCP,
dstPort: &fw.Port{Values: []uint16{80, 443}},
action: fw.ActionAccept,
@@ -874,7 +1293,7 @@ func TestRouteACLOrder(t *testing.T) {
{
// Drop rule added second but should be evaluated first
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
- dest: netip.MustParsePrefix("192.168.1.0/24"),
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
proto: fw.ProtocolTCP,
dstPort: &fw.Port{Values: []uint16{443}},
action: fw.ActionDrop,
@@ -912,7 +1331,7 @@ func TestRouteACLOrder(t *testing.T) {
name: "Multiple drop rules take precedence",
rules: []struct {
sources []netip.Prefix
- dest netip.Prefix
+ dest fw.Network
proto fw.Protocol
srcPort *fw.Port
dstPort *fw.Port
@@ -921,14 +1340,14 @@ func TestRouteACLOrder(t *testing.T) {
{
// Accept all
sources: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")},
- dest: netip.MustParsePrefix("0.0.0.0/0"),
+ dest: fw.Network{Prefix: netip.MustParsePrefix("0.0.0.0/0")},
proto: fw.ProtocolALL,
action: fw.ActionAccept,
},
{
// Drop specific port
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
- dest: netip.MustParsePrefix("192.168.1.0/24"),
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
proto: fw.ProtocolTCP,
dstPort: &fw.Port{Values: []uint16{443}},
action: fw.ActionDrop,
@@ -936,7 +1355,7 @@ func TestRouteACLOrder(t *testing.T) {
{
// Drop different port
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
- dest: netip.MustParsePrefix("192.168.1.0/24"),
+ dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
proto: fw.ProtocolTCP,
dstPort: &fw.Port{Values: []uint16{80}},
action: fw.ActionDrop,
@@ -1015,3 +1434,53 @@ func TestRouteACLOrder(t *testing.T) {
})
}
}
+
+func TestRouteACLSet(t *testing.T) {
+ ifaceMock := &IFaceMock{
+ SetFilterFunc: func(device.PacketFilter) error { return nil },
+ AddressFunc: func() wgaddr.Address {
+ return wgaddr.Address{
+ IP: net.ParseIP("100.10.0.100"),
+ Network: &net.IPNet{
+ IP: net.ParseIP("100.10.0.0"),
+ Mask: net.CIDRMask(16, 32),
+ },
+ }
+ },
+ }
+
+ manager, err := Create(ifaceMock, false, flowLogger)
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ require.NoError(t, manager.Close(nil))
+ })
+
+ set := fw.NewDomainSet(domain.List{"example.org"})
+
+ // Add rule that uses the set (initially empty)
+ rule, err := manager.AddRouteFiltering(
+ nil,
+ []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")},
+ fw.Network{Set: set},
+ fw.ProtocolTCP,
+ nil,
+ nil,
+ fw.ActionAccept,
+ )
+ require.NoError(t, err)
+ require.NotNil(t, rule)
+
+ srcIP := netip.MustParseAddr("100.10.0.1")
+ dstIP := netip.MustParseAddr("192.168.1.100")
+
+ // Check that traffic is dropped (empty set shouldn't match anything)
+ _, isAllowed := manager.routeACLsPass(srcIP, dstIP, fw.ProtocolTCP, 12345, 80)
+ require.False(t, isAllowed, "Empty set should not allow any traffic")
+
+ err = manager.UpdateSet(set, []netip.Prefix{netip.MustParsePrefix("192.168.1.0/24")})
+ require.NoError(t, err)
+
+ // Now the packet should be allowed
+ _, isAllowed = manager.routeACLsPass(srcIP, dstIP, fw.ProtocolTCP, 12345, 80)
+ require.True(t, isAllowed, "After set update, traffic to the added network should be allowed")
+}
diff --git a/client/firewall/uspfilter/uspfilter_test.go b/client/firewall/uspfilter/uspfilter_test.go
index a48a483f8..24a6a2c40 100644
--- a/client/firewall/uspfilter/uspfilter_test.go
+++ b/client/firewall/uspfilter/uspfilter_test.go
@@ -20,6 +20,7 @@ import (
"github.com/netbirdio/netbird/client/iface/device"
"github.com/netbirdio/netbird/client/iface/wgaddr"
"github.com/netbirdio/netbird/client/internal/netflow"
+ "github.com/netbirdio/netbird/management/domain"
)
var logger = log.NewFromLogrus(logrus.StandardLogger())
@@ -711,3 +712,203 @@ func TestStatefulFirewall_UDPTracking(t *testing.T) {
})
}
}
+
+func TestUpdateSetMerge(t *testing.T) {
+ ifaceMock := &IFaceMock{
+ SetFilterFunc: func(device.PacketFilter) error { return nil },
+ }
+
+ manager, err := Create(ifaceMock, false, flowLogger)
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ require.NoError(t, manager.Close(nil))
+ })
+
+ set := fw.NewDomainSet(domain.List{"example.org"})
+
+ initialPrefixes := []netip.Prefix{
+ netip.MustParsePrefix("10.0.0.0/24"),
+ netip.MustParsePrefix("192.168.1.0/24"),
+ }
+
+ rule, err := manager.AddRouteFiltering(
+ nil,
+ []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")},
+ fw.Network{Set: set},
+ fw.ProtocolTCP,
+ nil,
+ nil,
+ fw.ActionAccept,
+ )
+ require.NoError(t, err)
+ require.NotNil(t, rule)
+
+ // Update the set with initial prefixes
+ err = manager.UpdateSet(set, initialPrefixes)
+ require.NoError(t, err)
+
+ // Test initial prefixes work
+ srcIP := netip.MustParseAddr("100.10.0.1")
+ dstIP1 := netip.MustParseAddr("10.0.0.100")
+ dstIP2 := netip.MustParseAddr("192.168.1.100")
+ dstIP3 := netip.MustParseAddr("172.16.0.100")
+
+ _, isAllowed1 := manager.routeACLsPass(srcIP, dstIP1, fw.ProtocolTCP, 12345, 80)
+ _, isAllowed2 := manager.routeACLsPass(srcIP, dstIP2, fw.ProtocolTCP, 12345, 80)
+ _, isAllowed3 := manager.routeACLsPass(srcIP, dstIP3, fw.ProtocolTCP, 12345, 80)
+
+ require.True(t, isAllowed1, "Traffic to 10.0.0.100 should be allowed")
+ require.True(t, isAllowed2, "Traffic to 192.168.1.100 should be allowed")
+ require.False(t, isAllowed3, "Traffic to 172.16.0.100 should be denied")
+
+ newPrefixes := []netip.Prefix{
+ netip.MustParsePrefix("172.16.0.0/16"),
+ netip.MustParsePrefix("10.1.0.0/24"),
+ }
+
+ err = manager.UpdateSet(set, newPrefixes)
+ require.NoError(t, err)
+
+ // Check that all original prefixes are still included
+ _, isAllowed1 = manager.routeACLsPass(srcIP, dstIP1, fw.ProtocolTCP, 12345, 80)
+ _, isAllowed2 = manager.routeACLsPass(srcIP, dstIP2, fw.ProtocolTCP, 12345, 80)
+ require.True(t, isAllowed1, "Traffic to 10.0.0.100 should still be allowed after update")
+ require.True(t, isAllowed2, "Traffic to 192.168.1.100 should still be allowed after update")
+
+ // Check that new prefixes are included
+ dstIP4 := netip.MustParseAddr("172.16.1.100")
+ dstIP5 := netip.MustParseAddr("10.1.0.50")
+
+ _, isAllowed4 := manager.routeACLsPass(srcIP, dstIP4, fw.ProtocolTCP, 12345, 80)
+ _, isAllowed5 := manager.routeACLsPass(srcIP, dstIP5, fw.ProtocolTCP, 12345, 80)
+
+ require.True(t, isAllowed4, "Traffic to new prefix 172.16.0.0/16 should be allowed")
+ require.True(t, isAllowed5, "Traffic to new prefix 10.1.0.0/24 should be allowed")
+
+ // Verify the rule has all prefixes
+ manager.mutex.RLock()
+ foundRule := false
+ for _, r := range manager.routeRules {
+ if r.id == rule.ID() {
+ foundRule = true
+ require.Len(t, r.destinations, len(initialPrefixes)+len(newPrefixes),
+ "Rule should have all prefixes merged")
+ }
+ }
+ manager.mutex.RUnlock()
+ require.True(t, foundRule, "Rule should be found")
+}
+
+func TestUpdateSetDeduplication(t *testing.T) {
+ ifaceMock := &IFaceMock{
+ SetFilterFunc: func(device.PacketFilter) error { return nil },
+ }
+
+ manager, err := Create(ifaceMock, false, flowLogger)
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ require.NoError(t, manager.Close(nil))
+ })
+
+ set := fw.NewDomainSet(domain.List{"example.org"})
+
+ rule, err := manager.AddRouteFiltering(
+ nil,
+ []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")},
+ fw.Network{Set: set},
+ fw.ProtocolTCP,
+ nil,
+ nil,
+ fw.ActionAccept,
+ )
+ require.NoError(t, err)
+ require.NotNil(t, rule)
+
+ initialPrefixes := []netip.Prefix{
+ netip.MustParsePrefix("10.0.0.0/24"),
+ netip.MustParsePrefix("10.0.0.0/24"), // Duplicate
+ netip.MustParsePrefix("192.168.1.0/24"),
+ netip.MustParsePrefix("192.168.1.0/24"), // Duplicate
+ }
+
+ err = manager.UpdateSet(set, initialPrefixes)
+ require.NoError(t, err)
+
+ // Check the internal state for deduplication
+ manager.mutex.RLock()
+ foundRule := false
+ for _, r := range manager.routeRules {
+ if r.id == rule.ID() {
+ foundRule = true
+ // Should have deduplicated to 2 prefixes
+ require.Len(t, r.destinations, 2, "Duplicate prefixes should be removed")
+
+ // Check the prefixes are correct
+ expectedPrefixes := []netip.Prefix{
+ netip.MustParsePrefix("10.0.0.0/24"),
+ netip.MustParsePrefix("192.168.1.0/24"),
+ }
+ for i, prefix := range expectedPrefixes {
+ require.True(t, r.destinations[i] == prefix,
+ "Prefix should match expected value")
+ }
+ }
+ }
+ manager.mutex.RUnlock()
+ require.True(t, foundRule, "Rule should be found")
+
+ // Test with overlapping prefixes of different sizes
+ overlappingPrefixes := []netip.Prefix{
+ netip.MustParsePrefix("10.0.0.0/16"), // More general
+ netip.MustParsePrefix("10.0.0.0/24"), // More specific (already exists)
+ netip.MustParsePrefix("192.168.0.0/16"), // More general
+ netip.MustParsePrefix("192.168.1.0/24"), // More specific (already exists)
+ }
+
+ err = manager.UpdateSet(set, overlappingPrefixes)
+ require.NoError(t, err)
+
+ // Check that all prefixes are included (no deduplication of overlapping prefixes)
+ manager.mutex.RLock()
+ for _, r := range manager.routeRules {
+ if r.id == rule.ID() {
+ // Should have all 4 prefixes (2 original + 2 new more general ones)
+ require.Len(t, r.destinations, 4,
+ "Overlapping prefixes should not be deduplicated")
+
+ // Verify they're sorted correctly (more specific prefixes should come first)
+ prefixes := make([]string, 0, len(r.destinations))
+ for _, p := range r.destinations {
+ prefixes = append(prefixes, p.String())
+ }
+
+ // Check sorted order
+ require.Equal(t, []string{
+ "10.0.0.0/16",
+ "10.0.0.0/24",
+ "192.168.0.0/16",
+ "192.168.1.0/24",
+ }, prefixes, "Prefixes should be sorted")
+ }
+ }
+ manager.mutex.RUnlock()
+
+ // Test functionality with all prefixes
+ testCases := []struct {
+ dstIP netip.Addr
+ expected bool
+ desc string
+ }{
+ {netip.MustParseAddr("10.0.0.100"), true, "IP in both /16 and /24"},
+ {netip.MustParseAddr("10.0.1.100"), true, "IP only in /16"},
+ {netip.MustParseAddr("192.168.1.100"), true, "IP in both /16 and /24"},
+ {netip.MustParseAddr("192.168.2.100"), true, "IP only in /16"},
+ {netip.MustParseAddr("172.16.0.100"), false, "IP not in any prefix"},
+ }
+
+ srcIP := netip.MustParseAddr("100.10.0.1")
+ for _, tc := range testCases {
+ _, isAllowed := manager.routeACLsPass(srcIP, tc.dstIP, fw.ProtocolTCP, 12345, 80)
+ require.Equal(t, tc.expected, isAllowed, tc.desc)
+ }
+}
diff --git a/client/iface/bind/udp_mux.go b/client/iface/bind/udp_mux.go
index 5a471bf24..0e58499aa 100644
--- a/client/iface/bind/udp_mux.go
+++ b/client/iface/bind/udp_mux.go
@@ -458,6 +458,6 @@ func newBufferHolder(size int) *bufferHolder {
func getLogger() logging.LeveledLogger {
fac := logging.NewDefaultLoggerFactory()
- fac.Writer = log.StandardLogger().Writer()
+ //fac.Writer = log.StandardLogger().Writer()
return fac.NewLogger("ice")
}
diff --git a/client/internal/acl/id/id.go b/client/internal/acl/id/id.go
index 93f16b429..23451453e 100644
--- a/client/internal/acl/id/id.go
+++ b/client/internal/acl/id/id.go
@@ -18,7 +18,7 @@ func (r RuleID) ID() string {
func GenerateRouteRuleKey(
sources []netip.Prefix,
- destination netip.Prefix,
+ destination manager.Network,
proto manager.Protocol,
sPort *manager.Port,
dPort *manager.Port,
diff --git a/client/internal/acl/manager.go b/client/internal/acl/manager.go
index 61fbb10ca..6fa35d5c2 100644
--- a/client/internal/acl/manager.go
+++ b/client/internal/acl/manager.go
@@ -18,6 +18,7 @@ import (
firewall "github.com/netbirdio/netbird/client/firewall/manager"
"github.com/netbirdio/netbird/client/internal/acl/id"
"github.com/netbirdio/netbird/client/ssh"
+ "github.com/netbirdio/netbird/management/domain"
mgmProto "github.com/netbirdio/netbird/management/proto"
)
@@ -25,7 +26,7 @@ var ErrSourceRangesEmpty = errors.New("sources range is empty")
// Manager is a ACL rules manager
type Manager interface {
- ApplyFiltering(networkMap *mgmProto.NetworkMap)
+ ApplyFiltering(networkMap *mgmProto.NetworkMap, dnsRouteFeatureFlag bool)
}
type protoMatch struct {
@@ -53,7 +54,7 @@ func NewDefaultManager(fm firewall.Manager) *DefaultManager {
// ApplyFiltering firewall rules to the local firewall manager processed by ACL policy.
//
// If allowByDefault is true it appends allow ALL traffic rules to input and output chains.
-func (d *DefaultManager) ApplyFiltering(networkMap *mgmProto.NetworkMap) {
+func (d *DefaultManager) ApplyFiltering(networkMap *mgmProto.NetworkMap, dnsRouteFeatureFlag bool) {
d.mutex.Lock()
defer d.mutex.Unlock()
@@ -82,7 +83,7 @@ func (d *DefaultManager) ApplyFiltering(networkMap *mgmProto.NetworkMap) {
log.Errorf("failed to set legacy management flag: %v", err)
}
- if err := d.applyRouteACLs(networkMap.RoutesFirewallRules); err != nil {
+ if err := d.applyRouteACLs(networkMap.RoutesFirewallRules, dnsRouteFeatureFlag); err != nil {
log.Errorf("Failed to apply route ACLs: %v", err)
}
@@ -176,16 +177,16 @@ func (d *DefaultManager) applyPeerACLs(networkMap *mgmProto.NetworkMap) {
d.peerRulesPairs = newRulePairs
}
-func (d *DefaultManager) applyRouteACLs(rules []*mgmProto.RouteFirewallRule) error {
+func (d *DefaultManager) applyRouteACLs(rules []*mgmProto.RouteFirewallRule, dynamicResolver bool) error {
newRouteRules := make(map[id.RuleID]struct{}, len(rules))
var merr *multierror.Error
// Apply new rules - firewall manager will return existing rule ID if already present
for _, rule := range rules {
- id, err := d.applyRouteACL(rule)
+ id, err := d.applyRouteACL(rule, dynamicResolver)
if err != nil {
if errors.Is(err, ErrSourceRangesEmpty) {
- log.Debugf("skipping empty rule with destination %s: %v", rule.Destination, err)
+ log.Debugf("skipping empty sources rule with destination %s: %v", rule.Destination, err)
} else {
merr = multierror.Append(merr, fmt.Errorf("add route rule: %w", err))
}
@@ -208,7 +209,7 @@ func (d *DefaultManager) applyRouteACLs(rules []*mgmProto.RouteFirewallRule) err
return nberrors.FormatErrorOrNil(merr)
}
-func (d *DefaultManager) applyRouteACL(rule *mgmProto.RouteFirewallRule) (id.RuleID, error) {
+func (d *DefaultManager) applyRouteACL(rule *mgmProto.RouteFirewallRule, dynamicResolver bool) (id.RuleID, error) {
if len(rule.SourceRanges) == 0 {
return "", ErrSourceRangesEmpty
}
@@ -222,15 +223,9 @@ func (d *DefaultManager) applyRouteACL(rule *mgmProto.RouteFirewallRule) (id.Rul
sources = append(sources, source)
}
- var destination netip.Prefix
- if rule.IsDynamic {
- destination = getDefault(sources[0])
- } else {
- var err error
- destination, err = netip.ParsePrefix(rule.Destination)
- if err != nil {
- return "", fmt.Errorf("parse destination: %w", err)
- }
+ destination, err := determineDestination(rule, dynamicResolver, sources)
+ if err != nil {
+ return "", fmt.Errorf("determine destination: %w", err)
}
protocol, err := convertToFirewallProtocol(rule.Protocol)
@@ -580,6 +575,33 @@ func convertPortInfo(portInfo *mgmProto.PortInfo) *firewall.Port {
return nil
}
+func determineDestination(rule *mgmProto.RouteFirewallRule, dynamicResolver bool, sources []netip.Prefix) (firewall.Network, error) {
+ var destination firewall.Network
+
+ if rule.IsDynamic {
+ if dynamicResolver {
+ if len(rule.Domains) > 0 {
+ destination.Set = firewall.NewDomainSet(domain.FromPunycodeList(rule.Domains))
+ } else {
+ // isDynamic is set but no domains = outdated management server
+ log.Warn("connected to an older version of management server (no domains in rules), using default destination")
+ destination.Prefix = getDefault(sources[0])
+ }
+ } else {
+ // client resolves DNS, we (router) don't know the destination
+ destination.Prefix = getDefault(sources[0])
+ }
+ return destination, nil
+ }
+
+ prefix, err := netip.ParsePrefix(rule.Destination)
+ if err != nil {
+ return destination, fmt.Errorf("parse destination: %w", err)
+ }
+ destination.Prefix = prefix
+ return destination, nil
+}
+
func getDefault(prefix netip.Prefix) netip.Prefix {
if prefix.Addr().Is6() {
return netip.PrefixFrom(netip.IPv6Unspecified(), 0)
diff --git a/client/internal/acl/manager_test.go b/client/internal/acl/manager_test.go
index 9488d33ab..3595ca600 100644
--- a/client/internal/acl/manager_test.go
+++ b/client/internal/acl/manager_test.go
@@ -66,7 +66,7 @@ func TestDefaultManager(t *testing.T) {
acl := NewDefaultManager(fw)
t.Run("apply firewall rules", func(t *testing.T) {
- acl.ApplyFiltering(networkMap)
+ acl.ApplyFiltering(networkMap, false)
if len(acl.peerRulesPairs) != 2 {
t.Errorf("firewall rules not applied: %v", acl.peerRulesPairs)
@@ -92,7 +92,7 @@ func TestDefaultManager(t *testing.T) {
},
)
- acl.ApplyFiltering(networkMap)
+ acl.ApplyFiltering(networkMap, false)
// we should have one old and one new rule in the existed rules
if len(acl.peerRulesPairs) != 2 {
@@ -116,13 +116,13 @@ func TestDefaultManager(t *testing.T) {
networkMap.FirewallRules = networkMap.FirewallRules[:0]
networkMap.FirewallRulesIsEmpty = true
- if acl.ApplyFiltering(networkMap); len(acl.peerRulesPairs) != 0 {
+ if acl.ApplyFiltering(networkMap, false); len(acl.peerRulesPairs) != 0 {
t.Errorf("rules should be empty if FirewallRulesIsEmpty is set, got: %v", len(acl.peerRulesPairs))
return
}
networkMap.FirewallRulesIsEmpty = false
- acl.ApplyFiltering(networkMap)
+ acl.ApplyFiltering(networkMap, false)
if len(acl.peerRulesPairs) != 1 {
t.Errorf("rules should contain 1 rules if FirewallRulesIsEmpty is not set, got: %v", len(acl.peerRulesPairs))
return
@@ -359,7 +359,7 @@ func TestDefaultManagerEnableSSHRules(t *testing.T) {
}(fw)
acl := NewDefaultManager(fw)
- acl.ApplyFiltering(networkMap)
+ acl.ApplyFiltering(networkMap, false)
if len(acl.peerRulesPairs) != 3 {
t.Errorf("expect 3 rules (last must be SSH), got: %d", len(acl.peerRulesPairs))
diff --git a/client/internal/connect.go b/client/internal/connect.go
index 504c88c6f..832d58dcd 100644
--- a/client/internal/connect.go
+++ b/client/internal/connect.go
@@ -349,6 +349,25 @@ func (c *ConnectClient) Engine() *Engine {
return e
}
+// GetLatestNetworkMap returns the latest network map from the engine.
+func (c *ConnectClient) GetLatestNetworkMap() (*mgmProto.NetworkMap, error) {
+ engine := c.Engine()
+ if engine == nil {
+ return nil, errors.New("engine is not initialized")
+ }
+
+ networkMap, err := engine.GetLatestNetworkMap()
+ if err != nil {
+ return nil, fmt.Errorf("get latest network map: %w", err)
+ }
+
+ if networkMap == nil {
+ return nil, errors.New("network map is not available")
+ }
+
+ return networkMap, nil
+}
+
// Status returns the current client status
func (c *ConnectClient) Status() StatusType {
if c == nil {
diff --git a/client/internal/debug/debug.go b/client/internal/debug/debug.go
new file mode 100644
index 000000000..e07f981fe
--- /dev/null
+++ b/client/internal/debug/debug.go
@@ -0,0 +1,1022 @@
+package debug
+
+import (
+ "archive/zip"
+ "bufio"
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "io/fs"
+ "net"
+ "net/netip"
+ "os"
+ "path/filepath"
+ "runtime"
+ "runtime/pprof"
+ "sort"
+ "strings"
+ "time"
+
+ log "github.com/sirupsen/logrus"
+ "google.golang.org/protobuf/encoding/protojson"
+
+ "github.com/netbirdio/netbird/client/anonymize"
+ "github.com/netbirdio/netbird/client/internal"
+ "github.com/netbirdio/netbird/client/internal/peer"
+ "github.com/netbirdio/netbird/client/internal/statemanager"
+ mgmProto "github.com/netbirdio/netbird/management/proto"
+)
+
+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.
+netbird.err: Most recent, anonymized stderr log file of the NetBird client.
+netbird.out: Most recent, anonymized stdout log file of the NetBird client.
+routes.txt: Anonymized system routes, if --system-info flag was provided.
+interfaces.txt: Anonymized network interface information, if --system-info flag was provided.
+iptables.txt: Anonymized iptables rules with packet counters, if --system-info flag was provided.
+nftables.txt: Anonymized nftables rules with packet counters, if --system-info flag was provided.
+config.txt: Anonymized configuration information of the NetBird client.
+network_map.json: Anonymized network map containing peer configurations, routes, DNS settings, and firewall rules.
+state.json: Anonymized client state dump containing netbird states.
+mutex.prof: Mutex profiling information.
+goroutine.prof: Goroutine profiling information.
+block.prof: Block profiling information.
+heap.prof: Heap profiling information (snapshot of memory allocations).
+allocs.prof: Allocations profiling information.
+threadcreate.prof: Thread creation profiling information.
+
+
+Anonymization Process
+The files in this bundle have been anonymized to protect sensitive information. Here's how the anonymization was applied:
+
+IP Addresses
+
+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.
+
+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.
+
+Network Map
+The network_map.json file contains the following anonymized information:
+- Peer configurations (addresses, FQDNs, DNS settings)
+- Remote and offline peer information (allowed IPs, FQDNs)
+- Routes (network ranges, associated domains)
+- DNS configuration (nameservers, domains, custom zones)
+- Firewall rules (peer IPs, source/destination ranges)
+
+SSH keys in the network map are replaced with a placeholder value. All IP addresses and domains in the network map follow the same anonymization rules as described above.
+
+State File
+The state.json file contains anonymized internal state information of the NetBird client, including:
+- DNS settings and configuration
+- Firewall rules
+- Exclusion routes
+- Route selection
+- Other internal states that may be present
+
+The state file follows the same anonymization rules as other files:
+- IP addresses (both individual and CIDR ranges) are anonymized while preserving their structure
+- Domain names are consistently anonymized
+- Technical identifiers and non-sensitive data remain unchanged
+
+Mutex, Goroutines, Block, and Heap Profiling Files
+The goroutine, block, mutex, and heap profiling files contain process information that might help the NetBird team diagnose performance or memory issues. The information in these files doesn't contain personal data.
+You can check each using the following go command:
+
+go tool pprof -http=:8088 .prof
+
+For example, to view the heap profile:
+go tool pprof -http=:8088 heap.prof
+
+This will open a web browser tab with the profiling information.
+
+Routes
+For anonymized routes, the IP addresses are replaced as described above. The prefix length remains unchanged. Note that for prefixes, the anonymized IP might not be a network address, but the prefix length is still correct.
+
+Network Interfaces
+The interfaces.txt file contains information about network interfaces, including:
+- Interface name
+- Interface index
+- MTU (Maximum Transmission Unit)
+- Flags
+- IP addresses associated with each interface
+
+The IP addresses in the interfaces file are anonymized using the same process as described above. Interface names, indexes, MTUs, and flags are not anonymized.
+
+Configuration
+The config.txt file contains anonymized configuration information of the NetBird client. Sensitive information such as private keys and SSH keys are excluded. The following fields are anonymized:
+- ManagementURL
+- AdminURL
+- NATExternalIPs
+- CustomDNSAddress
+
+Other non-sensitive configuration options are included without anonymization.
+
+Firewall Rules (Linux only)
+The bundle includes two separate firewall rule files:
+
+iptables.txt:
+- Complete iptables ruleset with packet counters using 'iptables -v -n -L'
+- Includes all tables (filter, nat, mangle, raw, security)
+- Shows packet and byte counters for each rule
+- All IP addresses are anonymized
+- Chain names, table names, and other non-sensitive information remain unchanged
+
+nftables.txt:
+- Complete nftables ruleset obtained via 'nft -a list ruleset'
+- Includes rule handle numbers and packet counters
+- All tables, chains, and rules are included
+- Shows packet and byte counters for each rule
+- All IP addresses are anonymized
+- Chain names, table names, and other non-sensitive information remain unchanged
+`
+
+const (
+ clientLogFile = "client.log"
+ errorLogFile = "netbird.err"
+ stdoutLogFile = "netbird.out"
+
+ darwinErrorLogPath = "/var/log/netbird.out.log"
+ darwinStdoutLogPath = "/var/log/netbird.err.log"
+)
+
+type BundleGenerator struct {
+ anonymizer *anonymize.Anonymizer
+
+ // deps
+ internalConfig *internal.Config
+ statusRecorder *peer.Status
+ networkMap *mgmProto.NetworkMap
+ logFile string
+
+ // config
+ anonymize bool
+ clientStatus string
+ includeSystemInfo bool
+
+ archive *zip.Writer
+}
+
+type BundleConfig struct {
+ Anonymize bool
+ ClientStatus string
+ IncludeSystemInfo bool
+}
+
+type GeneratorDependencies struct {
+ InternalConfig *internal.Config
+ StatusRecorder *peer.Status
+ NetworkMap *mgmProto.NetworkMap
+ LogFile string
+}
+
+func NewBundleGenerator(deps GeneratorDependencies, cfg BundleConfig) *BundleGenerator {
+ return &BundleGenerator{
+ anonymizer: anonymize.NewAnonymizer(anonymize.DefaultAddresses()),
+
+ internalConfig: deps.InternalConfig,
+ statusRecorder: deps.StatusRecorder,
+ networkMap: deps.NetworkMap,
+ logFile: deps.LogFile,
+
+ anonymize: cfg.Anonymize,
+ clientStatus: cfg.ClientStatus,
+ includeSystemInfo: cfg.IncludeSystemInfo,
+ }
+}
+
+// Generate creates a debug bundle and returns the location.
+func (g *BundleGenerator) Generate() (resp string, err error) {
+ bundlePath, err := os.CreateTemp("", "netbird.debug.*.zip")
+ if err != nil {
+ return "", fmt.Errorf("create zip file: %w", err)
+ }
+ defer func() {
+ if closeErr := bundlePath.Close(); closeErr != nil && err == nil {
+ err = fmt.Errorf("close zip file: %w", closeErr)
+ }
+
+ if err != nil {
+ if removeErr := os.Remove(bundlePath.Name()); removeErr != nil {
+ log.Errorf("Failed to remove zip file: %v", removeErr)
+ }
+ }
+ }()
+
+ g.archive = zip.NewWriter(bundlePath)
+
+ if err := g.createArchive(); err != nil {
+ return "", err
+ }
+
+ if err := g.archive.Close(); err != nil {
+ return "", fmt.Errorf("close archive writer: %w", err)
+ }
+
+ return bundlePath.Name(), nil
+}
+
+func (g *BundleGenerator) createArchive() error {
+ if err := g.addReadme(); err != nil {
+ return fmt.Errorf("add readme: %w", err)
+ }
+
+ if err := g.addStatus(); err != nil {
+ return fmt.Errorf("add status: %w", err)
+ }
+
+ if g.statusRecorder != nil {
+ status := g.statusRecorder.GetFullStatus()
+ seedFromStatus(g.anonymizer, &status)
+ } else {
+ log.Debugf("no status recorder available for seeding")
+ }
+
+ if err := g.addConfig(); err != nil {
+ log.Errorf("Failed to add config to debug bundle: %v", err)
+ }
+
+ if g.includeSystemInfo {
+ g.addSystemInfo()
+ }
+
+ if err := g.addProf(); err != nil {
+ log.Errorf("Failed to add profiles to debug bundle: %v", err)
+ }
+
+ if err := g.addNetworkMap(); err != nil {
+ return fmt.Errorf("add network map: %w", err)
+ }
+
+ if err := g.addStateFile(); err != nil {
+ log.Errorf("Failed to add state file to debug bundle: %v", err)
+ }
+
+ if err := g.addCorruptedStateFiles(); err != nil {
+ log.Errorf("Failed to add corrupted state files to debug bundle: %v", err)
+ }
+
+ if g.logFile != "console" {
+ if err := g.addLogfile(); err != nil {
+ return fmt.Errorf("add log file: %w", err)
+ }
+ }
+ return nil
+}
+
+func (g *BundleGenerator) addSystemInfo() {
+ if err := g.addRoutes(); err != nil {
+ log.Errorf("Failed to add routes to debug bundle: %v", err)
+ }
+
+ if err := g.addInterfaces(); err != nil {
+ log.Errorf("Failed to add interfaces to debug bundle: %v", err)
+ }
+
+ if err := g.addFirewallRules(); err != nil {
+ log.Errorf("Failed to add firewall rules to debug bundle: %v", err)
+ }
+}
+
+func (g *BundleGenerator) addReadme() error {
+ readmeReader := strings.NewReader(readmeContent)
+ if err := g.addFileToZip(readmeReader, "README.txt"); err != nil {
+ return fmt.Errorf("add README file to zip: %w", err)
+ }
+ return nil
+}
+
+func (g *BundleGenerator) addStatus() error {
+ if status := g.clientStatus; status != "" {
+ statusReader := strings.NewReader(status)
+ if err := g.addFileToZip(statusReader, "status.txt"); err != nil {
+ return fmt.Errorf("add status file to zip: %w", err)
+ }
+ }
+ return nil
+}
+
+func (g *BundleGenerator) addConfig() error {
+ if g.internalConfig == nil {
+ log.Debug("skipping empty config in debug bundle")
+ return nil
+ }
+
+ var configContent strings.Builder
+ g.addCommonConfigFields(&configContent)
+
+ if g.anonymize {
+ if g.internalConfig.ManagementURL != nil {
+ configContent.WriteString(fmt.Sprintf("ManagementURL: %s\n", g.anonymizer.AnonymizeURI(g.internalConfig.ManagementURL.String())))
+ }
+ if g.internalConfig.AdminURL != nil {
+ configContent.WriteString(fmt.Sprintf("AdminURL: %s\n", g.anonymizer.AnonymizeURI(g.internalConfig.AdminURL.String())))
+ }
+ configContent.WriteString(fmt.Sprintf("NATExternalIPs: %v\n", anonymizeNATExternalIPs(g.internalConfig.NATExternalIPs, g.anonymizer)))
+ if g.internalConfig.CustomDNSAddress != "" {
+ configContent.WriteString(fmt.Sprintf("CustomDNSAddress: %s\n", g.anonymizer.AnonymizeString(g.internalConfig.CustomDNSAddress)))
+ }
+ } else {
+ if g.internalConfig.ManagementURL != nil {
+ configContent.WriteString(fmt.Sprintf("ManagementURL: %s\n", g.internalConfig.ManagementURL.String()))
+ }
+ if g.internalConfig.AdminURL != nil {
+ configContent.WriteString(fmt.Sprintf("AdminURL: %s\n", g.internalConfig.AdminURL.String()))
+ }
+ configContent.WriteString(fmt.Sprintf("NATExternalIPs: %v\n", g.internalConfig.NATExternalIPs))
+ if g.internalConfig.CustomDNSAddress != "" {
+ configContent.WriteString(fmt.Sprintf("CustomDNSAddress: %s\n", g.internalConfig.CustomDNSAddress))
+ }
+ }
+
+ // Add config content to zip file
+ configReader := strings.NewReader(configContent.String())
+ if err := g.addFileToZip(configReader, "config.txt"); err != nil {
+ return fmt.Errorf("add config file to zip: %w", err)
+ }
+
+ return nil
+}
+
+func (g *BundleGenerator) addCommonConfigFields(configContent *strings.Builder) {
+ configContent.WriteString("NetBird Client Configuration:\n\n")
+
+ // Add non-sensitive fields
+ configContent.WriteString(fmt.Sprintf("WgIface: %s\n", g.internalConfig.WgIface))
+ configContent.WriteString(fmt.Sprintf("WgPort: %d\n", g.internalConfig.WgPort))
+ if g.internalConfig.NetworkMonitor != nil {
+ configContent.WriteString(fmt.Sprintf("NetworkMonitor: %v\n", *g.internalConfig.NetworkMonitor))
+ }
+ configContent.WriteString(fmt.Sprintf("IFaceBlackList: %v\n", g.internalConfig.IFaceBlackList))
+ configContent.WriteString(fmt.Sprintf("DisableIPv6Discovery: %v\n", g.internalConfig.DisableIPv6Discovery))
+ configContent.WriteString(fmt.Sprintf("RosenpassEnabled: %v\n", g.internalConfig.RosenpassEnabled))
+ configContent.WriteString(fmt.Sprintf("RosenpassPermissive: %v\n", g.internalConfig.RosenpassPermissive))
+ if g.internalConfig.ServerSSHAllowed != nil {
+ configContent.WriteString(fmt.Sprintf("BundleGeneratorSSHAllowed: %v\n", *g.internalConfig.ServerSSHAllowed))
+ }
+ configContent.WriteString(fmt.Sprintf("DisableAutoConnect: %v\n", g.internalConfig.DisableAutoConnect))
+ configContent.WriteString(fmt.Sprintf("DNSRouteInterval: %s\n", g.internalConfig.DNSRouteInterval))
+
+ configContent.WriteString(fmt.Sprintf("DisableClientRoutes: %v\n", g.internalConfig.DisableClientRoutes))
+ configContent.WriteString(fmt.Sprintf("DisableBundleGeneratorRoutes: %v\n", g.internalConfig.DisableServerRoutes))
+ configContent.WriteString(fmt.Sprintf("DisableDNS: %v\n", g.internalConfig.DisableDNS))
+ configContent.WriteString(fmt.Sprintf("DisableFirewall: %v\n", g.internalConfig.DisableFirewall))
+
+ configContent.WriteString(fmt.Sprintf("BlockLANAccess: %v\n", g.internalConfig.BlockLANAccess))
+}
+
+func (g *BundleGenerator) addProf() (err error) {
+ defer func() {
+ if r := recover(); r != nil {
+ err = fmt.Errorf("panic while profiling: %v", r)
+ }
+ }()
+
+ runtime.SetBlockProfileRate(1)
+ _ = runtime.SetMutexProfileFraction(1)
+ defer runtime.SetBlockProfileRate(0)
+ defer runtime.SetMutexProfileFraction(0)
+
+ time.Sleep(5 * time.Second)
+
+ for _, profile := range []string{"goroutine", "block", "mutex", "heap", "allocs", "threadcreate"} {
+ var buff []byte
+ myBuff := bytes.NewBuffer(buff)
+ err := pprof.Lookup(profile).WriteTo(myBuff, 0)
+ if err != nil {
+ return fmt.Errorf("write %s profile: %w", profile, err)
+ }
+
+ if err := g.addFileToZip(myBuff, profile+".prof"); err != nil {
+ return fmt.Errorf("add %s file to zip: %w", profile, err)
+ }
+ }
+ return nil
+}
+
+func (g *BundleGenerator) addInterfaces() error {
+ interfaces, err := net.Interfaces()
+ if err != nil {
+ return fmt.Errorf("get interfaces: %w", err)
+ }
+
+ interfacesContent := formatInterfaces(interfaces, g.anonymize, g.anonymizer)
+ interfacesReader := strings.NewReader(interfacesContent)
+ if err := g.addFileToZip(interfacesReader, "interfaces.txt"); err != nil {
+ return fmt.Errorf("add interfaces file to zip: %w", err)
+ }
+
+ return nil
+}
+
+func (g *BundleGenerator) addNetworkMap() error {
+ if g.networkMap == nil {
+ log.Debugf("skipping empty network map in debug bundle")
+ return nil
+ }
+
+ if g.anonymize {
+ if err := anonymizeNetworkMap(g.networkMap, g.anonymizer); err != nil {
+ return fmt.Errorf("anonymize network map: %w", err)
+ }
+ }
+
+ options := protojson.MarshalOptions{
+ EmitUnpopulated: true,
+ UseProtoNames: true,
+ Indent: " ",
+ AllowPartial: true,
+ }
+
+ jsonBytes, err := options.Marshal(g.networkMap)
+ if err != nil {
+ return fmt.Errorf("generate json: %w", err)
+ }
+
+ if err := g.addFileToZip(bytes.NewReader(jsonBytes), "network_map.json"); err != nil {
+ return fmt.Errorf("add network map to zip: %w", err)
+ }
+
+ return nil
+}
+
+func (g *BundleGenerator) addStateFile() error {
+ path := statemanager.GetDefaultStatePath()
+ if path == "" {
+ return nil
+ }
+
+ data, err := os.ReadFile(path)
+ if err != nil {
+ if errors.Is(err, fs.ErrNotExist) {
+ return nil
+ }
+ return fmt.Errorf("read state file: %w", err)
+ }
+
+ if g.anonymize {
+ var rawStates map[string]json.RawMessage
+ if err := json.Unmarshal(data, &rawStates); err != nil {
+ return fmt.Errorf("unmarshal states: %w", err)
+ }
+
+ if err := anonymizeStateFile(&rawStates, g.anonymizer); err != nil {
+ return fmt.Errorf("anonymize state file: %w", err)
+ }
+
+ bs, err := json.MarshalIndent(rawStates, "", " ")
+ if err != nil {
+ return fmt.Errorf("marshal states: %w", err)
+ }
+ data = bs
+ }
+
+ if err := g.addFileToZip(bytes.NewReader(data), "state.json"); err != nil {
+ return fmt.Errorf("add state file to zip: %w", err)
+ }
+
+ return nil
+}
+
+func (g *BundleGenerator) addCorruptedStateFiles() error {
+ pattern := statemanager.GetDefaultStatePath()
+ if pattern == "" {
+ return nil
+ }
+ pattern += "*.corrupted.*"
+ matches, err := filepath.Glob(pattern)
+ if err != nil {
+ return fmt.Errorf("find corrupted state files: %w", err)
+ }
+
+ for _, match := range matches {
+ data, err := os.ReadFile(match)
+ if err != nil {
+ log.Warnf("Failed to read corrupted state file %s: %v", match, err)
+ continue
+ }
+
+ fileName := filepath.Base(match)
+ if err := 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
+ }
+
+ log.Debugf("Added corrupted state file to debug bundle: %s", fileName)
+ }
+
+ return nil
+}
+
+func (g *BundleGenerator) addLogfile() error {
+ if g.logFile == "" {
+ log.Debugf("skipping empty log file in debug bundle")
+ return nil
+ }
+
+ logDir := filepath.Dir(g.logFile)
+
+ if err := g.addSingleLogfile(g.logFile, clientLogFile); err != nil {
+ return fmt.Errorf("add client log file to zip: %w", err)
+ }
+
+ stdErrLogPath := filepath.Join(logDir, errorLogFile)
+ stdoutLogPath := filepath.Join(logDir, stdoutLogFile)
+ if runtime.GOOS == "darwin" {
+ stdErrLogPath = darwinErrorLogPath
+ stdoutLogPath = darwinStdoutLogPath
+ }
+
+ if err := g.addSingleLogfile(stdErrLogPath, errorLogFile); err != nil {
+ log.Warnf("Failed to add %s to zip: %v", errorLogFile, err)
+ }
+
+ if err := g.addSingleLogfile(stdoutLogPath, stdoutLogFile); err != nil {
+ log.Warnf("Failed to add %s to zip: %v", stdoutLogFile, err)
+ }
+
+ return nil
+}
+
+// addSingleLogfile adds a single log file to the archive
+func (g *BundleGenerator) addSingleLogfile(logPath, targetName string) error {
+ logFile, err := os.Open(logPath)
+ if err != nil {
+ return fmt.Errorf("open log file %s: %w", targetName, err)
+ }
+ defer func() {
+ if err := logFile.Close(); err != nil {
+ log.Errorf("Failed to close log file %s: %v", targetName, err)
+ }
+ }()
+
+ var logReader io.Reader
+ if g.anonymize {
+ var writer *io.PipeWriter
+ logReader, writer = io.Pipe()
+
+ go anonymizeLog(logFile, writer, g.anonymizer)
+ } else {
+ logReader = logFile
+ }
+
+ if err := g.addFileToZip(logReader, targetName); err != nil {
+ return fmt.Errorf("add %s to zip: %w", targetName, err)
+ }
+
+ return nil
+}
+
+func (g *BundleGenerator) addFileToZip(reader io.Reader, filename string) error {
+ header := &zip.FileHeader{
+ Name: filename,
+ Method: zip.Deflate,
+ Modified: time.Now(),
+
+ CreatorVersion: 20, // Version 2.0
+ ReaderVersion: 20, // Version 2.0
+ Flags: 0x800, // UTF-8 filename
+ }
+
+ // If the reader is a file, we can get more accurate information
+ if f, ok := reader.(*os.File); ok {
+ if stat, err := f.Stat(); err != nil {
+ log.Tracef("Failed to get file stat for %s: %v", filename, err)
+ } else {
+ header.Modified = stat.ModTime()
+ }
+ }
+
+ writer, err := g.archive.CreateHeader(header)
+ if err != nil {
+ return fmt.Errorf("create zip file header: %w", err)
+ }
+
+ if _, err := io.Copy(writer, reader); err != nil {
+ return fmt.Errorf("write file to zip: %w", err)
+ }
+
+ return nil
+}
+
+func seedFromStatus(a *anonymize.Anonymizer, status *peer.FullStatus) {
+ status.ManagementState.URL = a.AnonymizeURI(status.ManagementState.URL)
+ status.SignalState.URL = a.AnonymizeURI(status.SignalState.URL)
+
+ status.LocalPeerState.FQDN = a.AnonymizeDomain(status.LocalPeerState.FQDN)
+
+ for _, p := range status.Peers {
+ a.AnonymizeDomain(p.FQDN)
+ for route := range p.GetRoutes() {
+ a.AnonymizeRoute(route)
+ }
+ }
+
+ for route := range status.LocalPeerState.Routes {
+ a.AnonymizeRoute(route)
+ }
+
+ for _, nsGroup := range status.NSGroupStates {
+ for _, domain := range nsGroup.Domains {
+ a.AnonymizeDomain(domain)
+ }
+ }
+
+ for _, relay := range status.Relays {
+ if relay.URI != "" {
+ a.AnonymizeURI(relay.URI)
+ }
+ }
+}
+
+func formatRoutes(routes []netip.Prefix, anonymize bool, anonymizer *anonymize.Anonymizer) string {
+ var ipv4Routes, ipv6Routes []netip.Prefix
+
+ // Separate IPv4 and IPv6 routes
+ for _, route := range routes {
+ if route.Addr().Is4() {
+ ipv4Routes = append(ipv4Routes, route)
+ } else {
+ ipv6Routes = append(ipv6Routes, route)
+ }
+ }
+
+ // Sort IPv4 and IPv6 routes separately
+ sort.Slice(ipv4Routes, func(i, j int) bool {
+ return ipv4Routes[i].Bits() > ipv4Routes[j].Bits()
+ })
+ sort.Slice(ipv6Routes, func(i, j int) bool {
+ return ipv6Routes[i].Bits() > ipv6Routes[j].Bits()
+ })
+
+ var builder strings.Builder
+
+ // Format IPv4 routes
+ builder.WriteString("IPv4 Routes:\n")
+ for _, route := range ipv4Routes {
+ formatRoute(&builder, route, anonymize, anonymizer)
+ }
+
+ // Format IPv6 routes
+ builder.WriteString("\nIPv6 Routes:\n")
+ for _, route := range ipv6Routes {
+ formatRoute(&builder, route, anonymize, anonymizer)
+ }
+
+ return builder.String()
+}
+
+func formatRoute(builder *strings.Builder, route netip.Prefix, anonymize bool, anonymizer *anonymize.Anonymizer) {
+ if anonymize {
+ anonymizedIP := anonymizer.AnonymizeIP(route.Addr())
+ builder.WriteString(fmt.Sprintf("%s/%d\n", anonymizedIP, route.Bits()))
+ } else {
+ builder.WriteString(fmt.Sprintf("%s\n", route))
+ }
+}
+
+func formatInterfaces(interfaces []net.Interface, anonymize bool, anonymizer *anonymize.Anonymizer) string {
+ sort.Slice(interfaces, func(i, j int) bool {
+ return interfaces[i].Name < interfaces[j].Name
+ })
+
+ var builder strings.Builder
+ builder.WriteString("Network Interfaces:\n")
+
+ for _, iface := range interfaces {
+ builder.WriteString(fmt.Sprintf("\nInterface: %s\n", iface.Name))
+ builder.WriteString(fmt.Sprintf(" Index: %d\n", iface.Index))
+ builder.WriteString(fmt.Sprintf(" MTU: %d\n", iface.MTU))
+ builder.WriteString(fmt.Sprintf(" Flags: %v\n", iface.Flags))
+
+ addrs, err := iface.Addrs()
+ if err != nil {
+ builder.WriteString(fmt.Sprintf(" Addresses: Error retrieving addresses: %v\n", err))
+ } else {
+ builder.WriteString(" Addresses:\n")
+ for _, addr := range addrs {
+ prefix, err := netip.ParsePrefix(addr.String())
+ if err != nil {
+ builder.WriteString(fmt.Sprintf(" Error parsing address: %v\n", err))
+ continue
+ }
+ ip := prefix.Addr()
+ if anonymize {
+ ip = anonymizer.AnonymizeIP(ip)
+ }
+ builder.WriteString(fmt.Sprintf(" %s/%d\n", ip, prefix.Bits()))
+ }
+ }
+ }
+
+ return builder.String()
+}
+
+func anonymizeLog(reader io.Reader, writer *io.PipeWriter, anonymizer *anonymize.Anonymizer) {
+ defer func() {
+ // always nil
+ _ = writer.Close()
+ }()
+
+ scanner := bufio.NewScanner(reader)
+ for scanner.Scan() {
+ line := anonymizer.AnonymizeString(scanner.Text())
+ if _, err := writer.Write([]byte(line + "\n")); err != nil {
+ if err := writer.CloseWithError(fmt.Errorf("anonymize write: %w", err)); err != nil {
+ log.Errorf("Failed to close writer: %v", err)
+ }
+ return
+ }
+ }
+ if err := scanner.Err(); err != nil {
+ if err := writer.CloseWithError(fmt.Errorf("anonymize scan: %w", err)); err != nil {
+ log.Errorf("Failed to close writer: %v", err)
+ }
+ return
+ }
+}
+
+func anonymizeNATExternalIPs(ips []string, anonymizer *anonymize.Anonymizer) []string {
+ anonymizedIPs := make([]string, len(ips))
+ for i, ip := range ips {
+ parts := strings.SplitN(ip, "/", 2)
+
+ ip1, err := netip.ParseAddr(parts[0])
+ if err != nil {
+ anonymizedIPs[i] = ip
+ continue
+ }
+ ip1anon := anonymizer.AnonymizeIP(ip1)
+
+ if len(parts) == 2 {
+ ip2, err := netip.ParseAddr(parts[1])
+ if err != nil {
+ anonymizedIPs[i] = fmt.Sprintf("%s/%s", ip1anon, parts[1])
+ } else {
+ ip2anon := anonymizer.AnonymizeIP(ip2)
+ anonymizedIPs[i] = fmt.Sprintf("%s/%s", ip1anon, ip2anon)
+ }
+ } else {
+ anonymizedIPs[i] = ip1anon.String()
+ }
+ }
+ return anonymizedIPs
+}
+
+func anonymizeNetworkMap(networkMap *mgmProto.NetworkMap, anonymizer *anonymize.Anonymizer) error {
+ if networkMap.PeerConfig != nil {
+ anonymizePeerConfig(networkMap.PeerConfig, anonymizer)
+ }
+
+ for _, p := range networkMap.RemotePeers {
+ anonymizeRemotePeer(p, anonymizer)
+ }
+
+ for _, p := range networkMap.OfflinePeers {
+ anonymizeRemotePeer(p, anonymizer)
+ }
+
+ for _, r := range networkMap.Routes {
+ anonymizeRoute(r, anonymizer)
+ }
+
+ if networkMap.DNSConfig != nil {
+ anonymizeDNSConfig(networkMap.DNSConfig, anonymizer)
+ }
+
+ for _, rule := range networkMap.FirewallRules {
+ anonymizeFirewallRule(rule, anonymizer)
+ }
+
+ for _, rule := range networkMap.RoutesFirewallRules {
+ anonymizeRouteFirewallRule(rule, anonymizer)
+ }
+
+ return nil
+}
+
+func anonymizePeerConfig(config *mgmProto.PeerConfig, anonymizer *anonymize.Anonymizer) {
+ if config == nil {
+ return
+ }
+
+ if addr, err := netip.ParseAddr(config.Address); err == nil {
+ config.Address = anonymizer.AnonymizeIP(addr).String()
+ }
+
+ if config.SshConfig != nil && len(config.SshConfig.SshPubKey) > 0 {
+ config.SshConfig.SshPubKey = []byte("ssh-placeholder-key")
+ }
+
+ config.Dns = anonymizer.AnonymizeString(config.Dns)
+ config.Fqdn = anonymizer.AnonymizeDomain(config.Fqdn)
+}
+
+func anonymizeRemotePeer(peer *mgmProto.RemotePeerConfig, anonymizer *anonymize.Anonymizer) {
+ if peer == nil {
+ return
+ }
+
+ for i, ip := range peer.AllowedIps {
+ // Try to parse as prefix first (CIDR)
+ if prefix, err := netip.ParsePrefix(ip); err == nil {
+ anonIP := anonymizer.AnonymizeIP(prefix.Addr())
+ peer.AllowedIps[i] = fmt.Sprintf("%s/%d", anonIP, prefix.Bits())
+ } else if addr, err := netip.ParseAddr(ip); err == nil {
+ peer.AllowedIps[i] = anonymizer.AnonymizeIP(addr).String()
+ }
+ }
+
+ peer.Fqdn = anonymizer.AnonymizeDomain(peer.Fqdn)
+
+ if peer.SshConfig != nil && len(peer.SshConfig.SshPubKey) > 0 {
+ peer.SshConfig.SshPubKey = []byte("ssh-placeholder-key")
+ }
+}
+
+func anonymizeRoute(route *mgmProto.Route, anonymizer *anonymize.Anonymizer) {
+ if route == nil {
+ return
+ }
+
+ if prefix, err := netip.ParsePrefix(route.Network); err == nil {
+ anonIP := anonymizer.AnonymizeIP(prefix.Addr())
+ route.Network = fmt.Sprintf("%s/%d", anonIP, prefix.Bits())
+ }
+
+ for i, domain := range route.Domains {
+ route.Domains[i] = anonymizer.AnonymizeDomain(domain)
+ }
+
+ route.NetID = anonymizer.AnonymizeString(route.NetID)
+}
+
+func anonymizeDNSConfig(config *mgmProto.DNSConfig, anonymizer *anonymize.Anonymizer) {
+ if config == nil {
+ return
+ }
+
+ anonymizeNameBundleGeneratorGroups(config.NameServerGroups, anonymizer)
+ anonymizeCustomZones(config.CustomZones, anonymizer)
+}
+
+func anonymizeNameBundleGeneratorGroups(groups []*mgmProto.NameServerGroup, anonymizer *anonymize.Anonymizer) {
+ for _, group := range groups {
+ anonymizeBundleGenerators(group.NameServers, anonymizer)
+ anonymizeDomains(group.Domains, anonymizer)
+ }
+}
+
+func anonymizeBundleGenerators(servers []*mgmProto.NameServer, anonymizer *anonymize.Anonymizer) {
+ for _, server := range servers {
+ if addr, err := netip.ParseAddr(server.IP); err == nil {
+ server.IP = anonymizer.AnonymizeIP(addr).String()
+ }
+ }
+}
+
+func anonymizeDomains(domains []string, anonymizer *anonymize.Anonymizer) {
+ for i, domain := range domains {
+ domains[i] = anonymizer.AnonymizeDomain(domain)
+ }
+}
+
+func anonymizeCustomZones(zones []*mgmProto.CustomZone, anonymizer *anonymize.Anonymizer) {
+ for _, zone := range zones {
+ zone.Domain = anonymizer.AnonymizeDomain(zone.Domain)
+ anonymizeRecords(zone.Records, anonymizer)
+ }
+}
+
+func anonymizeRecords(records []*mgmProto.SimpleRecord, anonymizer *anonymize.Anonymizer) {
+ for _, record := range records {
+ record.Name = anonymizer.AnonymizeDomain(record.Name)
+ anonymizeRData(record, anonymizer)
+ }
+}
+
+func anonymizeRData(record *mgmProto.SimpleRecord, anonymizer *anonymize.Anonymizer) {
+ switch record.Type {
+ case 1, 28: // A or AAAA record
+ if addr, err := netip.ParseAddr(record.RData); err == nil {
+ record.RData = anonymizer.AnonymizeIP(addr).String()
+ }
+ default:
+ record.RData = anonymizer.AnonymizeString(record.RData)
+ }
+}
+
+func anonymizeFirewallRule(rule *mgmProto.FirewallRule, anonymizer *anonymize.Anonymizer) {
+ if rule == nil {
+ return
+ }
+
+ if addr, err := netip.ParseAddr(rule.PeerIP); err == nil {
+ rule.PeerIP = anonymizer.AnonymizeIP(addr).String()
+ }
+}
+
+func anonymizeRouteFirewallRule(rule *mgmProto.RouteFirewallRule, anonymizer *anonymize.Anonymizer) {
+ if rule == nil {
+ return
+ }
+
+ for i, sourceRange := range rule.SourceRanges {
+ if prefix, err := netip.ParsePrefix(sourceRange); err == nil {
+ anonIP := anonymizer.AnonymizeIP(prefix.Addr())
+ rule.SourceRanges[i] = fmt.Sprintf("%s/%d", anonIP, prefix.Bits())
+ }
+ }
+
+ if prefix, err := netip.ParsePrefix(rule.Destination); err == nil {
+ anonIP := anonymizer.AnonymizeIP(prefix.Addr())
+ rule.Destination = fmt.Sprintf("%s/%d", anonIP, prefix.Bits())
+ }
+}
+
+func anonymizeStateFile(rawStates *map[string]json.RawMessage, anonymizer *anonymize.Anonymizer) error {
+ for name, rawState := range *rawStates {
+ if string(rawState) == "null" {
+ continue
+ }
+
+ var state map[string]any
+ if err := json.Unmarshal(rawState, &state); err != nil {
+ return fmt.Errorf("unmarshal state %s: %w", name, err)
+ }
+
+ state = anonymizeValue(state, anonymizer).(map[string]any)
+
+ bs, err := json.Marshal(state)
+ if err != nil {
+ return fmt.Errorf("marshal state %s: %w", name, err)
+ }
+
+ (*rawStates)[name] = bs
+ }
+
+ return nil
+}
+
+func anonymizeValue(value any, anonymizer *anonymize.Anonymizer) any {
+ switch v := value.(type) {
+ case string:
+ return anonymizeString(v, anonymizer)
+ case map[string]any:
+ return anonymizeMap(v, anonymizer)
+ case []any:
+ return anonymizeSlice(v, anonymizer)
+ }
+ return value
+}
+
+func anonymizeString(v string, anonymizer *anonymize.Anonymizer) string {
+ if prefix, err := netip.ParsePrefix(v); err == nil {
+ anonIP := anonymizer.AnonymizeIP(prefix.Addr())
+ return fmt.Sprintf("%s/%d", anonIP, prefix.Bits())
+ }
+ if ip, err := netip.ParseAddr(v); err == nil {
+ return anonymizer.AnonymizeIP(ip).String()
+ }
+ return anonymizer.AnonymizeString(v)
+}
+
+func anonymizeMap(v map[string]any, anonymizer *anonymize.Anonymizer) map[string]any {
+ result := make(map[string]any, len(v))
+ for key, val := range v {
+ newKey := anonymizeMapKey(key, anonymizer)
+ result[newKey] = anonymizeValue(val, anonymizer)
+ }
+ return result
+}
+
+func anonymizeMapKey(key string, anonymizer *anonymize.Anonymizer) string {
+ if prefix, err := netip.ParsePrefix(key); err == nil {
+ anonIP := anonymizer.AnonymizeIP(prefix.Addr())
+ return fmt.Sprintf("%s/%d", anonIP, prefix.Bits())
+ }
+ if ip, err := netip.ParseAddr(key); err == nil {
+ return anonymizer.AnonymizeIP(ip).String()
+ }
+ return key
+}
+
+func anonymizeSlice(v []any, anonymizer *anonymize.Anonymizer) []any {
+ for i, val := range v {
+ v[i] = anonymizeValue(val, anonymizer)
+ }
+ return v
+}
diff --git a/client/server/debug_linux.go b/client/internal/debug/debug_linux.go
similarity index 92%
rename from client/server/debug_linux.go
rename to client/internal/debug/debug_linux.go
index 60bc40561..b4907beca 100644
--- a/client/server/debug_linux.go
+++ b/client/internal/debug/debug_linux.go
@@ -1,9 +1,8 @@
//go:build linux && !android
-package server
+package debug
import (
- "archive/zip"
"bytes"
"encoding/binary"
"fmt"
@@ -14,36 +13,31 @@ import (
"github.com/google/nftables"
"github.com/google/nftables/expr"
log "github.com/sirupsen/logrus"
-
- "github.com/netbirdio/netbird/client/anonymize"
- "github.com/netbirdio/netbird/client/proto"
)
// addFirewallRules collects and adds firewall rules to the archive
-func (s *Server) addFirewallRules(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) error {
+func (g *BundleGenerator) addFirewallRules() error {
log.Info("Collecting firewall rules")
- // Collect and add iptables rules
iptablesRules, err := collectIPTablesRules()
if err != nil {
log.Warnf("Failed to collect iptables rules: %v", err)
} else {
- if req.GetAnonymize() {
- iptablesRules = anonymizer.AnonymizeString(iptablesRules)
+ if g.anonymize {
+ iptablesRules = g.anonymizer.AnonymizeString(iptablesRules)
}
- if err := addFileToZip(archive, strings.NewReader(iptablesRules), "iptables.txt"); err != nil {
+ if err := g.addFileToZip(strings.NewReader(iptablesRules), "iptables.txt"); err != nil {
log.Warnf("Failed to add iptables rules to bundle: %v", err)
}
}
- // Collect and add nftables rules
nftablesRules, err := collectNFTablesRules()
if err != nil {
log.Warnf("Failed to collect nftables rules: %v", err)
} else {
- if req.GetAnonymize() {
- nftablesRules = anonymizer.AnonymizeString(nftablesRules)
+ if g.anonymize {
+ nftablesRules = g.anonymizer.AnonymizeString(nftablesRules)
}
- if err := addFileToZip(archive, strings.NewReader(nftablesRules), "nftables.txt"); err != nil {
+ if err := g.addFileToZip(strings.NewReader(nftablesRules), "nftables.txt"); err != nil {
log.Warnf("Failed to add nftables rules to bundle: %v", err)
}
}
@@ -65,16 +59,23 @@ func collectIPTablesRules() (string, error) {
builder.WriteString("\n")
}
- // Then get verbose statistics for each table
+ // Collect ipset information
+ ipsetOutput, err := collectIPSets()
+ if err != nil {
+ log.Warnf("Failed to collect ipset information: %v", err)
+ } else {
+ builder.WriteString("=== ipset list output ===\n")
+ builder.WriteString(ipsetOutput)
+ builder.WriteString("\n")
+ }
+
builder.WriteString("=== iptables -v -n -L output ===\n")
- // Get list of tables
tables := []string{"filter", "nat", "mangle", "raw", "security"}
for _, table := range tables {
builder.WriteString(fmt.Sprintf("*%s\n", table))
- // Get verbose statistics for the entire table
stats, err := getTableStatistics(table)
if err != nil {
log.Warnf("Failed to get statistics for table %s: %v", table, err)
@@ -87,6 +88,28 @@ func collectIPTablesRules() (string, error) {
return builder.String(), nil
}
+// collectIPSets collects information about ipsets
+func collectIPSets() (string, error) {
+ cmd := exec.Command("ipset", "list")
+ var stdout, stderr bytes.Buffer
+ cmd.Stdout = &stdout
+ cmd.Stderr = &stderr
+
+ if err := cmd.Run(); err != nil {
+ if strings.Contains(err.Error(), "executable file not found") {
+ return "", fmt.Errorf("ipset command not found: %w", err)
+ }
+ return "", fmt.Errorf("execute ipset list: %w (stderr: %s)", err, stderr.String())
+ }
+
+ ipsets := stdout.String()
+ if strings.TrimSpace(ipsets) == "" {
+ return "No ipsets found", nil
+ }
+
+ return ipsets, nil
+}
+
// collectIPTablesSave uses iptables-save to get rule definitions
func collectIPTablesSave() (string, error) {
cmd := exec.Command("iptables-save")
@@ -182,12 +205,10 @@ func formatTables(conn *nftables.Conn, tables []*nftables.Table) string {
continue
}
- // Format chains
for _, chain := range chains {
formatChain(conn, table, chain, &builder)
}
- // Format sets
if sets, err := conn.GetSets(table); err != nil {
log.Warnf("Failed to get sets for table %s: %v", table.Name, err)
} else if len(sets) > 0 {
diff --git a/client/internal/debug/debug_mobile.go b/client/internal/debug/debug_mobile.go
new file mode 100644
index 000000000..c00c65132
--- /dev/null
+++ b/client/internal/debug/debug_mobile.go
@@ -0,0 +1,7 @@
+//go:build ios || android
+
+package debug
+
+func (g *BundleGenerator) addRoutes() error {
+ return nil
+}
diff --git a/client/internal/debug/debug_nonlinux.go b/client/internal/debug/debug_nonlinux.go
new file mode 100644
index 000000000..ef93620a0
--- /dev/null
+++ b/client/internal/debug/debug_nonlinux.go
@@ -0,0 +1,8 @@
+//go:build !linux || android
+
+package debug
+
+// collectFirewallRules returns nothing on non-linux systems
+func (g *BundleGenerator) addFirewallRules() error {
+ return nil
+}
diff --git a/client/internal/debug/debug_nonmobile.go b/client/internal/debug/debug_nonmobile.go
new file mode 100644
index 000000000..3b487f07f
--- /dev/null
+++ b/client/internal/debug/debug_nonmobile.go
@@ -0,0 +1,25 @@
+//go:build !ios && !android
+
+package debug
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/netbirdio/netbird/client/internal/routemanager/systemops"
+)
+
+func (g *BundleGenerator) addRoutes() error {
+ routes, err := systemops.GetRoutesFromTable()
+ if err != nil {
+ return fmt.Errorf("get routes: %w", err)
+ }
+
+ // TODO: get routes including nexthop
+ routesContent := formatRoutes(routes, g.anonymize, g.anonymizer)
+ routesReader := strings.NewReader(routesContent)
+ if err := g.addFileToZip(routesReader, "routes.txt"); err != nil {
+ return fmt.Errorf("add routes file to zip: %w", err)
+ }
+ return nil
+}
diff --git a/client/internal/debug/debug_test.go b/client/internal/debug/debug_test.go
new file mode 100644
index 000000000..eb91fed66
--- /dev/null
+++ b/client/internal/debug/debug_test.go
@@ -0,0 +1,543 @@
+package debug
+
+import (
+ "encoding/json"
+ "net"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/netbirdio/netbird/client/anonymize"
+ mgmProto "github.com/netbirdio/netbird/management/proto"
+)
+
+func TestAnonymizeStateFile(t *testing.T) {
+ testState := map[string]json.RawMessage{
+ "null_state": json.RawMessage("null"),
+ "test_state": mustMarshal(map[string]any{
+ // Test simple fields
+ "public_ip": "203.0.113.1",
+ "private_ip": "192.168.1.1",
+ "protected_ip": "100.64.0.1",
+ "well_known_ip": "8.8.8.8",
+ "ipv6_addr": "2001:db8::1",
+ "private_ipv6": "fd00::1",
+ "domain": "test.example.com",
+ "uri": "stun:stun.example.com:3478",
+ "uri_with_ip": "turn:203.0.113.1:3478",
+ "netbird_domain": "device.netbird.cloud",
+
+ // Test CIDR ranges
+ "public_cidr": "203.0.113.0/24",
+ "private_cidr": "192.168.0.0/16",
+ "protected_cidr": "100.64.0.0/10",
+ "ipv6_cidr": "2001:db8::/32",
+ "private_ipv6_cidr": "fd00::/8",
+
+ // Test nested structures
+ "nested": map[string]any{
+ "ip": "203.0.113.2",
+ "domain": "nested.example.com",
+ "more_nest": map[string]any{
+ "ip": "203.0.113.3",
+ "domain": "deep.example.com",
+ },
+ },
+
+ // Test arrays
+ "string_array": []any{
+ "203.0.113.4",
+ "test1.example.com",
+ "test2.example.com",
+ },
+ "object_array": []any{
+ map[string]any{
+ "ip": "203.0.113.5",
+ "domain": "array1.example.com",
+ },
+ map[string]any{
+ "ip": "203.0.113.6",
+ "domain": "array2.example.com",
+ },
+ },
+
+ // Test multiple occurrences of same value
+ "duplicate_ip": "203.0.113.1", // Same as public_ip
+ "duplicate_domain": "test.example.com", // Same as domain
+
+ // Test URIs with various schemes
+ "stun_uri": "stun:stun.example.com:3478",
+ "turns_uri": "turns:turns.example.com:5349",
+ "http_uri": "http://web.example.com:80",
+ "https_uri": "https://secure.example.com:443",
+
+ // Test strings that might look like IPs but aren't
+ "not_ip": "300.300.300.300",
+ "partial_ip": "192.168",
+ "ip_like_string": "1234.5678",
+
+ // Test mixed content strings
+ "mixed_content": "Server at 203.0.113.1 (test.example.com) on port 80",
+
+ // Test empty and special values
+ "empty_string": "",
+ "null_value": nil,
+ "numeric_value": 42,
+ "boolean_value": true,
+ }),
+ "route_state": mustMarshal(map[string]any{
+ "routes": []any{
+ map[string]any{
+ "network": "203.0.113.0/24",
+ "gateway": "203.0.113.1",
+ "domains": []any{
+ "route1.example.com",
+ "route2.example.com",
+ },
+ },
+ map[string]any{
+ "network": "2001:db8::/32",
+ "gateway": "2001:db8::1",
+ "domains": []any{
+ "route3.example.com",
+ "route4.example.com",
+ },
+ },
+ },
+ // Test map with IP/CIDR keys
+ "refCountMap": map[string]any{
+ "203.0.113.1/32": map[string]any{
+ "Count": 1,
+ "Out": map[string]any{
+ "IP": "192.168.0.1",
+ "Intf": map[string]any{
+ "Name": "eth0",
+ "Index": 1,
+ },
+ },
+ },
+ "2001:db8::1/128": map[string]any{
+ "Count": 1,
+ "Out": map[string]any{
+ "IP": "fe80::1",
+ "Intf": map[string]any{
+ "Name": "eth0",
+ "Index": 1,
+ },
+ },
+ },
+ "10.0.0.1/32": map[string]any{ // private IP should remain unchanged
+ "Count": 1,
+ "Out": map[string]any{
+ "IP": "192.168.0.1",
+ },
+ },
+ },
+ }),
+ }
+
+ anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
+
+ // Pre-seed the domains we need to verify in the test assertions
+ anonymizer.AnonymizeDomain("test.example.com")
+ anonymizer.AnonymizeDomain("nested.example.com")
+ anonymizer.AnonymizeDomain("deep.example.com")
+ anonymizer.AnonymizeDomain("array1.example.com")
+
+ err := anonymizeStateFile(&testState, anonymizer)
+ require.NoError(t, err)
+
+ // Helper function to unmarshal and get nested values
+ var state map[string]any
+ err = json.Unmarshal(testState["test_state"], &state)
+ require.NoError(t, err)
+
+ // Test null state remains unchanged
+ require.Equal(t, "null", string(testState["null_state"]))
+
+ // Basic assertions
+ assert.NotEqual(t, "203.0.113.1", state["public_ip"])
+ assert.Equal(t, "192.168.1.1", state["private_ip"]) // Private IP unchanged
+ assert.Equal(t, "100.64.0.1", state["protected_ip"]) // Protected IP unchanged
+ assert.Equal(t, "8.8.8.8", state["well_known_ip"]) // Well-known IP unchanged
+ assert.NotEqual(t, "2001:db8::1", state["ipv6_addr"])
+ assert.Equal(t, "fd00::1", state["private_ipv6"]) // Private IPv6 unchanged
+ assert.NotEqual(t, "test.example.com", state["domain"])
+ assert.True(t, strings.HasSuffix(state["domain"].(string), ".domain"))
+ assert.Equal(t, "device.netbird.cloud", state["netbird_domain"]) // Netbird domain unchanged
+
+ // CIDR ranges
+ assert.NotEqual(t, "203.0.113.0/24", state["public_cidr"])
+ assert.Contains(t, state["public_cidr"], "/24") // Prefix preserved
+ assert.Equal(t, "192.168.0.0/16", state["private_cidr"]) // Private CIDR unchanged
+ assert.Equal(t, "100.64.0.0/10", state["protected_cidr"]) // Protected CIDR unchanged
+ assert.NotEqual(t, "2001:db8::/32", state["ipv6_cidr"])
+ assert.Contains(t, state["ipv6_cidr"], "/32") // IPv6 prefix preserved
+
+ // Nested structures
+ nested := state["nested"].(map[string]any)
+ assert.NotEqual(t, "203.0.113.2", nested["ip"])
+ assert.NotEqual(t, "nested.example.com", nested["domain"])
+ moreNest := nested["more_nest"].(map[string]any)
+ assert.NotEqual(t, "203.0.113.3", moreNest["ip"])
+ assert.NotEqual(t, "deep.example.com", moreNest["domain"])
+
+ // Arrays
+ strArray := state["string_array"].([]any)
+ assert.NotEqual(t, "203.0.113.4", strArray[0])
+ assert.NotEqual(t, "test1.example.com", strArray[1])
+ assert.True(t, strings.HasSuffix(strArray[1].(string), ".domain"))
+
+ objArray := state["object_array"].([]any)
+ firstObj := objArray[0].(map[string]any)
+ assert.NotEqual(t, "203.0.113.5", firstObj["ip"])
+ assert.NotEqual(t, "array1.example.com", firstObj["domain"])
+
+ // Duplicate values should be anonymized consistently
+ assert.Equal(t, state["public_ip"], state["duplicate_ip"])
+ assert.Equal(t, state["domain"], state["duplicate_domain"])
+
+ // URIs
+ assert.NotContains(t, state["stun_uri"], "stun.example.com")
+ assert.NotContains(t, state["turns_uri"], "turns.example.com")
+ assert.NotContains(t, state["http_uri"], "web.example.com")
+ assert.NotContains(t, state["https_uri"], "secure.example.com")
+
+ // Non-IP strings should remain unchanged
+ assert.Equal(t, "300.300.300.300", state["not_ip"])
+ assert.Equal(t, "192.168", state["partial_ip"])
+ assert.Equal(t, "1234.5678", state["ip_like_string"])
+
+ // Mixed content should have IPs and domains replaced
+ mixedContent := state["mixed_content"].(string)
+ assert.NotContains(t, mixedContent, "203.0.113.1")
+ assert.NotContains(t, mixedContent, "test.example.com")
+ assert.Contains(t, mixedContent, "Server at ")
+ assert.Contains(t, mixedContent, " on port 80")
+
+ // Special values should remain unchanged
+ assert.Equal(t, "", state["empty_string"])
+ assert.Nil(t, state["null_value"])
+ assert.Equal(t, float64(42), state["numeric_value"])
+ assert.Equal(t, true, state["boolean_value"])
+
+ // Check route state
+ var routeState map[string]any
+ err = json.Unmarshal(testState["route_state"], &routeState)
+ require.NoError(t, err)
+
+ routes := routeState["routes"].([]any)
+ route1 := routes[0].(map[string]any)
+ assert.NotEqual(t, "203.0.113.0/24", route1["network"])
+ assert.Contains(t, route1["network"], "/24")
+ assert.NotEqual(t, "203.0.113.1", route1["gateway"])
+ domains := route1["domains"].([]any)
+ assert.True(t, strings.HasSuffix(domains[0].(string), ".domain"))
+ assert.True(t, strings.HasSuffix(domains[1].(string), ".domain"))
+
+ // Check map keys are anonymized
+ refCountMap := routeState["refCountMap"].(map[string]any)
+ hasPublicIPKey := false
+ hasIPv6Key := false
+ hasPrivateIPKey := false
+ for key := range refCountMap {
+ if strings.Contains(key, "203.0.113.1") {
+ hasPublicIPKey = true
+ }
+ if strings.Contains(key, "2001:db8::1") {
+ hasIPv6Key = true
+ }
+ if key == "10.0.0.1/32" {
+ hasPrivateIPKey = true
+ }
+ }
+ assert.False(t, hasPublicIPKey, "public IP in key should be anonymized")
+ assert.False(t, hasIPv6Key, "IPv6 in key should be anonymized")
+ assert.True(t, hasPrivateIPKey, "private IP in key should remain unchanged")
+}
+
+func mustMarshal(v any) json.RawMessage {
+ data, err := json.Marshal(v)
+ if err != nil {
+ panic(err)
+ }
+ return data
+}
+
+func TestAnonymizeNetworkMap(t *testing.T) {
+ networkMap := &mgmProto.NetworkMap{
+ PeerConfig: &mgmProto.PeerConfig{
+ Address: "203.0.113.5",
+ Dns: "1.2.3.4",
+ Fqdn: "peer1.corp.example.com",
+ SshConfig: &mgmProto.SSHConfig{
+ SshPubKey: []byte("ssh-rsa AAAAB3NzaC1..."),
+ },
+ },
+ RemotePeers: []*mgmProto.RemotePeerConfig{
+ {
+ AllowedIps: []string{
+ "203.0.113.1/32",
+ "2001:db8:1234::1/128",
+ "192.168.1.1/32",
+ "100.64.0.1/32",
+ "10.0.0.1/32",
+ },
+ Fqdn: "peer2.corp.example.com",
+ SshConfig: &mgmProto.SSHConfig{
+ SshPubKey: []byte("ssh-rsa AAAAB3NzaC2..."),
+ },
+ },
+ },
+ Routes: []*mgmProto.Route{
+ {
+ Network: "197.51.100.0/24",
+ Domains: []string{"prod.example.com", "staging.example.com"},
+ NetID: "net-123abc",
+ },
+ },
+ DNSConfig: &mgmProto.DNSConfig{
+ NameServerGroups: []*mgmProto.NameServerGroup{
+ {
+ NameServers: []*mgmProto.NameServer{
+ {IP: "8.8.8.8"},
+ {IP: "1.1.1.1"},
+ {IP: "203.0.113.53"},
+ },
+ Domains: []string{"example.com", "internal.example.com"},
+ },
+ },
+ CustomZones: []*mgmProto.CustomZone{
+ {
+ Domain: "custom.example.com",
+ Records: []*mgmProto.SimpleRecord{
+ {
+ Name: "www.custom.example.com",
+ Type: 1,
+ RData: "203.0.113.10",
+ },
+ {
+ Name: "internal.custom.example.com",
+ Type: 1,
+ RData: "192.168.1.10",
+ },
+ },
+ },
+ },
+ },
+ }
+
+ // Create anonymizer with test addresses
+ anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
+
+ // Anonymize the network map
+ err := anonymizeNetworkMap(networkMap, anonymizer)
+ require.NoError(t, err)
+
+ // Test PeerConfig anonymization
+ peerCfg := networkMap.PeerConfig
+ require.NotEqual(t, "203.0.113.5", peerCfg.Address)
+
+ // Verify DNS and FQDN are properly anonymized
+ require.NotEqual(t, "1.2.3.4", peerCfg.Dns)
+ require.NotEqual(t, "peer1.corp.example.com", peerCfg.Fqdn)
+ require.True(t, strings.HasSuffix(peerCfg.Fqdn, ".domain"))
+
+ // Verify SSH key is replaced
+ require.Equal(t, []byte("ssh-placeholder-key"), peerCfg.SshConfig.SshPubKey)
+
+ // Test RemotePeers anonymization
+ remotePeer := networkMap.RemotePeers[0]
+
+ // Verify FQDN is anonymized
+ require.NotEqual(t, "peer2.corp.example.com", remotePeer.Fqdn)
+ require.True(t, strings.HasSuffix(remotePeer.Fqdn, ".domain"))
+
+ // Check that public IPs are anonymized but private IPs are preserved
+ for _, allowedIP := range remotePeer.AllowedIps {
+ ip, _, err := net.ParseCIDR(allowedIP)
+ require.NoError(t, err)
+
+ if ip.IsPrivate() || isInCGNATRange(ip) {
+ require.Contains(t, []string{
+ "192.168.1.1/32",
+ "100.64.0.1/32",
+ "10.0.0.1/32",
+ }, allowedIP)
+ } else {
+ require.NotContains(t, []string{
+ "203.0.113.1/32",
+ "2001:db8:1234::1/128",
+ }, allowedIP)
+ }
+ }
+
+ // Test Routes anonymization
+ route := networkMap.Routes[0]
+ require.NotEqual(t, "197.51.100.0/24", route.Network)
+ for _, domain := range route.Domains {
+ require.True(t, strings.HasSuffix(domain, ".domain"))
+ require.NotContains(t, domain, "example.com")
+ }
+
+ // Test DNS config anonymization
+ dnsConfig := networkMap.DNSConfig
+ nameServerGroup := dnsConfig.NameServerGroups[0]
+
+ // Verify well-known DNS servers are preserved
+ require.Equal(t, "8.8.8.8", nameServerGroup.NameServers[0].IP)
+ require.Equal(t, "1.1.1.1", nameServerGroup.NameServers[1].IP)
+
+ // Verify public DNS server is anonymized
+ require.NotEqual(t, "203.0.113.53", nameServerGroup.NameServers[2].IP)
+
+ // Verify domains are anonymized
+ for _, domain := range nameServerGroup.Domains {
+ require.True(t, strings.HasSuffix(domain, ".domain"))
+ require.NotContains(t, domain, "example.com")
+ }
+
+ // Test CustomZones anonymization
+ customZone := dnsConfig.CustomZones[0]
+ require.True(t, strings.HasSuffix(customZone.Domain, ".domain"))
+ require.NotContains(t, customZone.Domain, "example.com")
+
+ // Verify records are properly anonymized
+ for _, record := range customZone.Records {
+ require.True(t, strings.HasSuffix(record.Name, ".domain"))
+ require.NotContains(t, record.Name, "example.com")
+
+ ip := net.ParseIP(record.RData)
+ if ip != nil {
+ if !ip.IsPrivate() {
+ require.NotEqual(t, "203.0.113.10", record.RData)
+ } else {
+ require.Equal(t, "192.168.1.10", record.RData)
+ }
+ }
+ }
+}
+
+// Helper function to check if IP is in CGNAT range
+func isInCGNATRange(ip net.IP) bool {
+ cgnat := net.IPNet{
+ IP: net.ParseIP("100.64.0.0"),
+ Mask: net.CIDRMask(10, 32),
+ }
+ return cgnat.Contains(ip)
+}
+
+func TestAnonymizeFirewallRules(t *testing.T) {
+ // TODO: Add ipv6
+
+ // Example iptables-save output
+ iptablesSave := `# Generated by iptables-save v1.8.7 on Thu Dec 19 10:00:00 2024
+*filter
+:INPUT ACCEPT [0:0]
+:FORWARD ACCEPT [0:0]
+:OUTPUT ACCEPT [0:0]
+-A INPUT -s 192.168.1.0/24 -j ACCEPT
+-A INPUT -s 44.192.140.1/32 -j DROP
+-A FORWARD -s 10.0.0.0/8 -j DROP
+-A FORWARD -s 44.192.140.0/24 -d 52.84.12.34/24 -j ACCEPT
+COMMIT
+
+*nat
+:PREROUTING ACCEPT [0:0]
+:INPUT ACCEPT [0:0]
+:OUTPUT ACCEPT [0:0]
+:POSTROUTING ACCEPT [0:0]
+-A POSTROUTING -s 192.168.100.0/24 -j MASQUERADE
+-A PREROUTING -d 44.192.140.10/32 -p tcp -m tcp --dport 80 -j DNAT --to-destination 192.168.1.10:80
+COMMIT`
+
+ // Example iptables -v -n -L output
+ iptablesVerbose := `Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
+ pkts bytes target prot opt in out source destination
+ 0 0 ACCEPT all -- * * 192.168.1.0/24 0.0.0.0/0
+ 100 1024 DROP all -- * * 44.192.140.1 0.0.0.0/0
+
+Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
+ pkts bytes target prot opt in out source destination
+ 0 0 DROP all -- * * 10.0.0.0/8 0.0.0.0/0
+ 25 256 ACCEPT all -- * * 44.192.140.0/24 52.84.12.34/24
+
+Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
+ pkts bytes target prot opt in out source destination`
+
+ // Example nftables output
+ nftablesRules := `table inet filter {
+ chain input {
+ type filter hook input priority filter; policy accept;
+ ip saddr 192.168.1.1 accept
+ ip saddr 44.192.140.1 drop
+ }
+ chain forward {
+ type filter hook forward priority filter; policy accept;
+ ip saddr 10.0.0.0/8 drop
+ ip saddr 44.192.140.0/24 ip daddr 52.84.12.34/24 accept
+ }
+ }`
+
+ anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
+
+ // Test iptables-save anonymization
+ anonIptablesSave := anonymizer.AnonymizeString(iptablesSave)
+
+ // Private IP addresses should remain unchanged
+ assert.Contains(t, anonIptablesSave, "192.168.1.0/24")
+ assert.Contains(t, anonIptablesSave, "10.0.0.0/8")
+ assert.Contains(t, anonIptablesSave, "192.168.100.0/24")
+ assert.Contains(t, anonIptablesSave, "192.168.1.10")
+
+ // Public IP addresses should be anonymized to the default range
+ assert.NotContains(t, anonIptablesSave, "44.192.140.1")
+ assert.NotContains(t, anonIptablesSave, "44.192.140.0/24")
+ assert.NotContains(t, anonIptablesSave, "52.84.12.34")
+ assert.Contains(t, anonIptablesSave, "198.51.100.") // Default anonymous range
+
+ // Structure should be preserved
+ assert.Contains(t, anonIptablesSave, "*filter")
+ assert.Contains(t, anonIptablesSave, ":INPUT ACCEPT [0:0]")
+ assert.Contains(t, anonIptablesSave, "COMMIT")
+ assert.Contains(t, anonIptablesSave, "-j MASQUERADE")
+ assert.Contains(t, anonIptablesSave, "--dport 80")
+
+ // Test iptables verbose output anonymization
+ anonIptablesVerbose := anonymizer.AnonymizeString(iptablesVerbose)
+
+ // Private IP addresses should remain unchanged
+ assert.Contains(t, anonIptablesVerbose, "192.168.1.0/24")
+ assert.Contains(t, anonIptablesVerbose, "10.0.0.0/8")
+
+ // Public IP addresses should be anonymized to the default range
+ assert.NotContains(t, anonIptablesVerbose, "44.192.140.1")
+ assert.NotContains(t, anonIptablesVerbose, "44.192.140.0/24")
+ assert.NotContains(t, anonIptablesVerbose, "52.84.12.34")
+ assert.Contains(t, anonIptablesVerbose, "198.51.100.") // Default anonymous range
+
+ // Structure and counters should be preserved
+ assert.Contains(t, anonIptablesVerbose, "Chain INPUT (policy ACCEPT 0 packets, 0 bytes)")
+ assert.Contains(t, anonIptablesVerbose, "100 1024 DROP")
+ assert.Contains(t, anonIptablesVerbose, "pkts bytes target")
+
+ // Test nftables anonymization
+ anonNftables := anonymizer.AnonymizeString(nftablesRules)
+
+ // Private IP addresses should remain unchanged
+ assert.Contains(t, anonNftables, "192.168.1.1")
+ assert.Contains(t, anonNftables, "10.0.0.0/8")
+
+ // Public IP addresses should be anonymized to the default range
+ assert.NotContains(t, anonNftables, "44.192.140.1")
+ assert.NotContains(t, anonNftables, "44.192.140.0/24")
+ assert.NotContains(t, anonNftables, "52.84.12.34")
+ assert.Contains(t, anonNftables, "198.51.100.") // Default anonymous range
+
+ // Structure should be preserved
+ assert.Contains(t, anonNftables, "table inet filter {")
+ assert.Contains(t, anonNftables, "chain input {")
+ assert.Contains(t, anonNftables, "type filter hook input priority filter; policy accept;")
+}
diff --git a/client/internal/dnsfwd/forwarder.go b/client/internal/dnsfwd/forwarder.go
index 097daa9e2..8f6a31f47 100644
--- a/client/internal/dnsfwd/forwarder.go
+++ b/client/internal/dnsfwd/forwarder.go
@@ -3,17 +3,24 @@ package dnsfwd
import (
"context"
"errors"
+ "fmt"
+ "math"
"net"
"net/netip"
"strings"
"sync"
"time"
+ "github.com/hashicorp/go-multierror"
"github.com/miekg/dns"
log "github.com/sirupsen/logrus"
+ nberrors "github.com/netbirdio/netbird/client/errors"
+ firewall "github.com/netbirdio/netbird/client/firewall/manager"
"github.com/netbirdio/netbird/client/internal/peer"
nbdns "github.com/netbirdio/netbird/dns"
+ "github.com/netbirdio/netbird/management/domain"
+ "github.com/netbirdio/netbird/route"
)
const errResolveFailed = "failed to resolve query for domain=%s: %v"
@@ -22,25 +29,27 @@ const upstreamTimeout = 15 * time.Second
type DNSForwarder struct {
listenAddress string
ttl uint32
- domains []string
statusRecorder *peer.Status
dnsServer *dns.Server
mux *dns.ServeMux
- resId sync.Map
+ mutex sync.RWMutex
+ fwdEntries []*ForwarderEntry
+ firewall firewall.Manager
}
-func NewDNSForwarder(listenAddress string, ttl uint32, statusRecorder *peer.Status) *DNSForwarder {
+func NewDNSForwarder(listenAddress string, ttl uint32, firewall firewall.Manager, statusRecorder *peer.Status) *DNSForwarder {
log.Debugf("creating DNS forwarder with listen_address=%s ttl=%d", listenAddress, ttl)
return &DNSForwarder{
listenAddress: listenAddress,
ttl: ttl,
+ firewall: firewall,
statusRecorder: statusRecorder,
}
}
-func (f *DNSForwarder) Listen(domains []string, resIds map[string]string) error {
+func (f *DNSForwarder) Listen(entries []*ForwarderEntry) error {
log.Infof("listen DNS forwarder on address=%s", f.listenAddress)
mux := dns.NewServeMux()
@@ -52,32 +61,35 @@ func (f *DNSForwarder) Listen(domains []string, resIds map[string]string) error
f.dnsServer = dnsServer
f.mux = mux
- f.UpdateDomains(domains, resIds)
+ f.UpdateDomains(entries)
return dnsServer.ListenAndServe()
}
-func (f *DNSForwarder) UpdateDomains(domains []string, resIds map[string]string) {
- log.Debugf("Updating domains from %v to %v", f.domains, domains)
+func (f *DNSForwarder) UpdateDomains(entries []*ForwarderEntry) {
+ f.mutex.Lock()
+ defer f.mutex.Unlock()
- for _, d := range f.domains {
- f.mux.HandleRemove(d)
- f.statusRecorder.RemoveResolvedIPLookupEntry(d)
+ if f.mux == nil {
+ log.Debug("DNS mux is nil, skipping domain update")
+ f.fwdEntries = entries
+ return
}
- f.resId.Clear()
- newDomains := filterDomains(domains)
+ oldDomains := filterDomains(f.fwdEntries)
+
+ for _, d := range oldDomains {
+ f.mux.HandleRemove(d.PunycodeString())
+ }
+
+ newDomains := filterDomains(entries)
for _, d := range newDomains {
- f.mux.HandleFunc(d, f.handleDNSQuery)
+ f.mux.HandleFunc(d.PunycodeString(), f.handleDNSQuery)
}
- for domain, resId := range resIds {
- if domain != "" {
- f.resId.Store(domain, resId)
- }
- }
+ f.fwdEntries = entries
- f.domains = newDomains
+ log.Debugf("Updated domains from %v to %v", oldDomains, newDomains)
}
func (f *DNSForwarder) Close(ctx context.Context) error {
@@ -91,11 +103,11 @@ func (f *DNSForwarder) handleDNSQuery(w dns.ResponseWriter, query *dns.Msg) {
if len(query.Question) == 0 {
return
}
- log.Tracef("received DNS request for DNS forwarder: domain=%v type=%v class=%v",
- query.Question[0].Name, query.Question[0].Qtype, query.Question[0].Qclass)
-
question := query.Question[0]
- domain := question.Name
+ log.Tracef("received DNS request for DNS forwarder: domain=%v type=%v class=%v",
+ question.Name, question.Qtype, question.Qclass)
+
+ domain := strings.ToLower(question.Name)
resp := query.SetReply(query)
var network string
@@ -122,21 +134,7 @@ func (f *DNSForwarder) handleDNSQuery(w dns.ResponseWriter, query *dns.Msg) {
return
}
- resId, ok := f.resId.Load(strings.TrimSuffix(domain, "."))
- if ok {
- for _, ip := range ips {
- var ipWithSuffix string
- if ip.Is4() {
- ipWithSuffix = ip.String() + "/32"
- log.Tracef("resolved domain=%s to IPv4=%s", domain, ipWithSuffix)
- } else {
- ipWithSuffix = ip.String() + "/128"
- log.Tracef("resolved domain=%s to IPv6=%s", domain, ipWithSuffix)
- }
- f.statusRecorder.AddResolvedIPLookupEntry(ipWithSuffix, resId.(string))
- }
- }
-
+ f.updateInternalState(domain, ips)
f.addIPsToResponse(resp, domain, ips)
if err := w.WriteMsg(resp); err != nil {
@@ -144,6 +142,42 @@ func (f *DNSForwarder) handleDNSQuery(w dns.ResponseWriter, query *dns.Msg) {
}
}
+func (f *DNSForwarder) updateInternalState(domain string, ips []netip.Addr) {
+ var prefixes []netip.Prefix
+ mostSpecificResId, matchingEntries := f.getMatchingEntries(strings.TrimSuffix(domain, "."))
+ if mostSpecificResId != "" {
+ for _, ip := range ips {
+ var prefix netip.Prefix
+ if ip.Is4() {
+ prefix = netip.PrefixFrom(ip, 32)
+ } else {
+ prefix = netip.PrefixFrom(ip, 128)
+ }
+ prefixes = append(prefixes, prefix)
+ f.statusRecorder.AddResolvedIPLookupEntry(prefix, mostSpecificResId)
+ }
+ }
+
+ if f.firewall != nil {
+ f.updateFirewall(matchingEntries, prefixes)
+ }
+}
+
+func (f *DNSForwarder) updateFirewall(matchingEntries []*ForwarderEntry, prefixes []netip.Prefix) {
+ var merr *multierror.Error
+ for _, entry := range matchingEntries {
+ if err := f.firewall.UpdateSet(entry.Set, prefixes); err != nil {
+ merr = multierror.Append(merr, fmt.Errorf("update set for domain=%s: %w", entry.Domain, err))
+ }
+ }
+ if merr != nil {
+ log.Errorf("failed to update firewall sets (%d/%d): %v",
+ len(merr.Errors),
+ len(matchingEntries),
+ nberrors.FormatErrorOrNil(merr))
+ }
+}
+
// handleDNSError processes DNS lookup errors and sends an appropriate error response
func (f *DNSForwarder) handleDNSError(w dns.ResponseWriter, resp *dns.Msg, domain string, err error) {
var dnsErr *net.DNSError
@@ -204,15 +238,53 @@ func (f *DNSForwarder) addIPsToResponse(resp *dns.Msg, domain string, ips []neti
}
}
+// getMatchingEntries retrieves the resource IDs for a given domain.
+// It returns the most specific match and all matching resource IDs.
+func (f *DNSForwarder) getMatchingEntries(domain string) (route.ResID, []*ForwarderEntry) {
+ var selectedResId route.ResID
+ var bestScore int
+ var matches []*ForwarderEntry
+
+ f.mutex.RLock()
+ defer f.mutex.RUnlock()
+
+ for _, entry := range f.fwdEntries {
+ var score int
+ pattern := entry.Domain.PunycodeString()
+
+ switch {
+ case strings.HasPrefix(pattern, "*."):
+ baseDomain := strings.TrimPrefix(pattern, "*.")
+
+ if strings.EqualFold(domain, baseDomain) || strings.HasSuffix(domain, "."+baseDomain) {
+ score = len(baseDomain)
+ matches = append(matches, entry)
+ }
+ case domain == pattern:
+ score = math.MaxInt
+ matches = append(matches, entry)
+ default:
+ continue
+ }
+
+ if score > bestScore {
+ bestScore = score
+ selectedResId = entry.ResID
+ }
+ }
+
+ return selectedResId, matches
+}
+
// filterDomains returns a list of normalized domains
-func filterDomains(domains []string) []string {
- newDomains := make([]string, 0, len(domains))
- for _, d := range domains {
- if d == "" {
+func filterDomains(entries []*ForwarderEntry) domain.List {
+ newDomains := make(domain.List, 0, len(entries))
+ for _, d := range entries {
+ if d.Domain == "" {
log.Warn("empty domain in DNS forwarder")
continue
}
- newDomains = append(newDomains, nbdns.NormalizeZone(d))
+ newDomains = append(newDomains, domain.Domain(nbdns.NormalizeZone(d.Domain.PunycodeString())))
}
return newDomains
}
diff --git a/client/internal/dnsfwd/forwarder_test.go b/client/internal/dnsfwd/forwarder_test.go
new file mode 100644
index 000000000..f0829bbbd
--- /dev/null
+++ b/client/internal/dnsfwd/forwarder_test.go
@@ -0,0 +1,103 @@
+package dnsfwd
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/netbirdio/netbird/management/domain"
+ "github.com/netbirdio/netbird/route"
+)
+
+func Test_getMatchingEntries(t *testing.T) {
+ testCases := []struct {
+ name string
+ storedMappings map[string]route.ResID // key: domain pattern, value: resId
+ queryDomain string
+ expectedResId route.ResID
+ }{
+ {
+ name: "Empty map returns empty string",
+ storedMappings: map[string]route.ResID{},
+ queryDomain: "example.com",
+ expectedResId: "",
+ },
+ {
+ name: "Exact match returns stored resId",
+ storedMappings: map[string]route.ResID{"example.com": "res1"},
+ queryDomain: "example.com",
+ expectedResId: "res1",
+ },
+ {
+ name: "Wildcard pattern matches base domain",
+ storedMappings: map[string]route.ResID{"*.example.com": "res2"},
+ queryDomain: "example.com",
+ expectedResId: "res2",
+ },
+ {
+ name: "Wildcard pattern matches subdomain",
+ storedMappings: map[string]route.ResID{"*.example.com": "res3"},
+ queryDomain: "foo.example.com",
+ expectedResId: "res3",
+ },
+ {
+ name: "Wildcard pattern does not match different domain",
+ storedMappings: map[string]route.ResID{"*.example.com": "res4"},
+ queryDomain: "foo.notexample.com",
+ expectedResId: "",
+ },
+ {
+ name: "Non-wildcard pattern does not match subdomain",
+ storedMappings: map[string]route.ResID{"example.com": "res5"},
+ queryDomain: "foo.example.com",
+ expectedResId: "",
+ },
+ {
+ name: "Exact match over overlapping wildcard",
+ storedMappings: map[string]route.ResID{
+ "*.example.com": "resWildcard",
+ "foo.example.com": "resExact",
+ },
+ queryDomain: "foo.example.com",
+ expectedResId: "resExact",
+ },
+ {
+ name: "Overlapping wildcards: Select more specific wildcard",
+ storedMappings: map[string]route.ResID{
+ "*.example.com": "resA",
+ "*.sub.example.com": "resB",
+ },
+ queryDomain: "bar.sub.example.com",
+ expectedResId: "resB",
+ },
+ {
+ name: "Wildcard multi-level subdomain match",
+ storedMappings: map[string]route.ResID{
+ "*.example.com": "resMulti",
+ },
+ queryDomain: "a.b.example.com",
+ expectedResId: "resMulti",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ fwd := &DNSForwarder{}
+
+ var entries []*ForwarderEntry
+ for domainPattern, resId := range tc.storedMappings {
+ d, err := domain.FromString(domainPattern)
+ require.NoError(t, err)
+ entries = append(entries, &ForwarderEntry{
+ Domain: d,
+ ResID: resId,
+ })
+ }
+ fwd.UpdateDomains(entries)
+
+ got, _ := fwd.getMatchingEntries(tc.queryDomain)
+ assert.Equal(t, got, tc.expectedResId)
+ })
+ }
+}
diff --git a/client/internal/dnsfwd/manager.go b/client/internal/dnsfwd/manager.go
index a51ae7abb..e4a23450f 100644
--- a/client/internal/dnsfwd/manager.go
+++ b/client/internal/dnsfwd/manager.go
@@ -11,6 +11,8 @@ import (
nberrors "github.com/netbirdio/netbird/client/errors"
firewall "github.com/netbirdio/netbird/client/firewall/manager"
"github.com/netbirdio/netbird/client/internal/peer"
+ "github.com/netbirdio/netbird/management/domain"
+ "github.com/netbirdio/netbird/route"
)
const (
@@ -19,6 +21,13 @@ const (
dnsTTL = 60 //seconds
)
+// ForwarderEntry is a mapping from a domain to a resource ID and a hash of the parent domain list.
+type ForwarderEntry struct {
+ Domain domain.Domain
+ ResID route.ResID
+ Set firewall.Set
+}
+
type Manager struct {
firewall firewall.Manager
statusRecorder *peer.Status
@@ -34,7 +43,7 @@ func NewManager(fw firewall.Manager, statusRecorder *peer.Status) *Manager {
}
}
-func (m *Manager) Start(domains []string, resIds map[string]string) error {
+func (m *Manager) Start(fwdEntries []*ForwarderEntry) error {
log.Infof("starting DNS forwarder")
if m.dnsForwarder != nil {
return nil
@@ -44,9 +53,9 @@ func (m *Manager) Start(domains []string, resIds map[string]string) error {
return err
}
- m.dnsForwarder = NewDNSForwarder(fmt.Sprintf(":%d", ListenPort), dnsTTL, m.statusRecorder)
+ m.dnsForwarder = NewDNSForwarder(fmt.Sprintf(":%d", ListenPort), dnsTTL, m.firewall, m.statusRecorder)
go func() {
- if err := m.dnsForwarder.Listen(domains, resIds); err != nil {
+ if err := m.dnsForwarder.Listen(fwdEntries); err != nil {
// todo handle close error if it is exists
log.Errorf("failed to start DNS forwarder, err: %v", err)
}
@@ -55,12 +64,12 @@ func (m *Manager) Start(domains []string, resIds map[string]string) error {
return nil
}
-func (m *Manager) UpdateDomains(domains []string, resIds map[string]string) {
+func (m *Manager) UpdateDomains(entries []*ForwarderEntry) {
if m.dnsForwarder == nil {
return
}
- m.dnsForwarder.UpdateDomains(domains, resIds)
+ m.dnsForwarder.UpdateDomains(entries)
}
func (m *Manager) Stop(ctx context.Context) error {
@@ -81,34 +90,34 @@ func (m *Manager) Stop(ctx context.Context) error {
return nberrors.FormatErrorOrNil(mErr)
}
-func (h *Manager) allowDNSFirewall() error {
+func (m *Manager) allowDNSFirewall() error {
dport := &firewall.Port{
IsRange: false,
Values: []uint16{ListenPort},
}
- if h.firewall == nil {
+ if m.firewall == nil {
return nil
}
- dnsRules, err := h.firewall.AddPeerFiltering(nil, net.IP{0, 0, 0, 0}, firewall.ProtocolUDP, nil, dport, firewall.ActionAccept, "")
+ dnsRules, err := m.firewall.AddPeerFiltering(nil, net.IP{0, 0, 0, 0}, firewall.ProtocolUDP, nil, dport, firewall.ActionAccept, "")
if err != nil {
log.Errorf("failed to add allow DNS router rules, err: %v", err)
return err
}
- h.fwRules = dnsRules
+ m.fwRules = dnsRules
return nil
}
-func (h *Manager) dropDNSFirewall() error {
+func (m *Manager) dropDNSFirewall() error {
var mErr *multierror.Error
- for _, rule := range h.fwRules {
- if err := h.firewall.DeletePeerRule(rule); err != nil {
+ for _, rule := range m.fwRules {
+ if err := m.firewall.DeletePeerRule(rule); err != nil {
mErr = multierror.Append(mErr, fmt.Errorf("failed to delete DNS router rules, err: %v", err))
}
}
- h.fwRules = nil
+ m.fwRules = nil
return nberrors.FormatErrorOrNil(mErr)
}
diff --git a/client/internal/engine.go b/client/internal/engine.go
index 74a07927c..b16232883 100644
--- a/client/internal/engine.go
+++ b/client/internal/engine.go
@@ -527,7 +527,7 @@ func (e *Engine) blockLanAccess() {
if _, err := e.firewall.AddRouteFiltering(
nil,
[]netip.Prefix{v4},
- network,
+ firewallManager.Network{Prefix: network},
firewallManager.ProtocolALL,
nil,
nil,
@@ -960,21 +960,21 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
}
}
- // DNS forwarder
dnsRouteFeatureFlag := toDNSFeatureFlag(networkMap)
- dnsRouteDomains, resourceIds := toRouteDomains(e.config.WgPrivateKey.PublicKey().String(), networkMap.GetRoutes())
- e.updateDNSForwarder(dnsRouteFeatureFlag, dnsRouteDomains, resourceIds)
+ // apply routes first, route related actions might depend on routing being enabled
routes := toRoutes(networkMap.GetRoutes())
if err := e.routeManager.UpdateRoutes(serial, routes, dnsRouteFeatureFlag); err != nil {
log.Errorf("failed to update clientRoutes, err: %v", err)
}
- // acls might need routing to be enabled, so we apply after routes
if e.acl != nil {
- e.acl.ApplyFiltering(networkMap)
+ e.acl.ApplyFiltering(networkMap, dnsRouteFeatureFlag)
}
+ fwdEntries := toRouteDomains(e.config.WgPrivateKey.PublicKey().String(), routes)
+ e.updateDNSForwarder(dnsRouteFeatureFlag, fwdEntries)
+
// Ingress forward rules
if err := e.updateForwardRules(networkMap.GetForwardingRules()); err != nil {
log.Errorf("failed to update forward rules, err: %v", err)
@@ -1079,29 +1079,24 @@ func toRoutes(protoRoutes []*mgmProto.Route) []*route.Route {
return routes
}
-func toRouteDomains(myPubKey string, protoRoutes []*mgmProto.Route) ([]string, map[string]string) {
- if protoRoutes == nil {
- protoRoutes = []*mgmProto.Route{}
- }
-
- var dnsRoutes []string
- resIds := make(map[string]string)
- for _, protoRoute := range protoRoutes {
- if len(protoRoute.Domains) == 0 {
+func toRouteDomains(myPubKey string, routes []*route.Route) []*dnsfwd.ForwarderEntry {
+ var entries []*dnsfwd.ForwarderEntry
+ for _, route := range routes {
+ if len(route.Domains) == 0 {
continue
}
- if protoRoute.Peer == myPubKey {
- dnsRoutes = append(dnsRoutes, protoRoute.Domains...)
- // resource ID is the first part of the ID
- resId := strings.Split(protoRoute.ID, ":")
- for _, domain := range protoRoute.Domains {
- if len(resId) > 0 {
- resIds[domain] = resId[0]
- }
+ if route.Peer == myPubKey {
+ domainSet := firewallManager.NewDomainSet(route.Domains)
+ for _, d := range route.Domains {
+ entries = append(entries, &dnsfwd.ForwarderEntry{
+ Domain: d,
+ Set: domainSet,
+ ResID: route.GetResourceID(),
+ })
}
}
}
- return dnsRoutes, resIds
+ return entries
}
func toDNSConfig(protoDNSConfig *mgmProto.DNSConfig, network *net.IPNet) nbdns.Config {
@@ -1231,36 +1226,19 @@ func (e *Engine) createPeerConn(pubKey string, allowedIPs []netip.Prefix) (*peer
PreSharedKey: e.config.PreSharedKey,
}
- if e.config.RosenpassEnabled && !e.config.RosenpassPermissive {
- lk := []byte(e.config.WgPrivateKey.PublicKey().String())
- rk := []byte(wgConfig.RemoteKey)
- var keyInput []byte
- if string(lk) > string(rk) {
- //nolint:gocritic
- keyInput = append(lk[:16], rk[:16]...)
- } else {
- //nolint:gocritic
- keyInput = append(rk[:16], lk[:16]...)
- }
-
- key, err := wgtypes.NewKey(keyInput)
- if err != nil {
- return nil, err
- }
-
- wgConfig.PreSharedKey = &key
- }
-
// randomize connection timeout
timeout := time.Duration(rand.Intn(PeerConnectionTimeoutMax-PeerConnectionTimeoutMin)+PeerConnectionTimeoutMin) * time.Millisecond
config := peer.ConnConfig{
- Key: pubKey,
- LocalKey: e.config.WgPrivateKey.PublicKey().String(),
- Timeout: timeout,
- WgConfig: wgConfig,
- LocalWgPort: e.config.WgPort,
- RosenpassPubKey: e.getRosenpassPubKey(),
- RosenpassAddr: e.getRosenpassAddr(),
+ Key: pubKey,
+ LocalKey: e.config.WgPrivateKey.PublicKey().String(),
+ Timeout: timeout,
+ WgConfig: wgConfig,
+ LocalWgPort: e.config.WgPort,
+ RosenpassConfig: peer.RosenpassConfig{
+ PubKey: e.getRosenpassPubKey(),
+ Addr: e.getRosenpassAddr(),
+ PermissiveMode: e.config.RosenpassPermissive,
+ },
ICEConfig: icemaker.Config{
StunTurn: &e.stunTurn,
InterfaceBlackList: e.config.IFaceBlackList,
@@ -1768,7 +1746,10 @@ func (e *Engine) GetWgAddr() net.IP {
}
// updateDNSForwarder start or stop the DNS forwarder based on the domains and the feature flag
-func (e *Engine) updateDNSForwarder(enabled bool, domains []string, resIds map[string]string) {
+func (e *Engine) updateDNSForwarder(
+ enabled bool,
+ fwdEntries []*dnsfwd.ForwarderEntry,
+) {
if !enabled {
if e.dnsForwardMgr == nil {
return
@@ -1779,18 +1760,18 @@ func (e *Engine) updateDNSForwarder(enabled bool, domains []string, resIds map[s
return
}
- if len(domains) > 0 {
- log.Infof("enable domain router service for domains: %v", domains)
+ if len(fwdEntries) > 0 {
if e.dnsForwardMgr == nil {
e.dnsForwardMgr = dnsfwd.NewManager(e.firewall, e.statusRecorder)
- if err := e.dnsForwardMgr.Start(domains, resIds); err != nil {
+ if err := e.dnsForwardMgr.Start(fwdEntries); err != nil {
log.Errorf("failed to start DNS forward: %v", err)
e.dnsForwardMgr = nil
}
+
+ log.Infof("started domain router service with %d entries", len(fwdEntries))
} else {
- log.Infof("update domain router service for domains: %v", domains)
- e.dnsForwardMgr.UpdateDomains(domains, resIds)
+ e.dnsForwardMgr.UpdateDomains(fwdEntries)
}
} else if e.dnsForwardMgr != nil {
log.Infof("disable domain router service")
diff --git a/client/internal/peer/conn.go b/client/internal/peer/conn.go
index 85f94b53f..44e8997bc 100644
--- a/client/internal/peer/conn.go
+++ b/client/internal/peer/conn.go
@@ -60,6 +60,15 @@ type WgConfig struct {
PreSharedKey *wgtypes.Key
}
+type RosenpassConfig struct {
+ // RosenpassPubKey is this peer's Rosenpass public key
+ PubKey []byte
+ // RosenpassPubKey is this peer's RosenpassAddr server address (IP:port)
+ Addr string
+
+ PermissiveMode bool
+}
+
// ConnConfig is a peer Connection configuration
type ConnConfig struct {
// Key is a public key of a remote peer
@@ -73,10 +82,7 @@ type ConnConfig struct {
LocalWgPort int
- // RosenpassPubKey is this peer's Rosenpass public key
- RosenpassPubKey []byte
- // RosenpassPubKey is this peer's RosenpassAddr server address (IP:port)
- RosenpassAddr string
+ RosenpassConfig RosenpassConfig
// ICEConfig ICE protocol configuration
ICEConfig icemaker.Config
@@ -109,6 +115,8 @@ type Conn struct {
connIDICE nbnet.ConnectionID
beforeAddPeerHooks []nbnet.AddHookFunc
afterRemovePeerHooks []nbnet.RemoveHookFunc
+ // used to store the remote Rosenpass key for Relayed connection in case of connection update from ice
+ rosenpassRemoteKey []byte
wgProxyICE wgproxy.Proxy
wgProxyRelay wgproxy.Proxy
@@ -375,7 +383,7 @@ func (conn *Conn) onICEConnectionIsReady(priority ConnPriority, iceConnInfo ICEC
wgProxy.Work()
}
- if err = conn.configureWGEndpoint(ep); err != nil {
+ if err = conn.configureWGEndpoint(ep, iceConnInfo.RosenpassPubKey); err != nil {
conn.handleConfigurationFailure(err, wgProxy)
return
}
@@ -408,7 +416,7 @@ func (conn *Conn) onICEStateDisconnected() {
conn.dumpState.SwitchToRelay()
conn.wgProxyRelay.Work()
- if err := conn.configureWGEndpoint(conn.wgProxyRelay.EndpointAddr()); err != nil {
+ if err := conn.configureWGEndpoint(conn.wgProxyRelay.EndpointAddr(), conn.rosenpassRemoteKey); err != nil {
conn.log.Errorf("failed to switch to relay conn: %v", err)
}
@@ -478,7 +486,7 @@ func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) {
}
wgProxy.Work()
- if err := conn.configureWGEndpoint(wgProxy.EndpointAddr()); err != nil {
+ if err := conn.configureWGEndpoint(wgProxy.EndpointAddr(), rci.rosenpassPubKey); err != nil {
if err := wgProxy.CloseConn(); err != nil {
conn.log.Warnf("Failed to close relay connection: %v", err)
}
@@ -493,6 +501,7 @@ func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) {
}()
wgConfigWorkaround()
+ conn.rosenpassRemoteKey = rci.rosenpassPubKey
conn.currentConnPriority = connPriorityRelay
conn.statusRelay.Set(StatusConnected)
conn.setRelayedProxy(wgProxy)
@@ -556,13 +565,14 @@ func (conn *Conn) listenGuardEvent(ctx context.Context) {
}
}
-func (conn *Conn) configureWGEndpoint(addr *net.UDPAddr) error {
+func (conn *Conn) configureWGEndpoint(addr *net.UDPAddr, remoteRPKey []byte) error {
+ presharedKey := conn.presharedKey(remoteRPKey)
return conn.config.WgConfig.WgInterface.UpdatePeer(
conn.config.WgConfig.RemoteKey,
conn.config.WgConfig.AllowedIps,
defaultWgKeepAlive,
addr,
- conn.config.WgConfig.PreSharedKey,
+ presharedKey,
)
}
@@ -783,6 +793,44 @@ func (conn *Conn) AllowedIP() netip.Addr {
return conn.config.WgConfig.AllowedIps[0].Addr()
}
+func (conn *Conn) presharedKey(remoteRosenpassKey []byte) *wgtypes.Key {
+ if conn.config.RosenpassConfig.PubKey == nil {
+ return conn.config.WgConfig.PreSharedKey
+ }
+
+ if remoteRosenpassKey == nil && conn.config.RosenpassConfig.PermissiveMode {
+ return conn.config.WgConfig.PreSharedKey
+ }
+
+ determKey, err := conn.rosenpassDetermKey()
+ if err != nil {
+ conn.log.Errorf("failed to generate Rosenpass initial key: %v", err)
+ return conn.config.WgConfig.PreSharedKey
+ }
+
+ return determKey
+}
+
+// todo: move this logic into Rosenpass package
+func (conn *Conn) rosenpassDetermKey() (*wgtypes.Key, error) {
+ lk := []byte(conn.config.LocalKey)
+ rk := []byte(conn.config.Key) // remote key
+ var keyInput []byte
+ if string(lk) > string(rk) {
+ //nolint:gocritic
+ keyInput = append(lk[:16], rk[:16]...)
+ } else {
+ //nolint:gocritic
+ keyInput = append(rk[:16], lk[:16]...)
+ }
+
+ key, err := wgtypes.NewKey(keyInput)
+ if err != nil {
+ return nil, err
+ }
+ return &key, nil
+}
+
func isController(config ConnConfig) bool {
return config.LocalKey > config.Key
}
diff --git a/client/internal/peer/conn_test.go b/client/internal/peer/conn_test.go
index 505bedb7f..6d55cfff4 100644
--- a/client/internal/peer/conn_test.go
+++ b/client/internal/peer/conn_test.go
@@ -2,6 +2,7 @@ package peer
import (
"context"
+ "fmt"
"os"
"sync"
"testing"
@@ -161,3 +162,145 @@ func TestConn_Status(t *testing.T) {
})
}
}
+
+func TestConn_presharedKey(t *testing.T) {
+ conn1 := Conn{
+ config: ConnConfig{
+ Key: "LLHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
+ LocalKey: "RRHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
+ RosenpassConfig: RosenpassConfig{},
+ },
+ }
+ conn2 := Conn{
+ config: ConnConfig{
+ Key: "RRHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
+ LocalKey: "LLHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
+ RosenpassConfig: RosenpassConfig{},
+ },
+ }
+
+ tests := []struct {
+ conn1Permissive bool
+ conn1RosenpassEnabled bool
+ conn2Permissive bool
+ conn2RosenpassEnabled bool
+ conn1ExpectedInitialKey bool
+ conn2ExpectedInitialKey bool
+ }{
+ {
+ conn1Permissive: false,
+ conn1RosenpassEnabled: false,
+ conn2Permissive: false,
+ conn2RosenpassEnabled: false,
+ conn1ExpectedInitialKey: false,
+ conn2ExpectedInitialKey: false,
+ },
+ {
+ conn1Permissive: false,
+ conn1RosenpassEnabled: true,
+ conn2Permissive: false,
+ conn2RosenpassEnabled: true,
+ conn1ExpectedInitialKey: true,
+ conn2ExpectedInitialKey: true,
+ },
+ {
+ conn1Permissive: false,
+ conn1RosenpassEnabled: true,
+ conn2Permissive: false,
+ conn2RosenpassEnabled: false,
+ conn1ExpectedInitialKey: true,
+ conn2ExpectedInitialKey: false,
+ },
+ {
+ conn1Permissive: false,
+ conn1RosenpassEnabled: false,
+ conn2Permissive: false,
+ conn2RosenpassEnabled: true,
+ conn1ExpectedInitialKey: false,
+ conn2ExpectedInitialKey: true,
+ },
+ {
+ conn1Permissive: true,
+ conn1RosenpassEnabled: true,
+ conn2Permissive: false,
+ conn2RosenpassEnabled: false,
+ conn1ExpectedInitialKey: false,
+ conn2ExpectedInitialKey: false,
+ },
+ {
+ conn1Permissive: false,
+ conn1RosenpassEnabled: false,
+ conn2Permissive: true,
+ conn2RosenpassEnabled: true,
+ conn1ExpectedInitialKey: false,
+ conn2ExpectedInitialKey: false,
+ },
+ {
+ conn1Permissive: true,
+ conn1RosenpassEnabled: true,
+ conn2Permissive: true,
+ conn2RosenpassEnabled: true,
+ conn1ExpectedInitialKey: true,
+ conn2ExpectedInitialKey: true,
+ },
+ {
+ conn1Permissive: false,
+ conn1RosenpassEnabled: false,
+ conn2Permissive: false,
+ conn2RosenpassEnabled: true,
+ conn1ExpectedInitialKey: false,
+ conn2ExpectedInitialKey: true,
+ },
+ {
+ conn1Permissive: false,
+ conn1RosenpassEnabled: true,
+ conn2Permissive: true,
+ conn2RosenpassEnabled: true,
+ conn1ExpectedInitialKey: true,
+ conn2ExpectedInitialKey: true,
+ },
+ }
+
+ conn1.config.RosenpassConfig.PermissiveMode = true
+ for i, test := range tests {
+ tcase := i + 1
+ t.Run(fmt.Sprintf("Rosenpass test case %d", tcase), func(t *testing.T) {
+ conn1.config.RosenpassConfig = RosenpassConfig{}
+ conn2.config.RosenpassConfig = RosenpassConfig{}
+
+ if test.conn1RosenpassEnabled {
+ conn1.config.RosenpassConfig.PubKey = []byte("dummykey")
+ }
+ conn1.config.RosenpassConfig.PermissiveMode = test.conn1Permissive
+
+ if test.conn2RosenpassEnabled {
+ conn2.config.RosenpassConfig.PubKey = []byte("dummykey")
+ }
+ conn2.config.RosenpassConfig.PermissiveMode = test.conn2Permissive
+
+ conn1PresharedKey := conn1.presharedKey(conn2.config.RosenpassConfig.PubKey)
+ conn2PresharedKey := conn2.presharedKey(conn1.config.RosenpassConfig.PubKey)
+
+ if test.conn1ExpectedInitialKey {
+ if conn1PresharedKey == nil {
+ t.Errorf("Case %d: Expected conn1 to have a non-nil key, but got nil", tcase)
+ }
+ } else {
+ if conn1PresharedKey != nil {
+ t.Errorf("Case %d: Expected conn1 to have a nil key, but got %v", tcase, conn1PresharedKey)
+ }
+ }
+
+ // Assert conn2's key expectation
+ if test.conn2ExpectedInitialKey {
+ if conn2PresharedKey == nil {
+ t.Errorf("Case %d: Expected conn2 to have a non-nil key, but got nil", tcase)
+ }
+ } else {
+ if conn2PresharedKey != nil {
+ t.Errorf("Case %d: Expected conn2 to have a nil key, but got %v", tcase, conn2PresharedKey)
+ }
+ }
+ })
+ }
+}
diff --git a/client/internal/peer/handshaker.go b/client/internal/peer/handshaker.go
index d23727e96..224ea0262 100644
--- a/client/internal/peer/handshaker.go
+++ b/client/internal/peer/handshaker.go
@@ -154,8 +154,8 @@ func (h *Handshaker) sendOffer() error {
IceCredentials: IceCredentials{iceUFrag, icePwd},
WgListenPort: h.config.LocalWgPort,
Version: version.NetbirdVersion(),
- RosenpassPubKey: h.config.RosenpassPubKey,
- RosenpassAddr: h.config.RosenpassAddr,
+ RosenpassPubKey: h.config.RosenpassConfig.PubKey,
+ RosenpassAddr: h.config.RosenpassConfig.Addr,
}
addr, err := h.relay.RelayInstanceAddress()
@@ -174,8 +174,8 @@ func (h *Handshaker) sendAnswer() error {
IceCredentials: IceCredentials{uFrag, pwd},
WgListenPort: h.config.LocalWgPort,
Version: version.NetbirdVersion(),
- RosenpassPubKey: h.config.RosenpassPubKey,
- RosenpassAddr: h.config.RosenpassAddr,
+ RosenpassPubKey: h.config.RosenpassConfig.PubKey,
+ RosenpassAddr: h.config.RosenpassConfig.Addr,
}
addr, err := h.relay.RelayInstanceAddress()
if err == nil {
diff --git a/client/internal/peer/ice/agent.go b/client/internal/peer/ice/agent.go
index 2b66610e9..9b63cebf0 100644
--- a/client/internal/peer/ice/agent.go
+++ b/client/internal/peer/ice/agent.go
@@ -37,7 +37,8 @@ func NewAgent(iFaceDiscover stdnet.ExternalIFaceDiscover, config Config, candida
}
fac := logging.NewDefaultLoggerFactory()
- fac.Writer = log.StandardLogger().Writer()
+
+ //fac.Writer = log.StandardLogger().Writer()
agentConfig := &ice.AgentConfig{
MulticastDNSMode: ice.MulticastDNSModeDisabled,
diff --git a/client/internal/peer/route.go b/client/internal/peer/route.go
index c3567dcc9..e5e315e3c 100644
--- a/client/internal/peer/route.go
+++ b/client/internal/peer/route.go
@@ -6,12 +6,14 @@ import (
"sync"
log "github.com/sirupsen/logrus"
+
+ "github.com/netbirdio/netbird/route"
)
// routeEntry holds the route prefix and the corresponding resource ID.
type routeEntry struct {
prefix netip.Prefix
- resourceID string
+ resourceID route.ResID
}
type routeIDLookup struct {
@@ -24,7 +26,7 @@ type routeIDLookup struct {
resolvedIPs sync.Map
}
-func (r *routeIDLookup) AddLocalRouteID(resourceID string, route netip.Prefix) {
+func (r *routeIDLookup) AddLocalRouteID(resourceID route.ResID, route netip.Prefix) {
r.localLock.Lock()
defer r.localLock.Unlock()
@@ -56,7 +58,7 @@ func (r *routeIDLookup) RemoveLocalRouteID(route netip.Prefix) {
}
}
-func (r *routeIDLookup) AddRemoteRouteID(resourceID string, route netip.Prefix) {
+func (r *routeIDLookup) AddRemoteRouteID(resourceID route.ResID, route netip.Prefix) {
r.remoteLock.Lock()
defer r.remoteLock.Unlock()
@@ -87,7 +89,7 @@ func (r *routeIDLookup) RemoveRemoteRouteID(route netip.Prefix) {
}
}
-func (r *routeIDLookup) AddResolvedIP(resourceID string, route netip.Prefix) {
+func (r *routeIDLookup) AddResolvedIP(resourceID route.ResID, route netip.Prefix) {
r.resolvedIPs.Store(route.Addr(), resourceID)
}
@@ -97,19 +99,19 @@ func (r *routeIDLookup) RemoveResolvedIP(route netip.Prefix) {
// Lookup returns the resource ID for the given IP address
// and a bool indicating if the IP is an exit node.
-func (r *routeIDLookup) Lookup(ip netip.Addr) (string, bool) {
+func (r *routeIDLookup) Lookup(ip netip.Addr) (route.ResID, bool) {
if res, ok := r.resolvedIPs.Load(ip); ok {
- return res.(string), false
+ return res.(route.ResID), false
}
- var resourceID string
+ var resourceID route.ResID
var isExitNode bool
r.localLock.RLock()
for _, entry := range r.localRoutes {
if entry.prefix.Contains(ip) {
resourceID = entry.resourceID
- isExitNode = (entry.prefix.Bits() == 0)
+ isExitNode = entry.prefix.Bits() == 0
break
}
}
@@ -120,7 +122,7 @@ func (r *routeIDLookup) Lookup(ip netip.Addr) (string, bool) {
for _, entry := range r.remoteRoutes {
if entry.prefix.Contains(ip) {
resourceID = entry.resourceID
- isExitNode = (entry.prefix.Bits() == 0)
+ isExitNode = entry.prefix.Bits() == 0
break
}
}
diff --git a/client/internal/peer/status.go b/client/internal/peer/status.go
index 9b3fc744d..3eca6a8c9 100644
--- a/client/internal/peer/status.go
+++ b/client/internal/peer/status.go
@@ -21,6 +21,7 @@ import (
"github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/management/domain"
relayClient "github.com/netbirdio/netbird/relay/client"
+ "github.com/netbirdio/netbird/route"
)
const eventQueueSize = 10
@@ -313,7 +314,7 @@ func (d *Status) UpdatePeerState(receivedState State) error {
return nil
}
-func (d *Status) AddPeerStateRoute(peer string, route string, resourceId string) error {
+func (d *Status) AddPeerStateRoute(peer string, route string, resourceId route.ResID) error {
d.mux.Lock()
defer d.mux.Unlock()
@@ -581,7 +582,7 @@ func (d *Status) UpdateLocalPeerState(localPeerState LocalPeerState) {
}
// AddLocalPeerStateRoute adds a route to the local peer state
-func (d *Status) AddLocalPeerStateRoute(route, resourceId string) {
+func (d *Status) AddLocalPeerStateRoute(route string, resourceId route.ResID) {
d.mux.Lock()
defer d.mux.Unlock()
@@ -611,14 +612,11 @@ func (d *Status) RemoveLocalPeerStateRoute(route string) {
}
// AddResolvedIPLookupEntry adds a resolved IP lookup entry
-func (d *Status) AddResolvedIPLookupEntry(route, resourceId string) {
+func (d *Status) AddResolvedIPLookupEntry(prefix netip.Prefix, resourceId route.ResID) {
d.mux.Lock()
defer d.mux.Unlock()
- pref, err := netip.ParsePrefix(route)
- if err == nil {
- d.routeIDLookup.AddResolvedIP(resourceId, pref)
- }
+ d.routeIDLookup.AddResolvedIP(resourceId, prefix)
}
// RemoveResolvedIPLookupEntry removes a resolved IP lookup entry
@@ -723,7 +721,7 @@ func (d *Status) UpdateDNSStates(dnsStates []NSGroupState) {
d.nsGroupStates = dnsStates
}
-func (d *Status) UpdateResolvedDomainsStates(originalDomain domain.Domain, resolvedDomain domain.Domain, prefixes []netip.Prefix, resourceId string) {
+func (d *Status) UpdateResolvedDomainsStates(originalDomain domain.Domain, resolvedDomain domain.Domain, prefixes []netip.Prefix, resourceId route.ResID) {
d.mux.Lock()
defer d.mux.Unlock()
diff --git a/client/internal/routemanager/dnsinterceptor/handler.go b/client/internal/routemanager/dnsinterceptor/handler.go
index 68d81d968..6d51c88c0 100644
--- a/client/internal/routemanager/dnsinterceptor/handler.go
+++ b/client/internal/routemanager/dnsinterceptor/handler.go
@@ -234,7 +234,7 @@ func (d *DnsInterceptor) writeMsg(w dns.ResponseWriter, r *dns.Msg) error {
origPattern = writer.GetOrigPattern()
}
- resolvedDomain := domain.Domain(r.Question[0].Name)
+ resolvedDomain := domain.Domain(strings.ToLower(r.Question[0].Name))
// already punycode via RegisterHandler()
originalDomain := domain.Domain(origPattern)
@@ -328,6 +328,11 @@ func (d *DnsInterceptor) updateDomainPrefixes(resolvedDomain, originalDomain dom
// Update domain prefixes using resolved domain as key
if len(toAdd) > 0 || len(toRemove) > 0 {
+ if d.route.KeepRoute {
+ // replace stored prefixes with old + added
+ // nolint:gocritic
+ newPrefixes = append(oldPrefixes, toAdd...)
+ }
d.interceptedDomains[resolvedDomain] = newPrefixes
originalDomain = domain.Domain(strings.TrimSuffix(string(originalDomain), "."))
d.statusRecorder.UpdateResolvedDomainsStates(originalDomain, resolvedDomain, newPrefixes, d.route.GetResourceID())
@@ -338,7 +343,7 @@ func (d *DnsInterceptor) updateDomainPrefixes(resolvedDomain, originalDomain dom
originalDomain.SafeString(),
toAdd)
}
- if len(toRemove) > 0 {
+ if len(toRemove) > 0 && !d.route.KeepRoute {
log.Debugf("removed dynamic route(s) for domain=%s (pattern: domain=%s): %s",
resolvedDomain.SafeString(),
originalDomain.SafeString(),
diff --git a/client/internal/routemanager/manager.go b/client/internal/routemanager/manager.go
index ae0d1d220..078206ab9 100644
--- a/client/internal/routemanager/manager.go
+++ b/client/internal/routemanager/manager.go
@@ -259,8 +259,6 @@ func (m *DefaultManager) Stop(stateManager *statemanager.Manager) {
}
}
- m.ctx = nil
-
m.mux.Lock()
defer m.mux.Unlock()
m.clientRoutes = nil
@@ -292,7 +290,7 @@ func (m *DefaultManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Ro
return nil
}
- if err := m.serverRouter.updateRoutes(newServerRoutesMap); err != nil {
+ if err := m.serverRouter.updateRoutes(newServerRoutesMap, useNewDNSRoute); err != nil {
return fmt.Errorf("update routes: %w", err)
}
diff --git a/client/internal/routemanager/server_android.go b/client/internal/routemanager/server_android.go
index 48bb0380d..953210e9e 100644
--- a/client/internal/routemanager/server_android.go
+++ b/client/internal/routemanager/server_android.go
@@ -18,7 +18,7 @@ type serverRouter struct {
func (r serverRouter) cleanUp() {
}
-func (r serverRouter) updateRoutes(map[route.ID]*route.Route) error {
+func (r serverRouter) updateRoutes(map[route.ID]*route.Route, bool) error {
return nil
}
diff --git a/client/internal/routemanager/server_nonandroid.go b/client/internal/routemanager/server_nonandroid.go
index 18713ee65..131d4c170 100644
--- a/client/internal/routemanager/server_nonandroid.go
+++ b/client/internal/routemanager/server_nonandroid.go
@@ -35,7 +35,10 @@ func newServerRouter(ctx context.Context, wgInterface iface.WGIface, firewall fi
}, nil
}
-func (m *serverRouter) updateRoutes(routesMap map[route.ID]*route.Route) error {
+func (m *serverRouter) updateRoutes(routesMap map[route.ID]*route.Route, useNewDNSRoute bool) error {
+ m.mux.Lock()
+ defer m.mux.Unlock()
+
serverRoutesToRemove := make([]route.ID, 0)
for routeID := range m.routes {
@@ -73,7 +76,7 @@ func (m *serverRouter) updateRoutes(routesMap map[route.ID]*route.Route) error {
continue
}
- err := m.addToServerNetwork(newRoute)
+ err := m.addToServerNetwork(newRoute, useNewDNSRoute)
if err != nil {
log.Errorf("Unable to add route %s from server, got: %v", newRoute.ID, err)
continue
@@ -90,57 +93,30 @@ func (m *serverRouter) removeFromServerNetwork(route *route.Route) error {
return m.ctx.Err()
}
- m.mux.Lock()
- defer m.mux.Unlock()
-
- routerPair, err := routeToRouterPair(route)
- if err != nil {
- return fmt.Errorf("parse prefix: %w", err)
- }
-
- err = m.firewall.RemoveNatRule(routerPair)
- if err != nil {
+ routerPair := routeToRouterPair(route, false)
+ if err := m.firewall.RemoveNatRule(routerPair); err != nil {
return fmt.Errorf("remove routing rules: %w", err)
}
delete(m.routes, route.ID)
-
- routeStr := route.Network.String()
- if route.IsDynamic() {
- routeStr = route.Domains.SafeString()
- }
- m.statusRecorder.RemoveLocalPeerStateRoute(routeStr)
+ m.statusRecorder.RemoveLocalPeerStateRoute(route.NetString())
return nil
}
-func (m *serverRouter) addToServerNetwork(route *route.Route) error {
+func (m *serverRouter) addToServerNetwork(route *route.Route, useNewDNSRoute bool) error {
if m.ctx.Err() != nil {
log.Infof("Not adding to server network because context is done")
return m.ctx.Err()
}
- m.mux.Lock()
- defer m.mux.Unlock()
-
- routerPair, err := routeToRouterPair(route)
- if err != nil {
- return fmt.Errorf("parse prefix: %w", err)
- }
-
- err = m.firewall.AddNatRule(routerPair)
- if err != nil {
+ routerPair := routeToRouterPair(route, useNewDNSRoute)
+ if err := m.firewall.AddNatRule(routerPair); err != nil {
return fmt.Errorf("insert routing rules: %w", err)
}
m.routes[route.ID] = route
-
- routeStr := route.Network.String()
- if route.IsDynamic() {
- routeStr = route.Domains.SafeString()
- }
-
- m.statusRecorder.AddLocalPeerStateRoute(routeStr, route.GetResourceID())
+ m.statusRecorder.AddLocalPeerStateRoute(route.NetString(), route.GetResourceID())
return nil
}
@@ -148,31 +124,29 @@ func (m *serverRouter) addToServerNetwork(route *route.Route) error {
func (m *serverRouter) cleanUp() {
m.mux.Lock()
defer m.mux.Unlock()
- for _, r := range m.routes {
- routerPair, err := routeToRouterPair(r)
- if err != nil {
- log.Errorf("Failed to convert route to router pair: %v", err)
- continue
- }
- err = m.firewall.RemoveNatRule(routerPair)
- if err != nil {
+ for _, r := range m.routes {
+ routerPair := routeToRouterPair(r, false)
+ if err := m.firewall.RemoveNatRule(routerPair); err != nil {
log.Errorf("Failed to remove cleanup route: %v", err)
}
-
}
m.statusRecorder.CleanLocalPeerStateRoutes()
}
-func routeToRouterPair(route *route.Route) (firewall.RouterPair, error) {
- // TODO: add ipv6
+func routeToRouterPair(route *route.Route, useNewDNSRoute bool) firewall.RouterPair {
source := getDefaultPrefix(route.Network)
-
- destination := route.Network.Masked()
+ destination := firewall.Network{}
if route.IsDynamic() {
- // TODO: add ipv6 additionally
- destination = getDefaultPrefix(destination)
+ if useNewDNSRoute {
+ destination.Set = firewall.NewDomainSet(route.Domains)
+ } else {
+ // TODO: add ipv6 additionally
+ destination = getDefaultPrefix(destination.Prefix)
+ }
+ } else {
+ destination.Prefix = route.Network.Masked()
}
return firewall.RouterPair{
@@ -180,12 +154,16 @@ func routeToRouterPair(route *route.Route) (firewall.RouterPair, error) {
Source: source,
Destination: destination,
Masquerade: route.Masquerade,
- }, nil
+ }
}
-func getDefaultPrefix(prefix netip.Prefix) netip.Prefix {
+func getDefaultPrefix(prefix netip.Prefix) firewall.Network {
if prefix.Addr().Is6() {
- return netip.PrefixFrom(netip.IPv6Unspecified(), 0)
+ return firewall.Network{
+ Prefix: netip.PrefixFrom(netip.IPv6Unspecified(), 0),
+ }
+ }
+ return firewall.Network{
+ Prefix: netip.PrefixFrom(netip.IPv4Unspecified(), 0),
}
- return netip.PrefixFrom(netip.IPv4Unspecified(), 0)
}
diff --git a/client/internal/routemanager/systemops/systemops_bsd_test.go b/client/internal/routemanager/systemops/systemops_bsd_test.go
index 84b84483e..a83d7f1de 100644
--- a/client/internal/routemanager/systemops/systemops_bsd_test.go
+++ b/client/internal/routemanager/systemops/systemops_bsd_test.go
@@ -24,7 +24,6 @@ func init() {
testCases = append(testCases, []testCase{
{
name: "To more specific route without custom dialer via vpn",
- destination: "10.10.0.2:53",
expectedInterface: expectedVPNint,
dialer: &net.Dialer{},
expectedPacket: createPacketExpectation("100.64.0.1", 12345, "10.10.0.2", 53),
diff --git a/client/internal/routemanager/systemops/systemops_linux.go b/client/internal/routemanager/systemops/systemops_linux.go
index cf3c2f0aa..59b6346c6 100644
--- a/client/internal/routemanager/systemops/systemops_linux.go
+++ b/client/internal/routemanager/systemops/systemops_linux.go
@@ -45,7 +45,7 @@ var sysctlFailed bool
type ruleParams struct {
priority int
- fwmark int
+ fwmark uint32
tableID int
family int
invert bool
@@ -55,8 +55,8 @@ type ruleParams struct {
func getSetupRules() []ruleParams {
return []ruleParams{
- {100, -1, syscall.RT_TABLE_MAIN, netlink.FAMILY_V4, false, 0, "rule with suppress prefixlen v4"},
- {100, -1, syscall.RT_TABLE_MAIN, netlink.FAMILY_V6, false, 0, "rule with suppress prefixlen v6"},
+ {100, 0, syscall.RT_TABLE_MAIN, netlink.FAMILY_V4, false, 0, "rule with suppress prefixlen v4"},
+ {100, 0, syscall.RT_TABLE_MAIN, netlink.FAMILY_V6, false, 0, "rule with suppress prefixlen v6"},
{110, nbnet.ControlPlaneMark, NetbirdVPNTableID, netlink.FAMILY_V4, true, -1, "rule v4 netbird"},
{110, nbnet.ControlPlaneMark, NetbirdVPNTableID, netlink.FAMILY_V6, true, -1, "rule v6 netbird"},
}
diff --git a/client/internal/routemanager/systemops/systemops_linux_test.go b/client/internal/routemanager/systemops/systemops_linux_test.go
index 8f12740d0..f0d7472dc 100644
--- a/client/internal/routemanager/systemops/systemops_linux_test.go
+++ b/client/internal/routemanager/systemops/systemops_linux_test.go
@@ -27,14 +27,12 @@ func init() {
testCases = append(testCases, []testCase{
{
name: "To more specific route without custom dialer via physical interface",
- destination: "10.10.0.2:53",
expectedInterface: expectedInternalInt,
dialer: &net.Dialer{},
expectedPacket: createPacketExpectation("192.168.1.1", 12345, "10.10.0.2", 53),
},
{
name: "To more specific route (local) without custom dialer via physical interface",
- destination: "127.0.10.1:53",
expectedInterface: expectedLoopbackInt,
dialer: &net.Dialer{},
expectedPacket: createPacketExpectation("127.0.0.1", 12345, "127.0.10.1", 53),
@@ -134,6 +132,16 @@ func addDummyRoute(t *testing.T, dstCIDR string, gw net.IP, intf string) {
_, dstIPNet, err := net.ParseCIDR(dstCIDR)
require.NoError(t, err)
+ link, err := netlink.LinkByName(intf)
+ require.NoError(t, err)
+ linkIndex := link.Attrs().Index
+
+ route := &netlink.Route{
+ Dst: dstIPNet,
+ Gw: gw,
+ LinkIndex: linkIndex,
+ }
+
// Handle existing routes with metric 0
var originalNexthop net.IP
var originalLinkIndex int
@@ -145,32 +153,24 @@ func addDummyRoute(t *testing.T, dstCIDR string, gw net.IP, intf string) {
}
if originalNexthop != nil {
+ // remove original route
err = netlink.RouteDel(&netlink.Route{Dst: dstIPNet, Priority: 0})
- switch {
- case err != nil && !errors.Is(err, syscall.ESRCH):
- t.Logf("Failed to delete route: %v", err)
- case err == nil:
- t.Cleanup(func() {
- err := netlink.RouteAdd(&netlink.Route{Dst: dstIPNet, Gw: originalNexthop, LinkIndex: originalLinkIndex, Priority: 0})
- if err != nil && !errors.Is(err, syscall.EEXIST) {
- t.Fatalf("Failed to add route: %v", err)
- }
- })
- default:
- t.Logf("Failed to delete route: %v", err)
- }
+ assert.NoError(t, err)
+
+ // add new route
+ assert.NoError(t, netlink.RouteAdd(route))
+
+ t.Cleanup(func() {
+ // restore original route
+ assert.NoError(t, netlink.RouteDel(route))
+ err := netlink.RouteAdd(&netlink.Route{Dst: dstIPNet, Gw: originalNexthop, LinkIndex: originalLinkIndex, Priority: 0})
+ assert.NoError(t, err)
+ })
+
+ return
}
}
- link, err := netlink.LinkByName(intf)
- require.NoError(t, err)
- linkIndex := link.Attrs().Index
-
- route := &netlink.Route{
- Dst: dstIPNet,
- Gw: gw,
- LinkIndex: linkIndex,
- }
err = netlink.RouteDel(route)
if err != nil && !errors.Is(err, syscall.ESRCH) {
t.Logf("Failed to delete route: %v", err)
@@ -180,7 +180,6 @@ func addDummyRoute(t *testing.T, dstCIDR string, gw net.IP, intf string) {
if err != nil && !errors.Is(err, syscall.EEXIST) {
t.Fatalf("Failed to add route: %v", err)
}
- require.NoError(t, err)
}
func fetchOriginalGateway(family int) (net.IP, int, error) {
@@ -190,7 +189,11 @@ func fetchOriginalGateway(family int) (net.IP, int, error) {
}
for _, route := range routes {
- if route.Dst == nil && route.Priority == 0 {
+ ones := -1
+ if route.Dst != nil {
+ ones, _ = route.Dst.Mask.Size()
+ }
+ if route.Dst == nil || ones == 0 && route.Priority == 0 {
return route.Gw, route.LinkIndex, nil
}
}
diff --git a/client/internal/routemanager/systemops/systemops_unix_test.go b/client/internal/routemanager/systemops/systemops_unix_test.go
index d88c1ab6b..ad37f611f 100644
--- a/client/internal/routemanager/systemops/systemops_unix_test.go
+++ b/client/internal/routemanager/systemops/systemops_unix_test.go
@@ -31,7 +31,6 @@ type PacketExpectation struct {
type testCase struct {
name string
- destination string
expectedInterface string
dialer dialer
expectedPacket PacketExpectation
@@ -40,14 +39,12 @@ type testCase struct {
var testCases = []testCase{
{
name: "To external host without custom dialer via vpn",
- destination: "192.0.2.1:53",
expectedInterface: expectedVPNint,
dialer: &net.Dialer{},
expectedPacket: createPacketExpectation("100.64.0.1", 12345, "192.0.2.1", 53),
},
{
name: "To external host with custom dialer via physical interface",
- destination: "192.0.2.1:53",
expectedInterface: expectedExternalInt,
dialer: nbnet.NewDialer(),
expectedPacket: createPacketExpectation("192.168.0.1", 12345, "192.0.2.1", 53),
@@ -55,14 +52,12 @@ var testCases = []testCase{
{
name: "To duplicate internal route with custom dialer via physical interface",
- destination: "10.0.0.2:53",
expectedInterface: expectedInternalInt,
dialer: nbnet.NewDialer(),
expectedPacket: createPacketExpectation("192.168.1.1", 12345, "10.0.0.2", 53),
},
{
name: "To duplicate internal route without custom dialer via physical interface", // local route takes precedence
- destination: "10.0.0.2:53",
expectedInterface: expectedInternalInt,
dialer: &net.Dialer{},
expectedPacket: createPacketExpectation("192.168.1.1", 12345, "10.0.0.2", 53),
@@ -70,14 +65,12 @@ var testCases = []testCase{
{
name: "To unique vpn route with custom dialer via physical interface",
- destination: "172.16.0.2:53",
expectedInterface: expectedExternalInt,
dialer: nbnet.NewDialer(),
expectedPacket: createPacketExpectation("192.168.0.1", 12345, "172.16.0.2", 53),
},
{
name: "To unique vpn route without custom dialer via vpn",
- destination: "172.16.0.2:53",
expectedInterface: expectedVPNint,
dialer: &net.Dialer{},
expectedPacket: createPacketExpectation("100.64.0.1", 12345, "172.16.0.2", 53),
@@ -94,10 +87,11 @@ func TestRouting(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
setupTestEnv(t)
- filter := createBPFFilter(tc.destination)
+ dst := fmt.Sprintf("%s:%d", tc.expectedPacket.DstIP, tc.expectedPacket.DstPort)
+ filter := createBPFFilter(dst)
handle := startPacketCapture(t, tc.expectedInterface, filter)
- sendTestPacket(t, tc.destination, tc.expectedPacket.SrcPort, tc.dialer)
+ sendTestPacket(t, dst, tc.expectedPacket.SrcPort, tc.dialer)
packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
packet, err := packetSource.NextPacket()
diff --git a/client/internal/routeselector/routeselector.go b/client/internal/routeselector/routeselector.go
index 2874604fd..8ebdc63e5 100644
--- a/client/internal/routeselector/routeselector.go
+++ b/client/internal/routeselector/routeselector.go
@@ -10,20 +10,19 @@ import (
"golang.org/x/exp/maps"
"github.com/netbirdio/netbird/client/errors"
- route "github.com/netbirdio/netbird/route"
+ "github.com/netbirdio/netbird/route"
)
type RouteSelector struct {
- mu sync.RWMutex
- selectedRoutes map[route.NetID]struct{}
- selectAll bool
+ mu sync.RWMutex
+ deselectedRoutes map[route.NetID]struct{}
+ deselectAll bool
}
func NewRouteSelector() *RouteSelector {
return &RouteSelector{
- selectedRoutes: map[route.NetID]struct{}{},
- // default selects all routes
- selectAll: true,
+ deselectedRoutes: map[route.NetID]struct{}{},
+ deselectAll: false,
}
}
@@ -32,8 +31,11 @@ func (rs *RouteSelector) SelectRoutes(routes []route.NetID, appendRoute bool, al
rs.mu.Lock()
defer rs.mu.Unlock()
- if !appendRoute {
- rs.selectedRoutes = map[route.NetID]struct{}{}
+ if !appendRoute || rs.deselectAll {
+ maps.Clear(rs.deselectedRoutes)
+ for _, r := range allRoutes {
+ rs.deselectedRoutes[r] = struct{}{}
+ }
}
var err *multierror.Error
@@ -42,10 +44,10 @@ func (rs *RouteSelector) SelectRoutes(routes []route.NetID, appendRoute bool, al
err = multierror.Append(err, fmt.Errorf("route '%s' is not available", route))
continue
}
-
- rs.selectedRoutes[route] = struct{}{}
+ delete(rs.deselectedRoutes, route)
}
- rs.selectAll = false
+
+ rs.deselectAll = false
return errors.FormatErrorOrNil(err)
}
@@ -55,32 +57,26 @@ func (rs *RouteSelector) SelectAllRoutes() {
rs.mu.Lock()
defer rs.mu.Unlock()
- rs.selectAll = true
- rs.selectedRoutes = map[route.NetID]struct{}{}
+ rs.deselectAll = false
+ maps.Clear(rs.deselectedRoutes)
}
// DeselectRoutes removes specific routes from the selection.
-// If the selector is in "select all" mode, it will transition to "select specific" mode.
func (rs *RouteSelector) DeselectRoutes(routes []route.NetID, allRoutes []route.NetID) error {
rs.mu.Lock()
defer rs.mu.Unlock()
- if rs.selectAll {
- rs.selectAll = false
- rs.selectedRoutes = map[route.NetID]struct{}{}
- for _, route := range allRoutes {
- rs.selectedRoutes[route] = struct{}{}
- }
+ if rs.deselectAll {
+ return nil
}
var err *multierror.Error
-
for _, route := range routes {
if !slices.Contains(allRoutes, route) {
err = multierror.Append(err, fmt.Errorf("route '%s' is not available", route))
continue
}
- delete(rs.selectedRoutes, route)
+ rs.deselectedRoutes[route] = struct{}{}
}
return errors.FormatErrorOrNil(err)
@@ -91,8 +87,8 @@ func (rs *RouteSelector) DeselectAllRoutes() {
rs.mu.Lock()
defer rs.mu.Unlock()
- rs.selectAll = false
- rs.selectedRoutes = map[route.NetID]struct{}{}
+ rs.deselectAll = true
+ maps.Clear(rs.deselectedRoutes)
}
// IsSelected checks if a specific route is selected.
@@ -100,11 +96,12 @@ func (rs *RouteSelector) IsSelected(routeID route.NetID) bool {
rs.mu.RLock()
defer rs.mu.RUnlock()
- if rs.selectAll {
- return true
+ if rs.deselectAll {
+ return false
}
- _, selected := rs.selectedRoutes[routeID]
- return selected
+
+ _, deselected := rs.deselectedRoutes[routeID]
+ return !deselected
}
// FilterSelected removes unselected routes from the provided map.
@@ -112,13 +109,15 @@ func (rs *RouteSelector) FilterSelected(routes route.HAMap) route.HAMap {
rs.mu.RLock()
defer rs.mu.RUnlock()
- if rs.selectAll {
- return maps.Clone(routes)
+ if rs.deselectAll {
+ return route.HAMap{}
}
filtered := route.HAMap{}
for id, rt := range routes {
- if rs.IsSelected(id.NetID()) {
+ netID := id.NetID()
+ _, deselected := rs.deselectedRoutes[netID]
+ if !deselected {
filtered[id] = rt
}
}
@@ -131,11 +130,11 @@ func (rs *RouteSelector) MarshalJSON() ([]byte, error) {
defer rs.mu.RUnlock()
return json.Marshal(struct {
- SelectedRoutes map[route.NetID]struct{} `json:"selected_routes"`
- SelectAll bool `json:"select_all"`
+ DeselectedRoutes map[route.NetID]struct{} `json:"deselected_routes"`
+ DeselectAll bool `json:"deselect_all"`
}{
- SelectAll: rs.selectAll,
- SelectedRoutes: rs.selectedRoutes,
+ DeselectedRoutes: rs.deselectedRoutes,
+ DeselectAll: rs.deselectAll,
})
}
@@ -147,25 +146,25 @@ func (rs *RouteSelector) UnmarshalJSON(data []byte) error {
// Check for null or empty JSON
if len(data) == 0 || string(data) == "null" {
- rs.selectedRoutes = map[route.NetID]struct{}{}
- rs.selectAll = true
+ rs.deselectedRoutes = map[route.NetID]struct{}{}
+ rs.deselectAll = false
return nil
}
var temp struct {
- SelectedRoutes map[route.NetID]struct{} `json:"selected_routes"`
- SelectAll bool `json:"select_all"`
+ DeselectedRoutes map[route.NetID]struct{} `json:"deselected_routes"`
+ DeselectAll bool `json:"deselect_all"`
}
if err := json.Unmarshal(data, &temp); err != nil {
return err
}
- rs.selectedRoutes = temp.SelectedRoutes
- rs.selectAll = temp.SelectAll
+ rs.deselectedRoutes = temp.DeselectedRoutes
+ rs.deselectAll = temp.DeselectAll
- if rs.selectedRoutes == nil {
- rs.selectedRoutes = map[route.NetID]struct{}{}
+ if rs.deselectedRoutes == nil {
+ rs.deselectedRoutes = map[route.NetID]struct{}{}
}
return nil
diff --git a/client/internal/routeselector/routeselector_test.go b/client/internal/routeselector/routeselector_test.go
index b1671f254..cfa723246 100644
--- a/client/internal/routeselector/routeselector_test.go
+++ b/client/internal/routeselector/routeselector_test.go
@@ -66,12 +66,10 @@ func TestRouteSelector_SelectRoutes(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
rs := routeselector.NewRouteSelector()
- if tt.initialSelected != nil {
- err := rs.SelectRoutes(tt.initialSelected, false, allRoutes)
- require.NoError(t, err)
- }
+ err := rs.SelectRoutes(tt.initialSelected, false, allRoutes)
+ require.NoError(t, err)
- err := rs.SelectRoutes(tt.selectRoutes, tt.append, allRoutes)
+ err = rs.SelectRoutes(tt.selectRoutes, tt.append, allRoutes)
if tt.wantError {
assert.Error(t, err)
} else {
@@ -251,7 +249,8 @@ func TestRouteSelector_IsSelected(t *testing.T) {
assert.True(t, rs.IsSelected("route1"))
assert.True(t, rs.IsSelected("route2"))
assert.False(t, rs.IsSelected("route3"))
- assert.False(t, rs.IsSelected("route4"))
+ // Unknown route is selected by default
+ assert.True(t, rs.IsSelected("route4"))
}
func TestRouteSelector_FilterSelected(t *testing.T) {
@@ -297,8 +296,8 @@ func TestRouteSelector_NewRoutesBehavior(t *testing.T) {
initialState: func(rs *routeselector.RouteSelector) error {
return rs.SelectRoutes([]route.NetID{"route1", "route2"}, false, initialRoutes)
},
- // When specific routes were selected, new routes should remain unselected
- wantNewSelected: []route.NetID{"route1", "route2"},
+ // When specific routes were selected, new routes should be selected
+ wantNewSelected: []route.NetID{"route1", "route2", "route4", "route5"},
},
{
name: "New routes after deselect all",
@@ -315,16 +314,16 @@ func TestRouteSelector_NewRoutesBehavior(t *testing.T) {
rs.SelectAllRoutes()
return rs.DeselectRoutes([]route.NetID{"route1"}, initialRoutes)
},
- // After deselecting specific routes, new routes should remain unselected
- wantNewSelected: []route.NetID{"route2", "route3"},
+ // After deselecting specific routes, new routes should be selected
+ wantNewSelected: []route.NetID{"route2", "route3", "route4", "route5"},
},
{
name: "New routes after selecting with append",
initialState: func(rs *routeselector.RouteSelector) error {
return rs.SelectRoutes([]route.NetID{"route1"}, true, initialRoutes)
},
- // When routes were appended, new routes should remain unselected
- wantNewSelected: []route.NetID{"route1"},
+ // When routes were appended, new routes should be selected
+ wantNewSelected: []route.NetID{"route1", "route2", "route3", "route4", "route5"},
},
}
@@ -358,3 +357,283 @@ func TestRouteSelector_NewRoutesBehavior(t *testing.T) {
})
}
}
+
+func TestRouteSelector_MixedSelectionDeselection(t *testing.T) {
+ allRoutes := []route.NetID{"route1", "route2", "route3"}
+
+ tests := []struct {
+ name string
+ routesToSelect []route.NetID
+ selectAppend bool
+ routesToDeselect []route.NetID
+ selectFirst bool
+ wantSelectedFinal []route.NetID
+ }{
+ {
+ name: "1. Select A, then Deselect B",
+ routesToSelect: []route.NetID{"route1"},
+ selectAppend: false,
+ routesToDeselect: []route.NetID{"route2"},
+ selectFirst: true,
+ wantSelectedFinal: []route.NetID{"route1"},
+ },
+ {
+ name: "2. Select A, then Deselect A",
+ routesToSelect: []route.NetID{"route1"},
+ selectAppend: false,
+ routesToDeselect: []route.NetID{"route1"},
+ selectFirst: true,
+ wantSelectedFinal: []route.NetID{},
+ },
+ {
+ name: "3. Deselect A (from all), then Select B",
+ routesToSelect: []route.NetID{"route2"},
+ selectAppend: false,
+ routesToDeselect: []route.NetID{"route1"},
+ selectFirst: false,
+ wantSelectedFinal: []route.NetID{"route2"},
+ },
+ {
+ name: "4. Deselect A (from all), then Select A",
+ routesToSelect: []route.NetID{"route1"},
+ selectAppend: false,
+ routesToDeselect: []route.NetID{"route1"},
+ selectFirst: false,
+ wantSelectedFinal: []route.NetID{"route1"},
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ rs := routeselector.NewRouteSelector()
+
+ var err1, err2 error
+
+ if tt.selectFirst {
+ err1 = rs.SelectRoutes(tt.routesToSelect, tt.selectAppend, allRoutes)
+ require.NoError(t, err1)
+ err2 = rs.DeselectRoutes(tt.routesToDeselect, allRoutes)
+ require.NoError(t, err2)
+ } else {
+ err1 = rs.DeselectRoutes(tt.routesToDeselect, allRoutes)
+ require.NoError(t, err1)
+ err2 = rs.SelectRoutes(tt.routesToSelect, tt.selectAppend, allRoutes)
+ require.NoError(t, err2)
+ }
+
+ for _, r := range allRoutes {
+ assert.Equal(t, slices.Contains(tt.wantSelectedFinal, r), rs.IsSelected(r), "Route %s final state mismatch", r)
+ }
+ })
+ }
+}
+
+func TestRouteSelector_AfterDeselectAll(t *testing.T) {
+ allRoutes := []route.NetID{"route1", "route2", "route3"}
+
+ tests := []struct {
+ name string
+ initialAction func(rs *routeselector.RouteSelector) error
+ secondAction func(rs *routeselector.RouteSelector) error
+ wantSelected []route.NetID
+ wantError bool
+ }{
+ {
+ name: "Deselect all -> select specific routes",
+ initialAction: func(rs *routeselector.RouteSelector) error {
+ rs.DeselectAllRoutes()
+ return nil
+ },
+ secondAction: func(rs *routeselector.RouteSelector) error {
+ return rs.SelectRoutes([]route.NetID{"route1", "route2"}, false, allRoutes)
+ },
+ wantSelected: []route.NetID{"route1", "route2"},
+ },
+ {
+ name: "Deselect all -> select with append",
+ initialAction: func(rs *routeselector.RouteSelector) error {
+ rs.DeselectAllRoutes()
+ return nil
+ },
+ secondAction: func(rs *routeselector.RouteSelector) error {
+ return rs.SelectRoutes([]route.NetID{"route1"}, true, allRoutes)
+ },
+ wantSelected: []route.NetID{"route1"},
+ },
+ {
+ name: "Deselect all -> deselect specific",
+ initialAction: func(rs *routeselector.RouteSelector) error {
+ rs.DeselectAllRoutes()
+ return nil
+ },
+ secondAction: func(rs *routeselector.RouteSelector) error {
+ return rs.DeselectRoutes([]route.NetID{"route1"}, allRoutes)
+ },
+ wantSelected: []route.NetID{},
+ },
+ {
+ name: "Deselect all -> select all",
+ initialAction: func(rs *routeselector.RouteSelector) error {
+ rs.DeselectAllRoutes()
+ return nil
+ },
+ secondAction: func(rs *routeselector.RouteSelector) error {
+ rs.SelectAllRoutes()
+ return nil
+ },
+ wantSelected: []route.NetID{"route1", "route2", "route3"},
+ },
+ {
+ name: "Deselect all -> deselect non-existent route",
+ initialAction: func(rs *routeselector.RouteSelector) error {
+ rs.DeselectAllRoutes()
+ return nil
+ },
+ secondAction: func(rs *routeselector.RouteSelector) error {
+ return rs.DeselectRoutes([]route.NetID{"route4"}, allRoutes)
+ },
+ wantSelected: []route.NetID{},
+ wantError: false,
+ },
+ {
+ name: "Select specific -> deselect all -> select different",
+ initialAction: func(rs *routeselector.RouteSelector) error {
+ err := rs.SelectRoutes([]route.NetID{"route1"}, false, allRoutes)
+ if err != nil {
+ return err
+ }
+ rs.DeselectAllRoutes()
+ return nil
+ },
+ secondAction: func(rs *routeselector.RouteSelector) error {
+ return rs.SelectRoutes([]route.NetID{"route2", "route3"}, false, allRoutes)
+ },
+ wantSelected: []route.NetID{"route2", "route3"},
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ rs := routeselector.NewRouteSelector()
+
+ err := tt.initialAction(rs)
+ require.NoError(t, err)
+
+ err = tt.secondAction(rs)
+ if tt.wantError {
+ assert.Error(t, err)
+ } else {
+ assert.NoError(t, err)
+ }
+
+ for _, id := range allRoutes {
+ expected := slices.Contains(tt.wantSelected, id)
+ assert.Equal(t, expected, rs.IsSelected(id),
+ "Route %s selection state incorrect, expected %v", id, expected)
+ }
+
+ routes := route.HAMap{
+ "route1|10.0.0.0/8": {},
+ "route2|192.168.0.0/16": {},
+ "route3|172.16.0.0/12": {},
+ }
+
+ filtered := rs.FilterSelected(routes)
+ assert.Equal(t, len(tt.wantSelected), len(filtered),
+ "FilterSelected returned wrong number of routes")
+ })
+ }
+}
+
+func TestRouteSelector_ComplexScenarios(t *testing.T) {
+ allRoutes := []route.NetID{"route1", "route2", "route3", "route4"}
+
+ tests := []struct {
+ name string
+ actions []func(rs *routeselector.RouteSelector) error
+ wantSelected []route.NetID
+ }{
+ {
+ name: "Select all -> deselect specific -> select different with append",
+ actions: []func(rs *routeselector.RouteSelector) error{
+ func(rs *routeselector.RouteSelector) error {
+ rs.SelectAllRoutes()
+ return nil
+ },
+ func(rs *routeselector.RouteSelector) error {
+ return rs.DeselectRoutes([]route.NetID{"route1", "route2"}, allRoutes)
+ },
+ func(rs *routeselector.RouteSelector) error {
+ return rs.SelectRoutes([]route.NetID{"route1"}, true, allRoutes)
+ },
+ },
+ wantSelected: []route.NetID{"route1", "route3", "route4"},
+ },
+ {
+ name: "Deselect all -> select specific -> deselect one -> select different with append",
+ actions: []func(rs *routeselector.RouteSelector) error{
+ func(rs *routeselector.RouteSelector) error {
+ rs.DeselectAllRoutes()
+ return nil
+ },
+ func(rs *routeselector.RouteSelector) error {
+ return rs.SelectRoutes([]route.NetID{"route1", "route2"}, false, allRoutes)
+ },
+ func(rs *routeselector.RouteSelector) error {
+ return rs.DeselectRoutes([]route.NetID{"route2"}, allRoutes)
+ },
+ func(rs *routeselector.RouteSelector) error {
+ return rs.SelectRoutes([]route.NetID{"route3"}, true, allRoutes)
+ },
+ },
+ wantSelected: []route.NetID{"route1", "route3"},
+ },
+ {
+ name: "Select specific -> deselect specific -> select all -> deselect different",
+ actions: []func(rs *routeselector.RouteSelector) error{
+ func(rs *routeselector.RouteSelector) error {
+ return rs.SelectRoutes([]route.NetID{"route1", "route2"}, false, allRoutes)
+ },
+ func(rs *routeselector.RouteSelector) error {
+ return rs.DeselectRoutes([]route.NetID{"route2"}, allRoutes)
+ },
+ func(rs *routeselector.RouteSelector) error {
+ rs.SelectAllRoutes()
+ return nil
+ },
+ func(rs *routeselector.RouteSelector) error {
+ return rs.DeselectRoutes([]route.NetID{"route3", "route4"}, allRoutes)
+ },
+ },
+ wantSelected: []route.NetID{"route1", "route2"},
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ rs := routeselector.NewRouteSelector()
+
+ for i, action := range tt.actions {
+ err := action(rs)
+ require.NoError(t, err, "Action %d failed", i)
+ }
+
+ for _, id := range allRoutes {
+ expected := slices.Contains(tt.wantSelected, id)
+ assert.Equal(t, expected, rs.IsSelected(id),
+ "Route %s selection state incorrect", id)
+ }
+
+ routes := route.HAMap{
+ "route1|10.0.0.0/8": {},
+ "route2|192.168.0.0/16": {},
+ "route3|172.16.0.0/12": {},
+ "route4|10.10.0.0/16": {},
+ }
+
+ filtered := rs.FilterSelected(routes)
+ assert.Equal(t, len(tt.wantSelected), len(filtered),
+ "FilterSelected returned wrong number of routes")
+ })
+ }
+}
diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go
index d04d7a9c0..879fb8032 100644
--- a/client/proto/daemon.pb.go
+++ b/client/proto/daemon.pb.go
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.26.0
-// protoc v4.24.3
+// protoc v3.21.9
// source: daemon.proto
package proto
@@ -2277,6 +2277,7 @@ type DebugBundleRequest struct {
Anonymize bool `protobuf:"varint,1,opt,name=anonymize,proto3" json:"anonymize,omitempty"`
Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"`
SystemInfo bool `protobuf:"varint,3,opt,name=systemInfo,proto3" json:"systemInfo,omitempty"`
+ UploadURL string `protobuf:"bytes,4,opt,name=uploadURL,proto3" json:"uploadURL,omitempty"`
}
func (x *DebugBundleRequest) Reset() {
@@ -2332,12 +2333,21 @@ func (x *DebugBundleRequest) GetSystemInfo() bool {
return false
}
+func (x *DebugBundleRequest) GetUploadURL() string {
+ if x != nil {
+ return x.UploadURL
+ }
+ return ""
+}
+
type DebugBundleResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
- Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"`
+ Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"`
+ UploadedKey string `protobuf:"bytes,2,opt,name=uploadedKey,proto3" json:"uploadedKey,omitempty"`
+ UploadFailureReason string `protobuf:"bytes,3,opt,name=uploadFailureReason,proto3" json:"uploadFailureReason,omitempty"`
}
func (x *DebugBundleResponse) Reset() {
@@ -2379,6 +2389,20 @@ func (x *DebugBundleResponse) GetPath() string {
return ""
}
+func (x *DebugBundleResponse) GetUploadedKey() string {
+ if x != nil {
+ return x.UploadedKey
+ }
+ return ""
+}
+
+func (x *DebugBundleResponse) GetUploadFailureReason() string {
+ if x != nil {
+ return x.UploadFailureReason
+ }
+ return ""
+}
+
type GetLogLevelRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -3924,244 +3948,251 @@ var file_daemon_proto_rawDesc = []byte{
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x05, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x6f, 0x72,
0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x05, 0x72, 0x75, 0x6c,
- 0x65, 0x73, 0x22, 0x6a, 0x0a, 0x12, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c,
- 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x6e, 0x6f, 0x6e,
- 0x79, 0x6d, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x6e, 0x6f,
- 0x6e, 0x79, 0x6d, 0x69, 0x7a, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73,
- 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1e,
- 0x0a, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x03, 0x20, 0x01,
- 0x28, 0x08, 0x52, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x29,
- 0x0a, 0x13, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73,
- 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20,
- 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x22, 0x14, 0x0a, 0x12, 0x47, 0x65, 0x74,
- 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22,
- 0x3d, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65,
- 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18,
- 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c,
- 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x3c,
- 0x0a, 0x12, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71,
- 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20,
- 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67,
- 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x15, 0x0a, 0x13,
- 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f,
- 0x6e, 0x73, 0x65, 0x22, 0x1b, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04,
- 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65,
- 0x22, 0x13, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65,
- 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3b, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61,
- 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x06, 0x73,
- 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x64, 0x61,
- 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74,
- 0x65, 0x73, 0x22, 0x44, 0x0a, 0x11, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65,
- 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x65,
- 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x61,
- 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x02, 0x20,
- 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3b, 0x0a, 0x12, 0x43, 0x6c, 0x65, 0x61,
- 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25,
- 0x0a, 0x0e, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73,
- 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x53,
- 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x45, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53,
- 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73,
- 0x74, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
- 0x09, 0x73, 0x74, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c,
- 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3c, 0x0a, 0x13,
- 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f,
- 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, 0x73,
- 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x64, 0x65, 0x6c,
- 0x65, 0x74, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x3b, 0x0a, 0x1f, 0x53, 0x65,
- 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69,
- 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a,
- 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07,
- 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x22, 0x0a, 0x20, 0x53, 0x65, 0x74, 0x4e, 0x65,
- 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65,
- 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x76, 0x0a, 0x08, 0x54,
- 0x43, 0x50, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x79, 0x6e, 0x18, 0x01,
- 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x73, 0x79, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x63, 0x6b,
- 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x63, 0x6b, 0x12, 0x10, 0x0a, 0x03, 0x66,
- 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x66, 0x69, 0x6e, 0x12, 0x10, 0x0a,
- 0x03, 0x72, 0x73, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x72, 0x73, 0x74, 0x12,
- 0x10, 0x0a, 0x03, 0x70, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x70, 0x73,
- 0x68, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03,
- 0x75, 0x72, 0x67, 0x22, 0x80, 0x03, 0x0a, 0x12, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63,
- 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x6f,
- 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73,
- 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x73, 0x74, 0x69,
- 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
- 0x0d, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x70, 0x12, 0x1a,
- 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
- 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6f,
- 0x75, 0x72, 0x63, 0x65, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52,
- 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x64,
- 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18,
- 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69,
- 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74,
- 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63,
- 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x32, 0x0a, 0x09, 0x74, 0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67,
- 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e,
- 0x2e, 0x54, 0x43, 0x50, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x48, 0x00, 0x52, 0x08, 0x74, 0x63, 0x70,
- 0x46, 0x6c, 0x61, 0x67, 0x73, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x69, 0x63, 0x6d, 0x70,
- 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x01, 0x52, 0x08, 0x69,
- 0x63, 0x6d, 0x70, 0x54, 0x79, 0x70, 0x65, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x69, 0x63,
- 0x6d, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x02, 0x52,
- 0x08, 0x69, 0x63, 0x6d, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x88, 0x01, 0x01, 0x42, 0x0c, 0x0a, 0x0a,
- 0x5f, 0x74, 0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x69,
- 0x63, 0x6d, 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x69, 0x63, 0x6d,
- 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x22, 0x9f, 0x01, 0x0a, 0x0a, 0x54, 0x72, 0x61, 0x63, 0x65,
- 0x53, 0x74, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20,
- 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73,
- 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73,
- 0x61, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x03,
- 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x32, 0x0a,
- 0x12, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x65, 0x74, 0x61,
- 0x69, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x11, 0x66, 0x6f, 0x72,
- 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x88, 0x01,
- 0x01, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67,
- 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x22, 0x6e, 0x0a, 0x13, 0x54, 0x72, 0x61, 0x63,
- 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
- 0x2a, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32,
- 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x53, 0x74,
- 0x61, 0x67, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61, 0x67, 0x65, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x66,
- 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e,
- 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x44, 0x69, 0x73,
- 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x12, 0x0a, 0x10, 0x53, 0x75, 0x62, 0x73,
- 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x93, 0x04, 0x0a,
- 0x0b, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02,
- 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x38, 0x0a, 0x08,
- 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c,
- 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76,
- 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x52, 0x08, 0x73, 0x65,
- 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x38, 0x0a, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f,
- 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
- 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x61,
- 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x52, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79,
- 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28,
- 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x75, 0x73,
- 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52,
- 0x0b, 0x75, 0x73, 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x38, 0x0a, 0x09,
- 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32,
- 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
- 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d,
- 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x3d, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61,
- 0x74, 0x61, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
- 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x65,
- 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74,
- 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74,
- 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20,
- 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75,
- 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02,
- 0x38, 0x01, 0x22, 0x3a, 0x0a, 0x08, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x08,
- 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x57, 0x41, 0x52, 0x4e,
- 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x02,
- 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x52, 0x49, 0x54, 0x49, 0x43, 0x41, 0x4c, 0x10, 0x03, 0x22, 0x52,
- 0x0a, 0x08, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x0b, 0x0a, 0x07, 0x4e, 0x45,
- 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x44, 0x4e, 0x53, 0x10, 0x01,
- 0x12, 0x12, 0x0a, 0x0e, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x49,
- 0x4f, 0x4e, 0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49,
- 0x56, 0x49, 0x54, 0x59, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d,
- 0x10, 0x04, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52,
- 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x40, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65,
- 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x06, 0x65,
- 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61,
- 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74,
- 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2a, 0x62, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c,
- 0x65, 0x76, 0x65, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10,
- 0x00, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x41, 0x4e, 0x49, 0x43, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05,
- 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52,
- 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x04, 0x12, 0x08, 0x0a, 0x04,
- 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x05, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10,
- 0x06, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x07, 0x32, 0xb3, 0x0b, 0x0a,
- 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x36,
- 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e,
- 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e,
- 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70,
- 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53,
- 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
- 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75,
- 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69,
- 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
- 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02, 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d,
- 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x64,
- 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
- 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, 0x64,
- 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75,
- 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61,
- 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a,
- 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44,
- 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, 0x61, 0x65,
- 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
- 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
- 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66,
- 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d,
- 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70,
- 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65,
- 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
- 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75,
- 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73,
- 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
- 0x65, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x0e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74,
- 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53,
- 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71,
- 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65,
- 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70,
- 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x53, 0x0a, 0x10, 0x44, 0x65, 0x73, 0x65, 0x6c, 0x65,
- 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65,
- 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72,
- 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d,
- 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
- 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4a, 0x0a, 0x0f, 0x46,
- 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x14,
- 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x71,
- 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x6f,
- 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73,
- 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x62, 0x75, 0x67,
- 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
- 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
- 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75,
- 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
- 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c,
- 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67,
- 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64,
- 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65,
- 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x53,
- 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65,
- 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52,
- 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
- 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f,
- 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61,
- 0x74, 0x65, 0x73, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73,
- 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a,
- 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74,
- 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a,
- 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65,
- 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65,
- 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43,
+ 0x65, 0x73, 0x22, 0x88, 0x01, 0x0a, 0x12, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64,
+ 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x6e, 0x6f,
+ 0x6e, 0x79, 0x6d, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x6e,
+ 0x6f, 0x6e, 0x79, 0x6d, 0x69, 0x7a, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75,
+ 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12,
+ 0x1e, 0x0a, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x03, 0x20,
+ 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x12,
+ 0x1c, 0x0a, 0x09, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x55, 0x52, 0x4c, 0x18, 0x04, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x09, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x55, 0x52, 0x4c, 0x22, 0x7d, 0x0a,
+ 0x13, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70,
+ 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x20, 0x0a, 0x0b, 0x75, 0x70, 0x6c, 0x6f,
+ 0x61, 0x64, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x75,
+ 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x13, 0x75, 0x70,
+ 0x6c, 0x6f, 0x61, 0x64, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f,
+ 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x46,
+ 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x14, 0x0a, 0x12,
+ 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65,
+ 0x73, 0x74, 0x22, 0x3d, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65,
+ 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76,
+ 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
+ 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65,
+ 0x6c, 0x22, 0x3c, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c,
+ 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c,
+ 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
+ 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22,
+ 0x15, 0x0a, 0x13, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1b, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12,
+ 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e,
+ 0x61, 0x6d, 0x65, 0x22, 0x13, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65,
+ 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3b, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74,
+ 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25,
+ 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d,
+ 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, 0x73,
+ 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x44, 0x0a, 0x11, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74,
+ 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74,
+ 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09,
+ 0x73, 0x74, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c,
+ 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3b, 0x0a, 0x12, 0x43,
0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
- 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61,
- 0x74, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65,
- 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b,
- 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74,
- 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6f, 0x0a,
- 0x18, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65,
- 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x27, 0x2e, 0x64, 0x61, 0x65, 0x6d,
- 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70,
- 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
- 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e,
- 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74,
- 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48,
- 0x0a, 0x0b, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x1a, 0x2e,
- 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b,
- 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d,
- 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65,
- 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x44, 0x0a, 0x0f, 0x53, 0x75, 0x62, 0x73,
- 0x63, 0x72, 0x69, 0x62, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x64, 0x61,
- 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65,
- 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53,
- 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42,
- 0x0a, 0x09, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x64, 0x61,
- 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65,
- 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47,
- 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
- 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72,
- 0x6f, 0x74, 0x6f, 0x33,
+ 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61,
+ 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x63, 0x6c, 0x65, 0x61, 0x6e,
+ 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x45, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65,
+ 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d,
+ 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a,
+ 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22,
+ 0x3c, 0x0a, 0x13, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65,
+ 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d,
+ 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x3b, 0x0a,
+ 0x1f, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65,
+ 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+ 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
+ 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x22, 0x0a, 0x20, 0x53, 0x65,
+ 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69,
+ 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x76,
+ 0x0a, 0x08, 0x54, 0x43, 0x50, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x79,
+ 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x73, 0x79, 0x6e, 0x12, 0x10, 0x0a, 0x03,
+ 0x61, 0x63, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x63, 0x6b, 0x12, 0x10,
+ 0x0a, 0x03, 0x66, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x66, 0x69, 0x6e,
+ 0x12, 0x10, 0x0a, 0x03, 0x72, 0x73, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x72,
+ 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x70, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52,
+ 0x03, 0x70, 0x73, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28,
+ 0x08, 0x52, 0x03, 0x75, 0x72, 0x67, 0x22, 0x80, 0x03, 0x0a, 0x12, 0x54, 0x72, 0x61, 0x63, 0x65,
+ 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a,
+ 0x09, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x08, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65,
+ 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x70, 0x18, 0x02, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x0d, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49,
+ 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x03, 0x20,
+ 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x1f, 0x0a,
+ 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01,
+ 0x28, 0x0d, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x29,
+ 0x0a, 0x10, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x6f,
+ 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e,
+ 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72,
+ 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69,
+ 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x32, 0x0a, 0x09, 0x74, 0x63, 0x70, 0x5f, 0x66,
+ 0x6c, 0x61, 0x67, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65,
+ 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x43, 0x50, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x48, 0x00, 0x52, 0x08,
+ 0x74, 0x63, 0x70, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x69,
+ 0x63, 0x6d, 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x01,
+ 0x52, 0x08, 0x69, 0x63, 0x6d, 0x70, 0x54, 0x79, 0x70, 0x65, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a,
+ 0x09, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d,
+ 0x48, 0x02, 0x52, 0x08, 0x69, 0x63, 0x6d, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x88, 0x01, 0x01, 0x42,
+ 0x0c, 0x0a, 0x0a, 0x5f, 0x74, 0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x42, 0x0c, 0x0a,
+ 0x0a, 0x5f, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x42, 0x0c, 0x0a, 0x0a, 0x5f,
+ 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x22, 0x9f, 0x01, 0x0a, 0x0a, 0x54, 0x72,
+ 0x61, 0x63, 0x65, 0x53, 0x74, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65,
+ 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07,
+ 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d,
+ 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65,
+ 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64,
+ 0x12, 0x32, 0x0a, 0x12, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x64,
+ 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x11,
+ 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c,
+ 0x73, 0x88, 0x01, 0x01, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64,
+ 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x22, 0x6e, 0x0a, 0x13, 0x54,
+ 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
+ 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03,
+ 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63,
+ 0x65, 0x53, 0x74, 0x61, 0x67, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61, 0x67, 0x65, 0x73, 0x12, 0x2b,
+ 0x0a, 0x11, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6f, 0x73, 0x69, 0x74,
+ 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x66, 0x69, 0x6e, 0x61, 0x6c,
+ 0x44, 0x69, 0x73, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x12, 0x0a, 0x10, 0x53,
+ 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22,
+ 0x93, 0x04, 0x0a, 0x0b, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12,
+ 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12,
+ 0x38, 0x0a, 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28,
+ 0x0e, 0x32, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65,
+ 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x52,
+ 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x38, 0x0a, 0x08, 0x63, 0x61, 0x74,
+ 0x65, 0x67, 0x6f, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x64, 0x61,
+ 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74,
+ 0x2e, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x52, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67,
+ 0x6f, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x04,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x20, 0x0a,
+ 0x0b, 0x75, 0x73, 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x0b, 0x75, 0x73, 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12,
+ 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x06, 0x20, 0x01,
+ 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
+ 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09,
+ 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x3d, 0x0a, 0x08, 0x6d, 0x65, 0x74,
+ 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x64, 0x61,
+ 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74,
+ 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08,
+ 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61,
+ 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79,
+ 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76,
+ 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75,
+ 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x3a, 0x0a, 0x08, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74,
+ 0x79, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x57,
+ 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f,
+ 0x52, 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x52, 0x49, 0x54, 0x49, 0x43, 0x41, 0x4c, 0x10,
+ 0x03, 0x22, 0x52, 0x0a, 0x08, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x0b, 0x0a,
+ 0x07, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x44, 0x4e,
+ 0x53, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43,
+ 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x43, 0x4f, 0x4e, 0x4e, 0x45,
+ 0x43, 0x54, 0x49, 0x56, 0x49, 0x54, 0x59, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x59, 0x53,
+ 0x54, 0x45, 0x4d, 0x10, 0x04, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e,
+ 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x40, 0x0a, 0x11, 0x47, 0x65, 0x74,
+ 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b,
+ 0x0a, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13,
+ 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76,
+ 0x65, 0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2a, 0x62, 0x0a, 0x08, 0x4c,
+ 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f,
+ 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x41, 0x4e, 0x49, 0x43, 0x10, 0x01, 0x12,
+ 0x09, 0x0a, 0x05, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52,
+ 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x04, 0x12,
+ 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x05, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42,
+ 0x55, 0x47, 0x10, 0x06, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x07, 0x32,
+ 0xb3, 0x0b, 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
+ 0x65, 0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65,
+ 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+ 0x1a, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52,
+ 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69,
+ 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d,
+ 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52,
+ 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
+ 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70,
+ 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02, 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64,
+ 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
+ 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f,
+ 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12,
+ 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52,
+ 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
+ 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00,
+ 0x12, 0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
+ 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e,
+ 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f,
+ 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66,
+ 0x69, 0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43,
+ 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64,
+ 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52,
+ 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x4c, 0x69, 0x73,
+ 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d,
+ 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52,
+ 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
+ 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70,
+ 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x0e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74,
+ 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
+ 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73,
+ 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e,
+ 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52,
+ 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x53, 0x0a, 0x10, 0x44, 0x65, 0x73,
+ 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e,
+ 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74,
+ 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64,
+ 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77,
+ 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4a,
+ 0x0a, 0x0f, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65,
+ 0x73, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
+ 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e,
+ 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73,
+ 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65,
+ 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d,
+ 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65,
+ 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44,
+ 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
+ 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65,
+ 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74,
+ 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
+ 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c,
+ 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48,
+ 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e,
+ 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76,
+ 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d,
+ 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74,
+ 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
+ 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+ 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53,
+ 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12,
+ 0x45, 0x0a, 0x0a, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x2e,
+ 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74,
+ 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
+ 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70,
+ 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65,
+ 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44,
+ 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+ 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74,
+ 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00,
+ 0x12, 0x6f, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61,
+ 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x27, 0x2e, 0x64,
+ 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
+ 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65,
+ 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53,
+ 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73,
+ 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
+ 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74,
+ 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50,
+ 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64,
+ 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65,
+ 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x44, 0x0a, 0x0f, 0x53,
+ 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18,
+ 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62,
+ 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
+ 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x00, 0x30,
+ 0x01, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18,
+ 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74,
+ 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
+ 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
+ 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
+ 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto
index 49e577853..6c63a8f9b 100644
--- a/client/proto/daemon.proto
+++ b/client/proto/daemon.proto
@@ -336,10 +336,13 @@ message DebugBundleRequest {
bool anonymize = 1;
string status = 2;
bool systemInfo = 3;
+ string uploadURL = 4;
}
message DebugBundleResponse {
string path = 1;
+ string uploadedKey = 2;
+ string uploadFailureReason = 3;
}
enum LogLevel {
diff --git a/client/server/debug.go b/client/server/debug.go
index bdb1f7543..b42b1467a 100644
--- a/client/server/debug.go
+++ b/client/server/debug.go
@@ -3,558 +3,149 @@
package server
import (
- "archive/zip"
- "bufio"
- "bytes"
"context"
+ "crypto/sha256"
"encoding/json"
"errors"
"fmt"
"io"
- "io/fs"
- "net"
- "net/netip"
+ "net/http"
"os"
- "path/filepath"
- "runtime"
- "runtime/pprof"
- "sort"
- "strings"
- "time"
log "github.com/sirupsen/logrus"
- "google.golang.org/protobuf/encoding/protojson"
- "github.com/netbirdio/netbird/client/anonymize"
- "github.com/netbirdio/netbird/client/internal/peer"
- "github.com/netbirdio/netbird/client/internal/routemanager/systemops"
- "github.com/netbirdio/netbird/client/internal/statemanager"
+ "github.com/netbirdio/netbird/client/internal/debug"
"github.com/netbirdio/netbird/client/proto"
mgmProto "github.com/netbirdio/netbird/management/proto"
+ "github.com/netbirdio/netbird/upload-server/types"
)
-const readmeContent = `Netbird debug bundle
-This debug bundle contains the following files:
-
-status.txt: Anonymized status information of the NetBird client.
-client.log: Most recent, anonymized client log file of the NetBird client.
-netbird.err: Most recent, anonymized stderr log file of the NetBird client.
-netbird.out: Most recent, anonymized stdout log file of the NetBird client.
-routes.txt: Anonymized system routes, if --system-info flag was provided.
-interfaces.txt: Anonymized network interface information, if --system-info flag was provided.
-iptables.txt: Anonymized iptables rules with packet counters, if --system-info flag was provided.
-nftables.txt: Anonymized nftables rules with packet counters, if --system-info flag was provided.
-config.txt: Anonymized configuration information of the NetBird client.
-network_map.json: Anonymized network map containing peer configurations, routes, DNS settings, and firewall rules.
-state.json: Anonymized client state dump containing netbird states.
-mutex.prof: Mutex profiling information.
-goroutine.prof: Goroutine profiling information.
-block.prof: Block profiling information.
-
-
-Anonymization Process
-The files in this bundle have been anonymized to protect sensitive information. Here's how the anonymization was applied:
-
-IP Addresses
-
-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.
-
-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.
-
-Network Map
-The network_map.json file contains the following anonymized information:
-- Peer configurations (addresses, FQDNs, DNS settings)
-- Remote and offline peer information (allowed IPs, FQDNs)
-- Routes (network ranges, associated domains)
-- DNS configuration (nameservers, domains, custom zones)
-- Firewall rules (peer IPs, source/destination ranges)
-
-SSH keys in the network map are replaced with a placeholder value. All IP addresses and domains in the network map follow the same anonymization rules as described above.
-
-State File
-The state.json file contains anonymized internal state information of the NetBird client, including:
-- DNS settings and configuration
-- Firewall rules
-- Exclusion routes
-- Route selection
-- Other internal states that may be present
-
-The state file follows the same anonymization rules as other files:
-- IP addresses (both individual and CIDR ranges) are anonymized while preserving their structure
-- Domain names are consistently anonymized
-- Technical identifiers and non-sensitive data remain unchanged
-
-Mutex, Goroutines, and Block Profiling Files
-The goroutine, block, and mutex profiling files contains process information that might help the NetBird team diagnose performance issues. The information in these files don't contain personal data.
-You can check each using the following go command:
-
-go tool pprof -http=:8088 mutex.prof
-
-This will open a web browser tab with the profiling information.
-
-Routes
-For anonymized routes, the IP addresses are replaced as described above. The prefix length remains unchanged. Note that for prefixes, the anonymized IP might not be a network address, but the prefix length is still correct.
-
-Network Interfaces
-The interfaces.txt file contains information about network interfaces, including:
-- Interface name
-- Interface index
-- MTU (Maximum Transmission Unit)
-- Flags
-- IP addresses associated with each interface
-
-The IP addresses in the interfaces file are anonymized using the same process as described above. Interface names, indexes, MTUs, and flags are not anonymized.
-
-Configuration
-The config.txt file contains anonymized configuration information of the NetBird client. Sensitive information such as private keys and SSH keys are excluded. The following fields are anonymized:
-- ManagementURL
-- AdminURL
-- NATExternalIPs
-- CustomDNSAddress
-
-Other non-sensitive configuration options are included without anonymization.
-
-Firewall Rules (Linux only)
-The bundle includes two separate firewall rule files:
-
-iptables.txt:
-- Complete iptables ruleset with packet counters using 'iptables -v -n -L'
-- Includes all tables (filter, nat, mangle, raw, security)
-- Shows packet and byte counters for each rule
-- All IP addresses are anonymized
-- Chain names, table names, and other non-sensitive information remain unchanged
-
-nftables.txt:
-- Complete nftables ruleset obtained via 'nft -a list ruleset'
-- Includes rule handle numbers and packet counters
-- All tables, chains, and rules are included
-- Shows packet and byte counters for each rule
-- All IP addresses are anonymized
-- Chain names, table names, and other non-sensitive information remain unchanged
-`
-
-const (
- clientLogFile = "client.log"
- errorLogFile = "netbird.err"
- stdoutLogFile = "netbird.out"
-
- darwinErrorLogPath = "/var/log/netbird.out.log"
- darwinStdoutLogPath = "/var/log/netbird.err.log"
-)
+const maxBundleUploadSize = 50 * 1024 * 1024
// DebugBundle creates a debug bundle and returns the location.
func (s *Server) DebugBundle(_ context.Context, req *proto.DebugBundleRequest) (resp *proto.DebugBundleResponse, err error) {
s.mutex.Lock()
defer s.mutex.Unlock()
- bundlePath, err := os.CreateTemp("", "netbird.debug.*.zip")
- if err != nil {
- return nil, fmt.Errorf("create zip file: %w", err)
- }
- defer func() {
- if closeErr := bundlePath.Close(); closeErr != nil && err == nil {
- err = fmt.Errorf("close zip file: %w", closeErr)
- }
-
- if err != nil {
- if removeErr := os.Remove(bundlePath.Name()); removeErr != nil {
- log.Errorf("Failed to remove zip file: %v", removeErr)
- }
- }
- }()
-
- if err := s.createArchive(bundlePath, req); err != nil {
- return nil, err
- }
-
- return &proto.DebugBundleResponse{Path: bundlePath.Name()}, nil
-}
-
-func (s *Server) createArchive(bundlePath *os.File, req *proto.DebugBundleRequest) error {
- archive := zip.NewWriter(bundlePath)
- if err := s.addReadme(req, archive); err != nil {
- return fmt.Errorf("add readme: %w", err)
- }
-
- if err := s.addStatus(req, archive); err != nil {
- return fmt.Errorf("add status: %w", err)
- }
-
- anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
- status := s.statusRecorder.GetFullStatus()
- seedFromStatus(anonymizer, &status)
-
- if err := s.addConfig(req, anonymizer, archive); err != nil {
- log.Errorf("Failed to add config to debug bundle: %v", err)
- }
-
- if req.GetSystemInfo() {
- s.addSystemInfo(req, anonymizer, archive)
- }
-
- if err := s.addProf(req, anonymizer, archive); err != nil {
- log.Errorf("Failed to add goroutines rules to debug bundle: %v", err)
- }
-
- if err := s.addNetworkMap(req, anonymizer, archive); err != nil {
- return fmt.Errorf("add network map: %w", err)
- }
-
- if err := s.addStateFile(req, anonymizer, archive); err != nil {
- log.Errorf("Failed to add state file to debug bundle: %v", err)
- }
-
- if err := s.addCorruptedStateFiles(archive); err != nil {
- log.Errorf("Failed to add corrupted state files to debug bundle: %v", err)
- }
-
- if s.logFile != "console" {
- if err := s.addLogfile(req, anonymizer, archive); err != nil {
- return fmt.Errorf("add log file: %w", err)
- }
- }
-
- if err := archive.Close(); err != nil {
- return fmt.Errorf("close archive writer: %w", err)
- }
- return nil
-}
-
-func (s *Server) addSystemInfo(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) {
- if err := s.addRoutes(req, anonymizer, archive); err != nil {
- log.Errorf("Failed to add routes to debug bundle: %v", err)
- }
-
- if err := s.addInterfaces(req, anonymizer, archive); err != nil {
- log.Errorf("Failed to add interfaces to debug bundle: %v", err)
- }
-
- if err := s.addFirewallRules(req, anonymizer, archive); err != nil {
- log.Errorf("Failed to add firewall rules to debug bundle: %v", err)
- }
-}
-
-func (s *Server) addReadme(req *proto.DebugBundleRequest, archive *zip.Writer) error {
- if req.GetAnonymize() {
- readmeReader := strings.NewReader(readmeContent)
- if err := addFileToZip(archive, readmeReader, "README.txt"); err != nil {
- return fmt.Errorf("add README file to zip: %w", err)
- }
- }
- return nil
-}
-
-func (s *Server) addStatus(req *proto.DebugBundleRequest, archive *zip.Writer) error {
- if status := req.GetStatus(); status != "" {
- statusReader := strings.NewReader(status)
- if err := addFileToZip(archive, statusReader, "status.txt"); err != nil {
- return fmt.Errorf("add status file to zip: %w", err)
- }
- }
- return nil
-}
-
-func (s *Server) addConfig(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) error {
- var configContent strings.Builder
- s.addCommonConfigFields(&configContent)
-
- if req.GetAnonymize() {
- if s.config.ManagementURL != nil {
- configContent.WriteString(fmt.Sprintf("ManagementURL: %s\n", anonymizer.AnonymizeURI(s.config.ManagementURL.String())))
- }
- if s.config.AdminURL != nil {
- configContent.WriteString(fmt.Sprintf("AdminURL: %s\n", anonymizer.AnonymizeURI(s.config.AdminURL.String())))
- }
- configContent.WriteString(fmt.Sprintf("NATExternalIPs: %v\n", anonymizeNATExternalIPs(s.config.NATExternalIPs, anonymizer)))
- if s.config.CustomDNSAddress != "" {
- configContent.WriteString(fmt.Sprintf("CustomDNSAddress: %s\n", anonymizer.AnonymizeString(s.config.CustomDNSAddress)))
- }
- } else {
- if s.config.ManagementURL != nil {
- configContent.WriteString(fmt.Sprintf("ManagementURL: %s\n", s.config.ManagementURL.String()))
- }
- if s.config.AdminURL != nil {
- configContent.WriteString(fmt.Sprintf("AdminURL: %s\n", s.config.AdminURL.String()))
- }
- configContent.WriteString(fmt.Sprintf("NATExternalIPs: %v\n", s.config.NATExternalIPs))
- if s.config.CustomDNSAddress != "" {
- configContent.WriteString(fmt.Sprintf("CustomDNSAddress: %s\n", s.config.CustomDNSAddress))
- }
- }
-
- // Add config content to zip file
- configReader := strings.NewReader(configContent.String())
- if err := addFileToZip(archive, configReader, "config.txt"); err != nil {
- return fmt.Errorf("add config file to zip: %w", err)
- }
-
- return nil
-}
-
-func (s *Server) addCommonConfigFields(configContent *strings.Builder) {
- configContent.WriteString("NetBird Client Configuration:\n\n")
-
- // Add non-sensitive fields
- configContent.WriteString(fmt.Sprintf("WgIface: %s\n", s.config.WgIface))
- configContent.WriteString(fmt.Sprintf("WgPort: %d\n", s.config.WgPort))
- if s.config.NetworkMonitor != nil {
- configContent.WriteString(fmt.Sprintf("NetworkMonitor: %v\n", *s.config.NetworkMonitor))
- }
- configContent.WriteString(fmt.Sprintf("IFaceBlackList: %v\n", s.config.IFaceBlackList))
- configContent.WriteString(fmt.Sprintf("DisableIPv6Discovery: %v\n", s.config.DisableIPv6Discovery))
- configContent.WriteString(fmt.Sprintf("RosenpassEnabled: %v\n", s.config.RosenpassEnabled))
- configContent.WriteString(fmt.Sprintf("RosenpassPermissive: %v\n", s.config.RosenpassPermissive))
- if s.config.ServerSSHAllowed != nil {
- configContent.WriteString(fmt.Sprintf("ServerSSHAllowed: %v\n", *s.config.ServerSSHAllowed))
- }
- configContent.WriteString(fmt.Sprintf("DisableAutoConnect: %v\n", s.config.DisableAutoConnect))
- configContent.WriteString(fmt.Sprintf("DNSRouteInterval: %s\n", s.config.DNSRouteInterval))
-
- configContent.WriteString(fmt.Sprintf("DisableClientRoutes: %v\n", s.config.DisableClientRoutes))
- configContent.WriteString(fmt.Sprintf("DisableServerRoutes: %v\n", s.config.DisableServerRoutes))
- configContent.WriteString(fmt.Sprintf("DisableDNS: %v\n", s.config.DisableDNS))
- configContent.WriteString(fmt.Sprintf("DisableFirewall: %v\n", s.config.DisableFirewall))
-
- configContent.WriteString(fmt.Sprintf("BlockLANAccess: %v\n", s.config.BlockLANAccess))
-}
-
-func (s *Server) addProf(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) error {
- runtime.SetBlockProfileRate(1)
- _ = runtime.SetMutexProfileFraction(1)
- defer runtime.SetBlockProfileRate(0)
- defer runtime.SetMutexProfileFraction(0)
-
- time.Sleep(5 * time.Second)
-
- for _, profile := range []string{"goroutine", "block", "mutex"} {
- var buff []byte
- myBuff := bytes.NewBuffer(buff)
- err := pprof.Lookup(profile).WriteTo(myBuff, 0)
- if err != nil {
- return fmt.Errorf("write %s profile: %w", profile, err)
- }
-
- if err := addFileToZip(archive, myBuff, profile+".prof"); err != nil {
- return fmt.Errorf("add %s file to zip: %w", profile, err)
- }
- }
- return nil
-}
-
-func (s *Server) addRoutes(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) error {
- routes, err := systemops.GetRoutesFromTable()
- if err != nil {
- return fmt.Errorf("get routes: %w", err)
- }
-
- // TODO: get routes including nexthop
- routesContent := formatRoutes(routes, req.GetAnonymize(), anonymizer)
- routesReader := strings.NewReader(routesContent)
- if err := addFileToZip(archive, routesReader, "routes.txt"); err != nil {
- return fmt.Errorf("add routes file to zip: %w", err)
- }
- return nil
-}
-
-func (s *Server) addInterfaces(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) error {
- interfaces, err := net.Interfaces()
- if err != nil {
- return fmt.Errorf("get interfaces: %w", err)
- }
-
- interfacesContent := formatInterfaces(interfaces, req.GetAnonymize(), anonymizer)
- interfacesReader := strings.NewReader(interfacesContent)
- if err := addFileToZip(archive, interfacesReader, "interfaces.txt"); err != nil {
- return fmt.Errorf("add interfaces file to zip: %w", err)
- }
-
- return nil
-}
-
-func (s *Server) addNetworkMap(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) error {
networkMap, err := s.getLatestNetworkMap()
if err != nil {
- // Skip if network map is not available, but log it
- log.Debugf("skipping empty network map in debug bundle: %v", err)
- return nil
+ log.Warnf("failed to get latest network map: %v", err)
}
+ bundleGenerator := debug.NewBundleGenerator(
+ debug.GeneratorDependencies{
+ InternalConfig: s.config,
+ StatusRecorder: s.statusRecorder,
+ NetworkMap: networkMap,
+ LogFile: s.logFile,
+ },
+ debug.BundleConfig{
+ Anonymize: req.GetAnonymize(),
+ ClientStatus: req.GetStatus(),
+ IncludeSystemInfo: req.GetSystemInfo(),
+ },
+ )
- if req.GetAnonymize() {
- if err := anonymizeNetworkMap(networkMap, anonymizer); err != nil {
- return fmt.Errorf("anonymize network map: %w", err)
- }
- }
-
- options := protojson.MarshalOptions{
- EmitUnpopulated: true,
- UseProtoNames: true,
- Indent: " ",
- AllowPartial: true,
- }
-
- jsonBytes, err := options.Marshal(networkMap)
+ path, err := bundleGenerator.Generate()
if err != nil {
- return fmt.Errorf("generate json: %w", err)
+ return nil, fmt.Errorf("generate debug bundle: %w", err)
}
- if err := addFileToZip(archive, bytes.NewReader(jsonBytes), "network_map.json"); err != nil {
- return fmt.Errorf("add network map to zip: %w", err)
+ if req.GetUploadURL() == "" {
+
+ return &proto.DebugBundleResponse{Path: path}, nil
+ }
+ key, err := uploadDebugBundle(context.Background(), req.GetUploadURL(), s.config.ManagementURL.String(), path)
+ if err != nil {
+ return &proto.DebugBundleResponse{Path: path, UploadFailureReason: err.Error()}, nil
}
+ return &proto.DebugBundleResponse{Path: path, UploadedKey: key}, nil
+}
+
+func uploadDebugBundle(ctx context.Context, url, managementURL, filePath string) (key string, err error) {
+ response, err := getUploadURL(ctx, url, managementURL)
+ if err != nil {
+ return "", err
+ }
+
+ err = upload(ctx, filePath, response)
+ if err != nil {
+ return "", err
+ }
+ return response.Key, nil
+}
+
+func upload(ctx context.Context, filePath string, response *types.GetURLResponse) error {
+ fileData, err := os.Open(filePath)
+ if err != nil {
+ return fmt.Errorf("open file: %w", err)
+ }
+
+ defer fileData.Close()
+
+ stat, err := fileData.Stat()
+ if err != nil {
+ return fmt.Errorf("stat file: %w", err)
+ }
+
+ if stat.Size() > maxBundleUploadSize {
+ return fmt.Errorf("file size exceeds maximum limit of %d bytes", maxBundleUploadSize)
+ }
+
+ req, err := http.NewRequestWithContext(ctx, "PUT", response.URL, fileData)
+ if err != nil {
+ return fmt.Errorf("create PUT request: %w", err)
+ }
+
+ req.ContentLength = stat.Size()
+ req.Header.Set("Content-Type", "application/octet-stream")
+
+ putResp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return fmt.Errorf("upload failed: %v", err)
+ }
+ defer putResp.Body.Close()
+
+ if putResp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(putResp.Body)
+ return fmt.Errorf("upload status %d: %s", putResp.StatusCode, string(body))
+ }
return nil
}
-func (s *Server) addStateFile(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) error {
- path := statemanager.GetDefaultStatePath()
- if path == "" {
- return nil
- }
-
- data, err := os.ReadFile(path)
+func getUploadURL(ctx context.Context, url string, managementURL string) (*types.GetURLResponse, error) {
+ id := getURLHash(managementURL)
+ getReq, err := http.NewRequestWithContext(ctx, "GET", url+"?id="+id, nil)
if err != nil {
- if errors.Is(err, fs.ErrNotExist) {
- return nil
- }
- return fmt.Errorf("read state file: %w", err)
+ return nil, fmt.Errorf("create GET request: %w", err)
}
- if req.GetAnonymize() {
- var rawStates map[string]json.RawMessage
- if err := json.Unmarshal(data, &rawStates); err != nil {
- return fmt.Errorf("unmarshal states: %w", err)
- }
+ getReq.Header.Set(types.ClientHeader, types.ClientHeaderValue)
- if err := anonymizeStateFile(&rawStates, anonymizer); err != nil {
- return fmt.Errorf("anonymize state file: %w", err)
- }
+ resp, err := http.DefaultClient.Do(getReq)
+ if err != nil {
+ return nil, fmt.Errorf("get presigned URL: %w", err)
+ }
+ defer resp.Body.Close()
- bs, err := json.MarshalIndent(rawStates, "", " ")
- if err != nil {
- return fmt.Errorf("marshal states: %w", err)
- }
- data = bs
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return nil, fmt.Errorf("get presigned URL status %d: %s", resp.StatusCode, string(body))
}
- if err := addFileToZip(archive, bytes.NewReader(data), "state.json"); err != nil {
- return fmt.Errorf("add state file to zip: %w", err)
+ urlBytes, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("read response body: %w", err)
}
-
- return nil
+ var response types.GetURLResponse
+ if err := json.Unmarshal(urlBytes, &response); err != nil {
+ return nil, fmt.Errorf("unmarshal response: %w", err)
+ }
+ return &response, nil
}
-func (s *Server) addCorruptedStateFiles(archive *zip.Writer) error {
- pattern := statemanager.GetDefaultStatePath()
- if pattern == "" {
- return nil
- }
- pattern += "*.corrupted.*"
- matches, err := filepath.Glob(pattern)
- if err != nil {
- return fmt.Errorf("find corrupted state files: %w", err)
- }
-
- for _, match := range matches {
- data, err := os.ReadFile(match)
- if err != nil {
- log.Warnf("Failed to read corrupted state file %s: %v", match, err)
- continue
- }
-
- fileName := filepath.Base(match)
- if err := addFileToZip(archive, bytes.NewReader(data), "corrupted_states/"+fileName); err != nil {
- log.Warnf("Failed to add corrupted state file %s to zip: %v", fileName, err)
- continue
- }
-
- log.Debugf("Added corrupted state file to debug bundle: %s", fileName)
- }
-
- return nil
-}
-
-func (s *Server) addLogfile(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) error {
- logDir := filepath.Dir(s.logFile)
-
- if err := s.addSingleLogfile(s.logFile, clientLogFile, req, anonymizer, archive); err != nil {
- return fmt.Errorf("add client log file to zip: %w", err)
- }
-
- stdErrLogPath := filepath.Join(logDir, errorLogFile)
- stdoutLogPath := filepath.Join(logDir, stdoutLogFile)
- if runtime.GOOS == "darwin" {
- stdErrLogPath = darwinErrorLogPath
- stdoutLogPath = darwinStdoutLogPath
- }
-
- if err := s.addSingleLogfile(stdErrLogPath, errorLogFile, req, anonymizer, archive); err != nil {
- log.Warnf("Failed to add %s to zip: %v", errorLogFile, err)
- }
-
- if err := s.addSingleLogfile(stdoutLogPath, stdoutLogFile, req, anonymizer, archive); err != nil {
- log.Warnf("Failed to add %s to zip: %v", stdoutLogFile, err)
- }
-
- return nil
-}
-
-// addSingleLogfile adds a single log file to the archive
-func (s *Server) addSingleLogfile(logPath, targetName string, req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) error {
- logFile, err := os.Open(logPath)
- if err != nil {
- return fmt.Errorf("open log file %s: %w", targetName, err)
- }
- defer func() {
- if err := logFile.Close(); err != nil {
- log.Errorf("Failed to close log file %s: %v", targetName, err)
- }
- }()
-
- var logReader io.Reader
- if req.GetAnonymize() {
- var writer *io.PipeWriter
- logReader, writer = io.Pipe()
-
- go anonymizeLog(logFile, writer, anonymizer)
- } else {
- logReader = logFile
- }
-
- if err := addFileToZip(archive, logReader, targetName); err != nil {
- return fmt.Errorf("add %s to zip: %w", targetName, err)
- }
-
- return nil
-}
-
-// getLatestNetworkMap returns the latest network map from the engine if network map persistence is enabled
-func (s *Server) getLatestNetworkMap() (*mgmProto.NetworkMap, error) {
- if s.connectClient == nil {
- return nil, errors.New("connect client is not initialized")
- }
-
- engine := s.connectClient.Engine()
- if engine == nil {
- return nil, errors.New("engine is not initialized")
- }
-
- networkMap, err := engine.GetLatestNetworkMap()
- if err != nil {
- return nil, fmt.Errorf("get latest network map: %w", err)
- }
-
- if networkMap == nil {
- return nil, errors.New("network map is not available")
- }
-
- return networkMap, nil
+func getURLHash(url string) string {
+ return fmt.Sprintf("%x", sha256.Sum256([]byte(url)))
}
// GetLogLevel gets the current logging level for the server.
@@ -612,439 +203,12 @@ func (s *Server) SetNetworkMapPersistence(_ context.Context, req *proto.SetNetwo
return &proto.SetNetworkMapPersistenceResponse{}, nil
}
-func addFileToZip(archive *zip.Writer, reader io.Reader, filename string) error {
- header := &zip.FileHeader{
- Name: filename,
- Method: zip.Deflate,
- Modified: time.Now(),
-
- CreatorVersion: 20, // Version 2.0
- ReaderVersion: 20, // Version 2.0
- Flags: 0x800, // UTF-8 filename
+// getLatestNetworkMap returns the latest network map from the engine if network map persistence is enabled
+func (s *Server) getLatestNetworkMap() (*mgmProto.NetworkMap, error) {
+ cClient := s.connectClient
+ if cClient == nil {
+ return nil, errors.New("connect client is not initialized")
}
- // If the reader is a file, we can get more accurate information
- if f, ok := reader.(*os.File); ok {
- if stat, err := f.Stat(); err != nil {
- log.Tracef("Failed to get file stat for %s: %v", filename, err)
- } else {
- header.Modified = stat.ModTime()
- }
- }
-
- writer, err := archive.CreateHeader(header)
- if err != nil {
- return fmt.Errorf("create zip file header: %w", err)
- }
-
- if _, err := io.Copy(writer, reader); err != nil {
- return fmt.Errorf("write file to zip: %w", err)
- }
-
- return nil
-}
-
-func seedFromStatus(a *anonymize.Anonymizer, status *peer.FullStatus) {
- status.ManagementState.URL = a.AnonymizeURI(status.ManagementState.URL)
- status.SignalState.URL = a.AnonymizeURI(status.SignalState.URL)
-
- status.LocalPeerState.FQDN = a.AnonymizeDomain(status.LocalPeerState.FQDN)
-
- for _, peer := range status.Peers {
- a.AnonymizeDomain(peer.FQDN)
- for route := range peer.GetRoutes() {
- a.AnonymizeRoute(route)
- }
- }
-
- for route := range status.LocalPeerState.Routes {
- a.AnonymizeRoute(route)
- }
-
- for _, nsGroup := range status.NSGroupStates {
- for _, domain := range nsGroup.Domains {
- a.AnonymizeDomain(domain)
- }
- }
-
- for _, relay := range status.Relays {
- if relay.URI != "" {
- a.AnonymizeURI(relay.URI)
- }
- }
-}
-
-func formatRoutes(routes []netip.Prefix, anonymize bool, anonymizer *anonymize.Anonymizer) string {
- var ipv4Routes, ipv6Routes []netip.Prefix
-
- // Separate IPv4 and IPv6 routes
- for _, route := range routes {
- if route.Addr().Is4() {
- ipv4Routes = append(ipv4Routes, route)
- } else {
- ipv6Routes = append(ipv6Routes, route)
- }
- }
-
- // Sort IPv4 and IPv6 routes separately
- sort.Slice(ipv4Routes, func(i, j int) bool {
- return ipv4Routes[i].Bits() > ipv4Routes[j].Bits()
- })
- sort.Slice(ipv6Routes, func(i, j int) bool {
- return ipv6Routes[i].Bits() > ipv6Routes[j].Bits()
- })
-
- var builder strings.Builder
-
- // Format IPv4 routes
- builder.WriteString("IPv4 Routes:\n")
- for _, route := range ipv4Routes {
- formatRoute(&builder, route, anonymize, anonymizer)
- }
-
- // Format IPv6 routes
- builder.WriteString("\nIPv6 Routes:\n")
- for _, route := range ipv6Routes {
- formatRoute(&builder, route, anonymize, anonymizer)
- }
-
- return builder.String()
-}
-
-func formatRoute(builder *strings.Builder, route netip.Prefix, anonymize bool, anonymizer *anonymize.Anonymizer) {
- if anonymize {
- anonymizedIP := anonymizer.AnonymizeIP(route.Addr())
- builder.WriteString(fmt.Sprintf("%s/%d\n", anonymizedIP, route.Bits()))
- } else {
- builder.WriteString(fmt.Sprintf("%s\n", route))
- }
-}
-
-func formatInterfaces(interfaces []net.Interface, anonymize bool, anonymizer *anonymize.Anonymizer) string {
- sort.Slice(interfaces, func(i, j int) bool {
- return interfaces[i].Name < interfaces[j].Name
- })
-
- var builder strings.Builder
- builder.WriteString("Network Interfaces:\n")
-
- for _, iface := range interfaces {
- builder.WriteString(fmt.Sprintf("\nInterface: %s\n", iface.Name))
- builder.WriteString(fmt.Sprintf(" Index: %d\n", iface.Index))
- builder.WriteString(fmt.Sprintf(" MTU: %d\n", iface.MTU))
- builder.WriteString(fmt.Sprintf(" Flags: %v\n", iface.Flags))
-
- addrs, err := iface.Addrs()
- if err != nil {
- builder.WriteString(fmt.Sprintf(" Addresses: Error retrieving addresses: %v\n", err))
- } else {
- builder.WriteString(" Addresses:\n")
- for _, addr := range addrs {
- prefix, err := netip.ParsePrefix(addr.String())
- if err != nil {
- builder.WriteString(fmt.Sprintf(" Error parsing address: %v\n", err))
- continue
- }
- ip := prefix.Addr()
- if anonymize {
- ip = anonymizer.AnonymizeIP(ip)
- }
- builder.WriteString(fmt.Sprintf(" %s/%d\n", ip, prefix.Bits()))
- }
- }
- }
-
- return builder.String()
-}
-
-func anonymizeLog(reader io.Reader, writer *io.PipeWriter, anonymizer *anonymize.Anonymizer) {
- defer func() {
- // always nil
- _ = writer.Close()
- }()
-
- scanner := bufio.NewScanner(reader)
- for scanner.Scan() {
- line := anonymizer.AnonymizeString(scanner.Text())
- if _, err := writer.Write([]byte(line + "\n")); err != nil {
- writer.CloseWithError(fmt.Errorf("anonymize write: %w", err))
- return
- }
- }
- if err := scanner.Err(); err != nil {
- writer.CloseWithError(fmt.Errorf("anonymize scan: %w", err))
- return
- }
-}
-
-func anonymizeNATExternalIPs(ips []string, anonymizer *anonymize.Anonymizer) []string {
- anonymizedIPs := make([]string, len(ips))
- for i, ip := range ips {
- parts := strings.SplitN(ip, "/", 2)
-
- ip1, err := netip.ParseAddr(parts[0])
- if err != nil {
- anonymizedIPs[i] = ip
- continue
- }
- ip1anon := anonymizer.AnonymizeIP(ip1)
-
- if len(parts) == 2 {
- ip2, err := netip.ParseAddr(parts[1])
- if err != nil {
- anonymizedIPs[i] = fmt.Sprintf("%s/%s", ip1anon, parts[1])
- } else {
- ip2anon := anonymizer.AnonymizeIP(ip2)
- anonymizedIPs[i] = fmt.Sprintf("%s/%s", ip1anon, ip2anon)
- }
- } else {
- anonymizedIPs[i] = ip1anon.String()
- }
- }
- return anonymizedIPs
-}
-
-func anonymizeNetworkMap(networkMap *mgmProto.NetworkMap, anonymizer *anonymize.Anonymizer) error {
- if networkMap.PeerConfig != nil {
- anonymizePeerConfig(networkMap.PeerConfig, anonymizer)
- }
-
- for _, peer := range networkMap.RemotePeers {
- anonymizeRemotePeer(peer, anonymizer)
- }
-
- for _, peer := range networkMap.OfflinePeers {
- anonymizeRemotePeer(peer, anonymizer)
- }
-
- for _, r := range networkMap.Routes {
- anonymizeRoute(r, anonymizer)
- }
-
- if networkMap.DNSConfig != nil {
- anonymizeDNSConfig(networkMap.DNSConfig, anonymizer)
- }
-
- for _, rule := range networkMap.FirewallRules {
- anonymizeFirewallRule(rule, anonymizer)
- }
-
- for _, rule := range networkMap.RoutesFirewallRules {
- anonymizeRouteFirewallRule(rule, anonymizer)
- }
-
- return nil
-}
-
-func anonymizePeerConfig(config *mgmProto.PeerConfig, anonymizer *anonymize.Anonymizer) {
- if config == nil {
- return
- }
-
- if addr, err := netip.ParseAddr(config.Address); err == nil {
- config.Address = anonymizer.AnonymizeIP(addr).String()
- }
-
- if config.SshConfig != nil && len(config.SshConfig.SshPubKey) > 0 {
- config.SshConfig.SshPubKey = []byte("ssh-placeholder-key")
- }
-
- config.Dns = anonymizer.AnonymizeString(config.Dns)
- config.Fqdn = anonymizer.AnonymizeDomain(config.Fqdn)
-}
-
-func anonymizeRemotePeer(peer *mgmProto.RemotePeerConfig, anonymizer *anonymize.Anonymizer) {
- if peer == nil {
- return
- }
-
- for i, ip := range peer.AllowedIps {
- // Try to parse as prefix first (CIDR)
- if prefix, err := netip.ParsePrefix(ip); err == nil {
- anonIP := anonymizer.AnonymizeIP(prefix.Addr())
- peer.AllowedIps[i] = fmt.Sprintf("%s/%d", anonIP, prefix.Bits())
- } else if addr, err := netip.ParseAddr(ip); err == nil {
- peer.AllowedIps[i] = anonymizer.AnonymizeIP(addr).String()
- }
- }
-
- peer.Fqdn = anonymizer.AnonymizeDomain(peer.Fqdn)
-
- if peer.SshConfig != nil && len(peer.SshConfig.SshPubKey) > 0 {
- peer.SshConfig.SshPubKey = []byte("ssh-placeholder-key")
- }
-}
-
-func anonymizeRoute(route *mgmProto.Route, anonymizer *anonymize.Anonymizer) {
- if route == nil {
- return
- }
-
- if prefix, err := netip.ParsePrefix(route.Network); err == nil {
- anonIP := anonymizer.AnonymizeIP(prefix.Addr())
- route.Network = fmt.Sprintf("%s/%d", anonIP, prefix.Bits())
- }
-
- for i, domain := range route.Domains {
- route.Domains[i] = anonymizer.AnonymizeDomain(domain)
- }
-
- route.NetID = anonymizer.AnonymizeString(route.NetID)
-}
-
-func anonymizeDNSConfig(config *mgmProto.DNSConfig, anonymizer *anonymize.Anonymizer) {
- if config == nil {
- return
- }
-
- anonymizeNameServerGroups(config.NameServerGroups, anonymizer)
- anonymizeCustomZones(config.CustomZones, anonymizer)
-}
-
-func anonymizeNameServerGroups(groups []*mgmProto.NameServerGroup, anonymizer *anonymize.Anonymizer) {
- for _, group := range groups {
- anonymizeServers(group.NameServers, anonymizer)
- anonymizeDomains(group.Domains, anonymizer)
- }
-}
-
-func anonymizeServers(servers []*mgmProto.NameServer, anonymizer *anonymize.Anonymizer) {
- for _, server := range servers {
- if addr, err := netip.ParseAddr(server.IP); err == nil {
- server.IP = anonymizer.AnonymizeIP(addr).String()
- }
- }
-}
-
-func anonymizeDomains(domains []string, anonymizer *anonymize.Anonymizer) {
- for i, domain := range domains {
- domains[i] = anonymizer.AnonymizeDomain(domain)
- }
-}
-
-func anonymizeCustomZones(zones []*mgmProto.CustomZone, anonymizer *anonymize.Anonymizer) {
- for _, zone := range zones {
- zone.Domain = anonymizer.AnonymizeDomain(zone.Domain)
- anonymizeRecords(zone.Records, anonymizer)
- }
-}
-
-func anonymizeRecords(records []*mgmProto.SimpleRecord, anonymizer *anonymize.Anonymizer) {
- for _, record := range records {
- record.Name = anonymizer.AnonymizeDomain(record.Name)
- anonymizeRData(record, anonymizer)
- }
-}
-
-func anonymizeRData(record *mgmProto.SimpleRecord, anonymizer *anonymize.Anonymizer) {
- switch record.Type {
- case 1, 28: // A or AAAA record
- if addr, err := netip.ParseAddr(record.RData); err == nil {
- record.RData = anonymizer.AnonymizeIP(addr).String()
- }
- default:
- record.RData = anonymizer.AnonymizeString(record.RData)
- }
-}
-
-func anonymizeFirewallRule(rule *mgmProto.FirewallRule, anonymizer *anonymize.Anonymizer) {
- if rule == nil {
- return
- }
-
- if addr, err := netip.ParseAddr(rule.PeerIP); err == nil {
- rule.PeerIP = anonymizer.AnonymizeIP(addr).String()
- }
-}
-
-func anonymizeRouteFirewallRule(rule *mgmProto.RouteFirewallRule, anonymizer *anonymize.Anonymizer) {
- if rule == nil {
- return
- }
-
- for i, sourceRange := range rule.SourceRanges {
- if prefix, err := netip.ParsePrefix(sourceRange); err == nil {
- anonIP := anonymizer.AnonymizeIP(prefix.Addr())
- rule.SourceRanges[i] = fmt.Sprintf("%s/%d", anonIP, prefix.Bits())
- }
- }
-
- if prefix, err := netip.ParsePrefix(rule.Destination); err == nil {
- anonIP := anonymizer.AnonymizeIP(prefix.Addr())
- rule.Destination = fmt.Sprintf("%s/%d", anonIP, prefix.Bits())
- }
-}
-
-func anonymizeStateFile(rawStates *map[string]json.RawMessage, anonymizer *anonymize.Anonymizer) error {
- for name, rawState := range *rawStates {
- if string(rawState) == "null" {
- continue
- }
-
- var state map[string]any
- if err := json.Unmarshal(rawState, &state); err != nil {
- return fmt.Errorf("unmarshal state %s: %w", name, err)
- }
-
- state = anonymizeValue(state, anonymizer).(map[string]any)
-
- bs, err := json.Marshal(state)
- if err != nil {
- return fmt.Errorf("marshal state %s: %w", name, err)
- }
-
- (*rawStates)[name] = bs
- }
-
- return nil
-}
-
-func anonymizeValue(value any, anonymizer *anonymize.Anonymizer) any {
- switch v := value.(type) {
- case string:
- return anonymizeString(v, anonymizer)
- case map[string]any:
- return anonymizeMap(v, anonymizer)
- case []any:
- return anonymizeSlice(v, anonymizer)
- }
- return value
-}
-
-func anonymizeString(v string, anonymizer *anonymize.Anonymizer) string {
- if prefix, err := netip.ParsePrefix(v); err == nil {
- anonIP := anonymizer.AnonymizeIP(prefix.Addr())
- return fmt.Sprintf("%s/%d", anonIP, prefix.Bits())
- }
- if ip, err := netip.ParseAddr(v); err == nil {
- return anonymizer.AnonymizeIP(ip).String()
- }
- return anonymizer.AnonymizeString(v)
-}
-
-func anonymizeMap(v map[string]any, anonymizer *anonymize.Anonymizer) map[string]any {
- result := make(map[string]any, len(v))
- for key, val := range v {
- newKey := anonymizeMapKey(key, anonymizer)
- result[newKey] = anonymizeValue(val, anonymizer)
- }
- return result
-}
-
-func anonymizeMapKey(key string, anonymizer *anonymize.Anonymizer) string {
- if prefix, err := netip.ParsePrefix(key); err == nil {
- anonIP := anonymizer.AnonymizeIP(prefix.Addr())
- return fmt.Sprintf("%s/%d", anonIP, prefix.Bits())
- }
- if ip, err := netip.ParseAddr(key); err == nil {
- return anonymizer.AnonymizeIP(ip).String()
- }
- return key
-}
-
-func anonymizeSlice(v []any, anonymizer *anonymize.Anonymizer) []any {
- for i, val := range v {
- v[i] = anonymizeValue(val, anonymizer)
- }
- return v
+ return cClient.GetLatestNetworkMap()
}
diff --git a/client/server/debug_nonlinux.go b/client/server/debug_nonlinux.go
deleted file mode 100644
index c54ac9b6e..000000000
--- a/client/server/debug_nonlinux.go
+++ /dev/null
@@ -1,15 +0,0 @@
-//go:build !linux || android
-
-package server
-
-import (
- "archive/zip"
-
- "github.com/netbirdio/netbird/client/anonymize"
- "github.com/netbirdio/netbird/client/proto"
-)
-
-// collectFirewallRules returns nothing on non-linux systems
-func (s *Server) addFirewallRules(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) error {
- return nil
-}
diff --git a/client/server/debug_test.go b/client/server/debug_test.go
index ebd0bffbc..53d9ac8ed 100644
--- a/client/server/debug_test.go
+++ b/client/server/debug_test.go
@@ -1,543 +1,49 @@
package server
import (
- "encoding/json"
- "net"
- "strings"
+ "context"
+ "errors"
+ "net/http"
+ "os"
+ "path/filepath"
"testing"
- "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/netbirdio/netbird/client/anonymize"
- mgmProto "github.com/netbirdio/netbird/management/proto"
+ "github.com/netbirdio/netbird/upload-server/server"
+ "github.com/netbirdio/netbird/upload-server/types"
)
-func TestAnonymizeStateFile(t *testing.T) {
- testState := map[string]json.RawMessage{
- "null_state": json.RawMessage("null"),
- "test_state": mustMarshal(map[string]any{
- // Test simple fields
- "public_ip": "203.0.113.1",
- "private_ip": "192.168.1.1",
- "protected_ip": "100.64.0.1",
- "well_known_ip": "8.8.8.8",
- "ipv6_addr": "2001:db8::1",
- "private_ipv6": "fd00::1",
- "domain": "test.example.com",
- "uri": "stun:stun.example.com:3478",
- "uri_with_ip": "turn:203.0.113.1:3478",
- "netbird_domain": "device.netbird.cloud",
-
- // Test CIDR ranges
- "public_cidr": "203.0.113.0/24",
- "private_cidr": "192.168.0.0/16",
- "protected_cidr": "100.64.0.0/10",
- "ipv6_cidr": "2001:db8::/32",
- "private_ipv6_cidr": "fd00::/8",
-
- // Test nested structures
- "nested": map[string]any{
- "ip": "203.0.113.2",
- "domain": "nested.example.com",
- "more_nest": map[string]any{
- "ip": "203.0.113.3",
- "domain": "deep.example.com",
- },
- },
-
- // Test arrays
- "string_array": []any{
- "203.0.113.4",
- "test1.example.com",
- "test2.example.com",
- },
- "object_array": []any{
- map[string]any{
- "ip": "203.0.113.5",
- "domain": "array1.example.com",
- },
- map[string]any{
- "ip": "203.0.113.6",
- "domain": "array2.example.com",
- },
- },
-
- // Test multiple occurrences of same value
- "duplicate_ip": "203.0.113.1", // Same as public_ip
- "duplicate_domain": "test.example.com", // Same as domain
-
- // Test URIs with various schemes
- "stun_uri": "stun:stun.example.com:3478",
- "turns_uri": "turns:turns.example.com:5349",
- "http_uri": "http://web.example.com:80",
- "https_uri": "https://secure.example.com:443",
-
- // Test strings that might look like IPs but aren't
- "not_ip": "300.300.300.300",
- "partial_ip": "192.168",
- "ip_like_string": "1234.5678",
-
- // Test mixed content strings
- "mixed_content": "Server at 203.0.113.1 (test.example.com) on port 80",
-
- // Test empty and special values
- "empty_string": "",
- "null_value": nil,
- "numeric_value": 42,
- "boolean_value": true,
- }),
- "route_state": mustMarshal(map[string]any{
- "routes": []any{
- map[string]any{
- "network": "203.0.113.0/24",
- "gateway": "203.0.113.1",
- "domains": []any{
- "route1.example.com",
- "route2.example.com",
- },
- },
- map[string]any{
- "network": "2001:db8::/32",
- "gateway": "2001:db8::1",
- "domains": []any{
- "route3.example.com",
- "route4.example.com",
- },
- },
- },
- // Test map with IP/CIDR keys
- "refCountMap": map[string]any{
- "203.0.113.1/32": map[string]any{
- "Count": 1,
- "Out": map[string]any{
- "IP": "192.168.0.1",
- "Intf": map[string]any{
- "Name": "eth0",
- "Index": 1,
- },
- },
- },
- "2001:db8::1/128": map[string]any{
- "Count": 1,
- "Out": map[string]any{
- "IP": "fe80::1",
- "Intf": map[string]any{
- "Name": "eth0",
- "Index": 1,
- },
- },
- },
- "10.0.0.1/32": map[string]any{ // private IP should remain unchanged
- "Count": 1,
- "Out": map[string]any{
- "IP": "192.168.0.1",
- },
- },
- },
- }),
+func TestUpload(t *testing.T) {
+ if os.Getenv("DOCKER_CI") == "true" {
+ t.Skip("Skipping upload test on docker ci")
}
+ testDir := t.TempDir()
+ testURL := "http://localhost:8080"
+ t.Setenv("SERVER_URL", testURL)
+ t.Setenv("STORE_DIR", testDir)
+ srv := server.NewServer()
+ go func() {
+ if err := srv.Start(); err != nil && !errors.Is(err, http.ErrServerClosed) {
+ t.Errorf("Failed to start server: %v", err)
+ }
+ }()
+ t.Cleanup(func() {
+ if err := srv.Stop(); err != nil {
+ t.Errorf("Failed to stop server: %v", err)
+ }
+ })
- anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
-
- // Pre-seed the domains we need to verify in the test assertions
- anonymizer.AnonymizeDomain("test.example.com")
- anonymizer.AnonymizeDomain("nested.example.com")
- anonymizer.AnonymizeDomain("deep.example.com")
- anonymizer.AnonymizeDomain("array1.example.com")
-
- err := anonymizeStateFile(&testState, anonymizer)
+ file := filepath.Join(t.TempDir(), "tmpfile")
+ fileContent := []byte("test file content")
+ err := os.WriteFile(file, fileContent, 0640)
require.NoError(t, err)
-
- // Helper function to unmarshal and get nested values
- var state map[string]any
- err = json.Unmarshal(testState["test_state"], &state)
+ key, err := uploadDebugBundle(context.Background(), testURL+types.GetURLPath, testURL, file)
require.NoError(t, err)
-
- // Test null state remains unchanged
- require.Equal(t, "null", string(testState["null_state"]))
-
- // Basic assertions
- assert.NotEqual(t, "203.0.113.1", state["public_ip"])
- assert.Equal(t, "192.168.1.1", state["private_ip"]) // Private IP unchanged
- assert.Equal(t, "100.64.0.1", state["protected_ip"]) // Protected IP unchanged
- assert.Equal(t, "8.8.8.8", state["well_known_ip"]) // Well-known IP unchanged
- assert.NotEqual(t, "2001:db8::1", state["ipv6_addr"])
- assert.Equal(t, "fd00::1", state["private_ipv6"]) // Private IPv6 unchanged
- assert.NotEqual(t, "test.example.com", state["domain"])
- assert.True(t, strings.HasSuffix(state["domain"].(string), ".domain"))
- assert.Equal(t, "device.netbird.cloud", state["netbird_domain"]) // Netbird domain unchanged
-
- // CIDR ranges
- assert.NotEqual(t, "203.0.113.0/24", state["public_cidr"])
- assert.Contains(t, state["public_cidr"], "/24") // Prefix preserved
- assert.Equal(t, "192.168.0.0/16", state["private_cidr"]) // Private CIDR unchanged
- assert.Equal(t, "100.64.0.0/10", state["protected_cidr"]) // Protected CIDR unchanged
- assert.NotEqual(t, "2001:db8::/32", state["ipv6_cidr"])
- assert.Contains(t, state["ipv6_cidr"], "/32") // IPv6 prefix preserved
-
- // Nested structures
- nested := state["nested"].(map[string]any)
- assert.NotEqual(t, "203.0.113.2", nested["ip"])
- assert.NotEqual(t, "nested.example.com", nested["domain"])
- moreNest := nested["more_nest"].(map[string]any)
- assert.NotEqual(t, "203.0.113.3", moreNest["ip"])
- assert.NotEqual(t, "deep.example.com", moreNest["domain"])
-
- // Arrays
- strArray := state["string_array"].([]any)
- assert.NotEqual(t, "203.0.113.4", strArray[0])
- assert.NotEqual(t, "test1.example.com", strArray[1])
- assert.True(t, strings.HasSuffix(strArray[1].(string), ".domain"))
-
- objArray := state["object_array"].([]any)
- firstObj := objArray[0].(map[string]any)
- assert.NotEqual(t, "203.0.113.5", firstObj["ip"])
- assert.NotEqual(t, "array1.example.com", firstObj["domain"])
-
- // Duplicate values should be anonymized consistently
- assert.Equal(t, state["public_ip"], state["duplicate_ip"])
- assert.Equal(t, state["domain"], state["duplicate_domain"])
-
- // URIs
- assert.NotContains(t, state["stun_uri"], "stun.example.com")
- assert.NotContains(t, state["turns_uri"], "turns.example.com")
- assert.NotContains(t, state["http_uri"], "web.example.com")
- assert.NotContains(t, state["https_uri"], "secure.example.com")
-
- // Non-IP strings should remain unchanged
- assert.Equal(t, "300.300.300.300", state["not_ip"])
- assert.Equal(t, "192.168", state["partial_ip"])
- assert.Equal(t, "1234.5678", state["ip_like_string"])
-
- // Mixed content should have IPs and domains replaced
- mixedContent := state["mixed_content"].(string)
- assert.NotContains(t, mixedContent, "203.0.113.1")
- assert.NotContains(t, mixedContent, "test.example.com")
- assert.Contains(t, mixedContent, "Server at ")
- assert.Contains(t, mixedContent, " on port 80")
-
- // Special values should remain unchanged
- assert.Equal(t, "", state["empty_string"])
- assert.Nil(t, state["null_value"])
- assert.Equal(t, float64(42), state["numeric_value"])
- assert.Equal(t, true, state["boolean_value"])
-
- // Check route state
- var routeState map[string]any
- err = json.Unmarshal(testState["route_state"], &routeState)
+ id := getURLHash(testURL)
+ require.Contains(t, key, id+"/")
+ expectedFilePath := filepath.Join(testDir, key)
+ createdFileContent, err := os.ReadFile(expectedFilePath)
require.NoError(t, err)
-
- routes := routeState["routes"].([]any)
- route1 := routes[0].(map[string]any)
- assert.NotEqual(t, "203.0.113.0/24", route1["network"])
- assert.Contains(t, route1["network"], "/24")
- assert.NotEqual(t, "203.0.113.1", route1["gateway"])
- domains := route1["domains"].([]any)
- assert.True(t, strings.HasSuffix(domains[0].(string), ".domain"))
- assert.True(t, strings.HasSuffix(domains[1].(string), ".domain"))
-
- // Check map keys are anonymized
- refCountMap := routeState["refCountMap"].(map[string]any)
- hasPublicIPKey := false
- hasIPv6Key := false
- hasPrivateIPKey := false
- for key := range refCountMap {
- if strings.Contains(key, "203.0.113.1") {
- hasPublicIPKey = true
- }
- if strings.Contains(key, "2001:db8::1") {
- hasIPv6Key = true
- }
- if key == "10.0.0.1/32" {
- hasPrivateIPKey = true
- }
- }
- assert.False(t, hasPublicIPKey, "public IP in key should be anonymized")
- assert.False(t, hasIPv6Key, "IPv6 in key should be anonymized")
- assert.True(t, hasPrivateIPKey, "private IP in key should remain unchanged")
-}
-
-func mustMarshal(v any) json.RawMessage {
- data, err := json.Marshal(v)
- if err != nil {
- panic(err)
- }
- return data
-}
-
-func TestAnonymizeNetworkMap(t *testing.T) {
- networkMap := &mgmProto.NetworkMap{
- PeerConfig: &mgmProto.PeerConfig{
- Address: "203.0.113.5",
- Dns: "1.2.3.4",
- Fqdn: "peer1.corp.example.com",
- SshConfig: &mgmProto.SSHConfig{
- SshPubKey: []byte("ssh-rsa AAAAB3NzaC1..."),
- },
- },
- RemotePeers: []*mgmProto.RemotePeerConfig{
- {
- AllowedIps: []string{
- "203.0.113.1/32",
- "2001:db8:1234::1/128",
- "192.168.1.1/32",
- "100.64.0.1/32",
- "10.0.0.1/32",
- },
- Fqdn: "peer2.corp.example.com",
- SshConfig: &mgmProto.SSHConfig{
- SshPubKey: []byte("ssh-rsa AAAAB3NzaC2..."),
- },
- },
- },
- Routes: []*mgmProto.Route{
- {
- Network: "197.51.100.0/24",
- Domains: []string{"prod.example.com", "staging.example.com"},
- NetID: "net-123abc",
- },
- },
- DNSConfig: &mgmProto.DNSConfig{
- NameServerGroups: []*mgmProto.NameServerGroup{
- {
- NameServers: []*mgmProto.NameServer{
- {IP: "8.8.8.8"},
- {IP: "1.1.1.1"},
- {IP: "203.0.113.53"},
- },
- Domains: []string{"example.com", "internal.example.com"},
- },
- },
- CustomZones: []*mgmProto.CustomZone{
- {
- Domain: "custom.example.com",
- Records: []*mgmProto.SimpleRecord{
- {
- Name: "www.custom.example.com",
- Type: 1,
- RData: "203.0.113.10",
- },
- {
- Name: "internal.custom.example.com",
- Type: 1,
- RData: "192.168.1.10",
- },
- },
- },
- },
- },
- }
-
- // Create anonymizer with test addresses
- anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
-
- // Anonymize the network map
- err := anonymizeNetworkMap(networkMap, anonymizer)
- require.NoError(t, err)
-
- // Test PeerConfig anonymization
- peerCfg := networkMap.PeerConfig
- require.NotEqual(t, "203.0.113.5", peerCfg.Address)
-
- // Verify DNS and FQDN are properly anonymized
- require.NotEqual(t, "1.2.3.4", peerCfg.Dns)
- require.NotEqual(t, "peer1.corp.example.com", peerCfg.Fqdn)
- require.True(t, strings.HasSuffix(peerCfg.Fqdn, ".domain"))
-
- // Verify SSH key is replaced
- require.Equal(t, []byte("ssh-placeholder-key"), peerCfg.SshConfig.SshPubKey)
-
- // Test RemotePeers anonymization
- remotePeer := networkMap.RemotePeers[0]
-
- // Verify FQDN is anonymized
- require.NotEqual(t, "peer2.corp.example.com", remotePeer.Fqdn)
- require.True(t, strings.HasSuffix(remotePeer.Fqdn, ".domain"))
-
- // Check that public IPs are anonymized but private IPs are preserved
- for _, allowedIP := range remotePeer.AllowedIps {
- ip, _, err := net.ParseCIDR(allowedIP)
- require.NoError(t, err)
-
- if ip.IsPrivate() || isInCGNATRange(ip) {
- require.Contains(t, []string{
- "192.168.1.1/32",
- "100.64.0.1/32",
- "10.0.0.1/32",
- }, allowedIP)
- } else {
- require.NotContains(t, []string{
- "203.0.113.1/32",
- "2001:db8:1234::1/128",
- }, allowedIP)
- }
- }
-
- // Test Routes anonymization
- route := networkMap.Routes[0]
- require.NotEqual(t, "197.51.100.0/24", route.Network)
- for _, domain := range route.Domains {
- require.True(t, strings.HasSuffix(domain, ".domain"))
- require.NotContains(t, domain, "example.com")
- }
-
- // Test DNS config anonymization
- dnsConfig := networkMap.DNSConfig
- nameServerGroup := dnsConfig.NameServerGroups[0]
-
- // Verify well-known DNS servers are preserved
- require.Equal(t, "8.8.8.8", nameServerGroup.NameServers[0].IP)
- require.Equal(t, "1.1.1.1", nameServerGroup.NameServers[1].IP)
-
- // Verify public DNS server is anonymized
- require.NotEqual(t, "203.0.113.53", nameServerGroup.NameServers[2].IP)
-
- // Verify domains are anonymized
- for _, domain := range nameServerGroup.Domains {
- require.True(t, strings.HasSuffix(domain, ".domain"))
- require.NotContains(t, domain, "example.com")
- }
-
- // Test CustomZones anonymization
- customZone := dnsConfig.CustomZones[0]
- require.True(t, strings.HasSuffix(customZone.Domain, ".domain"))
- require.NotContains(t, customZone.Domain, "example.com")
-
- // Verify records are properly anonymized
- for _, record := range customZone.Records {
- require.True(t, strings.HasSuffix(record.Name, ".domain"))
- require.NotContains(t, record.Name, "example.com")
-
- ip := net.ParseIP(record.RData)
- if ip != nil {
- if !ip.IsPrivate() {
- require.NotEqual(t, "203.0.113.10", record.RData)
- } else {
- require.Equal(t, "192.168.1.10", record.RData)
- }
- }
- }
-}
-
-// Helper function to check if IP is in CGNAT range
-func isInCGNATRange(ip net.IP) bool {
- cgnat := net.IPNet{
- IP: net.ParseIP("100.64.0.0"),
- Mask: net.CIDRMask(10, 32),
- }
- return cgnat.Contains(ip)
-}
-
-func TestAnonymizeFirewallRules(t *testing.T) {
- // TODO: Add ipv6
-
- // Example iptables-save output
- iptablesSave := `# Generated by iptables-save v1.8.7 on Thu Dec 19 10:00:00 2024
-*filter
-:INPUT ACCEPT [0:0]
-:FORWARD ACCEPT [0:0]
-:OUTPUT ACCEPT [0:0]
--A INPUT -s 192.168.1.0/24 -j ACCEPT
--A INPUT -s 44.192.140.1/32 -j DROP
--A FORWARD -s 10.0.0.0/8 -j DROP
--A FORWARD -s 44.192.140.0/24 -d 52.84.12.34/24 -j ACCEPT
-COMMIT
-
-*nat
-:PREROUTING ACCEPT [0:0]
-:INPUT ACCEPT [0:0]
-:OUTPUT ACCEPT [0:0]
-:POSTROUTING ACCEPT [0:0]
--A POSTROUTING -s 192.168.100.0/24 -j MASQUERADE
--A PREROUTING -d 44.192.140.10/32 -p tcp -m tcp --dport 80 -j DNAT --to-destination 192.168.1.10:80
-COMMIT`
-
- // Example iptables -v -n -L output
- iptablesVerbose := `Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
- pkts bytes target prot opt in out source destination
- 0 0 ACCEPT all -- * * 192.168.1.0/24 0.0.0.0/0
- 100 1024 DROP all -- * * 44.192.140.1 0.0.0.0/0
-
-Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
- pkts bytes target prot opt in out source destination
- 0 0 DROP all -- * * 10.0.0.0/8 0.0.0.0/0
- 25 256 ACCEPT all -- * * 44.192.140.0/24 52.84.12.34/24
-
-Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
- pkts bytes target prot opt in out source destination`
-
- // Example nftables output
- nftablesRules := `table inet filter {
- chain input {
- type filter hook input priority filter; policy accept;
- ip saddr 192.168.1.1 accept
- ip saddr 44.192.140.1 drop
- }
- chain forward {
- type filter hook forward priority filter; policy accept;
- ip saddr 10.0.0.0/8 drop
- ip saddr 44.192.140.0/24 ip daddr 52.84.12.34/24 accept
- }
- }`
-
- anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
-
- // Test iptables-save anonymization
- anonIptablesSave := anonymizer.AnonymizeString(iptablesSave)
-
- // Private IP addresses should remain unchanged
- assert.Contains(t, anonIptablesSave, "192.168.1.0/24")
- assert.Contains(t, anonIptablesSave, "10.0.0.0/8")
- assert.Contains(t, anonIptablesSave, "192.168.100.0/24")
- assert.Contains(t, anonIptablesSave, "192.168.1.10")
-
- // Public IP addresses should be anonymized to the default range
- assert.NotContains(t, anonIptablesSave, "44.192.140.1")
- assert.NotContains(t, anonIptablesSave, "44.192.140.0/24")
- assert.NotContains(t, anonIptablesSave, "52.84.12.34")
- assert.Contains(t, anonIptablesSave, "198.51.100.") // Default anonymous range
-
- // Structure should be preserved
- assert.Contains(t, anonIptablesSave, "*filter")
- assert.Contains(t, anonIptablesSave, ":INPUT ACCEPT [0:0]")
- assert.Contains(t, anonIptablesSave, "COMMIT")
- assert.Contains(t, anonIptablesSave, "-j MASQUERADE")
- assert.Contains(t, anonIptablesSave, "--dport 80")
-
- // Test iptables verbose output anonymization
- anonIptablesVerbose := anonymizer.AnonymizeString(iptablesVerbose)
-
- // Private IP addresses should remain unchanged
- assert.Contains(t, anonIptablesVerbose, "192.168.1.0/24")
- assert.Contains(t, anonIptablesVerbose, "10.0.0.0/8")
-
- // Public IP addresses should be anonymized to the default range
- assert.NotContains(t, anonIptablesVerbose, "44.192.140.1")
- assert.NotContains(t, anonIptablesVerbose, "44.192.140.0/24")
- assert.NotContains(t, anonIptablesVerbose, "52.84.12.34")
- assert.Contains(t, anonIptablesVerbose, "198.51.100.") // Default anonymous range
-
- // Structure and counters should be preserved
- assert.Contains(t, anonIptablesVerbose, "Chain INPUT (policy ACCEPT 0 packets, 0 bytes)")
- assert.Contains(t, anonIptablesVerbose, "100 1024 DROP")
- assert.Contains(t, anonIptablesVerbose, "pkts bytes target")
-
- // Test nftables anonymization
- anonNftables := anonymizer.AnonymizeString(nftablesRules)
-
- // Private IP addresses should remain unchanged
- assert.Contains(t, anonNftables, "192.168.1.1")
- assert.Contains(t, anonNftables, "10.0.0.0/8")
-
- // Public IP addresses should be anonymized to the default range
- assert.NotContains(t, anonNftables, "44.192.140.1")
- assert.NotContains(t, anonNftables, "44.192.140.0/24")
- assert.NotContains(t, anonNftables, "52.84.12.34")
- assert.Contains(t, anonNftables, "198.51.100.") // Default anonymous range
-
- // Structure should be preserved
- assert.Contains(t, anonNftables, "table inet filter {")
- assert.Contains(t, anonNftables, "chain input {")
- assert.Contains(t, anonNftables, "type filter hook input priority filter; policy accept;")
+ require.Equal(t, fileContent, createdFileContent)
}
diff --git a/client/server/network.go b/client/server/network.go
index e0b01f763..93b7caa46 100644
--- a/client/server/network.go
+++ b/client/server/network.go
@@ -100,7 +100,7 @@ func (s *Server) ListNetworks(context.Context, *proto.ListNetworksRequest) (*pro
// Convert to proto format
for domain, ips := range domainMap {
- pbRoute.ResolvedIPs[domain.PunycodeString()] = &proto.IPList{
+ pbRoute.ResolvedIPs[domain.SafeString()] = &proto.IPList{
Ips: ips,
}
}
diff --git a/client/status/status.go b/client/status/status.go
index 43acc9197..f37e5b0f0 100644
--- a/client/status/status.go
+++ b/client/status/status.go
@@ -16,6 +16,7 @@ import (
"github.com/netbirdio/netbird/client/anonymize"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/proto"
+ "github.com/netbirdio/netbird/management/domain"
"github.com/netbirdio/netbird/version"
)
@@ -414,7 +415,7 @@ func ParseGeneralSummary(overview OutputOverview, showURL bool, showRelays bool,
signalConnString,
relaysString,
dnsServersString,
- overview.FQDN,
+ domain.Domain(overview.FQDN).SafeString(),
interfaceIP,
interfaceTypeString,
rosenpassEnabledStatus,
@@ -508,7 +509,7 @@ func parsePeers(peers PeersStateOutput, rosenpassEnabled, rosenpassPermissive bo
" Quantum resistance: %s\n"+
" Networks: %s\n"+
" Latency: %s\n",
- peerState.FQDN,
+ domain.Domain(peerState.FQDN).SafeString(),
peerState.IP,
peerState.PubKey,
peerState.Status,
diff --git a/client/system/info.go b/client/system/info.go
index 2a0343ca6..3a0c57156 100644
--- a/client/system/info.go
+++ b/client/system/info.go
@@ -185,3 +185,10 @@ func GetInfoWithChecks(ctx context.Context, checks []*proto.Checks) (*Info, erro
return info, nil
}
+
+// UpdateStaticInfo asynchronously updates static system and platform information
+func UpdateStaticInfo() {
+ go func() {
+ _ = updateStaticInfo()
+ }()
+}
diff --git a/client/system/static_info.go b/client/system/static_info.go
index fabe65a68..f178ec932 100644
--- a/client/system/static_info.go
+++ b/client/system/static_info.go
@@ -16,12 +16,6 @@ var (
once sync.Once
)
-func init() {
- go func() {
- _ = updateStaticInfo()
- }()
-}
-
func updateStaticInfo() StaticInfo {
once.Do(func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
diff --git a/client/system/static_info_stub.go b/client/system/static_info_stub.go
new file mode 100644
index 000000000..faa3e700b
--- /dev/null
+++ b/client/system/static_info_stub.go
@@ -0,0 +1,8 @@
+//go:build android || freebsd || ios
+
+package system
+
+// updateStaticInfo returns an empty implementation for unsupported platforms
+func updateStaticInfo() StaticInfo {
+ return StaticInfo{}
+}
diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go
index b2a6404bb..d0b1bacf6 100644
--- a/client/ui/client_ui.go
+++ b/client/ui/client_ui.go
@@ -457,7 +457,7 @@ func (s *serviceClient) menuUpClick() error {
if status.Status == string(internal.StatusConnected) {
log.Warnf("already connected")
- return err
+ return nil
}
if _, err := s.conn.Up(s.ctx, &proto.UpRequest{}); err != nil {
@@ -482,7 +482,7 @@ func (s *serviceClient) menuDownClick() error {
return err
}
- if status.Status != string(internal.StatusConnected) {
+ if status.Status != string(internal.StatusConnected) && status.Status != string(internal.StatusConnecting) {
log.Warnf("already down")
return nil
}
@@ -520,7 +520,9 @@ func (s *serviceClient) updateStatus() error {
}
var systrayIconState bool
- if status.Status == string(internal.StatusConnected) && !s.mUp.Disabled() {
+
+ switch {
+ case status.Status == string(internal.StatusConnected):
s.connected = true
s.sendNotification = true
if s.isUpdateIconActive {
@@ -535,7 +537,9 @@ func (s *serviceClient) updateStatus() error {
s.mNetworks.Enable()
go s.updateExitNodes()
systrayIconState = true
- } else if status.Status != string(internal.StatusConnected) && s.mUp.Disabled() {
+ case status.Status == string(internal.StatusConnecting):
+ s.setConnectingStatus()
+ case status.Status != string(internal.StatusConnected) && s.mUp.Disabled():
s.setDisconnectedStatus()
systrayIconState = false
}
@@ -594,6 +598,17 @@ func (s *serviceClient) setDisconnectedStatus() {
go s.updateExitNodes()
}
+func (s *serviceClient) setConnectingStatus() {
+ s.connected = false
+ systray.SetTemplateIcon(iconConnectingMacOS, s.icConnecting)
+ systray.SetTooltip("NetBird (Connecting)")
+ s.mStatus.SetTitle("Connecting")
+ s.mUp.Disable()
+ s.mDown.Enable()
+ s.mNetworks.Disable()
+ s.mExitNode.Disable()
+}
+
func (s *serviceClient) onTrayReady() {
systray.SetTemplateIcon(iconDisconnectedMacOS, s.icDisconnected)
systray.SetTooltip("NetBird")
diff --git a/client/ui/network.go b/client/ui/network.go
index b21554f09..ddd8d5000 100644
--- a/client/ui/network.go
+++ b/client/ui/network.go
@@ -456,19 +456,27 @@ func (s *serviceClient) toggleExitNode(nodeID string, item *systray.MenuItem) er
}
}
- if item.Checked() && len(ids) == 0 {
- // exit node is the only selected node, deselect it
+ // exit node is the only selected node, deselect it
+ deselectAll := item.Checked() && len(ids) == 0
+ if deselectAll {
ids = append(ids, nodeID)
- exitNode = nil
+ for _, node := range exitNodes {
+ if node.ID == nodeID {
+ // set desired state for recreation
+ node.Selected = false
+ }
+ }
}
// deselect all other selected exit nodes
- if err := s.deselectOtherExitNodes(conn, ids, item); err != nil {
+ if err := s.deselectOtherExitNodes(conn, ids); err != nil {
return err
}
- if err := s.selectNewExitNode(conn, exitNode, nodeID, item); err != nil {
- return err
+ if !deselectAll {
+ if err := s.selectNewExitNode(conn, exitNode, nodeID, item); err != nil {
+ return err
+ }
}
// linux/bsd doesn't handle Check/Uncheck well, so we recreate the menu
@@ -479,7 +487,7 @@ func (s *serviceClient) toggleExitNode(nodeID string, item *systray.MenuItem) er
return nil
}
-func (s *serviceClient) deselectOtherExitNodes(conn proto.DaemonServiceClient, ids []string, currentItem *systray.MenuItem) error {
+func (s *serviceClient) deselectOtherExitNodes(conn proto.DaemonServiceClient, ids []string) error {
// deselect all other selected exit nodes
if len(ids) > 0 {
deselectReq := &proto.SelectNetworksRequest{
@@ -494,9 +502,6 @@ func (s *serviceClient) deselectOtherExitNodes(conn proto.DaemonServiceClient, i
// uncheck all other exit node menu items
for _, i := range s.mExitNodeItems {
- if i.MenuItem == currentItem {
- continue
- }
i.Uncheck()
log.Infof("Unchecked exit node %v", i)
}
@@ -518,6 +523,7 @@ func (s *serviceClient) selectNewExitNode(conn proto.DaemonServiceClient, exitNo
}
item.Check()
+ log.Infof("Checked exit node '%s'", nodeID)
return nil
}
diff --git a/dns/dns.go b/dns/dns.go
index 8dfdf8526..3a1c76e56 100644
--- a/dns/dns.go
+++ b/dns/dns.go
@@ -111,6 +111,5 @@ func GetParsedDomainLabel(name string) (string, error) {
// NormalizeZone returns a normalized domain name without the wildcard prefix
func NormalizeZone(domain string) string {
- d, _ := strings.CutPrefix(domain, "*.")
- return d
+ return strings.TrimPrefix(domain, "*.")
}
diff --git a/go.mod b/go.mod
index af800282e..2b3ef9cd6 100644
--- a/go.mod
+++ b/go.mod
@@ -18,14 +18,14 @@ require (
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5
- github.com/vishvananda/netlink v1.2.1-beta.2
- golang.org/x/crypto v0.36.0
- golang.org/x/sys v0.31.0
+ github.com/vishvananda/netlink v1.3.0
+ golang.org/x/crypto v0.37.0
+ golang.org/x/sys v0.32.0
golang.zx2c4.com/wireguard v0.0.0-20230704135630-469159ecf7d1
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
golang.zx2c4.com/wireguard/windows v0.5.3
google.golang.org/grpc v1.64.1
- google.golang.org/protobuf v1.35.2
+ google.golang.org/protobuf v1.36.5
gopkg.in/natefinch/lumberjack.v2 v2.0.0
)
@@ -33,13 +33,15 @@ require (
fyne.io/fyne/v2 v2.5.3
fyne.io/systray v1.11.0
github.com/TheJumpCloud/jcapi-go v3.0.0+incompatible
+ github.com/aws/aws-sdk-go-v2 v1.36.3
+ github.com/aws/aws-sdk-go-v2/config v1.29.14
+ github.com/aws/aws-sdk-go-v2/service/s3 v1.79.2
github.com/c-robinson/iplib v1.0.3
github.com/caddyserver/certmagic v0.21.3
github.com/cilium/ebpf v0.15.0
github.com/coder/websocket v1.8.12
github.com/coreos/go-iptables v0.7.0
github.com/creack/pty v1.1.18
- github.com/davecgh/go-spew v1.1.1
github.com/eko/gocache/lib/v4 v4.2.0
github.com/eko/gocache/store/go_cache/v4 v4.2.2
github.com/eko/gocache/store/redis/v4 v4.2.2
@@ -47,9 +49,9 @@ require (
github.com/gliderlabs/ssh v0.3.8
github.com/godbus/dbus/v5 v5.1.0
github.com/golang/mock v1.6.0
- github.com/google/go-cmp v0.6.0
+ github.com/google/go-cmp v0.7.0
github.com/google/gopacket v1.1.19
- github.com/google/nftables v0.2.0
+ github.com/google/nftables v0.3.0
github.com/gopacket/gopacket v1.1.1
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.2-0.20240212192251-757544f21357
github.com/hashicorp/go-multierror v1.1.1
@@ -73,9 +75,9 @@ require (
github.com/pion/stun/v2 v2.0.0
github.com/pion/transport/v3 v3.0.1
github.com/pion/turn/v3 v3.0.1
- github.com/prometheus/client_golang v1.19.1
+ github.com/prometheus/client_golang v1.22.0
github.com/quic-go/quic-go v0.48.2
- github.com/redis/go-redis/v9 v9.7.1
+ github.com/redis/go-redis/v9 v9.7.3
github.com/rs/xid v1.3.0
github.com/shirou/gopsutil/v3 v3.24.4
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
@@ -100,10 +102,10 @@ require (
goauthentik.io/api/v3 v3.2023051.3
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a
- golang.org/x/net v0.36.0
- golang.org/x/oauth2 v0.19.0
- golang.org/x/sync v0.12.0
- golang.org/x/term v0.30.0
+ golang.org/x/net v0.39.0
+ golang.org/x/oauth2 v0.24.0
+ golang.org/x/sync v0.13.0
+ golang.org/x/term v0.31.0
google.golang.org/api v0.177.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.5.7
@@ -124,20 +126,22 @@ require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Microsoft/hcsshim v0.12.3 // indirect
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
- github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect
- github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect
- github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
- github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect
- github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect
- github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect
- github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
- github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
- github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect
+ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect
+ github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect
+ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.42.3 // indirect
- github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect
- github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
- github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
- github.com/aws/smithy-go v1.20.3 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect
+ github.com/aws/smithy-go v1.22.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
@@ -145,6 +149,7 @@ require (
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker v26.1.5+incompatible // indirect
@@ -183,16 +188,15 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
- github.com/josharian/native v1.1.0 // indirect
github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect
github.com/kelseyhightower/envconfig v1.4.0 // indirect
- github.com/klauspost/compress v1.17.8 // indirect
+ github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/libdns/libdns v0.2.2 // indirect
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect
- github.com/mdlayher/netlink v1.7.2 // indirect
+ github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect
github.com/mholt/acmez/v2 v2.0.1 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
@@ -201,6 +205,7 @@ require (
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
+ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nicksnyder/go-i18n/v2 v2.4.0 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
@@ -213,8 +218,8 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
- github.com/prometheus/common v0.53.0 // indirect
- github.com/prometheus/procfs v0.15.0 // indirect
+ github.com/prometheus/common v0.62.0 // indirect
+ github.com/prometheus/procfs v0.15.1 // indirect
github.com/rymdport/portal v0.3.0 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
@@ -234,7 +239,7 @@ require (
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/image v0.18.0 // indirect
golang.org/x/mod v0.17.0 // indirect
- golang.org/x/text v0.23.0 // indirect
+ golang.org/x/text v0.24.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
diff --git a/go.sum b/go.sum
index 25891fbf9..a90db83de 100644
--- a/go.sum
+++ b/go.sum
@@ -74,34 +74,44 @@ github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kd
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
-github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY=
-github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc=
-github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90=
-github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM=
-github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
-github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII=
+github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
+github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14=
+github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM=
+github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcuz+RjeziUtNJackkM=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 h1:lguz0bmOoGzozP9XfRJR1QIayEYo+2vP/No3OfLF0pU=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91LiqT1nbvzDukyqAlCv89ZmwaHw/ZFlFZg=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA=
github.com/aws/aws-sdk-go-v2/service/route53 v1.42.3 h1:MmLCRqP4U4Cw9gJ4bNrCG0mWqEtBlmAVleyelcHARMU=
github.com/aws/aws-sdk-go-v2/service/route53 v1.42.3/go.mod h1:AMPjK2YnRh0YgOID3PqhJA1BRNfXDfGOnSsKHtAe8yA=
-github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM=
-github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw=
-github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE=
-github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
-github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=
-github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.79.2 h1:tWUG+4wZqdMl/znThEk9tcCy8tTMxq8dW0JTgamohrY=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.79.2/go.mod h1:U5SNqwhXB3Xe6F47kXvWihPl/ilGaEDe8HD/50Z9wxc=
+github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8=
+github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
+github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
+github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
@@ -292,16 +302,17 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
-github.com/google/nftables v0.2.0 h1:PbJwaBmbVLzpeldoeUKGkE2RjstrjPKMl6oLrfEJ6/8=
-github.com/google/nftables v0.2.0/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4=
+github.com/google/nftables v0.3.0 h1:bkyZ0cbpVeMHXOrtlFc8ISmfVqq5gPJukoYieyVmITg=
+github.com/google/nftables v0.3.0/go.mod h1:BCp9FsrbF1Fn/Yu6CLUc9GGZFw/+hsxfluNXXmxBfRM=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
@@ -398,8 +409,6 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
-github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
-github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
@@ -411,8 +420,8 @@ github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dv
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
-github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
+github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
@@ -424,6 +433,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
@@ -444,8 +455,8 @@ github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
-github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
-github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
+github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg=
+github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o=
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
github.com/mholt/acmez/v2 v2.0.1 h1:3/3N0u1pLjMK4sNEAFSI+bcvzbPhRpY383sy1kLHJ6k=
@@ -482,6 +493,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nadoo/ipset v0.5.0 h1:5GJUAuZ7ITQQQGne5J96AmFjRtI8Avlbk6CabzYWVUc=
github.com/nadoo/ipset v0.5.0/go.mod h1:rYF5DQLRGGoQ8ZSWeK+6eX5amAuPqwFkWjhQlEITGJQ=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
@@ -560,19 +573,19 @@ github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndr
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
-github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
-github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
+github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
+github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
-github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE=
-github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U=
-github.com/prometheus/procfs v0.15.0 h1:A82kmvXJq2jTu5YUhSGNlYoxh85zLnKgPz4bMZgI5Ek=
-github.com/prometheus/procfs v0.15.0/go.mod h1:Y0RJ/Y5g5wJpkTisOtqwDSo4HwhGmLB4VQSw2sQJLHk=
+github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
+github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
+github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
+github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE=
github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
-github.com/redis/go-redis/v9 v9.7.1 h1:4LhKRCIduqXqtvCUlaq9c8bdHOkICjDMrr1+Zb3osAc=
-github.com/redis/go-redis/v9 v9.7.1/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
+github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
+github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
@@ -660,9 +673,8 @@ github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYg
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
-github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs=
-github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
-github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
+github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
+github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
@@ -747,8 +759,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
-golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
-golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
+golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
+golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -841,8 +853,8 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
-golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
-golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
+golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
+golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -856,8 +868,8 @@ golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
-golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg=
-golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8=
+golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
+golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -871,8 +883,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
-golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
+golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -897,7 +909,6 @@ golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -906,7 +917,6 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -934,14 +944,16 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
-golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
+golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -949,8 +961,8 @@ golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
-golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
-golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
+golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
+golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -964,8 +976,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
-golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1152,8 +1164,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
-google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
+google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/management/client/grpc.go b/management/client/grpc.go
index d3aaffec0..956aaebb2 100644
--- a/management/client/grpc.go
+++ b/management/client/grpc.go
@@ -128,7 +128,13 @@ func (c *GrpcClient) Sync(ctx context.Context, sysInfo *system.Info, msgHandler
return err
}
- return c.handleStream(ctx, *serverPubKey, sysInfo, msgHandler)
+ streamErr := c.handleStream(ctx, *serverPubKey, sysInfo, msgHandler)
+ if c.conn.GetState() != connectivity.Shutdown {
+ if err := c.conn.Close(); err != nil {
+ log.Warnf("failed closing connection to Management service: %s", err)
+ }
+ }
+ return streamErr
}
err := backoff.Retry(operation, defaultBackoff(ctx))
@@ -159,6 +165,7 @@ func (c *GrpcClient) handleStream(ctx context.Context, serverPubKey wgtypes.Key,
// blocking until error
err = c.receiveEvents(stream, serverPubKey, msgHandler)
if err != nil {
+ c.notifyDisconnected(err)
s, _ := gstatus.FromError(err)
switch s.Code() {
case codes.PermissionDenied:
@@ -167,7 +174,6 @@ func (c *GrpcClient) handleStream(ctx context.Context, serverPubKey wgtypes.Key,
log.Debugf("management connection context has been canceled, this usually indicates shutdown")
return nil
default:
- c.notifyDisconnected(err)
log.Warnf("disconnected from the Management service but will retry silently. Reason: %v", err)
return err
}
@@ -258,10 +264,10 @@ func (c *GrpcClient) receiveEvents(stream proto.ManagementService_SyncClient, se
return err
}
- err = msgHandler(decryptedResp)
- if err != nil {
+ if err := msgHandler(decryptedResp); err != nil {
log.Errorf("failed handling an update message received from Management Service: %v", err.Error())
- return err
+ // hide any grpc error code that is not relevant for management
+ return fmt.Errorf("msg handler error: %v", err.Error())
}
}
}
diff --git a/management/client/rest/users.go b/management/client/rest/users.go
index 372bcee45..31ffad051 100644
--- a/management/client/rest/users.go
+++ b/management/client/rest/users.go
@@ -80,3 +80,16 @@ func (a *UsersAPI) ResendInvitation(ctx context.Context, userID string) error {
return nil
}
+
+// Current gets the current user info
+// See more: https://docs.netbird.io/api/resources/users#retrieve-current-user
+func (a *UsersAPI) Current(ctx context.Context) (*api.User, error) {
+ resp, err := a.c.newRequest(ctx, "GET", "/api/users/current", nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ ret, err := parseResponse[api.User](resp)
+ return &ret, err
+}
diff --git a/management/client/rest/users_test.go b/management/client/rest/users_test.go
index 2ff8a0327..f68c5f083 100644
--- a/management/client/rest/users_test.go
+++ b/management/client/rest/users_test.go
@@ -196,8 +196,42 @@ func TestUsers_ResendInvitation_Err(t *testing.T) {
})
}
+func TestUsers_Current_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/users/current", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(testUser)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Users.Current(context.Background())
+ require.NoError(t, err)
+ assert.Equal(t, testUser, *ret)
+ })
+}
+
+func TestUsers_Current_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/users/current", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Users.Current(context.Background())
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Empty(t, ret)
+ })
+}
+
func TestUsers_Integration(t *testing.T) {
withBlackBoxServer(t, func(c *rest.Client) {
+ // rest client PAT is owner's
+ current, err := c.Users.Current(context.Background())
+ require.NoError(t, err)
+ assert.Equal(t, "a23efe53-63fb-11ec-90d6-0242ac120003", current.Id)
+ assert.Equal(t, "owner", current.Role)
+
user, err := c.Users.Create(context.Background(), api.UserCreateRequest{
AutoGroups: []string{},
Email: ptr("test@example.com"),
diff --git a/management/domain/domain.go b/management/domain/domain.go
index 2e089b01f..97acec688 100644
--- a/management/domain/domain.go
+++ b/management/domain/domain.go
@@ -1,12 +1,17 @@
package domain
import (
+ "strings"
+
"golang.org/x/net/idna"
)
+// Domain represents a punycode-encoded domain string.
+// This should only be converted from a string when the string already is in punycode, otherwise use FromString.
type Domain string
// String converts the Domain to a non-punycode string.
+// For an infallible conversion, use SafeString.
func (d Domain) String() (string, error) {
unicode, err := idna.ToUnicode(string(d))
if err != nil {
@@ -15,16 +20,17 @@ func (d Domain) String() (string, error) {
return unicode, nil
}
-// SafeString converts the Domain to a non-punycode string, falling back to the original string if conversion fails.
+// SafeString converts the Domain to a non-punycode string, falling back to the punycode string if conversion fails.
func (d Domain) SafeString() string {
str, err := d.String()
if err != nil {
- str = string(d)
+ return string(d)
}
return str
}
// PunycodeString returns the punycode representation of the Domain.
+// This should only be used if a punycode domain is expected but only a string is supported.
func (d Domain) PunycodeString() string {
return string(d)
}
@@ -35,5 +41,5 @@ func FromString(s string) (Domain, error) {
if err != nil {
return "", err
}
- return Domain(ascii), nil
+ return Domain(strings.ToLower(ascii)), nil
}
diff --git a/management/domain/list.go b/management/domain/list.go
index b6090c717..a988f4f70 100644
--- a/management/domain/list.go
+++ b/management/domain/list.go
@@ -5,6 +5,7 @@ import (
"strings"
)
+// List is a slice of punycode-encoded domain strings.
type List []Domain
// ToStringList converts a List to a slice of string.
@@ -53,7 +54,7 @@ func (d List) String() (string, error) {
func (d List) SafeString() string {
str, err := d.String()
if err != nil {
- return strings.Join(d.ToPunycodeList(), ", ")
+ return d.PunycodeString()
}
return str
}
@@ -101,7 +102,7 @@ func FromStringList(s []string) (List, error) {
func FromPunycodeList(s []string) List {
var dl List
for _, domain := range s {
- dl = append(dl, Domain(domain))
+ dl = append(dl, Domain(strings.ToLower(domain)))
}
return dl
}
diff --git a/management/domain/validate.go b/management/domain/validate.go
index bcbf26e05..a42aebe6f 100644
--- a/management/domain/validate.go
+++ b/management/domain/validate.go
@@ -22,8 +22,6 @@ func ValidateDomains(domains []string) (List, error) {
var domainList List
for _, d := range domains {
- d := strings.ToLower(d)
-
// handles length and idna conversion
punycode, err := FromString(d)
if err != nil {
diff --git a/management/proto/management.pb.go b/management/proto/management.pb.go
index f3f53bfd4..9d7fdc682 100644
--- a/management/proto/management.pb.go
+++ b/management/proto/management.pb.go
@@ -3057,6 +3057,8 @@ type RouteFirewallRule struct {
CustomProtocol uint32 `protobuf:"varint,8,opt,name=customProtocol,proto3" json:"customProtocol,omitempty"`
// PolicyID is the ID of the policy that this rule belongs to
PolicyID []byte `protobuf:"bytes,9,opt,name=PolicyID,proto3" json:"PolicyID,omitempty"`
+ // RouteID is the ID of the route that this rule belongs to
+ RouteID string `protobuf:"bytes,10,opt,name=RouteID,proto3" json:"RouteID,omitempty"`
}
func (x *RouteFirewallRule) Reset() {
@@ -3154,6 +3156,13 @@ func (x *RouteFirewallRule) GetPolicyID() []byte {
return nil
}
+func (x *RouteFirewallRule) GetRouteID() string {
+ if x != nil {
+ return x.RouteID
+ }
+ return ""
+}
+
type ForwardingRule struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -3702,7 +3711,7 @@ var file_management_proto_rawDesc = []byte{
0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52,
0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20,
0x01, 0x28, 0x0d, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x42, 0x0f, 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74,
- 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xed, 0x02, 0x0a, 0x11, 0x52, 0x6f,
+ 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x87, 0x03, 0x0a, 0x11, 0x52, 0x6f,
0x75, 0x74, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12,
0x22, 0x0a, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18,
0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e,
@@ -3725,66 +3734,68 @@ var file_management_proto_rawDesc = []byte{
0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x63,
0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x1a, 0x0a,
0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52,
- 0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x22, 0xf2, 0x01, 0x0a, 0x0e, 0x46, 0x6f,
- 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x34, 0x0a, 0x08,
- 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18,
- 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65,
- 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63,
- 0x6f, 0x6c, 0x12, 0x3e, 0x0a, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f,
- 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61,
- 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66,
- 0x6f, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f,
- 0x72, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64,
- 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x11, 0x74,
- 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73,
- 0x12, 0x3c, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f,
- 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
- 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0e,
- 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x2a, 0x4c,
- 0x0a, 0x0c, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0b,
- 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x41,
- 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, 0x07, 0x0a,
- 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x43, 0x4d, 0x50, 0x10, 0x04,
- 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55, 0x53, 0x54, 0x4f, 0x4d, 0x10, 0x05, 0x2a, 0x20, 0x0a, 0x0d,
- 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x0a,
- 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x2a, 0x22,
- 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06,
- 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52, 0x4f, 0x50,
- 0x10, 0x01, 0x32, 0x90, 0x04, 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e,
- 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69,
- 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45,
+ 0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x52, 0x6f, 0x75,
+ 0x74, 0x65, 0x49, 0x44, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x52, 0x6f, 0x75, 0x74,
+ 0x65, 0x49, 0x44, 0x22, 0xf2, 0x01, 0x0a, 0x0e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69,
+ 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x34, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63,
+ 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
+ 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63,
+ 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x3e, 0x0a, 0x0f,
+ 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x18,
+ 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
+ 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0f, 0x64, 0x65, 0x73,
+ 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x2c, 0x0a, 0x11,
+ 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73,
+ 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61,
+ 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x3c, 0x0a, 0x0e, 0x74, 0x72,
+ 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01,
+ 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
+ 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c,
+ 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x2a, 0x4c, 0x0a, 0x0c, 0x52, 0x75, 0x6c, 0x65,
+ 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e,
+ 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07,
+ 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03,
+ 0x12, 0x08, 0x0a, 0x04, 0x49, 0x43, 0x4d, 0x50, 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55,
+ 0x53, 0x54, 0x4f, 0x4d, 0x10, 0x05, 0x2a, 0x20, 0x0a, 0x0d, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69,
+ 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12,
+ 0x07, 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x2a, 0x22, 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65,
+ 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54,
+ 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x32, 0x90, 0x04, 0x0a,
+ 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69,
+ 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61,
+ 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74,
+ 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
+ 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64,
+ 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e,
+ 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45,
0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a,
0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63,
- 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12,
- 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
- 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65,
- 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
- 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73,
- 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x65,
- 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
- 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61, 0x6e,
- 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65,
- 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09, 0x69,
- 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
- 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d, 0x61,
- 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00,
- 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74,
- 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c,
- 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72,
- 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d,
- 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70,
- 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x18,
- 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61,
+ 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30,
+ 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65,
+ 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45,
+ 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e,
+ 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f,
+ 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74,
+ 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
+ 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
+ 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65,
+ 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d,
0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73,
- 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x08, 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x65,
- 0x74, 0x61, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
+ 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43,
+ 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c,
+ 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
- 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d,
- 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
- 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+ 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e,
+ 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00,
+ 0x12, 0x3d, 0x0a, 0x08, 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1c, 0x2e, 0x6d,
+ 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70,
+ 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e,
+ 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42,
+ 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+ 0x33,
}
var (
diff --git a/management/proto/management.proto b/management/proto/management.proto
index 0f1cdb97a..f0dc16ce2 100644
--- a/management/proto/management.proto
+++ b/management/proto/management.proto
@@ -509,6 +509,9 @@ message RouteFirewallRule {
// PolicyID is the ID of the policy that this rule belongs to
bytes PolicyID = 9;
+
+ // RouteID is the ID of the route that this rule belongs to
+ string RouteID = 10;
}
message ForwardingRule {
diff --git a/management/server/account.go b/management/server/account.go
index 1627959d2..ab1ffe8b3 100644
--- a/management/server/account.go
+++ b/management/server/account.go
@@ -17,6 +17,7 @@ import (
"time"
cacheStore "github.com/eko/gocache/lib/v4/store"
+ "github.com/eko/gocache/store/redis/v4"
"github.com/rs/xid"
log "github.com/sirupsen/logrus"
"github.com/vmihailenco/msgpack/v5"
@@ -237,7 +238,7 @@ func BuildManager(
if !isNil(am.idpManager) {
go func() {
- err := am.warmupIDPCache(ctx)
+ err := am.warmupIDPCache(ctx, cacheStore)
if err != nil {
log.WithContext(ctx).Warnf("failed warming up cache due to error: %v", err)
// todo retry?
@@ -275,6 +276,10 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco
return nil, status.Errorf(status.InvalidArgument, "peer login expiration can't be smaller than one hour")
}
+ if newSettings.DNSDomain != "" && !isDomainValid(newSettings.DNSDomain) {
+ return nil, status.Errorf(status.InvalidArgument, "invalid domain \"%s\" provided for DNS domain", newSettings.DNSDomain)
+ }
+
unlock := am.Store.AcquireWriteLockByUID(ctx, accountID)
defer unlock()
@@ -283,7 +288,7 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco
return nil, err
}
- allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Settings, operations.Write)
+ allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Settings, operations.Update)
if err != nil {
return nil, fmt.Errorf("failed to validate user permissions: %w", err)
}
@@ -325,6 +330,12 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco
account.Network.Serial++
}
+ if oldSettings.DNSDomain != newSettings.DNSDomain {
+ am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountDNSDomainUpdated, nil)
+ updateAccountPeers = true
+ account.Network.Serial++
+ }
+
err = am.handleInactivityExpirationSettings(ctx, oldSettings, newSettings, userID, accountID)
if err != nil {
return nil, err
@@ -484,7 +495,25 @@ func (am *DefaultAccountManager) newAccount(ctx context.Context, userID, domain
return nil, status.Errorf(status.Internal, "error while creating new account")
}
-func (am *DefaultAccountManager) warmupIDPCache(ctx context.Context) error {
+func (am *DefaultAccountManager) warmupIDPCache(ctx context.Context, store cacheStore.StoreInterface) error {
+ cold, err := am.isCacheCold(ctx, store)
+ if err != nil {
+ return err
+ }
+
+ if !cold {
+ log.WithContext(ctx).Debug("cache already populated, skipping warm up")
+ return nil
+ }
+
+ if delayStr, ok := os.LookupEnv("NB_IDP_CACHE_WARMUP_DELAY"); ok {
+ delay, err := time.ParseDuration(delayStr)
+ if err != nil {
+ return fmt.Errorf("invalid IDP warmup delay: %w", err)
+ }
+ time.Sleep(delay)
+ }
+
userData, err := am.idpManager.GetAllAccounts(ctx)
if err != nil {
return err
@@ -524,6 +553,32 @@ func (am *DefaultAccountManager) warmupIDPCache(ctx context.Context) error {
return nil
}
+// isCacheCold checks if the cache needs warming up.
+func (am *DefaultAccountManager) isCacheCold(ctx context.Context, store cacheStore.StoreInterface) (bool, error) {
+ if store.GetType() != redis.RedisType {
+ return true, nil
+ }
+
+ accountID, err := am.Store.GetAnyAccountID(ctx)
+ if err != nil {
+ if sErr, ok := status.FromError(err); ok && sErr.Type() == status.NotFound {
+ return true, nil
+ }
+ return false, err
+ }
+
+ _, err = store.Get(ctx, accountID)
+ if err == nil {
+ return false, nil
+ }
+
+ if notFoundErr := new(cacheStore.NotFound); errors.As(err, ¬FoundErr) {
+ return true, nil
+ }
+
+ return false, fmt.Errorf("failed to check cache: %w", err)
+}
+
// DeleteAccount deletes an account and all its users from local store and from the remote IDP if the requester is an admin and account owner
func (am *DefaultAccountManager) DeleteAccount(ctx context.Context, accountID, userID string) error {
unlock := am.Store.AcquireWriteLockByUID(ctx, accountID)
@@ -533,7 +588,7 @@ func (am *DefaultAccountManager) DeleteAccount(ctx context.Context, accountID, u
return err
}
- allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Accounts, operations.Write)
+ allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Accounts, operations.Delete)
if err != nil {
return fmt.Errorf("failed to validate user permissions: %w", err)
}
@@ -1057,6 +1112,19 @@ func (am *DefaultAccountManager) GetAccountByID(ctx context.Context, accountID s
return am.Store.GetAccount(ctx, accountID)
}
+// GetAccountMeta returns the account metadata associated with this account ID.
+func (am *DefaultAccountManager) GetAccountMeta(ctx context.Context, accountID string, userID string) (*types.AccountMeta, error) {
+ allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Accounts, operations.Read)
+ if err != nil {
+ return nil, status.NewPermissionValidationError(err)
+ }
+ if !allowed {
+ return nil, status.NewPermissionDeniedError()
+ }
+
+ return am.Store.GetAccountMeta(ctx, store.LockingStrengthShare, accountID)
+}
+
func (am *DefaultAccountManager) GetAccountIDFromUserAuth(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error) {
if userAuth.UserId == "" {
return "", "", errors.New(emptyUserID)
@@ -1480,8 +1548,15 @@ func isDomainValid(domain string) bool {
}
// GetDNSDomain returns the configured dnsDomain
-func (am *DefaultAccountManager) GetDNSDomain() string {
- return am.dnsDomain
+func (am *DefaultAccountManager) GetDNSDomain(settings *types.Settings) string {
+ if settings == nil {
+ return am.dnsDomain
+ }
+ if settings.DNSDomain == "" {
+ return am.dnsDomain
+ }
+
+ return settings.DNSDomain
}
func (am *DefaultAccountManager) onPeersInvalidated(ctx context.Context, accountID string) {
diff --git a/management/server/account/manager.go b/management/server/account/manager.go
index 807d05067..aed83349f 100644
--- a/management/server/account/manager.go
+++ b/management/server/account/manager.go
@@ -37,6 +37,7 @@ type Manager interface {
SaveOrAddUsers(ctx context.Context, accountID, initiatorUserID string, updates []*types.User, addIfNotExists bool) ([]*types.UserInfo, error)
GetSetupKey(ctx context.Context, accountID, userID, keyID string) (*types.SetupKey, error)
GetAccountByID(ctx context.Context, accountID string, userID string) (*types.Account, error)
+ GetAccountMeta(ctx context.Context, accountID string, userID string) (*types.AccountMeta, error)
AccountExists(ctx context.Context, accountID string) (bool, error)
GetAccountIDByUserID(ctx context.Context, userID, domain string) (string, error)
GetAccountIDFromUserAuth(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error)
@@ -59,15 +60,15 @@ type Manager interface {
GetGroup(ctx context.Context, accountId, groupID, userID string) (*types.Group, error)
GetAllGroups(ctx context.Context, accountID, userID string) ([]*types.Group, error)
GetGroupByName(ctx context.Context, groupName, accountID string) (*types.Group, error)
- SaveGroup(ctx context.Context, accountID, userID string, group *types.Group) error
- SaveGroups(ctx context.Context, accountID, userID string, newGroups []*types.Group) error
+ SaveGroup(ctx context.Context, accountID, userID string, group *types.Group, create bool) error
+ SaveGroups(ctx context.Context, accountID, userID string, newGroups []*types.Group, create bool) error
DeleteGroup(ctx context.Context, accountId, userId, groupID string) error
DeleteGroups(ctx context.Context, accountId, userId string, groupIDs []string) error
GroupAddPeer(ctx context.Context, accountId, groupID, peerID string) error
GroupDeletePeer(ctx context.Context, accountId, groupID, peerID string) error
GetPeerGroups(ctx context.Context, accountID, peerID string) ([]*types.Group, error)
GetPolicy(ctx context.Context, accountID, policyID, userID string) (*types.Policy, error)
- SavePolicy(ctx context.Context, accountID, userID string, policy *types.Policy) (*types.Policy, error)
+ SavePolicy(ctx context.Context, accountID, userID string, policy *types.Policy, create bool) (*types.Policy, error)
DeletePolicy(ctx context.Context, accountID, policyID, userID string) error
ListPolicies(ctx context.Context, accountID, userID string) ([]*types.Policy, error)
GetRoute(ctx context.Context, accountID string, routeID route.ID, userID string) (*route.Route, error)
@@ -80,7 +81,7 @@ type Manager interface {
SaveNameServerGroup(ctx context.Context, accountID, userID string, nsGroupToSave *nbdns.NameServerGroup) error
DeleteNameServerGroup(ctx context.Context, accountID, nsGroupID, userID string) error
ListNameServerGroups(ctx context.Context, accountID string, userID string) ([]*nbdns.NameServerGroup, error)
- GetDNSDomain() string
+ GetDNSDomain(settings *types.Settings) string
StoreEvent(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any)
GetEvents(ctx context.Context, accountID, userID string) ([]*activity.Event, error)
GetDNSSettings(ctx context.Context, accountID string, userID string) (*types.DNSSettings, error)
@@ -93,7 +94,7 @@ type Manager interface {
HasConnectedChannel(peerID string) bool
GetExternalCacheManager() ExternalCacheManager
GetPostureChecks(ctx context.Context, accountID, postureChecksID, userID string) (*posture.Checks, error)
- SavePostureChecks(ctx context.Context, accountID, userID string, postureChecks *posture.Checks) (*posture.Checks, error)
+ SavePostureChecks(ctx context.Context, accountID, userID string, postureChecks *posture.Checks, create bool) (*posture.Checks, error)
DeletePostureChecks(ctx context.Context, accountID, postureChecksID, userID string) error
ListPostureChecks(ctx context.Context, accountID, userID string) ([]*posture.Checks, error)
GetIdpManager() idp.Manager
@@ -114,4 +115,5 @@ type Manager interface {
CreateAccountByPrivateDomain(ctx context.Context, initiatorId, domain string) (*types.Account, error)
UpdateToPrimaryAccount(ctx context.Context, accountId string) (*types.Account, error)
GetOwnerInfo(ctx context.Context, accountId string) (*types.UserInfo, error)
+ GetCurrentUserInfo(ctx context.Context, accountID, userID string) (*types.UserInfo, error)
}
diff --git a/management/server/account_test.go b/management/server/account_test.go
index cf4523e70..fe082d9a0 100644
--- a/management/server/account_test.go
+++ b/management/server/account_test.go
@@ -14,30 +14,30 @@ import (
"time"
"github.com/golang/mock/gomock"
-
- nbAccount "github.com/netbirdio/netbird/management/server/account"
- "github.com/netbirdio/netbird/management/server/integrations/port_forwarding"
- "github.com/netbirdio/netbird/management/server/permissions"
- "github.com/netbirdio/netbird/management/server/settings"
- "github.com/netbirdio/netbird/management/server/util"
-
- resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
- routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
- networkTypes "github.com/netbirdio/netbird/management/server/networks/types"
-
+ "github.com/netbirdio/netbird/management/server/idp"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
nbdns "github.com/netbirdio/netbird/dns"
+ nbAccount "github.com/netbirdio/netbird/management/server/account"
"github.com/netbirdio/netbird/management/server/activity"
+ "github.com/netbirdio/netbird/management/server/cache"
nbcontext "github.com/netbirdio/netbird/management/server/context"
+ "github.com/netbirdio/netbird/management/server/integrations/port_forwarding"
+ resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
+ routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
+ networkTypes "github.com/netbirdio/netbird/management/server/networks/types"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
+ "github.com/netbirdio/netbird/management/server/permissions"
"github.com/netbirdio/netbird/management/server/posture"
+ "github.com/netbirdio/netbird/management/server/settings"
"github.com/netbirdio/netbird/management/server/store"
"github.com/netbirdio/netbird/management/server/telemetry"
+ "github.com/netbirdio/netbird/management/server/testutil"
"github.com/netbirdio/netbird/management/server/types"
+ "github.com/netbirdio/netbird/management/server/util"
"github.com/netbirdio/netbird/route"
)
@@ -1115,7 +1115,7 @@ func TestAccountManager_NetworkUpdates_SaveGroup(t *testing.T) {
Name: "GroupA",
Peers: []string{},
}
- if err := manager.SaveGroup(context.Background(), account.Id, userID, &group); err != nil {
+ if err := manager.SaveGroup(context.Background(), account.Id, userID, &group, true); err != nil {
t.Errorf("save group: %v", err)
return
}
@@ -1131,7 +1131,7 @@ func TestAccountManager_NetworkUpdates_SaveGroup(t *testing.T) {
Action: types.PolicyTrafficActionAccept,
},
},
- })
+ }, true)
require.NoError(t, err)
updMsg := manager.peersUpdateManager.CreateChannel(context.Background(), peer1.ID)
@@ -1150,7 +1150,7 @@ func TestAccountManager_NetworkUpdates_SaveGroup(t *testing.T) {
}()
group.Peers = []string{peer1.ID, peer2.ID, peer3.ID}
- if err := manager.SaveGroup(context.Background(), account.Id, userID, &group); err != nil {
+ if err := manager.SaveGroup(context.Background(), account.Id, userID, &group, true); err != nil {
t.Errorf("save group: %v", err)
return
}
@@ -1192,7 +1192,7 @@ func TestAccountManager_NetworkUpdates_SavePolicy(t *testing.T) {
Name: "GroupA",
Peers: []string{peer1.ID, peer2.ID},
}
- if err := manager.SaveGroup(context.Background(), account.Id, userID, &group); err != nil {
+ if err := manager.SaveGroup(context.Background(), account.Id, userID, &group, true); err != nil {
t.Errorf("save group: %v", err)
return
}
@@ -1223,7 +1223,7 @@ func TestAccountManager_NetworkUpdates_SavePolicy(t *testing.T) {
Action: types.PolicyTrafficActionAccept,
},
},
- })
+ }, true)
if err != nil {
t.Errorf("delete default rule: %v", err)
return
@@ -1240,7 +1240,7 @@ func TestAccountManager_NetworkUpdates_DeletePeer(t *testing.T) {
Name: "GroupA",
Peers: []string{peer1.ID, peer3.ID},
}
- if err := manager.SaveGroup(context.Background(), account.Id, userID, &group); err != nil {
+ if err := manager.SaveGroup(context.Background(), account.Id, userID, &group, true); err != nil {
t.Errorf("save group: %v", err)
return
}
@@ -1256,7 +1256,7 @@ func TestAccountManager_NetworkUpdates_DeletePeer(t *testing.T) {
Action: types.PolicyTrafficActionAccept,
},
},
- })
+ }, true)
if err != nil {
t.Errorf("save policy: %v", err)
return
@@ -1295,7 +1295,7 @@ func TestAccountManager_NetworkUpdates_DeleteGroup(t *testing.T) {
ID: "groupA",
Name: "GroupA",
Peers: []string{peer1.ID, peer2.ID, peer3.ID},
- })
+ }, true)
require.NoError(t, err, "failed to save group")
@@ -1315,7 +1315,7 @@ func TestAccountManager_NetworkUpdates_DeleteGroup(t *testing.T) {
Action: types.PolicyTrafficActionAccept,
},
},
- })
+ }, true)
if err != nil {
t.Errorf("save policy: %v", err)
return
@@ -3201,3 +3201,53 @@ func Test_UpdateToPrimaryAccount(t *testing.T) {
assert.NoError(t, err)
assert.True(t, account.IsDomainPrimaryAccount)
}
+
+func TestDefaultAccountManager_IsCacheCold(t *testing.T) {
+ manager, err := createManager(t)
+ require.NoError(t, err)
+
+ t.Run("memory cache", func(t *testing.T) {
+ t.Run("should always return true", func(t *testing.T) {
+ cacheStore, err := cache.NewStore(context.Background(), 100*time.Millisecond, 300*time.Millisecond)
+ require.NoError(t, err)
+
+ cold, err := manager.isCacheCold(context.Background(), cacheStore)
+ assert.NoError(t, err)
+ assert.True(t, cold)
+ })
+ })
+
+ t.Run("redis cache", func(t *testing.T) {
+ cleanup, redisURL, err := testutil.CreateRedisTestContainer()
+ require.NoError(t, err)
+ t.Cleanup(cleanup)
+ t.Setenv(cache.RedisStoreEnvVar, redisURL)
+
+ cacheStore, err := cache.NewStore(context.Background(), 100*time.Millisecond, 300*time.Millisecond)
+ require.NoError(t, err)
+
+ t.Run("should return true when no account exists", func(t *testing.T) {
+ cold, err := manager.isCacheCold(context.Background(), cacheStore)
+ assert.NoError(t, err)
+ assert.True(t, cold)
+ })
+
+ account, err := manager.GetOrCreateAccountByUser(context.Background(), userID, "")
+ require.NoError(t, err)
+
+ t.Run("should return true when account is not found in cache", func(t *testing.T) {
+ cold, err := manager.isCacheCold(context.Background(), cacheStore)
+ assert.NoError(t, err)
+ assert.True(t, cold)
+ })
+
+ t.Run("should return false when account is found in cache", func(t *testing.T) {
+ err = cacheStore.Set(context.Background(), account.Id, &idp.UserData{ID: "v", Name: "vv"})
+ require.NoError(t, err)
+
+ cold, err := manager.isCacheCold(context.Background(), cacheStore)
+ assert.NoError(t, err)
+ assert.False(t, cold)
+ })
+ })
+}
diff --git a/management/server/activity/codes.go b/management/server/activity/codes.go
index 46ae754cf..ed4be82e2 100644
--- a/management/server/activity/codes.go
+++ b/management/server/activity/codes.go
@@ -169,6 +169,8 @@ const (
ResourceAddedToGroup Activity = 82
ResourceRemovedFromGroup Activity = 83
+
+ AccountDNSDomainUpdated Activity = 84
)
var activityMap = map[Activity]Code{
@@ -264,6 +266,8 @@ var activityMap = map[Activity]Code{
ResourceAddedToGroup: {"Resource added to group", "resource.group.add"},
ResourceRemovedFromGroup: {"Resource removed from group", "resource.group.delete"},
+
+ AccountDNSDomainUpdated: {"Account DNS domain updated", "account.dns.domain.update"},
}
// StringCode returns a string code of the activity
diff --git a/management/server/cache/idp_test.go b/management/server/cache/idp_test.go
index beefcd9bd..3fcfbb11a 100644
--- a/management/server/cache/idp_test.go
+++ b/management/server/cache/idp_test.go
@@ -8,12 +8,11 @@ import (
"github.com/eko/gocache/lib/v4/store"
"github.com/redis/go-redis/v9"
- "github.com/testcontainers/testcontainers-go"
- testcontainersredis "github.com/testcontainers/testcontainers-go/modules/redis"
"github.com/vmihailenco/msgpack/v5"
"github.com/netbirdio/netbird/management/server/cache"
"github.com/netbirdio/netbird/management/server/idp"
+ "github.com/netbirdio/netbird/management/server/testutil"
)
func TestNewIDPCacheManagers(t *testing.T) {
@@ -27,21 +26,11 @@ func TestNewIDPCacheManagers(t *testing.T) {
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
if tc.redis {
- ctx := context.Background()
- redisContainer, err := testcontainersredis.RunContainer(ctx, testcontainers.WithImage("redis:7"))
+ cleanup, redisURL, err := testutil.CreateRedisTestContainer()
if err != nil {
t.Fatalf("couldn't start redis container: %s", err)
}
- defer func() {
- if err := redisContainer.Terminate(ctx); err != nil {
- t.Logf("failed to terminate container: %s", err)
- }
- }()
- redisURL, err := redisContainer.ConnectionString(ctx)
- if err != nil {
- t.Fatalf("couldn't get connection string: %s", err)
- }
-
+ t.Cleanup(cleanup)
t.Setenv(cache.RedisStoreEnvVar, redisURL)
}
cacheStore, err := cache.NewStore(context.Background(), cache.DefaultIDPCacheExpirationMax, cache.DefaultIDPCacheCleanupInterval)
diff --git a/management/server/dns.go b/management/server/dns.go
index d457db773..a3f32c2a9 100644
--- a/management/server/dns.go
+++ b/management/server/dns.go
@@ -81,7 +81,7 @@ func (am *DefaultAccountManager) SaveDNSSettings(ctx context.Context, accountID
return status.Errorf(status.InvalidArgument, "the dns settings provided are nil")
}
- allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Write)
+ allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Update)
if err != nil {
return status.NewPermissionValidationError(err)
}
diff --git a/management/server/dns_test.go b/management/server/dns_test.go
index 8a0e0cd02..36476b14c 100644
--- a/management/server/dns_test.go
+++ b/management/server/dns_test.go
@@ -504,7 +504,7 @@ func TestDNSAccountPeersUpdate(t *testing.T) {
Name: "GroupB",
Peers: []string{},
},
- })
+ }, true)
assert.NoError(t, err)
updMsg := manager.peersUpdateManager.CreateChannel(context.Background(), peer1.ID)
@@ -564,7 +564,7 @@ func TestDNSAccountPeersUpdate(t *testing.T) {
ID: "groupA",
Name: "GroupA",
Peers: []string{peer1.ID, peer2.ID, peer3.ID},
- })
+ }, true)
assert.NoError(t, err)
done := make(chan struct{})
diff --git a/management/server/group.go b/management/server/group.go
index c102cedb8..87d649228 100644
--- a/management/server/group.go
+++ b/management/server/group.go
@@ -66,17 +66,21 @@ func (am *DefaultAccountManager) GetGroupByName(ctx context.Context, groupName,
}
// SaveGroup object of the peers
-func (am *DefaultAccountManager) SaveGroup(ctx context.Context, accountID, userID string, newGroup *types.Group) error {
+func (am *DefaultAccountManager) SaveGroup(ctx context.Context, accountID, userID string, newGroup *types.Group, create bool) error {
unlock := am.Store.AcquireWriteLockByUID(ctx, accountID)
defer unlock()
- return am.SaveGroups(ctx, accountID, userID, []*types.Group{newGroup})
+ return am.SaveGroups(ctx, accountID, userID, []*types.Group{newGroup}, create)
}
// SaveGroups adds new groups to the account.
// Note: This function does not acquire the global lock.
// It is the caller's responsibility to ensure proper locking is in place before invoking this method.
-func (am *DefaultAccountManager) SaveGroups(ctx context.Context, accountID, userID string, groups []*types.Group) error {
- allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Groups, operations.Write)
+func (am *DefaultAccountManager) SaveGroups(ctx context.Context, accountID, userID string, groups []*types.Group, create bool) error {
+ operation := operations.Create
+ if !create {
+ operation = operations.Update
+ }
+ allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Groups, operation)
if err != nil {
return status.NewPermissionValidationError(err)
}
@@ -154,6 +158,13 @@ func (am *DefaultAccountManager) prepareGroupEvents(ctx context.Context, transac
return nil
}
+ settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
+ if err != nil {
+ log.WithContext(ctx).Debugf("failed to get account settings for group events: %v", err)
+ return nil
+ }
+ dnsDomain := am.GetDNSDomain(settings)
+
for _, peerID := range addedPeers {
peer, ok := peers[peerID]
if !ok {
@@ -164,7 +175,7 @@ func (am *DefaultAccountManager) prepareGroupEvents(ctx context.Context, transac
eventsToStore = append(eventsToStore, func() {
meta := map[string]any{
"group": newGroup.Name, "group_id": newGroup.ID,
- "peer_ip": peer.IP.String(), "peer_fqdn": peer.FQDN(am.GetDNSDomain()),
+ "peer_ip": peer.IP.String(), "peer_fqdn": peer.FQDN(dnsDomain),
}
am.StoreEvent(ctx, userID, peer.ID, accountID, activity.GroupAddedToPeer, meta)
})
@@ -180,7 +191,7 @@ func (am *DefaultAccountManager) prepareGroupEvents(ctx context.Context, transac
eventsToStore = append(eventsToStore, func() {
meta := map[string]any{
"group": newGroup.Name, "group_id": newGroup.ID,
- "peer_ip": peer.IP.String(), "peer_fqdn": peer.FQDN(am.GetDNSDomain()),
+ "peer_ip": peer.IP.String(), "peer_fqdn": peer.FQDN(dnsDomain),
}
am.StoreEvent(ctx, userID, peer.ID, accountID, activity.GroupRemovedFromPeer, meta)
})
@@ -203,7 +214,7 @@ func (am *DefaultAccountManager) DeleteGroup(ctx context.Context, accountID, use
// If an error occurs while deleting a group, the function skips it and continues deleting other groups.
// Errors are collected and returned at the end.
func (am *DefaultAccountManager) DeleteGroups(ctx context.Context, accountID, userID string, groupIDs []string) error {
- allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Groups, operations.Write)
+ allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Groups, operations.Delete)
if err != nil {
return status.NewPermissionValidationError(err)
}
diff --git a/management/server/group_test.go b/management/server/group_test.go
index dffaa80e3..4966f2b33 100644
--- a/management/server/group_test.go
+++ b/management/server/group_test.go
@@ -40,7 +40,7 @@ func TestDefaultAccountManager_CreateGroup(t *testing.T) {
}
for _, group := range account.Groups {
group.Issued = types.GroupIssuedIntegration
- err = am.SaveGroup(context.Background(), account.Id, groupAdminUserID, group)
+ err = am.SaveGroup(context.Background(), account.Id, groupAdminUserID, group, true)
if err != nil {
t.Errorf("should allow to create %s groups", types.GroupIssuedIntegration)
}
@@ -48,7 +48,7 @@ func TestDefaultAccountManager_CreateGroup(t *testing.T) {
for _, group := range account.Groups {
group.Issued = types.GroupIssuedJWT
- err = am.SaveGroup(context.Background(), account.Id, groupAdminUserID, group)
+ err = am.SaveGroup(context.Background(), account.Id, groupAdminUserID, group, true)
if err != nil {
t.Errorf("should allow to create %s groups", types.GroupIssuedJWT)
}
@@ -56,7 +56,7 @@ func TestDefaultAccountManager_CreateGroup(t *testing.T) {
for _, group := range account.Groups {
group.Issued = types.GroupIssuedAPI
group.ID = ""
- err = am.SaveGroup(context.Background(), account.Id, groupAdminUserID, group)
+ err = am.SaveGroup(context.Background(), account.Id, groupAdminUserID, group, true)
if err == nil {
t.Errorf("should not create api group with the same name, %s", group.Name)
}
@@ -162,7 +162,7 @@ func TestDefaultAccountManager_DeleteGroups(t *testing.T) {
}
}
- err = manager.SaveGroups(context.Background(), account.Id, groupAdminUserID, groups)
+ err = manager.SaveGroups(context.Background(), account.Id, groupAdminUserID, groups, true)
assert.NoError(t, err, "Failed to save test groups")
testCases := []struct {
@@ -382,13 +382,13 @@ func initTestGroupAccount(am *DefaultAccountManager) (*DefaultAccountManager, *t
return nil, nil, err
}
- _ = am.SaveGroup(context.Background(), accountID, groupAdminUserID, groupForRoute)
- _ = am.SaveGroup(context.Background(), accountID, groupAdminUserID, groupForRoute2)
- _ = am.SaveGroup(context.Background(), accountID, groupAdminUserID, groupForNameServerGroups)
- _ = am.SaveGroup(context.Background(), accountID, groupAdminUserID, groupForPolicies)
- _ = am.SaveGroup(context.Background(), accountID, groupAdminUserID, groupForSetupKeys)
- _ = am.SaveGroup(context.Background(), accountID, groupAdminUserID, groupForUsers)
- _ = am.SaveGroup(context.Background(), accountID, groupAdminUserID, groupForIntegration)
+ _ = am.SaveGroup(context.Background(), accountID, groupAdminUserID, groupForRoute, true)
+ _ = am.SaveGroup(context.Background(), accountID, groupAdminUserID, groupForRoute2, true)
+ _ = am.SaveGroup(context.Background(), accountID, groupAdminUserID, groupForNameServerGroups, true)
+ _ = am.SaveGroup(context.Background(), accountID, groupAdminUserID, groupForPolicies, true)
+ _ = am.SaveGroup(context.Background(), accountID, groupAdminUserID, groupForSetupKeys, true)
+ _ = am.SaveGroup(context.Background(), accountID, groupAdminUserID, groupForUsers, true)
+ _ = am.SaveGroup(context.Background(), accountID, groupAdminUserID, groupForIntegration, true)
acc, err := am.Store.GetAccount(context.Background(), account.Id)
if err != nil {
@@ -426,7 +426,7 @@ func TestGroupAccountPeersUpdate(t *testing.T) {
Name: "GroupE",
Peers: []string{peer2.ID},
},
- })
+ }, true)
assert.NoError(t, err)
updMsg := manager.peersUpdateManager.CreateChannel(context.Background(), peer1.ID)
@@ -446,7 +446,7 @@ func TestGroupAccountPeersUpdate(t *testing.T) {
ID: "groupB",
Name: "GroupB",
Peers: []string{peer1.ID, peer2.ID},
- })
+ }, true)
assert.NoError(t, err)
select {
@@ -524,7 +524,7 @@ func TestGroupAccountPeersUpdate(t *testing.T) {
Action: types.PolicyTrafficActionAccept,
},
},
- })
+ }, true)
assert.NoError(t, err)
// Saving a group linked to policy should update account peers and send peer update
@@ -539,7 +539,7 @@ func TestGroupAccountPeersUpdate(t *testing.T) {
ID: "groupA",
Name: "GroupA",
Peers: []string{peer1.ID, peer2.ID},
- })
+ }, true)
assert.NoError(t, err)
select {
@@ -608,7 +608,7 @@ func TestGroupAccountPeersUpdate(t *testing.T) {
ID: "groupC",
Name: "GroupC",
Peers: []string{peer1.ID, peer3.ID},
- })
+ }, true)
assert.NoError(t, err)
select {
@@ -649,7 +649,7 @@ func TestGroupAccountPeersUpdate(t *testing.T) {
ID: "groupA",
Name: "GroupA",
Peers: []string{peer1.ID, peer2.ID, peer3.ID},
- })
+ }, true)
assert.NoError(t, err)
select {
@@ -676,7 +676,7 @@ func TestGroupAccountPeersUpdate(t *testing.T) {
ID: "groupD",
Name: "GroupD",
Peers: []string{peer1.ID},
- })
+ }, true)
assert.NoError(t, err)
select {
@@ -723,7 +723,7 @@ func TestGroupAccountPeersUpdate(t *testing.T) {
ID: "groupE",
Name: "GroupE",
Peers: []string{peer2.ID, peer3.ID},
- })
+ }, true)
assert.NoError(t, err)
select {
diff --git a/management/server/groups/manager.go b/management/server/groups/manager.go
index 48e28d4f8..df4b6c3d6 100644
--- a/management/server/groups/manager.go
+++ b/management/server/groups/manager.go
@@ -72,7 +72,7 @@ func (m *managerImpl) GetAllGroupsMap(ctx context.Context, accountID, userID str
}
func (m *managerImpl) AddResourceToGroup(ctx context.Context, accountID, userID, groupID string, resource *types.Resource) error {
- ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Groups, operations.Write)
+ ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Groups, operations.Update)
if err != nil {
return err
}
diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go
index a7ed639c3..43d35f643 100644
--- a/management/server/grpcserver.go
+++ b/management/server/grpcserver.go
@@ -480,20 +480,12 @@ func (s *GRPCServer) Login(ctx context.Context, req *proto.EncryptedMessage) (*p
s.ephemeralManager.OnPeerDisconnected(ctx, peer)
}
- var relayToken *Token
- if s.config.Relay != nil && len(s.config.Relay.Addresses) > 0 {
- relayToken, err = s.secretsManager.GenerateRelayToken()
- if err != nil {
- log.Errorf("failed generating Relay token: %v", err)
- }
+ loginResp, err := s.prepareLoginResponse(ctx, peer, netMap, postureChecks)
+ if err != nil {
+ log.WithContext(ctx).Warnf("failed preparing login response for peer %s: %s", peerKey, err)
+ return nil, status.Errorf(codes.Internal, "failed logging in peer")
}
- // if peer has reached this point then it has logged in
- loginResp := &proto.LoginResponse{
- NetbirdConfig: toNetbirdConfig(s.config, nil, relayToken, nil),
- PeerConfig: toPeerConfig(peer, netMap.Network, s.accountManager.GetDNSDomain(), false),
- Checks: toProtocolChecks(ctx, postureChecks),
- }
encryptedResp, err := encryption.EncryptMessage(peerKey, s.wgKey, loginResp)
if err != nil {
log.WithContext(ctx).Warnf("failed encrypting peer %s message", peer.ID)
@@ -506,6 +498,32 @@ func (s *GRPCServer) Login(ctx context.Context, req *proto.EncryptedMessage) (*p
}, nil
}
+func (s *GRPCServer) prepareLoginResponse(ctx context.Context, peer *nbpeer.Peer, netMap *types.NetworkMap, postureChecks []*posture.Checks) (*proto.LoginResponse, error) {
+ var relayToken *Token
+ var err error
+ if s.config.Relay != nil && len(s.config.Relay.Addresses) > 0 {
+ relayToken, err = s.secretsManager.GenerateRelayToken()
+ if err != nil {
+ log.Errorf("failed generating Relay token: %v", err)
+ }
+ }
+
+ settings, err := s.settingsManager.GetSettings(ctx, peer.AccountID, activity.SystemInitiator)
+ if err != nil {
+ log.WithContext(ctx).Warnf("failed getting settings for peer %s: %s", peer.Key, err)
+ return nil, status.Errorf(codes.Internal, "failed getting settings")
+ }
+
+ // if peer has reached this point then it has logged in
+ loginResp := &proto.LoginResponse{
+ NetbirdConfig: toNetbirdConfig(s.config, nil, relayToken, nil),
+ PeerConfig: toPeerConfig(peer, netMap.Network, s.accountManager.GetDNSDomain(settings), false),
+ Checks: toProtocolChecks(ctx, postureChecks),
+ }
+
+ return loginResp, nil
+}
+
// processJwtToken validates the existence of a JWT token in the login request, and returns the corresponding user ID if
// the token is valid.
//
@@ -712,7 +730,7 @@ func (s *GRPCServer) sendInitialSync(ctx context.Context, peerKey wgtypes.Key, p
return status.Errorf(codes.Internal, "error handling request")
}
- plainResp := toSyncResponse(ctx, s.config, peer, turnToken, relayToken, networkMap, s.accountManager.GetDNSDomain(), postureChecks, nil, settings.RoutingPeerDNSResolutionEnabled, settings.Extra)
+ plainResp := toSyncResponse(ctx, s.config, peer, turnToken, relayToken, networkMap, s.accountManager.GetDNSDomain(settings), postureChecks, nil, settings.RoutingPeerDNSResolutionEnabled, settings.Extra)
encryptedResp, err := encryption.EncryptMessage(peerKey, s.wgKey, plainResp)
if err != nil {
diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml
index 5d346f226..1dba20426 100644
--- a/management/server/http/api/openapi.yml
+++ b/management/server/http/api/openapi.yml
@@ -43,9 +43,30 @@ components:
example: ch8i4ug6lnn4g9hqv7l0
settings:
$ref: '#/components/schemas/AccountSettings'
+ domain:
+ description: Account domain
+ type: string
+ example: netbird.io
+ domain_category:
+ description: Account domain category
+ type: string
+ example: private
+ created_at:
+ description: Account creation date (UTC)
+ type: string
+ format: date-time
+ example: "2023-05-05T09:00:35.477782Z"
+ created_by:
+ description: Account creator
+ type: string
+ example: google-oauth2|277474792786460067937
required:
- id
- settings
+ - domain
+ - domain_category
+ - created_at
+ - created_by
AccountSettings:
type: object
properties:
@@ -91,6 +112,10 @@ components:
description: Enables or disables DNS resolution on the routing peers
type: boolean
example: true
+ dns_domain:
+ description: Allows to define a custom dns domain for the account
+ type: string
+ example: my-organization.org
extra:
$ref: '#/components/schemas/AccountExtraSettings'
required:
@@ -2010,6 +2035,32 @@ components:
- tx_bytes
- tx_packets
- events
+ NetworkTrafficEventsResponse:
+ type: object
+ properties:
+ data:
+ type: array
+ description: List of network traffic events
+ items:
+ $ref: "#/components/schemas/NetworkTrafficEvent"
+ page:
+ type: integer
+ description: Current page number
+ page_size:
+ type: integer
+ description: Number of items per page
+ total_records:
+ type: integer
+ description: Total number of event records available
+ total_pages:
+ type: integer
+ description: Total number of pages available
+ required:
+ - data
+ - page
+ - page_size
+ - total_records
+ - total_pages
responses:
not_found:
description: Resource not found
@@ -2417,6 +2468,29 @@ paths:
"$ref": "#/components/responses/forbidden"
'500':
"$ref": "#/components/responses/internal_error"
+ /api/users/current:
+ get:
+ summary: Retrieve current user
+ description: Get information about the current user
+ tags: [ Users ]
+ security:
+ - BearerAuth: [ ]
+ - TokenAuth: [ ]
+ responses:
+ '200':
+ description: A User object
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/User'
+ '400':
+ "$ref": "#/components/responses/bad_request"
+ '401':
+ "$ref": "#/components/responses/requires_authentication"
+ '403':
+ "$ref": "#/components/responses/forbidden"
+ '500':
+ "$ref": "#/components/responses/internal_error"
/api/peers:
get:
summary: List all Peers
@@ -4203,15 +4277,77 @@ paths:
tags: [ Events ]
x-cloud-only: true
x-experimental: true
+ parameters:
+ - name: page
+ in: query
+ description: Page number
+ required: false
+ schema:
+ type: integer
+ minimum: 1
+ default: 1
+ - name: page_size
+ in: query
+ description: Number of items per page
+ required: false
+ schema:
+ type: integer
+ minimum: 1
+ maximum: 50000
+ default: 1000
+ - name: user_id
+ in: query
+ description: Filter by user ID
+ required: false
+ schema:
+ type: string
+ - name: protocol
+ in: query
+ description: Filter by protocol
+ required: false
+ schema:
+ type: integer
+ - name: type
+ in: query
+ description: Filter by event type
+ required: false
+ schema:
+ type: string
+ enum: [TYPE_UNKNOWN, TYPE_START, TYPE_END, TYPE_DROP]
+ - name: direction
+ in: query
+ description: Filter by direction
+ required: false
+ schema:
+ type: string
+ enum: [INGRESS, EGRESS, DIRECTION_UNKNOWN]
+ - name: search
+ in: query
+ description: Filters events with a partial match on user email, source and destination names and source and destination addresses
+ required: false
+ schema:
+ type: string
+ - name: start_date
+ in: query
+ description: Start date for filtering events (ISO 8601 format, e.g., 2024-01-01T00:00:00Z).
+ required: false
+ schema:
+ type: string
+ format: date-time
+ - name: end_date
+ in: query
+ description: End date for filtering events (ISO 8601 format, e.g., 2024-01-31T23:59:59Z).
+ required: false
+ schema:
+ type: string
+ format: date-time
responses:
"200":
description: List of network traffic events
content:
application/json:
schema:
- type: array
- items:
- $ref: "#/components/schemas/NetworkTrafficEvent"
+ $ref: "#/components/schemas/NetworkTrafficEventsResponse"
'400':
"$ref": "#/components/responses/bad_request"
'401':
diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go
index b73c64de2..a2a554b17 100644
--- a/management/server/http/api/types.gen.go
+++ b/management/server/http/api/types.gen.go
@@ -185,6 +185,21 @@ const (
UserPermissionsDashboardViewLimited UserPermissionsDashboardView = "limited"
)
+// Defines values for GetApiEventsNetworkTrafficParamsType.
+const (
+ GetApiEventsNetworkTrafficParamsTypeTYPEDROP GetApiEventsNetworkTrafficParamsType = "TYPE_DROP"
+ GetApiEventsNetworkTrafficParamsTypeTYPEEND GetApiEventsNetworkTrafficParamsType = "TYPE_END"
+ GetApiEventsNetworkTrafficParamsTypeTYPESTART GetApiEventsNetworkTrafficParamsType = "TYPE_START"
+ GetApiEventsNetworkTrafficParamsTypeTYPEUNKNOWN GetApiEventsNetworkTrafficParamsType = "TYPE_UNKNOWN"
+)
+
+// Defines values for GetApiEventsNetworkTrafficParamsDirection.
+const (
+ GetApiEventsNetworkTrafficParamsDirectionDIRECTIONUNKNOWN GetApiEventsNetworkTrafficParamsDirection = "DIRECTION_UNKNOWN"
+ GetApiEventsNetworkTrafficParamsDirectionEGRESS GetApiEventsNetworkTrafficParamsDirection = "EGRESS"
+ GetApiEventsNetworkTrafficParamsDirectionINGRESS GetApiEventsNetworkTrafficParamsDirection = "INGRESS"
+)
+
// AccessiblePeer defines model for AccessiblePeer.
type AccessiblePeer struct {
// CityName Commonly used English name of the city
@@ -223,6 +238,18 @@ type AccessiblePeer struct {
// Account defines model for Account.
type Account struct {
+ // CreatedAt Account creation date (UTC)
+ CreatedAt time.Time `json:"created_at"`
+
+ // CreatedBy Account creator
+ CreatedBy string `json:"created_by"`
+
+ // Domain Account domain
+ Domain string `json:"domain"`
+
+ // DomainCategory Account domain category
+ DomainCategory string `json:"domain_category"`
+
// Id Account ID
Id string `json:"id"`
Settings AccountSettings `json:"settings"`
@@ -247,7 +274,9 @@ type AccountRequest struct {
// AccountSettings defines model for AccountSettings.
type AccountSettings struct {
- Extra *AccountExtraSettings `json:"extra,omitempty"`
+ // DnsDomain Allows to define a custom dns domain for the account
+ DnsDomain *string `json:"dns_domain,omitempty"`
+ Extra *AccountExtraSettings `json:"extra,omitempty"`
// GroupsPropagationEnabled Allows propagate the new user auto groups to peers that belongs to the user
GroupsPropagationEnabled *bool `json:"groups_propagation_enabled,omitempty"`
@@ -890,6 +919,24 @@ type NetworkTrafficICMP struct {
Type int `json:"type"`
}
+// NetworkTrafficEventsResponse defines model for NetworkTrafficEventsResponse.
+type NetworkTrafficEventsResponse struct {
+ // Data List of network traffic events
+ Data []NetworkTrafficEvent `json:"data"`
+
+ // Page Current page number
+ Page int `json:"page"`
+
+ // PageSize Number of items per page
+ PageSize int `json:"page_size"`
+
+ // TotalPages Total number of pages available
+ TotalPages int `json:"total_pages"`
+
+ // TotalRecords Total number of event records available
+ TotalRecords int `json:"total_records"`
+}
+
// NetworkTrafficLocation defines model for NetworkTrafficLocation.
type NetworkTrafficLocation struct {
// CityName Name of the city (if known).
@@ -1741,6 +1788,42 @@ type UserRequest struct {
Role string `json:"role"`
}
+// GetApiEventsNetworkTrafficParams defines parameters for GetApiEventsNetworkTraffic.
+type GetApiEventsNetworkTrafficParams struct {
+ // Page Page number
+ Page *int `form:"page,omitempty" json:"page,omitempty"`
+
+ // PageSize Number of items per page
+ PageSize *int `form:"page_size,omitempty" json:"page_size,omitempty"`
+
+ // UserId Filter by user ID
+ UserId *string `form:"user_id,omitempty" json:"user_id,omitempty"`
+
+ // Protocol Filter by protocol
+ Protocol *int `form:"protocol,omitempty" json:"protocol,omitempty"`
+
+ // Type Filter by event type
+ Type *GetApiEventsNetworkTrafficParamsType `form:"type,omitempty" json:"type,omitempty"`
+
+ // Direction Filter by direction
+ Direction *GetApiEventsNetworkTrafficParamsDirection `form:"direction,omitempty" json:"direction,omitempty"`
+
+ // Search Filters events with a partial match on user email, source and destination names and source and destination addresses
+ Search *string `form:"search,omitempty" json:"search,omitempty"`
+
+ // StartDate Start date for filtering events (ISO 8601 format, e.g., 2024-01-01T00:00:00Z).
+ StartDate *time.Time `form:"start_date,omitempty" json:"start_date,omitempty"`
+
+ // EndDate End date for filtering events (ISO 8601 format, e.g., 2024-01-31T23:59:59Z).
+ EndDate *time.Time `form:"end_date,omitempty" json:"end_date,omitempty"`
+}
+
+// GetApiEventsNetworkTrafficParamsType defines parameters for GetApiEventsNetworkTraffic.
+type GetApiEventsNetworkTrafficParamsType string
+
+// GetApiEventsNetworkTrafficParamsDirection defines parameters for GetApiEventsNetworkTraffic.
+type GetApiEventsNetworkTrafficParamsDirection string
+
// GetApiPeersParams defines parameters for GetApiPeers.
type GetApiPeersParams struct {
// Name Filter peers by name
diff --git a/management/server/http/handler.go b/management/server/http/handler.go
index 4e2faae4b..3d4de31d0 100644
--- a/management/server/http/handler.go
+++ b/management/server/http/handler.go
@@ -62,6 +62,7 @@ func NewAPIHandler(
authManager,
accountManager.GetAccountIDFromUserAuth,
accountManager.SyncUserJWTGroups,
+ accountManager.GetUserFromUserAuth,
)
corsMiddleware := cors.AllowAll()
@@ -83,6 +84,8 @@ func NewAPIHandler(
users.AddEndpoints(accountManager, router)
setup_keys.AddEndpoints(accountManager, router)
policies.AddEndpoints(accountManager, LocationManager, router)
+ policies.AddPostureCheckEndpoints(accountManager, LocationManager, router)
+ policies.AddLocationsEndpoints(accountManager, LocationManager, permissionsManager, router)
groups.AddEndpoints(accountManager, router)
routes.AddEndpoints(accountManager, router)
dns.AddEndpoints(accountManager, router)
diff --git a/management/server/http/handlers/accounts/accounts_handler.go b/management/server/http/handlers/accounts/accounts_handler.go
index 6c8f8028a..7cad26bd6 100644
--- a/management/server/http/handlers/accounts/accounts_handler.go
+++ b/management/server/http/handlers/accounts/accounts_handler.go
@@ -47,13 +47,19 @@ func (h *handler) getAllAccounts(w http.ResponseWriter, r *http.Request) {
accountID, userID := userAuth.AccountId, userAuth.UserId
+ meta, err := h.accountManager.GetAccountMeta(r.Context(), accountID, userID)
+ if err != nil {
+ util.WriteError(r.Context(), err, w)
+ return
+ }
+
settings, err := h.settingsManager.GetSettings(r.Context(), accountID, userID)
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
- resp := toAccountResponse(accountID, settings)
+ resp := toAccountResponse(accountID, settings, meta)
util.WriteJSONObject(r.Context(), w, []*api.Account{resp})
}
@@ -113,6 +119,9 @@ func (h *handler) updateAccount(w http.ResponseWriter, r *http.Request) {
if req.Settings.RoutingPeerDnsResolutionEnabled != nil {
settings.RoutingPeerDNSResolutionEnabled = *req.Settings.RoutingPeerDnsResolutionEnabled
}
+ if req.Settings.DnsDomain != nil {
+ settings.DNSDomain = *req.Settings.DnsDomain
+ }
updatedAccount, err := h.accountManager.UpdateAccountSettings(r.Context(), accountID, userID, settings)
if err != nil {
@@ -120,7 +129,13 @@ func (h *handler) updateAccount(w http.ResponseWriter, r *http.Request) {
return
}
- resp := toAccountResponse(updatedAccount.Id, updatedAccount.Settings)
+ meta, err := h.accountManager.GetAccountMeta(r.Context(), accountID, userID)
+ if err != nil {
+ util.WriteError(r.Context(), err, w)
+ return
+ }
+
+ resp := toAccountResponse(updatedAccount.Id, updatedAccount.Settings, meta)
util.WriteJSONObject(r.Context(), w, &resp)
}
@@ -149,7 +164,7 @@ func (h *handler) deleteAccount(w http.ResponseWriter, r *http.Request) {
util.WriteJSONObject(r.Context(), w, util.EmptyObject{})
}
-func toAccountResponse(accountID string, settings *types.Settings) *api.Account {
+func toAccountResponse(accountID string, settings *types.Settings, meta *types.AccountMeta) *api.Account {
jwtAllowGroups := settings.JWTAllowGroups
if jwtAllowGroups == nil {
jwtAllowGroups = []string{}
@@ -166,6 +181,7 @@ func toAccountResponse(accountID string, settings *types.Settings) *api.Account
JwtAllowGroups: &jwtAllowGroups,
RegularUsersViewBlocked: settings.RegularUsersViewBlocked,
RoutingPeerDnsResolutionEnabled: &settings.RoutingPeerDNSResolutionEnabled,
+ DnsDomain: &settings.DNSDomain,
}
if settings.Extra != nil {
@@ -177,7 +193,11 @@ func toAccountResponse(accountID string, settings *types.Settings) *api.Account
}
return &api.Account{
- Id: accountID,
- Settings: apiSettings,
+ Id: accountID,
+ Settings: apiSettings,
+ CreatedAt: meta.CreatedAt,
+ CreatedBy: meta.CreatedBy,
+ Domain: meta.Domain,
+ DomainCategory: meta.DomainCategory,
}
}
diff --git a/management/server/http/handlers/accounts/accounts_handler_test.go b/management/server/http/handlers/accounts/accounts_handler_test.go
index e971a6514..57bbffc7c 100644
--- a/management/server/http/handlers/accounts/accounts_handler_test.go
+++ b/management/server/http/handlers/accounts/accounts_handler_test.go
@@ -50,6 +50,12 @@ func initAccountsTestData(t *testing.T, account *types.Account) *handler {
accCopy.UpdateSettings(newSettings)
return accCopy, nil
},
+ GetAccountByIDFunc: func(ctx context.Context, accountID string, userID string) (*types.Account, error) {
+ return account.Copy(), nil
+ },
+ GetAccountMetaFunc: func(ctx context.Context, accountID string, userID string) (*types.AccountMeta, error) {
+ return account.GetMeta(), nil
+ },
},
settingsManager: settingsMockManager,
}
@@ -102,6 +108,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
JwtAllowGroups: &[]string{},
RegularUsersViewBlocked: true,
RoutingPeerDnsResolutionEnabled: br(false),
+ DnsDomain: sr(""),
},
expectedArray: true,
expectedID: accountID,
@@ -122,6 +129,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
JwtAllowGroups: &[]string{},
RegularUsersViewBlocked: false,
RoutingPeerDnsResolutionEnabled: br(false),
+ DnsDomain: sr(""),
},
expectedArray: false,
expectedID: accountID,
@@ -142,6 +150,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
JwtAllowGroups: &[]string{"test"},
RegularUsersViewBlocked: true,
RoutingPeerDnsResolutionEnabled: br(false),
+ DnsDomain: sr(""),
},
expectedArray: false,
expectedID: accountID,
@@ -162,6 +171,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
JwtAllowGroups: &[]string{},
RegularUsersViewBlocked: true,
RoutingPeerDnsResolutionEnabled: br(false),
+ DnsDomain: sr(""),
},
expectedArray: false,
expectedID: accountID,
diff --git a/management/server/http/handlers/groups/groups_handler.go b/management/server/http/handlers/groups/groups_handler.go
index 667095018..3ae833dc0 100644
--- a/management/server/http/handlers/groups/groups_handler.go
+++ b/management/server/http/handlers/groups/groups_handler.go
@@ -143,7 +143,7 @@ func (h *handler) updateGroup(w http.ResponseWriter, r *http.Request) {
IntegrationReference: existingGroup.IntegrationReference,
}
- if err := h.accountManager.SaveGroup(r.Context(), accountID, userID, &group); err != nil {
+ if err := h.accountManager.SaveGroup(r.Context(), accountID, userID, &group, false); err != nil {
log.WithContext(r.Context()).Errorf("failed updating group %s under account %s %v", groupID, accountID, err)
util.WriteError(r.Context(), err, w)
return
@@ -203,7 +203,7 @@ func (h *handler) createGroup(w http.ResponseWriter, r *http.Request) {
Issued: types.GroupIssuedAPI,
}
- err = h.accountManager.SaveGroup(r.Context(), accountID, userID, &group)
+ err = h.accountManager.SaveGroup(r.Context(), accountID, userID, &group, true)
if err != nil {
util.WriteError(r.Context(), err, w)
return
diff --git a/management/server/http/handlers/groups/groups_handler_test.go b/management/server/http/handlers/groups/groups_handler_test.go
index f4ac34e53..2caa2f5bf 100644
--- a/management/server/http/handlers/groups/groups_handler_test.go
+++ b/management/server/http/handlers/groups/groups_handler_test.go
@@ -35,7 +35,7 @@ var TestPeers = map[string]*nbpeer.Peer{
func initGroupTestData(initGroups ...*types.Group) *handler {
return &handler{
accountManager: &mock_server.MockAccountManager{
- SaveGroupFunc: func(_ context.Context, accountID, userID string, group *types.Group) error {
+ SaveGroupFunc: func(_ context.Context, accountID, userID string, group *types.Group, create bool) error {
if !strings.HasPrefix(group.ID, "id-") {
group.ID = "id-was-set"
}
diff --git a/management/server/http/handlers/peers/peers_handler.go b/management/server/http/handlers/peers/peers_handler.go
index fa78836d8..58ea06ea3 100644
--- a/management/server/http/handlers/peers/peers_handler.go
+++ b/management/server/http/handlers/peers/peers_handler.go
@@ -65,7 +65,13 @@ func (h *Handler) getPeer(ctx context.Context, accountID, peerID, userID string,
util.WriteError(ctx, err, w)
return
}
- dnsDomain := h.accountManager.GetDNSDomain()
+ settings, err := h.accountManager.GetAccountSettings(ctx, accountID, activity.SystemInitiator)
+ if err != nil {
+ util.WriteError(ctx, err, w)
+ return
+ }
+
+ dnsDomain := h.accountManager.GetDNSDomain(settings)
grps, _ := h.accountManager.GetPeerGroups(ctx, accountID, peerID)
grpsInfoMap := groups.ToGroupsInfoMap(grps, 0)
@@ -110,7 +116,13 @@ func (h *Handler) updatePeer(ctx context.Context, accountID, userID, peerID stri
util.WriteError(ctx, err, w)
return
}
- dnsDomain := h.accountManager.GetDNSDomain()
+
+ settings, err := h.accountManager.GetAccountSettings(ctx, accountID, activity.SystemInitiator)
+ if err != nil {
+ util.WriteError(ctx, err, w)
+ return
+ }
+ dnsDomain := h.accountManager.GetDNSDomain(settings)
peerGroups, err := h.accountManager.GetPeerGroups(ctx, accountID, peer.ID)
if err != nil {
@@ -192,7 +204,12 @@ func (h *Handler) GetAllPeers(w http.ResponseWriter, r *http.Request) {
return
}
- dnsDomain := h.accountManager.GetDNSDomain()
+ settings, err := h.accountManager.GetAccountSettings(r.Context(), accountID, activity.SystemInitiator)
+ if err != nil {
+ util.WriteError(r.Context(), err, w)
+ return
+ }
+ dnsDomain := h.accountManager.GetDNSDomain(settings)
grps, _ := h.accountManager.GetAllGroups(r.Context(), accountID, userID)
@@ -279,7 +296,7 @@ func (h *Handler) GetAccessiblePeers(w http.ResponseWriter, r *http.Request) {
return
}
- dnsDomain := h.accountManager.GetDNSDomain()
+ dnsDomain := h.accountManager.GetDNSDomain(account.Settings)
customZone := account.GetPeersCustomZone(r.Context(), dnsDomain)
netMap := account.GetPeerNetworkMap(r.Context(), peerID, customZone, validPeers, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap(), nil)
diff --git a/management/server/http/handlers/peers/peers_handler_test.go b/management/server/http/handlers/peers/peers_handler_test.go
index a03c3c29d..a1fc13dd3 100644
--- a/management/server/http/handlers/peers/peers_handler_test.go
+++ b/management/server/http/handlers/peers/peers_handler_test.go
@@ -152,7 +152,7 @@ func initTestMetaData(peers ...*nbpeer.Peer) *Handler {
},
}, nil
},
- GetDNSDomainFunc: func() string {
+ GetDNSDomainFunc: func(settings *types.Settings) string {
return "netbird.selfhosted"
},
GetAccountFunc: func(ctx context.Context, accountID string) (*types.Account, error) {
@@ -172,6 +172,9 @@ func initTestMetaData(peers ...*nbpeer.Peer) *Handler {
_, ok := statuses[peerID]
return ok
},
+ GetAccountSettingsFunc: func(ctx context.Context, accountID string, userID string) (*types.Settings, error) {
+ return account.Settings, nil
+ },
},
}
}
diff --git a/management/server/http/handlers/policies/geolocation_handler_test.go b/management/server/http/handlers/policies/geolocation_handler_test.go
index fbdc324d6..b7b53f53f 100644
--- a/management/server/http/handlers/policies/geolocation_handler_test.go
+++ b/management/server/http/handlers/policies/geolocation_handler_test.go
@@ -10,6 +10,7 @@ import (
"path/filepath"
"testing"
+ "github.com/golang/mock/gomock"
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
@@ -17,6 +18,9 @@ import (
"github.com/netbirdio/netbird/management/server/geolocation"
"github.com/netbirdio/netbird/management/server/http/api"
"github.com/netbirdio/netbird/management/server/mock_server"
+ "github.com/netbirdio/netbird/management/server/permissions"
+ "github.com/netbirdio/netbird/management/server/permissions/modules"
+ "github.com/netbirdio/netbird/management/server/permissions/operations"
"github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/util"
)
@@ -41,6 +45,14 @@ func initGeolocationTestData(t *testing.T) *geolocationsHandler {
assert.NoError(t, err)
t.Cleanup(func() { _ = geo.Stop() })
+ ctrl := gomock.NewController(t)
+ permissionsManagerMock := permissions.NewMockManager(ctrl)
+ permissionsManagerMock.
+ EXPECT().
+ ValidateUserPermissions(gomock.Any(), gomock.Any(), gomock.Any(), modules.Policies, operations.Read).
+ Return(true, nil).
+ AnyTimes()
+
return &geolocationsHandler{
accountManager: &mock_server.MockAccountManager{
GetUserByIDFunc: func(ctx context.Context, id string) (*types.User, error) {
@@ -48,6 +60,7 @@ func initGeolocationTestData(t *testing.T) *geolocationsHandler {
},
},
geolocationManager: geo,
+ permissionsManager: permissionsManagerMock,
}
}
diff --git a/management/server/http/handlers/policies/geolocations_handler.go b/management/server/http/handlers/policies/geolocations_handler.go
index fb19887dc..84c8ea0aa 100644
--- a/management/server/http/handlers/policies/geolocations_handler.go
+++ b/management/server/http/handlers/policies/geolocations_handler.go
@@ -11,6 +11,9 @@ import (
"github.com/netbirdio/netbird/management/server/geolocation"
"github.com/netbirdio/netbird/management/server/http/api"
"github.com/netbirdio/netbird/management/server/http/util"
+ "github.com/netbirdio/netbird/management/server/permissions"
+ "github.com/netbirdio/netbird/management/server/permissions/modules"
+ "github.com/netbirdio/netbird/management/server/permissions/operations"
"github.com/netbirdio/netbird/management/server/status"
)
@@ -22,19 +25,21 @@ var (
type geolocationsHandler struct {
accountManager account.Manager
geolocationManager geolocation.Geolocation
+ permissionsManager permissions.Manager
}
-func addLocationsEndpoint(accountManager account.Manager, locationManager geolocation.Geolocation, router *mux.Router) {
- locationHandler := newGeolocationsHandlerHandler(accountManager, locationManager)
+func AddLocationsEndpoints(accountManager account.Manager, locationManager geolocation.Geolocation, permissionsManager permissions.Manager, router *mux.Router) {
+ locationHandler := newGeolocationsHandlerHandler(accountManager, locationManager, permissionsManager)
router.HandleFunc("/locations/countries", locationHandler.getAllCountries).Methods("GET", "OPTIONS")
router.HandleFunc("/locations/countries/{country}/cities", locationHandler.getCitiesByCountry).Methods("GET", "OPTIONS")
}
// newGeolocationsHandlerHandler creates a new Geolocations handler
-func newGeolocationsHandlerHandler(accountManager account.Manager, geolocationManager geolocation.Geolocation) *geolocationsHandler {
+func newGeolocationsHandlerHandler(accountManager account.Manager, geolocationManager geolocation.Geolocation, permissionsManager permissions.Manager) *geolocationsHandler {
return &geolocationsHandler{
accountManager: accountManager,
geolocationManager: geolocationManager,
+ permissionsManager: permissionsManager,
}
}
@@ -98,20 +103,22 @@ func (l *geolocationsHandler) getCitiesByCountry(w http.ResponseWriter, r *http.
}
func (l *geolocationsHandler) authenticateUser(r *http.Request) error {
- userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
+ ctx := r.Context()
+
+ userAuth, err := nbcontext.GetUserAuthFromContext(ctx)
if err != nil {
return err
}
- _, userID := userAuth.AccountId, userAuth.UserId
+ accountID, userID := userAuth.AccountId, userAuth.UserId
- user, err := l.accountManager.GetUserByID(r.Context(), userID)
+ allowed, err := l.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Policies, operations.Read)
if err != nil {
- return err
+ return status.NewPermissionValidationError(err)
}
- if !user.HasAdminPower() {
- return status.Errorf(status.PermissionDenied, "user is not allowed to perform this action")
+ if !allowed {
+ return status.NewPermissionDeniedError()
}
return nil
}
diff --git a/management/server/http/handlers/policies/policies_handler.go b/management/server/http/handlers/policies/policies_handler.go
index 01a09842a..9ff7ea0ea 100644
--- a/management/server/http/handlers/policies/policies_handler.go
+++ b/management/server/http/handlers/policies/policies_handler.go
@@ -28,7 +28,6 @@ func AddEndpoints(accountManager account.Manager, locationManager geolocation.Ge
router.HandleFunc("/policies/{policyId}", policiesHandler.updatePolicy).Methods("PUT", "OPTIONS")
router.HandleFunc("/policies/{policyId}", policiesHandler.getPolicy).Methods("GET", "OPTIONS")
router.HandleFunc("/policies/{policyId}", policiesHandler.deletePolicy).Methods("DELETE", "OPTIONS")
- addPostureCheckEndpoint(accountManager, locationManager, router)
}
// newHandler creates a new policies handler
@@ -96,7 +95,7 @@ func (h *handler) updatePolicy(w http.ResponseWriter, r *http.Request) {
return
}
- h.savePolicy(w, r, accountID, userID, policyID)
+ h.savePolicy(w, r, accountID, userID, policyID, false)
}
// createPolicy handles policy creation request
@@ -109,11 +108,11 @@ func (h *handler) createPolicy(w http.ResponseWriter, r *http.Request) {
accountID, userID := userAuth.AccountId, userAuth.UserId
- h.savePolicy(w, r, accountID, userID, "")
+ h.savePolicy(w, r, accountID, userID, "", true)
}
// savePolicy handles policy creation and update
-func (h *handler) savePolicy(w http.ResponseWriter, r *http.Request, accountID string, userID string, policyID string) {
+func (h *handler) savePolicy(w http.ResponseWriter, r *http.Request, accountID string, userID string, policyID string, create bool) {
var req api.PutApiPoliciesPolicyIdJSONRequestBody
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
util.WriteErrorResponse("couldn't parse JSON request", http.StatusBadRequest, w)
@@ -280,7 +279,7 @@ func (h *handler) savePolicy(w http.ResponseWriter, r *http.Request, accountID s
policy.SourcePostureChecks = *req.SourcePostureChecks
}
- policy, err := h.accountManager.SavePolicy(r.Context(), accountID, userID, policy)
+ policy, err := h.accountManager.SavePolicy(r.Context(), accountID, userID, policy, create)
if err != nil {
util.WriteError(r.Context(), err, w)
return
diff --git a/management/server/http/handlers/policies/policies_handler_test.go b/management/server/http/handlers/policies/policies_handler_test.go
index 6450295eb..6f3dbc792 100644
--- a/management/server/http/handlers/policies/policies_handler_test.go
+++ b/management/server/http/handlers/policies/policies_handler_test.go
@@ -34,7 +34,7 @@ func initPoliciesTestData(policies ...*types.Policy) *handler {
}
return policy, nil
},
- SavePolicyFunc: func(_ context.Context, _, _ string, policy *types.Policy) (*types.Policy, error) {
+ SavePolicyFunc: func(_ context.Context, _, _ string, policy *types.Policy, create bool) (*types.Policy, error) {
if !strings.HasPrefix(policy.ID, "id-") {
policy.ID = "id-was-set"
policy.Rules[0].ID = "id-was-set"
diff --git a/management/server/http/handlers/policies/posture_checks_handler.go b/management/server/http/handlers/policies/posture_checks_handler.go
index b99649dbc..2925f96ef 100644
--- a/management/server/http/handlers/policies/posture_checks_handler.go
+++ b/management/server/http/handlers/policies/posture_checks_handler.go
@@ -21,14 +21,13 @@ type postureChecksHandler struct {
geolocationManager geolocation.Geolocation
}
-func addPostureCheckEndpoint(accountManager account.Manager, locationManager geolocation.Geolocation, router *mux.Router) {
+func AddPostureCheckEndpoints(accountManager account.Manager, locationManager geolocation.Geolocation, router *mux.Router) {
postureCheckHandler := newPostureChecksHandler(accountManager, locationManager)
router.HandleFunc("/posture-checks", postureCheckHandler.getAllPostureChecks).Methods("GET", "OPTIONS")
router.HandleFunc("/posture-checks", postureCheckHandler.createPostureCheck).Methods("POST", "OPTIONS")
router.HandleFunc("/posture-checks/{postureCheckId}", postureCheckHandler.updatePostureCheck).Methods("PUT", "OPTIONS")
router.HandleFunc("/posture-checks/{postureCheckId}", postureCheckHandler.getPostureCheck).Methods("GET", "OPTIONS")
router.HandleFunc("/posture-checks/{postureCheckId}", postureCheckHandler.deletePostureCheck).Methods("DELETE", "OPTIONS")
- addLocationsEndpoint(accountManager, locationManager, router)
}
// newPostureChecksHandler creates a new PostureChecks handler
@@ -85,7 +84,7 @@ func (p *postureChecksHandler) updatePostureCheck(w http.ResponseWriter, r *http
return
}
- p.savePostureChecks(w, r, accountID, userID, postureChecksID)
+ p.savePostureChecks(w, r, accountID, userID, postureChecksID, false)
}
// createPostureCheck handles posture check creation request
@@ -98,7 +97,7 @@ func (p *postureChecksHandler) createPostureCheck(w http.ResponseWriter, r *http
accountID, userID := userAuth.AccountId, userAuth.UserId
- p.savePostureChecks(w, r, accountID, userID, "")
+ p.savePostureChecks(w, r, accountID, userID, "", true)
}
// getPostureCheck handles a posture check Get request identified by ID
@@ -151,7 +150,7 @@ func (p *postureChecksHandler) deletePostureCheck(w http.ResponseWriter, r *http
}
// savePostureChecks handles posture checks create and update
-func (p *postureChecksHandler) savePostureChecks(w http.ResponseWriter, r *http.Request, accountID, userID, postureChecksID string) {
+func (p *postureChecksHandler) savePostureChecks(w http.ResponseWriter, r *http.Request, accountID, userID, postureChecksID string, create bool) {
var (
err error
req api.PostureCheckUpdate
@@ -176,7 +175,7 @@ func (p *postureChecksHandler) savePostureChecks(w http.ResponseWriter, r *http.
return
}
- postureChecks, err = p.accountManager.SavePostureChecks(r.Context(), accountID, userID, postureChecks)
+ postureChecks, err = p.accountManager.SavePostureChecks(r.Context(), accountID, userID, postureChecks, create)
if err != nil {
util.WriteError(r.Context(), err, w)
return
diff --git a/management/server/http/handlers/policies/posture_checks_handler_test.go b/management/server/http/handlers/policies/posture_checks_handler_test.go
index e3844caa2..e875b3738 100644
--- a/management/server/http/handlers/policies/posture_checks_handler_test.go
+++ b/management/server/http/handlers/policies/posture_checks_handler_test.go
@@ -40,7 +40,7 @@ func initPostureChecksTestData(postureChecks ...*posture.Checks) *postureChecksH
}
return p, nil
},
- SavePostureChecksFunc: func(_ context.Context, accountID, userID string, postureChecks *posture.Checks) (*posture.Checks, error) {
+ SavePostureChecksFunc: func(_ context.Context, accountID, userID string, postureChecks *posture.Checks, create bool) (*posture.Checks, error) {
postureChecks.ID = "postureCheck"
testPostureChecks[postureChecks.ID] = postureChecks
diff --git a/management/server/http/handlers/users/users_handler.go b/management/server/http/handlers/users/users_handler.go
index 19f56c464..c69c6b944 100644
--- a/management/server/http/handlers/users/users_handler.go
+++ b/management/server/http/handlers/users/users_handler.go
@@ -25,6 +25,7 @@ type handler struct {
func AddEndpoints(accountManager account.Manager, router *mux.Router) {
userHandler := newHandler(accountManager)
router.HandleFunc("/users", userHandler.getAllUsers).Methods("GET", "OPTIONS")
+ router.HandleFunc("/users/current", userHandler.getCurrentUser).Methods("GET", "OPTIONS")
router.HandleFunc("/users/{userId}", userHandler.updateUser).Methods("PUT", "OPTIONS")
router.HandleFunc("/users/{userId}", userHandler.deleteUser).Methods("DELETE", "OPTIONS")
router.HandleFunc("/users", userHandler.createUser).Methods("POST", "OPTIONS")
@@ -259,6 +260,29 @@ func (h *handler) inviteUser(w http.ResponseWriter, r *http.Request) {
util.WriteJSONObject(r.Context(), w, util.EmptyObject{})
}
+func (h *handler) getCurrentUser(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodGet {
+ util.WriteErrorResponse("wrong HTTP method", http.StatusMethodNotAllowed, w)
+ return
+ }
+ ctx := r.Context()
+ userAuth, err := nbcontext.GetUserAuthFromContext(ctx)
+ if err != nil {
+ util.WriteError(r.Context(), err, w)
+ return
+ }
+
+ accountID, userID := userAuth.AccountId, userAuth.UserId
+
+ user, err := h.accountManager.GetCurrentUserInfo(ctx, accountID, userID)
+ if err != nil {
+ util.WriteError(r.Context(), err, w)
+ return
+ }
+
+ util.WriteJSONObject(r.Context(), w, toUserResponse(user, userID))
+}
+
func toUserResponse(user *types.UserInfo, currenUserID string) *api.User {
autoGroups := user.AutoGroups
if autoGroups == nil {
diff --git a/management/server/http/handlers/users/users_handler_test.go b/management/server/http/handlers/users/users_handler_test.go
index a6a904a4c..604954819 100644
--- a/management/server/http/handlers/users/users_handler_test.go
+++ b/management/server/http/handlers/users/users_handler_test.go
@@ -9,6 +9,7 @@ import (
"net/http"
"net/http/httptest"
"testing"
+ "time"
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
@@ -123,6 +124,64 @@ func initUsersTestData() *handler {
return nil
},
+ GetCurrentUserInfoFunc: func(ctx context.Context, accountID, userID string) (*types.UserInfo, error) {
+ switch userID {
+ case "not-found":
+ return nil, status.NewUserNotFoundError("not-found")
+ case "not-of-account":
+ return nil, status.NewUserNotPartOfAccountError()
+ case "blocked-user":
+ return nil, status.NewUserBlockedError()
+ case "service-user":
+ return nil, status.NewPermissionDeniedError()
+ case "owner":
+ return &types.UserInfo{
+ ID: "owner",
+ Name: "",
+ Role: "owner",
+ Status: "active",
+ IsServiceUser: false,
+ IsBlocked: false,
+ NonDeletable: false,
+ Issued: "api",
+ Permissions: types.UserPermissions{
+ DashboardView: "full",
+ },
+ }, nil
+ case "regular-user":
+ return &types.UserInfo{
+ ID: "regular-user",
+ Name: "",
+ Role: "user",
+ Status: "active",
+ IsServiceUser: false,
+ IsBlocked: false,
+ NonDeletable: false,
+ Issued: "api",
+ Permissions: types.UserPermissions{
+ DashboardView: "limited",
+ },
+ }, nil
+
+ case "admin-user":
+ return &types.UserInfo{
+ ID: "admin-user",
+ Name: "",
+ Role: "admin",
+ Status: "active",
+ IsServiceUser: false,
+ IsBlocked: false,
+ NonDeletable: false,
+ LastLogin: time.Time{},
+ Issued: "api",
+ Permissions: types.UserPermissions{
+ DashboardView: "full",
+ },
+ }, nil
+ }
+
+ return nil, fmt.Errorf("user id %s not handled", userID)
+ },
},
}
}
@@ -481,3 +540,73 @@ func TestDeleteUser(t *testing.T) {
})
}
}
+
+func TestCurrentUser(t *testing.T) {
+ tt := []struct {
+ name string
+ expectedStatus int
+ requestAuth nbcontext.UserAuth
+ }{
+ {
+ name: "without auth",
+ expectedStatus: http.StatusInternalServerError,
+ },
+ {
+ name: "user not found",
+ requestAuth: nbcontext.UserAuth{UserId: "not-found"},
+ expectedStatus: http.StatusNotFound,
+ },
+ {
+ name: "not of account",
+ requestAuth: nbcontext.UserAuth{UserId: "not-of-account"},
+ expectedStatus: http.StatusForbidden,
+ },
+ {
+ name: "blocked user",
+ requestAuth: nbcontext.UserAuth{UserId: "blocked-user"},
+ expectedStatus: http.StatusForbidden,
+ },
+ {
+ name: "service user",
+ requestAuth: nbcontext.UserAuth{UserId: "service-user"},
+ expectedStatus: http.StatusForbidden,
+ },
+ {
+ name: "owner",
+ requestAuth: nbcontext.UserAuth{UserId: "owner"},
+ expectedStatus: http.StatusOK,
+ },
+ {
+ name: "regular user",
+ requestAuth: nbcontext.UserAuth{UserId: "regular-user"},
+ expectedStatus: http.StatusOK,
+ },
+ {
+ name: "admin user",
+ requestAuth: nbcontext.UserAuth{UserId: "admin-user"},
+ expectedStatus: http.StatusOK,
+ },
+ }
+
+ userHandler := initUsersTestData()
+ for _, tc := range tt {
+ t.Run(tc.name, func(t *testing.T) {
+ req := httptest.NewRequest(http.MethodGet, "/api/users/current", nil)
+ if tc.requestAuth.UserId != "" {
+ req = nbcontext.SetUserAuthInRequest(req, tc.requestAuth)
+ }
+
+ rr := httptest.NewRecorder()
+
+ userHandler.getCurrentUser(rr, req)
+
+ res := rr.Result()
+ defer res.Body.Close()
+
+ if status := rr.Code; status != tc.expectedStatus {
+ t.Fatalf("handler returned wrong status code: got %v want %v",
+ status, tc.expectedStatus)
+ }
+ })
+ }
+}
diff --git a/management/server/http/middleware/auth_middleware.go b/management/server/http/middleware/auth_middleware.go
index a8e6790a9..f2732fbf8 100644
--- a/management/server/http/middleware/auth_middleware.go
+++ b/management/server/http/middleware/auth_middleware.go
@@ -15,16 +15,20 @@ import (
"github.com/netbirdio/netbird/management/server/http/middleware/bypass"
"github.com/netbirdio/netbird/management/server/http/util"
"github.com/netbirdio/netbird/management/server/status"
+ "github.com/netbirdio/netbird/management/server/types"
)
type EnsureAccountFunc func(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error)
type SyncUserJWTGroupsFunc func(ctx context.Context, userAuth nbcontext.UserAuth) error
+type GetUserFromUserAuthFunc func(ctx context.Context, userAuth nbcontext.UserAuth) (*types.User, error)
+
// AuthMiddleware middleware to verify personal access tokens (PAT) and JWT tokens
type AuthMiddleware struct {
- authManager auth.Manager
- ensureAccount EnsureAccountFunc
- syncUserJWTGroups SyncUserJWTGroupsFunc
+ authManager auth.Manager
+ ensureAccount EnsureAccountFunc
+ getUserFromUserAuth GetUserFromUserAuthFunc
+ syncUserJWTGroups SyncUserJWTGroupsFunc
}
// NewAuthMiddleware instance constructor
@@ -32,11 +36,13 @@ func NewAuthMiddleware(
authManager auth.Manager,
ensureAccount EnsureAccountFunc,
syncUserJWTGroups SyncUserJWTGroupsFunc,
+ getUserFromUserAuth GetUserFromUserAuthFunc,
) *AuthMiddleware {
return &AuthMiddleware{
- authManager: authManager,
- ensureAccount: ensureAccount,
- syncUserJWTGroups: syncUserJWTGroups,
+ authManager: authManager,
+ ensureAccount: ensureAccount,
+ syncUserJWTGroups: syncUserJWTGroups,
+ getUserFromUserAuth: getUserFromUserAuth,
}
}
@@ -123,6 +129,12 @@ func (m *AuthMiddleware) checkJWTFromRequest(r *http.Request, auth []string) (*h
log.WithContext(ctx).Errorf("HTTP server failed to sync user JWT groups: %s", err)
}
+ _, err = m.getUserFromUserAuth(ctx, userAuth)
+ if err != nil {
+ log.WithContext(ctx).Errorf("HTTP server failed to update user from user auth: %s", err)
+ return r, err
+ }
+
return nbcontext.SetUserAuthInRequest(r, userAuth), nil
}
@@ -155,6 +167,11 @@ func (m *AuthMiddleware) checkPATFromRequest(r *http.Request, auth []string) (*h
IsPAT: true,
}
+ if impersonate, ok := r.URL.Query()["account"]; ok && len(impersonate) == 1 {
+ userAuth.AccountId = impersonate[0]
+ userAuth.IsChild = ok
+ }
+
return nbcontext.SetUserAuthInRequest(r, userAuth), nil
}
diff --git a/management/server/http/middleware/auth_middleware_test.go b/management/server/http/middleware/auth_middleware_test.go
index 3dc7d51cb..2285ed244 100644
--- a/management/server/http/middleware/auth_middleware_test.go
+++ b/management/server/http/middleware/auth_middleware_test.go
@@ -190,6 +190,9 @@ func TestAuthMiddleware_Handler(t *testing.T) {
func(ctx context.Context, userAuth nbcontext.UserAuth) error {
return nil
},
+ func(ctx context.Context, userAuth nbcontext.UserAuth) (*types.User, error) {
+ return &types.User{}, nil
+ },
)
handlerToTest := authMiddleware.Handler(nextHandler)
@@ -239,14 +242,15 @@ func TestAuthMiddleware_Handler_Child(t *testing.T) {
},
},
{
- name: "Valid PAT Token ignores child",
+ name: "Valid PAT Token accesses child",
path: "/test?account=xyz",
authHeader: "Token " + PAT,
expectedUserAuth: &nbcontext.UserAuth{
- AccountId: accountID,
+ AccountId: "xyz",
UserId: userID,
Domain: testAccount.Domain,
DomainCategory: testAccount.DomainCategory,
+ IsChild: true,
IsPAT: true,
},
},
@@ -291,6 +295,9 @@ func TestAuthMiddleware_Handler_Child(t *testing.T) {
func(ctx context.Context, userAuth nbcontext.UserAuth) error {
return nil
},
+ func(ctx context.Context, userAuth nbcontext.UserAuth) (*types.User, error) {
+ return &types.User{}, nil
+ },
)
for _, tc := range tt {
diff --git a/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go b/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go
index e2c2c1d85..d82e08be9 100644
--- a/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go
+++ b/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go
@@ -21,6 +21,8 @@ import (
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools"
)
+const modulePeers = "peers"
+
// Map to store peers, groups, users, and setupKeys by name
var benchCasesPeers = map[string]testing_tools.BenchmarkCase{
"Peers - XS": {Peers: 5, Groups: 10000, Users: 10000, SetupKeys: 10000},
@@ -34,15 +36,8 @@ var benchCasesPeers = map[string]testing_tools.BenchmarkCase{
}
func BenchmarkUpdatePeer(b *testing.B) {
- var expectedMetrics = map[string]testing_tools.PerformanceMetrics{
- "Peers - XS": {MinMsPerOpLocal: 400, MaxMsPerOpLocal: 600, MinMsPerOpCICD: 600, MaxMsPerOpCICD: 3500},
- "Peers - S": {MinMsPerOpLocal: 100, MaxMsPerOpLocal: 130, MinMsPerOpCICD: 80, MaxMsPerOpCICD: 200},
- "Peers - M": {MinMsPerOpLocal: 130, MaxMsPerOpLocal: 150, MinMsPerOpCICD: 100, MaxMsPerOpCICD: 300},
- "Peers - L": {MinMsPerOpLocal: 230, MaxMsPerOpLocal: 270, MinMsPerOpCICD: 200, MaxMsPerOpCICD: 500},
- "Groups - L": {MinMsPerOpLocal: 400, MaxMsPerOpLocal: 600, MinMsPerOpCICD: 650, MaxMsPerOpCICD: 3500},
- "Users - L": {MinMsPerOpLocal: 200, MaxMsPerOpLocal: 400, MinMsPerOpCICD: 250, MaxMsPerOpCICD: 600},
- "Setup Keys - L": {MinMsPerOpLocal: 200, MaxMsPerOpLocal: 400, MinMsPerOpCICD: 250, MaxMsPerOpCICD: 600},
- "Peers - XL": {MinMsPerOpLocal: 600, MaxMsPerOpLocal: 1000, MinMsPerOpCICD: 600, MaxMsPerOpCICD: 2000},
+ if os.Getenv("CI") != "true" {
+ b.Skip("Skipping because CI is not set")
}
log.SetOutput(io.Discard)
@@ -70,21 +65,14 @@ func BenchmarkUpdatePeer(b *testing.B) {
apiHandler.ServeHTTP(recorder, req)
}
- testing_tools.EvaluateBenchmarkResults(b, name, time.Since(start), expectedMetrics[name], recorder)
+ testing_tools.EvaluateBenchmarkResults(b, name, time.Since(start), recorder, modulePeers, testing_tools.OperationUpdate)
})
}
}
func BenchmarkGetOnePeer(b *testing.B) {
- var expectedMetrics = map[string]testing_tools.PerformanceMetrics{
- "Peers - XS": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 40, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 70},
- "Peers - S": {MinMsPerOpLocal: 1, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 70},
- "Peers - M": {MinMsPerOpLocal: 9, MaxMsPerOpLocal: 18, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 70},
- "Peers - L": {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 90, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 200},
- "Groups - L": {MinMsPerOpLocal: 80, MaxMsPerOpLocal: 130, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 200},
- "Users - L": {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 90, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 200},
- "Setup Keys - L": {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 90, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 200},
- "Peers - XL": {MinMsPerOpLocal: 200, MaxMsPerOpLocal: 400, MinMsPerOpCICD: 200, MaxMsPerOpCICD: 750},
+ if os.Getenv("CI") != "true" {
+ b.Skip("Skipping because CI is not set")
}
log.SetOutput(io.Discard)
@@ -104,21 +92,14 @@ func BenchmarkGetOnePeer(b *testing.B) {
apiHandler.ServeHTTP(recorder, req)
}
- testing_tools.EvaluateBenchmarkResults(b, name, time.Since(start), expectedMetrics[name], recorder)
+ testing_tools.EvaluateBenchmarkResults(b, name, time.Since(start), recorder, modulePeers, testing_tools.OperationGetOne)
})
}
}
func BenchmarkGetAllPeers(b *testing.B) {
- var expectedMetrics = map[string]testing_tools.PerformanceMetrics{
- "Peers - XS": {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 70, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 100},
- "Peers - S": {MinMsPerOpLocal: 2, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 100},
- "Peers - M": {MinMsPerOpLocal: 20, MaxMsPerOpLocal: 50, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 100},
- "Peers - L": {MinMsPerOpLocal: 110, MaxMsPerOpLocal: 150, MinMsPerOpCICD: 100, MaxMsPerOpCICD: 300},
- "Groups - L": {MinMsPerOpLocal: 150, MaxMsPerOpLocal: 200, MinMsPerOpCICD: 130, MaxMsPerOpCICD: 500},
- "Users - L": {MinMsPerOpLocal: 100, MaxMsPerOpLocal: 170, MinMsPerOpCICD: 100, MaxMsPerOpCICD: 400},
- "Setup Keys - L": {MinMsPerOpLocal: 100, MaxMsPerOpLocal: 170, MinMsPerOpCICD: 100, MaxMsPerOpCICD: 400},
- "Peers - XL": {MinMsPerOpLocal: 450, MaxMsPerOpLocal: 800, MinMsPerOpCICD: 500, MaxMsPerOpCICD: 1500},
+ if os.Getenv("CI") != "true" {
+ b.Skip("Skipping because CI is not set")
}
log.SetOutput(io.Discard)
@@ -138,21 +119,14 @@ func BenchmarkGetAllPeers(b *testing.B) {
apiHandler.ServeHTTP(recorder, req)
}
- testing_tools.EvaluateBenchmarkResults(b, name, time.Since(start), expectedMetrics[name], recorder)
+ testing_tools.EvaluateBenchmarkResults(b, name, time.Since(start), recorder, modulePeers, testing_tools.OperationGetAll)
})
}
}
func BenchmarkDeletePeer(b *testing.B) {
- var expectedMetrics = map[string]testing_tools.PerformanceMetrics{
- "Peers - XS": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 18},
- "Peers - S": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 18},
- "Peers - M": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 18},
- "Peers - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 18},
- "Groups - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 18},
- "Users - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 18},
- "Setup Keys - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 18},
- "Peers - XL": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 18},
+ if os.Getenv("CI") != "true" {
+ b.Skip("Skipping because CI is not set")
}
log.SetOutput(io.Discard)
@@ -172,7 +146,7 @@ func BenchmarkDeletePeer(b *testing.B) {
apiHandler.ServeHTTP(recorder, req)
}
- testing_tools.EvaluateBenchmarkResults(b, name, time.Since(start), expectedMetrics[name], recorder)
+ testing_tools.EvaluateBenchmarkResults(b, name, time.Since(start), recorder, modulePeers, testing_tools.OperationDelete)
})
}
}
diff --git a/management/server/http/testing/benchmarks/setupkeys_handler_benchmark_test.go b/management/server/http/testing/benchmarks/setupkeys_handler_benchmark_test.go
index ed643f75e..f99b541f8 100644
--- a/management/server/http/testing/benchmarks/setupkeys_handler_benchmark_test.go
+++ b/management/server/http/testing/benchmarks/setupkeys_handler_benchmark_test.go
@@ -33,16 +33,11 @@ var benchCasesSetupKeys = map[string]testing_tools.BenchmarkCase{
"Setup Keys - XL": {Peers: 500, Groups: 50, Users: 100, SetupKeys: 25000},
}
+const moduleSetupKeys = "setup_keys"
+
func BenchmarkCreateSetupKey(b *testing.B) {
- var expectedMetrics = map[string]testing_tools.PerformanceMetrics{
- "Setup Keys - XS": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 17},
- "Setup Keys - S": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 17},
- "Setup Keys - M": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 17},
- "Setup Keys - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 17},
- "Peers - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 17},
- "Groups - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 17},
- "Users - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 17},
- "Setup Keys - XL": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 17},
+ if os.Getenv("CI") != "true" {
+ b.Skip("Skipping because CI is not set")
}
log.SetOutput(io.Discard)
@@ -74,21 +69,14 @@ func BenchmarkCreateSetupKey(b *testing.B) {
apiHandler.ServeHTTP(recorder, req)
}
- testing_tools.EvaluateBenchmarkResults(b, name, time.Since(start), expectedMetrics[name], recorder)
+ testing_tools.EvaluateBenchmarkResults(b, name, time.Since(start), recorder, moduleSetupKeys, testing_tools.OperationCreate)
})
}
}
func BenchmarkUpdateSetupKey(b *testing.B) {
- var expectedMetrics = map[string]testing_tools.PerformanceMetrics{
- "Setup Keys - XS": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 3, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 19},
- "Setup Keys - S": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 3, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 19},
- "Setup Keys - M": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 3, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 19},
- "Setup Keys - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 3, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 19},
- "Peers - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 3, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 19},
- "Groups - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 3, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 19},
- "Users - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 3, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 19},
- "Setup Keys - XL": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 3, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 19},
+ if os.Getenv("CI") != "true" {
+ b.Skip("Skipping because CI is not set")
}
log.SetOutput(io.Discard)
@@ -121,21 +109,14 @@ func BenchmarkUpdateSetupKey(b *testing.B) {
apiHandler.ServeHTTP(recorder, req)
}
- testing_tools.EvaluateBenchmarkResults(b, name, time.Since(start), expectedMetrics[name], recorder)
+ testing_tools.EvaluateBenchmarkResults(b, name, time.Since(start), recorder, moduleSetupKeys, testing_tools.OperationUpdate)
})
}
}
func BenchmarkGetOneSetupKey(b *testing.B) {
- var expectedMetrics = map[string]testing_tools.PerformanceMetrics{
- "Setup Keys - XS": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 16},
- "Setup Keys - S": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 16},
- "Setup Keys - M": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 16},
- "Setup Keys - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 16},
- "Peers - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 16},
- "Groups - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 16},
- "Users - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 16},
- "Setup Keys - XL": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 16},
+ if os.Getenv("CI") != "true" {
+ b.Skip("Skipping because CI is not set")
}
log.SetOutput(io.Discard)
@@ -155,21 +136,14 @@ func BenchmarkGetOneSetupKey(b *testing.B) {
apiHandler.ServeHTTP(recorder, req)
}
- testing_tools.EvaluateBenchmarkResults(b, name, time.Since(start), expectedMetrics[name], recorder)
+ testing_tools.EvaluateBenchmarkResults(b, name, time.Since(start), recorder, moduleSetupKeys, testing_tools.OperationGetOne)
})
}
}
func BenchmarkGetAllSetupKeys(b *testing.B) {
- var expectedMetrics = map[string]testing_tools.PerformanceMetrics{
- "Setup Keys - XS": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 12},
- "Setup Keys - S": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 15},
- "Setup Keys - M": {MinMsPerOpLocal: 5, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 40},
- "Setup Keys - L": {MinMsPerOpLocal: 30, MaxMsPerOpLocal: 50, MinMsPerOpCICD: 30, MaxMsPerOpCICD: 150},
- "Peers - L": {MinMsPerOpLocal: 30, MaxMsPerOpLocal: 50, MinMsPerOpCICD: 30, MaxMsPerOpCICD: 150},
- "Groups - L": {MinMsPerOpLocal: 30, MaxMsPerOpLocal: 50, MinMsPerOpCICD: 30, MaxMsPerOpCICD: 150},
- "Users - L": {MinMsPerOpLocal: 30, MaxMsPerOpLocal: 50, MinMsPerOpCICD: 30, MaxMsPerOpCICD: 150},
- "Setup Keys - XL": {MinMsPerOpLocal: 140, MaxMsPerOpLocal: 220, MinMsPerOpCICD: 150, MaxMsPerOpCICD: 500},
+ if os.Getenv("CI") != "true" {
+ b.Skip("Skipping because CI is not set")
}
log.SetOutput(io.Discard)
@@ -189,21 +163,14 @@ func BenchmarkGetAllSetupKeys(b *testing.B) {
apiHandler.ServeHTTP(recorder, req)
}
- testing_tools.EvaluateBenchmarkResults(b, name, time.Since(start), expectedMetrics[name], recorder)
+ testing_tools.EvaluateBenchmarkResults(b, name, time.Since(start), recorder, moduleSetupKeys, testing_tools.OperationGetAll)
})
}
}
func BenchmarkDeleteSetupKey(b *testing.B) {
- var expectedMetrics = map[string]testing_tools.PerformanceMetrics{
- "Setup Keys - XS": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 16},
- "Setup Keys - S": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 16},
- "Setup Keys - M": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 16},
- "Setup Keys - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 16},
- "Peers - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 16},
- "Groups - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 16},
- "Users - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 16},
- "Setup Keys - XL": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 16},
+ if os.Getenv("CI") != "true" {
+ b.Skip("Skipping because CI is not set")
}
log.SetOutput(io.Discard)
@@ -223,7 +190,7 @@ func BenchmarkDeleteSetupKey(b *testing.B) {
apiHandler.ServeHTTP(recorder, req)
}
- testing_tools.EvaluateBenchmarkResults(b, name, time.Since(start), expectedMetrics[name], recorder)
+ testing_tools.EvaluateBenchmarkResults(b, name, time.Since(start), recorder, moduleSetupKeys, testing_tools.OperationDelete)
})
}
}
diff --git a/management/server/http/testing/benchmarks/users_handler_benchmark_test.go b/management/server/http/testing/benchmarks/users_handler_benchmark_test.go
index b7deab334..c0b641a70 100644
--- a/management/server/http/testing/benchmarks/users_handler_benchmark_test.go
+++ b/management/server/http/testing/benchmarks/users_handler_benchmark_test.go
@@ -13,6 +13,7 @@ import (
"testing"
"time"
+ "github.com/prometheus/client_golang/prometheus/push"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
@@ -21,6 +22,8 @@ import (
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools"
)
+const moduleUsers = "users"
+
// Map to store peers, groups, users, and setupKeys by name
var benchCasesUsers = map[string]testing_tools.BenchmarkCase{
"Users - XS": {Peers: 10000, Groups: 10000, Users: 5, SetupKeys: 10000},
@@ -34,15 +37,8 @@ var benchCasesUsers = map[string]testing_tools.BenchmarkCase{
}
func BenchmarkUpdateUser(b *testing.B) {
- var expectedMetrics = map[string]testing_tools.PerformanceMetrics{
- "Users - XS": {MinMsPerOpLocal: 100, MaxMsPerOpLocal: 160, MinMsPerOpCICD: 100, MaxMsPerOpCICD: 310},
- "Users - S": {MinMsPerOpLocal: 0.3, MaxMsPerOpLocal: 3, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 15},
- "Users - M": {MinMsPerOpLocal: 1, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 3, MaxMsPerOpCICD: 20},
- "Users - L": {MinMsPerOpLocal: 5, MaxMsPerOpLocal: 20, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 50},
- "Peers - L": {MinMsPerOpLocal: 80, MaxMsPerOpLocal: 150, MinMsPerOpCICD: 80, MaxMsPerOpCICD: 310},
- "Groups - L": {MinMsPerOpLocal: 10, MaxMsPerOpLocal: 50, MinMsPerOpCICD: 20, MaxMsPerOpCICD: 120},
- "Setup Keys - L": {MinMsPerOpLocal: 5, MaxMsPerOpLocal: 20, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 50},
- "Users - XL": {MinMsPerOpLocal: 30, MaxMsPerOpLocal: 100, MinMsPerOpCICD: 60, MaxMsPerOpCICD: 280},
+ if os.Getenv("CI") != "true" {
+ b.Skip("Skipping because CI is not set")
}
log.SetOutput(io.Discard)
@@ -75,23 +71,13 @@ func BenchmarkUpdateUser(b *testing.B) {
apiHandler.ServeHTTP(recorder, req)
}
- testing_tools.EvaluateBenchmarkResults(b, name, time.Since(start), expectedMetrics[name], recorder)
+ testing_tools.EvaluateBenchmarkResults(b, name, time.Since(start), recorder, moduleUsers, testing_tools.OperationUpdate)
})
}
}
func BenchmarkGetOneUser(b *testing.B) {
b.Skip("Skipping benchmark as endpoint is missing")
- var expectedMetrics = map[string]testing_tools.PerformanceMetrics{
- "Users - XS": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 12},
- "Users - S": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 12},
- "Users - M": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 12},
- "Users - L": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 12},
- "Peers - L": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 12},
- "Groups - L": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 12},
- "Setup Keys - L": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 12},
- "Users - XL": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 12},
- }
log.SetOutput(io.Discard)
defer log.SetOutput(os.Stderr)
@@ -109,21 +95,14 @@ func BenchmarkGetOneUser(b *testing.B) {
apiHandler.ServeHTTP(recorder, req)
}
- testing_tools.EvaluateBenchmarkResults(b, name, time.Since(start), expectedMetrics[name], recorder)
+ testing_tools.EvaluateBenchmarkResults(b, name, time.Since(start), recorder, moduleUsers, testing_tools.OperationGetOne)
})
}
}
func BenchmarkGetAllUsers(b *testing.B) {
- var expectedMetrics = map[string]testing_tools.PerformanceMetrics{
- "Users - XS": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 75},
- "Users - S": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 75},
- "Users - M": {MinMsPerOpLocal: 3, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 75},
- "Users - L": {MinMsPerOpLocal: 10, MaxMsPerOpLocal: 20, MinMsPerOpCICD: 10, MaxMsPerOpCICD: 100},
- "Peers - L": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 25, MinMsPerOpCICD: 10, MaxMsPerOpCICD: 100},
- "Groups - L": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 25, MinMsPerOpCICD: 10, MaxMsPerOpCICD: 100},
- "Setup Keys - L": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 25, MinMsPerOpCICD: 10, MaxMsPerOpCICD: 100},
- "Users - XL": {MinMsPerOpLocal: 80, MaxMsPerOpLocal: 120, MinMsPerOpCICD: 50, MaxMsPerOpCICD: 300},
+ if os.Getenv("CI") != "true" {
+ b.Skip("Skipping because CI is not set")
}
log.SetOutput(io.Discard)
@@ -142,21 +121,14 @@ func BenchmarkGetAllUsers(b *testing.B) {
apiHandler.ServeHTTP(recorder, req)
}
- testing_tools.EvaluateBenchmarkResults(b, name, time.Since(start), expectedMetrics[name], recorder)
+ testing_tools.EvaluateBenchmarkResults(b, name, time.Since(start), recorder, moduleUsers, testing_tools.OperationGetAll)
})
}
}
func BenchmarkDeleteUsers(b *testing.B) {
- var expectedMetrics = map[string]testing_tools.PerformanceMetrics{
- "Users - XS": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50},
- "Users - S": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50},
- "Users - M": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50},
- "Users - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50},
- "Peers - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50},
- "Groups - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50},
- "Setup Keys - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50},
- "Users - XL": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50},
+ if os.Getenv("CI") != "true" {
+ b.Skip("Skipping because CI is not set")
}
log.SetOutput(io.Discard)
@@ -175,7 +147,32 @@ func BenchmarkDeleteUsers(b *testing.B) {
apiHandler.ServeHTTP(recorder, req)
}
- testing_tools.EvaluateBenchmarkResults(b, name, time.Since(start), expectedMetrics[name], recorder)
+ testing_tools.EvaluateBenchmarkResults(b, name, time.Since(start), recorder, moduleUsers, testing_tools.OperationDelete)
})
}
}
+
+func TestMain(m *testing.M) {
+ exitCode := m.Run()
+
+ if exitCode == 0 && os.Getenv("CI") == "true" {
+ runID := os.Getenv("GITHUB_RUN_ID")
+ storeEngine := os.Getenv("NETBIRD_STORE_ENGINE")
+ err := push.New("http://localhost:9091", "api_benchmark").
+ Collector(testing_tools.BenchmarkDuration).
+ Grouping("ci_run", runID).
+ Grouping("store_engine", storeEngine).
+ Push()
+ if err != nil {
+ log.Printf("Failed to push metrics: %v", err)
+ } else {
+ time.Sleep(1 * time.Minute)
+ _ = push.New("http://localhost:9091", "api_benchmark").
+ Grouping("ci_run", runID).
+ Grouping("store_engine", storeEngine).
+ Delete()
+ }
+ }
+
+ os.Exit(exitCode)
+}
diff --git a/management/server/http/testing/testing_tools/tools.go b/management/server/http/testing/testing_tools/tools.go
index 12e68e983..8c5d2e386 100644
--- a/management/server/http/testing/testing_tools/tools.go
+++ b/management/server/http/testing/testing_tools/tools.go
@@ -15,11 +15,11 @@ import (
"time"
"github.com/golang-jwt/jwt"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"github.com/netbirdio/management-integrations/integrations"
-
"github.com/netbirdio/netbird/management/server/peers"
"github.com/netbirdio/netbird/management/server/permissions"
"github.com/netbirdio/netbird/management/server/settings"
@@ -66,6 +66,20 @@ const (
ExpiredKeyId = "expiredKeyId"
ExistingKeyName = "existingKey"
+
+ OperationCreate = "create"
+ OperationUpdate = "update"
+ OperationDelete = "delete"
+ OperationGetOne = "get_one"
+ OperationGetAll = "get_all"
+)
+
+var BenchmarkDuration = prometheus.NewGaugeVec(
+ prometheus.GaugeOpts{
+ Name: "benchmark_duration_ms",
+ Help: "Benchmark duration per op in ms",
+ },
+ []string{"module", "operation", "test_case", "branch"},
)
type TB interface {
@@ -309,30 +323,25 @@ func PopulateTestData(b *testing.B, am *server.DefaultAccountManager, peers, gro
}
-func EvaluateBenchmarkResults(b *testing.B, name string, duration time.Duration, perfMetrics PerformanceMetrics, recorder *httptest.ResponseRecorder) {
+func EvaluateBenchmarkResults(b *testing.B, testCase string, duration time.Duration, recorder *httptest.ResponseRecorder, module string, operation string) {
b.Helper()
+ branch := os.Getenv("GIT_BRANCH")
+ if branch == "" {
+ b.Fatalf("environment variable GIT_BRANCH is not set")
+ }
+
if recorder.Code != http.StatusOK {
- b.Fatalf("Benchmark %s failed: unexpected status code %d", name, recorder.Code)
+ b.Fatalf("Benchmark %s failed: unexpected status code %d", testCase, recorder.Code)
}
msPerOp := float64(duration.Nanoseconds()) / float64(b.N) / 1e6
+
+ gauge := BenchmarkDuration.WithLabelValues(module, operation, testCase, branch)
+ gauge.Set(msPerOp)
+
b.ReportMetric(msPerOp, "ms/op")
- minExpected := perfMetrics.MinMsPerOpLocal
- maxExpected := perfMetrics.MaxMsPerOpLocal
- if os.Getenv("CI") == "true" {
- minExpected = perfMetrics.MinMsPerOpCICD
- maxExpected = perfMetrics.MaxMsPerOpCICD
- }
-
- if msPerOp < minExpected {
- b.Fatalf("Benchmark %s failed: too fast (%.2f ms/op, minimum %.2f ms/op)", name, msPerOp, minExpected)
- }
-
- if msPerOp > maxExpected {
- b.Fatalf("Benchmark %s failed: too slow (%.2f ms/op, maximum %.2f ms/op)", name, msPerOp, maxExpected)
- }
}
func mockValidateAndParseToken(_ context.Context, token string) (nbcontext.UserAuth, *jwt.Token, error) {
diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go
index 008a7059f..2b57e6888 100644
--- a/management/server/mock_server/account_mock.go
+++ b/management/server/mock_server/account_mock.go
@@ -44,8 +44,8 @@ type MockAccountManager struct {
GetGroupFunc func(ctx context.Context, accountID, groupID, userID string) (*types.Group, error)
GetAllGroupsFunc func(ctx context.Context, accountID, userID string) ([]*types.Group, error)
GetGroupByNameFunc func(ctx context.Context, accountID, groupName string) (*types.Group, error)
- SaveGroupFunc func(ctx context.Context, accountID, userID string, group *types.Group) error
- SaveGroupsFunc func(ctx context.Context, accountID, userID string, groups []*types.Group) error
+ SaveGroupFunc func(ctx context.Context, accountID, userID string, group *types.Group, create bool) error
+ SaveGroupsFunc func(ctx context.Context, accountID, userID string, groups []*types.Group, create bool) error
DeleteGroupFunc func(ctx context.Context, accountID, userId, groupID string) error
DeleteGroupsFunc func(ctx context.Context, accountId, userId string, groupIDs []string) error
GroupAddPeerFunc func(ctx context.Context, accountID, groupID, peerID string) error
@@ -53,7 +53,7 @@ type MockAccountManager struct {
GetPeerGroupsFunc func(ctx context.Context, accountID, peerID string) ([]*types.Group, error)
DeleteRuleFunc func(ctx context.Context, accountID, ruleID, userID string) error
GetPolicyFunc func(ctx context.Context, accountID, policyID, userID string) (*types.Policy, error)
- SavePolicyFunc func(ctx context.Context, accountID, userID string, policy *types.Policy) (*types.Policy, error)
+ SavePolicyFunc func(ctx context.Context, accountID, userID string, policy *types.Policy, create bool) (*types.Policy, error)
DeletePolicyFunc func(ctx context.Context, accountID, policyID, userID string) error
ListPoliciesFunc func(ctx context.Context, accountID, userID string) ([]*types.Policy, error)
GetUsersFromAccountFunc func(ctx context.Context, accountID, userID string) (map[string]*types.UserInfo, error)
@@ -83,7 +83,7 @@ type MockAccountManager struct {
CreateUserFunc func(ctx context.Context, accountID, userID string, key *types.UserInfo) (*types.UserInfo, error)
GetAccountIDFromUserAuthFunc func(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error)
DeleteAccountFunc func(ctx context.Context, accountID, userID string) error
- GetDNSDomainFunc func() string
+ GetDNSDomainFunc func(settings *types.Settings) string
StoreEventFunc func(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any)
GetEventsFunc func(ctx context.Context, accountID, userID string) ([]*activity.Event, error)
GetDNSSettingsFunc func(ctx context.Context, accountID, userID string) (*types.DNSSettings, error)
@@ -97,7 +97,7 @@ type MockAccountManager struct {
HasConnectedChannelFunc func(peerID string) bool
GetExternalCacheManagerFunc func() account.ExternalCacheManager
GetPostureChecksFunc func(ctx context.Context, accountID, postureChecksID, userID string) (*posture.Checks, error)
- SavePostureChecksFunc func(ctx context.Context, accountID, userID string, postureChecks *posture.Checks) (*posture.Checks, error)
+ SavePostureChecksFunc func(ctx context.Context, accountID, userID string, postureChecks *posture.Checks, create bool) (*posture.Checks, error)
DeletePostureChecksFunc func(ctx context.Context, accountID, postureChecksID, userID string) error
ListPostureChecksFunc func(ctx context.Context, accountID, userID string) ([]*posture.Checks, error)
GetIdpManagerFunc func() idp.Manager
@@ -115,6 +115,8 @@ type MockAccountManager struct {
CreateAccountByPrivateDomainFunc func(ctx context.Context, initiatorId, domain string) (*types.Account, error)
UpdateToPrimaryAccountFunc func(ctx context.Context, accountId string) (*types.Account, error)
GetOwnerInfoFunc func(ctx context.Context, accountID string) (*types.UserInfo, error)
+ GetCurrentUserInfoFunc func(ctx context.Context, accountID, userID string) (*types.UserInfo, error)
+ GetAccountMetaFunc func(ctx context.Context, accountID, userID string) (*types.AccountMeta, error)
}
func (am *MockAccountManager) UpdateAccountPeers(ctx context.Context, accountID string) {
@@ -322,17 +324,17 @@ func (am *MockAccountManager) GetGroupByName(ctx context.Context, accountID, gro
}
// SaveGroup mock implementation of SaveGroup from server.AccountManager interface
-func (am *MockAccountManager) SaveGroup(ctx context.Context, accountID, userID string, group *types.Group) error {
+func (am *MockAccountManager) SaveGroup(ctx context.Context, accountID, userID string, group *types.Group, create bool) error {
if am.SaveGroupFunc != nil {
- return am.SaveGroupFunc(ctx, accountID, userID, group)
+ return am.SaveGroupFunc(ctx, accountID, userID, group, create)
}
return status.Errorf(codes.Unimplemented, "method SaveGroup is not implemented")
}
// SaveGroups mock implementation of SaveGroups from server.AccountManager interface
-func (am *MockAccountManager) SaveGroups(ctx context.Context, accountID, userID string, groups []*types.Group) error {
+func (am *MockAccountManager) SaveGroups(ctx context.Context, accountID, userID string, groups []*types.Group, create bool) error {
if am.SaveGroupsFunc != nil {
- return am.SaveGroupsFunc(ctx, accountID, userID, groups)
+ return am.SaveGroupsFunc(ctx, accountID, userID, groups, create)
}
return status.Errorf(codes.Unimplemented, "method SaveGroups is not implemented")
}
@@ -386,9 +388,9 @@ func (am *MockAccountManager) GetPolicy(ctx context.Context, accountID, policyID
}
// SavePolicy mock implementation of SavePolicy from server.AccountManager interface
-func (am *MockAccountManager) SavePolicy(ctx context.Context, accountID, userID string, policy *types.Policy) (*types.Policy, error) {
+func (am *MockAccountManager) SavePolicy(ctx context.Context, accountID, userID string, policy *types.Policy, create bool) (*types.Policy, error) {
if am.SavePolicyFunc != nil {
- return am.SavePolicyFunc(ctx, accountID, userID, policy)
+ return am.SavePolicyFunc(ctx, accountID, userID, policy, create)
}
return nil, status.Errorf(codes.Unimplemented, "method SavePolicy is not implemented")
}
@@ -618,9 +620,9 @@ func (am *MockAccountManager) GetPeers(ctx context.Context, accountID, userID, n
}
// GetDNSDomain mocks GetDNSDomain of the AccountManager interface
-func (am *MockAccountManager) GetDNSDomain() string {
+func (am *MockAccountManager) GetDNSDomain(settings *types.Settings) string {
if am.GetDNSDomainFunc != nil {
- return am.GetDNSDomainFunc()
+ return am.GetDNSDomainFunc(settings)
}
return ""
}
@@ -722,9 +724,9 @@ func (am *MockAccountManager) GetPostureChecks(ctx context.Context, accountID, p
}
// SavePostureChecks mocks SavePostureChecks of the AccountManager interface
-func (am *MockAccountManager) SavePostureChecks(ctx context.Context, accountID, userID string, postureChecks *posture.Checks) (*posture.Checks, error) {
+func (am *MockAccountManager) SavePostureChecks(ctx context.Context, accountID, userID string, postureChecks *posture.Checks, create bool) (*posture.Checks, error) {
if am.SavePostureChecksFunc != nil {
- return am.SavePostureChecksFunc(ctx, accountID, userID, postureChecks)
+ return am.SavePostureChecksFunc(ctx, accountID, userID, postureChecks, create)
}
return nil, status.Errorf(codes.Unimplemented, "method SavePostureChecks is not implemented")
}
@@ -802,6 +804,14 @@ func (am *MockAccountManager) GetAccountByID(ctx context.Context, accountID stri
return nil, status.Errorf(codes.Unimplemented, "method GetAccountByID is not implemented")
}
+// GetAccountByID mocks GetAccountByID of the AccountManager interface
+func (am *MockAccountManager) GetAccountMeta(ctx context.Context, accountID string, userID string) (*types.AccountMeta, error) {
+ if am.GetAccountMetaFunc != nil {
+ return am.GetAccountMetaFunc(ctx, accountID, userID)
+ }
+ return nil, status.Errorf(codes.Unimplemented, "method GetAccountMeta is not implemented")
+}
+
// GetUserByID mocks GetUserByID of the AccountManager interface
func (am *MockAccountManager) GetUserByID(ctx context.Context, id string) (*types.User, error) {
if am.GetUserByIDFunc != nil {
@@ -871,3 +881,10 @@ func (am *MockAccountManager) GetOwnerInfo(ctx context.Context, accountId string
}
return nil, status.Errorf(codes.Unimplemented, "method GetOwnerInfo is not implemented")
}
+
+func (am *MockAccountManager) GetCurrentUserInfo(ctx context.Context, accountID, userID string) (*types.UserInfo, error) {
+ if am.GetCurrentUserInfoFunc != nil {
+ return am.GetCurrentUserInfoFunc(ctx, accountID, userID)
+ }
+ return nil, status.Errorf(codes.Unimplemented, "method GetCurrentUserInfo is not implemented")
+}
diff --git a/management/server/nameserver.go b/management/server/nameserver.go
index 773377f7a..797d7c11c 100644
--- a/management/server/nameserver.go
+++ b/management/server/nameserver.go
@@ -38,7 +38,7 @@ func (am *DefaultAccountManager) CreateNameServerGroup(ctx context.Context, acco
unlock := am.Store.AcquireWriteLockByUID(ctx, accountID)
defer unlock()
- allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Nameservers, operations.Write)
+ allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Nameservers, operations.Create)
if err != nil {
return nil, status.NewPermissionValidationError(err)
}
@@ -99,7 +99,7 @@ func (am *DefaultAccountManager) SaveNameServerGroup(ctx context.Context, accoun
return status.Errorf(status.InvalidArgument, "nameserver group provided is nil")
}
- allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Nameservers, operations.Write)
+ allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Nameservers, operations.Update)
if err != nil {
return status.NewPermissionValidationError(err)
}
@@ -149,7 +149,7 @@ func (am *DefaultAccountManager) DeleteNameServerGroup(ctx context.Context, acco
unlock := am.Store.AcquireWriteLockByUID(ctx, accountID)
defer unlock()
- allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Nameservers, operations.Write)
+ allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Nameservers, operations.Delete)
if err != nil {
return status.NewPermissionValidationError(err)
}
diff --git a/management/server/nameserver_test.go b/management/server/nameserver_test.go
index dd1149a03..1ba790797 100644
--- a/management/server/nameserver_test.go
+++ b/management/server/nameserver_test.go
@@ -965,7 +965,7 @@ func TestNameServerAccountPeersUpdate(t *testing.T) {
Name: "GroupB",
Peers: []string{peer1.ID, peer2.ID, peer3.ID},
},
- })
+ }, true)
assert.NoError(t, err)
updMsg := manager.peersUpdateManager.CreateChannel(context.Background(), peer1.ID)
diff --git a/management/server/networks/manager.go b/management/server/networks/manager.go
index eba3a1fe1..1c46e9281 100644
--- a/management/server/networks/manager.go
+++ b/management/server/networks/manager.go
@@ -60,7 +60,7 @@ func (m *managerImpl) GetAllNetworks(ctx context.Context, accountID, userID stri
}
func (m *managerImpl) CreateNetwork(ctx context.Context, userID string, network *types.Network) (*types.Network, error) {
- ok, err := m.permissionsManager.ValidateUserPermissions(ctx, network.AccountID, userID, modules.Networks, operations.Write)
+ ok, err := m.permissionsManager.ValidateUserPermissions(ctx, network.AccountID, userID, modules.Networks, operations.Create)
if err != nil {
return nil, status.NewPermissionValidationError(err)
}
@@ -96,7 +96,7 @@ func (m *managerImpl) GetNetwork(ctx context.Context, accountID, userID, network
}
func (m *managerImpl) UpdateNetwork(ctx context.Context, userID string, network *types.Network) (*types.Network, error) {
- ok, err := m.permissionsManager.ValidateUserPermissions(ctx, network.AccountID, userID, modules.Networks, operations.Write)
+ ok, err := m.permissionsManager.ValidateUserPermissions(ctx, network.AccountID, userID, modules.Networks, operations.Update)
if err != nil {
return nil, status.NewPermissionValidationError(err)
}
@@ -118,7 +118,7 @@ func (m *managerImpl) UpdateNetwork(ctx context.Context, userID string, network
}
func (m *managerImpl) DeleteNetwork(ctx context.Context, accountID, userID, networkID string) error {
- ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Networks, operations.Write)
+ ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Networks, operations.Delete)
if err != nil {
return status.NewPermissionValidationError(err)
}
diff --git a/management/server/networks/resources/manager.go b/management/server/networks/resources/manager.go
index 9efd1fae6..21d1e54de 100644
--- a/management/server/networks/resources/manager.go
+++ b/management/server/networks/resources/manager.go
@@ -95,7 +95,7 @@ func (m *managerImpl) GetAllResourceIDsInAccount(ctx context.Context, accountID,
}
func (m *managerImpl) CreateResource(ctx context.Context, userID string, resource *types.NetworkResource) (*types.NetworkResource, error) {
- ok, err := m.permissionsManager.ValidateUserPermissions(ctx, resource.AccountID, userID, modules.Networks, operations.Write)
+ ok, err := m.permissionsManager.ValidateUserPermissions(ctx, resource.AccountID, userID, modules.Networks, operations.Create)
if err != nil {
return nil, status.NewPermissionValidationError(err)
}
@@ -187,7 +187,7 @@ func (m *managerImpl) GetResource(ctx context.Context, accountID, userID, networ
}
func (m *managerImpl) UpdateResource(ctx context.Context, userID string, resource *types.NetworkResource) (*types.NetworkResource, error) {
- ok, err := m.permissionsManager.ValidateUserPermissions(ctx, resource.AccountID, userID, modules.Networks, operations.Write)
+ ok, err := m.permissionsManager.ValidateUserPermissions(ctx, resource.AccountID, userID, modules.Networks, operations.Update)
if err != nil {
return nil, status.NewPermissionValidationError(err)
}
@@ -307,7 +307,7 @@ func (m *managerImpl) updateResourceGroups(ctx context.Context, transaction stor
}
func (m *managerImpl) DeleteResource(ctx context.Context, accountID, userID, networkID, resourceID string) error {
- ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Networks, operations.Write)
+ ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Networks, operations.Delete)
if err != nil {
return status.NewPermissionValidationError(err)
}
diff --git a/management/server/networks/routers/manager.go b/management/server/networks/routers/manager.go
index 2c8f7f677..7b488b361 100644
--- a/management/server/networks/routers/manager.go
+++ b/management/server/networks/routers/manager.go
@@ -80,7 +80,7 @@ func (m *managerImpl) GetAllRoutersInAccount(ctx context.Context, accountID, use
}
func (m *managerImpl) CreateRouter(ctx context.Context, userID string, router *types.NetworkRouter) (*types.NetworkRouter, error) {
- ok, err := m.permissionsManager.ValidateUserPermissions(ctx, router.AccountID, userID, modules.Networks, operations.Write)
+ ok, err := m.permissionsManager.ValidateUserPermissions(ctx, router.AccountID, userID, modules.Networks, operations.Create)
if err != nil {
return nil, status.NewPermissionValidationError(err)
}
@@ -149,7 +149,7 @@ func (m *managerImpl) GetRouter(ctx context.Context, accountID, userID, networkI
}
func (m *managerImpl) UpdateRouter(ctx context.Context, userID string, router *types.NetworkRouter) (*types.NetworkRouter, error) {
- ok, err := m.permissionsManager.ValidateUserPermissions(ctx, router.AccountID, userID, modules.Networks, operations.Write)
+ ok, err := m.permissionsManager.ValidateUserPermissions(ctx, router.AccountID, userID, modules.Networks, operations.Update)
if err != nil {
return nil, status.NewPermissionValidationError(err)
}
@@ -195,7 +195,7 @@ func (m *managerImpl) UpdateRouter(ctx context.Context, userID string, router *t
}
func (m *managerImpl) DeleteRouter(ctx context.Context, accountID, userID, networkID, routerID string) error {
- ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Networks, operations.Write)
+ ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Networks, operations.Delete)
if err != nil {
return status.NewPermissionValidationError(err)
}
diff --git a/management/server/peer.go b/management/server/peer.go
index 05e3b176b..908610fbe 100644
--- a/management/server/peer.go
+++ b/management/server/peer.go
@@ -190,7 +190,7 @@ func (am *DefaultAccountManager) UpdatePeer(ctx context.Context, accountID, user
unlock := am.Store.AcquireWriteLockByUID(ctx, accountID)
defer unlock()
- allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Peers, operations.Write)
+ allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Peers, operations.Update)
if err != nil {
return nil, status.NewPermissionValidationError(err)
}
@@ -206,6 +206,7 @@ func (am *DefaultAccountManager) UpdatePeer(ctx context.Context, accountID, user
var sshChanged bool
var loginExpirationChanged bool
var inactivityExpirationChanged bool
+ var dnsDomain string
err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
peer, err = transaction.GetPeerByID(ctx, store.LockingStrengthUpdate, accountID, update.ID)
@@ -223,7 +224,9 @@ func (am *DefaultAccountManager) UpdatePeer(ctx context.Context, accountID, user
return err
}
- update, requiresPeerUpdates, err = am.integratedPeerValidator.ValidatePeer(ctx, update, peer, userID, accountID, am.GetDNSDomain(), peerGroupList, settings.Extra)
+ dnsDomain = am.GetDNSDomain(settings)
+
+ update, requiresPeerUpdates, err = am.integratedPeerValidator.ValidatePeer(ctx, update, peer, userID, accountID, dnsDomain, peerGroupList, settings.Extra)
if err != nil {
return err
}
@@ -276,11 +279,11 @@ func (am *DefaultAccountManager) UpdatePeer(ctx context.Context, accountID, user
if !peer.SSHEnabled {
event = activity.PeerSSHDisabled
}
- am.StoreEvent(ctx, userID, peer.IP.String(), accountID, event, peer.EventMeta(am.GetDNSDomain()))
+ am.StoreEvent(ctx, userID, peer.IP.String(), accountID, event, peer.EventMeta(dnsDomain))
}
if peerLabelChanged {
- am.StoreEvent(ctx, userID, peer.ID, accountID, activity.PeerRenamed, peer.EventMeta(am.GetDNSDomain()))
+ am.StoreEvent(ctx, userID, peer.ID, accountID, activity.PeerRenamed, peer.EventMeta(dnsDomain))
}
if loginExpirationChanged {
@@ -288,7 +291,7 @@ func (am *DefaultAccountManager) UpdatePeer(ctx context.Context, accountID, user
if !peer.LoginExpirationEnabled {
event = activity.PeerLoginExpirationDisabled
}
- am.StoreEvent(ctx, userID, peer.IP.String(), accountID, event, peer.EventMeta(am.GetDNSDomain()))
+ am.StoreEvent(ctx, userID, peer.IP.String(), accountID, event, peer.EventMeta(dnsDomain))
if peer.AddedWithSSOLogin() && peer.LoginExpirationEnabled && settings.PeerLoginExpirationEnabled {
am.checkAndSchedulePeerLoginExpiration(ctx, accountID)
@@ -300,7 +303,7 @@ func (am *DefaultAccountManager) UpdatePeer(ctx context.Context, accountID, user
if !peer.InactivityExpirationEnabled {
event = activity.PeerInactivityExpirationDisabled
}
- am.StoreEvent(ctx, userID, peer.IP.String(), accountID, event, peer.EventMeta(am.GetDNSDomain()))
+ am.StoreEvent(ctx, userID, peer.IP.String(), accountID, event, peer.EventMeta(dnsDomain))
if peer.AddedWithSSOLogin() && peer.InactivityExpirationEnabled && settings.PeerInactivityExpirationEnabled {
am.checkAndSchedulePeerInactivityExpiration(ctx, accountID)
@@ -321,7 +324,7 @@ func (am *DefaultAccountManager) DeletePeer(ctx context.Context, accountID, peer
unlock := am.Store.AcquireWriteLockByUID(ctx, accountID)
defer unlock()
- allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Peers, operations.Write)
+ allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Peers, operations.Delete)
if err != nil {
return status.NewPermissionValidationError(err)
}
@@ -413,7 +416,7 @@ func (am *DefaultAccountManager) GetNetworkMap(ctx context.Context, peerID strin
if err != nil {
return nil, err
}
- customZone := account.GetPeersCustomZone(ctx, am.dnsDomain)
+ customZone := account.GetPeersCustomZone(ctx, am.GetDNSDomain(account.Settings))
proxyNetworkMaps, err := am.proxyController.GetProxyNetworkMaps(ctx, account.Id)
if err != nil {
@@ -574,8 +577,13 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s
ExtraDNSLabels: peer.ExtraDNSLabels,
AllowExtraDNSLabels: allowExtraDNSLabels,
}
+ settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
+ if err != nil {
+ return fmt.Errorf("failed to get account settings: %w", err)
+ }
+
opEvent.TargetID = newPeer.ID
- opEvent.Meta = newPeer.EventMeta(am.GetDNSDomain())
+ opEvent.Meta = newPeer.EventMeta(am.GetDNSDomain(settings))
if !addedByUser {
opEvent.Meta["setup_key_name"] = setupKeyName
}
@@ -591,10 +599,6 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s
}
}
- settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
- if err != nil {
- return fmt.Errorf("failed to get account settings: %w", err)
- }
newPeer = am.integratedPeerValidator.PreparePeer(ctx, accountID, newPeer, groupsToAdd, settings.Extra)
err = transaction.AddPeerToAllGroup(ctx, store.LockingStrengthUpdate, accountID, newPeer.ID)
@@ -1024,7 +1028,7 @@ func (am *DefaultAccountManager) getValidatedPeerWithMap(ctx context.Context, is
return nil, nil, nil, err
}
- customZone := account.GetPeersCustomZone(ctx, am.dnsDomain)
+ customZone := account.GetPeersCustomZone(ctx, am.GetDNSDomain(account.Settings))
proxyNetworkMaps, err := am.proxyController.GetProxyNetworkMaps(ctx, account.Id)
if err != nil {
@@ -1060,7 +1064,12 @@ func (am *DefaultAccountManager) handleExpiredPeer(ctx context.Context, transact
log.WithContext(ctx).Debugf("failed to update user last login: %v", err)
}
- am.StoreEvent(ctx, user.Id, peer.ID, user.AccountID, activity.UserLoggedInPeer, peer.EventMeta(am.GetDNSDomain()))
+ settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthShare, peer.AccountID)
+ if err != nil {
+ return fmt.Errorf("failed to get account settings: %w", err)
+ }
+
+ am.StoreEvent(ctx, user.Id, peer.ID, user.AccountID, activity.UserLoggedInPeer, peer.EventMeta(am.GetDNSDomain(settings)))
return nil
}
@@ -1174,7 +1183,8 @@ func (am *DefaultAccountManager) UpdateAccountPeers(ctx context.Context, account
semaphore := make(chan struct{}, 10)
dnsCache := &DNSConfigCache{}
- customZone := account.GetPeersCustomZone(ctx, am.dnsDomain)
+ dnsDomain := am.GetDNSDomain(account.Settings)
+ customZone := account.GetPeersCustomZone(ctx, dnsDomain)
resourcePolicies := account.GetResourcePoliciesMap()
routers := account.GetResourceRoutersMap()
@@ -1215,7 +1225,7 @@ func (am *DefaultAccountManager) UpdateAccountPeers(ctx context.Context, account
return
}
- update := toSyncResponse(ctx, nil, p, nil, nil, remotePeerNetworkMap, am.GetDNSDomain(), postureChecks, dnsCache, account.Settings.RoutingPeerDNSResolutionEnabled, extraSetting)
+ update := toSyncResponse(ctx, nil, p, nil, nil, remotePeerNetworkMap, dnsDomain, postureChecks, dnsCache, account.Settings.RoutingPeerDNSResolutionEnabled, extraSetting)
am.peersUpdateManager.SendUpdate(ctx, p.ID, &UpdateMessage{Update: update, NetworkMap: remotePeerNetworkMap})
}(peer)
}
@@ -1270,7 +1280,8 @@ func (am *DefaultAccountManager) UpdateAccountPeer(ctx context.Context, accountI
}
dnsCache := &DNSConfigCache{}
- customZone := account.GetPeersCustomZone(ctx, am.dnsDomain)
+ dnsDomain := am.GetDNSDomain(account.Settings)
+ customZone := account.GetPeersCustomZone(ctx, dnsDomain)
resourcePolicies := account.GetResourcePoliciesMap()
routers := account.GetResourceRoutersMap()
@@ -1299,7 +1310,7 @@ func (am *DefaultAccountManager) UpdateAccountPeer(ctx context.Context, accountI
return
}
- update := toSyncResponse(ctx, nil, peer, nil, nil, remotePeerNetworkMap, am.GetDNSDomain(), postureChecks, dnsCache, account.Settings.RoutingPeerDNSResolutionEnabled, extraSettings)
+ update := toSyncResponse(ctx, nil, peer, nil, nil, remotePeerNetworkMap, dnsDomain, postureChecks, dnsCache, account.Settings.RoutingPeerDNSResolutionEnabled, extraSettings)
am.peersUpdateManager.SendUpdate(ctx, peer.ID, &UpdateMessage{Update: update, NetworkMap: remotePeerNetworkMap})
}
@@ -1484,6 +1495,12 @@ func isPeerInActiveGroup(ctx context.Context, transaction store.Store, accountID
func deletePeers(ctx context.Context, am *DefaultAccountManager, transaction store.Store, accountID, userID string, peers []*nbpeer.Peer) ([]func(), error) {
var peerDeletedEvents []func()
+ settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
+ if err != nil {
+ return nil, err
+ }
+ dnsDomain := am.GetDNSDomain(settings)
+
for _, peer := range peers {
if err := am.integratedPeerValidator.PeerDeleted(ctx, accountID, peer.ID); err != nil {
return nil, err
@@ -1514,7 +1531,7 @@ func deletePeers(ctx context.Context, am *DefaultAccountManager, transaction sto
})
am.peersUpdateManager.CloseChannel(ctx, peer.ID)
peerDeletedEvents = append(peerDeletedEvents, func() {
- am.StoreEvent(ctx, userID, peer.ID, accountID, activity.PeerRemovedByUser, peer.EventMeta(am.GetDNSDomain()))
+ am.StoreEvent(ctx, userID, peer.ID, accountID, activity.PeerRemovedByUser, peer.EventMeta(dnsDomain))
})
}
diff --git a/management/server/peer_test.go b/management/server/peer_test.go
index 0afaed829..406c3e49e 100644
--- a/management/server/peer_test.go
+++ b/management/server/peer_test.go
@@ -303,12 +303,12 @@ func TestAccountManager_GetNetworkMapWithPolicy(t *testing.T) {
group1.Peers = append(group1.Peers, peer1.ID)
group2.Peers = append(group2.Peers, peer2.ID)
- err = manager.SaveGroup(context.Background(), account.Id, userID, &group1)
+ err = manager.SaveGroup(context.Background(), account.Id, userID, &group1, true)
if err != nil {
t.Errorf("expecting group1 to be added, got failure %v", err)
return
}
- err = manager.SaveGroup(context.Background(), account.Id, userID, &group2)
+ err = manager.SaveGroup(context.Background(), account.Id, userID, &group2, true)
if err != nil {
t.Errorf("expecting group2 to be added, got failure %v", err)
return
@@ -327,7 +327,7 @@ func TestAccountManager_GetNetworkMapWithPolicy(t *testing.T) {
},
},
}
- policy, err = manager.SavePolicy(context.Background(), account.Id, userID, policy)
+ policy, err = manager.SavePolicy(context.Background(), account.Id, userID, policy, true)
if err != nil {
t.Errorf("expecting rule to be added, got failure %v", err)
return
@@ -375,7 +375,7 @@ func TestAccountManager_GetNetworkMapWithPolicy(t *testing.T) {
}
policy.Enabled = false
- _, err = manager.SavePolicy(context.Background(), account.Id, userID, policy)
+ _, err = manager.SavePolicy(context.Background(), account.Id, userID, policy, true)
if err != nil {
t.Errorf("expecting rule to be added, got failure %v", err)
return
@@ -1478,7 +1478,7 @@ func TestPeerAccountPeersUpdate(t *testing.T) {
Name: "GroupC",
Peers: []string{},
},
- })
+ }, true)
require.NoError(t, err)
// create a user with auto groups
@@ -1654,7 +1654,7 @@ func TestPeerAccountPeersUpdate(t *testing.T) {
Action: types.PolicyTrafficActionAccept,
},
},
- })
+ }, true)
require.NoError(t, err)
done := make(chan struct{})
diff --git a/management/server/permissions/operations/operation.go b/management/server/permissions/operations/operation.go
index af709de3a..11481234f 100644
--- a/management/server/permissions/operations/operation.go
+++ b/management/server/permissions/operations/operation.go
@@ -3,6 +3,8 @@ package operations
type Operation string
const (
- Read Operation = "read"
- Write Operation = "write"
+ Create Operation = "create"
+ Read Operation = "read"
+ Update Operation = "update"
+ Delete Operation = "delete"
)
diff --git a/management/server/permissions/roles/admin.go b/management/server/permissions/roles/admin.go
index a826d186a..af3a81297 100644
--- a/management/server/permissions/roles/admin.go
+++ b/management/server/permissions/roles/admin.go
@@ -9,13 +9,17 @@ import (
var Admin = RolePermissions{
Role: types.UserRoleAdmin,
AutoAllowNew: map[operations.Operation]bool{
- operations.Read: true,
- operations.Write: true,
+ operations.Read: true,
+ operations.Create: true,
+ operations.Update: true,
+ operations.Delete: true,
},
Permissions: Permissions{
modules.Accounts: {
- operations.Read: true,
- operations.Write: false,
+ operations.Read: true,
+ operations.Create: false,
+ operations.Update: false,
+ operations.Delete: false,
},
},
}
diff --git a/management/server/permissions/roles/auditor.go b/management/server/permissions/roles/auditor.go
new file mode 100644
index 000000000..33d8651f4
--- /dev/null
+++ b/management/server/permissions/roles/auditor.go
@@ -0,0 +1,16 @@
+package roles
+
+import (
+ "github.com/netbirdio/netbird/management/server/permissions/operations"
+ "github.com/netbirdio/netbird/management/server/types"
+)
+
+var Auditor = RolePermissions{
+ Role: types.UserRoleAuditor,
+ AutoAllowNew: map[operations.Operation]bool{
+ operations.Read: true,
+ operations.Create: false,
+ operations.Update: false,
+ operations.Delete: false,
+ },
+}
diff --git a/management/server/permissions/roles/network_admin.go b/management/server/permissions/roles/network_admin.go
new file mode 100644
index 000000000..761933386
--- /dev/null
+++ b/management/server/permissions/roles/network_admin.go
@@ -0,0 +1,91 @@
+package roles
+
+import (
+ "github.com/netbirdio/netbird/management/server/permissions/modules"
+ "github.com/netbirdio/netbird/management/server/permissions/operations"
+ "github.com/netbirdio/netbird/management/server/types"
+)
+
+var NetworkAdmin = RolePermissions{
+ Role: types.UserRoleNetworkAdmin,
+ AutoAllowNew: map[operations.Operation]bool{
+ operations.Read: false,
+ operations.Create: false,
+ operations.Update: false,
+ operations.Delete: false,
+ },
+ Permissions: Permissions{
+ modules.Networks: {
+ operations.Read: true,
+ operations.Create: true,
+ operations.Update: true,
+ operations.Delete: true,
+ },
+ modules.Groups: {
+ operations.Read: true,
+ operations.Create: false,
+ operations.Update: false,
+ operations.Delete: false,
+ },
+ modules.Settings: {
+ operations.Read: true,
+ operations.Create: false,
+ operations.Update: false,
+ operations.Delete: false,
+ },
+ modules.Accounts: {
+ operations.Read: true,
+ operations.Create: false,
+ operations.Update: false,
+ operations.Delete: false,
+ },
+ modules.Dns: {
+ operations.Read: true,
+ operations.Create: true,
+ operations.Update: true,
+ operations.Delete: true,
+ },
+ modules.Nameservers: {
+ operations.Read: true,
+ operations.Create: true,
+ operations.Update: true,
+ operations.Delete: true,
+ },
+ modules.Events: {
+ operations.Read: true,
+ operations.Create: false,
+ operations.Update: false,
+ operations.Delete: false,
+ },
+ modules.Policies: {
+ operations.Read: true,
+ operations.Create: true,
+ operations.Update: true,
+ operations.Delete: true,
+ },
+ modules.Routes: {
+ operations.Read: true,
+ operations.Create: true,
+ operations.Update: true,
+ operations.Delete: true,
+ },
+ modules.Users: {
+ operations.Read: true,
+ operations.Create: false,
+ operations.Update: false,
+ operations.Delete: false,
+ },
+ modules.SetupKeys: {
+ operations.Read: true,
+ operations.Create: false,
+ operations.Update: false,
+ operations.Delete: false,
+ },
+ modules.Pats: {
+ operations.Read: true,
+ operations.Create: true,
+ operations.Update: true,
+ operations.Delete: true,
+ },
+ },
+}
diff --git a/management/server/permissions/roles/owner.go b/management/server/permissions/roles/owner.go
index f739d18ea..668470e47 100644
--- a/management/server/permissions/roles/owner.go
+++ b/management/server/permissions/roles/owner.go
@@ -8,7 +8,9 @@ import (
var Owner = RolePermissions{
Role: types.UserRoleOwner,
AutoAllowNew: map[operations.Operation]bool{
- operations.Read: true,
- operations.Write: true,
+ operations.Read: true,
+ operations.Create: true,
+ operations.Update: true,
+ operations.Delete: true,
},
}
diff --git a/management/server/permissions/roles/role_permissions.go b/management/server/permissions/roles/role_permissions.go
index dda7e6b99..754e568f5 100644
--- a/management/server/permissions/roles/role_permissions.go
+++ b/management/server/permissions/roles/role_permissions.go
@@ -15,7 +15,9 @@ type RolePermissions struct {
type Permissions map[modules.Module]map[operations.Operation]bool
var RolesMap = map[types.UserRole]RolePermissions{
- types.UserRoleOwner: Owner,
- types.UserRoleAdmin: Admin,
- types.UserRoleUser: User,
+ types.UserRoleOwner: Owner,
+ types.UserRoleAdmin: Admin,
+ types.UserRoleUser: User,
+ types.UserRoleAuditor: Auditor,
+ types.UserRoleNetworkAdmin: NetworkAdmin,
}
diff --git a/management/server/permissions/roles/user.go b/management/server/permissions/roles/user.go
index 6e8a9307b..bb3df0aea 100644
--- a/management/server/permissions/roles/user.go
+++ b/management/server/permissions/roles/user.go
@@ -8,7 +8,9 @@ import (
var User = RolePermissions{
Role: types.UserRoleUser,
AutoAllowNew: map[operations.Operation]bool{
- operations.Read: false,
- operations.Write: false,
+ operations.Read: false,
+ operations.Create: false,
+ operations.Update: false,
+ operations.Delete: false,
},
}
diff --git a/management/server/policy.go b/management/server/policy.go
index 8f56bd493..1e9331d43 100644
--- a/management/server/policy.go
+++ b/management/server/policy.go
@@ -31,11 +31,15 @@ func (am *DefaultAccountManager) GetPolicy(ctx context.Context, accountID, polic
}
// SavePolicy in the store
-func (am *DefaultAccountManager) SavePolicy(ctx context.Context, accountID, userID string, policy *types.Policy) (*types.Policy, error) {
+func (am *DefaultAccountManager) SavePolicy(ctx context.Context, accountID, userID string, policy *types.Policy, create bool) (*types.Policy, error) {
unlock := am.Store.AcquireWriteLockByUID(ctx, accountID)
defer unlock()
- allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Policies, operations.Write)
+ operation := operations.Create
+ if !create {
+ operation = operations.Update
+ }
+ allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Policies, operation)
if err != nil {
return nil, status.NewPermissionValidationError(err)
}
@@ -87,7 +91,7 @@ func (am *DefaultAccountManager) DeletePolicy(ctx context.Context, accountID, po
unlock := am.Store.AcquireWriteLockByUID(ctx, accountID)
defer unlock()
- allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Policies, operations.Write)
+ allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Policies, operations.Delete)
if err != nil {
return status.NewPermissionValidationError(err)
}
diff --git a/management/server/policy_test.go b/management/server/policy_test.go
index 10b7fc2d1..0c1160cda 100644
--- a/management/server/policy_test.go
+++ b/management/server/policy_test.go
@@ -883,7 +883,7 @@ func TestPolicyAccountPeersUpdate(t *testing.T) {
Name: "GroupD",
Peers: []string{peer1.ID, peer2.ID},
},
- })
+ }, true)
assert.NoError(t, err)
updMsg := manager.peersUpdateManager.CreateChannel(context.Background(), peer1.ID)
@@ -915,7 +915,7 @@ func TestPolicyAccountPeersUpdate(t *testing.T) {
Action: types.PolicyTrafficActionAccept,
},
},
- })
+ }, true)
assert.NoError(t, err)
select {
@@ -947,7 +947,7 @@ func TestPolicyAccountPeersUpdate(t *testing.T) {
Action: types.PolicyTrafficActionAccept,
},
},
- })
+ }, true)
assert.NoError(t, err)
select {
@@ -979,7 +979,7 @@ func TestPolicyAccountPeersUpdate(t *testing.T) {
Action: types.PolicyTrafficActionAccept,
},
},
- })
+ }, true)
assert.NoError(t, err)
select {
@@ -1010,7 +1010,7 @@ func TestPolicyAccountPeersUpdate(t *testing.T) {
Action: types.PolicyTrafficActionAccept,
},
},
- })
+ }, true)
assert.NoError(t, err)
select {
@@ -1030,7 +1030,7 @@ func TestPolicyAccountPeersUpdate(t *testing.T) {
}()
policyWithSourceAndDestinationPeers.Enabled = false
- policyWithSourceAndDestinationPeers, err = manager.SavePolicy(context.Background(), account.Id, userID, policyWithSourceAndDestinationPeers)
+ policyWithSourceAndDestinationPeers, err = manager.SavePolicy(context.Background(), account.Id, userID, policyWithSourceAndDestinationPeers, true)
assert.NoError(t, err)
select {
@@ -1051,7 +1051,7 @@ func TestPolicyAccountPeersUpdate(t *testing.T) {
policyWithSourceAndDestinationPeers.Description = "updated description"
policyWithSourceAndDestinationPeers.Rules[0].Destinations = []string{"groupA"}
- policyWithSourceAndDestinationPeers, err = manager.SavePolicy(context.Background(), account.Id, userID, policyWithSourceAndDestinationPeers)
+ policyWithSourceAndDestinationPeers, err = manager.SavePolicy(context.Background(), account.Id, userID, policyWithSourceAndDestinationPeers, true)
assert.NoError(t, err)
select {
@@ -1071,7 +1071,7 @@ func TestPolicyAccountPeersUpdate(t *testing.T) {
}()
policyWithSourceAndDestinationPeers.Enabled = true
- policyWithSourceAndDestinationPeers, err = manager.SavePolicy(context.Background(), account.Id, userID, policyWithSourceAndDestinationPeers)
+ policyWithSourceAndDestinationPeers, err = manager.SavePolicy(context.Background(), account.Id, userID, policyWithSourceAndDestinationPeers, true)
assert.NoError(t, err)
select {
diff --git a/management/server/posture_checks.go b/management/server/posture_checks.go
index 9b8067b8c..f91e89b45 100644
--- a/management/server/posture_checks.go
+++ b/management/server/posture_checks.go
@@ -31,11 +31,15 @@ func (am *DefaultAccountManager) GetPostureChecks(ctx context.Context, accountID
}
// SavePostureChecks saves a posture check.
-func (am *DefaultAccountManager) SavePostureChecks(ctx context.Context, accountID, userID string, postureChecks *posture.Checks) (*posture.Checks, error) {
+func (am *DefaultAccountManager) SavePostureChecks(ctx context.Context, accountID, userID string, postureChecks *posture.Checks, create bool) (*posture.Checks, error) {
unlock := am.Store.AcquireWriteLockByUID(ctx, accountID)
defer unlock()
- allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Policies, operations.Write)
+ operation := operations.Create
+ if !create {
+ operation = operations.Update
+ }
+ allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Policies, operation)
if err != nil {
return nil, status.NewPermissionValidationError(err)
}
diff --git a/management/server/posture_checks_test.go b/management/server/posture_checks_test.go
index bad162f05..232955f7d 100644
--- a/management/server/posture_checks_test.go
+++ b/management/server/posture_checks_test.go
@@ -33,7 +33,7 @@ func TestDefaultAccountManager_PostureCheck(t *testing.T) {
t.Run("Generic posture check flow", func(t *testing.T) {
// regular users can not create checks
- _, err = am.SavePostureChecks(context.Background(), account.Id, regularUserID, &posture.Checks{})
+ _, err = am.SavePostureChecks(context.Background(), account.Id, regularUserID, &posture.Checks{}, true)
assert.Error(t, err)
// regular users cannot list check
@@ -48,7 +48,7 @@ func TestDefaultAccountManager_PostureCheck(t *testing.T) {
MinVersion: "0.26.0",
},
},
- })
+ }, true)
assert.NoError(t, err)
// admin users can list check
@@ -68,7 +68,7 @@ func TestDefaultAccountManager_PostureCheck(t *testing.T) {
},
},
},
- })
+ }, true)
assert.Error(t, err)
// admins can update posture checks
@@ -77,7 +77,7 @@ func TestDefaultAccountManager_PostureCheck(t *testing.T) {
MinVersion: "0.27.0",
},
}
- _, err = am.SavePostureChecks(context.Background(), account.Id, adminUserID, postureCheck)
+ _, err = am.SavePostureChecks(context.Background(), account.Id, adminUserID, postureCheck, true)
assert.NoError(t, err)
// users should not be able to delete posture checks
@@ -137,7 +137,7 @@ func TestPostureCheckAccountPeersUpdate(t *testing.T) {
Name: "GroupC",
Peers: []string{},
},
- })
+ }, true)
assert.NoError(t, err)
updMsg := manager.peersUpdateManager.CreateChannel(context.Background(), peer1.ID)
@@ -156,7 +156,7 @@ func TestPostureCheckAccountPeersUpdate(t *testing.T) {
},
},
}
- postureCheckA, err = manager.SavePostureChecks(context.Background(), account.Id, userID, postureCheckA)
+ postureCheckA, err = manager.SavePostureChecks(context.Background(), account.Id, userID, postureCheckA, true)
require.NoError(t, err)
postureCheckB := &posture.Checks{
@@ -177,7 +177,7 @@ func TestPostureCheckAccountPeersUpdate(t *testing.T) {
close(done)
}()
- postureCheckB, err = manager.SavePostureChecks(context.Background(), account.Id, userID, postureCheckB)
+ postureCheckB, err = manager.SavePostureChecks(context.Background(), account.Id, userID, postureCheckB, true)
assert.NoError(t, err)
select {
@@ -200,7 +200,7 @@ func TestPostureCheckAccountPeersUpdate(t *testing.T) {
MinVersion: "0.29.0",
},
}
- _, err = manager.SavePostureChecks(context.Background(), account.Id, userID, postureCheckB)
+ _, err = manager.SavePostureChecks(context.Background(), account.Id, userID, postureCheckB, true)
assert.NoError(t, err)
select {
@@ -232,7 +232,7 @@ func TestPostureCheckAccountPeersUpdate(t *testing.T) {
close(done)
}()
- policy, err = manager.SavePolicy(context.Background(), account.Id, userID, policy)
+ policy, err = manager.SavePolicy(context.Background(), account.Id, userID, policy, true)
assert.NoError(t, err)
select {
@@ -261,7 +261,7 @@ func TestPostureCheckAccountPeersUpdate(t *testing.T) {
close(done)
}()
- _, err = manager.SavePostureChecks(context.Background(), account.Id, userID, postureCheckB)
+ _, err = manager.SavePostureChecks(context.Background(), account.Id, userID, postureCheckB, true)
assert.NoError(t, err)
select {
@@ -280,7 +280,7 @@ func TestPostureCheckAccountPeersUpdate(t *testing.T) {
}()
policy.SourcePostureChecks = []string{}
- _, err := manager.SavePolicy(context.Background(), account.Id, userID, policy)
+ _, err := manager.SavePolicy(context.Background(), account.Id, userID, policy, true)
assert.NoError(t, err)
select {
@@ -308,7 +308,7 @@ func TestPostureCheckAccountPeersUpdate(t *testing.T) {
}
})
- _, err = manager.SavePostureChecks(context.Background(), account.Id, userID, postureCheckB)
+ _, err = manager.SavePostureChecks(context.Background(), account.Id, userID, postureCheckB, true)
assert.NoError(t, err)
// Updating linked posture check to policy with no peers should not trigger account peers update and not send peer update
@@ -325,7 +325,7 @@ func TestPostureCheckAccountPeersUpdate(t *testing.T) {
},
},
SourcePostureChecks: []string{postureCheckB.ID},
- })
+ }, true)
assert.NoError(t, err)
done := make(chan struct{})
@@ -339,7 +339,7 @@ func TestPostureCheckAccountPeersUpdate(t *testing.T) {
MinVersion: "0.29.0",
},
}
- _, err = manager.SavePostureChecks(context.Background(), account.Id, userID, postureCheckB)
+ _, err = manager.SavePostureChecks(context.Background(), account.Id, userID, postureCheckB, true)
assert.NoError(t, err)
select {
@@ -369,7 +369,7 @@ func TestPostureCheckAccountPeersUpdate(t *testing.T) {
},
},
SourcePostureChecks: []string{postureCheckB.ID},
- })
+ }, true)
assert.NoError(t, err)
done := make(chan struct{})
@@ -383,7 +383,7 @@ func TestPostureCheckAccountPeersUpdate(t *testing.T) {
MinVersion: "0.29.0",
},
}
- _, err = manager.SavePostureChecks(context.Background(), account.Id, userID, postureCheckB)
+ _, err = manager.SavePostureChecks(context.Background(), account.Id, userID, postureCheckB, true)
assert.NoError(t, err)
select {
@@ -408,7 +408,7 @@ func TestPostureCheckAccountPeersUpdate(t *testing.T) {
},
},
SourcePostureChecks: []string{postureCheckB.ID},
- })
+ }, true)
assert.NoError(t, err)
done := make(chan struct{})
@@ -426,7 +426,7 @@ func TestPostureCheckAccountPeersUpdate(t *testing.T) {
},
},
}
- _, err = manager.SavePostureChecks(context.Background(), account.Id, userID, postureCheckB)
+ _, err = manager.SavePostureChecks(context.Background(), account.Id, userID, postureCheckB, true)
assert.NoError(t, err)
select {
@@ -465,7 +465,7 @@ func TestArePostureCheckChangesAffectPeers(t *testing.T) {
NBVersionCheck: &posture.NBVersionCheck{MinVersion: "0.33.1"},
},
}
- postureCheckA, err = manager.SavePostureChecks(context.Background(), account.Id, adminUserID, postureCheckA)
+ postureCheckA, err = manager.SavePostureChecks(context.Background(), account.Id, adminUserID, postureCheckA, true)
require.NoError(t, err, "failed to save postureCheckA")
postureCheckB := &posture.Checks{
@@ -475,7 +475,7 @@ func TestArePostureCheckChangesAffectPeers(t *testing.T) {
NBVersionCheck: &posture.NBVersionCheck{MinVersion: "0.33.1"},
},
}
- postureCheckB, err = manager.SavePostureChecks(context.Background(), account.Id, adminUserID, postureCheckB)
+ postureCheckB, err = manager.SavePostureChecks(context.Background(), account.Id, adminUserID, postureCheckB, true)
require.NoError(t, err, "failed to save postureCheckB")
policy := &types.Policy{
@@ -490,7 +490,7 @@ func TestArePostureCheckChangesAffectPeers(t *testing.T) {
SourcePostureChecks: []string{postureCheckA.ID},
}
- policy, err = manager.SavePolicy(context.Background(), account.Id, adminUserID, policy)
+ policy, err = manager.SavePolicy(context.Background(), account.Id, adminUserID, policy, true)
require.NoError(t, err, "failed to save policy")
t.Run("posture check exists and is linked to policy with peers", func(t *testing.T) {
@@ -514,7 +514,7 @@ func TestArePostureCheckChangesAffectPeers(t *testing.T) {
t.Run("posture check is linked to policy with no peers in source groups", func(t *testing.T) {
policy.Rules[0].Sources = []string{"groupB"}
policy.Rules[0].Destinations = []string{"groupA"}
- _, err = manager.SavePolicy(context.Background(), account.Id, adminUserID, policy)
+ _, err = manager.SavePolicy(context.Background(), account.Id, adminUserID, policy, true)
require.NoError(t, err, "failed to update policy")
result, err := arePostureCheckChangesAffectPeers(context.Background(), manager.Store, account.Id, postureCheckA.ID)
@@ -525,7 +525,7 @@ func TestArePostureCheckChangesAffectPeers(t *testing.T) {
t.Run("posture check is linked to policy with no peers in destination groups", func(t *testing.T) {
policy.Rules[0].Sources = []string{"groupA"}
policy.Rules[0].Destinations = []string{"groupB"}
- _, err = manager.SavePolicy(context.Background(), account.Id, adminUserID, policy)
+ _, err = manager.SavePolicy(context.Background(), account.Id, adminUserID, policy, true)
require.NoError(t, err, "failed to update policy")
result, err := arePostureCheckChangesAffectPeers(context.Background(), manager.Store, account.Id, postureCheckA.ID)
@@ -546,7 +546,7 @@ func TestArePostureCheckChangesAffectPeers(t *testing.T) {
t.Run("posture check is linked to policy with non-existent group", func(t *testing.T) {
policy.Rules[0].Sources = []string{"nonExistentGroup"}
policy.Rules[0].Destinations = []string{"nonExistentGroup"}
- _, err = manager.SavePolicy(context.Background(), account.Id, adminUserID, policy)
+ _, err = manager.SavePolicy(context.Background(), account.Id, adminUserID, policy, true)
require.NoError(t, err, "failed to update policy")
result, err := arePostureCheckChangesAffectPeers(context.Background(), manager.Store, account.Id, postureCheckA.ID)
diff --git a/management/server/route.go b/management/server/route.go
index 453da92b3..02755a708 100644
--- a/management/server/route.go
+++ b/management/server/route.go
@@ -120,7 +120,7 @@ func (am *DefaultAccountManager) CreateRoute(ctx context.Context, accountID stri
unlock := am.Store.AcquireWriteLockByUID(ctx, accountID)
defer unlock()
- allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Routes, operations.Write)
+ allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Routes, operations.Create)
if err != nil {
return nil, status.NewPermissionValidationError(err)
}
@@ -238,7 +238,7 @@ func (am *DefaultAccountManager) SaveRoute(ctx context.Context, accountID, userI
return status.Errorf(status.InvalidArgument, "identifier should be between 1 and %d", route.MaxNetIDChar)
}
- allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Routes, operations.Write)
+ allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Routes, operations.Update)
if err != nil {
return status.NewPermissionValidationError(err)
}
@@ -313,7 +313,7 @@ func (am *DefaultAccountManager) DeleteRoute(ctx context.Context, accountID stri
unlock := am.Store.AcquireWriteLockByUID(ctx, accountID)
defer unlock()
- allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Routes, operations.Write)
+ allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Routes, operations.Delete)
if err != nil {
return status.NewPermissionValidationError(err)
}
@@ -398,7 +398,9 @@ func toProtocolRoutesFirewallRules(rules []*types.RouteFirewallRule) []*proto.Ro
Protocol: getProtoProtocol(rule.Protocol),
PortInfo: getProtoPortInfo(rule),
IsDynamic: rule.IsDynamic,
+ Domains: rule.Domains.ToPunycodeList(),
PolicyID: []byte(rule.PolicyID),
+ RouteID: string(rule.RouteID),
}
}
diff --git a/management/server/route_test.go b/management/server/route_test.go
index 351dad8f7..833477b55 100644
--- a/management/server/route_test.go
+++ b/management/server/route_test.go
@@ -1215,7 +1215,7 @@ func TestGetNetworkMap_RouteSync(t *testing.T) {
Name: "peer1 group",
Peers: []string{peer1ID},
}
- err = am.SaveGroup(context.Background(), account.Id, userID, newGroup)
+ err = am.SaveGroup(context.Background(), account.Id, userID, newGroup, true)
require.NoError(t, err)
rules, err := am.ListPolicies(context.Background(), account.Id, "testingUser")
@@ -1227,7 +1227,7 @@ func TestGetNetworkMap_RouteSync(t *testing.T) {
newPolicy.Rules[0].Sources = []string{newGroup.ID}
newPolicy.Rules[0].Destinations = []string{newGroup.ID}
- _, err = am.SavePolicy(context.Background(), account.Id, userID, newPolicy)
+ _, err = am.SavePolicy(context.Background(), account.Id, userID, newPolicy, true)
require.NoError(t, err)
err = am.DeletePolicy(context.Background(), account.Id, defaultRule.ID, userID)
@@ -1505,7 +1505,7 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*types.Accou
}
for _, group := range newGroup {
- err = am.SaveGroup(context.Background(), accountID, userID, group)
+ err = am.SaveGroup(context.Background(), accountID, userID, group, true)
if err != nil {
return nil, err
}
@@ -1850,6 +1850,7 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) {
Destination: "192.168.0.0/16",
Protocol: "all",
Port: 80,
+ RouteID: "route1:peerA",
},
{
SourceRanges: []string{
@@ -1861,6 +1862,7 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) {
Destination: "192.168.0.0/16",
Protocol: "all",
Port: 320,
+ RouteID: "route1:peerA",
},
}
additionalFirewallRule := []*types.RouteFirewallRule{
@@ -1872,6 +1874,7 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) {
Destination: "192.168.10.0/16",
Protocol: "tcp",
Port: 80,
+ RouteID: "route4:peerA",
},
{
SourceRanges: []string{
@@ -1880,6 +1883,7 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) {
Action: "accept",
Destination: "192.168.10.0/16",
Protocol: "all",
+ RouteID: "route4:peerA",
},
}
@@ -1888,6 +1892,9 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) {
// peerD is also the routing peer for route1, should contain same routes firewall rules as peerA
routesFirewallRules = account.GetPeerRoutesFirewallRules(context.Background(), "peerD", validatedPeers)
assert.Len(t, routesFirewallRules, 2)
+ for _, rule := range expectedRoutesFirewallRules {
+ rule.RouteID = "route1:peerD"
+ }
assert.ElementsMatch(t, orderRuleSourceRanges(routesFirewallRules), orderRuleSourceRanges(expectedRoutesFirewallRules))
// peerE is a single routing peer for route 2 and route 3
@@ -1901,6 +1908,7 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) {
Destination: existingNetwork.String(),
Protocol: "tcp",
PortRange: types.RulePortRange{Start: 80, End: 350},
+ RouteID: "route2",
},
{
SourceRanges: []string{"0.0.0.0/0"},
@@ -1909,6 +1917,7 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) {
Protocol: "all",
Domains: domain.List{"example.com"},
IsDynamic: true,
+ RouteID: "route3",
},
{
SourceRanges: []string{"::/0"},
@@ -1917,6 +1926,7 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) {
Protocol: "all",
Domains: domain.List{"example.com"},
IsDynamic: true,
+ RouteID: "route3",
},
}
assert.ElementsMatch(t, orderRuleSourceRanges(routesFirewallRules), orderRuleSourceRanges(expectedRoutesFirewallRules))
@@ -1959,7 +1969,7 @@ func TestRouteAccountPeersUpdate(t *testing.T) {
Name: "GroupC",
Peers: []string{},
},
- })
+ }, true)
assert.NoError(t, err)
updMsg := manager.peersUpdateManager.CreateChannel(context.Background(), peer1ID)
@@ -2143,7 +2153,7 @@ func TestRouteAccountPeersUpdate(t *testing.T) {
ID: "groupB",
Name: "GroupB",
Peers: []string{peer1ID},
- })
+ }, true)
assert.NoError(t, err)
select {
@@ -2183,7 +2193,7 @@ func TestRouteAccountPeersUpdate(t *testing.T) {
ID: "groupC",
Name: "GroupC",
Peers: []string{peer1ID},
- })
+ }, true)
assert.NoError(t, err)
select {
@@ -2676,6 +2686,7 @@ func TestAccount_GetPeerNetworkResourceFirewallRules(t *testing.T) {
Destination: "192.168.0.0/16",
Protocol: "all",
Port: 80,
+ RouteID: "resource2:peerA",
},
{
SourceRanges: []string{
@@ -2687,6 +2698,7 @@ func TestAccount_GetPeerNetworkResourceFirewallRules(t *testing.T) {
Destination: "192.168.0.0/16",
Protocol: "all",
Port: 320,
+ RouteID: "resource2:peerA",
},
}
@@ -2701,6 +2713,7 @@ func TestAccount_GetPeerNetworkResourceFirewallRules(t *testing.T) {
Port: 80,
Domains: domain.List{"example.com"},
IsDynamic: true,
+ RouteID: "resource4:peerA",
},
{
SourceRanges: []string{
@@ -2711,6 +2724,7 @@ func TestAccount_GetPeerNetworkResourceFirewallRules(t *testing.T) {
Protocol: "all",
Domains: domain.List{"example.com"},
IsDynamic: true,
+ RouteID: "resource4:peerA",
},
}
assert.ElementsMatch(t, orderRuleSourceRanges(firewallRules), orderRuleSourceRanges(append(expectedFirewallRules, additionalFirewallRules...)))
@@ -2719,6 +2733,9 @@ func TestAccount_GetPeerNetworkResourceFirewallRules(t *testing.T) {
_, routes, sourcePeers = account.GetNetworkResourcesRoutesToSync(context.Background(), "peerD", resourcePoliciesMap, resourceRoutersMap)
firewallRules = account.GetPeerNetworkResourceFirewallRules(context.Background(), account.Peers["peerD"], validatedPeers, routes, resourcePoliciesMap)
assert.Len(t, firewallRules, 2)
+ for _, rule := range expectedFirewallRules {
+ rule.RouteID = "resource2:peerD"
+ }
assert.ElementsMatch(t, orderRuleSourceRanges(firewallRules), orderRuleSourceRanges(expectedFirewallRules))
assert.Len(t, sourcePeers, 3)
@@ -2736,6 +2753,7 @@ func TestAccount_GetPeerNetworkResourceFirewallRules(t *testing.T) {
Destination: "10.10.10.0/24",
Protocol: "tcp",
PortRange: types.RulePortRange{Start: 80, End: 350},
+ RouteID: "resource1:peerE",
},
}
assert.ElementsMatch(t, orderRuleSourceRanges(firewallRules), orderRuleSourceRanges(expectedFirewallRules))
@@ -2758,6 +2776,7 @@ func TestAccount_GetPeerNetworkResourceFirewallRules(t *testing.T) {
Destination: "10.12.12.1/32",
Protocol: "tcp",
Port: 8080,
+ RouteID: "resource5:peerL",
},
}
assert.ElementsMatch(t, orderRuleSourceRanges(firewallRules), orderRuleSourceRanges(expectedFirewallRules))
diff --git a/management/server/setupkey.go b/management/server/setupkey.go
index f205a170f..b0903c8d0 100644
--- a/management/server/setupkey.go
+++ b/management/server/setupkey.go
@@ -58,7 +58,7 @@ func (am *DefaultAccountManager) CreateSetupKey(ctx context.Context, accountID s
unlock := am.Store.AcquireWriteLockByUID(ctx, accountID)
defer unlock()
- allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.SetupKeys, operations.Write)
+ allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.SetupKeys, operations.Create)
if err != nil {
return nil, status.NewPermissionValidationError(err)
}
@@ -110,7 +110,7 @@ func (am *DefaultAccountManager) SaveSetupKey(ctx context.Context, accountID str
unlock := am.Store.AcquireWriteLockByUID(ctx, accountID)
defer unlock()
- allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.SetupKeys, operations.Write)
+ allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.SetupKeys, operations.Update)
if err != nil {
return nil, status.NewPermissionValidationError(err)
}
@@ -203,7 +203,7 @@ func (am *DefaultAccountManager) GetSetupKey(ctx context.Context, accountID, use
// DeleteSetupKey removes the setup key from the account
func (am *DefaultAccountManager) DeleteSetupKey(ctx context.Context, accountID, userID, keyID string) error {
- allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.SetupKeys, operations.Write)
+ allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.SetupKeys, operations.Delete)
if err != nil {
return status.NewPermissionValidationError(err)
}
diff --git a/management/server/setupkey_test.go b/management/server/setupkey_test.go
index 6e1e1cf7d..a561de40d 100644
--- a/management/server/setupkey_test.go
+++ b/management/server/setupkey_test.go
@@ -41,7 +41,7 @@ func TestDefaultAccountManager_SaveSetupKey(t *testing.T) {
Name: "group_name_2",
Peers: []string{},
},
- })
+ }, true)
if err != nil {
t.Fatal(err)
}
@@ -109,7 +109,7 @@ func TestDefaultAccountManager_CreateSetupKey(t *testing.T) {
ID: "group_1",
Name: "group_name_1",
Peers: []string{},
- })
+ }, true)
if err != nil {
t.Fatal(err)
}
@@ -118,7 +118,7 @@ func TestDefaultAccountManager_CreateSetupKey(t *testing.T) {
ID: "group_2",
Name: "group_name_2",
Peers: []string{},
- })
+ }, true)
if err != nil {
t.Fatal(err)
}
@@ -403,7 +403,7 @@ func TestSetupKeyAccountPeersUpdate(t *testing.T) {
ID: "groupA",
Name: "GroupA",
Peers: []string{peer1.ID, peer2.ID, peer3.ID},
- })
+ }, true)
assert.NoError(t, err)
policy := &types.Policy{
@@ -418,7 +418,7 @@ func TestSetupKeyAccountPeersUpdate(t *testing.T) {
},
},
}
- _, err = manager.SavePolicy(context.Background(), account.Id, userID, policy)
+ _, err = manager.SavePolicy(context.Background(), account.Id, userID, policy, true)
require.NoError(t, err)
updMsg := manager.peersUpdateManager.CreateChannel(context.Background(), peer1.ID)
diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go
index aacb56ab8..7d3b288e0 100644
--- a/management/server/store/sql_store.go
+++ b/management/server/store/sql_store.go
@@ -658,6 +658,21 @@ func (s *SqlStore) GetAllAccounts(ctx context.Context) (all []*types.Account) {
return all
}
+func (s *SqlStore) GetAccountMeta(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.AccountMeta, error) {
+ var accountMeta types.AccountMeta
+ result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&types.Account{}).
+ First(&accountMeta, idQueryCondition, accountID)
+ if result.Error != nil {
+ log.WithContext(ctx).Errorf("error when getting account meta %s from the store: %s", accountID, result.Error)
+ if errors.Is(result.Error, gorm.ErrRecordNotFound) {
+ return nil, status.NewAccountNotFoundError(accountID)
+ }
+ return nil, status.NewGetAccountFromStoreError(result.Error)
+ }
+
+ return &accountMeta, nil
+}
+
func (s *SqlStore) GetAccount(ctx context.Context, accountID string) (*types.Account, error) {
start := time.Now()
defer func() {
@@ -785,6 +800,19 @@ func (s *SqlStore) GetAccountByPeerPubKey(ctx context.Context, peerKey string) (
return s.GetAccount(ctx, peer.AccountID)
}
+func (s *SqlStore) GetAnyAccountID(ctx context.Context) (string, error) {
+ var account types.Account
+ result := s.db.WithContext(ctx).Select("id").Limit(1).Find(&account)
+ if result.Error != nil {
+ return "", status.NewGetAccountFromStoreError(result.Error)
+ }
+ if result.RowsAffected == 0 {
+ return "", status.Errorf(status.NotFound, "account not found: index lookup failed")
+ }
+
+ return account.Id, nil
+}
+
func (s *SqlStore) GetAccountIDByPeerPubKey(ctx context.Context, peerKey string) (string, error) {
var peer nbpeer.Peer
var accountID string
diff --git a/management/server/store/sql_store_test.go b/management/server/store/sql_store_test.go
index 589e727e9..8bd8ce098 100644
--- a/management/server/store/sql_store_test.go
+++ b/management/server/store/sql_store_test.go
@@ -3247,3 +3247,44 @@ func TestSqlStore_SaveGroups_LargeBatch(t *testing.T) {
require.NoError(t, err)
require.Equal(t, 8003, len(accountGroups))
}
+
+func TestSqlStore_GetAccountMeta(t *testing.T) {
+ store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir())
+ t.Cleanup(cleanup)
+ require.NoError(t, err)
+
+ accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b"
+ accountMeta, err := store.GetAccountMeta(context.Background(), LockingStrengthShare, accountID)
+ require.NoError(t, err)
+ require.NotNil(t, accountMeta)
+ require.Equal(t, accountID, accountMeta.AccountID)
+ require.Equal(t, "edafee4e-63fb-11ec-90d6-0242ac120003", accountMeta.CreatedBy)
+ require.Equal(t, "test.com", accountMeta.Domain)
+ require.Equal(t, "private", accountMeta.DomainCategory)
+ require.Equal(t, time.Date(2024, time.October, 2, 14, 1, 38, 210000000, time.UTC), accountMeta.CreatedAt.UTC())
+}
+
+func TestSqlStore_GetAnyAccountID(t *testing.T) {
+ t.Run("should return account ID when accounts exist", func(t *testing.T) {
+ store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir())
+ t.Cleanup(cleanup)
+ require.NoError(t, err)
+
+ accountID, err := store.GetAnyAccountID(context.Background())
+ require.NoError(t, err)
+ assert.Equal(t, "bf1c8084-ba50-4ce7-9439-34653001fc3b", accountID)
+ })
+
+ t.Run("should return error when no accounts exist", func(t *testing.T) {
+ store, cleanup, err := NewTestStoreFromSQL(context.Background(), "", t.TempDir())
+ t.Cleanup(cleanup)
+ require.NoError(t, err)
+
+ accountID, err := store.GetAnyAccountID(context.Background())
+ require.Error(t, err)
+ sErr, ok := status.FromError(err)
+ assert.True(t, ok)
+ assert.Equal(t, sErr.Type(), status.NotFound)
+ assert.Empty(t, accountID)
+ })
+}
diff --git a/management/server/store/store.go b/management/server/store/store.go
index a071c1325..73b366831 100644
--- a/management/server/store/store.go
+++ b/management/server/store/store.go
@@ -50,10 +50,12 @@ type Store interface {
GetAccountsCounter(ctx context.Context) (int64, error)
GetAllAccounts(ctx context.Context) []*types.Account
GetAccount(ctx context.Context, accountID string) (*types.Account, error)
+ GetAccountMeta(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.AccountMeta, error)
AccountExists(ctx context.Context, lockStrength LockingStrength, id string) (bool, error)
GetAccountDomainAndCategory(ctx context.Context, lockStrength LockingStrength, accountID string) (string, string, error)
GetAccountByUser(ctx context.Context, userID string) (*types.Account, error)
GetAccountByPeerPubKey(ctx context.Context, peerKey string) (*types.Account, error)
+ GetAnyAccountID(ctx context.Context) (string, error)
GetAccountIDByPeerPubKey(ctx context.Context, peerKey string) (string, error)
GetAccountIDByUserID(ctx context.Context, lockStrength LockingStrength, userID string) (string, error)
GetAccountIDBySetupKey(ctx context.Context, peerKey string) (string, error)
diff --git a/management/server/testdata/extended-store.sql b/management/server/testdata/extended-store.sql
index 2859e82c8..7900dabf5 100644
--- a/management/server/testdata/extended-store.sql
+++ b/management/server/testdata/extended-store.sql
@@ -25,7 +25,7 @@ CREATE INDEX `idx_routes_account_id` ON `routes`(`account_id`);
CREATE INDEX `idx_name_server_groups_account_id` ON `name_server_groups`(`account_id`);
CREATE INDEX `idx_posture_checks_account_id` ON `posture_checks`(`account_id`);
-INSERT INTO accounts VALUES('bf1c8084-ba50-4ce7-9439-34653001fc3b','','2024-10-02 16:01:38.210014+02:00','test.com','private',1,'af1c8024-ha40-4ce2-9418-34653101fc3c','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',0,86400000000000,0,0,0,'',NULL,NULL,NULL);
+INSERT INTO accounts VALUES('bf1c8084-ba50-4ce7-9439-34653001fc3b','edafee4e-63fb-11ec-90d6-0242ac120003','2024-10-02 16:01:38.210000+02:00','test.com','private',1,'af1c8024-ha40-4ce2-9418-34653101fc3c','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',0,86400000000000,0,0,0,'',NULL,NULL,NULL);
INSERT INTO setup_keys VALUES('A2C8E62B-38F5-4553-B31E-DD66C696CEBB','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBB','Default key','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,NULL,'["cfefqs706sqkneg59g2g"]',0,0);
INSERT INTO setup_keys VALUES('A2C8E62B-38F5-4553-B31E-DD66C696CEBC','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBC','Faulty key with non existing group','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,NULL,'["abcd"]',0,0);
INSERT INTO users VALUES('edafee4e-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','admin',0,0,'','["cfefqs706sqkneg59g3g"]',0,NULL,'2024-10-02 16:01:38.210678+02:00','api',0,'');
diff --git a/management/server/testutil/store.go b/management/server/testutil/store.go
index a7381da12..7f6a824a4 100644
--- a/management/server/testutil/store.go
+++ b/management/server/testutil/store.go
@@ -11,6 +11,7 @@ import (
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/mysql"
"github.com/testcontainers/testcontainers-go/modules/postgres"
+ testcontainersredis "github.com/testcontainers/testcontainers-go/modules/redis"
"github.com/testcontainers/testcontainers-go/wait"
)
@@ -108,3 +109,28 @@ func CreatePostgresTestContainer() (func(), string, error) {
func noOpCleanup() {
// no-op
}
+
+// CreateRedisTestContainer creates a new Redis container for testing.
+func CreateRedisTestContainer() (func(), string, error) {
+ ctx := context.Background()
+
+ redisContainer, err := testcontainersredis.RunContainer(ctx, testcontainers.WithImage("redis:7"))
+ if err != nil {
+ return nil, "", err
+ }
+
+ cleanup := func() {
+ timeoutCtx, cancelFunc := context.WithTimeout(ctx, 1*time.Second)
+ defer cancelFunc()
+ if err = redisContainer.Terminate(timeoutCtx); err != nil {
+ log.WithContext(ctx).Warnf("failed to stop redis container %s: %s", redisContainer.GetContainerID(), err)
+ }
+ }
+
+ redisURL, err := redisContainer.ConnectionString(ctx)
+ if err != nil {
+ return nil, "", err
+ }
+
+ return cleanup, redisURL, nil
+}
diff --git a/management/server/testutil/store_ios.go b/management/server/testutil/store_ios.go
index 35de3992d..c3dd839d3 100644
--- a/management/server/testutil/store_ios.go
+++ b/management/server/testutil/store_ios.go
@@ -14,3 +14,9 @@ func CreateMysqlTestContainer() (func(), string, error) {
// Empty function for MySQL
}, "", nil
}
+
+func CreateRedisTestContainer() (func(), string, error) {
+ return func() {
+ // Empty function for Redis
+ }, "", nil
+}
diff --git a/management/server/types/account.go b/management/server/types/account.go
index 687709991..8315f5796 100644
--- a/management/server/types/account.go
+++ b/management/server/types/account.go
@@ -40,6 +40,17 @@ const (
type LookupMap map[string]struct{}
+// AccountMeta is a struct that contains a stripped down version of the Account object.
+// It doesn't carry any peers, groups, policies, or routes, etc. Just some metadata (e.g. ID, created by, created at, etc).
+type AccountMeta struct {
+ // AccountId is the unique identifier of the account
+ AccountID string `gorm:"column:id"`
+ CreatedAt time.Time
+ CreatedBy string
+ Domain string
+ DomainCategory string
+}
+
// Account represents a unique account of the system
type Account struct {
// we have to name column to aid as it collides with Network.Id when work with associations
@@ -855,6 +866,16 @@ func (a *Account) Copy() *Account {
}
}
+func (a *Account) GetMeta() *AccountMeta {
+ return &AccountMeta{
+ AccountID: a.Id,
+ CreatedBy: a.CreatedBy,
+ CreatedAt: a.CreatedAt,
+ Domain: a.Domain,
+ DomainCategory: a.DomainCategory,
+ }
+}
+
func (a *Account) GetGroupAll() (*Group, error) {
for _, g := range a.Groups {
if g.Name == "All" {
@@ -1219,6 +1240,7 @@ func getDefaultPermit(route *route.Route) []*RouteFirewallRule {
Protocol: string(PolicyRuleProtocolALL),
Domains: route.Domains,
IsDynamic: route.IsDynamic(),
+ RouteID: route.ID,
}
rules = append(rules, &rule)
@@ -1267,7 +1289,7 @@ func (a *Account) GetPeerNetworkResourceFirewallRules(ctx context.Context, peer
if route.Peer != peer.Key {
continue
}
- resourceAppliedPolicies := resourcePolicies[route.GetResourceID()]
+ resourceAppliedPolicies := resourcePolicies[string(route.GetResourceID())]
distributionPeers := getPoliciesSourcePeers(resourceAppliedPolicies, a.Groups)
rules := a.getRouteFirewallRules(ctx, peer.ID, resourceAppliedPolicies, route, validatedPeersMap, distributionPeers)
diff --git a/management/server/types/firewall_rule.go b/management/server/types/firewall_rule.go
index d98a56871..ef54abea2 100644
--- a/management/server/types/firewall_rule.go
+++ b/management/server/types/firewall_rule.go
@@ -62,6 +62,7 @@ func generateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule
baseRule := RouteFirewallRule{
PolicyID: rule.PolicyID,
+ RouteID: route.ID,
SourceRanges: sourceRanges,
Action: string(rule.Action),
Destination: route.Network.String(),
diff --git a/management/server/types/route_firewall_rule.go b/management/server/types/route_firewall_rule.go
index 5b752bc36..c09c64a3d 100644
--- a/management/server/types/route_firewall_rule.go
+++ b/management/server/types/route_firewall_rule.go
@@ -2,6 +2,7 @@ package types
import (
"github.com/netbirdio/netbird/management/domain"
+ "github.com/netbirdio/netbird/route"
)
// RouteFirewallRule a firewall rule applicable for a routed network.
@@ -9,6 +10,9 @@ type RouteFirewallRule struct {
// PolicyID is the ID of the policy this rule is derived from
PolicyID string
+ // RouteID is the ID of the route this rule belongs to.
+ RouteID route.ID
+
// SourceRanges IP ranges of the routing peers.
SourceRanges []string
diff --git a/management/server/types/settings.go b/management/server/types/settings.go
index 7054ede8c..c8de2a98c 100644
--- a/management/server/types/settings.go
+++ b/management/server/types/settings.go
@@ -39,6 +39,9 @@ type Settings struct {
// RoutingPeerDNSResolutionEnabled enabled the DNS resolution on the routing peers
RoutingPeerDNSResolutionEnabled bool
+ // DNSDomain is the custom domain for that account
+ DNSDomain string
+
// Extra is a dictionary of Account settings
Extra *ExtraSettings `gorm:"embedded;embeddedPrefix:extra_"`
}
@@ -58,6 +61,7 @@ func (s *Settings) Copy() *Settings {
PeerInactivityExpiration: s.PeerInactivityExpiration,
RoutingPeerDNSResolutionEnabled: s.RoutingPeerDNSResolutionEnabled,
+ DNSDomain: s.DNSDomain,
}
if s.Extra != nil {
settings.Extra = s.Extra.Copy()
diff --git a/management/server/types/user.go b/management/server/types/user.go
index 5f7a4f2cb..a2596b3cb 100644
--- a/management/server/types/user.go
+++ b/management/server/types/user.go
@@ -15,6 +15,8 @@ const (
UserRoleUser UserRole = "user"
UserRoleUnknown UserRole = "unknown"
UserRoleBillingAdmin UserRole = "billing_admin"
+ UserRoleAuditor UserRole = "auditor"
+ UserRoleNetworkAdmin UserRole = "network_admin"
UserStatusActive UserStatus = "active"
UserStatusDisabled UserStatus = "disabled"
@@ -35,6 +37,10 @@ func StrRoleToUserRole(strRole string) UserRole {
return UserRoleUser
case "billing_admin":
return UserRoleBillingAdmin
+ case "auditor":
+ return UserRoleAuditor
+ case "network_admin":
+ return UserRoleNetworkAdmin
default:
return UserRoleUnknown
}
diff --git a/management/server/user.go b/management/server/user.go
index 3dee3f014..b46ed24cf 100644
--- a/management/server/user.go
+++ b/management/server/user.go
@@ -27,7 +27,7 @@ func (am *DefaultAccountManager) createServiceUser(ctx context.Context, accountI
unlock := am.Store.AcquireWriteLockByUID(ctx, accountID)
defer unlock()
- allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, initiatorUserID, modules.Users, operations.Write)
+ allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, initiatorUserID, modules.Users, operations.Create)
if err != nil {
return nil, status.NewPermissionValidationError(err)
}
@@ -85,7 +85,7 @@ func (am *DefaultAccountManager) inviteNewUser(ctx context.Context, accountID, u
return nil, err
}
- allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Users, operations.Write)
+ allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Users, operations.Create)
if err != nil {
return nil, status.NewPermissionValidationError(err)
}
@@ -238,7 +238,7 @@ func (am *DefaultAccountManager) DeleteUser(ctx context.Context, accountID, init
return err
}
- allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, initiatorUserID, modules.Users, operations.Write)
+ allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, initiatorUserID, modules.Users, operations.Delete)
if err != nil {
return status.NewPermissionValidationError(err)
}
@@ -295,7 +295,7 @@ func (am *DefaultAccountManager) InviteUser(ctx context.Context, accountID strin
return status.Errorf(status.PreconditionFailed, "IdP manager must be enabled to send user invites")
}
- allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, initiatorUserID, modules.Users, operations.Write)
+ allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, initiatorUserID, modules.Users, operations.Create)
if err != nil {
return status.NewPermissionValidationError(err)
}
@@ -342,7 +342,7 @@ func (am *DefaultAccountManager) CreatePAT(ctx context.Context, accountID string
return nil, status.Errorf(status.InvalidArgument, "expiration has to be between 1 and 365")
}
- allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, initiatorUserID, modules.Pats, operations.Write)
+ allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, initiatorUserID, modules.Pats, operations.Create)
if err != nil {
return nil, status.NewPermissionValidationError(err)
}
@@ -384,7 +384,7 @@ func (am *DefaultAccountManager) DeletePAT(ctx context.Context, accountID string
unlock := am.Store.AcquireWriteLockByUID(ctx, accountID)
defer unlock()
- allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, initiatorUserID, modules.Pats, operations.Write)
+ allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, initiatorUserID, modules.Pats, operations.Delete)
if err != nil {
return status.NewPermissionValidationError(err)
}
@@ -506,7 +506,7 @@ func (am *DefaultAccountManager) SaveOrAddUsers(ctx context.Context, accountID,
return nil, nil //nolint:nilnil
}
- allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, initiatorUserID, modules.Users, operations.Write)
+ allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, initiatorUserID, modules.Users, operations.Create) // TODO: split by Create and Update
if err != nil {
return nil, status.NewPermissionValidationError(err)
}
@@ -824,32 +824,33 @@ func (am *DefaultAccountManager) GetUsersFromAccount(ctx context.Context, accoun
if err != nil {
return nil, status.NewPermissionValidationError(err)
}
+
user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID)
if err != nil {
return nil, fmt.Errorf("failed to get user: %w", err)
}
- accountUsers := []*types.User{user}
- if allowed {
+ accountUsers := []*types.User{}
+ switch {
+ case allowed:
accountUsers, err = am.Store.GetAccountUsers(ctx, store.LockingStrengthShare, accountID)
if err != nil {
return nil, err
}
+ case user.AccountID == accountID:
+ accountUsers = append(accountUsers, user)
+ default:
+ return map[string]*types.UserInfo{}, nil
}
return am.BuildUserInfosForAccount(ctx, accountID, initiatorUserID, accountUsers)
}
// BuildUserInfosForAccount builds user info for the given account.
-func (am *DefaultAccountManager) BuildUserInfosForAccount(ctx context.Context, accountID, initiatorUserID string, accountUsers []*types.User) (map[string]*types.UserInfo, error) {
+func (am *DefaultAccountManager) BuildUserInfosForAccount(ctx context.Context, accountID, _ string, accountUsers []*types.User) (map[string]*types.UserInfo, error) {
var queriedUsers []*idp.UserData
var err error
- initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID)
- if err != nil {
- return nil, err
- }
-
if !isNil(am.idpManager) {
users := make(map[string]userLoggedInOnce, len(accountUsers))
usersFromIntegration := make([]*idp.UserData, 0)
@@ -888,11 +889,6 @@ func (am *DefaultAccountManager) BuildUserInfosForAccount(ctx context.Context, a
// in case of self-hosted, or IDP doesn't return anything, we will return the locally stored userInfo
if len(queriedUsers) == 0 {
for _, accountUser := range accountUsers {
- if initiatorUser.IsRegularUser() && initiatorUser.Id != accountUser.Id {
- // if user is not an admin then show only current user and do not show other users
- continue
- }
-
info, err := accountUser.ToUserInfo(nil, settings)
if err != nil {
return nil, err
@@ -904,11 +900,6 @@ func (am *DefaultAccountManager) BuildUserInfosForAccount(ctx context.Context, a
}
for _, localUser := range accountUsers {
- if initiatorUser.IsRegularUser() && initiatorUser.Id != localUser.Id {
- // if user is not an admin then show only current user and do not show other users
- continue
- }
-
var info *types.UserInfo
if queriedUser, contains := findUserInIDPUserdata(localUser.Id, queriedUsers); contains {
info, err = localUser.ToUserInfo(queriedUser, settings)
@@ -949,6 +940,12 @@ func (am *DefaultAccountManager) BuildUserInfosForAccount(ctx context.Context, a
// expireAndUpdatePeers expires all peers of the given user and updates them in the account
func (am *DefaultAccountManager) expireAndUpdatePeers(ctx context.Context, accountID string, peers []*nbpeer.Peer) error {
+ settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
+ if err != nil {
+ return err
+ }
+ dnsDomain := am.GetDNSDomain(settings)
+
var peerIDs []string
for _, peer := range peers {
// nolint:staticcheck
@@ -966,7 +963,7 @@ func (am *DefaultAccountManager) expireAndUpdatePeers(ctx context.Context, accou
am.StoreEvent(
ctx,
peer.UserID, peer.ID, accountID,
- activity.PeerLoginExpired, peer.EventMeta(am.GetDNSDomain()),
+ activity.PeerLoginExpired, peer.EventMeta(dnsDomain),
)
}
@@ -1005,7 +1002,7 @@ func (am *DefaultAccountManager) deleteUserFromIDP(ctx context.Context, targetUs
// If an error occurs while deleting the user, the function skips it and continues deleting other users.
// Errors are collected and returned at the end.
func (am *DefaultAccountManager) DeleteRegularUsers(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string, userInfos map[string]*types.UserInfo) error {
- allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, initiatorUserID, modules.Users, operations.Write)
+ allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, initiatorUserID, modules.Users, operations.Delete)
if err != nil {
return status.NewPermissionValidationError(err)
}
@@ -1241,3 +1238,30 @@ func validateUserInvite(invite *types.UserInfo) error {
return nil
}
+
+// GetCurrentUserInfo retrieves the account's current user info
+func (am *DefaultAccountManager) GetCurrentUserInfo(ctx context.Context, accountID, userID string) (*types.UserInfo, error) {
+ user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID)
+ if err != nil {
+ return nil, err
+ }
+
+ if user.IsBlocked() {
+ return nil, status.NewUserBlockedError()
+ }
+
+ if user.IsServiceUser {
+ return nil, status.NewPermissionDeniedError()
+ }
+
+ if err := am.permissionsManager.ValidateAccountAccess(ctx, accountID, user, false); err != nil {
+ return nil, err
+ }
+
+ userInfo, err := am.getUserInfo(ctx, user, accountID)
+ if err != nil {
+ return nil, err
+ }
+
+ return userInfo, nil
+}
diff --git a/management/server/user_test.go b/management/server/user_test.go
index c5da4ec88..83c5ac49a 100644
--- a/management/server/user_test.go
+++ b/management/server/user_test.go
@@ -13,6 +13,7 @@ import (
nbcache "github.com/netbirdio/netbird/management/server/cache"
nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/permissions"
+ "github.com/netbirdio/netbird/management/server/status"
"github.com/netbirdio/netbird/management/server/util"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
@@ -1419,7 +1420,7 @@ func TestUserAccountPeersUpdate(t *testing.T) {
ID: "groupA",
Name: "GroupA",
Peers: []string{peer1.ID, peer2.ID, peer3.ID},
- })
+ }, true)
require.NoError(t, err)
policy := &types.Policy{
@@ -1434,7 +1435,7 @@ func TestUserAccountPeersUpdate(t *testing.T) {
},
},
}
- _, err = manager.SavePolicy(context.Background(), account.Id, userID, policy)
+ _, err = manager.SavePolicy(context.Background(), account.Id, userID, policy, true)
require.NoError(t, err)
updMsg := manager.peersUpdateManager.CreateChannel(context.Background(), peer1.ID)
@@ -1607,3 +1608,175 @@ func TestSaveOrAddUser_PreventAccountSwitch(t *testing.T) {
assert.Equal(t, account1.Users[targetId].AccountID, user.AccountID)
assert.Equal(t, account1.Users[targetId].AutoGroups, user.AutoGroups)
}
+
+func TestDefaultAccountManager_GetCurrentUserInfo(t *testing.T) {
+ store, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir())
+ if err != nil {
+ t.Fatalf("Error when creating store: %s", err)
+ }
+ t.Cleanup(cleanup)
+
+ account1 := newAccountWithId(context.Background(), "account1", "account1Owner", "")
+ account1.Settings.RegularUsersViewBlocked = false
+ account1.Users["blocked-user"] = &types.User{
+ Id: "blocked-user",
+ AccountID: account1.Id,
+ Blocked: true,
+ }
+ account1.Users["service-user"] = &types.User{
+ Id: "service-user",
+ IsServiceUser: true,
+ ServiceUserName: "service-user",
+ }
+ account1.Users["regular-user"] = &types.User{
+ Id: "regular-user",
+ Role: types.UserRoleUser,
+ }
+ account1.Users["admin-user"] = &types.User{
+ Id: "admin-user",
+ Role: types.UserRoleAdmin,
+ }
+ require.NoError(t, store.SaveAccount(context.Background(), account1))
+
+ account2 := newAccountWithId(context.Background(), "account2", "account2Owner", "")
+ account2.Users["settings-blocked-user"] = &types.User{
+ Id: "settings-blocked-user",
+ Role: types.UserRoleUser,
+ }
+ require.NoError(t, store.SaveAccount(context.Background(), account2))
+
+ permissionsManager := permissions.NewManager(store)
+ am := DefaultAccountManager{
+ Store: store,
+ eventStore: &activity.InMemoryEventStore{},
+ permissionsManager: permissionsManager,
+ }
+
+ tt := []struct {
+ name string
+ accountId string
+ userId string
+ expectedErr error
+ expectedResult *types.UserInfo
+ }{
+ {
+ name: "not found",
+ accountId: account1.Id,
+ userId: "not-found",
+ expectedErr: status.NewUserNotFoundError("not-found"),
+ },
+ {
+ name: "not part of account",
+ accountId: account1.Id,
+ userId: "account2Owner",
+ expectedErr: status.NewUserNotPartOfAccountError(),
+ },
+ {
+ name: "blocked",
+ accountId: account1.Id,
+ userId: "blocked-user",
+ expectedErr: status.NewUserBlockedError(),
+ },
+ {
+ name: "service user",
+ accountId: account1.Id,
+ userId: "service-user",
+ expectedErr: status.NewPermissionDeniedError(),
+ },
+ {
+ name: "owner user",
+ accountId: account1.Id,
+ userId: "account1Owner",
+ expectedResult: &types.UserInfo{
+ ID: "account1Owner",
+ Name: "",
+ Role: "owner",
+ AutoGroups: []string{},
+ Status: "active",
+ IsServiceUser: false,
+ IsBlocked: false,
+ NonDeletable: false,
+ LastLogin: time.Time{},
+ Issued: "api",
+ IntegrationReference: integration_reference.IntegrationReference{},
+ Permissions: types.UserPermissions{
+ DashboardView: "full",
+ },
+ },
+ },
+ {
+ name: "regular user",
+ accountId: account1.Id,
+ userId: "regular-user",
+ expectedResult: &types.UserInfo{
+ ID: "regular-user",
+ Name: "",
+ Role: "user",
+ Status: "active",
+ IsServiceUser: false,
+ IsBlocked: false,
+ NonDeletable: false,
+ LastLogin: time.Time{},
+ Issued: "api",
+ IntegrationReference: integration_reference.IntegrationReference{},
+ Permissions: types.UserPermissions{
+ DashboardView: "limited",
+ },
+ },
+ },
+ {
+ name: "admin user",
+ accountId: account1.Id,
+ userId: "admin-user",
+ expectedResult: &types.UserInfo{
+ ID: "admin-user",
+ Name: "",
+ Role: "admin",
+ Status: "active",
+ IsServiceUser: false,
+ IsBlocked: false,
+ NonDeletable: false,
+ LastLogin: time.Time{},
+ Issued: "api",
+ IntegrationReference: integration_reference.IntegrationReference{},
+ Permissions: types.UserPermissions{
+ DashboardView: "full",
+ },
+ },
+ },
+ {
+ name: "settings blocked regular user",
+ accountId: account2.Id,
+ userId: "settings-blocked-user",
+ expectedResult: &types.UserInfo{
+ ID: "settings-blocked-user",
+ Name: "",
+ Role: "user",
+ Status: "active",
+ IsServiceUser: false,
+ IsBlocked: false,
+ NonDeletable: false,
+ LastLogin: time.Time{},
+ Issued: "api",
+ IntegrationReference: integration_reference.IntegrationReference{},
+ Permissions: types.UserPermissions{
+ DashboardView: "blocked",
+ },
+ },
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.name, func(t *testing.T) {
+ result, err := am.GetCurrentUserInfo(context.Background(), tc.accountId, tc.userId)
+
+ if tc.expectedErr != nil {
+ assert.Equal(t, err, tc.expectedErr)
+ return
+ }
+
+ require.NoError(t, err)
+ assert.EqualValues(t, tc.expectedResult, result)
+ })
+ }
+}
diff --git a/release_files/install.sh b/release_files/install.sh
index 459645c58..49e313f2f 100755
--- a/release_files/install.sh
+++ b/release_files/install.sh
@@ -109,6 +109,9 @@ add_apt_repo() {
curl -sSL https://pkgs.netbird.io/debian/public.key \
| ${SUDO} gpg --dearmor -o /usr/share/keyrings/netbird-archive-keyring.gpg
+ # Explicitly set the file permission
+ ${SUDO} chmod 0644 /usr/share/keyrings/netbird-archive-keyring.gpg
+
echo 'deb [signed-by=/usr/share/keyrings/netbird-archive-keyring.gpg] https://pkgs.netbird.io/debian stable main' \
| ${SUDO} tee /etc/apt/sources.list.d/netbird.list
@@ -196,6 +199,21 @@ install_native_binaries() {
fi
}
+# Handle macOS .pkg installer
+install_pkg() {
+ case "$(uname -m)" in
+ x86_64) ARCH="amd64" ;;
+ arm64|aarch64) ARCH="arm64" ;;
+ *) echo "Unsupported macOS arch: $(uname -m)" >&2; exit 1 ;;
+ esac
+
+ PKG_URL=$(curl -sIL -o /dev/null -w '%{url_effective}' "https://pkgs.netbird.io/macos/${ARCH}")
+ echo "Downloading NetBird macOS installer from https://pkgs.netbird.io/macos/${ARCH}"
+ curl -fsSL -o /tmp/netbird.pkg "${PKG_URL}"
+ ${SUDO} installer -pkg /tmp/netbird.pkg -target /
+ rm -f /tmp/netbird.pkg
+}
+
check_use_bin_variable() {
if [ "${USE_BIN_INSTALL}-x" = "true-x" ]; then
echo "The installation will be performed using binary files"
@@ -262,6 +280,16 @@ install_netbird() {
${SUDO} pacman -Syy
add_aur_repo
;;
+ pkg)
+ # Check if the package is already installed
+ if [ -f /Library/Receipts/netbird.pkg ]; then
+ echo "NetBird is already installed. Please remove it before proceeding."
+ exit 1
+ fi
+
+ # Install the package
+ install_pkg
+ ;;
brew)
# Remove Netbird if it had been installed using Homebrew before
if brew ls --versions netbird >/dev/null 2>&1; then
@@ -271,7 +299,7 @@ install_netbird() {
netbird service stop
netbird service uninstall
- # Unlik the app
+ # Unlink the app
brew unlink netbird
fi
@@ -309,7 +337,7 @@ install_netbird() {
echo "package_manager=$PACKAGE_MANAGER" | ${SUDO} tee "$CONFIG_FILE" > /dev/null
# Load and start netbird service
- if [ "$PACKAGE_MANAGER" != "rpm-ostree" ]; then
+ if [ "$PACKAGE_MANAGER" != "rpm-ostree" ] && [ "$PACKAGE_MANAGER" != "pkg" ]; then
if ! ${SUDO} netbird service install 2>&1; then
echo "NetBird service has already been loaded"
fi
@@ -448,9 +476,8 @@ if type uname >/dev/null 2>&1; then
# Check the availability of a compatible package manager
if check_use_bin_variable; then
PACKAGE_MANAGER="bin"
- elif [ -x "$(command -v brew)" ]; then
- PACKAGE_MANAGER="brew"
- echo "The installation will be performed using brew package manager"
+ else
+ PACKAGE_MANAGER="pkg"
fi
;;
esac
@@ -468,4 +495,4 @@ case "$UPDATE_FLAG" in
;;
*)
install_netbird
-esac
+esac
\ No newline at end of file
diff --git a/route/hauniqueid.go b/route/hauniqueid.go
index 4d952beba..064608171 100644
--- a/route/hauniqueid.go
+++ b/route/hauniqueid.go
@@ -4,13 +4,14 @@ import "strings"
const haSeparator = "|"
+// HAUniqueID is a unique identifier that is used to group high availability routes.
type HAUniqueID string
func (id HAUniqueID) String() string {
return string(id)
}
-// NetID returns the Network ID from the HAUniqueID
+// NetID returns the NetID from the HAUniqueID
func (id HAUniqueID) NetID() NetID {
if i := strings.LastIndex(string(id), haSeparator); i != -1 {
return NetID(id[:i])
diff --git a/route/route.go b/route/route.go
index f7bf3ea87..722dacc2d 100644
--- a/route/route.go
+++ b/route/route.go
@@ -6,8 +6,6 @@ import (
"slices"
"strings"
- log "github.com/sirupsen/logrus"
-
"github.com/netbirdio/netbird/management/domain"
"github.com/netbirdio/netbird/management/server/status"
)
@@ -46,10 +44,16 @@ const (
DomainNetwork
)
+// ID is the unique route ID.
type ID string
+// ResID is the resourceID part of a route.ID (first part before the colon).
+type ResID string
+
+// NetID is the route network identifier, a human-readable string.
type NetID string
+// HAMap is a map of HAUniqueID to a list of routes.
type HAMap map[HAUniqueID][]*Route
// NetworkType route network type
@@ -162,21 +166,25 @@ func (r *Route) IsDynamic() bool {
return r.NetworkType == DomainNetwork
}
+// GetHAUniqueID returns the HAUniqueID for the route, it can be used for grouping.
func (r *Route) GetHAUniqueID() HAUniqueID {
- if r.IsDynamic() {
- domains, err := r.Domains.String()
- if err != nil {
- log.Errorf("Failed to convert domains to string: %v", err)
- domains = r.Domains.PunycodeString()
- }
- return HAUniqueID(fmt.Sprintf("%s%s%s", r.NetID, haSeparator, domains))
- }
- return HAUniqueID(fmt.Sprintf("%s%s%s", r.NetID, haSeparator, r.Network.String()))
+ return HAUniqueID(fmt.Sprintf("%s%s%s", r.NetID, haSeparator, r.NetString()))
}
-// GetResourceID returns the Networks Resource ID from a route ID
-func (r *Route) GetResourceID() string {
- return strings.Split(string(r.ID), ":")[0]
+// GetResourceID returns the Networks ResID from the route ID.
+// It's the part before the first colon in the ID string.
+func (r *Route) GetResourceID() ResID {
+ return ResID(strings.Split(string(r.ID), ":")[0])
+}
+
+// NetString returns the network string.
+// If the route is dynamic, it returns the domains as comma-separated punycode-encoded string.
+// If the route is not dynamic, it returns the network (prefix) string.
+func (r *Route) NetString() string {
+ if r.IsDynamic() {
+ return r.Domains.SafeString()
+ }
+ return r.Network.String()
}
// ParseNetwork Parses a network prefix string and returns a netip.Prefix object and if is invalid, IPv4 or IPv6
diff --git a/upload-server/Dockerfile b/upload-server/Dockerfile
new file mode 100644
index 000000000..a38c6fbb8
--- /dev/null
+++ b/upload-server/Dockerfile
@@ -0,0 +1,3 @@
+FROM gcr.io/distroless/base:debug
+ENTRYPOINT [ "/go/bin/netbird-upload" ]
+COPY netbird-upload /go/bin/netbird-upload
diff --git a/upload-server/main.go b/upload-server/main.go
new file mode 100644
index 000000000..dcfb35cdf
--- /dev/null
+++ b/upload-server/main.go
@@ -0,0 +1,22 @@
+package main
+
+import (
+ "errors"
+ "log"
+ "net/http"
+
+ "github.com/netbirdio/netbird/upload-server/server"
+ "github.com/netbirdio/netbird/util"
+)
+
+func main() {
+ err := util.InitLog("info", "console")
+ if err != nil {
+ log.Fatalf("Failed to initialize logger: %v", err)
+ }
+
+ srv := server.NewServer()
+ if err = srv.Start(); err != nil && !errors.Is(err, http.ErrServerClosed) {
+ log.Fatalf("Failed to start server: %v", err)
+ }
+}
diff --git a/upload-server/server/local.go b/upload-server/server/local.go
new file mode 100644
index 000000000..f12c472d2
--- /dev/null
+++ b/upload-server/server/local.go
@@ -0,0 +1,124 @@
+package server
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "os"
+ "path/filepath"
+
+ log "github.com/sirupsen/logrus"
+
+ "github.com/netbirdio/netbird/upload-server/types"
+)
+
+const (
+ defaultDir = "/var/lib/netbird"
+ putHandler = "/{dir}/{file}"
+)
+
+type local struct {
+ url string
+ dir string
+}
+
+func configureLocalHandlers(mux *http.ServeMux) error {
+ envURL, ok := os.LookupEnv("SERVER_URL")
+ if !ok {
+ return fmt.Errorf("SERVER_URL environment variable is required")
+ }
+ _, err := url.Parse(envURL)
+ if err != nil {
+ return fmt.Errorf("SERVER_URL environment variable is invalid: %w", err)
+ }
+
+ dir := defaultDir
+ envDir, ok := os.LookupEnv("STORE_DIR")
+ if ok {
+ if !filepath.IsAbs(envDir) {
+ return fmt.Errorf("STORE_DIR environment variable should point to an absolute path, e.g. /tmp")
+ }
+ log.Infof("Using local directory: %s", envDir)
+ dir = envDir
+ }
+
+ l := &local{
+ url: envURL,
+ dir: dir,
+ }
+ mux.HandleFunc(types.GetURLPath, l.handlerGetUploadURL)
+ mux.HandleFunc(putURLPath+putHandler, l.handlePutRequest)
+
+ return nil
+}
+
+func (l *local) handlerGetUploadURL(w http.ResponseWriter, r *http.Request) {
+ if !isValidRequest(w, r) {
+ return
+ }
+
+ objectKey := getObjectKey(w, r)
+ if objectKey == "" {
+ return
+ }
+
+ uploadURL, err := l.getUploadURL(objectKey)
+ if err != nil {
+ http.Error(w, "failed to get upload URL", http.StatusInternalServerError)
+ log.Errorf("Failed to get upload URL: %v", err)
+ return
+ }
+
+ respondGetRequest(w, uploadURL, objectKey)
+}
+
+func (l *local) getUploadURL(objectKey string) (string, error) {
+ parsedUploadURL, err := url.Parse(l.url)
+ if err != nil {
+ return "", fmt.Errorf("failed to parse upload URL: %w", err)
+ }
+ newURL := parsedUploadURL.JoinPath(parsedUploadURL.Path, putURLPath, objectKey)
+ return newURL.String(), nil
+}
+
+func (l *local) handlePutRequest(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPut {
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ body, err := io.ReadAll(r.Body)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("failed to read body: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ uploadDir := r.PathValue("dir")
+ if uploadDir == "" {
+ http.Error(w, "missing dir path", http.StatusBadRequest)
+ return
+ }
+ uploadFile := r.PathValue("file")
+ if uploadFile == "" {
+ http.Error(w, "missing file name", http.StatusBadRequest)
+ return
+ }
+
+ dirPath := filepath.Join(l.dir, uploadDir)
+ err = os.MkdirAll(dirPath, 0750)
+ if err != nil {
+ http.Error(w, "failed to create upload dir", http.StatusInternalServerError)
+ log.Errorf("Failed to create upload dir: %v", err)
+ return
+ }
+
+ file := filepath.Join(dirPath, uploadFile)
+ if err := os.WriteFile(file, body, 0600); err != nil {
+ http.Error(w, "failed to write file", http.StatusInternalServerError)
+ log.Errorf("Failed to write file %s: %v", file, err)
+ return
+ }
+ log.Infof("Uploading file %s", file)
+ w.WriteHeader(http.StatusOK)
+}
diff --git a/upload-server/server/local_test.go b/upload-server/server/local_test.go
new file mode 100644
index 000000000..bd8a87809
--- /dev/null
+++ b/upload-server/server/local_test.go
@@ -0,0 +1,65 @@
+package server
+
+import (
+ "bytes"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/netbirdio/netbird/upload-server/types"
+)
+
+func Test_LocalHandlerGetUploadURL(t *testing.T) {
+ mockURL := "http://localhost:8080"
+ t.Setenv("SERVER_URL", mockURL)
+ t.Setenv("STORE_DIR", t.TempDir())
+
+ mux := http.NewServeMux()
+ err := configureLocalHandlers(mux)
+ require.NoError(t, err)
+
+ req := httptest.NewRequest(http.MethodGet, types.GetURLPath+"?id=test-file", nil)
+ req.Header.Set(types.ClientHeader, types.ClientHeaderValue)
+
+ rec := httptest.NewRecorder()
+ mux.ServeHTTP(rec, req)
+
+ require.Equal(t, http.StatusOK, rec.Code)
+
+ var response types.GetURLResponse
+ err = json.Unmarshal(rec.Body.Bytes(), &response)
+ require.NoError(t, err)
+ require.Contains(t, response.URL, "test-file/")
+ require.NotEmpty(t, response.Key)
+ require.Contains(t, response.Key, "test-file/")
+
+}
+
+func Test_LocalHandlePutRequest(t *testing.T) {
+ mockDir := t.TempDir()
+ mockURL := "http://localhost:8080"
+ t.Setenv("SERVER_URL", mockURL)
+ t.Setenv("STORE_DIR", mockDir)
+
+ mux := http.NewServeMux()
+ err := configureLocalHandlers(mux)
+ require.NoError(t, err)
+
+ fileContent := []byte("test file content")
+ req := httptest.NewRequest(http.MethodPut, putURLPath+"/uploads/test.txt", bytes.NewReader(fileContent))
+
+ rec := httptest.NewRecorder()
+ mux.ServeHTTP(rec, req)
+
+ require.Equal(t, http.StatusOK, rec.Code)
+
+ expectedFilePath := filepath.Join(mockDir, "uploads", "test.txt")
+ createdFileContent, err := os.ReadFile(expectedFilePath)
+ require.NoError(t, err)
+ require.Equal(t, fileContent, createdFileContent)
+}
diff --git a/upload-server/server/s3.go b/upload-server/server/s3.go
new file mode 100644
index 000000000..c0976acb5
--- /dev/null
+++ b/upload-server/server/s3.go
@@ -0,0 +1,69 @@
+package server
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "os"
+ "time"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/config"
+ "github.com/aws/aws-sdk-go-v2/service/s3"
+ log "github.com/sirupsen/logrus"
+
+ "github.com/netbirdio/netbird/upload-server/types"
+)
+
+type sThree struct {
+ ctx context.Context
+ bucket string
+ presignClient *s3.PresignClient
+}
+
+func configureS3Handlers(mux *http.ServeMux) error {
+ bucket := os.Getenv(bucketVar)
+ region, ok := os.LookupEnv("AWS_REGION")
+ if !ok {
+ return fmt.Errorf("AWS_REGION environment variable is required")
+ }
+ ctx := context.Background()
+ cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region))
+ if err != nil {
+ return fmt.Errorf("unable to load SDK config: %w", err)
+ }
+
+ client := s3.NewFromConfig(cfg)
+
+ handler := &sThree{
+ ctx: ctx,
+ bucket: bucket,
+ presignClient: s3.NewPresignClient(client),
+ }
+ mux.HandleFunc(types.GetURLPath, handler.handlerGetUploadURL)
+ return nil
+}
+
+func (s *sThree) handlerGetUploadURL(w http.ResponseWriter, r *http.Request) {
+ if !isValidRequest(w, r) {
+ return
+ }
+
+ objectKey := getObjectKey(w, r)
+ if objectKey == "" {
+ return
+ }
+
+ req, err := s.presignClient.PresignPutObject(s.ctx, &s3.PutObjectInput{
+ Bucket: aws.String(s.bucket),
+ Key: aws.String(objectKey),
+ }, s3.WithPresignExpires(15*time.Minute))
+
+ if err != nil {
+ http.Error(w, "failed to presign URL", http.StatusInternalServerError)
+ log.Errorf("Presign error: %v", err)
+ return
+ }
+
+ respondGetRequest(w, req.URL, objectKey)
+}
diff --git a/upload-server/server/s3_test.go b/upload-server/server/s3_test.go
new file mode 100644
index 000000000..26b0ecd09
--- /dev/null
+++ b/upload-server/server/s3_test.go
@@ -0,0 +1,103 @@
+package server
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "runtime"
+ "testing"
+
+ "github.com/aws/aws-sdk-go-v2/config"
+ "github.com/aws/aws-sdk-go-v2/service/s3"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/testcontainers/testcontainers-go"
+ "github.com/testcontainers/testcontainers-go/wait"
+
+ "github.com/netbirdio/netbird/upload-server/types"
+)
+
+func Test_S3HandlerGetUploadURL(t *testing.T) {
+ if runtime.GOOS != "linux" && os.Getenv("CI") == "true" {
+ t.Skip("Skipping test on non-Linux and CI environment due to docker dependency")
+ }
+ if runtime.GOOS == "windows" {
+ t.Skip("Skipping test on Windows due to potential docker dependency")
+ }
+
+ awsEndpoint := "http://127.0.0.1:4566"
+ awsRegion := "us-east-1"
+
+ ctx := context.Background()
+ containerRequest := testcontainers.ContainerRequest{
+ Image: "localstack/localstack:s3-latest",
+ ExposedPorts: []string{"4566:4566/tcp"},
+ WaitingFor: wait.ForLog("Ready"),
+ }
+
+ c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
+ ContainerRequest: containerRequest,
+ Started: true,
+ })
+ if err != nil {
+ t.Error(err)
+ }
+ defer func(c testcontainers.Container, ctx context.Context) {
+ if err := c.Terminate(ctx); err != nil {
+ t.Log(err)
+ }
+ }(c, ctx)
+
+ t.Setenv("AWS_REGION", awsRegion)
+ t.Setenv("AWS_ENDPOINT_URL", awsEndpoint)
+ t.Setenv("AWS_ACCESS_KEY_ID", "test")
+ t.Setenv("AWS_SECRET_ACCESS_KEY", "test")
+
+ cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(awsRegion), config.WithBaseEndpoint(awsEndpoint))
+ if err != nil {
+ t.Error(err)
+ }
+
+ client := s3.NewFromConfig(cfg, func(o *s3.Options) {
+ o.UsePathStyle = true
+ o.BaseEndpoint = cfg.BaseEndpoint
+ })
+
+ bucketName := "test"
+ if _, err := client.CreateBucket(ctx, &s3.CreateBucketInput{
+ Bucket: &bucketName,
+ }); err != nil {
+ t.Error(err)
+ }
+
+ list, err := client.ListBuckets(ctx, &s3.ListBucketsInput{})
+ if err != nil {
+ t.Error(err)
+ }
+
+ assert.Equal(t, len(list.Buckets), 1)
+ assert.Equal(t, *list.Buckets[0].Name, bucketName)
+
+ t.Setenv(bucketVar, bucketName)
+
+ mux := http.NewServeMux()
+ err = configureS3Handlers(mux)
+ require.NoError(t, err)
+
+ req := httptest.NewRequest(http.MethodGet, types.GetURLPath+"?id=test-file", nil)
+ req.Header.Set(types.ClientHeader, types.ClientHeaderValue)
+
+ rec := httptest.NewRecorder()
+ mux.ServeHTTP(rec, req)
+
+ require.Equal(t, http.StatusOK, rec.Code)
+
+ var response types.GetURLResponse
+ err = json.Unmarshal(rec.Body.Bytes(), &response)
+ require.NoError(t, err)
+ require.Contains(t, response.URL, "test-file/")
+ require.NotEmpty(t, response.Key)
+ require.Contains(t, response.Key, "test-file/")
+}
diff --git a/upload-server/server/server.go b/upload-server/server/server.go
new file mode 100644
index 000000000..29ef72732
--- /dev/null
+++ b/upload-server/server/server.go
@@ -0,0 +1,109 @@
+package server
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "os"
+ "time"
+
+ "github.com/google/uuid"
+ log "github.com/sirupsen/logrus"
+
+ "github.com/netbirdio/netbird/upload-server/types"
+)
+
+const (
+ putURLPath = "/upload"
+ bucketVar = "BUCKET"
+)
+
+type Server struct {
+ srv *http.Server
+}
+
+func NewServer() *Server {
+ address := os.Getenv("SERVER_ADDRESS")
+ if address == "" {
+ log.Infof("SERVER_ADDRESS environment variable was not set, using 0.0.0.0:8080")
+ address = "0.0.0.0:8080"
+ }
+ mux := http.NewServeMux()
+ err := configureMux(mux)
+ if err != nil {
+ log.Fatalf("Failed to configure server: %v", err)
+ }
+ mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ http.Error(w, "not found", http.StatusNotFound)
+ })
+
+ return &Server{
+ srv: &http.Server{Addr: address, Handler: mux},
+ }
+}
+
+func (s *Server) Start() error {
+ log.Infof("Starting upload server on %s", s.srv.Addr)
+ return s.srv.ListenAndServe()
+}
+
+func (s *Server) Stop() error {
+ if s.srv != nil {
+ log.Infof("Stopping upload server on %s", s.srv.Addr)
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ return s.srv.Shutdown(ctx)
+ }
+ return nil
+}
+
+func configureMux(mux *http.ServeMux) error {
+ _, ok := os.LookupEnv(bucketVar)
+ if ok {
+ return configureS3Handlers(mux)
+ } else {
+ return configureLocalHandlers(mux)
+ }
+}
+
+func getObjectKey(w http.ResponseWriter, r *http.Request) string {
+ id := r.URL.Query().Get("id")
+ if id == "" {
+ http.Error(w, "id query param required", http.StatusBadRequest)
+ return ""
+ }
+
+ return id + "/" + uuid.New().String()
+}
+
+func isValidRequest(w http.ResponseWriter, r *http.Request) bool {
+ if r.Method != http.MethodGet {
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ return false
+ }
+
+ if r.Header.Get(types.ClientHeader) != types.ClientHeaderValue {
+ http.Error(w, "unauthorized", http.StatusUnauthorized)
+ return false
+ }
+ return true
+}
+func respondGetRequest(w http.ResponseWriter, uploadURL string, objectKey string) {
+ response := types.GetURLResponse{
+ URL: uploadURL,
+ Key: objectKey,
+ }
+
+ rdata, err := json.Marshal(response)
+ if err != nil {
+ http.Error(w, "failed to marshal response", http.StatusInternalServerError)
+ log.Errorf("Marshal error: %v", err)
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+ _, err = w.Write(rdata)
+ if err != nil {
+ log.Errorf("Write error: %v", err)
+ }
+}
diff --git a/upload-server/types/upload.go b/upload-server/types/upload.go
new file mode 100644
index 000000000..35d003582
--- /dev/null
+++ b/upload-server/types/upload.go
@@ -0,0 +1,16 @@
+package types
+
+const (
+ // ClientHeader is the header used to identify the client
+ ClientHeader = "x-nb-client"
+ // ClientHeaderValue is the value of the ClientHeader
+ ClientHeaderValue = "netbird"
+ // GetURLPath is the path for the GetURL request
+ GetURLPath = "/upload-url"
+)
+
+// GetURLResponse is the response for the GetURL request
+type GetURLResponse struct {
+ URL string
+ Key string
+}